Builtin Library
- ..
- README.md5.1 KB
- bindings.yaml1.2 KB
- shader.wgsl12.9 KB
README.md
terrain_erode_droplet
Droplet hydraulic erosion over one tile, one batch at a time.
Each thread carries one droplet. It is placed by a hash of its index, the op's seed, the batch number and the tile's field-wide base cell, then runs downhill: it picks up material where it moves fast over steep ground and drops it where the ground flattens or rises. That is what cuts channels into a ridge and lays the fans out below them, and it is why the result looks like water shaped it rather than like a filter ran over it.
Reproducibility
The pass never writes height. Every change accumulates into delta as
fixed-point integers, 1/65536 m per unit, through atomicAdd. Integer addition
commutes; float addition does not. Thousands of droplets landing on one cell
therefore sum to the same total whatever order the threads resolve in, and the
same seed over the same stack gives the same terrain every run — on any GPU,
with any scheduling.
A float accumulator would be a few last bits different each run, and everything downstream that assumes a seed names a landscape (storage, streaming, a shared world) would drift.
What one batch may move
A batch works out every droplet's change from the ground as it stood when the batch began and adds them all at the end, so nothing in a batch can see what the rest of it is doing to the cell it is standing on. Two bounds are what keep that from running away.
share is the part of one cell's rainfall a droplet carries. A batch rains
dropletCount droplets over the interior and each walks up to apron cells, so
a cell is stepped on dropletCount * apron / interiorCells times a batch and
every one of those steps adds into the same accumulator. Scaling each droplet's
change by the reciprocal is what makes rain a sampling density: doubling it
resolves the flow better and moves the same material. Rain too sparse to step on
every cell gives a droplet one cell's rainfall and no more, because a droplet
carrying several cells' worth would put a whole batch's cut into a single step
and reopen the swing below. A run below that density — fewer than
interiorCells / apron droplets a batch — therefore samples the flow sparsely
and moves proportionally less; above it the result stops changing with rain.
MAX_SHARE is the most of the drop below a cell one batch may cut, and the most
of the rise above it one batch may fill. Without it a droplet cuts the whole
drop, which leaves the cell level with the one it drains into — and every other
droplet in the batch cuts that same drop from the same starting ground, so the
cell ends the batch far below its neighbour. The next batch finds the pair
reversed and cuts back further, and the swing grows every batch until the
fixed-point accumulator wraps. terrain_erode_thermal names the same bound; a
quarter rather than a half, because water gathers where the ground funnels it
and a cell along a channel is stepped on several times over what an average
cell gets.
Tiling
A droplet takes at most maxSteps steps, and the caller keeps that at or below
the apron width. Past that it would read cells whose real owner is a
neighbouring tile and whose contents have not been exchanged since the batch
began, and the two tiles would disagree about the ground at their seam.
Droplets spawn in the interior only. A spawn in the apron would place a second
droplet on ground the neighbouring tile has already rained on, doubling the
rainfall in a band along every seam. Material a droplet carries out of the
interior and into the apron is not lost: terrain_delta_gather adds it into
the neighbour's interior before either tile folds its accumulator into height.
What a droplet is still carrying when its walk ends goes with it. A run therefore lowers the ground it cuts: material leaves the uplands, and a field has no basin outside itself for it to arrive in. The fall in the field's mean is a small fraction of the depth the channels reach.
Parameters
Lane 1 carries seed, batch, dropletCount, maxSteps; lane 2
solubility, deposition, evaporation, inertia; lane 4
minSlope, capacity, gravity, apron. Heights are metres and horizontal
distances are cells, so capacity and minSlope read as metres of fall per
cell of run.
Every op carries a mask, and for a simulation it bounds what a step MOVES. The mask scales both what a step cuts and what it lays down, and the material a step declines to move stays where it was — in the ground, or in the droplet — so a droplet carries its load ACROSS ground the mask holds out rather than losing it there.
What a droplet still holds when its walk ends lands where the walk ended, whatever the mask reads there. The ground it walked is already lighter by exactly that much, so scaling that last drop would leave the difference nowhere and the pass would remove material instead of moving it. The mask therefore bounds where a run cuts exactly, and where it deposits everywhere except the cell a walk ends on. An op that names no mask erodes at full strength everywhere.