Hydraulic Erosion: How Rivers Carve Mountains
A raw Perlin-noise heightmap (a randomly generated, smoothly varying elevation map) looks like crumpled foil — bumpy but lifeless. Hydraulic erosion is the algorithm that turns it into a believable landscape: simulated raindrops pick up sediment on the way down, carve valleys, and deposit material where the water slows — the same physical process that shaped every river delta and canyon on Earth.
1. Why raw noise isn't enough
A heightmap generated purely from Perlin noise / fBm has a fatal visual tell: every hill looks like every other hill. Real terrain is anisotropic — water always flows downhill along the steepest path, so mountains accumulate sharp ridgelines and river valleys carved over millions of years, while noise-only terrain has no directional memory at all.
Hydraulic erosion fixes this by literally simulating water moving across the terrain, picking up and depositing sediment as it goes. Run a few hundred thousand simulated raindrops over a heightmap and ridgelines sharpen, valleys deepen, and dendritic (tree-like) river networks emerge entirely from the physics — no hand-authored branching rules required.
Grid-based models (Musgrave et al., 1989) track water and sediment volume per heightmap cell and are easy to run on the GPU. Particle/droplet-based models (Beyer, 2015; Hjelle, 2017) simulate individual raindrops as they roll downhill — simpler to implement, and the approach this article focuses on.
2. The droplet erosion model
Each droplet is a small simulated particle with a position, direction, speed, water volume and sediment load. On every step it:
- Samples the height and gradient of the terrain under it (bilinear interpolation between the 4 surrounding grid cells)
- Updates its direction — partly following the downhill gradient, partly keeping its previous momentum (inertia)
- Moves one step in that direction
- Computes how much sediment the water can carry at its new speed (the sediment capacity)
- Either erodes the terrain (if under capacity) or deposits sediment (if over capacity)
- Loses a fraction of its water to evaporation
where ∇h is the terrain gradient at the droplet's position and inertia ∈ [0, 1] (typically 0.05) trades responsiveness for smoother, more natural-looking channels
A droplet dies when it evaporates completely, flows off the edge of the map, or exceeds a maximum lifetime (typically 30–60 steps). Millions of droplets, each cheap and independent, are simulated to erode the whole map.
3. Sediment capacity and the transport equation
The core physical idea, borrowed from real fluvial geomorphology, is that fast, deep water carries more sediment than slow, shallow water. The droplet's sediment capacity at each step is:
where: Δh — height change along the step (negative = downhill), minSlope — floor value preventing capacity collapsing to zero on flat ground, speed — droplet velocity, water — remaining water volume, Kc — sediment capacity constant (tuning parameter)
This single formula explains most river geomorphology at a glance: steep upper reaches move fast and erode aggressively, carving V-shaped valleys; as the slope flattens near the coast, capacity drops and the river deposits its load, building deltas and floodplains.
4. Erosion, deposition and evaporation
Comparing current sediment load s against capacity
C decides whether the droplet erodes or deposits:
if s < C: erode min((C − s) · erodeRate, −Δh) from the terrain under the droplet, distributed across a small brush radius, and add it to s
Eroding through a brush (a soft circular falloff, typically radius 2–4 cells) rather than a single point avoids the spiky, single-pixel artefacts that plagued early implementations and produces smoother channel walls.
typical evaporateRate ≈ 0.01 per step — the droplet gradually dries up, forcing it to deposit any remaining sediment before it vanishes
5. Thermal erosion — talus and scree
Hydraulic erosion alone can leave slopes steeper than any real material can sustain — cliffs of loose soil at 80°. Real hillsides are also shaped by thermal (mass-wasting) erosion: freeze-thaw cycles and gravity slowly move loose material downhill until every slope obeys the angle of repose — the maximum slope a granular material can hold before it slides.
if slope(c, n) > tanThresholdAngle:
Δmove = (slope − threshold) · c · settlingRate
move Δmove of material from c to n
Applied as a cheap post-process after hydraulic erosion, thermal erosion rounds off unrealistically sharp cliffs and produces the characteristic scree slopes (loose rock aprons) seen at the base of real mountains.
6. Pseudocode
One droplet's full lifetime (simplified from Hjelle's 2017 implementation):
function simulateDroplet(heightmap, startPos):
pos = startPos
dir = { x: 0, y: 0 }
speed = 1
water = 1
sediment = 0
for step in 0..maxLifetime:
// 1. Sample height + gradient (bilinear)
[height, gradient] = sampleHeightAndGradient(heightmap, pos)
// 2. Update direction with inertia, then move
dir = dir * inertia - gradient * (1 - inertia)
dir = normalize(dir)
pos += dir
if outOfBounds(pos): break
// 3. Compute height difference after the move
newHeight = sampleHeight(heightmap, pos)
deltaH = newHeight - height
// 4. Sediment capacity for this step
capacity = max(-deltaH, minSlope) * speed * water * Kc
if sediment > capacity or deltaH > 0:
// Deposit excess sediment
amount = deltaH > 0
? min(deltaH, sediment)
: (sediment - capacity) * depositRate
sediment -= amount
deposit(heightmap, pos, amount)
else:
// Erode terrain, capped by the height drop available
amount = min((capacity - sediment) * erodeRate, -deltaH)
erode(heightmap, pos, amount, brushRadius)
sediment += amount
// 5. Update speed and evaporate water
speed = sqrt(speed*speed + deltaH * gravity)
water *= (1 - evaporateRate)
if water < minWaterThreshold: break
7. Scaling it up: grid-based and GPU erosion
The droplet model above is embarrassingly parallel across droplets, but two droplets writing to the same cell at the same time is a race condition — awkward on a GPU. Two common solutions:
- Batched CPU droplets: simulate droplets sequentially on the CPU (or in small non-overlapping batches), which is what most offline terrain tools (World Machine, Gaea) do for a 1024×1024 heightmap in a few seconds.
- Grid-based shallow water erosion (Mei, Decaudin & Hu, 2007): instead of discrete droplets, store a continuous water-height field over the whole grid and step it forward with a simplified shallow-water equation — this maps naturally onto WebGL fragment shaders / compute shaders and erodes the entire terrain in parallel every frame.
For real-time in-browser terrain (like this site's WebGL demos), grid-based shallow-water erosion running as a ping-pong render-target pass is the standard choice — droplet erosion is easier to reason about but harder to parallelise on the GPU.
🏔️ Try the procedural terrain simulation
Interactive WebGL terrain built from layered Perlin noise — explore the ridgelines and valleys the erosion algorithm on this page would carve.
Open simulation →