# 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