본문 바로가기

이론

동적으로 섬 생성하기 - 4 (지형 생성 해보기)

전 편을 읽으신다면 이해에 큰 도움이 되실겁니다.

 

이제 유니티에서 섬을 그려봅시다.

 

노이즈는 FastNoiseLite라는 라이브러리를 사용해서 만들건데요,

이 파일만 유니티 프로젝트에 임포트하시면 되겠습니다.

윈도우 기준 저 페이지에서 우클릭하신 후 다른 이름으로 저장하신다면 .cs 파일로 저장될 겁니다.

 

MapEditorGUI.cs
0.00MB
Map.cs
0.00MB
MapDrawer.cs
0.00MB
MapTest.unity
0.18MB

또, 위의 코드와 씬을 모조리 받으신 후 유니티 프로젝트에 임포트하시고 읽으실 것을 추천드립니다.

코드가 비대해져서 더 이상 따라쓰시거나 복붙하시기가 좀 힘들 것 같네요....

여기서 새로 만든 MapEditorGUI.cs 파일은 인스펙터에서 게임을 시작하지 않아도

인스펙터의 Map 스크립트 필드의 값을 바꾸거나 Generate Map 버튼을 클릭함으로써 맵을 생성하도록 해줍니다.

 

맵의 바이옴을 시각화했던 것처럼, 맵의 모양도 시각화해봅시다.

첨부한 MapTest 씬으로 이동하세요.

 

우선, 맵의 모양을 만드는 CreateMapShape 함수를 점진적으로 개발해보겠습니다.

(위의 코드는 완성본 코드입니다.)

 

private float[] CreateMapShape(Vector2Int size, float frequency, int octave)
{
    // 인스펙터에서 필드의 값이 바뀌거나 Generate Map 버튼을 눌렀을 때 새로운 섬이 생성됨.
    // 인스펙터에서 설정된 시드가 0이라면 새로운 섬을 생성.
    // 그렇지 않다면 설정된 시드를 그대로 사용한다.
    var seed = (_seed == 0) ? Random.Range(1, int.MaxValue) : _seed;

    var noise = new FastNoiseLite();
    // 펄린 노이즈를 생성하도록 변경
    noise.SetNoiseType(FastNoiseLite.NoiseType.Perlin);
    // 기본적인 프렉탈 노이즈타입으로 설정 (Fractional Brownian motion)
    noise.SetFractalType(FastNoiseLite.FractalType.FBm);
    noise.SetFrequency(frequency);
    noise.SetFractalOctaves(octave);
    noise.SetSeed(seed);
    
    // 색은 0~1 범위이며 0은 검은색, 1은 흰색을 나타낸다.
    var colorDatas = new float[size.x * size.y];
    var index = 0;
    for (int y = 0; y < size.y; ++y)
    {
        for (int x = 0; x < size.x; ++x)
        {
            var noiseColorFactor = noise.GetNoise(x, y);
            // 노이즈의 범위가 -1~1이기 때문에, 0~1 범위로 변환
            noiseColorFactor = (noiseColorFactor + 1) * 0.5f;
            
            colorDatas[index] = noiseColorFactor;
            ++index;
        }
    }
    return colorDatas;
}

결과물로 나온 노이즈

위 코드블럭을 CreateMapShape 함수에 덮어쓴다면, 위 사진처럼 노이즈가 생성됩니다.

 

노이즈의 속성 조절

 

인스펙터에 있는 Map 스크립트의 seed 필드에 아무 값이나 넣으셔서 시드를 고정하시고.

noiseFrequency, noiseOctave 필드를 수정함으로써 노이즈의 정밀도와 디테일을 조절하실 수 있습니다.

 

아무튼 이 노이즈를 토대로 맵의 아웃라인을 생성할 것입니다.

설명은 코드블럭의 주석으로 충분하리라 생각합니다. 이해가 힘드시다면 댓글에 질문해주셔도 괘안습니다.

이 파일을 사용하시면 유니티를 거치지 않고도 노이즈를 편하게 시뮬레이팅하실 수 있으니

잘 이용하시면 생산성에 큰 기여를 합니다.

 

노이즈의 픽셀이 어두울수록 낮은 고도, 밝을수록 높은 고도로 설정할 것입니다.

이번엔 노이즈 색이 특정 밝기를 초과한다면 육지, 그 이하라면 바다로 정의합시다.

 

//colorDatas[index] = noiseColorFactor;

var color = noiseColorFactor > landNoiseThreshold ? 1f : 0f;
colorDatas[index] = color;

기존의 색 정보를 대입하는 부분을 다음과 같이 변경한 후,

에디터에서 LandNoiseThreshold 값을 적절히 조절하시다보면...

 

 

이런 식으로 무한한 땅이 만들어집니다. 비록 섬의 형태는 아니지만, 그래도 그럴듯한 육지의 형태가 보입니다.

섬의 형태로 만드는 방법은 간단합니다. 노이즈를 마스킹하면 끝입니다.

 

원형 그래디언트 마스크.

 

섬은 사방으로 열린 형태이고 내륙으로 들어갈수록 지대가 높아지는 경향을 띄기 때문에

다음과 같은 원형 그래디언트 마스크를 노이즈에 겹칠 것입니다.

맵의 정보를 넣어주는 루프문을 다음과 같이 수정합시다. 주석이 추가된 부분이 새로 추가된 부분입니다.

 

// 그래디언트 마스크 생성
var mask = MapDrawer.GetRadialGradientMask(size, noiseMaskRadius);
var colorDatas = new float[size.x * size.y];
var index = 0;
for (int y = 0; y < size.y; ++y)
{
    for (int x = 0; x < size.x; ++x)
    {
        var noiseColorFactor = noise.GetNoise(x, y);
        noiseColorFactor = (noiseColorFactor + 1) * 0.5f;
        // 생성한 노이즈의 픽셀마다 같은 자리의 마스크 픽셀 색을 곱한다.
        noiseColorFactor *= mask[index];

        var color = noiseColorFactor > landNoiseThreshold ? 1f : 0f;
        colorDatas[index] = color;
        ++index;
    }
}

 

그 후 LandNoiseThreshold 값을 적당히 줄여본다면...

 

그럴듯하다...

그럴 듯한 섬의 모양이 양산됩니다.

첨부해드린 코드와 씬에서 NoiseMaskRadius 필드의 값을 높힐수록, 마스킹되는 범위가 넓어집니다.

 

물론 직접 만든 마스크도 사용하실 수 있겠지만, 코드를 좀 수정하셔야 합니다.

 

그래디언트 마스크를 생성하는 GetRadialGradientMask 함수의 동작에 관심이 있으시다면 코드를 설명 주석과 함께 읽으시고, 관심 없으시다면 그냥 넘어가셔도 무방합니다.

 

public static float[] GetRadialGradientMask(Vector2Int size, int maskRadius)
{
    var colorData = new float[size.x * size.y];

    // 맵의 중점
    var center = size / 2;
    // 맵의 반지름
    var radius = center.y;
    var index = 0;
    for (int y = 0; y < size.y; ++y)
    {
        for (int x = 0; x < size.x; ++x)
        {
            var position = new Vector2Int(x, y);
            // 맵의 중점으로부터의 거리에 따라 색을 결정한다. 물론 설정해둔 마스킹 범위도 고려한다.
            var distFromCenter = Vector2Int.Distance(center, position) + (radius - maskRadius);
            var colorFactor = distFromCenter / radius;
            // 거리가 멀수록 색은 1에 가까워지만
            // 원래 의도는 내륙에 가까울수록 땅은 고지대여야하기 때문에 색을 반전한다.
            colorData[index++] = 1 - colorFactor;
        }
    }
    return colorData;
}