[Unity] Planet Generation [3] - layered noise

suhan0304·2024년 3월 2일
0
post-thumbnail

preview Link : https://velog.io/@suhan0304/Unity-Planet-Generation-2-Procedural-Planets
gitHub Link : https://github.com/suhan0304/Planet-Generation
PlanetGeneration


layered noise

행성의 지표면을 지금의 매끄러운 표면이 아니라 실제 행성의 표면처럼 랜덤하게 울퉁불퉁할 수 있도록, 지표면의 높낮이가 랜덤하게 정해지도록 layered noise를 구현해보자.


이때 중요한 점은 Random noise가 아니라 우리에게 필요한건 Coherent noise 즉, 일종의 일관된 noise가 필요하다. 실제로 지구의 지표면을 보면 모든 지표면이 울퉁불퉁한 것이 아니라 평지도 어느 정도 있으면서 특정 위치에 산이 모여있는 형태를 가지고 있는 것을 확인할 수 있다.


Noise.cs는 위 깃허브 프로젝트에서 확인할 수 있다. Noise 스크립트를 libnoise-dotnet의 일부를 사용하였고 libnoise-dotnet은 무료 소프트웨어이며 재배포 및 수정이 가능하다. 이제 노이즈를 생성해서 지표면에 적용시키기 위해 NoiseFilter 스크립트를 작성해보자.

NoiseFilter.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class NoiseFilter
{
    Noise noise = new Noise();

    public float Evalute(Vector3 point)
    {
        float noiseValue = noise.Evaluate(point);
        return noiseValue;
    }
}

이제 노이즈 필터를 행성이 생성될 때 적용될 수 있도록 ShapeGenerator에 NoiseFilter가 적용되도록 작성한다.

ShapeGenerator.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ShapeGenerator 
{
    ShapeSettings settings;
    NoiseFilter noiseFilter;
    public ShapeGenerator(ShapeSettings settings)
    {
        this.settings = settings;
        noiseFilter = new NoiseFilter();
    }

    public Vector3 CalculatePoinOnPlanet(Vector3 pointOnUnitSphere)
    {
        float elevation = noiseFilter.Evalute(pointOnUnitSphere);
        return pointOnUnitSphere * settings.planetRadius * (1 + elevation); ;
    }
}

유니티 에디터로 돌아가 컴파일을 완료하면 행성의 모양이 많이 변형된 것을 확인할 수 있다.

하지만 실제로 이러한 행성 모양은 없을 뿐더러 noise 정도를 설정할 필요가 있기 때문에 NoiseSettings에서 strength, roughness, centre를 설정해준후에 해당 값을 기반으로 ShapeGeneration이 진행되도록 수정해주었다.

NoiseSettings

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[System.Serializable]
public class NoiseSettings 
{
    public float strength = 1;
    public float roughness = 1;
    public Vector3 centre;
}

ShapeGenerator.cs

public ShapeGenerator(ShapeSettings settings)
{
    this.settings = settings;
    noiseFilter = new NoiseFilter(settings.noiseSettings);
}

ShapeSettings

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[CreateAssetMenu()]
public class ShapeSettings : ScriptableObject
{
    public float planetRadius = 1;
    public NoiseSettings noiseSettings;
}

이제 Planet의 NoiseSettings의 값들을 설정하면서 다양한 작업을 수행할 수 있다.

strength를 줄여서 noise의 강도를 제어할 수 있으며

roughness를 설정해서 울퉁불퉁한 표면을 더 세밀하게 나타낼 수 있다.


하지만 실제로 이러한 설정 값만을 조절해서 실제 우리가 사는 지구의 모양과 같은 행성을 만들기는 어려운 것을 확인할 수 있다. 실제로 우리가 원하는 행성의 표면과 같은 모양을 얻기 위해서는 여러 레이어의 노이즈를 더해가면서 행성을 만들어서 지구와 같은 표면을 얻을 수 있다.

이때 여러 레이어의 노이즈를 구할때 roughness에 따른 진동수를 점차 증가시키면서 행성을 더한다.

진동수의 증가와 동시에 진폭을 감소시키면서 레이어를 더하면서 더욱 더 우리가 원하는 행성의 모양에 가깝도록 구현할 수 있다.

NoiseSettings.cs

public class NoiseSettings 
{
    public float strength = 1;
    [Range(1, 8)]
    public int numLayers = 1;
    public float baseRoughness = 1;
    public float roughness = 2;
    public float persistence = .5f;
    public Vector3 centre;
    public float minValue;
}

numLayers는 더할 레이어들의 수이며 persistence는 레이어들이 생성되면서 각 레이어의 진폭이 감소되는 비율이다. .5f 이기 때문에 진폭이 각 레이어에서 절반으로 줄어들면서 생성된다. 기본 roughness는 2에서 1씩 점차 증가되도록 설정한다. 이 값들을 노이즈 필터에 적용시켜 레이어들을 생성하도록 한다. 또한 특정 값보다 낮은 값을 가지는 noiseValue는 minValue를 사용해 0으로 고정시키도록 한다.

NoiseFilter.cs

public float Evalute(Vector3 point)
{
    float noiseValue = 0;
    float frequency = settings.baseRoughness;
    float amplitude = 1;

    for (int i = 0; i < settings.numLayers; i++)
    {
        float v = noise.Evaluate(point * frequency + settings.centre);
        noiseValue += (v + 1) * .5f * amplitude;
        frequency *= settings.roughness;
        amplitude *= settings.persistence;
    }

    noiseValue = Mathf.Max(0, noiseValue - settings.minValue);
    return noiseValue * settings.strength;
}

수치들을 잘 다듬으면 바다(해수면)과 육지를 가진 지구와 비슷한 행성을 구현할 수 있다.


ShapeSettings.cs

[CreateAssetMenu()]
public class ShapeSettings : ScriptableObject
{
    public float planetRadius = 1;
    public NoiseLayer[] noiseLayers;

    [System.Serializable]
    public class NoiseLayer
    {
        public bool enables = true;
        public NoiseSettings noiseSettings;
    }
}

ShapeGenerator.cs

public class ShapeGenerator 
{
    ShapeSettings settings;
    NoiseFilter[] noiseFilters;

    public ShapeGenerator(ShapeSettings settings)
    {
        this.settings = settings;
        noiseFilters = new NoiseFilter[settings.noiseLayers.Length];
        for(int i = 0; i < noiseFilters.Length; i++)
        {
            noiseFilters[i] = new NoiseFilter(settings.noiseLayers[i].noiseSettings);
        }
    }

    public Vector3 CalculatePoinOnPlanet(Vector3 pointOnUnitSphere)
    {
        float elevation = 0;
        for (int i = 0; i < noiseFilters.Length; i++)
        {
            if (settings.noiseLayers[i].enables)
                elevation += noiseFilters[i].Evalute(pointOnUnitSphere);
        }
        return pointOnUnitSphere * settings.planetRadius * (1 + elevation); ;
    }
}

noiseFilters를 배열로 유지하면서 enabled한 noiseFilter만 행성에 영향을 주도록 수정해주었다.

  • Strength와 Base Roughness가 작아서 높이가 그렇게 높지 않은 언덕을 넓게 형성함

  • Strength와 Base Roughness가 비교적 높아서 높이가 좀 높은 산 같은 지형이 좁게 형성됨

위와 같이 다른 strength와 roughness, persistence를 가지는 noiseFilter를 각각 만들어주었다. 그런 후에 두 레이어를 동시에 enabled 해주면 두 가지의 노이즈 필터를 동시에 적용시켜줄 수 있다.

  • 높이가 낮은 지표면(대륙)과 높이가 높은 산이 동시에 존재하는 듯한 행성을 구현할 수 있음

행성을 보면 산이 갑자기 바다 위에서 형성되면서 좀 부자연스러운 부분들을 확인할 수 있다.

이러한 부자연스러움을 개선하기위해 산이 나타날 수 있는 위치를 첫번째 레이어 상의 minValue가 아닌, 즉 첫번째 레이어에서 바다가 아니라 언덕을 생성한 위치에서 두 번째 레이어가 산을 생성할 수 있도록 마스크를 사용하도록 한다.

ShapeSettings.cs

public class NoiseLayer
{
    public bool enabled = true;
    public bool useFirstLayerAsMask;
    public NoiseSettings noiseSettings;
}

ShapeGenerator.cs

public Vector3 CalculatePoinOnPlanet(Vector3 pointOnUnitSphere)
{
    float firstLayerValue = 0;
    float elevation = 0;

    if (noiseFilters.Length > 0)
    {
        firstLayerValue = noiseFilters[0].Evalute(pointOnUnitSphere);
        if (settings.noiseLayers[0].enabled)
        {
            elevation = firstLayerValue;
        }
    }
    for (int i = 1; i < noiseFilters.Length; i++)
    {
        if (settings.noiseLayers[i].enabled)
        {
            float mask = (settings.noiseLayers[i].useFirstLayerAsMask) ? firstLayerValue : 1;
            elevation += noiseFilters[i].Evalute(pointOnUnitSphere) * mask;
        }
    }
    return pointOnUnitSphere * settings.planetRadius * (1 + elevation); ;
}

이러면 첫 번째 레이어를 마스크로 사용할지 말지를 설정해주면 첫 번째 레이어가 생성한 대륙에서만 그 다음 noiseFilter가 지면을 추가적으로 생성하도록 할 수 있다.

두 번째 noiseFilter의 noiseSettings의 Use First Layer As Mask를 체크해주자 바다의 부자연스러운 산들이 대부분 사라지는 것을 확인할 수 있다. 이제 Strength를 조절해서 대륙에 높은 산들이 생성되도록 설정해준다.


첫 번째 noiseFilter로 대륙을 생성해주고 두 번째 noiseFilter로 산을 구현하면서 실제 지구와 비슷한 모양의 행성을 아래와 같이 구현했다.

profile
Be Honest, Be Harder, Be Stronger

0개의 댓글