콘텐츠로 이동

Unity Terrain System 고급 활용

Unity Terrain System은 대규모 지형을 효율적으로 렌더링하고 편집하는 내장 시스템입니다. LOD, Batching, GPU Instancing을 자동 처리하며, TerrainData API를 통해 런타임에 높이맵과 텍스처를 동적으로 수정할 수 있습니다.


public class TerrainModifier : MonoBehaviour
{
[SerializeField] private Terrain _terrain;
private TerrainData _data;
void Awake()
{
_data = _terrain.terrainData;
}
// 특정 월드 좌표의 높이를 올리기
public void RaiseAt(Vector3 worldPos, float radius, float amount)
{
// 월드 → 지형 UV 좌표 변환
Vector3 terrainPos = worldPos - _terrain.transform.position;
int res = _data.heightmapResolution;
float normalX = terrainPos.x / _data.size.x;
float normalZ = terrainPos.z / _data.size.z;
int cx = Mathf.RoundToInt(normalX * (res - 1));
int cz = Mathf.RoundToInt(normalZ * (res - 1));
int pixelRadius = Mathf.RoundToInt(
radius / _data.size.x * (res - 1));
int xMin = Mathf.Clamp(cx - pixelRadius, 0, res - 1);
int zMin = Mathf.Clamp(cz - pixelRadius, 0, res - 1);
int xMax = Mathf.Clamp(cx + pixelRadius, 0, res - 1);
int zMax = Mathf.Clamp(cz + pixelRadius, 0, res - 1);
int w = xMax - xMin + 1;
int h = zMax - zMin + 1;
float[,] heights = _data.GetHeights(xMin, zMin, w, h);
for (int z = 0; z < h; z++)
for (int x = 0; x < w; x++)
{
float dist = Vector2.Distance(
new Vector2(x, z),
new Vector2(pixelRadius, pixelRadius));
float falloff = Mathf.Clamp01(1f - dist / pixelRadius);
heights[z, x] += amount * falloff * Time.deltaTime;
}
_data.SetHeights(xMin, zMin, heights);
}
// 특정 위치의 실제 높이 조회
public float GetHeightAt(Vector3 worldPos)
{
return _terrain.SampleHeight(worldPos);
}
}

public class TerrainPainter : MonoBehaviour
{
[SerializeField] private Terrain _terrain;
// 경사도에 따른 텍스처 자동 배분
public void AutoPaintBySlope()
{
var data = _terrain.terrainData;
int res = data.alphamapResolution;
int layerCount = data.alphamapLayers;
float[,,] alphas = data.GetAlphamaps(0, 0, res, res);
for (int z = 0; z < res; z++)
for (int x = 0; x < res; x++)
{
// 정규화된 좌표
float nx = (float)x / res;
float nz = (float)z / res;
// 경사도 계산 (0=평지, 90=절벽)
float slope = data.GetSteepness(nx, nz);
float slopeNorm = Mathf.Clamp01(slope / 45f);
// Layer 0: 잔디 (평지), Layer 1: 바위 (경사)
alphas[z, x, 0] = 1f - slopeNorm;
alphas[z, x, 1] = slopeNorm;
// 나머지 레이어는 0으로 초기화
for (int l = 2; l < layerCount; l++)
alphas[z, x, l] = 0f;
}
data.SetAlphamaps(0, 0, alphas);
}
// 브러쉬로 특정 레이어 페인팅
public void PaintLayer(Vector3 worldPos, int layerIndex,
float radius, float strength)
{
var data = _terrain.terrainData;
var localPos = worldPos - _terrain.transform.position;
float normalX = localPos.x / data.size.x;
float normalZ = localPos.z / data.size.z;
int alphaRes = data.alphamapResolution;
int cx = Mathf.RoundToInt(normalX * alphaRes);
int cz = Mathf.RoundToInt(normalZ * alphaRes);
int r = Mathf.RoundToInt(radius / data.size.x * alphaRes);
int xMin = Mathf.Clamp(cx - r, 0, alphaRes - 1);
int zMin = Mathf.Clamp(cz - r, 0, alphaRes - 1);
int w = Mathf.Min(r * 2, alphaRes - xMin);
int h = Mathf.Min(r * 2, alphaRes - zMin);
float[,,] alphas = data.GetAlphamaps(xMin, zMin, w, h);
int layers = data.alphamapLayers;
for (int z = 0; z < h; z++)
for (int x = 0; x < w; x++)
{
float dist = Vector2.Distance(new Vector2(x, z), new Vector2(r, r));
float falloff = Mathf.Clamp01(1f - dist / r);
float paint = strength * falloff * Time.deltaTime;
alphas[z, x, layerIndex] = Mathf.Clamp01(
alphas[z, x, layerIndex] + paint);
// 합이 1이 되도록 정규화
float total = 0f;
for (int l = 0; l < layers; l++) total += alphas[z, x, l];
if (total > 0f)
for (int l = 0; l < layers; l++)
alphas[z, x, l] /= total;
}
data.SetAlphamaps(xMin, zMin, alphas);
}
}

public class TerrainPopulator : MonoBehaviour
{
[SerializeField] private Terrain _terrain;
// 런타임 트리 배치
public void PlantTreeAt(Vector3 worldPos, int prototypeIndex)
{
var data = _terrain.terrainData;
var localPos = worldPos - _terrain.transform.position;
var instance = new TreeInstance
{
prototypeIndex = prototypeIndex,
position = new Vector3(
localPos.x / data.size.x,
0f,
localPos.z / data.size.z),
widthScale = Random.Range(0.8f, 1.2f),
heightScale = Random.Range(0.8f, 1.2f),
color = Color.white,
lightmapColor = Color.white
};
_terrain.AddTreeInstance(instance);
}
// 디테일(풀/꽃) 배치
public void SetGrassAt(Vector3 worldPos, int layer, int density)
{
var data = _terrain.terrainData;
var localPos = worldPos - _terrain.transform.position;
int res = data.detailResolution;
int x = Mathf.RoundToInt(localPos.x / data.size.x * res);
int z = Mathf.RoundToInt(localPos.z / data.size.z * res);
int[,] details = data.GetDetailLayer(x, z, 1, 1, layer);
details[0, 0] = density;
data.SetDetailLayer(x, z, layer, details);
}
}

// Terrain 설정 (Inspector)
// - Pixel Error: 높을수록 LOD 공격적 (5~15 권장)
// - Base Map Distance: 원거리에서 저해상도 알파맵 사용
// - Detail Distance/Density: 디테일 렌더 거리 조정
// - Tree Distance: 트리 빌보드 전환 거리
public class TerrainOptimizer : MonoBehaviour
{
[SerializeField] private Terrain[] _terrains;
// 카메라 거리에 따른 동적 LOD
void Update()
{
var camPos = Camera.main.transform.position;
foreach (var terrain in _terrains)
{
float dist = Vector3.Distance(
terrain.transform.position, camPos);
terrain.heightmapPixelError = dist < 200f ? 5f : 15f;
terrain.detailObjectDistance = dist < 100f ? 80f : 40f;
}
}
}

5. Terrain.Flush와 CollisionFlags 동기화

섹션 제목: “5. Terrain.Flush와 CollisionFlags 동기화”
public class TerrainRuntimeEditor : MonoBehaviour
{
[SerializeField] private Terrain _terrain;
// 높이맵 수정 후 콜라이더 즉시 동기화
public void ModifyAndSync(Vector3 worldPos, float amount)
{
var data = _terrain.terrainData;
// 높이맵 수정 (생략)
// data.SetHeights(x, z, heights);
// 물리 콜라이더를 새 높이맵과 동기화
_terrain.Flush(); // 내부적으로 TerrainCollider.Refresh() 호출
// Flush 전까지 Raycast는 이전 높이맵 기준으로 동작
}
// 인접 Terrain 스티칭 — 경계 이음새 제거
public void StitchNeighbors()
{
// 동/서/남/북 이웃 Terrain 연결
Terrain right = GetNeighbor(Vector3.right);
Terrain left = GetNeighbor(Vector3.left);
Terrain top = GetNeighbor(Vector3.forward);
Terrain bottom = GetNeighbor(Vector3.back);
_terrain.SetNeighbors(left, top, right, bottom);
// SetNeighbors 호출 후 이음새 LOD 전환이 매끄럽게 처리됨
}
private Terrain GetNeighbor(Vector3 direction)
{
var origin = _terrain.transform.position
+ direction * _terrain.terrainData.size.x
+ Vector3.up * 10f;
if (Physics.Raycast(origin, Vector3.down, out var hit, 20f))
return hit.collider.GetComponent<Terrain>();
return null;
}
}

TerrainData.SetHeightsSetAlphamaps로 런타임 지형 변형과 텍스처 페인팅이 가능합니다. 대규모 수정 후에는 Flush() 또는 씬 저장이 필요합니다. 성능은 heightmapPixelError(LOD), detailObjectDistance(풀 거리), treeDistance(트리 거리)를 카메라 거리에 따라 동적 조정하는 것으로 가장 효과적으로 개선할 수 있습니다.