A droplet carves where it speeds up and fills where it slows
Erosion drops thousands of simulated raindrops on a noise field every frame. Each one cuts rock where it accelerates and drops sediment where it slows, and the ridgelines fall out of that.
Drop a single raindrop on a field of random noise. It doesn't know it's in a demo. It finds the downhill direction under its feet, starts rolling, and picks up speed. Where it's moving fast it scrapes a little rock loose and carries it along; where the ground flattens and it slows, it sets that rock back down. One droplet does almost nothing. Run ten thousand of them a second and the noise stops being noise: valleys braid together, ridgelines sharpen, and the whole surface starts to look like a place water has lived.
Most procedural-terrain demos on the web stop at layered noise. It photographs fine, a bumpy heightmap with a green-to-white color ramp, but it never quite reads as terrain, because real terrain isn't random. It's the residue of water moving downhill for millions of years, cutting here and dumping there. Noise has no memory of water. Erosion gives it one, and it does it live in the tab instead of as an offline bake you only see the before-and-after of.
What a droplet actually carries
The interesting part, and the part worth stealing even if you never open the app, is that a droplet is a tiny state machine with almost nothing in it: a position, a velocity, how much water it holds, and how much sediment it's currently carrying. Each step it reads the local gradient of the heightmap, accelerates downhill, and moves.
The rule that turns that into terrain is sediment capacity. A fast droplet on a steep slope can hold a lot of dirt; a slow one on a flat can't hold much. So every step compares what the droplet is carrying against what it's now able to carry. Carrying less than capacity? It has room, so it erodes the ground under it and pockets the difference. Carrying more than capacity, because it just slowed down or spread out? It can't hold the surplus, so it deposits. That's the whole loop. Nobody tells the simulation where rivers go. Rivers are just the places where a lot of droplets kept accelerating in the same direction, cutting a channel that then steers the next droplets into it. Ridgelines are the seams that no droplet found a reason to descend. It's the classic droplet hydraulic model, simplified hard enough to run thousands of iterations per frame in plain JavaScript on the CPU, with no compute shader or WebGPU dependency in the way.
The terrain never gains a gram
Here's the detail I'm quietly proud of. Erosion and deposition are the same operation with the sign flipped, and they're wired so the amount a droplet takes out of one cell is exactly the amount it later puts back into others. Total heightmap mass is conserved to the gram. The terrain redistributes; it never drifts up or sags away underneath you over a long run. That's not a cosmetic nicety. Get it slightly wrong and a simulation like this slowly inflates or deflates the whole world, and after a minute you're looking at a plateau or a pit instead of mountains. The heightmap is the single source of truth, too: noise writes it, erosion mutates it in place, and the WebGL2 renderer rebuilds vertex heights and normals from it every frame. No duplicate copy of the world to keep in sync, which is most of why the carving shows up frame by frame instead of as a swap.
The honest limit lives in one slider. Droplets run on the CPU, so "erosion strength" is really "how many droplets do I simulate this frame," and that trades directly against frame time. Turn it to the max on a phone and you'll feel it. That was the deal for not pulling in a GPU compute dependency, and for terrain this size it's a deal I'd take again.
Try it
Open Erosion, let a fresh noise field generate, and push the erosion-strength slider up from zero. Within a couple of seconds you'll watch valleys cut themselves in real time. Drag to orbit, scroll to zoom, and let it sit until the status readout says it's converged. Then reroll the seed and do it again, because the same seed always carves the same mountains. The source is on GitHub.
This post is part of the build log: every app my automated factory ships gets written up here, honestly. Browse everything at apps.charliekrug.com. Comments are open below.
Loading comments…