Unity Terrain System 고급 활용
Unity Terrain System은 대규모 지형을 효율적으로 렌더링하고 편집하는 내장 시스템입니다. LOD, Batching, GPU Instancing을 자동 처리하며, TerrainData API를 통해 런타임에 높이맵과 텍스처를 동적으로 수정할 수 있습니다.
1. TerrainData 런타임 수정
섹션 제목: “1. TerrainData 런타임 수정”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); }}2. 알파맵(텍스처 스플래팅)
섹션 제목: “2. 알파맵(텍스처 스플래팅)”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); }}3. 트리와 디테일 배치
섹션 제목: “3. 트리와 디테일 배치”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); }}4. 성능 최적화
섹션 제목: “4. 성능 최적화”// 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; } }}TerrainData.SetHeights와 SetAlphamaps로 런타임 지형 변형과 텍스처 페인팅이 가능합니다. 대규모 수정 후에는 Flush() 또는 씬 저장이 필요합니다. 성능은 heightmapPixelError(LOD), detailObjectDistance(풀 거리), treeDistance(트리 거리)를 카메라 거리에 따라 동적 조정하는 것으로 가장 효과적으로 개선할 수 있습니다.