Article Earth Science · ≈ ⏱ 11 min read

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.

TL;DR: Hydraulic erosion simulates raindrops rolling downhill across a heightmap, picking up sediment where water moves fast and depositing it where the flow slows down. Repeating this for hundreds of thousands of droplets automatically carves realistic V-shaped valleys, sharp ridgelines and branching river networks out of flat, generic noise — no hand-drawn terrain features required.

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.

Two families of algorithm

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
Direction update (with inertia) dir = dir · inertia − ∇h(pos) · (1 − inertia)

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:

Sediment capacity C = max(−Δh, minSlope) · speed · water · Kc

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:

Erosion / deposition rule if s > C: deposit (s − C) · depositRate of sediment onto the 4 surrounding cells (bilinear weights)

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.

Evaporation water(t + 1) = water(t) · (1 − evaporateRate)

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.

Talus / thermal erosion pass for each cell c with neighbour n:
  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.
Practical rule

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.

▶ Live Demo

🏔️ 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 →

🔗 Related Simulations

🏔️Terrain 🌍Tectonic Plates 🌊Ocean 💧Fluid