[유니티] 절차적 생성을 위한 섬 생성 기법

박근영·2021년 6월 8일
0

PCG

목록 보기
5/10
post-thumbnail

Perlin Noise란?

연속적인 일련의 의사 난수 값을 생성하는 알고리즘으로 유기적인 모양의 노이즈를 생성한다.


왼쪽 노이즈는 랜덤으로 생성된 노이즈로, 유기적인 연결 없이 단절된 형태를 띠기 때문에 지형 생성 노이즈로 적합하지 않다.

펄린 노이즈 파라미터

  • 시드 (seed)
  • 주파수 (frequency)
  • 옥타브 (octave)

시드 : 무작위 생성을 위한 값
주파수 : 노이즈의 간격과 관련된 값. 주파수가 클수록 노이즈가 세밀해진다.

고주파 노이즈저주파 노이즈

옥타브 : 여러 주파수의 노이즈를 중첩시킬 때 쓰는 값
옥타브가 커질수록 높은 주파수와 낮은 진폭의 노이즈가 중첩된다.

중첩되지 않은 여러 노이즈중첩된 노이즈

처음 노이즈가 전체적인 형태를 결정하고 이후 다른 노이즈들이 합쳐지면서 디테일한 노이즈가 만들어 진다. 이러한 방법을 프랙탈 노이즈라고 한다.

이러한 방법으로 생성된 노이즈를 지형 생성의 height 맵으로 사용하면 다음 사진과 같이 나타난다.

컬러 적용 전컬러 적용 후

노이즈 맵에서 값(0에서 1 사이)을 추출해 알맞은 색상 값을 적용한 모습이다. 그러나 섬을 생성하기 위해서는 외곽이 바다로 둘러 쌓인 모습이 되어야 하기 때문에 노이즈 맵과 그라디언트 맵을 합쳐주었다.

노이즈 맵그라디언트 맵노이즈 맵 + 그라디언트 맵

이 노이즈 맵에 다시 컬러 맵을 적용하면 다음과 같이 완성된 형태의 섬을 생성할 수 있다.


소스 코드

펄린 노이즈

using UnityEngine;

public class PerlinNoise : MonoBehaviour
{
    public float[,] GenerateMap(int width, int height, float scale, float octaves, float persistance, float lacunarity, float xOrg, float yOrg)
    {
        float[,] noiseMap = new float[width, height];
        scale = Mathf.Max(0.0001f, scale);
        float maxNoiseHeight = float.MinValue; //최대 값을 담기 위한 변수
        float minNoiseHeight = float.MaxValue; //최소 값을 담기 위한 변수
        for (int x = 0; x < width; x++)
        {
            for (int y = 0; y < height; y++)
            {
                float amplitude = 1; //진폭. 노이즈의 폭과 관련된 값.
                float frequency = 1; //주파수. 노이즈의 간격과 관련된 값. 주파수가 커질수록 노이즈가 세밀해짐
                float noiseHeight = 0;

                for (int i = 0; i < octaves; i++) //옥타브가 증가할수록 높은 주파수와 낮은 진폭의 노이즈가 중첩됨.
                {
                    float xCoord = xOrg + x / scale * frequency;
                    float yCoord = yOrg + y / scale * frequency;
                    float perlinValue = Mathf.PerlinNoise(xCoord, yCoord) * 2 - 1; //0~1 사이의 값을 반환하는 함수. 2를 곱하고 1을 빼서 -1~1 사이의 값으로 변환
                    noiseHeight += perlinValue * amplitude;
                    amplitude *= persistance;
                    frequency *= lacunarity;
                }
                if (noiseHeight > maxNoiseHeight) maxNoiseHeight = noiseHeight;
                else if (noiseHeight < minNoiseHeight) minNoiseHeight = noiseHeight;
                noiseMap[x, y] = noiseHeight;
            }
        }
        for (int x = 0; x < width; x++)
        {
            for (int y = 0;y < height; y++)
            {
                noiseMap[x, y] = Mathf.InverseLerp(minNoiseHeight, maxNoiseHeight, noiseMap[x, y]); //lerp의 역함수로 최솟값과 최댓값의 사잇값을 3번째 인자로 넣으면 0~1사이의 값을 반환
            }
        }
        return noiseMap;
    }
}

그라디언트 맵

using UnityEngine;

public class Gradient : MonoBehaviour
{
    [SerializeField] private Texture2D gradientTex;

    public float[,] GenerateMap(int width, int height)
    {
        float[,] gradientMap = new float[width, height];
        for (int x = 0; x < width; x++)
        {
            for (int y = 0; y < height; y++)
            {
                int xCoord = Mathf.RoundToInt(x * (float)gradientTex.width / width); //텍스처 값과 크기 값에 맞춰 좌표 저장
                int yCoord = Mathf.RoundToInt(y * (float)gradientTex.height / height);
                gradientMap[x, y] = gradientTex.GetPixel(xCoord, yCoord).grayscale; //텍스처에서 색상을 가져와 그레이 스케일로 배열에 저장
            }
        }
        return gradientMap;
    }
}

컬러 맵

using UnityEngine;

public class MapDisplay : MonoBehaviour
{
    [SerializeField] private SpriteRenderer spriteRenderer;

    [Range(0f, 1f)]
    public float[] fillPercents;
    public Color[] fillColors;

    public void DrawNoiseMap(float[,] noiseMap, float[,] gradientMap, bool useColorMap)
    {
        int width = noiseMap.GetLength(0);
        int height = noiseMap.GetLength(1);
        Texture2D noiseTex = new Texture2D(width, height);
        noiseTex.filterMode = FilterMode.Point;
        Color[] colorMap = new Color[width * height];
        for (int x = 0; x < width; x++)
        {
            for (int y = 0; y < height; y++)
            {
                colorMap[x * height + y] = CalcColor(noiseMap[x, y], gradientMap[x, y], useColorMap);
            }
        }
        noiseTex.SetPixels(colorMap);
        noiseTex.Apply();

        spriteRenderer.sprite = Sprite.Create(noiseTex, new Rect(0, 0, width, height), new Vector2(0.5f, 0.5f));
    }

    private Color CalcColor(float noiseValue, float gradientValue, bool useColorMap)
    {
        float value = noiseValue + gradientValue;
        value = Mathf.InverseLerp(0, 2, value); //노이즈 맵과 그라디언트 맵을 더한 값을 0~1사이의 값으로 변환
        Color color = Color.Lerp(Color.black, Color.white, value); //변환된 값에 해당하는 색상을 그레이스케일로 저장
        if (useColorMap)
        {
            for (int i = 0; i < fillPercents.Length; i++)
            {
                if (color.grayscale < fillPercents[i])
                {
                    color = fillColors[i]; //미리 설정한 색상 범위에 따라 색상 변환
                    break;
                }
            }
        }
        return color;
    }
}

생성 코드

using UnityEngine;

public class IslandGeneratorByPerlinNoise : MonoBehaviour
{
    public int width = 256;
    public int height = 256;
    public float scale = 1.0f;
    public int octaves = 3;
    public float persistance = 0.5f;
    public float lacunarity = 2;

    private float xOrg = 0;
    private float yOrg = 0;

    public string seed;
    public bool useRandomSeed;

    public bool useColorMap;
    public bool useGradientMap;

    [SerializeField] private PerlinNoise perlinNoise;
    [SerializeField] private Gradient gradient;
    [SerializeField] private MapDisplay mapDisplay;

    private void Update()
    {
        if (Input.GetMouseButtonDown(0))
        {
            if (useRandomSeed) seed = Time.time.ToString(); //시드
            System.Random pseudoRandom = new System.Random(seed.GetHashCode()); //의사 난수
            xOrg = pseudoRandom.Next(0, 99999); //의사 난수로 부터 랜덤 값 추출
            yOrg = pseudoRandom.Next(0, 99999);
            GenerateMap();
        }
    }

    private void GenerateMap()
    {
        float[,] noiseMap = perlinNoise.GenerateMap(width, height, scale, octaves, persistance, lacunarity, xOrg, yOrg); //노이즈 맵 생성
        float[,] gradientMap = gradient.GenerateMap(width, height); //그라디언트 맵 생성
        if (useGradientMap) mapDisplay.DrawNoiseMap(noiseMap, gradientMap, useColorMap); //노이즈 맵과 그라디언트 맵 결합
        else mapDisplay.DrawNoiseMap(noiseMap, noiseMap, useColorMap);
    }
}

참고한 사이트

https://chipmunk.tistory.com/4?category=1011704
https://m.blog.naver.com/dj3630/221512874599
https://youtu.be/MRNFcywkUSA

profile
게임 개발자를 지망하는 학생입니다. 블로그 이전했습니다. https://1217pgy.tistory.com/

0개의 댓글