# 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