---
name: terrain-erosion
description: >
  Build a single-file Three.js heightmap terrain: valley sculpt, stream-power
  incision, hydraulic droplets, thermal settling, downhill rills, a cliff fold,
  and a baked slope colour. Use when generating or changing procedural terrain,
  valleys, cliffs, gullies or erosion. The reference file is terrain.html
  beside this skill. If they disagree, the HTML wins.
---

# Terrain erosion

One HTML file. No bundler, no npm, no framework. Three.js 0.186.1 from a CDN via an import map (`three.webgpu.js`, `three.tsl.js`, addons). `WebGPURenderer` only: set `renderer._getFallback = null` and stop if `navigator.gpu` is missing. No WebGL fallback. No GLSL strings. `MeshLambertNodeMaterial`, baked albedo through a TSL `texture()` colour node, baked normal on `normalMap`. OrbitControls.

Open it through a server. `file://` will not load the ES modules. `php -S localhost:8000` or `npx serve` in the folder is enough.

Do not ship a texture pack unless the user hands you tileable photos. This file bakes colour. Photo triplanar splats are a later pass, not the default.

## Constants

- `SIM = 512` erosion grid
- `MESH = 1024` render grid. 1536 is the same code. Change `MESH` and nothing else.
- `TEX = 1024` baked colour
- `WORLD = 1400` width in world units
- Droplets default `130000`, life about 45, brush radius 3
- First seed `1978`, so a cold load is comparable. Regenerate picks a new seed.

## Order

Run these in this order on every regenerate. Do not merge passes. Do not drop one because the previous pass "already adds detail".

1. **Sculpt.** Seeded 2D simplex. FBM for the rolling base (`pow` the 0–1 FBM so plains stay flat). Ridged FBM for the two massifs, masked so they sit off a sine path down the middle. Lerp height toward a low path height inside the valley mask. Domain-warp the noise sample, or the ridges align to the grid.
2. **Stream power.** Braun & Willett 2013, implicit. For each cell, receiver is the steepest of 8 neighbours (diagonal distance `sqrt(2)`). Counting-sort cells high to low. Accumulate drainage area `A` high to low. Incise low to high so the receiver is already solved:

   `F = kdt * min(A, acap)^m / distance`
   `h = (h + F * h_receiver) / (1 + F)`
   then add a little uplift on the high ground, or the peaks wash flat.

   Defaults that behave: `kdt` 0.12, `m` 0.5, about 55 iterations, uplift under 1. A larger `kdt` stays stable and still looks wrong (knife ridges, no valley).
3. **Put the wash back.** Lerp the valley mask back toward the path height. Stream power will eat the floor. A walkable level needs that floor.
4. **Droplets.** Random start, inertia blended with the downhill gradient, sediment capacity from slope × speed × water, evaporate, die at the edge. Erode only when the slope is steep. `smooth(tan 40°, tan 50°, slope)` gates it. Below that, deposit or walk. Spread the take across a small radial brush. Flats that get scratched look like noise again.
5. **Thermal.** If a cell is steeper than the talus (about 65°, `tan` of that times the cell size), move material to the lowest 4-neighbour.
6. **Upsample** to `MESH` with bilinear samples from the sim. Add rill detail only on slopes: high frequency across the downhill vector, low frequency along it. Then a short stream-power pass at mesh resolution (no uplift, cap the drainage so one basin doesn't carve a canyon through the wash). Put the wash back again. Then Voronoi plate jitter and crack seams on steep ground, off the wash, for crags.
7. **Polish flats.** Blur, weighted by flatness, blur radius varying with noise (1 or 2 cells). A fixed-radius blur is what makes soft ground look like a filter. The wash is always polished.
8. **Fold.** After normals would have been computed from the height, push vertices on slopes past about 45° along the downhill XZ direction. Amplitude varies with noise. A heightmap cannot store an overhang. The mesh can. This is a cheat. Keep it small.
9. **Bake.** At `TEX`, from slope, concavity (blurred height minus height), wear (what droplets removed) and deposit (what they dropped):
   - steep, convex, worn: rock
   - low slope plus deposit, plus the valley mask, plus concentrated drainage: sand
   - the rest: grass, drier with altitude
   - store rock weight in alpha if a later pass needs it
   Cliff-facing texels should sample noise in across-slope / height space, or one column of texels smears down the wall.
10. **Dressing.** A few hundred instanced rocks on the valley margins, where the slope is in between flat and cliff. Distant cylinder rings with a ridged top, flat-shaded, no fog, as haze layers. They are silhouettes, not more terrain.

## Level design rules

- The path is sacred. Any pass that cuts downward has to be followed by "put the wash back".
- Erosion marks belong on ground the camera reads as rock. Not on the floor someone would walk.
- Gullies and rills point downhill. Isotropic noise on a slope reads as static.
- Uniform blur reads as CG. Vary the radius, or skip the blur on steep ground.
- The fold is a camera trick. It should read from the path. It will look wrong from under the cliff. Leave it anyway.
- Colour follows the sim (wear, deposit, slope). Do not paint a slope mask that ignores where the water went.

## What not to do

- Do not erode flats with droplets "for detail".
- Do not skip restoring the valley after stream power.
- Do not replace stream power with more droplets and call it a gully network.
- Do not add a build step.
- Do not load Poly Haven (or any) photos unless the user supplied the files. If they did, splat them triplanar by the baked rock weight, and keep the baked colour as a large-scale shade on top. Triplanar: three world-plane projections, blended by `abs(normal)`.
- Do not bump `MESH` and also "optimise" the second stream-power pass away. That pass is the fine gully network the 512 grid cannot represent.

## Live controls

Sun elevation, sun azimuth, fog distance and normal-map strength can change without a regenerate. Everything under sculpt and erosion waits for Regenerate. Say so in the UI.
