Lagrangian particle-based hydraulic erosion simulation with momentum-coupled meandering, thermal diffusion, and a dual-layer bedrock–sediment terrain model. Water droplets traverse a heightmap, eroding bedrock, transporting sediment, and depositing material to produce realistic fluvial landforms such as drainage basins, meandering rivers, alluvial fans, and meander scarring.
- p5.js 2.2.3 (2D & WebGL 3D rendering)
- Web Worker (hydraulic + thermal solver)
- GLSL shaders (3D terrain mesh with dynamic lighting)
- Tweakpane 4.x (parameter interface)
- Gradient-driven droplet advection with momentum-coupled velocity.
- Capacity-limited erosion and deposition on separate bedrock and sediment layers.
- Exponential moving-average discharge and momentum fields for stream persistence.
- Thermal erosion via eight-neighbour slope relaxation.
- Perlin-noise terrain generation with octave control.
- Fluvia.js — p5.js lifecycle and configuration
- core/AppCore.js — Simulation lifecycle, worker management, and state coordination
- worker/FluviaWorker.js — Worker-side hydraulic and thermal erosion solver
- model/Terrain.js — Typed-array terrain field storage and noise generation
- render/Renderer.js — 2D pixel rendering and 3D shader-based terrain mesh
- render/Camera.js — Quaternion-based 3D orbit camera
- math/Quaternion.js — Quaternion algebra for camera rotation
- analysis/Analyser.js — Topographic, hydrological, and mass-balance statistics
- ui/GUI.js — Tweakpane parameter interface (5 tabs)
- ui/InputHandler.js — Keyboard, mouse, and touch input
- media/Media.js — Image, video, heightmap, and world-state import/export
- model/RLECodec.js — Run-length encoding for Float32Array map serialisation
Fluvia/
├─ index.html
├─ Fluvia.js
├─ core/
│ └─ AppCore.js
├─ model/
│ ├─ Terrain.js
│ └─ RLECodec.js
├─ render/
│ ├─ Camera.js
│ └─ Renderer.js
├─ ui/
│ ├─ GUI.js
│ └─ InputHandler.js
├─ analysis/
│ └─ Analyser.js
├─ media/
│ └─ Media.js
├─ worker/
│ └─ FluviaWorker.js
└─ math/
└─ Quaternion.js
- Hydraulic Erosion Model
- Droplet Dynamics
- Erosion and Deposition
- Momentum-Coupled Meandering
- Thermal Erosion
- Terrain State Fields
- Model Parameters
Fluvia implements a Lagrangian particle approach wherein which many short-lived water droplets are spawned on a terrain surface each frame. Each droplet carries a volume, a velocity, and a sediment load. As it flows downhill under gravity, it erodes material where its capacity exceeds its load, and deposits material where its load exceeds capacity. Over many thousands of droplet lifetimes, the cumulative effect reshapes the terrain into geomorphologically plausible landforms.
The terrain is represented by two coupled scalar fields — bedrock elevation and sediment depth — whose sum gives the total surface height. A persistent discharge field records accumulated water flow, and a persistent momentum field
Each simulation frame spawns
The local gradient is estimated by central finite differences on the total height field
where
Gravity accelerates the droplet along the surface normal, inversely weighted by volume:
where
After momentum coupling (§4), the velocity vector is rescaled to a fixed diagonal magnitude:
This ensures that droplets advance at most one cell diagonally per step, preventing tunnelling issues (that plague some other implementations).
Every step, the droplet's volume and carried sediment decay exponentially by:
where
Transport capacity
where:
-
$\eta$ is the entrainment coefficient. -
$D_i$ is the raw accumulated discharge at cell$i$ . -
$\text{erf}$ is the Gauss error function (Cody rational approximation). -
$\Delta h = h_{\text{start}} - h_{\text{end}}$ is the elevation drop to the next cell.
When
where
When
where
Realistic river meandering emerges from coupling each droplet's velocity to the persistent momentum field. The momentum field accumulates droplet motion at each cell via an exponential moving average:
where
At each droplet step, the velocity is adjusted by the local momentum:
where
After each hydraulic step, thermal erosion relaxes steep slopes at the droplet's location. For each of eight neighbours (cardinal distance = 1, diagonal =
If
The transfer amount is subtracted from the higher cell and added to the lower, rendering first from the sediment layer and then from bedrock. This mitigates the formation of unrealistically vertical and exaggerated cliffs.
All fields are stored as Float32Array of size
| Field | Purpose |
|---|---|
heightMap |
Total aggregated surface elevation (bedrock + sediment) |
bedrockMap |
Bedrock layer elevation |
sedimentMap |
Accumulated sediment depth |
originalHeightMap |
Initial bedrock (for delta visualisation and reset) |
dischargeMap |
Smoothed cumulative water flow (erf-normalised, ie. error function) |
momentumX, momentumY |
Separated persistent flow direction components |
dischargeTrack |
Per-step discharge accumulator (resets each frame) |
momentumXTrack, momentumYTrack |
Per-step momentum accumulators, akin to previous |
Initial terrain is produced by Perlin noise with tunable scale, octave count, and amplitude falloff, followed by a
| Parameter | Default | Range | Description |
|---|---|---|---|
dropletsPerFrame |
256 | 0–512 | Droplets spawned each frame |
maxAge |
500 | 128–512 | Maximum droplet lifetime (steps) |
minVolume |
0.01 | 0.001–0.1 | Volume threshold for termination |
| Parameter | Symbol | Default | Range |
|---|---|---|---|
| Sediment erosion rate | 0.1 | 0–0.2 | |
| Bedrock erosion rate | 0.1 | 0–0.2 | |
| Deposition rate | 0.1 | 0–0.2 | |
| Evaporation rate | 0.001 | 0.001–1 | |
| Precipitation rate | — | 1 | 0–5 |
| Entrainment | 1 | 0–10 | |
| Gravity | 1 | 0.1–5 | |
| Momentum transfer | 1 | 0–4 |
| Parameter | Symbol | Default | Range |
|---|---|---|---|
| Max height difference | 0.01 | 0.01–1 | |
| Settling rate | 0.8 | 0–1 |
| Parameter | Default | Range |
|---|---|---|
| Terrain size | 256 | 128, 256, 512 |
| Noise scale | 0.6 | 0.1–5 |
| Noise octaves | 8 | 1–12 |
| Amplitude falloff | 0.6 | 0–1 |
| Parameter | Default | Options |
|---|---|---|
| Render method | 3D | 2D, 3D |
| Surface map | Composite | Composite, Height, Slope, Discharge, Sediment, Delta |
| Colour map | viridis | All loaded LUTs |
| Height scale | 100 | 1–256 |
Note
The composite surface map is for cosmetic and visual purposes and it 'composites' slope-weighted terrain colour, sediment overlay, and water overlay with optional specular highlights when rendering in 3D. Five RGB palette colours (sky, steep, flat, sediment, water) can be customised independently.
Each frame, the main thread transfers all nine terrain ArrayBuffers to the web worker with ownership (zero-copy). The worker executes
Main Worker
│─── step(params, buffers) ──→│
│ │ for each droplet:
│ │ advect, erode/deposit,
│ │ update discharge & momentum
│ │ thermal erosion pass
│ │ analysis pass
│←── result(buffers, statistics) ──│
In '3D' mode, the terrain is rendered as a height-displaced quad mesh via a two-pass GLSL pipeline:
- Vertex shader: displaces mesh vertices by the height texture.
- Fragment shader: samples the composite texture (pre-rendered on the CPU from terrain maps and palette colours).
- The specular highlights are calculated from a half-vector dot product a dedicated parameter for controlling intensity.
The camera uses quaternion object based orbit control with smooth interpolation (
Per-frame analysis computes 30+ metrics across four categories:
| Category | Metrics |
|---|---|
| Topography | Mean/std-dev elevation, min/max height, rugosity (surface area / cell count), slope complexity |
| Hydrology | Total water volume, active water cells, drainage density (%), hydraulic residence time, discharge bounds |
| Mass balance | Erosion rate (vol/s), sediment flux (vol/s), total sediment, total bedrock |
| Composite | Supplementary water/sediment/flat/steep coverage (%), mean slope weight, mean sediment alpha, mean water alpha |
| Key | Action |
|---|---|
P / Space |
Pause / resume |
G |
Generate new terrain |
R |
Reset terrain (undo erosion) |
I / K |
Droplets per frame ±16 |
| Key | Action |
|---|---|
1 / 2 |
Switch to 2D / 3D |
M |
Cycle surface map (Shift = reverse) |
C |
Cycle colour map (Shift = reverse) |
[ / ] |
Height scale ±4 ({/} = ±16) |
O |
Toggle statistics overlay |
L |
Toggle legend |
| Key | Action |
|---|---|
W / S / A / D |
Orbit pitch / yaw (Shift = 2× speed) |
Q / E |
Zoom out / in (Shift = 2× speed) |
| Mouse drag | Orbit |
| Scroll wheel | Zoom |
| Two-finger pinch | Zoom (touch) |
| Key | Action |
|---|---|
F |
Export image |
V |
Start / stop recording |
U |
Import heightmap image |
Shift+I |
Import parameters (JSON) |
Shift+P |
Export parameters (JSON) |
Shift+J |
Export statistics (JSON) |
Shift+K |
Export statistics (CSV) |
Shift+W |
Export world state (JSON + maps) |
Shift+Q |
Import world state (JSON) |
| Key | Action |
|---|---|
H |
Toggle GUI panel |
# |
Toggle keymap reference overlay |
- McDonald, N. Meandering rivers in particle-based hydraulic erosion simulations (2023). https://www.nickmcd.me/2023/12/12/meandering-rivers-in-particle-based-hydraulic-erosion-simulations
- McDonald, N. Simple particle-based hydraulic erosion (2020). https://nickmcd.me/2020/04/10/simple-particle-based-hydraulic-erosion/
cd library/Fluvia
python3 -m http.server 8080Open http://localhost:8080.