[C#, Unity, 절차적 생성] 절차적 지형 생성 - 2.프랙탈 합

2025. 4. 24. 19:51·Unity,C#/절차적생성(PCG)
728x90

■ 프랙탈 합(Fractal Noise)

[PerlinNoise]

하나의 PerlinNoise는 큰 흐름만 존재하고 세부 디테일이 부족하기 때문에, 복잡한 자연 지형을 표현하기엔 한계가 있다.

[프랙탈 합]

복잡한 자연 지형을 표현하기 위해 동일한 PerlinNoise를 여러 옥타브(Octave)에 걸쳐 주파수(Frequency - 빈도)는 높이고, 진폭(Amplitude-세기)은 줄이며 반복적으로 합산한 복합 노이즈인 프렉탈 합(Fractal Noise)를 사용한다.

  • 이러한 방식은 fBm(Fractal Brownian Motion)이라고도 불리며, 다양한 랜덤 패턴의 기반이 된다.
  • Fractal Noise는 자연 지형, 구름, 바위 텍스처 등의 절차적 생성에 사용된다.

 

보통 다음과 같은 파라미터로 주파수와 진폭을 제어한다.

  1. Lacunarity : 주파수 증가율.
  2. Persistance : 진폭 감소율.

 

■ 프렉탈 합 구현

전체적인 구현 순서를 정리하면 다음과 같다.

  1. 각 옥타브 마다 입력 좌표에 더해질 랜덤한 OctaveOffset벡터 생성.
  2. 입력 좌표에 주파수를 곱해 PerlinNoise를 샘플링할 좌표를 결정한다.
  3. 샘플링된 PerlinNoise값에 진폭을 곱해 누적한다.
  4. 주파수와 진폭을 조절한다.
  5. 2~4번 과정을 옥타브 수 만큼 반복한다.

1. OctaveOffset

PerlinNoise는 같은 입력 좌표에 대해 항상 같은 값을 반환하므로, 옥타브마다 동일한 위치를 샘플링하면 결국 비슷한 패턴이 중첩되어 단조롭고 반복적인 패턴이 생성될 것이다.

  • 이를 방지하기 위해, 옥타브마다 서로 다른 위치를 샘플링하도록 OctaveOffset을 더해준다.
  • 이로 인해 서로 다른 디테일 레이어들이 중첩되면서 더 자연스러운 지형이 생성된다.
public static class NoiseMapGenerator
{
    private static int _seed;
    private static float _scale;
    private static int _width;
    private static int _height;
    private static int _octave;
    private static float _lacunarity;
    private static float _persistence;
    
    public static float[,] PerlinNoise(NoiseData data)
    {
        InitNoiseData(data);
        
        if (_scale <= 0)
            _scale = 0.0001f;

        var noiseMap = new float[_height, _width];
        
        // 옥타브 오프셋 벡터 생성
        var octaveOffset = new Vector2[_octave];
        var prng = new Random(_seed);       

        for (var i = 0; i < _octave; ++i)
        {
            float xPos = prng.Next(-100000, 100000);
            float yPos = prng.Next(-100000, 100000);

            octaveOffset[i] = new Vector2(xPos, yPos);
        }
	}
}

 

OctaveOffset의 난수 범위가 -10만~10만 인 이유

PerlinNoise는 격자점마다 저장된 랜덤한 벡터를 직접 계산하지 않고, 미리 만들어진 256 길이의 테이블을 참조해 사용한다. 이 테이블은 256 단위로 입력 좌표가 반복되는 구조를 갖고 있기 때문에 입력 값이 256 단위로 차이 나면, 같은 노이즈 값이 반복된다.

Mathf.PerlinNoise(0,0);        // 약 0.4652731
Mathf.PerlinNoise(256, 256);   // 약 0.4652731
  • 따라서 256보다 충분히 큰 오프셋을 줘야 반복되는 패턴을 방지할 수 있다.
  • 범위가 작으면 PerlinNoise의 부드러운 성질로 인해 옥타브 간의 차이가 거의 없어지게 된다.

 

2. 좌표 샘플링

public static float[,] PerlinNoise(NoiseData data)
{
    var halfWidth = _width / 2f;
    var halfHeight = _height / 2f;
    var minHeight = float.MaxValue;
    var maxHeight = float.MinValue;

    // PerlinNoise 계산 시작
    for (var y = 0; y < _height; ++y)
    {
        for (var x = 0; x < _width; ++x)
        {
            // 주파수와 진폭
            var frequency = 1f;
            var amplitude = 1f;
            var noiseHeight = 0f;

            // 옥타브 수만큼 반복
            for (var i = 0; i < _octave; ++i)
            {
                // PerlinNoise의 좌표를 중앙 기준으로 정규화
                var sampleX = (x - halfWidth) / _scale * frequency + octaveOffset[i].x;
                var sampleY = (y - halfHeight) / _scale * frequency + octaveOffset[i].y;

                var perlin = Mathf.PerlinNoise(sampleX, sampleY);
                noiseHeight += perlin * amplitude;

                frequency *= _lacunarity;
                amplitude *= _persistence;
            }

            noiseMap[y, x] = noiseHeight;

            // 높이 정규화
            if (noiseHeight < minHeight) minHeight = noiseHeight;
            if (noiseHeight > maxHeight) maxHeight = noiseHeight;
        }
    }
}

PerlinNoise의 좌표 샘플링을 화면의 중앙부터 시작하도록, 입력 좌표에 너비/높이의 절반을 빼 [-width/2 ~ width/2]의 범위로 조정한다.

  • 즉, 화면의 정중앙을 PerlinNoise(0,0)으로 정렬하고, 그 주변은 음/양수 방향으로 퍼져나가며 샘플링하는 구조가 된다.
입력 좌표(x) 샘플링 좌표(sampleX)
x = 0 sampleX = (x - width / 2) = -256
x = 256 sampleX = 0
x = 512 sampleX = 256

 

PerlinNoise는 연속된 좌표 공간에서 부드럽게 변화하는 함수이기 때문에, 입력 좌표를 Scale로 나누면 변화 간격(크기)을 조절할 수 있다.

  • 나누는 값이 클수록 결과값의 변화 폭이 작아지며, 이웃 픽셀 간 차이도 작다.
    • → 무늬가 크고 부드러워짐 (확대 효과)
  • 나누는 값이 작을수록 결과값의 변화가 크고 자주 바뀐다.
    • → 무늬가 작고 복잡해짐 (축소 효과)

 

또한 샘플링 좌표에 주파수(Frequency)를 곱하는 이유는 좌표 간 간격을 인위적으로 벌려 노이즈의 “진동 속도(변화 주기)”를 빠르게 만들기 위함이다.

좌표 * 주파수 의미 결과
x * 1 기본 패턴 부드럽고 넓은 무늬
x * 2 2배 빠른 변화 촘촘한 무늬
x * 4 더 빠르게 진동 매우 세밀한 디테일

 

주파수를 높일 경우, 같은 거리를 이동해도 노이즈 값은 자주 바뀌기 때문에 더 다양하고 촘촘한 패턴이 생성된다.

 

3. 높이 계산

var perlin = Mathf.PerlinNoise(sampleX, sampleY);
noiseHeight += perlin * amplitude;

frequency *= _lacunarity;
amplitude *= _persistence;

계산된 샘플링 좌표(sampleX, sampleY)를 기반으로 PerlinNoise값을 계산하고, 여기에 진폭을 곱해 해당 옥타브의 노이즈 기여값을 누적한다.

  • 주파수엔 lacunarity(1~n의 범위)를 곱해 옥타브가 반복될수록 주파수는 증가된다.
  • 진폭엔 persistence(0.1~0.9의 범위)를 곱해 반복될수록 진폭은 감소한다.

 

4. 정규화

// 정규화 시작
for (var y = 0; y < _height; y++)
{
	for (var x = 0; x < _width; x++)
	{
		// a, b사이에 value가 어느 정도에 위치해있는지 0.0~1.0사이의 값으로 반환
		noiseMap[y, x] = Mathf.InverseLerp(minHeight, maxHeight, noiseMap[y, x]);
	}
}

PerlinNoise는 옥타브 수, 진폭, 시드 등에 따라 최솟값과 최댓값이 달라질 수 있기 때문에, 모든 좌표 값을 0~1 사이로 정규화해야 한다.

  • 나중에 텍스쳐로 변환할 때, 정확한 밝기/색상으로 표현하기 위해 정규화를 진행한다.

 

■ 텍스쳐 생성

PerlinNoise의 결과를 시각적으로 확인하기 위해, 높이에 따라 색상이 적용된 텍스처를 생성해보자.

[ColorData 스크립터블 오브젝트]

  • 높이가 0.2 미만이면 파란색(물),
  • 0.4 미만이면 노란색(모래)처럼 지형의 높이에 따라 색상을 구분하고자 할 때, 이를 위한 데이터를 ScriptableObject로 관리한다.

🛣️ 텍스쳐 생성 함수

public static class NoiseTextureGenerator
{
    private static ColorHeight[] _colorHeights;
    
    public static Texture2D GenerateNoiseTexture(float[,] noiseMap, ColorHeight[] colorHeights)
    {
        var tex = new Texture2D(noiseMap.GetLength(1), noiseMap.GetLength(0));
        _colorHeights = colorHeights;

        for (var y = 0; y < noiseMap.GetLength(0); ++y)
        {
            for (var x = 0; x < noiseMap.GetLength(1); ++x)
            {
                var color = ApplyHeightColor(noiseMap[y, x]);
                tex.SetPixel(x, y, color);
            }
        }
        
        tex.filterMode = FilterMode.Point;
        tex.wrapMode = TextureWrapMode.Clamp;
        tex.Apply();
        
        return tex;
    }
}

 

📦 ColorData 정의

using System;
using UnityEngine;

[CreateAssetMenu(fileName = "ColorData", menuName = "PCG/ColorData")]
public class ColorData : ScriptableObject
{
    public ColorHeight[] NoiseColor;
}

[Serializable]
public struct ColorHeight
{
    [Range(0f, 1f), SerializeField] private float height;
    [SerializeField] private Color color;

    public float Height => height;
    public Color Color => color;
}
  • ColorHeight는 특정 높이 이하일 때 사용할 색상을 정의한다.
  • 여러 개를 배열로 두어, 낮은 높이부터 차례대로 검사하며 색상을 적용할 수 있다.

 

🖼️ 텍스처 적용

public void GeneratePerlinNoiseMap()
{
	_noiseMap = NoiseMapGenerator.PerlinNoise(noiseData);
	
	var noiseTex = NoiseTextureGenerator.GenerateNoiseTexture(_noiseMap, colorData.NoiseColor);
		noiseRenderer.sprite = Sprite.Create(noiseTex,
		new Rect(0, 0, noiseData.Width, noiseData.Height), new Vector2(0.5f, 0.5f));
}

 

■ 결과 확인

[Noise Sacle 변화에 따른 차이(확대/축소)]
[주파수의 증가율(Lacunarity) 변화에 따른 차이]

Lacunarity를 조절하면 지형의 세밀함(복잡도)을 제어할 수 있다.

[진폭의 감소율(Persistence) 변화에 따른 차이]

Persistence가 클수록 진폭 감소가 급격해져 높은 옥타브들의 영향력이 커진다. 즉, 픽셀 간 매우 큰 높이차가 발생한다.

■ FalloffMap

[FalloffMap]

현재 생성된 지형은 외곽이 직각으로 잘린 형태라 어색하다. 이를 자연스럽게 만들기 위해, 중앙은 유지하고 외곽은 부드럽게 감쇠되는 값을 가지는 원형 FalloffMap을 적용하면 더 자연스러운 섬 형태를 만들 수 있다.

  • 중앙은 0에 가깝고, 외각으로 갈수록 1에 가까운 값을 가진다.

 

1. 구현

public class FalloffMap : MonoBehaviour
{
    [Header("Falloff Map")] 
    // 곡선의 가파름 제어
    [SerializeField, Range(1f, 5f)] private float dampingCurveWeight; 
    // 곡선의 감쇠 비율을 설정
    [SerializeField, Range(0f, 1f)] private float attenuationRatio;         
    [SerializeField] private Vector2 centerOffset;
    [SerializeField, Range(0f, 5f)] private float radius;

    public float[,] GenerateFalloffMap((int width, int height) size)
    {
        var fallOffMap = new float[size.height, size.width];
        var center = new Vector2
            ((float)size.width / 2 + centerOffset.x, 
            (float)size.height / 2 + centerOffset.y);

        for (var y = 0; y < size.height; ++y)
        {
            for (var x = 0; x < size.width; ++x)
            {
                var distance = Vector2.Distance(new Vector2(x, y), center);
                var gradient = distance / (size.width * radius);
                gradient = Mathf.Clamp01(gradient);

                fallOffMap[y, x] = Evaluate(gradient);
            }
        }

        return fallOffMap;
    }
 }

중심으로부터 각 픽셀까지의 거리를 계산한 뒤, 그 값을 width(가로 크기) * radius(감쇠 제외 범위)로 나누어 0~1 사이의 비율(Gradient)로 정규화한다.

  • 이렇게 만들어진 값은 Evaluate() 함수를 통해 곡선 형태의 감쇠로 변환된다.

 

2. S-curve 감쇠 함수

선형 변화만으로는 감쇠의 완급 조절이 어렵기 때문에, S-curve 형태의 감쇠 함수를 사용해 중심은 천천히, 외곽은 급격하게 값이 증가하도록 한다.

 

S-커브 기반 감쇠 함수

 

www.desmos.com

$$ f(x) = \frac{x^a}{x^a + (b-b\cdot x)^a} $$

  • $x$ = 입력으로 들어온 값.
  • $a =$ dampingCurveWeight(곡선의 가파름을 제어한다.)
  • $b =$ attenuationRation(감쇠의 강도를 조절한다.)
private float Evaluate(float val)
{
	var numerator = Mathf.Pow(val, dampingCurveWeight);
	var denominator = 
		numerator + Mathf.Pow(attenuationRatio - attenuationRatio * val, dampingCurveWeight);
	
	return numerator / denominator;
}

numerator와 denominator는 수식을 그대로 코드로 옮겼다. 이 함수는 val = 0일 땐 0, val = 1일 땐 1에 가까운 값을 반환하면서 중앙은 완만하고 외각은 급격히 상승하는 S자 형태의 감쇠곡선을 형성한다.

 

3. 시각화

FalloffMap을 시각화하고 싶다면, 텍스쳐 생성 부분의 함수에서 파라미터로 FalloffMap을 받고, for루프 안을 다음과 같이 수정해 주면 된다.

var val = falloff[y, x];
var color = new Color(val, val, val);
tex.SetPixel(x, y, color);
  • 텍스쳐 필터 모드는 Bilinear로 한다.

 

■ 최종 결과

기존 생성한 NoiseMap에 FallOffMap을 뺀 뒤, 그 값을 다시 0~1 범위로 정규화해 주면 최종적인 섬 지형의 형태가 만들어진다.

public void ApplyFallOffMap()
{
	_fractalMap = new float[noiseData.Height, noiseData.Width];
	        
	for (var y = 0; y < noiseData.Height; ++y)
	{
		for (var x = 0; x < noiseData.Width; ++x)
		{
			_fractalMap[y, x] = Mathf.Clamp01(_noiseMap[y, x] - _fallOffMap[y, x]);
		}
	}
	
	var tex = NoiseTextureGenerator.GenerateNoiseTexture(_fractalMap, colorData.NoiseColor);
	fractalRenderer.sprite = Sprite.Create(tex, 
	new Rect(0, 0, noiseData.Width, noiseData.Height), new Vector2(0.5f, 0.5f));
}

[최종 결과]

다음에는 이 NoiseMap을 사용하여 터레인을 생성하고 높이별 색상을 적용시켜 실제 지형을 생성해보자.

728x90

'Unity,C# > 절차적생성(PCG)' 카테고리의 다른 글

[C#, Unity, 절차적 생성] 절차적 던전 생성 - 2. 방 생성  (0) 2025.05.12
[C#, Unity, 절차적 생성] 절차적 던전 생성 - 1. Binary Space Partitioning  (1) 2025.05.07
[C#, Unity, 절차적 생성] 절차적 지형 생성 - 3.터레인 생성  (0) 2025.04.28
[C#, Unity, 절차적 생성] 절차적 지형 생성 - 1.PerlinNoise  (1) 2025.04.16
[C#, Unity, 절차적 생성] Recursive Backtracking 알고리즘을 사용한 절차적 미로 생성  (4) 2025.04.12
'Unity,C#/절차적생성(PCG)' 카테고리의 다른 글
  • [C#, Unity, 절차적 생성] 절차적 던전 생성 - 1. Binary Space Partitioning
  • [C#, Unity, 절차적 생성] 절차적 지형 생성 - 3.터레인 생성
  • [C#, Unity, 절차적 생성] 절차적 지형 생성 - 1.PerlinNoise
  • [C#, Unity, 절차적 생성] Recursive Backtracking 알고리즘을 사용한 절차적 미로 생성
브라더스톤
브라더스톤
유티니, C#과 관련한 여러 정보를 끄적여둔 블로그입니다. Email : dkavmdk98@gmail.com
  • 브라더스톤
    젊은 프로그래머의 슬픔
    브라더스톤
  • 전체
    오늘
    어제
    • 개발 노트 (26) N
      • Unity,C# (26) N
        • Unity 정보 (5) N
        • 알고리즘 (10)
        • 자료구조 (2)
        • 절차적생성(PCG) (9)
      • C++ (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    C#
    binary space partitioning
    절차적 던전 생성
    최단경로찾기
    절차적던전생성
    pcg
    PerlinNoise
    자료구조
    이진공간분할법
    CustomEditorWindow
    커스텀 윈도우
    정렬알고리즘
    커스텀 인스펙터
    unity
    이진공간분할
    절차적지형생성
    알고리즘
    BSP
    CustomWindow
    Custom Inspector
  • 최근 댓글

  • 최근 글

  • 250x250
  • hELLO· Designed By정상우.v4.10.3
브라더스톤
[C#, Unity, 절차적 생성] 절차적 지형 생성 - 2.프랙탈 합
상단으로

티스토리툴바