Guide

Game terrain generation explained

Terrain generation is the pipeline that turns abstract height and material data into walkable hills, valleys, cliffs, and beaches players traverse. Whether you sculpt a single island in an editor or stream infinite chunks from layered noise, the same building blocks appear: a heightmap (or voxel field) defines elevation, a mesh tessellates that data into triangles, splat maps blend grass, rock, and snow textures, and separate collision geometry keeps physics stable. Terrain sits at the intersection of procedural generation, level design, and performance engineering — get it wrong and players fall through the world, get stuck on invisible walls, or stare at repetitive Perlin blobs. This guide covers heightmap sources, noise layering, mesh and LOD strategies, biome assignment, erosion passes, chunk streaming, navmesh baking, and a production checklist.

Heightmaps: the foundation

A heightmap is a 2D grid where each cell stores elevation — usually a 16-bit grayscale image or a float array. Higher resolution means finer detail but more vertices and memory. Common workflows:

  • Authored — artists paint height in World Machine, Gaea, Blender, or Unity Terrain Tools. Full creative control; fixed world size unless you tile carefully.
  • Procedural — code fills the grid from noise functions and rules. Infinite or very large worlds; harder to guarantee specific landmarks without constraint layers.
  • Hybrid — procedural base with stamped mountains, rivers carved by spline tools, and hand-placed hero peaks. Most shipped open-world games use this mix.

Store heights in world units (meters) or normalized 0–1 values scaled at mesh build time. Normalized storage compresses well and makes blending between chunks easier, but document your vertical scale — a 0.01 height delta at 4 km world width is a gentle slope; at 400 m width it is a cliff.

Voxel terrains (Minecraft, Teardown) replace the heightmap with a 3D density field. You can overhang and carve caves, but mesh extraction (marching cubes, dual contouring) and collision are heavier. Heightmap terrain is still the default for third-person action games and flight sims where the playable surface is mostly a 2.5D shell.

Layered noise: from flat blobs to believable hills

Raw Perlin or Simplex noise looks like smooth, repetitive hills. Production terrain stacks multiple octaves (frequency layers) with decreasing amplitude:

  • Base layer — low frequency, large amplitude: continent-scale hills.
  • Detail layers — higher frequency, lower amplitude: ridges and bumps.
  • Masks — separate noise channels control where detail applies (e.g. only above a certain elevation).

Fractal Brownian motion (fBm) sums octaves with a persistence factor (typically 0.4–0.6). Ridged multifractal noise inverts absolute values to create sharp mountain ridges — useful for alpine biomes. Domain warping feeds noise coordinates into another noise offset, breaking grid-aligned artifacts that make procedural terrain look obviously synthetic.

Always use a seeded RNG so the same world regenerates identically — critical for multiplayer, saves, and debugging. Version your noise parameters; changing persistence between builds invalidates every stored chunk hash.

From heightfield to mesh

The classic approach tessellates each heightmap cell into two triangles. Vertex count equals (width - 1) × (height - 1) × 2 triangles. A 512×512 heightmap is ~524k triangles — too heavy for one draw call on mobile. Mitigations:

  • Chunking — split the world into tiles (e.g. 64×64 or 128×128 cells per chunk). Generate, render, and unload chunks around the player.
  • LOD — distant chunks use coarser height sampling. See LOD systems for screen-space metrics and pop-in fixes.
  • Skirts — vertical geometry strips along chunk edges hide T-junction cracks when adjacent LOD levels differ.
  • Normal and tangent recalculation — after stitching chunks, recompute normals at shared edges or lighting seams appear.

Collision meshes often use a lower resolution than render meshes — half or quarter sample rate is common. Players should not notice visual-vs-physics mismatch on gentle slopes; on stair-stepped collision, characters jitter. Test with your character controller step height and capsule radius.

Splat maps and material blending

A single grass texture stretched across a mountain looks flat. Splat maps (or weight maps) store per-texel blend weights for ground materials — typically RGBA channels map to four layers (grass, dirt, rock, snow). Rules assign weights:

  • Slope — steep faces get rock; flat areas get grass.
  • Elevation — snow above a height threshold.
  • Moisture / temperature noise — secondary biomes within the same elevation band.
  • Distance to water — sand or mud near shorelines.

Blend shaders sample splat weights and tile textures at different scales (macro variation breaks repetition). Triplanar projection reduces stretching on cliffs. Keep draw calls in mind: four-layer splatting in one pass beats four separate terrain materials with alpha blending overdraw.

Erosion and hydraulic carving

Pure noise mountains look too smooth and symmetric. Thermal erosion moves material downslope when local slope exceeds a talus angle — sharp peaks shed debris into valleys. Hydraulic erosion simulates rain droplets carrying sediment: water picks up soil, deposits it in flats, and carves river channels over thousands of iterations.

Erosion is expensive offline (minutes per 2k heightmap in desktop tools) but transforms believability. For runtime generation, precompute erosion kernels or use cheaper approximation passes (blur valleys, sharpen ridges). River splines — designer-drawn curves that subtract a U-shaped channel from the heightfield — give readable navigation landmarks and quest paths without full fluid simulation.

Biomes, vegetation, and props

Biomes are regions with shared rules: which trees spawn, grass density, ambient audio, enemy tables. Assign biomes from height + moisture + temperature (Whittaker diagram logic) or from Voronoi cells for hard borders. Poisson disk sampling scatters trees and rocks with minimum spacing so props do not overlap.

Separate macro placement (forest density map) from micro placement (individual mesh instances). Macro can be low-res; micro runs when a chunk loads. Use hierarchical instancing (GPU draw indirect) for thousands of identical trees. Props should respect slope limits — no upright pines on 60-degree cliffs unless that is intentional comedy.

Tie terrain to gameplay: chokepoints on ridge lines, cover in rocky outcrops, visibility from peak height. Terrain is not just visuals; it shapes pathfinding cost maps and line-of-sight for AI perception.

Streaming, persistence, and multiplayer

Open worlds stream chunks in a radius around the player. Each chunk needs:

  1. Deterministic generation from (seed, chunkX, chunkZ, version).
  2. Async build on a worker thread — never block the main thread on mesh creation.
  3. LRU cache with memory budget; evict far chunks and destroy physics bodies.
  4. Optional disk cache of baked meshes for faster reload (invalidate on generator version bump).

Multiplayer requires identical terrain on all clients. Server-authoritative chunk seeds or a shared world seed plus agreed algorithm version prevents desync. If players can modify terrain (building, mining), store deltas per chunk in the save file or replicate edit operations, not full heightmaps every frame.

Navmesh, water, and edge cases

AI navigation needs a navmesh baked from walkable surfaces. Steep slopes above your agent's maxSlope angle are excluded. Recalculate navmesh per chunk on load, or use a hierarchical navmesh with off-mesh links for jumps. Water volumes are separate collision triggers; shorelines need careful height sampling so characters do not snag on triangle edges at the waterline.

Test boundary conditions: world edges (invisible walls vs wrap), floating-point precision at coordinates far from origin (consider a floating origin that recenters the world around the player), and underground tunnels that break the heightmap assumption — those need separate interior meshes or voxel carving.

Production checklist

  1. Pick heightmap resolution per chunk target (e.g. 129×129 vertices for 128 quads) and document world scale in meters.
  2. Implement seeded layered noise with versioned parameters; log seed and version in saves.
  3. Build render mesh, collision mesh, and navmesh at appropriate LOD tiers — do not share one ultra-dense mesh for all three.
  4. Stitch chunk edges with skirts or shared border vertices; verify normals and splat weights match across seams.
  5. Assign materials via slope, height, and moisture rules; validate triplanar or UV tiling on cliffs.
  6. Run erosion or river carving offline for hero regions; use cheaper approximations for infinite runtime terrain.
  7. Scatter vegetation with Poisson sampling and slope filters; budget instance counts per chunk for target hardware.
  8. Stream chunks asynchronously with a memory cap; profile main-thread stalls during load spikes.
  9. Bake navmesh per chunk after collision is ready; test agent paths along chunk borders.
  10. Playtest traversal with the real character controller — invisible walls and fall-through bugs surface only in motion.

Key takeaways

  • Heightmaps are the universal starting point; voxels add overhangs at higher cost.
  • Layered noise plus masks beats single-octave Perlin for believable variety.
  • Chunking and LOD are mandatory for large worlds — one giant mesh does not ship.
  • Splat maps and erosion sell the illusion; raw noise alone looks procedural.
  • Collision, navmesh, and render meshes are related but not identical — generate each deliberately.

Related reading