# 3. Trees, Details and the Flyweight Part of [[Terrain]]. Previous: [[2. Terrain Layers and Splat Rules]]. Next: [[4. Level Design with Terrain - Landmarks and Sightlines]]. > [!note] Verification status > `TerrainData.SetTreeInstances(TreeInstance[], bool snapToHeightmap)`, `treePrototypes`, `SetDetailLayer`, `detailWidth/detailHeight`, `DetailScatterMode` (CoverageMode / InstanceCountMode) checked on the 6000.5 Scripting Reference, 2026-11-10. The exact visual meaning of a detail density integer depends on the scatter mode and prototype density - the docs don't give a single number, so tune by eye. **Code not compiled in Unity.** ## 3.1 The Flyweight, literally Robert Nystrom's Flyweight chapter uses *a forest* as its example (see [[Game Programming Patterns (Review)]]). Unity's terrain is that chapter: | Flyweight role | Unity terrain | |---|---| | Intrinsic (shared, heavy) | `TreePrototype` - prefab, mesh, materials, LODs; `DetailPrototype` - grass texture or mesh | | Extrinsic (per instance, tiny) | `TreeInstance` - normalized position, prototype index, width/height scale, rotation, color | | Renderer that batches | Terrain tree/detail renderer, GPU instancing on detail meshes | A tree placed on the terrain is *not* a GameObject. 20,000 trees cost 20,000 small structs, not 20,000 Transforms. The trade-off: no per-tree scripts. If a tree needs behaviour (chop it, burn it), you either swap it for a GameObject when the player is near, or place that tree as a normal prefab. ## 3.2 Trees by hand 1. Terrain > **Paint Trees** > Edit Trees > Add Tree. Use a prefab with **LOD Group** (SpeedTree or mesh LODs). 2. Brush Size / Tree Density / Height variation. Shift-click erases. 3. **Mass Place Trees** for a quick fill, then erase clearings by hand. 4. Tree colliders: enable on the prototype prefab collider + Terrain Collider **Enable Tree Colliders**. Capsule colliders only; keep them cheap. ## 3.3 Script: rule-based tree scatter Places trees by slope and height bands, keeps them off the path layer, keeps clearings around marked points (spawn, landmark base, combat arenas), enforces approximate spacing. ```csharp // Assets/Scripts/Terrain/TerrainTreeScatter.cs using System.Collections.Generic; using UnityEngine; #if UNITY_EDITOR using UnityEditor; #endif [RequireComponent(typeof(Terrain))] public class TerrainTreeScatter : MonoBehaviour { public int seed = 7; [Min(1)] public int attempts = 20000; [Tooltip("Approximate minimum distance between trees, metres")] [Min(0.5f)] public float minSpacing = 4f; [Header("Where trees may grow")] public Vector2 slopeRangeDeg = new Vector2(0f, 30f); public Vector2 heightRange01 = new Vector2(0.06f, 0.6f); [Header("Keep off painted layers (e.g. path)")] public int excludeSplatLayer = 4; [Range(0f, 1f)] public float excludeThreshold = 0.3f; [Header("Clearings")] public Transform[] keepClearPoints; public float keepClearRadius = 15f; [Header("Variation")] public Vector2 scaleRange = new Vector2(0.8f, 1.3f); [ContextMenu("Scatter Trees")] public void Scatter() { Terrain terrain = GetComponent<Terrain>(); TerrainData data = terrain.terrainData; int protoCount = data.treePrototypes.Length; if (protoCount == 0) { Debug.LogWarning("Add at least one tree prototype first."); return; } var rng = new System.Random(seed); var placed = new List<TreeInstance>(attempts / 4); var occupied = new HashSet<Vector2Int>(); Vector3 size = data.size; Vector3 origin = terrain.GetPosition(); float[,,] alpha = null; int aw = data.alphamapWidth, ah = data.alphamapHeight; if (excludeSplatLayer >= 0 && excludeSplatLayer < data.alphamapLayers) alpha = data.GetAlphamaps(0, 0, aw, ah); for (int i = 0; i < attempts; i++) { float nx = (float)rng.NextDouble(); float nz = (float)rng.NextDouble(); float slope = data.GetSteepness(nx, nz); if (slope < slopeRangeDeg.x || slope > slopeRangeDeg.y) continue; float h01 = data.GetInterpolatedHeight(nx, nz) / size.y; if (h01 < heightRange01.x || h01 > heightRange01.y) continue; if (alpha != null) { int ax = Mathf.Clamp(Mathf.RoundToInt(nx * (aw - 1)), 0, aw - 1); int az = Mathf.Clamp(Mathf.RoundToInt(nz * (ah - 1)), 0, ah - 1); if (alpha[az, ax, excludeSplatLayer] > excludeThreshold) continue; } Vector3 world = origin + new Vector3(nx * size.x, 0f, nz * size.z); if (InClearing(world)) continue; // Grid hash spacing: one tree per cell, and no tree in neighbouring cells. var cell = new Vector2Int(Mathf.FloorToInt(world.x / minSpacing), Mathf.FloorToInt(world.z / minSpacing)); if (NeighbourOccupied(occupied, cell)) continue; occupied.Add(cell); float s = Mathf.Lerp(scaleRange.x, scaleRange.y, (float)rng.NextDouble()); placed.Add(new TreeInstance { position = new Vector3(nx, 0f, nz), // normalized; y is set by snapping prototypeIndex = rng.Next(protoCount), widthScale = s, heightScale = s, rotation = (float)(rng.NextDouble() * Mathf.PI * 2.0), color = Color.white, lightmapColor = Color.white }); } #if UNITY_EDITOR Undo.RegisterCompleteObjectUndo(data, "Scatter Trees"); #endif data.SetTreeInstances(placed.ToArray(), true); Debug.Log(quot;Placed {placed.Count} trees from {attempts} attempts."); } bool InClearing(Vector3 world) { if (keepClearPoints == null) return false; float r2 = keepClearRadius * keepClearRadius; foreach (Transform t in keepClearPoints) { if (t == null) continue; Vector2 d = new Vector2(world.x - t.position.x, world.z - t.position.z); if (d.sqrMagnitude < r2) return true; } return false; } static bool NeighbourOccupied(HashSet<Vector2Int> occupied, Vector2Int c) { for (int dx = -1; dx <= 1; dx++) for (int dz = -1; dz <= 1; dz++) if (occupied.Contains(new Vector2Int(c.x + dx, c.y + dz))) return true; return false; } } ``` **Explanation** - `TreeInstance.position` is **normalized** (0-1 across the tile). Passing `snapToHeightmap: true` sets the Y for you. - The spacing is a **Spatial Partition** (uniform grid hash). Checking the 3x3 neighbourhood gives spacing between `minSpacing` and ~2x `minSpacing` - fine for forests, not exact Poisson-disk sampling. - The clearing check is your level design talking to the generator: clearings are where fights, camps and landmarks are *read*. Trees block sightlines; part 4 measures that. ## 3.4 Details (grass, rocks, flowers) Details are drawn only within **Detail Distance**. They are the single biggest terrain cost on mobile. 1. Paint Details > Edit Details > **Add Detail Mesh** (preferred; use GPU instancing on its material) or **Add Grass Texture**. 2. Terrain Settings > **Detail Scatter Mode**: *Coverage* (values mean how much area to cover, scaled by the prototype's density) or *Instance Count* (values mean instance count per sample). Pick one per project and keep it. 3. Detail Distance 60-80 m on desktop; much lower on Quest (part 6). ## 3.5 Script: grass density from the splatmap Grass where the grass layer is strong, nothing on path/rock. ```csharp // Assets/Scripts/Terrain/TerrainGrassFromSplat.cs using UnityEngine; #if UNITY_EDITOR using UnityEditor; #endif [RequireComponent(typeof(Terrain))] public class TerrainGrassFromSplat : MonoBehaviour { public int detailLayer = 0; public int grassSplatLayer = 0; [Range(0f, 1f)] public float threshold = 0.5f; [Tooltip("Max value written to the detail map. Meaning depends on Detail Scatter Mode - tune by eye.")] public int maxValue = 8; [ContextMenu("Paint Grass From Splat")] public void Paint() { TerrainData data = GetComponent<Terrain>().terrainData; if (detailLayer >= data.detailPrototypes.Length) { Debug.LogWarning("No such detail prototype."); return; } if (grassSplatLayer >= data.alphamapLayers) { Debug.LogWarning("No such splat layer."); return; } int dw = data.detailWidth, dh = data.detailHeight; int aw = data.alphamapWidth, ah = data.alphamapHeight; float[,,] alpha = data.GetAlphamaps(0, 0, aw, ah); int[,] density = new int[dh, dw]; for (int y = 0; y < dh; y++) { for (int x = 0; x < dw; x++) { int ax = Mathf.RoundToInt(x / (float)(dw - 1) * (aw - 1)); int ay = Mathf.RoundToInt(y / (float)(dh - 1) * (ah - 1)); float a = alpha[ay, ax, grassSplatLayer]; density[y, x] = a < threshold ? 0 : Mathf.RoundToInt(Mathf.InverseLerp(threshold, 1f, a) * maxValue); } } #if UNITY_EDITOR Undo.RegisterCompleteObjectUndo(data, "Grass From Splat"); #endif data.SetDetailLayer(0, 0, detailLayer, density); } } ``` > [!tip] Keep detail maps square > Heights and alphamaps are documented as `[y, x]`. The `SetDetailLayer` docs example indexes its map as `[x, y]`. With a square detail resolution (the default) the difference can't bite you; with a non-square one, test with a single painted stripe first. ## 3.6 Exercise Run the scatter with and without the clearing points. Stand at the spawn with your character camera: in which version can you see the landmark? That question leads straight into part 4. ## Sources - `TerrainData.SetTreeInstances`: https://docs.unity3d.com/6000.5/Documentation/ScriptReference/TerrainData.SetTreeInstances.html - `TerrainData.SetDetailLayer`: https://docs.unity3d.com/6000.5/Documentation/ScriptReference/TerrainData.SetDetailLayer.html - `DetailScatterMode`: https://docs.unity3d.com/6000.5/Documentation/ScriptReference/DetailScatterMode.html - Trees: https://docs.unity3d.com/6000.5/Documentation/Manual/terrain-Trees.html - Grass and details: https://docs.unity3d.com/6000.5/Documentation/Manual/terrain-Grass.html - Flyweight: https://gameprogrammingpatterns.com/flyweight.html - Spatial Partition: https://gameprogrammingpatterns.com/spatial-partition.html