From 9f449fb1047ad6b69da92bf503ba80b1a6b36248 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 2 Oct 2026 23:04:30 +0200 Subject: [PATCH] Imagination pass 3b: the void. The old main menu found in Aetheria e6181c11, site-ground r3, the void scenario in interval-ops r2 Ruling operator-ground-void-brush answers Q6 outside the options. The original main menu's brush, floor, sun, orbit, camera follow, authored light and stardust rule are read from Aetheria at e6181c11 and carried in their units; the lighting is the original's analytic pulse made 3D; the TAA reprojects by camera and flow; the saving test gains the void scenario and asserts 2x on it. Co-Authored-By: Claude Fable 5.1 --- ...erval-ground-cut-interval-ops.r2.spec.json | 16 +- ...terval-ground-cut-site-ground.r3.spec.json | 240 ++++++++++++++++++ docs/cultmath-interval-ground-cut.md | 143 ++++++++--- 3 files changed, 357 insertions(+), 42 deletions(-) create mode 100644 docs/cultmath-interval-ground-cut-site-ground.r3.spec.json diff --git a/docs/cultmath-interval-ground-cut-interval-ops.r2.spec.json b/docs/cultmath-interval-ground-cut-interval-ops.r2.spec.json index 841723a7..98ef5e7f 100644 --- a/docs/cultmath-interval-ground-cut-interval-ops.r2.spec.json +++ b/docs/cultmath-interval-ground-cut-interval-ops.r2.spec.json @@ -2,7 +2,7 @@ "campaign": "cultmath-tapes", "cut": "interval-ops", "revision": 2, - "title": "Interval ops r2: envelope-then-noise fields, scenarios with authored empty space, the tile march on cell ranges, iv_frustum_ball, wasted probes removed, dxc run", + "title": "Interval ops r2: envelope-then-noise fields, scenarios with authored empty space including the void, the tile march on cell ranges, iv_frustum_ball, wasted probes removed, dxc run", "repo": "GameCult/CultLib", "branch": "hands/cultmath-interval-ops", "base": "c9bd003b", @@ -15,13 +15,14 @@ "(Bound, continued.) iv_s = iv_add(y, iv_h); iv_fade = the ordered endpoints of fade over iv_s (monotone); iv_n = iv_snoise_ball(ball.xyz * F0, ball.w * F0) with ball = iv_frustum_ball(..., warp = D); iv_s' = iv_add(iv_s, iv_scale(iv_mul(iv_fade, iv_n), A)); the slice is provably empty iff iv_s'.lo >= F. When iv_fade.hi == 0 the noise term is identically zero and iv_snoise_ball is not evaluated: the envelope alone proves the slice empty at no snoise cost. That short-circuit is the compounding the ruling expects from tiles.", "Where the warp lives in the bound. Aetheria warps only the noise argument (triNoise3d(pos + fl * ...)); the surface height is read at the unwarped xz and the displacement is applied to y afterwards. So the envelope is evaluated on the pre-distortion SDF over the unwarped slice (no D), and only the noise ball is enlarged by D (iv_frustum_ball's warp argument). A warp applied to the envelope's argument would be unsound without enlarging the box by D; the field is defined so that it is not. The test EnvelopeBoundEnclosesDensity pins this: 2,000 seeded slices x 64 warped points, the composed bound encloses density(p) with no tolerance (every op is monotone or already widened in r1; if a seeded case fails at an endpoint, widen in the implementation, never the test).", "iv_frustum_ball(float2 centreSlope, float z0, float z1, float footprintPerDepth, float warp) -> float4(centre, radius), in C# and both HLSL copies, is the one new CultMath function: the noise ball of a tile over a depth segment. Camera: pinhole at the origin looking +z, focal length f pixels; a pixel's ray is p(z') = (m z', z'); a tile's slopes lie in m_c + [-N/(2f), N/(2f)]^2 (full footprint, so sub-pixel jitter is covered), s = N / (sqrt(2) f). Centre c = (m_c z_m, z_m), z_m = (z0 + z1) / 2; radius r = ((z1 - z0) / 2) |(m_c, 1)| + z1 s + warp. Proof: a point of slope m_c + e (|e| <= s) at depth z' is c + (z' - z_m)(m_c, 1) + z'(e, 0), the terms bounded by the two radius terms, and the warp moves it by at most D (WarpedPointsStayEnclosed). The slice is a convex polytope, so every point between rays is enclosed too. The two vectors are never parallel, so the inequality is strict and float32 cannot cross it.", - "(Tile ball, continued.) Test TileBallEnclosesEveryRaySegment: 2,000 seeded tiles (N in {1, 4, 8, 16}, f in [500, 3000], |m_c| <= 1.1, segments across each scenario's z range), 64 points each with sub-pixel offsets, both depth ends and a flow of length <= warp; no tolerance. Hand mutations that must die, listed in the file header: z0 for z1 in the lateral term; |m_c| for |(m_c, 1)|; the warp dropped.", + "(Tile ball, continued.) Test TileBallEnclosesEveryRaySegment: 2,000 seeded tiles (N in {1, 4, 8, 16}, f in [500, 3000], |m_c| <= 1.1, segments across each scenario's z range), 64 points each with sub-pixel offsets, both depth ends and a flow of length <= warp; no tolerance. Hand mutations that must die, listed in the file header: z0 for z1 in the lateral term; |m_c| for |(m_c, 1)|; the warp dropped. A moving camera needs nothing more: every frame's pre-pass evaluates iv_frustum_ball from that frame's camera and never carries a ball, a mask or a probe result across frames; the only cross-frame state is the TAA history, which is colour, not a soundness input. The test says so by drawing each tile's camera independently.", "Implementation review of r1's march (NoiseBoundTests.cs:344-376), each item fixed here: (1) every probe was charged one snoise per ray; the tile shares it (ruling). (2) Coarse probes were attempted where the ball could never prove emptiness: iv_snoise_ball's hi is n(c) + L r clamped to 1, so a probe with L r F0 >= 1 + cutoff is a guaranteed failure; at r1's StepMax = 2 that is L r F0 = 3.7, so every coarse probe was a wasted evaluation (the 0.74x row). r2 attempts a noise probe only when L * radius * F0 < 1 - (the cutoff the probe needs), otherwise the level is 'maybe' at no cost. (3) Growth was capped at StepMax = 8 Step0, so a ray through clear space paid Depth / StepMax probes; r2 grows to the remaining range (the hierarchy is over cell-index ranges, halving on maybe, doubling on skip, capped only by the grid's end).", "(Review, continued.) (4) The dense sample evaluated the detail octave even where occupancy was 0; r2's dense sample evaluates coverage first and the detail only when the envelope and coverage leave density > 0, as Aetheria's `if (dist < _SafetyDistance)` does; the dense baseline gets cheaper and the comparison honest. (5) The finest-level probe before integrating a cell stays: amortized over the tile it costs 1/N^2 and can skip 3 snoise per ray. (6) The step never regrew after a dense cell until a skip; with (3) it regrows from the next skip, which is enough.", "The march, on cell ranges. The dense grid is a scenario-owned list of cell boundaries z_0 < ... < z_J (uniform for the slab scenarios; Aetheria's quadratic spacing z_j = near + (j/J)^2 (far - near), J = 256, for the Aetheria-like ones, from CloudShader.shader:116). MarchTile takes a range [j0, j1): the envelope probe over the slice (z_j0, z_j1); if empty, skip and double the next range; else if the noise probe is affordable (item 2) and proves empty, skip; else if the range is more than one cell, halve; else the cell is dense. It runs to the grid's end once per tile (a pre-pass does not know any ray's transmittance) and yields the dense cells; each of the N^2 rays integrates exactly those cells in order with the dense march's own Integrate and early-out at 0.02. Counted separately: snoise evaluations and envelope evaluations (pointwise in the dense march; interval probes in the tile march), per ray with probes divided by N^2.", "The cost model, printed and reasoned about in snoise units: a pointwise envelope evaluation or an envelope probe costs e = 0.2 (a handful of pulses or a height-map fetch against ~150 flops of snoise; stated as an estimate, both counts printed so any weight can be applied). Per scenario the test prints: dense snoise and envelope per ray, tile snoise and envelope probes per ray, the combined cost ratio, and the probe-free ceiling (dense cost over the cost of the dense cells alone). The 2x contract is on the combined cost at N = 8 for scenarios (a) and (c); (b) and (d) are printed as the worst cases and not asserted.", - "Scenarios (IV-REPORT lines, each with 64 tiles at N in {1, 4, 8, 16}, warp in {0, D}): (a) height fog: h = 0, camera at y = S + 30, rays with slope m_y in [0, 0.5] (level and up), Aetheria's grid, expected near-total skip (0 snoise either way; the envelope count carries the ratio). (b) inside the fog: h = 0, camera at y = F - 10, rays level, expected near 1x: every ray saturates within a few cells and the pre-pass still probes the whole grid. (c) Aetheria-like: the zone bowl R = 2000 and four wells of mass {100, 1000, 10000, 1000} at seeded positions inside R, camera at a seeded xz in the bowl at y = max(0, S + 10 - h(camera)), rays gazing across with m_y in [-0.25, 0.1], Aetheria's units (noise scale 414 so F0 = 1/414, D = 15 * 8 / 2 = 60, A = 20 with snoise in [-1, 1]), Aetheria's grid. (d) the uniform slab: r1's field and parameters, kept as the reported worst case. The headline is (c) at N = 8, 200 tiles.", - "Imagination's predictions for the report to confirm or refute, N = 8, combined cost: (a) above 50x (dense pays 256 envelope evaluations per ray, the tile about ten probes shared by 64 rays). (b) 0.8-1.0x. (c) 2-4x: the clear cells before the fog surface collapse to a few probes per tile, the fog cells cost the same on both sides; the noise ball proves nothing at Aetheria's scale (L r F0 >= 1.6 at the finest cell with D = 60), so item 2 keeps it from being evaluated and the saving is the envelope's. (d) 1.3-1.5x: the ceiling at L = 10.099261 with the cheaper dense baseline. If (a) or (c) misses 2x, skip the assertion the way r1 did, naming a new question saving-2x-scenarios with the printed numbers; do not tune the fields, the grids or e.", + "Scenarios (IV-REPORT lines, each with 64 tiles at N in {1, 4, 8, 16}, warp in {0, D}): (a) height fog: h = 0, camera at y = S + 30, rays with slope m_y in [0, 0.5] (level and up), Aetheria's grid, expected near-total skip (0 snoise either way; the envelope count carries the ratio). (b) inside the fog: h = 0, camera at y = F - 10, rays level, expected near 1x: every ray saturates within a few cells and the pre-pass still probes the whole grid. (c) Aetheria-like: the zone bowl R = 2000 and four wells of mass {100, 1000, 10000, 1000} at seeded positions inside R, camera at a seeded xz in the bowl at y = max(0, S + 10 - h(camera)), rays gazing across with m_y in [-0.25, 0.1], Aetheria's units (noise scale 414 so F0 = 1/414, D = 15 * 8 / 2 = 60, A = 20 with snoise in [-1, 1]), Aetheria's grid. (d) the uniform slab: r1's field and parameters, kept as the reported worst case. The headline is (e) at N = 8, 200 tiles; (c) is asserted too.", + "(e) the void, the shipped scene (ruling operator-ground-void-brush; the original's units from Aetheria e6181c11, see the site-ground r3 spec): h(xz) = 75 + 40 (1 - (r / 256)^2)^16 for r = |xz - sun.xz| < 256 (the pseudo-Gaussian brush, half depth at r = 52), floor offset F = -16.46, floor blend 39.6, S = 50, A = 20, F0 = 0.01, D = 10; the fog top is at y = -91 outside the void and -131 at the sun. Camera inside the bowl at sun + (150 along the tangent, 25 up) = y -105, looking at the orbit centre at the sun's depth, rays over the whole frustum (vertical FOV 60, f = 935): up into clear sky, level into the walls, down toward the sun. Uniform grid, StepDense 10, 96 cells to 960. Expected: sky tiles retire the slab in a few envelope probes, wall and floor tiles hand over to dense cells where opacity terminates in a few.", + "Predictions to confirm or refute, N = 8, combined cost: (a) above 50x (dense pays 256 envelope evaluations per ray, the tile about ten probes shared by 64 rays). (b) 0.8-1.0x. (c) 2-4x: clear cells collapse to a few probes per tile, fog cells cost the same on both sides; the noise ball proves nothing at Aetheria's scale (L r F0 >= 1.6 at the finest cell with D = 60), so item 2 keeps it from being evaluated and the saving is the envelope's. (e) 2-3x on the whole frustum: sky rays go from 96 envelope evaluations to a few probes per tile, wall rays cost about the same on both sides, and the mix decides; the noise ball proves nothing here either (L r F0 >= 1.5 at the finest cell with D = 10). (d) 1.3-1.5x: the ceiling at L = 10.099261 with the cheaper dense baseline. If (a), (c) or (e) misses 2x, skip the assertion the way r1 did, naming a new question saving-2x-scenarios with the printed numbers; do not tune the fields, the grids or e.", "Stryker on Yggdrasil from a plain clone: --since:c9bd003b -m math.Interval.cs; report timeouts separately from kills (a timeout is not a demonstrated kill); if the saving test is what times mutants out, exclude it from Stryker's test selection (a measurement, not a pin) rather than raising the timeout. Compile checks on Starfire, one job, detached with a log, polled in the foreground: FXC cs_5_0 over every cultmath_iv_* including the new function, both tracked copies; then tools/get-dxc.ps1 (pinned, ruling operator-compiler-downloads) and dxc -T lib_6_3 -HV 2021 on both copies. Carried from r1 and still out of scope, name them in the report: the Unity CultMath.dll is not rebuilt; the Unity HLSL copies stay one blob with packages/cultmath/shaders; public asfloat(uint) stays. Branch from c9bd003b; if main moved by docs only, say so as r1 did." ], "deletes": [ @@ -87,7 +88,7 @@ "path": "packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs", "line": 409 }, - "change": "IntervalSkipHalvesEvaluations: scenarios (a)-(d) x N in {1, 4, 8, 16} x warp in {0, D}, 64 tiles each; headline (c) at N 8, 200 tiles; assert >= 2x combined cost for (a) and (c) at N = 8, else Skip naming saving-2x-scenarios." + "change": "IntervalSkipHalvesEvaluations: scenarios (a)-(e) x N in {1, 4, 8, 16} x warp in {0, D}, 64 tiles each; headline (e) at N 8, 200 tiles; assert >= 2x combined cost for (a), (c) and (e) at N = 8, else Skip naming saving-2x-scenarios." }, { "location": { @@ -158,7 +159,7 @@ }, { "name": "NoiseBoundTests.IntervalSkipHalvesEvaluations", - "pins": "scenarios (a) and (c) at N = 8 cost at most half the dense combined cost with transmittance within 1e-3; prints every scenario, N and warp with both counts and the ceiling; a shortfall skips naming saving-2x-scenarios." + "pins": "scenarios (a), (c) and (e) at N = 8 cost at most half the dense combined cost with transmittance within 1e-3; prints every scenario, N and warp with both counts and the ceiling; a shortfall skips naming saving-2x-scenarios." }, { "name": "IntervalTests.EveryOpEnclosesItsPoints (existing)", @@ -184,7 +185,7 @@ }, { "pattern": "Assert\\.Skip", - "scope": "NoiseBoundTests.cs: at most one, in IntervalSkipHalvesEvaluations, naming saving-2x-scenarios; 0 if (a) and (c) meet 2x." + "scope": "NoiseBoundTests.cs: at most one, in IntervalSkipHalvesEvaluations, naming saving-2x-scenarios; 0 if (a), (c) and (e) meet 2x." }, { "pattern": "10\\.099261|0\\.35f", @@ -209,6 +210,7 @@ }, "rulings": [ "cultmath-tapes:ruling:operator-tile-amortized-march", + "cultmath-tapes:ruling:operator-ground-void-brush", "cultmath-tapes:ruling:operator-compiler-downloads", "cultmath-tapes:ruling:operator-campaign-home" ], diff --git a/docs/cultmath-interval-ground-cut-site-ground.r3.spec.json b/docs/cultmath-interval-ground-cut-site-ground.r3.spec.json new file mode 100644 index 00000000..0643988d --- /dev/null +++ b/docs/cultmath-interval-ground-cut-site-ground.r3.spec.json @@ -0,0 +1,240 @@ +{ + "campaign": "cultmath-tapes", + "cut": "site-ground", + "revision": 3, + "title": "Site ground r3: the void. A pseudo-Gaussian brush carves a bowl in a cloud floor, a sun at its bottom lights the cavity and orbits, the camera follows low; tile pre-pass, camera-aware TAA", + "repo": "GameCult/gamecult-site", + "branch": "main", + "base": "535d170", + "depends_on": [ + "site-masthead:cut_spec:wash-home-only.r1", + "cultmath-tapes:cut_spec:glsl-lowering.r2", + "cultmath-tapes:cut_spec:interval-ops.r2" + ], + "first": [ + "Map: F:/Projects/CultLib/docs/cultmath-interval-ground-cut.md, 'Pass 3' (the void scene, the composed bound, the pre-pass, the budget), then 'The shader design' for what pass 3 does not restate (sampled wash, palette input, pausing, contrast). Rulings in force: cultmath-tapes:ruling:operator-ground-void-brush (the scene), operator-tile-amortized-march (the march), site-masthead:ruling:operator-chromeless-column; wash-home-only's authority map. Re-pin base to main after wash-home-only lands. Stardust and DoF stay the next cut; leave the named composite slot. r2's token, readback, controller, vendoring, parity and fallback items stand unless this revision names them.", + "The original, read once (Aetheria at e6181c11, 2021-03-13, Assets/Scenes/Main Menu.unity; Prefabs/RPG/Planets/Sun.prefab; Materials/Brushes/Gravity Well.mat, Main Menu Sun Fog Tint.mat, Main Menu Ambient Fog Tint.mat, Sector Boundary Fog.mat; Materials/Fog Marching Main Menu.mat; Shaders/Raymarching/VolumeRender.shader at that commit; Shaders/Brushes/PowerBrush.shader). What survives, in the original's units: the brush is PowerBrush (1 - x^2)^16 with depth 40 over radius 256 (half depth at r = 52, pseudo-Gaussian sigma about 45), the sun's own gravity-well child; a wide boundary bowl (depth 75, power 0.25, radius 1500) is flat across the scene and acts as a constant 75; floor offset -16.46, floor blend 39.6, so the fog top is at y = -91 outside the void and dips to -131 at the sun; the sun sphere (radius 12.5) sits at y = -130, the bottom of the void.", + "What survives (continued): the sun is a child of 'Center', which rotates 5 degrees per second about y (72 s period), at orbit radius 175; the camera is a Cinemachine vcam following the sun (framing transposer, distance 150, damping 10, handheld noise amplitude 1 at frequency 0.25) and looking at 'Look Target' (0, -130, 0), the orbit centre at the sun's depth; the saved camera sits at y = -27, 64 above the fog top. The lighting was authored, not marched: the sun's 'Fog Tint' brush (1 - x^2)^3, depth 2, radius 128, colour (1, 0.4, 0.1) orange, painted into the tint map, times a scene-wide ambient tint (0.18, 0.59, 1) blue, depth 0.01. Stardust: 256^2 particles, spacing 2, floor -60, ceiling 0, height exponent 800 / (ceiling + wellDepth) so particles climb higher where the well is deeper (the void fills with stardust). Noise: strength 150, frequency 0.01, safety 50. Not surviving: the 2021 d() details, the tint map's runtime contents, the video.", + "The field, in the original's units (world units; the camera is near the sun, so float32 is fine): well map h(xz) = H0 + Dw (1 - (r / Rw)^2)^16 for r = |xz - sun.xz| < Rw, else H0; H0 = 75, Dw = 40, Rw = 256. Pre-distortion SDF s(p) = p.y + h(p.xz) - F with F = -16.46, so the fog surface is s = 0: y = -91 outside, -131 at the sun. fade(s) = 1 - smoothstep(0.75 S, S, s), S = 50. s' = s + A fade(s) n(warp(p)), n the two-phase warped coverage noise plus half the detail octave at F0 = 0.01, A = 20 (authored; the original's 150 is too violent for a 40-deep bowl). density = max(0, -s' / B) K, B = 39.6, K the extinction. Clear space is s' >= 0, and above s >= S no noise is evaluated. The brush is radial in xz and carves a bowl in a cloud floor, as the original did (Q7 a); a true 3D spherical cavity is Q7 b.", + "Scene motion. The sun orbits the origin at radius 175 in xz with period 72 s at depth y = -130; the void (h) is centred on the sun and moves with it. The camera follows the sun at horizontal distance 150 along the orbit's tangent behind it, at y = sun.y + 25 = -105 (inside the bowl: 14 below the rim at -91), looking at the orbit centre at the sun's depth (0, -130, 0), with a slow handheld drift (two low-frequency snoise on time, amplitude 2 units). Camera, sun and void positions are one function of time in ground.js (worldAt(t)), uploaded as uniforms; the shaders never integrate motion.", + "The flow field, authored around the void: flow(xz) = SWIRL * tangent(xz - sun.xz) * (1 - (r / Rw)^2)^2 + GLOBAL * (snoise(xz Ff + o1), snoise(xz Ff + o2)) (the swirl is largest mid-slope and zero at the rim and the centre; the global term is r2's). The flow texture is a world-space window of side 4 Rw centred on the sun, quarter of the canvas resolution, re-rendered every frame; warp, hue, motion vectors and (next cut) the stardust read it, so D4 holds with a moving scene. D = FLOW_MAX * PERIOD / 2 as before, with FLOW_MAX the authored maximum of the swirl plus the global amplitude.", + "The march (tile pre-pass, interval-ops r2's design). Pre-pass at W/8 x H/8: per tile, the loop over the slab's cell ranges (uniform cells, StepDense = 10, 96 cells to a far plane of 960 along +view depth): the envelope probe first (exact box: y and xz ranges by cultmath_iv_mul of the depth and slope intervals, h's interval from the box's least and greatest distance to the sun's xz, monotone), then the noise ball (cultmath_iv_frustum_ball with D, cultmath_iv_snoise_ball) only when fade.hi > 0 and L r F0 leaves room to prove anything; writes the 96-bit mask of cells not proven empty to an RGBA32UI texel. Main pass: texelFetch the mask, place S samples stratified over the unmasked cells only (exact: masked cells are zero), integrate transmittance and the depth-weighted mean distance, and apply the light below. With the camera inside the void, sky tiles retire the whole slab in a few envelope probes; wall and floor tiles mark the cells at the wall.", + "Lighting, the cheapest version that keeps it moody (the original's own mechanism, made 3D): at each sample, light = SUN_COLOUR * (1 - (d / Rl)^2)^3 for d = |p - sun| < Rl, Rl = 128, plus AMBIENT * ambientTint; colour = density * (light + ambient) * hue(flow). No light march: zero extra snoise, one length and one pow per sample. The far cavity stays dark because the pulse is zero beyond Rl; the walls brighten by proximity to the sun; the sun itself is a small additive disc drawn in the resolve at its projected position, occluded by the accumulated transmittance along its ray. Not in this cut, measured before it is ever added: one envelope-only transmittance sample toward the sun per primary sample (self-shadowing by the bowl's geometry), about +25% march cost; it is desktop-only if ever.", + "TAA with a moving camera. ground.js uploads this frame's and the previous frame's view-projection. For each pixel the resolve reconstructs the world point at the march's depth-weighted mean distance (written by the main pass into a second history channel, R16F), moves it back by the field motion (p + flow(p) * dt: the pattern moves at -flow), projects it with the previous VP and fetches history at that uv. The same 3x3 min/max clamp, alpha = max(1/frames, 1/32), out-of-bounds alpha 1; history reset on resize, first frame and budget step. Transmittance, colour and the depth channel are all reprojected by the one motion vector. ?ground=debug-mv shows |mv| with the camera moving and the flow frozen, and with the flow moving and the camera frozen, so both terms are seen separately.", + "Stardust distribution and the sun disc are the next cut (site-stardust), authored to the void: particles spawned in the bowl's volume with the original's height rule (more headroom where the well is deeper), lit by the same pulse, moving with the swirl. This cut leaves the composite slot and the depth history they need.", + "Fit with the tokens. Inputs: --gamecult-nebula-0 (orange 255 138 42) is SUN_COLOUR; --gamecult-nebula-2 (sky 89 183 255) is the ambient tint; --gamecult-nebula-1 (violet 109 96 255) is the cloud hue's second pole; --gamecult-nebula-base the sky. Outputs from the 8x8 readback as r2, with one change: --gamecult-light-0-at is written from the sun's projected screen position (quantised to 1%, at most 2 Hz), so the CSS wash's main light follows the sun while the shader runs; --gamecult-light-0 and -a come from the texel there; lights 1 and 2 keep their seed positions and sample their colours. When the canvas is absent the seeds paint a still of the same composition (the seed light-0-at is the sun's mean screen position).", + "Prove in the browser pane, in addition to r2's list: with ?ground=debug the pre-pass's mean probes per tile and the fraction of cells masked, and the mean snoise per pixel against a dense-reference frame (?ground=dense: no pre-pass, S samples over the whole slab; the two frames' mean luminance agree within 1/255); the camera reprojection test above; the orbit (after 36 s the sun's screen position has moved half an orbit and the TAA shows no smear at the walls); the contrast clamp with the lit cavity (max luminance <= 0.035 still holds; the sun disc is exempt only inside its own radius, which the clamp test masks and reports)." + ], + "deletes": [ + { + "path": "site/quartz/styles/custom.scss", + "lines": 6, + "note": "ritual body block 119-124: its own background goes; its seeds live under :root:has(body[data-slug]) (r2)." + }, + { + "path": "site/quartz/styles/custom.scss", + "lines": 9, + "note": "body 10-27: the literal colours and stops become token references (r2)." + } + ], + "keeps_moves": [ + "r2's tokens, sampled wash, controller, vendoring (now two files: the site vendors CultMath.glsl only, MIT; the parity run concatenates CultMath.Phacelle.glsl for the phacelle family), parity flags, fallbacks and the contrast clamp: unchanged except where this revision names them.", + "--gamecult-wash and the index override from wash-home-only: untouched.", + ".page::before, the tour/Pitch VN ground, the Thing deck's .hero-bg, the masthead, the engine: untouched.", + "The CSS wash stays the ground when the canvas is absent, not yet drawn, or removed by the controller." + ], + "adds": [ + "site/quartz/components/GameCultGround.tsx: as r2.", + "site/quartz/static/ground/ground.js (~500 lines): r2's passes plus worldAt(t) (sun, void, camera), the two view-projections, the pre-pass, the mask texture, the depth history, the sun-disc composite and the light-0-at writer.", + "site/quartz/static/ground/flow.frag.glsl (world-space window, swirl plus global), prepass.frag.glsl (the tile loop, mask out), cloud.frag.glsl (masked sampling, light, depth out), taa.frag.glsl (camera plus flow reprojection), downsample.frag.glsl.", + "site/quartz/static/ground/CultMath.glsl and glsl-parity.json, vendored from CultLib at interval-ops r2's head or later (cultmath_iv_frustum_ball present).", + "scripts/ground/check-cultmath-glsl.ps1: the vendoring drift check.", + "custom.scss and docs/brand-design-language.md: as r2, with the void composition named in the brand doc's ground paragraph." + ], + "file_changes": [ + { + "location": { + "path": "site/quartz/styles/custom.scss", + "line": 4, + "end_line": 8 + }, + "change": ":root block: the fifteen ground seeds and --gamecult-ground-mean; light-0-at seeded at the sun's mean screen position." + }, + { + "location": { + "path": "site/quartz/styles/custom.scss", + "line": 10, + "end_line": 31 + }, + "change": "body: four nebula input tokens; radials and the linear gradient reference the ground tokens (r2)." + }, + { + "location": { + "path": "site/quartz/styles/custom.scss", + "line": 103, + "end_line": 124 + }, + "change": "ritual block: background declaration deleted; :root:has(body[data-slug]) carries its seeds and nebula inputs (r2)." + }, + { + "location": { + "path": "site/quartz.layout.ts", + "line": 19 + }, + "change": "header: [GameCultThemeLock(), GameCultGround(), GameCultMasthead()]." + }, + { + "location": { + "path": "docs/brand-design-language.md", + "line": 69, + "end_line": 84 + }, + "change": "wash section rewritten: the void scene, the sampled wash, the seeds, the nebula inputs and what each drives." + } + ], + "authority_map": { + "owner": "The shader owns the ground. ground.js owns worldAt(t) (sun, void, camera), the flow texture, the pre-pass mask, the march, the TAA, the readback and the sampled tokens; custom.scss owns the four nebula inputs, --gamecult-wash and the seeds; CultLib owns every noise and interval function; the composed bound in prepass.frag.glsl is built from cultmath_iv_* only.", + "inputs": [ + "the four nebula tokens and --gamecult-wash (computed on body)", + "prefers-reduced-motion, visibilityState, canvas size, devicePixelRatio, elapsed time", + "measured GPU frame time (timer query, fence, or rAF cadence)", + "the shader's own 8x8 readback and the sun's projected position", + "the vendored CultMath.glsl at its pinned CultLib commit" + ], + "outputs": [ + "one opaque moving canvas under the page, luminance <= 0.035 outside the sun disc", + "the fifteen ground tokens and --gamecult-ground-mean on :root inline, at most 2 Hz; light-0-at follows the sun", + "debug, dense-reference and parity readbacks (console, flag only)" + ], + "derived_state": [ + "sun, void and camera positions are derived from time by worldAt(t); nothing integrates motion frame to frame", + "the tile mask is derived each frame from that frame's camera and the field; it is never carried across frames", + "the flow texture is the one owner of motion: warp, hue, motion vectors and the stardust read it", + "the TAA history (colour, transmittance, depth) is a cache reprojected by camera and flow; reset on resize, first frame and budget step", + "the ground tokens are derived from the readback while the shader runs; the seeds are the fallback; the shader never reads them", + "S and dpr are derived from measurement within the session; the sessionStorage token set is a cache" + ], + "forbidden_writers": [ + "no colour, alpha, position or radius literal in ground.js or any GLSL file; the scene's dimensions (H0, Dw, Rw, F, S, Rl, orbit radius, period, camera offset) are named constants in one place in ground.js, uploaded as uniforms", + "no sample placed in a masked cell; no probe in the main pass; no early-out of the pre-pass on transmittance", + "no light march in this cut; the light is the analytic pulse", + "no read of the ground output tokens by the shader or ground.js except the seed read before the first readback", + "no userAgent, platform or maxTouchPoints; no class or attribute set on body or html; no second wash token; no second flow evaluation; no noise or interval function defined in the site's GLSL", + "no synchronous readPixels to client memory; one requestAnimationFrame scheduler; localStorage never" + ], + "shared_paths": [ + "every page paints one canvas and one token set; the ritual essays differ only in seeds and inputs", + "slow device, hidden tab, reduced motion, missing WebGL2 and missing half-float all pass through the one controller", + "the 8x8 readback and the sun projection are the only paths from the shader to CSS", + "the pre-pass and the main pass read the same uniforms from the same worldAt(t); the dense-reference mode differs only by skipping the mask" + ], + "deletion_line": "As r2: the ritual block's own background and the body literals go in the same commit the tokens are declared. Nothing else in the engine or content changes. Against pass 2's design this revision removes the static camera and the everywhere-clouds field; it adds one pre-pass, one mask texture, one depth history channel and the scene function." + }, + "verification": { + "builds": [ + "Deploy Quartz green on the landing commit: gh run list -R GameCult/gamecult-site -w \"Deploy Quartz\" -L 1", + "scripts/ground/check-cultmath-glsl.ps1 exits 0 against a CultLib checkout at the pinned commit", + "the built stylesheet contains rgb(var(--gamecult-light-0) / calc(var(--gamecult-light-0-a) * var(--gamecult-wash))) verbatim and the ritual :root:has rule" + ], + "tests": [ + { + "name": "browser pane, /?ground=debug: max relative luminance over the canvas outside the sun disc after 32 frames <= 0.035", + "pins": "text contrast with the lit cavity." + }, + { + "name": "browser pane, /?ground=debug vs /?ground=dense: mean luminance within 1/255; mean snoise per pixel printed for both; masked-cell fraction and probes per tile printed", + "pins": "the mask culls only provably empty cells; the saving on the shipped scene is a number." + }, + { + "name": "browser pane, /?ground=debug-mv: |mv| non-zero with the camera moving and flow frozen, and with flow moving and camera frozen; zero under reduced motion", + "pins": "both reprojection terms are live and separable." + }, + { + "name": "browser pane, / for 40 s: the sun's screen position moves about half an orbit; no history smear at the walls (3x3 clamp); light-0-at on :root follows within 2 s", + "pins": "the moving scene converges and the CSS wash follows the sun." + }, + { + "name": "browser pane, /?ground=parity: every fixture family within its tolerance class on WebGL2, cultmath_iv_frustum_ball exact; numbers and GPU string in the report", + "pins": "GLSL = C# on a device, including the tile ball." + }, + { + "name": "browser pane, /?ground=parity-wash and a ritual essay: canvas vs CSS seed wash within 2/255 at 16 points with density 0 and the sun disc off", + "pins": "the seeds and the shader's base agree." + }, + { + "name": "reduced motion, hidden tab, WebGL2 disabled, sessionStorage cleared: as r2", + "pins": "every fallback goes through the controller." + }, + { + "name": "git diff base..head --stat: custom.scss, quartz.layout.ts, docs/brand-design-language.md, the new component, static/ground/*, scripts/ground/* only", + "pins": "no engine, content or masthead change." + } + ], + "negative": [ + { + "pattern": "#[0-9a-fA-F]{6}\\b|rgba?\\(|\\b(255|138|109|183)\\b", + "scope": "site/quartz/static/ground/*.js and *.glsl: 0 matches." + }, + { + "pattern": "gamecult-ground-|gamecult-light-", + "scope": "site/quartz/static/ground/*.glsl: 0 matches; in ground.js only inside the token writer and the seed read." + }, + { + "pattern": "userAgent|navigator\\.platform|maxTouchPoints|localStorage|setInterval|classList|setAttribute\\(", + "scope": "site/quartz/static/ground/ground.js: 0 matches." + }, + { + "pattern": "cultmath_iv_", + "scope": "cloud.frag.glsl, taa.frag.glsl, flow.frag.glsl: 0 matches; the bound lives only in prepass.frag.glsl." + }, + { + "pattern": "float cultmath_|vec4 cultmath_|float snoise|hash\\(", + "scope": "every site .frag.glsl: 0 matches; cultmath_* is defined only in the vendored CultMath.glsl." + }, + { + "pattern": "Mozilla|MPL|cultmath_phacelle", + "scope": "site/quartz/static/ground/CultMath.glsl: 0 matches; the site vendors the MIT file." + }, + { + "pattern": "radial-gradient\\([^)]*rgba?\\([0-9]", + "scope": "site/quartz/styles/custom.scss, body rule and ritual block: 0 matches." + }, + { + "pattern": "gamecult-titlebar|gamecult-nav-link|sidebar|composite|GameCult-Quartz", + "scope": "git diff base..head changed lines: 0 matches." + } + ], + "operator": [ + "open / on the desktop and on the phone; say whether the void, the sun's orbit and the camera's height read as the old main menu (uniforms: A, SWIRL, Rl, the camera offset)", + "on the phone with ?ground=debug, read the logged GPU ms per pass and the controller's final S/dpr; say whether it got warm after five minutes", + "say whether the analytic light is moody enough or the self-shadowing sample (desktop only) should be tried", + "navigate / -> /Blog -> back; say whether the ground is continuous (Q5)" + ] + }, + "estimate": { + "lines_added": 1250, + "lines_removed": 15, + "dependencies_added": [], + "dependencies_removed": [], + "formats_added": [ + "GLSL ES 3.00 shader files in static/ground", + "vendored glsl-parity.json", + "one sessionStorage key holding the last sampled token set", + "an RGBA32UI tile-mask texture and an R16F depth history (GPU-side only)" + ], + "formats_removed": [], + "targets_added": [], + "targets_removed": [] + }, + "rulings": [ + "cultmath-tapes:ruling:operator-ground-void-brush", + "cultmath-tapes:ruling:operator-tile-amortized-march", + "cultmath-tapes:ruling:operator-split-phacelle", + "site-masthead:ruling:operator-chromeless-column" + ], + "questions": [ + "Q4 in the map: palette input tokens (a, recommended)", + "Q5 in the map: carry the last sampled wash across page loads (a, recommended)", + "Q7 in the map: the brush carves a bowl in a cloud floor as the original (a, recommended) or a 3D spherical cavity in a cloud volume (b)" + ] +} diff --git a/docs/cultmath-interval-ground-cut.md b/docs/cultmath-interval-ground-cut.md index 7b53b134..d803292a 100644 --- a/docs/cultmath-interval-ground-cut.md +++ b/docs/cultmath-interval-ground-cut.md @@ -331,20 +331,24 @@ TFLOP available; estimates, replaced by measurement): | Pass | Work | ms | | --- | --- | --- | | Flow texture, quarter res | 160 k texels × 2 snoise | 0.05 | -| Tile pre-pass (pass 3), 1/64 res, `N = 8` | 40 k tiles × ≤ 144 probes, mostly envelope-only; ≈ 0.4–2 snoise-equivalents per pixel, measured | 0.2–0.8 | -| March at `S = 8` | 2.6 M × 8 snoise, placed in unmasked cells only | 3.1 | -| TAA resolve + DoF gather | ~19 texel reads per pixel | 1.0 | +| Tile pre-pass (pass 3), 1/64 res, `N = 8` | 40 k tiles × ≤ 144 probes, mostly envelope-only; in the void most sky tiles finish in a few probes; ≈ 0.4–2 snoise-equivalents per pixel, measured | 0.2–0.8 | +| March at `S = 8` | 2.6 M × 8 snoise, placed in unmasked cells only, plus the analytic sun light (one length, one pow per sample, ≈ 5%) | 3.3 | +| TAA resolve + DoF gather | ~19 texel reads per pixel, plus the depth-history fetch and one matrix multiply for the camera reprojection | 1.1 | +| Sun self-shadowing sample (named, not taken) | one envelope-only transmittance sample per primary sample, ≈ +25% of the march | +0.8, not on the phone | | Stardust layer, half res, 4,096 particles | fill-bound, small | 0.2 | | 8×8 downsample + PBO readback at 2 Hz | tiny | 0.05 | -| **Total at `S = 8`** | | **≈ 4.4** | -| Total at `S = 6` / `S = 4` | | ≈ 3.6 / 2.9 | +| **Total at `S = 8`, the void scene (pass 3)** | | **≈ 4.9** | +| Total at `S = 6` / `S = 4` | | ≈ 4.0 / 3.2 | -Said plainly: at `S = 8` the full stack does not fit the 4 ms phone budget on -the estimate. The controller will settle a mid-range phone at `S = 4–6`, -which the TAA covers (convergence in ~32 frames, about a second). What gives, -in order, is the sample count, then the resolution; the stardust and the DoF -are never dropped before the clouds' samples, because they cost a quarter of -the march. On desktop at `S = 16` the stack is ≈ 1.6 ms. +Said plainly: at `S = 8` the void scene with the pre-pass, the analytic light +and the camera reprojection does not fit the 4 ms phone budget on the +estimate; `S = 6` sits at the line and `S = 4` is under it. The controller +will settle a mid-range phone at `S = 4–6`, which the TAA covers (convergence +in ~32 frames, about a second), and the pre-pass is what makes `S = 4` look +like more: the samples land only on the walls. What gives, in order, is the +sample count, then the resolution; the stardust and the DoF are never dropped +before the clouds' samples; the self-shadowing sample is not taken on any +device until measured. On desktop at `S = 16` the stack is ≈ 1.9 ms. Measurement, in this order, all in `ground.js` behind a debug flag and used by the controller: `EXT_disjoint_timer_query_webgl2` where present (Chrome @@ -437,7 +441,8 @@ Pinned: CultLib `main` `d0ea37f8`; `hands/cultmath-interval-ops` at `c9bd003b` (report `cut-interval-ops.h1`); `hands/cultmath-glsl-lowering` at `5ae207e8` (report `cut-glsl-lowering.h1`, branched from interval-ops). Rulings: `operator-tile-amortized-march`, `operator-split-phacelle`, -`operator-compiler-downloads`, `self-exp-measure-in-site`. +`operator-compiler-downloads`, `self-exp-measure-in-site`, +`operator-ground-void-brush` (answers Q6; the void scene below). ### The operator's words @@ -517,25 +522,89 @@ warped points of 2,000 slices, no tolerance). | (b) inside the fog | `h = 0`, camera at `y = F − 10`, rays level | 0.8–1.0x: every ray saturates in a few cells; the pre-pass still probes the whole grid | | (c) Aetheria-like | zone bowl `R = 2000` + four wells (masses 100, 1000, 10000, 1000), camera above the fog in the bowl, rays gazing across (`m_y ∈ [−0.25, 0.1]`), Aetheria's units and 256-sample quadratic grid | 2–4x: the clear cells before the fog surface collapse to a few probes per tile; the fog cells cost the same on both sides. The noise ball proves nothing at Aetheria's scale (`L r F0 ≥ 1.6` at the finest cell with `D = 60`), so it is not evaluated; the saving is the envelope's | | (d) uniform slab | r1's field, kept as the worst case | 1.3–1.5x: the ceiling at `L = 10.099261` with the cheaper baseline | +| (e) the void, the shipped scene | the original's bowl and units, camera inside the bowl at `y = −105`, rays over the whole frustum | 2–3x: sky rays go from 96 envelope evaluations to a few probes per tile; wall rays cost the same on both sides; the mix decides. The noise ball proves nothing here either (`L r F0 ≥ 1.5` at the finest cell) | -The 2x contract is asserted on (a) and (c) at `N = 8`; (b) and (d) are -printed. A shortfall skips the assertion naming `saving-2x-scenarios`; the +The 2x contract is asserted on (a), (c) and (e) at `N = 8`; (b) and (d) are +printed. The headline is (e). A shortfall skips the assertion naming `saving-2x-scenarios`; the fields, grids and `e` are not tuned. The honest summary: at a Lipschitz bound near 10 the noise ball is weak everywhere; what intervals buy on these fields is the envelope's empty space, found exactly, and the tiles make finding it nearly free. -### The site ground gets the envelope +### The site ground: the void (ruling `operator-ground-void-brush`) -The site's field becomes Aetheria's structure at site scale: a nebula floor -`y = −h(x, z)` below the camera (camera at `y = 0`, looking `+z`), `h` three -authored `PowerPulse` wells seeded in the slab plus a shallow bowl, clear sky -above the safety distance, the coverage noise displacing the floor with the -flow warp as before, the hue from the flow texture as before. The screen's -upper region is clear space (the stardust shows through; the DoF blurs it by -the clouds' opacity where there are clouds); the lower region is the floor, -dipping into the wells the viewer gazes across. Which look the operator wants -is Q6. +The operator's words: "There was an old main menu visual with a purely +authored field, first thing I did all the way back when the shader was new. I +believe it was a single negative pseudo gaussian spherical brush creating a +void in the clouds, with a sun gently nestled at the bottom lighting the +cavity and orbiting along a track with the camera following low, very moody, +gives us lots of options for authoring the flow field and stardust +distribution" + +**The original, found.** Aetheria `e6181c11` ("Main Menu", 2021-03-13): +`Assets/Scenes/Main Menu.unity`, the `Sun` prefab instance under a `Center` +pivot, `Materials/Brushes/Gravity Well.mat` and +`Main Menu Sun Fog Tint.mat`, `Fog Marching Main Menu.mat` on +`VolumeRender.shader` of that commit. What survives, in its units: + +| Element | Original value | +| --- | --- | +| The brush | `PowerBrush` `(1 − x²)^16`, depth 40, radius 256 (half depth at r = 52; a Gaussian of σ ≈ 45); the sun's own gravity-well child. A boundary bowl (depth 75, power 0.25, radius 1500) is flat across the scene: a constant 75 | +| The floor | offset −16.46, blend 39.6: the fog top is `y = −91` outside the void, `−131` at the sun | +| The sun | sphere radius 12.5 at `y = −130`: the bottom of the void | +| The orbit | `Center` rotates 5°/s about y (72 s period); the sun at radius 175 | +| The camera | Cinemachine vcam following the sun (framing transposer, distance 150, damping 10, handheld noise amplitude 1 at 0.25 Hz), looking at `Look Target` `(0, −130, 0)`, the orbit centre at the sun's depth; saved at `y = −27` | +| The light | authored, not marched: the sun's tint brush `(1 − x²)^3`, depth 2, radius 128, orange `(1, .4, .1)`, plus a scene-wide blue ambient tint `(0.18, 0.59, 1)` × 0.01 | +| Stardust | 256² particles, spacing 2, floor −60, ceiling 0, height exponent `800 / (ceiling + wellDepth)`: more headroom where the well is deeper, so the void fills with stardust | +| Noise | strength 150, frequency 0.01, safety 50 | + +Not surviving: the 2021 `d()` displacement details (the 2021-05-30 "Volumetric +Redux" rewrote it), the tint map's runtime contents, the video. The brush is +radial in `xz` and carves a bowl in a cloud floor; "spherical" in the +operator's words is the brush, not a 3D cavity (Q7). + +**The site scene** (`site-ground` r3): the same units. `h(xz) = 75 + 40 (1 − +(r/256)²)^16`, `s = y + h − F`, `F = −16.46`, fog where `s' < 0`, `S = 50`, +`A = 20` (the original's 150 is too violent for a 40-deep bowl), `F0 = 0.01`. +The sun orbits the origin at radius 175, period 72 s, `y = −130`; the void +moves with it. The camera follows 150 behind along the tangent at `y = −105`: +inside the bowl, 14 below the rim, looking at the orbit centre at the sun's +depth, with a slow handheld drift. `worldAt(t)` in `ground.js` is the one +owner of sun, void and camera; nothing integrates motion. The flow is authored +around the void: a swirl `SWIRL · tangent · (1 − (r/Rw)²)²` (largest +mid-slope, zero at rim and centre) plus the global noise flow, rendered into a +world-space window of side `4 Rw` around the sun, so warp, hue, motion vectors +and the stardust read one texture (D4). The stardust (next cut) spawns in the +bowl with the original's height rule and is lit by the same pulse. + +**Lighting, cheapest moody version:** the original's mechanism made 3D. At a +sample, `light = SUN · (1 − (d/Rl)²)^3` for `d = |p − sun| < Rl = 128`, plus +the blue ambient; `colour = density · (light + ambient) · hue(flow)`. Zero +extra `snoise`; the far cavity stays dark because the pulse is zero beyond +`Rl`; the sun is a small additive disc in the resolve, occluded by the +accumulated transmittance. A self-shadowing step (one envelope-only +transmittance sample toward the sun per primary sample, no noise, ≈ +25% on +the march) is named, not taken: desktop-only if measurement ever allows. + +**TAA with a moving camera:** the main pass writes the depth-weighted mean +march distance to an `R16F` history channel; the resolve reconstructs the +world point at that depth, moves it back by the field motion (`p + flow·dt`), +projects it with the previous frame's view-projection and fetches history +there. Same 3×3 clamp, same `α`, same resets. Colour, transmittance and depth +are reprojected by the one motion vector. + +**Fit with the tokens:** `--gamecult-nebula-0` (orange) is the sun's colour, +`--gamecult-nebula-2` (sky) the ambient, `--gamecult-nebula-1` the cloud hue's +second pole, the base the sky. Outputs as pass 2, with one change: +`--gamecult-light-0-at` is written from the sun's projected position, so the +CSS wash's main light follows the sun; its seed is the sun's mean screen +position, so the fallback is a still of the same composition. + +**Soundness with a moving camera:** each frame's pre-pass evaluates +`iv_frustum_ball` from that frame's camera; no ball, mask or probe result is +carried across frames. The only cross-frame state is the TAA history, which +is colour, not a soundness input. The saving test draws each tile's camera +independently and says so. The march: a **low-resolution pre-pass** at `W/8 × H/8` runs the tile loop over the slab's cell ranges (the envelope first, the noise ball only when the @@ -577,17 +646,21 @@ timeouts are reported as timeouts, not kills. ## Questions -**Q6. The site ground's envelope look.** The envelope gives the shader empty -space to prove; it also changes what the page looks like. -- (a) A nebula floor below the camera with authored wells and clear sky - above: Aetheria's gaze-across-the-well composition; the upper screen is - stardust over the base colour, the lower screen clouds dipping into wells. - **Recommended**: the real shape, the culling pays, and it is the video the - operator described. -- (b) A cloud wall facing the camera with authored clear pockets (holes the - envelope carves): closer to pass 2's everywhere-clouds look; culling pays - only inside the pockets. -- (c) Both, blended by page: the home page (a), the ritual essays (b). +**Q6. The site ground's envelope look.** Answered outside the options by +`cultmath-tapes:ruling:operator-ground-void-brush`: the old main menu's void +(pass 3, "The site ground: the void"). + +**Q7. What "spherical" means for the brush.** +- (a) As the original: the brush is radial in `xz` and carves a bowl in a + cloud floor; the sun sits at the bowl's bottom; clear sky above the rim. + **Recommended**: it is the scene that existed, and its parameters survive. +- (b) A true 3D spherical cavity in a cloud volume (clouds above as well as + below, the sun at the cavity's bottom). The culling is the same (the + envelope is monotone in distance either way); the look is more enclosed and + the sky, stardust and DoF composition changes. + +Specs for pass 3: `docs/cultmath-interval-ground-cut-site-ground.r3.spec.json` +beside the two CultLib r2 specs. **Q4. Where the shader's palette input comes from.** - (a) Four new input tokens in `custom.scss` (`--gamecult-nebula-0/1/2`,