scene: ridge heightfields, slope shading, endless world-x drift #81

Merged
void merged 2 commits from nibu/ridge-heightfield into dev 2026-11-10 19:05:28 +00:00
Member

What changed

The ridge scene (behind the existing scene flag, src/app/features/scene/ridge/) is no longer four solid cards on a periodic strip.

  • Heightfield per layer (ridgeWorld.ts profile): ridged multifractal value noise (each octave folded at its crest, weighted by the previous octave's signal), domain-warped, plus a soft swell so valleys are not flat. One function; each layer is a parameter row (cells, relief, crag, octaves, warp, gully, mist). Not periodic: noise lattice is hashed per cell, lacunarity 2.07.
  • Shading (ridgeBake.ts, pure, no DOM): per column the crest slope gives a normal, lit against the sun (sunDirection from sunAzimuth); lit side vs shadow side, a lit crest rim, a light-to-haze gradient down each face (valley mist), erosion streaks (folded noise at gully width, stronger on steep columns). Contrast scales with the existing transmittance; base colour, haze and edge blur are still layerColour / hazeColour / edgeBlur (AERIAL not duplicated).
  • Endless drift: world-x is baked in chunks of 1.2 viewport widths. Per layer a track holds 3 chunk canvases (two the window can reach, one ahead). First view bakes only chunk 0 of each layer; the rest are baked in ~6 ms tasks (setTimeout, columns per task sized from the last slice's measured time). The compositor slides the track (one Web Animation per chunk, fill: forwards); on finish the chunks move one slot left and the track restarts at 0 in the same task (no jump at ~3-8 px/s), and the canvas that left is baked with the next stretch of world. A second animation sways the whole rig (up to 5% sideways, 3% down, 150 s loop): the slow yaw and pitch. No per-frame script. 2D canvas, no WebGL; reduced-motion pause, still tier and the cadence guard are kept (flattening draws the baked chunks in view).
  • onBake option reports each chunk's cost; harness/scene/ridgeDemo.ts exposes it on window.bakes / window.longTasks.
  • Removed: the periodic noise (fbmPeriodic, valueNoisePeriodic), FBM, the period-tiled soften and scatter. README section updated.

Memory: 3 chunk canvases of 1.2 viewport widths per layer (3.6 widths) vs 2 before; about 1.8x layer canvas memory.

Measured (Chromium, SwiftShader on a CPU-only host; 1280x720 DPR 1; 5 runs, median; host load average about 30 on 16 cores, so noisy and pessimistic)

first view (4 layers, one task) per layer far to near one background chunk longest background task
desktop 71 ms (59-93) 34 / 16 / 10 / 8 ms 22 ms 49 ms
CPU 4x (CDP setCPUThrottlingRate 4) 227 ms (190-247) 89 / 56 / 50 / 29 ms 59 ms 47 ms

Budget proposed: first view under ~150 ms desktop (met, no octaves cut: 5/5/5/4) and no background task over 50 ms. The longest background task is under 50 ms in the final run but not reliably: an earlier run of the same code under heavier load showed 109 ms (desktop) and 118 ms (4x). Remaining unsliced steps: the single blur draw (a banded blur cost more in margins than it saved and was reverted) and the deliberately single first-view task. Page long tasks: the first-view task (77-131 ms desktop, 265-330 ms at 4x) plus single-run outliers of 461 and 676 ms I could not attribute (none of my own measured tasks is that long; host contention is the likely cause).

Unmeasured: the Redmi (phone). No real GPU was seen; these are CPU numbers from software rendering and 4x throttle is a proxy. The cadence guard's frame clock was stubbed in the screenshot/measurement driver (SwiftShader frames would trip it); the guard is covered by unit tests.

Verification

  • npx vitest run: 50 files, 722 tests pass (scene/ridge: 77). Scene-level tests now cover the endless drift (the fake animation can finish): each chunk's pixels are the pure bake of its world columns, rotation repositions and reorders slots and restarts the drift (one chunk of distance), the recycled canvas is rebaked with the next chunk id, three rotations in a row, and every blur reads a per-job scratch canvas of exactly that chunk's padded width. The ridged core has direct tests for the fold, the domain warp, previous-octave weighting, non-integer lacunarity and contrast by distance.
  • Soul-pass mutants M2, M4, M5, M6, M8, M9, M10, M11, M12, M15, M17, M18, M19 all killed.
  • Fixed: the shared scratch canvas (right-edge alpha fade on chunks of layers with a different blur pad); each job now owns its scratch.
  • eslint on Ridge.ts: 4 errors, the same four as on dev (prefer-destructuring, syncPlayState twice, report); rotate ordering fixed. no-param-reassign is disabled at the top of Ridge.ts and ridgeBake.ts. prettier clean.
  • npm run typecheck fails repo-wide on this host (795 errors, matrix-js-sdk types etc.); in the touched files only the existing VitestUtils error in Ridge.test.ts remains.
  • README: sun direction is 0.9 sin(2 pi * sunAzimuth) (toward +x for azimuth 0 to 0.5, strongest at 0.25); with no sunAzimuth the face lighting falls back to 0.3 and the in-scatter is off.

Preview

Built locally (standalone page with numbers and screenshots); to be published at a fixed slug by the sanctioned script once it exists. Void's stable tip-of-dev URL https://minds.gamecult.org/void/rat-dev/ will show the scene once this is merged into dev.

"Either mode" in the brief was taken to mean the CSS-ground/2D ridge scene vs the tiers (full/still); the mesh tier (PR #23) is not touched.

🤖 Generated with Claude Code

## What changed The ridge scene (behind the existing scene flag, `src/app/features/scene/ridge/`) is no longer four solid cards on a periodic strip. - **Heightfield per layer** (`ridgeWorld.ts` `profile`): ridged multifractal value noise (each octave folded at its crest, weighted by the previous octave's signal), domain-warped, plus a soft swell so valleys are not flat. One function; each layer is a parameter row (cells, relief, crag, octaves, warp, gully, mist). Not periodic: noise lattice is hashed per cell, lacunarity 2.07. - **Shading** (`ridgeBake.ts`, pure, no DOM): per column the crest slope gives a normal, lit against the sun (`sunDirection` from `sunAzimuth`); lit side vs shadow side, a lit crest rim, a light-to-haze gradient down each face (valley mist), erosion streaks (folded noise at gully width, stronger on steep columns). Contrast scales with the existing `transmittance`; base colour, haze and edge blur are still `layerColour` / `hazeColour` / `edgeBlur` (AERIAL not duplicated). - **Endless drift**: world-x is baked in chunks of 1.2 viewport widths. Per layer a track holds 3 chunk canvases (two the window can reach, one ahead). First view bakes only chunk 0 of each layer; the rest are baked in ~6 ms tasks (`setTimeout`, columns per task sized from the last slice's measured time). The compositor slides the track (one Web Animation per chunk, `fill: forwards`); on finish the chunks move one slot left and the track restarts at 0 in the same task (no jump at ~3-8 px/s), and the canvas that left is baked with the next stretch of world. A second animation sways the whole rig (up to 5% sideways, 3% down, 150 s loop): the slow yaw and pitch. No per-frame script. 2D canvas, no WebGL; reduced-motion pause, `still` tier and the cadence guard are kept (flattening draws the baked chunks in view). - `onBake` option reports each chunk's cost; `harness/scene/ridgeDemo.ts` exposes it on `window.bakes` / `window.longTasks`. - Removed: the periodic noise (`fbmPeriodic`, `valueNoisePeriodic`), `FBM`, the period-tiled soften and scatter. README section updated. Memory: 3 chunk canvases of 1.2 viewport widths per layer (3.6 widths) vs 2 before; about 1.8x layer canvas memory. ## Measured (Chromium, SwiftShader on a CPU-only host; 1280x720 DPR 1; 5 runs, median; host load average about 30 on 16 cores, so noisy and pessimistic) | | first view (4 layers, one task) | per layer far to near | one background chunk | longest background task | |---|---|---|---|---| | desktop | 71 ms (59-93) | 34 / 16 / 10 / 8 ms | 22 ms | 49 ms | | CPU 4x (CDP `setCPUThrottlingRate` 4) | 227 ms (190-247) | 89 / 56 / 50 / 29 ms | 59 ms | 47 ms | Budget proposed: first view under ~150 ms desktop (met, no octaves cut: 5/5/5/4) and no background task over 50 ms. The longest background task is under 50 ms in the final run but **not reliably**: an earlier run of the same code under heavier load showed 109 ms (desktop) and 118 ms (4x). Remaining unsliced steps: the single blur draw (a banded blur cost more in margins than it saved and was reverted) and the deliberately single first-view task. Page long tasks: the first-view task (77-131 ms desktop, 265-330 ms at 4x) plus single-run outliers of 461 and 676 ms I could not attribute (none of my own measured tasks is that long; host contention is the likely cause). **Unmeasured:** the Redmi (phone). No real GPU was seen; these are CPU numbers from software rendering and 4x throttle is a proxy. The cadence guard's frame clock was stubbed in the screenshot/measurement driver (SwiftShader frames would trip it); the guard is covered by unit tests. ## Verification - `npx vitest run`: 50 files, 722 tests pass (scene/ridge: 77). Scene-level tests now cover the endless drift (the fake animation can finish): each chunk's pixels are the pure bake of its world columns, rotation repositions and reorders slots and restarts the drift (one chunk of distance), the recycled canvas is rebaked with the next chunk id, three rotations in a row, and every blur reads a per-job scratch canvas of exactly that chunk's padded width. The ridged core has direct tests for the fold, the domain warp, previous-octave weighting, non-integer lacunarity and contrast by distance. - Soul-pass mutants M2, M4, M5, M6, M8, M9, M10, M11, M12, M15, M17, M18, M19 all killed. - Fixed: the shared scratch canvas (right-edge alpha fade on chunks of layers with a different blur pad); each job now owns its scratch. - eslint on `Ridge.ts`: 4 errors, the same four as on `dev` (`prefer-destructuring`, `syncPlayState` twice, `report`); `rotate` ordering fixed. `no-param-reassign` is disabled at the top of `Ridge.ts` and `ridgeBake.ts`. prettier clean. - `npm run typecheck` fails repo-wide on this host (795 errors, matrix-js-sdk types etc.); in the touched files only the existing `VitestUtils` error in `Ridge.test.ts` remains. - README: sun direction is `0.9 sin(2 pi * sunAzimuth)` (toward +x for azimuth 0 to 0.5, strongest at 0.25); with no `sunAzimuth` the face lighting falls back to 0.3 and the in-scatter is off. ## Preview Built locally (standalone page with numbers and screenshots); to be published at a fixed slug by the sanctioned script once it exists. Void's stable tip-of-dev URL https://minds.gamecult.org/void/rat-dev/ will show the scene once this is merged into dev. "Either mode" in the brief was taken to mean the CSS-ground/2D ridge scene vs the tiers (full/still); the mesh tier (PR #23) is not touched. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
scene: ridge heightfields with slope shading and endless world-x drift
Some checks failed
Build pull request / Build pull request (pull_request) Has been cancelled
9391e4753f
Each ridge layer is a function of world-x (ridged multifractal value noise,
domain-warped, one function and a parameter row per layer) instead of a
periodic strip. World-x is baked in 1.2-viewport chunks on demand: the first
view bakes one chunk per layer, the rest are baked in ~6 ms tasks, and the
compositor slides a track that is recycled each chunk. A slow yaw/pitch sway
rides on top. ridgeBake.ts shades each column from the crest slope against
the sun with a lit rim, a light-to-haze face gradient and erosion streaks,
reusing the existing aerial perspective as the depth treatment.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
scene: ridge chunk jobs own their scratch canvas; tests for the endless drift and the ridged core
All checks were successful
Build pull request / Build pull request (pull_request) Successful in 4m6s
d172c4eda3
Soul pass fixes. The shared scratch canvas was resized when a job was created
but read in a deferred task, so chunks of layers with a different blur pad got
a faded right edge. Each job now makes its own scratch when it first runs and
drops it when done. A chunk is baked, put and scatter-filled in column bands,
the blur is one draw; allocation is a task of its own. rotate no longer needs a
forward reference (startDrift takes the finish callback).

Tests: the fake animation can finish, so the rotation is tested at scene level
(world columns per chunk, slot repositioning, restart, recycled chunk id, one
chunk of drift distance, per-job scratch width at the moment of each blur).
The ridged core has direct tests for the fold, the warp, the previous-octave
weighting, the lacunarity and contrast by distance. README sun statement
matches sunDirection.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Author
Member

Browser check at head d172c4ed (headless Chromium on a CPU-only host; visual work, so looked at rather than mutation-tested).

  • Chunk right edges: all 12 chunk canvases (4 layers x 3 chunks), desktop 1280x720 and phone 390x844 at DPR 2: alpha 255 over the last 8 columns, bottom 10% of rows. Adjacent-chunk seam difference 0.00-0.17 against an in-chunk maximum of 0.98-2.01 (the defect measured 7.67 before the per-job scratch fix).
  • Drift, 30 s at 60x animation speed: 9 rotations over four layers, canvas count constant at 14, at most 5 animations, 3 chunks per track, 0 coverage gaps, chunk ids run sequentially per layer, a recycled canvas moves exactly 3 chunk widths. A one-frame hold shows at each rotation (animation restart) and the next frame catches up; position stays continuous, and at real speed that hold is under 0.15 px.
  • Screenshots (desktop and phone, t=0 and t=8 s) are local, not published; the preview page waits for gamecult-ops #91's slug form.

Known gaps: the ridge shape is a 1D crest profile shaded per column, not a 2D heightfield; the longest background task sits at the 50 ms edge (49 ms desktop, 47 ms at 4x throttle; 109-118 ms once under heavy host load); some unexplained page long tasks (461-676 ms single runs); no real GPU and no Redmi measurement.

Browser check at head d172c4ed (headless Chromium on a CPU-only host; visual work, so looked at rather than mutation-tested). - Chunk right edges: all 12 chunk canvases (4 layers x 3 chunks), desktop 1280x720 and phone 390x844 at DPR 2: alpha 255 over the last 8 columns, bottom 10% of rows. Adjacent-chunk seam difference 0.00-0.17 against an in-chunk maximum of 0.98-2.01 (the defect measured 7.67 before the per-job scratch fix). - Drift, 30 s at 60x animation speed: 9 rotations over four layers, canvas count constant at 14, at most 5 animations, 3 chunks per track, 0 coverage gaps, chunk ids run sequentially per layer, a recycled canvas moves exactly 3 chunk widths. A one-frame hold shows at each rotation (animation restart) and the next frame catches up; position stays continuous, and at real speed that hold is under 0.15 px. - Screenshots (desktop and phone, t=0 and t=8 s) are local, not published; the preview page waits for gamecult-ops #91's slug form. Known gaps: the ridge shape is a 1D crest profile shaded per column, not a 2D heightfield; the longest background task sits at the 50 ms edge (49 ms desktop, 47 ms at 4x throttle; 109-118 ms once under heavy host load); some unexplained page long tasks (461-676 ms single runs); no real GPU and no Redmi measurement.
void merged commit 1fe3e2c92f into dev 2026-11-10 19:05:28 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
GameCult/Ratatoskr!81
No description provided.