콘텐츠로 이동

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;
}
}
}

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