본문 바로가기
유니티/절차적 생성

[유니티] 절차적 섬 생성 - Perlin Noise

by 개발자 박근영 2021. 11. 15.

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

 

동적으로 섬 생성하기 - 3 (노이즈를 사용한 지형 생성)

지난 시간에는 바이옴을 다각형으로 나눠봤습니다. 궁극적인 목표는 결국 섬을 생성하는 것이기 때문에, 이번 시간에는 지형을 생성해 볼 것입니다. 우리가 사는 지구의 지형은 불규칙적입니다

chipmunk.tistory.com

 

Perlin Noise

펄린 노이즈 - Ken Perlin 이 1980년대 초 영화 '트론' 제작 중 컴퓨터 효과를 위한 단계적 텍...

blog.naver.com

 

댓글