# 2. Terrain Layers and Splat Rules
Part of [[Terrain]]. Previous: [[1. Terrain Setup and Heightmaps]]. Next: [[3. Trees Details and the Flyweight]].
> [!note] Verification status
> `GetSteepness` (degrees, normalized coords), `GetInterpolatedHeight`, `SetAlphamaps`, `alphamapLayers`, and the URP Terrain Lit options (Enable Height-based Blend, Height Transition, Enable Per-pixel Normal which needs Draw Instanced) checked on 6000.5 docs, 2026-11-10. Layer-count cost per pass on mobile is **not verified** here - measure. **Code not compiled in Unity.**
## 2.1 Terrain Layers
A **Terrain Layer** (`Create > Terrain Layer`) is an asset: Diffuse (albedo), Normal Map, Mask Map (R metallic, G AO, B height, A smoothness), tiling size/offset. Because it is an asset, the same "Grass" layer can be shared by every tile - another Flyweight.
The **splatmap** (alphamap) stores, per texel, a weight per layer. Weights at a texel should sum to 1. `TerrainData.alphamapResolution` sets its size; 512 on a 1 km tile is ~2 m per texel.
Steps:
1. Create layers: `TL_Grass`, `TL_Dirt`, `TL_Rock`, `TL_Snow`, `TL_Path`. Assign textures. Set **Tile Size** to the real-world size of the texture (grass 4 m, rock 8 m).
2. Terrain > **Paint Texture** > Edit Terrain Layers > Add Layer. The first layer floods the terrain.
3. Paint the **path** layer by hand along the route you designed in part 4. Paths are design, not noise - never auto-generate them.
## 2.2 URP Terrain Lit settings that matter
On the terrain's material (URP Terrain Lit):
- **Enable Height-based Blend**: URP renders only the layer with the greatest height (Mask Map blue channel) at each pixel, with a **Height Transition** in world units. Gives crisp rock-in-grass edges instead of mushy cross-fades.
- **Enable Per-pixel Normal**: samples the normal map Unity generates from the heightmap per pixel, so a low-res mesh still lights crisply. Works **only with Draw Instanced** on in Terrain Settings.
## 2.3 Script: data-driven auto-splat
Rules: "rock where slope > 35 deg", "snow above 75% height", "grass on flat low ground", "dirt as a fallback". Each rule is a band in height and slope with a soft edge. The script preserves hand-painted **locked** layers (your path) by not overwriting texels where that layer is already strong.
```csharp
// Assets/Scripts/Terrain/TerrainAutoSplat.cs
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
[System.Serializable]
public class SplatRule
{
public string label = "Grass";
[Tooltip("Index into TerrainData.terrainLayers")]
public int layerIndex;
[Tooltip("Normalized height band (0 = terrain base, 1 = terrain size.y)")]
public Vector2 heightRange01 = new Vector2(0f, 1f);
[Tooltip("Slope band in degrees")]
public Vector2 slopeRangeDeg = new Vector2(0f, 90f);
[Min(0f)] public float weight = 1f;
}
[RequireComponent(typeof(Terrain))]
public class TerrainAutoSplat : MonoBehaviour
{
public SplatRule[] rules =
{
new SplatRule { label = "Grass", layerIndex = 0, heightRange01 = new Vector2(0f, 0.6f), slopeRangeDeg = new Vector2(0f, 25f) },
new SplatRule { label = "Dirt", layerIndex = 1, heightRange01 = new Vector2(0f, 0.75f), slopeRangeDeg = new Vector2(20f, 40f), weight = 0.7f },
new SplatRule { label = "Rock", layerIndex = 2, heightRange01 = new Vector2(0f, 1f), slopeRangeDeg = new Vector2(35f, 90f) },
new SplatRule { label = "Snow", layerIndex = 3, heightRange01 = new Vector2(0.75f, 1f), slopeRangeDeg = new Vector2(0f, 40f) },
};
[Tooltip("Soft edge of the height band, normalized")]
public float heightBlend = 0.03f;
[Tooltip("Soft edge of the slope band, degrees")]
public float slopeBlend = 5f;
[Header("Preserve hand painting")]
[Tooltip("Layer to keep where already painted (e.g. path). -1 = none")]
public int lockedLayer = 4;
[Range(0f, 1f)] public float lockedThreshold = 0.25f;
[ContextMenu("Apply Splat Rules")]
public void Apply()
{
TerrainData data = GetComponent<Terrain>().terrainData;
int layers = data.alphamapLayers;
int w = data.alphamapWidth;
int h = data.alphamapHeight;
if (layers == 0) { Debug.LogWarning("Add Terrain Layers first."); return; }
#if UNITY_EDITOR
Undo.RegisterCompleteObjectUndo(data, "Auto Splat");
#endif
float[,,] existing = data.GetAlphamaps(0, 0, w, h);
float[,,] maps = new float[h, w, layers];
float[] weights = new float[layers];
for (int y = 0; y < h; y++)
{
for (int x = 0; x < w; x++)
{
if (lockedLayer >= 0 && lockedLayer < layers && existing[y, x, lockedLayer] >= lockedThreshold)
{
for (int i = 0; i < layers; i++) maps[y, x, i] = existing[y, x, i];
continue;
}
float nx = x / (float)(w - 1);
float ny = y / (float)(h - 1);
float height01 = data.GetInterpolatedHeight(nx, ny) / data.size.y;
float slope = data.GetSteepness(nx, ny);
System.Array.Clear(weights, 0, layers);
float total = 0f;
foreach (SplatRule rule in rules)
{
if (rule.layerIndex < 0 || rule.layerIndex >= layers) continue;
float wH = Band(height01, rule.heightRange01, heightBlend);
float wS = Band(slope, rule.slopeRangeDeg, slopeBlend);
float wgt = wH * wS * rule.weight;
weights[rule.layerIndex] += wgt;
total += wgt;
}
if (total <= 0f) { weights[0] = 1f; total = 1f; } // fallback: first layer
for (int i = 0; i < layers; i++) maps[y, x, i] = weights[i] / total;
}
}
data.SetAlphamaps(0, 0, maps);
}
// 1 inside [min,max], ramps to 0 over 'blend' outside it.
static float Band(float v, Vector2 range, float blend)
{
float lower = Mathf.InverseLerp(range.x - blend, range.x, v);
float upper = Mathf.InverseLerp(range.y + blend, range.y, v);
return Mathf.Min(lower, upper);
}
}
```
**How to use**
1. Add layers in this order: Grass (0), Dirt (1), Rock (2), Snow (3), Path (4).
2. Hand-paint the Path layer.
3. Add `TerrainAutoSplat` to the terrain, tweak bands, context menu > *Apply Splat Rules*. Re-run every time you re-sculpt; the path survives.
**Explanation**
- Alphamap arrays are `[y, x, layer]`, with y along world Z - same convention as heights.
- `GetSteepness` returns degrees for normalized coordinates, which is the same unit your controller's slope limit uses. Connect the two: a "rock" band starting at your max walkable slope tells players visually *you can't climb this* (a Level Up! readability win - texture as signage).
- The rules are a **Type Object**: designers add a rule, not a code path.
## 2.4 Exercise
Set the rock band start equal to the NavMesh Agent's **Max Slope**. Bake a NavMesh. Does every rock-textured area fall outside the NavMesh? If not, your visuals are lying to the player.
## Sources
- `TerrainData.GetSteepness`: https://docs.unity3d.com/6000.5/Documentation/ScriptReference/TerrainData.GetSteepness.html
- `TerrainData.SetAlphamaps`: https://docs.unity3d.com/6000.5/Documentation/ScriptReference/TerrainData.SetAlphamaps.html
- Terrain Layer: https://docs.unity3d.com/6000.5/Documentation/Manual/class-TerrainLayer.html
- URP Terrain Lit shader: https://docs.unity3d.com/6000.5/Documentation/Manual/urp/shader-terrain-lit.html