Skip to content

Erosion in three regions, and a three-column shell - #13

Merged
Lincoln504 merged 4 commits into
pr/06-rock-sky-surf-shorefrom
pr/07-erosion-regions-and-shell
Jul 29, 2026
Merged

Erosion in three regions, and a three-column shell#13
Lincoln504 merged 4 commits into
pr/06-rock-sky-surf-shorefrom
pr/07-erosion-regions-and-shell

Conversation

@Lincoln504

Copy link
Copy Markdown
Collaborator
  • Erosion in three altitude bands, not one setting for the whole hill
  • docs: two investigations — a three-panel shell, and an operator stack
  • One gully system, three regions: the bands blend settings, not results
  • Three columns: say it on the left, see it in the middle, tune it on the right

35 files changed, 4612 insertions(+), 844 deletions(-) · gate green at 98daac2 (fmt, clippy, workspace tests against a real adapter, wasm build, headless-Chromium smoke) · payload 464695 gz (113373 from the previous PR, against the 6 MB NFR-2 budget)

Part of a 12-PR stack over pr/00-ci. Based on pr/06-rock-sky-surf-shore; retargets to main once that lands.

⚠️ Please merge with a merge commit — squash or rebase rewrites the SHAs and breaks every PR above this one in the stack.

A mountain does not weather the same way at every height, and the eye
knows it. Geomorphology names the three regimes and they are exactly the
three the palette already paints: below the treeline transport is
diffusive — creep under soil and roots, convex slopes, broad hollows and
no gullies; between the lines it is advective, water running on bare rock
and concentrating into channels, which is the branching-gully country the
filter was built for; above the snowline there is little running water at
all and the shaping is frost and ice, so the cuts are finer, sharper and
confined to faces steep enough to shed. One parameter set from sea level
to the summit gets at most one of the three right.

So `ErosionFilter` is now three `ErosionZone`s over one `ErosionShape`,
plus the two lines they meet at and the controls that stay shared —
concentration, which is a global tilt and per band would collapse into a
second strength slider, and sediment, which is a material opacity and
gets per-band behaviour for free because a band that is off deposits no
ridge to paint.

The weights are differences of one monotone ramp, so they are an exact
partition of unity for any lines and any blend width, with no clamp and
no divide. That is load-bearing rather than tidy: the sum *is* the
filter's strength, and a partition that dipped to 0.9 in a crossfade
would leave a terrace following the treeline — the one artefact this
feature must not introduce. Height is an offset, so the bands add; a band
switched off contributes nothing and its ground keeps the shape it came
in with, faded by the same ramp instead of ending on a contour.

The lines are measured on the filter's *input* surface, not its own
result — the alternative is a mask that depends on its output, which is
an iteration or a lie.

On the GPU it is still one dispatch and still four storage buffers. The
band loop skips against exactly zero (smoothstep saturates exactly), so
outside a crossfade it runs one band and never three; each band carries
its own derived octave count and gradient stencil, since both follow that
band's gully scale. `Zone` is padded to 48 bytes so the array stride is
16-byte aligned under either side's layout rules — not the place to find
out V3D disagrees. `range` became two scalars rather than a vec2f, which
retires the alignment hazard the old struct had a paragraph about.

Pre-band documents load their flat parameter set into all three bands via
a `#[serde(from)]` shim — dropping those fields would have silently reset
every saved scene's erosion.

Reachable from the console: `filter zone=alpine strength=1.3`, and `off`
means the filter or the band depending on whether a zone is named.
Retuning a band that is off does not switch it back on. Echo prints all
three bands and both lines, so a transcript still replays.

Presets that had one reason to band, band: the canyon and monument
caprock finally hold still (the top of a canyon is not a summit, it is
the surface the canyon was cut into), the fjord runs three processes down
its walls, and alpine is the one it was designed on. Ashfields does not —
ash falls on everything equally.

Tests: CPU/GPU parity gains four banded cases including bands that
disagree about scale (so about octave count and stencil too), one band
off, degenerate lines, and bands under bedding. Plus the partition summing
to one outside [0,1] and for hostile lines, a band off sparing the other
bands' altitudes, identical bands being bit-equal to the unbanded filter,
and a rendered frame proving two different band edits are two different
pictures.
Neither is implemented. Both are written to be argued with.

UI-PROPOSAL: the current shell's controls inventoried and re-homed into
three columns — assistant, viewport, settings — with a header for state
and file operations and the bottom bar reduced to scene info. The layout
is cheap because the engine has no swapchain resize path at all, and the
proposal is explicit that it must stay that way. Right panel is
multi-open collapsible sections rather than tabs, because the settings
are consecutive stages of one filter chain and tabs sever the
adjacencies. The erosion bands get an elevation strip instead of
twenty-one labelled sliders.

LAYERS-PROPOSAL: whether the fixed terrain pass chain should become an
ordered stack of operators, and whether the assistant should address
them. Recommends not yet, in that order: the chain has no defined height
space (units in the working buffer depend on which stages ran), masks
cannot reference another operator's output without a fifth storage
binding the Pi will not give us, and the assistant has an IR already —
what it lacks is a printer. Says plainly which parts are worth doing
regardless of the rest.
The bands shipped blending *results* — both evaluated in the crossfade,
weighted and summed. That works, but it means two gully systems dissolving
into each other rather than one whose character changes. Blending the
settings instead and evaluating once is the better picture, and it is
cheaper. The measurement decided which settings can do it.

The gully scale cannot. Two integers are derived from it — the octave
count and the gradient stencil in texels — and interpolating it steps
them. Swept over the range the presets' own bands spanned (0.075 to 0.20
at 256²) that is one octave-count step worth up to 0.026 of the height
range, nearly seven texels of relief, plus forty-eight stencil steps
worth up to 0.013 each, every one falling on an altitude isoline. A
terrace following the treeline is the artefact the exact partition of
unity exists to prevent, and it would be self-defeating to let it back in
through the parameters. So the scale is shared, and everything else
interpolates.

Deposition became a band setting, because it does not follow the
carving: debris leaves the summits and arrives in the valleys, so a
mountain with heavy alluvium on its peaks and none in its bottoms is the
picture drawn backwards. The opacity folds into the ridge map inside the
kernel, so the material needs no idea that bands exist. Its *tone* is
regional too and comes from the palette — frost debris in a couloir,
washed grit mid-slope, fine mixed silt on a valley floor — which costs no
control at all: the palette owns colour, the Lab owns amounts. That moved
the three tones into the material lookup, which the terrain's fragment
stage already binds, rather than spending env texels on them.

Then the audit, by the method this filter's own controls were pruned by:
sweep one control over its range in one band and measure a rendered
frame, area and depth separately so a control that repaints a small area
strongly is not scored as weak. Nothing was dead. But detail spread
(3.2/1.7 mean levels end to end) and rounding (1.9/1.6) came in five to
ten times under strength (16.4/14.3) and gully weight (11.3/2.3) in
*both* bands, and all three describe the same thing — what the gullies
look like — so they joined the scale as shared. The band panel is five
sliders instead of seven, and the band blend width lost its slider
entirely: it has one right value, the same 0.08 the material uses for its
own windows.

Also fixed on the way through: the WGSL params struct used `vec2f`/`vec3f`
padding, which gives the struct alignment `#[repr(C)]` does not, and the
dispatch refused a 172-byte buffer where the shader wanted 176. Plain
scalars on both sides now, with the count chosen so the array lands on a
16-byte boundary with no padding member at all.
…he right

The shell was a mode bar over a preset rail over a viewport, with three
full-screen Labs that slid across it. Both halves of that were takeovers.
The mode you were in decided what the left rail was for, so finding the
material presets meant leaving Create; and a Lab covered the controls you
were not in, so you could not watch the erosion sliders and the material
at once. The assistant, meanwhile, floated over the picture as a bar,
which is the shape of a thing that interrupts rather than a place you work.

Now: header, three columns, info bar.

The header is document state and document verbs — undo/redo, new/save/open,
the .muse recorder, heightmap io, the adapter, the shortcut sheet, Render.
Nothing in it shapes the landscape. The bottom bar is information and
nothing pressable; it prints the engine's own status_line rather than
restating the same facts beside it.

The settings column is every scene control in the order the engine applies
them. Collapsible sections, not tabs, and the reason is structural rather
than aesthetic: these are consecutive stages of one filter chain — bedding
runs before the erosion filter, plants read the material, the water level
is a percentile of the terrain's own heights — and tabs would either sever
those adjacencies or put the same control in two places. Several open at
once; three at boot.

The assistant is docked and full height, with the conversation growing
upward from the input.

Two things are genuinely lost and both are deliberate. The Labs' Apply/
Cancel pair cannot exist on a panel that is always open — undo is what it
was, and every control here dispatches one ordinary undoable command. And
the mode bar's context-sensitive preset rail is gone; the material and sky
palettes now sit in their own sections, beside the controls they interact
with, which is where you would look for them anyway.

The gesture bracket the Lab frame owned moved to the settings column, so a
slider drag is still one undo step rather than one per debounce tick — the
one thing in this change that would have broken silently.

Nothing here costs GPU work. The canvas keeps its fixed backing store and
CSS letterboxes it, so a narrower viewport is one layout pass and no
swapchain reconfigure; that property is load-bearing on the Pi and is now
written down where the grid is defined.
@Lincoln504
Lincoln504 merged commit df888b9 into pr/06-rock-sky-surf-shore Jul 29, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant