# 1. Terrain Setup and Heightmaps
Part of [[Terrain]]. Next: [[2. Terrain Layers and Splat Rules]].
> [!note] Verification status
> `heightmapResolution` clamping values, `SetHeights` ([y,x] indexing, 0-1 values) and `Terrain.CreateTerrainGameObject` checked on the 6000.5 Scripting Reference, 2026-11-10. Terrain Tools 5.3.3 = version released for 6000.5. **Code not compiled in Unity.**
## 1.1 Mental model
| Object | What it is | Lives where |
|---|---|---|
| `Terrain` component | The renderer: LOD, pixel error, tree/detail drawing distances | Scene |
| `TerrainData` asset | Heights, alphamaps (splat), tree instances, detail density maps, layer list | Project (`.asset`) |
| `TerrainCollider` | Physics heightfield that reads the *same* `TerrainData` | Scene |
| `TerrainLayer` asset | One texture set (albedo, normal, mask) + tiling | Project |
Two components, one data asset. That is why editing heights in Play mode edits the asset on disk (see [[7. Terrain Pitfalls]]).
**Heights are normalized.** Every heightmap sample is a float 0-1, multiplied by `terrainData.size.y`. A 600 m tall terrain with a sample of 0.5 is 300 m above the terrain's transform. Choose `size.y` first; changing it later rescales everything.
**Heightmap resolution vs size.** `heightmapResolution` is clamped to 33, 65, 129, 257, 513, 1025, 2049 or 4097 samples per side. Sample spacing = `size.x / (resolution - 1)`. A 1000 m tile at 513 gives ~1.95 m between samples; that is your smallest sculptable feature. A ditch narrower than ~4 m (two samples) will not exist.
| Use | Tile size | Heightmap | Spacing |
|---|---|---|---|
| Quest / mobile arena | 250-500 m | 257-513 | ~1 m |
| Desktop open area | 1000 m | 513-1025 | ~1-2 m |
| Big world | 1000 m tiles in a grid | 513-1025 per tile | Stream tiles via scenes ([[SceneManagement in Unity 6]]) |
## 1.2 Create a tile
1. **GameObject > 3D Object > Terrain.** Unity creates a `Terrain` GameObject and a `New Terrain.asset` in `Assets/`. Move the asset into `Assets/Terrain/Data/` immediately and rename it (`Island_TerrainData`).
2. Select the terrain, open the gear tab (**Terrain Settings**). Under **Mesh Resolution**: Terrain Width/Length 1000, Height 600. Under **Texture Resolutions**: Heightmap Resolution 513.
3. Set **Grouping ID** if you will have several tiles; **Auto Connect** stitches neighbors with the same ID (the 6000.5 manual's Terrain Settings page).
4. Position the transform so the playable centre is near the world origin (floating point precision matters more for VR physics and audio than you'd think).
## 1.3 Install Terrain Tools (optional but recommended)
Terrain Tools adds brushes (bridge, clone, noise, erosion, terrace, slope flatten), a Toolbox window (create/split tiles, import/export heightmaps), and multi-tile settings.
1. **Window > Package Manager > + > Add package by name**: `com.unity.terrain-tools`. Package Manager offers 5.3.3 on 6000.5 (the version listed as released for 6000.5 on 2026-11-10).
2. **Window > Terrain > Terrain Toolbox** for creation/import/export.
3. The extra brushes appear in the Terrain inspector's **Sculpt** dropdown.
> [!tip] Teaching order
> Have students sculpt the macro shape with **Raise/Lower** + **Smooth**, then **Set Height** for the playable plateau, then *only then* add Terrain Tools' noise and erosion for micro detail. Noise first hides the level design under texture.
## 1.4 Sculpting workflow for readable terrain
1. **Macro (10 min):** big brush (100+ m), low opacity. Block the landmark peak, the valley route, the basin where the climax happens.
2. **Playable shelves:** **Set Height** to flatten spawn, camps, arenas. Flat ground reads as "safe/usable".
3. **Edges:** steep slopes (> your character's max slope) are *walls*. Use them deliberately as boundaries instead of invisible colliders.
4. **Micro:** Terrain Tools **Noise** / **Hydraulic Erosion** at low strength.
5. **Walk it** with your real character controller at real metrics ([[5. Gray-Box Metrics Gym]]). Bake a NavMesh and look at what is reachable ([[NavMesh in Unity 6 (AI Navigation 2.x)]]).
## 1.5 Importing a heightmap
External tools (Gaea, World Machine, Houdini, a DEM from real geography) export **16-bit RAW**.
1. Terrain Settings > **Import Raw...** (or Terrain Toolbox > Import Heightmap).
2. Match **Depth** (16 bit), **Resolution** (must be one of the allowed sizes; 1025x1025 for a 1025 heightmap), **Byte Order** (Windows = little-endian is what most tools write), **Terrain Size**.
3. If the result is noise or stripes: wrong byte order or wrong resolution.
Keep the source RAW in the repo under LFS ([[2. Git LFS]]) so you can re-import after changing size.
## 1.6 Script: procedural shaper with a landmark and a plateau
A starting landscape you can regenerate with different seeds while you test layouts. It writes fBm noise, raises a smooth **landmark peak** at a world XZ position, and blends a **flat plateau** for the spawn. Put it on the Terrain GameObject and use the component's context menu (three dots > *Generate Heights*). Undo works in the editor.
```csharp
// Assets/Scripts/Terrain/ProceduralTerrainShaper.cs
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
[RequireComponent(typeof(Terrain))]
public class ProceduralTerrainShaper : MonoBehaviour
{
[Header("Noise (fBm)")]
public int seed = 1234;
[Min(1)] public int octaves = 5;
[Tooltip("Cycles per world metre for the first octave.")]
public float baseFrequency = 0.004f;
[Range(0f, 1f)] public float persistence = 0.5f;
public float lacunarity = 2f;
[Range(0f, 1f)] public float baseHeight = 0.05f;
[Range(0f, 1f)] public float noiseAmplitude = 0.25f;
[Header("Landmark peak (world XZ)")]
public Vector2 peakWorldXZ = new Vector2(500f, 700f);
public float peakRadius = 120f;
[Range(0f, 1f)] public float peakHeight = 0.9f;
[Header("Spawn plateau (world XZ)")]
public Vector2 plateauWorldXZ = new Vector2(200f, 200f);
public float plateauRadius = 60f;
public float plateauBlend = 30f;
[Range(0f, 1f)] public float plateauHeight = 0.08f;
[ContextMenu("Generate Heights")]
public void Generate()
{
Terrain terrain = GetComponent<Terrain>();
TerrainData data = terrain.terrainData;
int res = data.heightmapResolution;
Vector3 size = data.size;
Vector3 origin = terrain.GetPosition();
#if UNITY_EDITOR
Undo.RegisterCompleteObjectUndo(data, "Generate Terrain Heights");
#endif
// Heights are indexed [y, x] where y runs along world Z.
float[,] heights = new float[res, res];
var rng = new System.Random(seed);
float offsetX = rng.Next(-10000, 10000);
float offsetZ = rng.Next(-10000, 10000);
for (int z = 0; z < res; z++)
{
for (int x = 0; x < res; x++)
{
float wx = origin.x + (x / (float)(res - 1)) * size.x;
float wz = origin.z + (z / (float)(res - 1)) * size.z;
float h = baseHeight + noiseAmplitude * Fbm(wx + offsetX, wz + offsetZ);
// Landmark: smoothstep dome, max() so it never digs a hole.
float dPeak = Vector2.Distance(new Vector2(wx, wz), peakWorldXZ);
float p = Mathf.Clamp01(1f - dPeak / peakRadius);
p = p * p * (3f - 2f * p);
h = Mathf.Max(h, p * peakHeight);
// Plateau: blend towards a constant height near the spawn.
float dPlat = Vector2.Distance(new Vector2(wx, wz), plateauWorldXZ);
float t = Mathf.InverseLerp(plateauRadius + plateauBlend, plateauRadius, dPlat);
h = Mathf.Lerp(h, plateauHeight, t);
heights[z, x] = Mathf.Clamp01(h);
}
}
data.SetHeights(0, 0, heights);
}
float Fbm(float x, float z)
{
float sum = 0f, amp = 1f, freq = baseFrequency, norm = 0f;
for (int i = 0; i < octaves; i++)
{
sum += amp * Mathf.PerlinNoise(x * freq, z * freq);
norm += amp;
amp *= persistence;
freq *= lacunarity;
}
return sum / norm; // 0..1
}
void OnDrawGizmosSelected()
{
Terrain terrain = GetComponent<Terrain>();
if (terrain == null || terrain.terrainData == null) return;
float top = terrain.GetPosition().y + terrain.terrainData.size.y;
Gizmos.color = Color.yellow;
Gizmos.DrawWireSphere(new Vector3(peakWorldXZ.x, top * peakHeight, peakWorldXZ.y), peakRadius);
Gizmos.color = Color.cyan;
Gizmos.DrawWireSphere(new Vector3(plateauWorldXZ.x, top * plateauHeight, plateauWorldXZ.y), plateauRadius);
}
}
```
**Why it is written this way**
- World-space sampling (not index-space) means the same seed produces matching noise across neighboring tiles, so seams line up.
- `Mathf.Max` for the peak preserves any higher noise around it; `Lerp` for the plateau guarantees flat, predictable spawn ground.
- `Undo.RegisterCompleteObjectUndo` on the `TerrainData` is what makes Ctrl+Z work after a destructive write.
- `Mathf.PerlinNoise` is fine for editor-time generation. For runtime generation of large maps, move the loop into a Burst job (see [[2. Jobs and Burst for GameObject Projects]]).
## 1.7 Script: create a tile from code
Useful for test scenes and teaching (students see exactly what a terrain is made of).
```csharp
// Assets/Editor/CreateTerrainTileMenu.cs
using UnityEditor;
using UnityEngine;
public static class CreateTerrainTileMenu
{
[MenuItem("Tools/Terrain/Create 500m Test Tile")]
static void Create()
{
var data = new TerrainData
{
heightmapResolution = 513, // clamped to an allowed value by Unity
size = new Vector3(500f, 150f, 500f)
};
data.SetDetailResolution(512, 32);
data.alphamapResolution = 512;
AssetDatabase.CreateAsset(data, AssetDatabase.GenerateUniqueAssetPath("Assets/TestTile_TerrainData.asset"));
GameObject go = Terrain.CreateTerrainGameObject(data);
go.name = "TestTile";
Undo.RegisterCreatedObjectUndo(go, "Create Terrain Tile");
Selection.activeGameObject = go;
}
}
```
Note the order: set `heightmapResolution` **before** `size`, because changing the resolution can rescale size in some versions (behaviour unverified on 6000.5; setting resolution first is the safe habit).
## 1.8 Exercise
Sculpt a 500 m island by hand with one peak, one plateau, one valley route between them. Then reproduce it with `ProceduralTerrainShaper` and compare: what did your hands do that the noise could not? (Usually: the route.)
## Sources
- `TerrainData` (heightmapResolution clamping): https://docs.unity3d.com/6000.5/Documentation/ScriptReference/TerrainData.html
- `TerrainData.SetHeights`: https://docs.unity3d.com/6000.5/Documentation/ScriptReference/TerrainData.SetHeights.html
- `Terrain.CreateTerrainGameObject`: https://docs.unity3d.com/6000.5/Documentation/ScriptReference/Terrain.CreateTerrainGameObject.html
- Terrain settings (Grouping ID, Auto Connect, Pixel Error): https://docs.unity3d.com/6000.5/Documentation/Manual/terrain-OtherSettings.html
- Heightmaps: https://docs.unity3d.com/6000.5/Documentation/Manual/terrain-Heightmaps.html
- Terrain Tools 5.3 manual: https://docs.unity3d.com/Packages/
[email protected]/manual/index.html