[Unity] Planet Generation [5] - shader

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

이제 우리가 원하는 행성 표면은 모두 만들었다. 이제 이 행성 표면에 쉐이더를 적용시켜서 실제 지구처럼 보이도록 수정해보자. 이 때 각 지점의 지형 높이를 기준으로 색상을 선택하도록 하여 높이 고저차로 색을 수정해준다.


일단 먼저 Universal RP를 임포트해준다.

이제 아래와 같이 URP Asset, Lit Shader Graph, Material을 하나씩 생성해준다.

그 다음 ColorSettings 스크립트를 수정한다.


ColorSettings.cs

[CreateAssetMenu()]
public class ColorSettings : ScriptableObject
{
    public Color planetColor;
    public Material planetMaterial;
}

그 다음 Planet을 생성할 때 Mesh에서 기존 Standard를 찾아오는 부분을 제거하고 ColorSettings에서 선언해준 planetMaterial을 가져오도록 스크립트를 수정한다.

Planet.cs

void Initialize()
{
    shapeGenerator = new ShapeGenerator(shapeSettings);

    if (meshFilters == null || meshFilters.Length == 0)
    {
        meshFilters = new MeshFilter[6];
    }
    terrainFaces = new TerrainFace[6];

    Vector3[] directions = { Vector3.up, Vector3.down, Vector3.left, Vector3.right, Vector3.forward, Vector3.back };

    for (int i = 0; i < 6; i++)
    {
        if (meshFilters[i] == null)
        {
            GameObject meshObj = new GameObject("mesh");
            meshObj.transform.parent = transform;

            meshObj.AddComponent<MeshRenderer>();
            meshFilters[i] = meshObj.AddComponent<MeshFilter>();
            meshFilters[i].sharedMesh = new Mesh();
        }

        meshFilters[i].GetComponent<MeshRenderer>().sharedMaterial = colorSettings.planetMaterial;

        terrainFaces[i] = new TerrainFace(shapeGenerator, meshFilters[i].sharedMesh, resolution, directions[i]);
        bool renderFace = faceRenderMask == FaceRenderMask.All || (int)faceRenderMask - 1 == i;
        meshFilters[i].gameObject.SetActive(renderFace);
    


최대 최소 고도를 구하기 위해 MinMax 클래스 스크립트를 아래와 작성해준 후에 높이에 따른 색상 조절 로직을 구현해보자.

MinMax.cs

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

public class MinMax 
{
    public float Min {  get; private set; }
    public float Max { get; private set; }

    public MinMax()
    {
        Min = float.MaxValue;
        Max = float.MinValue;
    }

    public void AddValue(float v)
    {
        if (v > Max)
        {
            Max = v;
        }
        if (v < Min)
        {
            Min = v;
        }
    }
}

ShapeGenerator.cs

public class ShapeGenerator 
{
    ShapeSettings settings;
    INoiseFilter[] noiseFilters;
    public MinMax elevationMinMax;

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

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

        if (noiseFilters.Length > 0)
        {
            firstLayerValue = noiseFilters[0].Evaluate(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].Evaluate(pointOnUnitSphere) * mask;
            }
        }
        elevation = settings.planetRadius * (1 + elevation);
        elevationMinMax.AddValue(elevation);
        return pointOnUnitSphere * elevation;
    }
}

위처럼 작성해서 각 점들의 elevation을 계산할 때마다 elevationMinMax에 AddValue해주면서 고도의 최대, 최소값을 저장해둔다. 이제 이 높이를 기준으로 색상을 구현해보자.

ColorGenerator.cs

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

public class ColorGenerator 
{
    ColorSettings settings;

    public ColorGenerator(ColorSettings settings)
    {
        this.settings = settings;   
    }

    public void UpdateElevation(MinMax elevationMinMax)
    {
        settings.planetMaterial.SetVector("_elevationMinMax", new Vector4(elevationMinMax.Min, elevationMinMax.Max));
    }
}

이후에 Planet에서 ColorGenerator를 선언 후 초기화해주고 Mesh를 생성해준 후에 ColorGenerator의 UpdateElevation 메소드를 호출해준다.

Planet.cs

ShapeGenerator shapeGenerator;
ColorGenerator colorGenerator;

void Initialize()
{
    shapeGenerator = new ShapeGenerator(shapeSettings);
    colorGenerator = new ColorGenerator(colorSettings);

	//생략
}

void GenerateMesh()
{
    for (int i = 0; i < 6;i++)
    {
        if (meshFilters[i].gameObject.activeSelf)
        {
            terrainFaces[i].ConstructMesh();
        }
    }
    colorGenerator.UpdateElevation(shapeGenerator.elevationMinMax);
}

이제 Planet Shader를 수정해보자. 다음과 같이 ElevationMinMax를 선언해준후 reference를 _elevationMinMax로 수정해줘서 ColorGenerator에서 접근할 수 있도록 해준다. 그 다음 해당 Property를 Split해줘서 최소 최대를 따로 가져온다. 그 후에 texture 2D를 적용시켜서 해당 값을 Base Color로 설정해준다.

이제 Gradient 값을 ColorSettings에서 관리할 수 있도록 스크립트를 수정한다.

ColorSettings.cs

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

[CreateAssetMenu()]
public class ColorSettings : ScriptableObject
{
    public Gradient gradient;
    public Material planetMaterial;
}

ColorGenerator.cs

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

public class ColorGenerator 
{
    ColorSettings settings;
    Texture2D texture;
    const int textureResolution = 50;

    public void UpdateSettings(ColorSettings settings)
    {
        this.settings = settings;
        if (texture == null)
        {
            texture = new Texture2D(textureResolution, 1);
        }
    }

    public void UpdateElevation(MinMax elevationMinMax)
    {
        settings.planetMaterial.SetVector("_elevationMinMax", new Vector4(elevationMinMax.Min, elevationMinMax.Max));
    }

    public void UpdateColors()
    {
        Color[] colors = new Color[textureResolution];
        for (int i = 0;i < textureResolution; i++)
        {
            colors[i] = settings.gradient.Evaluate(i / (textureResolution - 1f));
        }
        texture.SetPixels(colors);
        texture.Apply();
        settings.planetMaterial.SetTexture("_texture", texture);
    }
}

ShapeGenerator.cs

public void UpdateSettings(ShapeSettings settings)
{
    this.settings = settings;
    noiseFilters = new INoiseFilter[settings.noiseLayers.Length];
    for(int i = 0; i < noiseFilters.Length; i++)
    {
        noiseFilters[i] = NoiseFilterFactory.CreateNoiseFilter(settings.noiseLayers[i].noiseSettings);
    }
    elevationMinMax = new MinMax();
}

Planet.cs

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

public class Planet : MonoBehaviour {

    [Range(2,256)]
    public int resolution = 10;
    public bool autoUpdate = true;
    public enum FaceRenderMask { All, Top, Bottom, Left, Right, Front, Back };
    public FaceRenderMask faceRenderMask;

    public ShapeSettings shapeSettings;
    public ColorSettings colorSettings;

    [HideInInspector]
    public bool shapeSettingFoldout;
    [HideInInspector]
    public bool colorSettingFoldout;

    ShapeGenerator shapeGenerator = new ShapeGenerator();
    ColorGenerator colorGenerator = new ColorGenerator();

    [SerializeField, HideInInspector]
    MeshFilter[] meshFilters;
    TerrainFace[] terrainFaces;

    private void OnValidate()
    {
        GeneratePlanet();
    }

    void Initialize()
    {
        shapeGenerator.UpdateSettings(shapeSettings);
        colorGenerator.UpdateSettings(colorSettings);
        //생략
    }


    void GenerateColors()
    {
        colorGenerator.UpdateColors();
    }
}

이제 Gradient를 적절히 수정한 후에 Geneerate Planet을 누르면 아래와 같이 높이에 따른 색상 적용이 구현된다.

이제 진짜 지구처럼 보이는 것을 확인할 수 있다.

profile
Be Honest, Be Harder, Be Stronger

0개의 댓글