현재 구현한 쉐이더는 지형의 높이에 따라서 색상이 조절된다. 하지만 실제 지구는 높이 뿐만 아니라 여러 요소의 영향으로 지형의 색상이 결정된다. 따라서 이번에는 위도에 따라 행성을 다양한 색상의 영역으로 나누어 간단한 Biomes 경계를 더욱 유기적으로 만들고 혼합되도록 구현해보자.
ColorSettings에서 biomeColorSettings를 관리하도록 하고 biomeColorSettings 안에 Gradient를 넣어준다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[CreateAssetMenu()]
public class ColorSettings : ScriptableObject
{
public Material planetMaterial;
public BiomeColorSettings biomeColorSettings;
[System.Serializable]
public class BiomeColorSettings
{
public Biome[] biomes;
[System.Serializable]
public class Biome
{
public Gradient gradient;
public Color tint;
[Range(0, 1)]
public float startHeight;
[Range(0, 1)]
public float tintPercent;
}
}
}
이제 ColorGenerator에서 적절한 biome을 가져와서 Color를 구현하도록 코드를 수정한다.
BiomePercentFromPoint로 pointOnUnitSphere, 즉 행성을 이루고 있는 정점들이 어떠한 생물 군계에 있는지 계산해서 해당 정보를 저장하고, UV 채널을 사용하여 셰이더의 색상을 그리도록 기능을 구현한다.
public void UpdateSettings(ColorSettings settings)
{
this.settings = settings;
if (texture == null || texture.height != settings.biomeColorSettings.biomes.Length)
{
texture = new Texture2D(textureResolution, settings.biomeColorSettings.biomes.Length, TextureFormat.RGBA32, false);
}
}
public float BiomePercentFromPoint (Vector3 pointOnUnitSphere)
{
float heightPercent = (pointOnUnitSphere.y + 1) / 2f;
float biomeIndex = 0;
int numBiomes = settings.biomeColorSettings.biomes.Length;
for (int i = 0; i < numBiomes; i++)
{
if (settings.biomeColorSettings.biomes[i].startHeight < heightPercent)
{
biomeIndex = i;
}
else
{
break;
}
}
return biomeIndex / Mathf.Max(1 , numBiomes - 1);
}
public void UpdateColors()
{
Color[] colors = new Color[textureResolution * texture.height];
int colorIndex = 0;
foreach(var biome in settings.biomeColorSettings.biomes)
{
for (int i = 0; i < textureResolution; i++)
{
Color gradientCol = biome.gradient.Evaluate(i / (textureResolution - 1f)); ;
Color tintCol = biome.tint;
colors[colorIndex] = gradientCol * (1 - biome.tintPercent) + tintCol * biome.tintPercent;
colorIndex++;
}
}
texture.SetPixels(colors);
texture.Apply();
settings.planetMaterial.SetTexture("_texture", texture);
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class TerrainFace {
ShapeGenerator shapeGenerator;
Mesh mesh;
int resolution;
Vector3 localUp;
Vector3 axisA;
Vector3 axisB;
public TerrainFace(ShapeGenerator shapeGenerator, Mesh mesh, int resolution, Vector3 localUp)
{
this.shapeGenerator = shapeGenerator;
this.mesh = mesh;
this.resolution = resolution;
this.localUp = localUp;
axisA = new Vector3(localUp.y, localUp.z, localUp.x);
axisB = Vector3.Cross(localUp, axisA);
}
public void ConstructMesh()
{
Vector3[] vertices = new Vector3[resolution * resolution];
int[] triangles = new int[(resolution - 1) * (resolution - 1) * 6];
int triIndex = 0;
Vector2[] uv = mesh.uv;
for (int y = 0; y < resolution; y++)
{
for (int x = 0; x < resolution; x++)
{
int i = x + y * resolution;
Vector2 percent = new Vector2(x, y) / (resolution - 1);
Vector3 pointOnUnitCube = localUp + (percent.x - .5f) * 2 * axisA + (percent.y - .5f) * 2 * axisB;
Vector3 pointOnUnitSphere = pointOnUnitCube.normalized;
vertices[i] = shapeGenerator.CalculatePoinOnPlanet(pointOnUnitSphere);
if (x != resolution - 1 && y != resolution - 1)
{
triangles[triIndex] = i;
triangles[triIndex + 1] = i + resolution + 1;
triangles[triIndex + 2] = i + resolution;
triangles[triIndex + 3] = i;
triangles[triIndex + 4] = i + 1;
triangles[triIndex + 5] = i + resolution + 1;
triIndex += 6;
}
}
}
mesh.Clear();
mesh.vertices = vertices;
mesh.triangles = triangles;
mesh.RecalculateNormals();
mesh.uv = uv;
}
public void UpdateUVs(ColorGenerator colorGenerator)
{
Vector2[] uv = new Vector2[resolution * resolution];
for (int y = 0; y < resolution; y++)
{
for (int x = 0; x < resolution; x++)
{
int i = x + y * resolution;
Vector2 percent = new Vector2(x, y) / (resolution - 1);
Vector3 pointOnUnitCube = localUp + (percent.x - .5f) * 2 * axisA + (percent.y - .5f) * 2 * axisB;
Vector3 pointOnUnitSphere = pointOnUnitCube.normalized;
uv[i] = new Vector2(colorGenerator.BiomePercentFromPoint(pointOnUnitSphere), 0);
}
}
mesh.uv = uv;
}
}
void GenerateColors()
{
colorGenerator.UpdateColors();
for (int i = 0; i < 6; i++)
{
if (meshFilters[i].gameObject.activeSelf)
{
terrainFaces[i].UpdateUVs(colorGenerator);
}
}
}
이제 Planet 쉐이더로 가서 아래와 수정해준다.
이제 Planet의 인스펙터에서 Biomes에서 Biome을 만들어준다.
위도에 따라서 두 바이옴으로 색이 나뉜 것을 확인할 수 있다. 실제 바이옴의 경계는 저렇게 직선으로 되어있지 않기 때문에 어느 정도 노이즈를 추가해서 잘 섞이도록 구현해보자.
public class BiomeColorSettings
{
public Biome[] biomes;
public NoiseSettings noise;
public float noiseOffset;
public float noiseStrength;
[Range(0,1)]
public float blendAmount;
[System.Serializable]
public class Biome
{
public Gradient gradient;
public Color tint;
[Range(0, 1)]
public float startHeight;
[Range(0, 1)]
public float tintPercent;
}
}
INoiseFilter biomeNoiseFilter;
public void UpdateSettings(ColorSettings settings)
{
this.settings = settings;
if (texture == null || texture.height != settings.biomeColorSettings.biomes.Length)
{
texture = new Texture2D(textureResolution, settings.biomeColorSettings.biomes.Length, TextureFormat.RGBA32, false);
}
biomeNoiseFilter = NoiseFilterFactory.CreateNoiseFilter(settings.biomeColorSettings.noise);
}
public float BiomePercentFromPoint (Vector3 pointOnUnitSphere)
{
float heightPercent = (pointOnUnitSphere.y + 1) / 2f;
heightPercent += (biomeNoiseFilter.Evaluate(pointOnUnitSphere) - settings.biomeColorSettings.noiseOffset) * settings.biomeColorSettings.noiseStrength;
float biomeIndex = 0;
int numBiomes = settings.biomeColorSettings.biomes.Length;
for (int i = 0; i < numBiomes; i++)
{
if (settings.biomeColorSettings.biomes[i].startHeight < heightPercent)
{
biomeIndex = i;
}
else
{
break;
}
}
return biomeIndex / Mathf.Max(1 , numBiomes - 1);
}
이제 NoiseSettings를 적절히 설정해주면 biome이 모양이 잘 섞이는 것을 확인할 수 있다.
경계의 색상이 선으로 구분되지 않고 색 또한 잘 혼합되도록 구현해보자.
blend range 만큼의 blend 범위를 잡아주고 가중치 weight를 blend range의 범위에 맞게 0 ~ 1까지 설정한 후에 색상이 해당 숫자의 비율만큼 혼합되도록 한다.
[System.Serializable]
public class BiomeColorSettings
{
public Biome[] biomes;
public NoiseSettings noise;
public float noiseOffset;
public float noiseStrength;
[Range(0,1)]
public float blendAmount;
[System.Serializable]
public class Biome
{
public Gradient gradient;
public Color tint;
[Range(0, 1)]
public float startHeight;
[Range(0, 1)]
public float tintPercent;
}
}
public float BiomePercentFromPoint (Vector3 pointOnUnitSphere)
{
float heightPercent = (pointOnUnitSphere.y + 1) / 2f;
heightPercent += (biomeNoiseFilter.Evaluate(pointOnUnitSphere) - settings.biomeColorSettings.noiseOffset) * settings.biomeColorSettings.noiseStrength;
float biomeIndex = 0;
int numBiomes = settings.biomeColorSettings.biomes.Length;
float blendRange = settings.biomeColorSettings.blendAmount / 2f + .001f; ;
for (int i = 0; i < numBiomes; i++)
{
float dst = heightPercent - settings.biomeColorSettings.biomes[i].startHeight;
float weight = Mathf.InverseLerp(-blendRange, blendRange, dst);
biomeIndex *= (1 - weight);
biomeIndex += i * weight;
}
return biomeIndex / Mathf.Max(1 , numBiomes - 1);
}
이제 Blend Amount를 증가시키면 색상이 잘 혼합되는 것을 확인할 수 있다.
이제 위도별로 biome을 생성해주고 biome 별로 Gradient에 따른 색상을 적절히 조절해보자.
위도에 따라 biome이 선택되고 biome에 따라 Gradient에 따른 쉐이더 색상이 설정되면서 biome간 경계에서는 두 개의 색상이 적절히 섞이면서 생성되어 실제 지구와 같이 구현할 수 있다.