■ 터레인 생성
앞서 생성했던 PerlinNoise 맵을 HeightMap으로 사용하여 Terrain을 만들어보자. Unity에서 Terrain을 생성하는 과정은 매우 간단하다.
1. Terrain 생성 코드
public void CreateTerrain()
{
var terrainData = terrain.terrainData;
terrainData.heightmapResolution = Mathf.Max(noiseData.Width, noiseData.Height);
terrainData.size = new Vector3(noiseData.Width, maxHeight, noiseData.Height);
terrainData.SetHeights(0, 0, _fractalMap);
SetShaderProperty();
}
- heightmapResolution : 높이맵의 해상도를 지정한다. 가로/세로 중 더 큰 값을 사용한다.
- Size : Terrain의 실제 월드 크기를 설정한다. (width, 최대높이, Height) 순서로 입력한다.
- 여기서 최대 높이는 PerlinNoise에서 생성된 최대 높이가 아닌, 월드 좌표상의 최대 높이를 의미한다.
- SetHeights : (0,0) 위치부터 시작해서, 지정한 2차원 배열(float[,])을 HeightMap으로 적용한다.
정리하면, 터레인의 크기와 높이 해상도를 설정한 뒤, 만든 노이즈 맵을 Terrain에 적용하는 구조이다.
■ Terrain Shader
1. Triplanar Mapping
일반적으론 텍스쳐 매핑에서는 각 정점(Vertex)이 텍스쳐 좌표계(UV) 상 어디에 위치하는지를 기반으로 텍스쳐를 적용한다.
하지만 런타임에 생성되는 Terrain처럼 UV좌표가 없는 경우, 기존 방식으로는 매핑할 수 없다.
- 특히 Y축 기준으로 텍스처를 투영하면, 수직 벽면에서는 텍스쳐가 심하게 찢어지고 왜곡된다.
이를 해결하기 위해 사용하는 방식이 Triplanar Mapping(삼축 매핑)이다.
TriplanarMapping 원리
먼저, 임시 UV좌표를 3축 방향으로 생성하고, 각 축에 대해 텍스쳐를 샘플링한다.
- X축 투영 : (Z, Y) 좌표 사용
- Y축 투영 : (X, Z) 좌표 사용
- Z축 투영 : (X, Y) 좌표 사용
각 축 방향으로부터의 월드 노말을 사용해 가중치를 계산한다. (노말이 수직에 가까울수록 텍스쳐 영향력이 커진다.)
- X, Y, Z축 각각에 대해 얼마나 이 방향을 향하고 있는지에 따라 텍스쳐의 영향력을 조절한다.
각각 X, Y, Z 축으로 투영한 텍스쳐를, 노멀 기반 가중치를 사용해 블렌딩해 합친다. 결과적으로 모든 면에 자연스럽게 텍스쳐가 이어진다.
2. Shader Graph 구현
Triplanar Mapping을 사용해서 터레인에 텍스쳐를 적용하고, 높이별로 텍스쳐를 적용시키고 경계선을 없애 자연스러운 지형을 만들어보자.
2-1. 정규화 처리
먼저 터레인의 Y좌표(높이)를 얻기 위해 Split노드를 사용해 Position에서 Y 성분만 분리한다.
이후 Remap 노드를 사용해 In Min/Max 범위(0~터레인 최대 높이)를 0~1로 다시 매핑한다.
- 터레인 전체 높이를 0~1 범위로 정규화시켜 블렌딩 제어를 직관적이게 만든다.
2-2. 텍스쳐 블렌딩
Triplanar노드를 사용해 텍스쳐를 좌표계에 따라 삼면 매핑한다.
→ Position 노드를 Triplanar의 Position에,
→ Normal 노드를 Triplanar의 Normal에 연결해 준다.
이제 높이별로 텍스쳐를 부드럽게 연결하기 위해 SmoothStep 노드를 사용한다. SmoothStep의 파라미터는 BlendingRange라는 Vector2 프로퍼티로 관리한다.
- BlendingRange.x : 블렌딩이 시작되는 높이
- BlendingRange.y : 블렌딩이 완료되는 높이
즉, $h < x$라면 텍스쳐가 온전히 적용되고, $x<h<y$범위에선 텍스쳐가 부드럽게 다른 텍스쳐로 전환되다가 $h > y$가 되면 다음 텍스쳐로 교체된다.
이렇게 만든 SmoothStep의 결과를 Lerp의 Input T에 연결하고, A에 이전 텍스쳐, B에 다음 텍스쳐를 연결하여 자연스럽게 섞는다.
- 모든 블렌딩이 완료되면, 마지막 Lerp의 결과를 BaseColor에 연결하여 출력한다.
- 새로운 텍스처를 추가하고 싶다면, Triplanar 매핑을 거친 후, 이전 Lerp의 결과와 새 텍스처를 다시 Lerp로 부드럽게 연결해 주면 된다.
2-3. 코드 적용
public void CreateTerrain()
{
var terrainData = terrain.terrainData;
terrainData.heightmapResolution = Mathf.Max(noiseData.Width, noiseData.Height);
terrainData.size = new Vector3(noiseData.Width, maxHeight, noiseData.Height);
terrainData.SetHeights(0, 0, _fractalMap);
SetShaderProperty();
}
private void SetShaderProperty()
{
terrainShader.SetVector(MinMaxHeight, new Vector2(0f, maxHeight));
if (blendingRangeNames.Count != blendingRanges.Length) return;
for (var i = 0; i < blendingRangeNames.Count; ++i)
{
var id = Shader.PropertyToID(blendingRangeNames[i]);
terrainShader.SetVector(id, blendingRanges[i]);
}
}
터레인 생성이 완료된 뒤, 인스펙터에서 설정한 BlendingRange 값을 셰이더에 적용한다.
- blendingRangeNames : 셰이더 프로퍼티 이름을 저장한 리스트.
- blendingRanges : 각 텍스처의 블렌딩 범위를 조절하는 값.
SetShaderProperty 함수는 두 리스트의 크기가 일치하는 경우에만 실행되며, 각 이름에 맞는 블렌딩 범위를 셰이더에 전달해 준다.
■ 최종 결과
'Unity,C# > 절차적생성(PCG)' 카테고리의 다른 글
[C#, Unity, 절차적 생성] 절차적 던전 생성 - 2. 방 생성 (0) | 2025.05.12 |
---|---|
[C#, Unity, 절차적 생성] 절차적 던전 생성 - 1. Binary Space Partitioning (1) | 2025.05.07 |
[C#, Unity, 절차적 생성] 절차적 지형 생성 - 2.프랙탈 합 (2) | 2025.04.24 |
[C#, Unity, 절차적 생성] 절차적 지형 생성 - 1.PerlinNoise (1) | 2025.04.16 |
[C#, Unity, 절차적 생성] Recursive Backtracking 알고리즘을 사용한 절차적 미로 생성 (4) | 2025.04.12 |