# 5. Runtime Terrain Queries and Deformation Part of [[Terrain]]. Previous: [[4. Level Design with Terrain - Landmarks and Sightlines]]. Next: [[6. Terrain Performance Mobile and Quest]]. > [!note] Verification status > `SetHeightsDelayLOD` ("does not update the LOD information... call TerrainData.SyncHeightmap") and `GetHeights`/`GetAlphamaps` checked on the 6000.5 Scripting Reference, 2026-11-10. Whether the `TerrainCollider` reflects `SetHeightsDelayLOD` changes before `SyncHeightmap` is **not verified** - the script syncs every frame it changed, which is the safe path. **Code not compiled in Unity.** ## 5.1 Surface type under a point Footsteps, impact effects and tyre grip all want "what is the ground here?" On terrain that means: which splat layer has the highest weight at this XZ. ```csharp // Assets/Scripts/Terrain/TerrainSurface.cs using UnityEngine; public static class TerrainSurface { /// Returns the index of the strongest Terrain Layer at a world position, or -1. /// Allocates a tiny array per call - fine for footsteps, cache the full map for per-frame use. public static int DominantLayer(Terrain terrain, Vector3 world, out TerrainLayer layer) { layer = null; if (terrain == null) return -1; TerrainData data = terrain.terrainData; Vector3 local = world - terrain.GetPosition(); int ax = Mathf.Clamp(Mathf.FloorToInt(local.x / data.size.x * data.alphamapWidth), 0, data.alphamapWidth - 1); int az = Mathf.Clamp(Mathf.FloorToInt(local.z / data.size.z * data.alphamapHeight), 0, data.alphamapHeight - 1); float[,,] sample = data.GetAlphamaps(ax, az, 1, 1); int best = -1; float bestWeight = -1f; for (int i = 0; i < data.alphamapLayers; i++) { if (sample[0, 0, i] > bestWeight) { bestWeight = sample[0, 0, i]; best = i; } } if (best >= 0) layer = data.terrainLayers[best]; return best; } } ``` Hook it into footsteps by mapping `TerrainLayer` assets to surface types: ```csharp // Assets/Scripts/Terrain/TerrainFootstepSurface.cs using System; using UnityEngine; public class TerrainFootstepSurface : MonoBehaviour { [Serializable] public struct LayerToSurface { public TerrainLayer layer; public string surfaceId; } public LayerToSurface[] mapping; public string fallbackSurface = "Dirt"; // Call from an Animation Event or your step timer. public string SurfaceUnderFeet() { Terrain t = Terrain.activeTerrain; // single-tile case; use a TerrainAt() lookup for grids if (TerrainSurface.DominantLayer(t, transform.position, out TerrainLayer layer) < 0) return fallbackSurface; foreach (LayerToSurface m in mapping) if (m.layer == layer) return m.surfaceId; return fallbackSurface; } } ``` Pass `surfaceId` to the footstep player in [[4. Footsteps and Surface Audio]] and to the impact service in [[3. Worked Effect - Impact Sparks with One Graph]] (dust on dirt, sparks on rock). ## 5.2 Never edit the asset at runtime: clone it `TerrainData` is an asset. Changing heights in Play mode **changes the asset on disk** in the editor, and the change survives exiting Play mode. Clone it at startup: ```csharp // Assets/Scripts/Terrain/RuntimeTerrainClone.cs using UnityEngine; [DefaultExecutionOrder(-1000)] [RequireComponent(typeof(Terrain))] public class RuntimeTerrainClone : MonoBehaviour { void Awake() { Terrain terrain = GetComponent<Terrain>(); TerrainData clone = Instantiate(terrain.terrainData); clone.name = terrain.terrainData.name + " (Runtime)"; terrain.terrainData = clone; TerrainCollider col = GetComponent<TerrainCollider>(); if (col != null) col.terrainData = clone; } } ``` The clone costs memory (a full copy of heights, splat, details). That's the price of deformable ground; budget for it on Quest. ## 5.3 Crater deformation (Dirty Flag) Explosions dig craters. Many explosions per frame should cost **one** LOD rebuild: write with `SetHeightsDelayLOD`, mark dirty, call `SyncHeightmap` once in `LateUpdate`. That's the **Dirty Flag** pattern from [[Game Programming Patterns (Review)]]. ```csharp // Assets/Scripts/Terrain/TerrainCrater.cs using UnityEngine; [RequireComponent(typeof(Terrain))] public class TerrainCrater : MonoBehaviour { public float defaultRadius = 6f; public float defaultDepth = 2f; Terrain terrain; bool dirty; void Awake() => terrain = GetComponent<Terrain>(); public void Blast(Vector3 worldPoint) => Blast(worldPoint, defaultRadius, defaultDepth); public void Blast(Vector3 worldPoint, float radius, float depth) { TerrainData data = terrain.terrainData; int res = data.heightmapResolution; Vector3 local = worldPoint - terrain.GetPosition(); float cellX = data.size.x / (res - 1); float cellZ = data.size.z / (res - 1); int cx = Mathf.RoundToInt(local.x / cellX); int cz = Mathf.RoundToInt(local.z / cellZ); int rx = Mathf.CeilToInt(radius / cellX); int rz = Mathf.CeilToInt(radius / cellZ); int x0 = Mathf.Clamp(cx - rx, 0, res - 1); int z0 = Mathf.Clamp(cz - rz, 0, res - 1); int x1 = Mathf.Clamp(cx + rx, 0, res - 1); int z1 = Mathf.Clamp(cz + rz, 0, res - 1); int w = x1 - x0 + 1; int h = z1 - z0 + 1; if (w <= 0 || h <= 0) return; float[,] heights = data.GetHeights(x0, z0, w, h); // [y, x] float depth01 = depth / data.size.y; for (int z = 0; z < h; z++) { for (int x = 0; x < w; x++) { float dx = (x0 + x - cx) * cellX; float dz = (z0 + z - cz) * cellZ; float d = Mathf.Sqrt(dx * dx + dz * dz) / radius; if (d >= 1f) continue; float bowl = 1f - d * d; // parabolic bowl heights[z, x] = Mathf.Max(0f, heights[z, x] - depth01 * bowl); } } data.SetHeightsDelayLOD(x0, z0, heights); dirty = true; } void LateUpdate() { if (!dirty) return; terrain.terrainData.SyncHeightmap(); dirty = false; } } ``` **Explanation and caveats** - Reads and writes only the affected rectangle - never the whole heightmap. - Trees standing in the crater keep their old height; remove or re-snap nearby `TreeInstance`s if it matters. - A baked NavMesh does not update. For deformable terrain, use NavMesh surface rebuild-by-area (see [[NavMesh in Unity 6 (AI Navigation 2.x)]]) or restrict deformation to cosmetic depth. - Multiplayer: deformation must be deterministic and replicated as events (position, radius, depth), never as heightmaps - see [[Multiplayer with Netcode for GameObjects]]. - Baked lighting doesn't follow; keep craters shallow or rely on realtime main light. ## 5.4 Exercise Wire a projectile from [[Physics 3D]] to call `Blast` on impact, play an impact VFX chosen by `TerrainSurface.DominantLayer`, and play a footstep-style sound from the same surface id. One query, three kinds of feedback. ## Sources - `TerrainData.SetHeightsDelayLOD`: https://docs.unity3d.com/6000.5/Documentation/ScriptReference/TerrainData.SetHeightsDelayLOD.html - `TerrainData.SyncHeightmap`: https://docs.unity3d.com/6000.5/Documentation/ScriptReference/TerrainData.SyncHeightmap.html - `TerrainData` (GetHeights, GetAlphamaps, terrainLayers): https://docs.unity3d.com/6000.5/Documentation/ScriptReference/TerrainData.html - Dirty Flag: https://gameprogrammingpatterns.com/dirty-flag.html