Grow Terrain from Local Height Rules
Binary caves ask:
wall or floor?
Terrain needs richer state.
Let each cell store a height:
0.0 = low
1.0 = high
Now local rules can smooth, raise, erode and classify terrain.
The challenge is not creating a pretty array.
It is creating a process whose output we can explain and control.
Start with a height field
import numpy as np
def random_heightmap(
rows=120,
cols=160,
seed=42,
):
rng = np.random.default_rng(seed)
return rng.random(
(rows, cols)
)
Raw independent noise contains variation but little large-scale geography.
Add a local mean
def local_mean(grid):
total = np.zeros_like(grid)
for dy in (-1, 0, 1):
for dx in (-1, 0, 1):
total += np.roll(
np.roll(
grid,
dy,
axis=0,
),
dx,
axis=1,
)
return total / 9.0
Blend toward the neighborhood:
def smooth_step(
height,
strength=0.35,
):
mean = local_mean(height)
return (
(1 - strength) * height
+ strength * mean
)
Repeated smoothing creates broad spatial regions.
But smoothing alone is not a terrain generator.
If we continue forever, it removes differences.
Add a competing process
Introduce persistent uplift:
uplift = np.zeros_like(height)
uplift[
30:92,
55:108,
] = 0.003
Then:
def terrain_step(
height,
uplift,
):
height = smooth_step(
height,
strength=0.35,
)
height = height + uplift
return np.clip(
height,
0.0,
1.0,
)
Now two influences compete:
smoothing
-> reduces sharp local differences
uplift
-> continually creates elevation
(Verified over 60 steps: values stay in range, and the uplifted region averages above the field mean — the competition is real, not notional.)
The renderer also applies a gentle radial edge falloff so the demonstration develops an island-like boundary.

Smoothing-plus-uplift sits in a real lineage: local erosion simulators such as thermal erosion move material downhill wherever slopes exceed a stability angle, using the same neighborhood machinery with a physical criterion instead of a blend factor. This chapter’s version is the gentler cousin — controlled texture rather than physical simulation — and should not be mistaken for the erosive kind.
Inspect the final height field directly

This is important: the primary generated object is the height field.
Water, plains, hills and mountains are interpretations derived from it.
Derive semantic terrain classes
WATER = 0
PLAINS = 1
HILLS = 2
MOUNTAINS = 3
def classify_height(height):
terrain = np.zeros_like(
height,
dtype=np.uint8,
)
terrain[
(height >= 0.35)
& (height < 0.55)
] = PLAINS
terrain[
(height >= 0.55)
& (height < 0.75)
] = HILLS
terrain[
height >= 0.75
] = MOUNTAINS
return terrain
This separates:
simulation / generation state:
continuous height
game-facing interpretation:
terrain class
That separation keeps the underlying process reusable.
Add a second continuous field
Height alone does not determine every world property.
Add moisture:
moisture = np.zeros_like(height)
moisture[:, :20] = 1.0
Diffuse it inland:
for _ in range(100):
moisture = (
moisture
+ 0.1 * laplacian(moisture)
)
moisture[:, :20] = 1.0
The diffusion rate 0.1 respects the stability limit from Chapter 12 (rate ≤ 1/4), and the ocean edge is re-imposed as a maintained source at every step.
Now each cell can be thought of as:
[height, moisture]
and biome classification can depend on both.
The grid is becoming a layered local state machine.
Global design goals should remain explicit
A game may require:
30-45% water
one large connected continent
flat spawn region
mountains away from spawn
river reaches ocean
Do not force a cellular smoothing rule to guarantee all of those.
Use:
local rules
-> organic structure
measurement
-> evaluate candidate
graph/search constraints
-> guarantee global requirements
This is the same lesson we learned from cave generation.
Measure the world
def terrain_stats(
height,
water_level=0.35,
):
return {
"mean_height": float(
height.mean()
),
"height_std": float(
height.std()
),
"water_fraction": float(
np.mean(
height < water_level
)
),
"mountain_fraction": float(
np.mean(
height > 0.75
)
),
}
Now seeds and parameter sets can be searched instead of judged only by screenshots.
One idea to keep
Terrain generation becomes easier to reason about when each component has an explicit role and an explicit scope:
| Component | Role | Models | Does not model |
|---|---|---|---|
| height field + smoothing | organic spatial texture | correlated terrain variation | real erosion physics |
| uplift + clipping | persistent elevation | highlands that survive smoothing | tectonic process |
| moisture diffusion | second interacting field | inland falloff for biomes | hydrology |
| semantic classes | game-facing interpretation | water/plains/hills/mountains | any dynamics at all |
| global constraints | design guarantees | connected continents, spawn rules | emergence |
Local CA-style rules are excellent at producing spatial texture.
They do not need to carry every high-level requirement themselves.
In the next chapter we will use the same local machinery without pretending to simulate a world at all: we will deliberately treat cellular rules as visual texture generators.
Research
Beneš, B. — Parallel Implementation of Terrain Erosion Applied to the Surface of Mars (AFRIGRAPH 2001). The cellular-erosion lineage this chapter’s smoothing belongs beside: per-cell neighborhood operations that move material above a stability threshold, processed in parallel over the height field. Read it to see what turns local smoothing into a physical claim — and what this chapter deliberately leaves out. https://cs.purdue.edu/homes/bbenes/papers/Benes01AFRIGRAPH.pdf
Zenil, H. & Martinez, G. J. — Cellular Automata (Scholarpedia). Covers continuous-valued extensions beyond the finite-state definition and the reaction-diffusion frame — the definitional context for treating height and moisture fields as legitimate cellular-automaton state. http://www.scholarpedia.org/article/Cellular_automata