# 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