Imagination pass 2: the ground moves, the CSS wash is sampled from it, stardust and DoF mapped
Folds in the operator's rulings: Q1 cultmath-tapes, Q2 samples then resolution, Q3 the shader owns a dynamic ground and the CSS wash is sampled from its result. The nebula is flow-warped (two-phase flow map) with one flow texture owning motion for the warp, the TAA motion vectors, the hue and the stardust. Interval culling encloses the warp by enlarging the ball; interval-ops gains that test. New fourth cut site-stardust: stateless instanced particles plus an opacity-driven gather on the stardust layer, the web form of the raymarch-driven DoF. Budget re-checked: the full stack lands a mid-range phone at S = 4-6, not 8. Docs only. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
49e0fefb69
commit
dbc2a6295b
5 changed files with 574 additions and 368 deletions
|
|
@ -137,5 +137,5 @@
|
|||
"targets_removed": []
|
||||
},
|
||||
"rulings": [],
|
||||
"questions": ["Q1 in the map: campaign home (cultmath-tapes, recommended)"]
|
||||
"questions": []
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@
|
|||
"Noise bounds: CULTMATH_SNOISE_LIPSCHITZ (static const float, provenance in the comment: measured max |snoise_grad| over 1e6 seeded points plus 1e4 gradient-ascent refinements, times 1.10; the test that measures it is committed as a slow, explicitly-run test and the constant is pinned by a fast test). iv_snoise_ball(float3 c, float r) = [snoise(c) - L r, snoise(c) + L r] intersected with [-1, 1]. iv_fbm_ball(c, r, octaves, lacunarity, gain) = the octave sum of iv_snoise_ball(c * f_i, r * f_i) scaled by a_i, octaves clamped to [0, 16] like fbm_grad.",
|
||||
"Write the enclosure property first, as the shared harness: for each op, 10,000 seeded random intervals (widths from 1e-4 to 10, centres in [-20, 20]) and 16 points inside each; assert f(x) in op(I) with no tolerance (the bound is computed in the same float32 arithmetic; where rounding could exclude an endpoint, widen the op by one ulp in the implementation, not the test, and say so in design.md). For the ball bounds: 2,000 balls, r in [1e-3, 2], 64 points inside each.",
|
||||
"The saving test: a representative coverage field (one snoise octave at F0 = 0.35, cutoff 0.25) and 1,000 seeded rays through a slab of depth 12; march once at the dense step and once with the adaptive interval march from the map (Δ0, ΔMAX = 8 Δ0, ΔDENSE = Δ0/2, grow on skip, halve on maybe); count snoise evaluations; assert the integrated transmittance agrees within 1e-3 and the evaluation count is at least 2x lower. Print both counts and the ratio; the cut report quotes them.",
|
||||
"The warp enclosure test (the site's march rests on it): for 2,000 seeded (centre c, radius r, warp bound D) triples and random flow vectors with |flow| <= D, every warped point c' = x + flow (x in the ball of radius r) lies in the ball of radius r + D, and snoise(c') lies in iv_snoise_ball(c, r + D). This is a property of the existing bound, not a new function; it is written as a named test so the domain-warp use is pinned and documented in design.md.",
|
||||
"The saving test's field is domain-warped: coverage = sum over two phases of snoise(p + flow * (phase_k - 0.5) * PERIOD) with a bounded flow, matching the map's field shape, so the interval radius includes D.",
|
||||
"Tightness: a committed test prints the mean width ratio (interval width / true range over the 64 points) for iv_snoise_ball at r = 0.05, 0.25, 1.0 and for iv_fbm_ball at 4 octaves. No assertion beyond finiteness; the number is for the report and for a later affine cut to beat.",
|
||||
"Verification on Yggdrasil: dotnet test from packages/cultmath/tests/CultMath.Tests; Stryker --since:c49c76ee from a plain clone. Starfire: FXC cs_5_0 kernel calling every iv_* function and both ball bounds; dxc -T lib_6_3 -HV 2021. Detached with a log, polled in the foreground; never a background shell task. Numbers in comments or docs come from committed tests only.",
|
||||
"If glsl-lowering has merged to main before this branch does, rebase and regenerate shaders/CultMath.glsl (CULTMATH_WRITE_GLSL=1) so the pin test is green on the merge; if not, the lowering cut regenerates after this one. Either order works because the GLSL is derived."
|
||||
|
|
@ -92,6 +94,10 @@
|
|||
"name": "NoiseBoundTests.SnoiseBallEnclosesPoints / FbmBallEnclosesPoints",
|
||||
"pins": "the ball bounds enclose snoise and fbm over 2,000 balls x 64 points."
|
||||
},
|
||||
{
|
||||
"name": "NoiseBoundTests.WarpedPointsStayEnclosed",
|
||||
"pins": "a domain warp bounded by D is enclosed by enlarging the ball radius by D; the site's interval march is sound."
|
||||
},
|
||||
{
|
||||
"name": "NoiseBoundTests.LipschitzConstantPinsSampledGradients",
|
||||
"pins": "no sampled |snoise_grad| exceeds the constant; a constant lowered by 1% fails."
|
||||
|
|
@ -136,5 +142,5 @@
|
|||
"targets_removed": []
|
||||
},
|
||||
"rulings": [],
|
||||
"questions": ["Q1 in the map: campaign home (cultmath-tapes, recommended)"]
|
||||
"questions": []
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"campaign": "cultmath-tapes",
|
||||
"cut": "site-ground",
|
||||
"revision": 1,
|
||||
"title": "Site ground: a full-screen per-pixel WebGL2 cloud shader under every page, interval-culled, TAA-accumulated, driven by the body tokens, budget-controlled by measurement, CSS wash as fallback",
|
||||
"revision": 2,
|
||||
"title": "Site ground: a per-pixel WebGL2 nebula under every page, flow-warped, interval-culled, TAA-reprojected, budget-controlled by measurement; the CSS wash is sampled from the result, seeds fall back",
|
||||
"repo": "GameCult/gamecult-site",
|
||||
"branch": "main",
|
||||
"base": "535d170",
|
||||
|
|
@ -12,22 +12,24 @@
|
|||
"cultmath-tapes:cut_spec:interval-ops.r1"
|
||||
],
|
||||
"first": [
|
||||
"Map: F:/Projects/CultLib/docs/cultmath-interval-ground-cut.md, 'The shader design', 'Budget and how it is measured', D3-D6, Q2 and Q3. Rulings in force: site-masthead:ruling:operator-chromeless-column; the wash-home-only authority map (no second wash token, no class toggled by script, no page rule setting a radial alpha). Re-pin base to main after wash-home-only lands; line numbers below are from 535d170 and shift; re-read by selector.",
|
||||
"Aetheria reference, read once, copy nothing: Raymarching/CloudShader.shader IntegrateRaymarch (:83-104) for the transmittance integration; its pass 2 (:261-314) for what not to do (gamma 0.5 variance clip, fixed 0.05 blend, packed depth, the density2 bug at :311). Volumetric.cginc for the fill+coverage structure only.",
|
||||
"Engine facts to confirm before writing: how GameCult-Quartz (sibling checkout or GAMECULT_QUARTZ_ROOT) bundles a component's afterDOMLoaded (esbuild) and whether a dynamic import() of /static/ground/ground.js is allowed, else a module script tag emitted from the component; that site/quartz/static/** is served at /static/**; enableSPA is false (quartz.config.ts:15) so a route change is a page load. Local build: scripts/quartz/quartz.ps1 dev.",
|
||||
"Tokens (Q3 a): declare on body beside --gamecult-wash: --gamecult-ground-0/1/2 (#03070d #07111a #09141f), and for N in 0..2: --gamecult-light-N (\"255 138 42\" / \"109 96 255\" / \"89 183 255\"), --gamecult-light-N-a (0.18/0.16/0.14), --gamecult-light-N-at (\"78% 14%\" / \"18% 10%\" / \"52% 0%\"), --gamecult-light-N-r (18% / 24% / 28%). The body background becomes radial-gradient(circle at var(--gamecult-light-0-at), rgb(var(--gamecult-light-0) / calc(var(--gamecult-light-0-a) * var(--gamecult-wash))), transparent var(--gamecult-light-0-r)) x3 over linear-gradient(180deg, var(--gamecult-ground-0) 0%, var(--gamecult-ground-1) 44%, var(--gamecult-ground-2) 100%). The ritual block (custom.scss:119-124) overrides the fifteen tokens with its values (yellow 255 178 30 .15 at 78% 8% 24rem; cobalt 49 88 255 .22 at 18% 4% 30rem; violet 91 54 184 .19 at 44% 20% 34rem; stops #030414 0%, #070b24 42%, #040514 100%) and its own background declaration is deleted; it inherits body's.",
|
||||
"ground.js owns: WebGL2 context on #gamecult-ground (alpha:false, antialias:false, powerPreference 'low-power'); capability check (WebGL2, EXT_color_buffer_half_float or float); token read via getComputedStyle(document.body) -> uniforms, resolving % radii as CSS does (circle, farthest-corner of the background positioning area) and rem/px lengths via the root font size; two RGBA16F history targets; the march pass and the resolve pass; the scheduler (30 fps cadence while accumulating or drifting, visibilityState pause, reduced-motion stop at 32 frames); the budget controller; the debug flag (?ground=debug) that logs measurements and switches the march to evals/unfinished output.",
|
||||
"Budget controller, one function, one state machine: measure GPU time per frame with EXT_disjoint_timer_query_webgl2 when present, else a fenceSync after the resolve polled to completion, else rAF cadence. Over a 60-frame window: if the median exceeds the budget (4.0 ms), step down: S 16 -> 8 -> 4, then dpr -> 1 (Q2 a), then remove the canvas (CSS wash). Never step up within a session. A median rising 50% over 5 minutes also steps down (thermal). No storage, no userAgent, no maxTouchPoints.",
|
||||
"cloud.frag.glsl: #version 300 es; precision highp float; precision highp int; then the vendored CultMath.glsl is concatenated by ground.js before this file's body. The field, march and integration exactly as the map's pseudocode; uniforms: resolution, time, frame, jitter, S, intensity, three lights (rgb, alpha, centre px, radius px), three stops, Z0/Z1, F0, CUTOFF, K, drift. The output luminance clamp at 0.035. taa.frag.glsl: history + current, 3x3 min/max clamp, alpha = max(1/frames, 1/32), writes the new history and the screen in one pass via drawBuffers or two cheap draws.",
|
||||
"Vendoring (D3): site/quartz/static/ground/CultMath.glsl is byte-equal to CultLib shaders/CultMath.glsl at the commit named in its first line (a comment: generated by CultLib <sha>). A script scripts/ground/check-cultmath-glsl.ps1 diffs the vendored file against git show <sha>:packages/cultmath/shaders/CultMath.glsl from a CultLib checkout and exits non-zero on drift; Hands runs it in the report.",
|
||||
"Parity on the device (closes the GLSL leg of the proving ground): with ?ground=parity, ground.js evaluates the fixture from CultLib tests/CultMath.Tests/fixtures/glsl-parity.json (vendored beside CultMath.glsl, same pin) in a 1-pixel-per-point pass, reads back float bit patterns, and reports per function the max ulp distance and the count outside the fixture's tolerance class. Hands runs it in the browser pane and quotes the numbers and the GPU/driver string.",
|
||||
"Prove in the browser pane: readback of the max relative luminance over the canvas after 32 frames on / at intensity 1 (<= 0.035); the token parity probe (density forced to 0: canvas vs CSS wash at 16 points within 2/255, on / and on a ritual essay); the debug readback (mean evals per pixel and unfinished-ray fraction < 1%); GPU ms per pass with the timer query at the pane's desktop size and at the mobile preset's pixel count. Phone GPU time is operator verification."
|
||||
"Map: F:/Projects/CultLib/docs/cultmath-interval-ground-cut.md, 'The shader design' (field, culling with the warp, TAA motion-aware, sampled wash, palette input), 'Budget', D4-D8, Q4 and Q5. Rulings in force: site-masthead:ruling:operator-chromeless-column; wash-home-only's authority map (no second wash token, no class toggled by script, no page rule setting a radial alpha). Re-pin base to main after wash-home-only lands; line numbers below are from 535d170; re-read by selector. Stardust and DoF are the next cut: leave the resolve pass with a named, empty composite slot for them (a uniform-gated branch is not needed; a comment and a function stub are).",
|
||||
"Aetheria reference, read once, copy nothing: Volumetric.cginc:165-221 (two-phase flow-map warp, tri2 cross-fade), :130-135 (globalFlow); Raymarching/CloudShader.shader:83-104 (integration) and :261-314 (what not to do: gamma 0.5 clip, fixed 0.05 blend, packed depth, the density2 bug at :311).",
|
||||
"Engine facts to confirm before writing: how GameCult-Quartz bundles a component's afterDOMLoaded and whether a dynamic import() of /static/ground/ground.js is allowed, else emit a module script tag; site/quartz/static/** served at /static/**; enableSPA false (quartz.config.ts:15). Local build: scripts/quartz/quartz.ps1 dev.",
|
||||
"Tokens. Inputs (Q4 a), on body in custom.scss beside --gamecult-wash: --gamecult-nebula-0/1/2 (\"255 138 42\", \"109 96 255\", \"89 183 255\") and --gamecult-nebula-base (#07111a). Outputs with seeds, on :root: --gamecult-ground-0/1/2 (#03070d #07111a #09141f), and for N in 0..2 --gamecult-light-N (rgb triple), -a (0.18/0.16/0.14), -at (\"78% 14%\"/\"18% 10%\"/\"52% 0%\"), -r (18%/24%/28%), plus --gamecult-ground-mean (#07111a). The body background becomes radial-gradient(circle at var(--gamecult-light-0-at), rgb(var(--gamecult-light-0) / calc(var(--gamecult-light-0-a) * var(--gamecult-wash))), transparent var(--gamecult-light-0-r)) x3 over linear-gradient(180deg, var(--gamecult-ground-0) 0%, var(--gamecult-ground-1) 44%, var(--gamecult-ground-2) 100%).",
|
||||
"Ritual essays: their seeds go under :root:has(body[data-slug=\"Blog/...\"]) for the three slugs (yellow 255 178 30 .15 at 78% 8% 24rem; cobalt 49 88 255 .22 at 18% 4% 30rem; violet 91 54 184 .19 at 44% 20% 34rem; stops #030414 0%, #070b24 42%, #040514 100%; nebula inputs likewise overridden to yellow/cobalt/violet and base #050614). The ritual block's own background declaration is deleted; it inherits body's.",
|
||||
"ground.js owns: WebGL2 context on #gamecult-ground (alpha:false, antialias:false, powerPreference 'low-power'); capability check (WebGL2, EXT_color_buffer_half_float or float); input token read via getComputedStyle(document.body) -> uniforms; the flow texture pass (quarter res, RG16F, two snoise per texel, once per frame); the march pass; the TAA resolve with reprojection; the 8x8 downsample and the async PBO readback; the token writer on document.documentElement.style; the scheduler (30 fps cadence, visibilityState pause, reduced-motion freeze and stop at 32 frames); the budget controller; the debug flag (?ground=debug) and parity flags.",
|
||||
"Sampled wash: after the resolve, draw the final frame into an 8x8 RGBA8 target; gl.readPixels into a PIXEL_PACK_BUFFER; gl.fenceSync; on later frames poll getSyncParameter(SYNC_STATUS) and only when SIGNALED call getBufferSubData. Never readPixels to client memory directly (that stalls). At most 2 Hz. Compute the token values as the map states (row means for the stops; light colour and excess at the seed light's centre texel; the mean for --gamecult-ground-mean) and write them with setProperty on :root only when a value moved by more than 1/255. Q5 (a): keep the last written set in sessionStorage under one key and apply it on load before the first frame; it is a cache, and a missing or malformed entry means the seeds.",
|
||||
"Budget controller, one function, one state machine: GPU time per frame by EXT_disjoint_timer_query_webgl2 when present, else a fenceSync after the resolve polled to completion, else rAF cadence. Over a 60-frame window: if the median exceeds the budget (4.0 ms), step down: S 16 -> 8 -> 4, then dpr -> 1 (Q2), then remove the canvas (the seeds paint). Never step up within a session. A median rising 50% over 5 minutes also steps down. No storage of the verdict, no userAgent, no maxTouchPoints.",
|
||||
"cloud.frag.glsl: #version 300 es; precision highp float; precision highp int; the vendored CultMath.glsl is concatenated by ground.js before this file's body. The field, warp, interval march (ball radius F0 * (Δ/2 + D) with D = FLOW_MAX * PERIOD / 2) and integration exactly as the map's pseudocode; hue from the flow texture's channels mixed over the three nebula inputs; uniforms: resolution, time, dt, frame, jitter, S, intensity, nebula colours, base, Z0/Z1, F0, CUTOFF, K, PERIOD, FLOW_MAX. flow.frag.glsl: the two low-frequency snoise. taa.frag.glsl: motion vector from the flow texture (mv = project(-flow * dt)), history fetched at uv - mv, 3x3 min/max clamp, alpha = max(1/frames, 1/32), out-of-bounds alpha 1; writes new history and the screen (luminance clamp 0.035) via drawBuffers or two draws.",
|
||||
"Vendoring (D3): site/quartz/static/ground/CultMath.glsl byte-equal to CultLib shaders/CultMath.glsl at the commit named in its first line. scripts/ground/check-cultmath-glsl.ps1 diffs it against git show <sha>:packages/cultmath/shaders/CultMath.glsl from a CultLib checkout and exits non-zero on drift.",
|
||||
"Parity on the device: with ?ground=parity, ground.js evaluates the vendored fixture (CultLib tests/CultMath.Tests/fixtures/glsl-parity.json) one pixel per point, reads back float bit patterns, and reports per function the max ulp distance and the count outside its tolerance class. With ?ground=parity-wash, density and stardust are forced to 0 and the flow frozen; the canvas must equal the CSS seed wash within 2/255 at 16 points on / and on a ritual essay (this pins that the seed values and the shader's base agree at zero density, which is what the fallback promises).",
|
||||
"Prove in the browser pane: max relative luminance after 32 frames on / at intensity 1 (<= 0.035); the parity-wash probe; the debug readback (mean evals per pixel, unfinished-ray fraction < 1%); the token writer (after 2 s on /, :root's inline style carries all fifteen tokens and --gamecult-ground-mean, each within the expected range, and getComputedStyle(body).backgroundImage resolves to the sampled values); the motion test (with ?ground=debug-mv the resolve writes |mv| into red: non-zero where flow is non-zero, zero under reduced motion); GPU ms per pass with the timer query at the pane's desktop size and at the mobile preset's pixel count. Phone GPU time is operator verification."
|
||||
],
|
||||
"deletes": [
|
||||
{
|
||||
"path": "site/quartz/styles/custom.scss",
|
||||
"lines": 6,
|
||||
"note": "ritual body block 119-124: its own background (three radials and the linear gradient) goes; it overrides the tokens instead."
|
||||
"note": "ritual body block 119-124: its own background (three radials and the linear gradient) goes; its seeds live under :root:has(body[data-slug]) and it inherits body's ground."
|
||||
},
|
||||
{
|
||||
"path": "site/quartz/styles/custom.scss",
|
||||
|
|
@ -36,28 +38,32 @@
|
|||
}
|
||||
],
|
||||
"keeps_moves": [
|
||||
"--gamecult-wash and the index override from wash-home-only: untouched; the shader reads the same token as its intensity.",
|
||||
"--gamecult-wash and the index override from wash-home-only: untouched; the shader reads it as intensity and the CSS wash still multiplies by it.",
|
||||
".page::before (the grid overlay), 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; it is the same tokens.",
|
||||
"docs/brand-design-language.md 'The ground is a wash, not a flat fill' (69-84): rewritten to show the token form and to say the shader is the same ground rendered volumetrically, with the CSS as the fallback."
|
||||
"The CSS wash stays the ground when the canvas is absent, not yet drawn, or removed by the controller; it renders the sampled tokens when there are any and the seeds otherwise.",
|
||||
"docs/brand-design-language.md 'The ground is a wash, not a flat fill' (69-84): rewritten: the ground is the shader; the CSS wash is sampled from it; the seeds are today's literal wash; the four nebula inputs are where the palette is tuned."
|
||||
],
|
||||
"adds": [
|
||||
"site/quartz/components/GameCultGround.tsx: renders <canvas id=\"gamecult-ground\" aria-hidden=\"true\"></canvas> and mounts /static/ground/ground.js after DOM load; added to sharedPageComponents.header after GameCultThemeLock (quartz.layout.ts:19).",
|
||||
"site/quartz/static/ground/ground.js (~300 lines, no dependencies, no build step): context, tokens, buffers, scheduler, controller, debug/parity modes.",
|
||||
"site/quartz/static/ground/cloud.frag.glsl and taa.frag.glsl; a shared 3-vertex full-screen vertex shader inline in ground.js.",
|
||||
"site/quartz/components/GameCultGround.tsx: renders <canvas id=\"gamecult-ground\" aria-hidden=\"true\"></canvas>, mounts /static/ground/ground.js after DOM load; in sharedPageComponents.header after GameCultThemeLock (quartz.layout.ts:19). Its beforeDOMLoaded script applies the sessionStorage token cache to :root if present (Q5 a), so the first paint matches the last sample.",
|
||||
"site/quartz/static/ground/ground.js (~400 lines, no dependencies, no build step): context, tokens in and out, flow/march/resolve/downsample passes, PBO readback, scheduler, controller, debug and parity modes, a named composite slot for the stardust cut.",
|
||||
"site/quartz/static/ground/flow.frag.glsl, cloud.frag.glsl, taa.frag.glsl, downsample.frag.glsl; a shared 3-vertex full-screen vertex shader inline in ground.js.",
|
||||
"site/quartz/static/ground/CultMath.glsl and glsl-parity.json, vendored from CultLib with the pin in the header.",
|
||||
"scripts/ground/check-cultmath-glsl.ps1: the vendoring drift check.",
|
||||
"custom.scss: the fifteen tokens on body with one comment each line group; #gamecult-ground { position: fixed; inset: 0; width: 100%; height: 100%; z-index: -1; pointer-events: none; } ; the ritual override block.",
|
||||
"docs/brand-design-language.md: the wash section in token form; one paragraph on the ground shader: what it reads, what it may not hold, and the fallback."
|
||||
"custom.scss: the four nebula input tokens on body; the fifteen ground seeds plus --gamecult-ground-mean on :root with the ritual overrides under :root:has(body[data-slug]); #gamecult-ground { position: fixed; inset: 0; width: 100%; height: 100%; z-index: -1; pointer-events: none; }.",
|
||||
"docs/brand-design-language.md: the wash section rewritten as above; one paragraph on the ground shader: what it reads (inputs), what it writes (outputs), what it may not hold, and the fallback."
|
||||
],
|
||||
"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 declared beside color-scheme."
|
||||
},
|
||||
{
|
||||
"location": { "path": "site/quartz/styles/custom.scss", "line": 10, "end_line": 31 },
|
||||
"change": "body: fifteen ground tokens declared; radials and the linear gradient reference them; alphas keep the wash-home-only calc."
|
||||
"change": "body: four nebula input tokens declared; radials and the linear gradient reference the ground tokens; alphas keep the wash-home-only calc."
|
||||
},
|
||||
{
|
||||
"location": { "path": "site/quartz/styles/custom.scss", "line": 103, "end_line": 124 },
|
||||
"change": "ritual block: token overrides replace its background declaration."
|
||||
"change": "ritual block: background declaration deleted; a :root:has(body[data-slug]) rule carries its seeds and nebula inputs."
|
||||
},
|
||||
{
|
||||
"location": { "path": "site/quartz.layout.ts", "line": 19 },
|
||||
|
|
@ -65,46 +71,53 @@
|
|||
},
|
||||
{
|
||||
"location": { "path": "docs/brand-design-language.md", "line": 69, "end_line": 84 },
|
||||
"change": "wash section rewritten in token form plus the shader paragraph."
|
||||
"change": "wash section rewritten: shader-owned ground, sampled wash, seeds, nebula inputs."
|
||||
}
|
||||
],
|
||||
"authority_map": {
|
||||
"owner": "custom.scss body tokens own the ground's colours, lights and intensity; ground.js owns scheduling, budget and TAA; cloud.frag.glsl owns the field; CultLib owns every noise and interval function the field calls.",
|
||||
"owner": "The shader owns the ground. custom.scss owns the four nebula inputs, --gamecult-wash and the seed values; ground.js owns the flow texture, the march, the TAA, the readback and the sampled ground tokens on :root; CultLib owns every noise and interval function the field calls.",
|
||||
"inputs": [
|
||||
"computed body tokens (15 + --gamecult-wash)",
|
||||
"prefers-reduced-motion, visibilityState, canvas size, devicePixelRatio",
|
||||
"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",
|
||||
"the vendored CultMath.glsl at its pinned CultLib commit"
|
||||
],
|
||||
"outputs": [
|
||||
"one opaque canvas under the page, luminance <= 0.035, identical to the CSS wash at zero density",
|
||||
"one opaque moving canvas under the page, luminance <= 0.035",
|
||||
"the fifteen ground tokens and --gamecult-ground-mean on :root inline, at most 2 Hz",
|
||||
"debug and parity readbacks (console, flag only)"
|
||||
],
|
||||
"derived_state": [
|
||||
"every uniform colour, position, radius and the intensity are derived from the tokens; the shader holds no colour",
|
||||
"the TAA history is a cache; reset on resize and first frame",
|
||||
"the ground tokens are derived from the shader's result while it runs; the seeds are the fallback; the CSS wash renders whichever is in force; the shader never reads them",
|
||||
"the flow texture is the one owner of motion: warp, hue, motion vectors (and the stardust, next cut) read it",
|
||||
"the TAA history is a cache, reprojected by the flow; reset on resize, first frame and budget step",
|
||||
"S and dpr are derived from measurement within the session; never stored",
|
||||
"CultMath.glsl in the site is derived from CultLib at the pinned commit; the drift check pins it"
|
||||
"the sessionStorage token set is a cache of the last sample; absent or malformed means seeds",
|
||||
"CultMath.glsl in the site is derived from CultLib at the pinned commit"
|
||||
],
|
||||
"forbidden_writers": [
|
||||
"no colour, alpha, position or radius literal in ground.js or any GLSL file",
|
||||
"no read of --gamecult-ground-* or --gamecult-light-* by the shader or by ground.js, except the seed read before the first readback",
|
||||
"no navigator.userAgent, platform, maxTouchPoints, or any device-name branch",
|
||||
"no class or attribute set on body or html by ground.js; no second wash token; no page rule setting a radial alpha",
|
||||
"no class or attribute set on body or html by ground.js; token writes are setProperty on :root only",
|
||||
"no second wash token; no page rule setting a radial alpha; no second flow evaluation outside flow.frag.glsl",
|
||||
"no noise or interval function defined in the site's GLSL; only cultmath_* calls",
|
||||
"no localStorage/sessionStorage/cookies; no setInterval; one requestAnimationFrame scheduler"
|
||||
"no synchronous readPixels to client memory; no setInterval; one requestAnimationFrame scheduler; localStorage never"
|
||||
],
|
||||
"shared_paths": [
|
||||
"home, every content page and the ritual essays paint through the same tokens in CSS and in the shader",
|
||||
"home, every content page and the ritual essays paint one canvas and one token set, in CSS and in the shader",
|
||||
"slow device, hidden tab, reduced motion, missing WebGL2 and missing half-float all pass through the one controller",
|
||||
"the 8x8 readback is the only path from the shader to CSS; the beforeDOMLoaded cache apply and the live writer set the same tokens the same way",
|
||||
"the same Deploy Quartz workflow; static files ship with the overlay"
|
||||
],
|
||||
"deletion_line": "The ritual block's own background and the nine body literals go in the same commit the tokens are declared. The canvas, the two shaders, ground.js and the vendored library are the whole addition; nothing else in the engine or the content changes."
|
||||
"deletion_line": "The ritual block's own background and the nine body literals go in the same commit the tokens are declared. The canvas, four shaders, ground.js and the vendored library are the whole addition; nothing else in the engine or the content changes."
|
||||
},
|
||||
"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"
|
||||
"the built stylesheet contains rgb(var(--gamecult-light-0) / calc(var(--gamecult-light-0-a) * var(--gamecult-wash))) verbatim and a :root:has(body[data-slug=\"Blog/small-ritual-of-reach\"]) rule"
|
||||
],
|
||||
"tests": [
|
||||
{
|
||||
|
|
@ -112,19 +125,23 @@
|
|||
"pins": "text contrast: body text keeps >= 7:1 on every ground pixel."
|
||||
},
|
||||
{
|
||||
"name": "browser pane, /?ground=parity-wash and a ritual essay: canvas vs CSS wash at 16 points within 2/255 per channel with density forced to 0",
|
||||
"pins": "the shader and the CSS render the same tokens; no second palette."
|
||||
"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 flow frozen",
|
||||
"pins": "the seeds and the shader's base agree; the fallback promise holds."
|
||||
},
|
||||
{
|
||||
"name": "browser pane, / after 2 s: :root inline style carries the sixteen tokens; body's computed backgroundImage resolves to the sampled values; a read 1 s later differs only where the frame changed",
|
||||
"pins": "the CSS wash is sampled from the shader, at most 2 Hz, without a stall (no readPixels-to-client in the profiler)."
|
||||
},
|
||||
{
|
||||
"name": "browser pane, /?ground=parity: every fixture function within its tolerance class on WebGL2; numbers and GPU string in the report",
|
||||
"pins": "GLSL = C# on a device; the proving ground's third runtime."
|
||||
},
|
||||
{
|
||||
"name": "browser pane, /?ground=debug at desktop and at the mobile preset: mean evals per pixel, unfinished-ray fraction < 1%, GPU ms per pass by timer query",
|
||||
"pins": "the step budget and the interval saving on the shipped field."
|
||||
"name": "browser pane, /?ground=debug at desktop and the mobile preset: mean evals per pixel, unfinished rays < 1%, GPU ms per pass; ?ground=debug-mv shows motion where flow is non-zero",
|
||||
"pins": "the step budget, the interval saving with the warp, and the reprojection on the shipped field."
|
||||
},
|
||||
{
|
||||
"name": "reduced motion emulated: frames stop at 32 and drift is 0; hidden tab: no frames; WebGL2 disabled: no canvas in the DOM and the CSS wash paints",
|
||||
"name": "reduced motion emulated: frames stop at 32, mv is 0, time frozen; hidden tab: no frames; WebGL2 disabled: no canvas in the DOM and the seed wash paints; sessionStorage cleared: seeds paint first",
|
||||
"pins": "every fallback goes through the controller and lands on the same CSS ground."
|
||||
},
|
||||
{
|
||||
|
|
@ -135,19 +152,27 @@
|
|||
"negative": [
|
||||
{
|
||||
"pattern": "#[0-9a-fA-F]{6}\\b|rgba?\\(|\\b(255|138|109|183)\\b",
|
||||
"scope": "site/quartz/static/ground/ground.js, cloud.frag.glsl, taa.frag.glsl: 0 matches."
|
||||
"scope": "site/quartz/static/ground/*.js and *.glsl: 0 matches."
|
||||
},
|
||||
{
|
||||
"pattern": "userAgent|navigator\\.platform|maxTouchPoints|localStorage|sessionStorage|setInterval|classList|setAttribute\\(",
|
||||
"scope": "site/quartz/static/ground/ground.js: 0 matches (the canvas is created by the component, not by script)."
|
||||
"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, never in a uniform upload."
|
||||
},
|
||||
{
|
||||
"pattern": "userAgent|navigator\\.platform|maxTouchPoints|localStorage|setInterval|classList|setAttribute\\(",
|
||||
"scope": "site/quartz/static/ground/ground.js: 0 matches."
|
||||
},
|
||||
{
|
||||
"pattern": "readPixels\\([^)]*(Uint8Array|Float32Array|Uint16Array)",
|
||||
"scope": "ground.js: 0 matches outside the parity and debug readbacks, which are flag-gated and named."
|
||||
},
|
||||
{
|
||||
"pattern": "float cultmath_|vec4 cultmath_|float snoise|hash\\(",
|
||||
"scope": "cloud.frag.glsl and taa.frag.glsl: 0 matches; the only definitions of cultmath_* live in the vendored CultMath.glsl."
|
||||
"scope": "flow/cloud/taa/downsample.frag.glsl: 0 matches; cultmath_* is defined only in the vendored CultMath.glsl."
|
||||
},
|
||||
{
|
||||
"pattern": "radial-gradient\\([^)]*rgba?\\([0-9]",
|
||||
"scope": "site/quartz/styles/custom.scss, body rule and ritual block: 0 matches (every radial colour is a token)."
|
||||
"scope": "site/quartz/styles/custom.scss, body rule and ritual block: 0 matches."
|
||||
},
|
||||
{
|
||||
"pattern": "gamecult-titlebar|gamecult-nav-link|sidebar|composite|GameCult-Quartz",
|
||||
|
|
@ -155,17 +180,18 @@
|
|||
}
|
||||
],
|
||||
"operator": [
|
||||
"open / on the desktop and on the phone; say whether the clouds are unobtrusive enough and whether the drift speed is right (one uniform, drift)",
|
||||
"open / on the desktop and on the phone; say whether the nebula's motion and intensity are right (uniforms: PERIOD, FLOW_MAX, intensity)",
|
||||
"on the phone with ?ground=debug, read the logged GPU ms and the controller's final S/dpr; say whether it got warm after five minutes",
|
||||
"navigate / -> /Blog -> back; say whether the ground is continuous or shows a seed flash (Q5)",
|
||||
"open a ritual essay and confirm its ground still reads as its own paper"
|
||||
]
|
||||
},
|
||||
"estimate": {
|
||||
"lines_added": 760,
|
||||
"lines_added": 980,
|
||||
"lines_removed": 15,
|
||||
"dependencies_added": [],
|
||||
"dependencies_removed": [],
|
||||
"formats_added": ["GLSL ES 3.00 shader files in static/ground", "vendored glsl-parity.json"],
|
||||
"formats_added": ["GLSL ES 3.00 shader files in static/ground", "vendored glsl-parity.json", "one sessionStorage key holding the last sampled token set"],
|
||||
"formats_removed": [],
|
||||
"targets_added": [],
|
||||
"targets_removed": []
|
||||
|
|
@ -174,8 +200,7 @@
|
|||
"site-masthead:ruling:operator-chromeless-column"
|
||||
],
|
||||
"questions": [
|
||||
"Q1 in the map: campaign home (cultmath-tapes, recommended)",
|
||||
"Q2 in the map: dpr step before the CSS fallback (a, recommended)",
|
||||
"Q3 in the map: ground tokens on body (a, recommended)"
|
||||
"Q4 in the map: palette input tokens (a, recommended)",
|
||||
"Q5 in the map: carry the last sampled wash across page loads (a, recommended)"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
141
docs/cultmath-interval-ground-cut-site-stardust.spec.json
Normal file
141
docs/cultmath-interval-ground-cut-site-stardust.spec.json
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
{
|
||||
"campaign": "cultmath-tapes",
|
||||
"cut": "site-stardust",
|
||||
"revision": 1,
|
||||
"title": "Site stardust: stateless particles advected by the ground's flow texture, rendered to a half-res layer, blurred by the clouds' accumulated opacity (raymarch-driven DoF), composited behind the nebula",
|
||||
"repo": "GameCult/gamecult-site",
|
||||
"branch": "main",
|
||||
"base": "535d170",
|
||||
"depends_on": [
|
||||
"cultmath-tapes:cut_spec:site-ground.r2"
|
||||
],
|
||||
"first": [
|
||||
"Map: F:/Projects/CultLib/docs/cultmath-interval-ground-cut.md, 'Stardust', 'Depth of field driven by the raymarch', 'Budget', D4, D9, D10. Re-pin base to main after site-ground lands. The composite slot site-ground left in the resolve is where this cut plugs in; do not restructure the resolve.",
|
||||
"Aetheria reference, read once, copy nothing: Compute/Stardust/Stardust.compute:118-147 (grid cell -> hashed spawn, lifetime = frac(time/period + hash), position -= flow * lifetime * period, size = parabola(lifetime, 2), colour from a hue ramp x nebula tint); Stardust.shader:49-86 (billboard from quadPoints by SV_VertexID/SV_InstanceID, powerPulse falloff, distance exponents); CustomDoF/DepthOfField.hlsl:29-32 (CoC read from a raymarch output), :150-201 (disk gather weighted by CoC), :217-242 (combine). The web version is far smaller: no distance exponents (the slab is thin), no shadow pass, no near/far split.",
|
||||
"Particles are stateless: no buffers are updated. One static 6-vertex quad VBO; drawArraysInstanced with count N; the vertex shader derives everything from gl_InstanceID, time and the flow texture (vertex texture fetch is standard in WebGL2): cell = (id mod G, id / G) over the slab mid-plane; hashes via cultmath_pcg3d (the vendored CultMath.glsl is concatenated before the vertex source too) for spawn jitter, lifetime offset, size and brightness; lifetime = fract(t / PERIOD_P + offset); position = spawn - flow(spawn uv) * lifetime * PERIOD_P; size = base * parabola(lifetime, 2); colour = hue at the screen position (from the flow texture's channels, the same mix as the clouds) * brightness. Reduced motion freezes t as site-ground does.",
|
||||
"Stardust layer: half-resolution RGBA16F target, cleared each frame, additive blend, powerPulse falloff in the fragment shader; N = 16384 desktop, 4096 phone; the controller scales N with S (N halves when S halves) and the layer follows dpr.",
|
||||
"DoF: in the resolve's composite slot, read opacity = 1 - T from the reprojected, clamped history (not the current frame) and gather the stardust layer with an 8-tap Poisson disk of radius RMAX * opacity in layer texels (RMAX ~ 12 px at dpr 1, scaled by dpr); weight taps by a soft disk; composite out = ground + clouds + stardust_blurred * T, then the luminance clamp. All stardust is behind the whole cloud layer (D9).",
|
||||
"Debug flags: ?ground=debug-stardust writes the stardust layer alone; ?ground=debug-coc writes opacity into red. Prove in the browser pane: the stardust and the clouds move together (with ?ground=debug-mv from site-ground and the particle velocity written in green under ?ground=debug-stardust-v, the two directions agree within 5 degrees at 64 sampled pixels where |flow| is above a threshold); luminance clamp still holds after the composite; GPU ms per pass (stardust layer, gather) with the timer query at desktop and at the mobile preset's pixel count; the full-stack total against the budget table; the unfinished-ray fraction unchanged by this cut.",
|
||||
"Budget contract (map, 'Budget'): the stardust layer plus the gather cost no more than a quarter of the march at the same S; if the measurement says otherwise, the gather drops to 4 taps before N drops, and the report says which. Stardust is never cut before the clouds' S on the ladder."
|
||||
],
|
||||
"deletes": [],
|
||||
"keeps_moves": [
|
||||
"site-ground's flow texture, march, TAA, readback and token writer: untouched; the stardust reads the flow texture and the hue mix the same way the clouds do.",
|
||||
"The sampled wash now includes the stardust in its 8x8 readback by construction (it samples the final frame); nothing changes in the writer.",
|
||||
"The budget controller gains one scaled quantity (N) and no new state; the ladder order is unchanged."
|
||||
],
|
||||
"adds": [
|
||||
"site/quartz/static/ground/stardust.vert.glsl and stardust.frag.glsl: the instanced billboard and the powerPulse sprite.",
|
||||
"ground.js: the stardust pass (one VBO, one program, drawArraysInstanced), the half-res layer, N in the controller, the gather in the resolve's composite slot, the two debug flags.",
|
||||
"taa.frag.glsl: the composite slot filled: opacity from history, the 8-tap gather, out = ground + clouds + stardust_blurred * T.",
|
||||
"docs/brand-design-language.md: two sentences under the ground shader paragraph: the stardust and the DoF, and that they read the same flow and hue as the nebula."
|
||||
],
|
||||
"file_changes": [
|
||||
{
|
||||
"location": { "path": "site/quartz/static/ground/ground.js", "line": 1 },
|
||||
"change": "stardust pass, layer, N scaling, gather hookup, debug flags; line numbers are site-ground's and are re-read by function name."
|
||||
},
|
||||
{
|
||||
"location": { "path": "site/quartz/static/ground/taa.frag.glsl", "line": 1 },
|
||||
"change": "the named composite slot becomes the opacity-weighted gather and the final composite."
|
||||
},
|
||||
{
|
||||
"location": { "path": "docs/brand-design-language.md", "line": 69 },
|
||||
"change": "two sentences on stardust and DoF under the ground shader paragraph (post site-ground numbering)."
|
||||
}
|
||||
],
|
||||
"authority_map": {
|
||||
"owner": "ground.js owns the stardust pass and the DoF gather; the flow texture (site-ground) owns the particles' motion; the TAA history owns the opacity the DoF reads; the nebula input tokens own the particles' hue through the same mix as the clouds.",
|
||||
"inputs": [
|
||||
"the flow texture and the hue mix (site-ground)",
|
||||
"the reprojected, clamped history's transmittance",
|
||||
"time, N and dpr from the controller; gl_InstanceID"
|
||||
],
|
||||
"outputs": [
|
||||
"a half-res stardust layer per frame",
|
||||
"the composite with stardust blurred by cloud opacity behind the nebula, still under the luminance clamp"
|
||||
],
|
||||
"derived_state": [
|
||||
"every particle's position, lifetime, size and colour are derived from gl_InstanceID, time and the flow texture each frame; nothing is stored or updated",
|
||||
"N is derived from S by the controller; the layer size from dpr",
|
||||
"the blur radius is derived from the accumulated opacity; there is no second CoC source"
|
||||
],
|
||||
"forbidden_writers": [
|
||||
"no particle buffer update, transform feedback or CPU-side particle state",
|
||||
"no second flow function or hue mix; the vertex shader samples the flow texture",
|
||||
"no colour literal; no per-depth transmittance; no second march",
|
||||
"no new controller state beyond N; the ladder order is not changed",
|
||||
"no change to the readback or the token writer"
|
||||
],
|
||||
"shared_paths": [
|
||||
"clouds and stardust read one flow texture and one hue mix, so they move and colour as one",
|
||||
"the one resolve pass composites ground, clouds and stardust and applies the one luminance clamp",
|
||||
"the one controller scales S, N and dpr together"
|
||||
],
|
||||
"deletion_line": "Nothing is deleted. The addition is two small shaders, one pass and one gather; it buys the stardust and the DoF the operator named, and it must prove it fits the same budget."
|
||||
},
|
||||
"verification": {
|
||||
"builds": [
|
||||
"Deploy Quartz green on the landing commit",
|
||||
"scripts/ground/check-cultmath-glsl.ps1 still exits 0"
|
||||
],
|
||||
"tests": [
|
||||
{
|
||||
"name": "browser pane, /?ground=debug-stardust-v with ?ground=debug-mv: particle velocity and cloud motion vectors agree within 5 degrees at 64 sampled pixels where |flow| exceeds the threshold",
|
||||
"pins": "one flow: the stardust and the nebula move as one."
|
||||
},
|
||||
{
|
||||
"name": "browser pane, /?ground=debug-coc: opacity is 0 where the clouds are empty and rises inside them; the stardust gather radius follows it (a visible blur under a dense cloud, sharp in a gap)",
|
||||
"pins": "the raymarch drives the DoF, as in Aetheria's hack."
|
||||
},
|
||||
{
|
||||
"name": "browser pane, /?ground=debug: max relative luminance after 32 frames <= 0.035 with stardust on",
|
||||
"pins": "the clamp holds after the composite."
|
||||
},
|
||||
{
|
||||
"name": "browser pane, timer query at desktop and at the mobile preset: stardust layer + gather <= 25% of the march at the same S; full-stack totals quoted against the budget table",
|
||||
"pins": "the stardust fits the same budget or the report says what gave."
|
||||
},
|
||||
{
|
||||
"name": "reduced motion: particles frozen, frames stop at 32; hidden tab: no frames",
|
||||
"pins": "the stardust obeys the one controller."
|
||||
}
|
||||
],
|
||||
"negative": [
|
||||
{
|
||||
"pattern": "bufferSubData|transformFeedback|TRANSFORM_FEEDBACK|new Float32Array\\((N|count|particles)",
|
||||
"scope": "ground.js stardust section: 0 matches; particles are stateless."
|
||||
},
|
||||
{
|
||||
"pattern": "#[0-9a-fA-F]{6}\\b|rgba?\\(",
|
||||
"scope": "stardust.vert.glsl, stardust.frag.glsl, ground.js: 0 matches."
|
||||
},
|
||||
{
|
||||
"pattern": "snoise\\(|float cultmath_",
|
||||
"scope": "stardust.vert.glsl and stardust.frag.glsl: 0 matches; the flow and hue come from the texture, not from a second evaluation."
|
||||
},
|
||||
{
|
||||
"pattern": "readPixels|setProperty",
|
||||
"scope": "git diff base..head changed lines in ground.js: 0 matches; the readback and the token writer are not touched."
|
||||
}
|
||||
],
|
||||
"operator": [
|
||||
"open / on the desktop and on the phone; say whether the stardust sells the motion and whether the blur under the clouds reads like the video (RMAX, N, PERIOD_P are the dials)",
|
||||
"on the phone with ?ground=debug, read the full-stack GPU ms and the final S/N/dpr; say whether it got warm"
|
||||
]
|
||||
},
|
||||
"estimate": {
|
||||
"lines_added": 320,
|
||||
"lines_removed": 4,
|
||||
"dependencies_added": [],
|
||||
"dependencies_removed": [],
|
||||
"formats_added": [],
|
||||
"formats_removed": [],
|
||||
"targets_added": [],
|
||||
"targets_removed": []
|
||||
},
|
||||
"rulings": [
|
||||
"site-masthead:ruling:operator-chromeless-column"
|
||||
],
|
||||
"questions": []
|
||||
}
|
||||
|
|
@ -1,9 +1,8 @@
|
|||
# CultMath intervals and the gamecult.org ground: cut map
|
||||
|
||||
Status: Imagination pass 1, 2026-10-02. Nothing here is ruled except what the
|
||||
operator said, quoted below. The campaign named here does not exist in the mind
|
||||
yet; the Self admits it if the operator takes Q1. Where this map and the Body
|
||||
disagree, the Body wins and this map is stale.
|
||||
Status: Imagination pass 2, 2026-10-02. Pass 1's Q1–Q3 are answered (below);
|
||||
the Self opens the `cultmath-tapes` campaign from this revision. Where this
|
||||
map and the Body disagree, the Body wins and this map is stale.
|
||||
|
||||
Pinned HEADs (every `file:line` below is against these):
|
||||
|
||||
|
|
@ -11,7 +10,7 @@ Pinned HEADs (every `file:line` below is against these):
|
|||
| --- | --- | --- |
|
||||
| CultLib | `main` | `c49c76ee` (CultMath 0.3.0 merged at `614fd445`) |
|
||||
| gamecult-site | `main` | `535d170` (before `wash-home-only` lands) |
|
||||
| Aetheria | local checkout | read only; `Volumetric.cginc`, `Raymarching/CloudShader.shader`, `Clouds.shader`, `Zone Display/VolumeCloudRenderer.cs` |
|
||||
| Aetheria | local checkout | read only; `Volumetric.cginc`, `Raymarching/CloudShader.shader`, `Compute/Stardust/Stardust.{compute,shader,cs}`, `CustomDoF/DepthOfField.hlsl`, `Zone Display/VolumeCloudRenderer.cs` |
|
||||
|
||||
## The operator's words
|
||||
|
||||
|
|
@ -27,11 +26,22 @@ Pinned HEADs (every `file:line` below is against these):
|
|||
we're accumulating samples with TAA (Aetheria's TAA is honestly
|
||||
embarrassing and it still handles smooth 1080p clouds on my tired old
|
||||
1070)"
|
||||
3. On pass 1's Q3 (ground tokens as the shader's input): "I like that, but the
|
||||
background should be nice and dynamic and we should sample the wash from
|
||||
the result instead. Is there any point sending you video? I don't think
|
||||
your vision model has anything in it to encode motion. Anyway, this video
|
||||
really blew people away, with its combination of the clouds, the flowing
|
||||
stardust and my hacked DoF shader where I replaced the circle of confusion
|
||||
calculation with an output from the raymarching shader, I think it was
|
||||
final transmittance? Doesn't matter, looks great to have the clouds blur
|
||||
what's behind them. This does remind me we're totally gonna need that
|
||||
stardust too, it really sells the motion of the nebulae"
|
||||
|
||||
So: full screen, per pixel, on phones too; few samples per frame; TAA
|
||||
accumulates them; interval arithmetic makes the few samples count. The
|
||||
"static frame on phones" idea is overruled. The static frame survives only
|
||||
under `prefers-reduced-motion`.
|
||||
Rulings from pass 1: **Q1** new campaign `cultmath-tapes`. **Q2** the slow
|
||||
ladder is samples, then resolution, then the CSS wash. **Q3** answered outside
|
||||
the options: the shader owns the ground and it moves; the CSS wash is sampled
|
||||
from the shader's result; the ground tokens are seed and fallback only, never
|
||||
the shader's input; stardust and the raymarch-driven DoF are in scope.
|
||||
|
||||
## Ownership
|
||||
|
||||
|
|
@ -40,374 +50,398 @@ under `prefers-reduced-motion`.
|
|||
**CultLib, `packages/cultmath`.** This is the parked campaign's own first step
|
||||
(`docs/cultmath-tape-target.md`, sketch step 1: "CultMath `interval` in C# and
|
||||
HLSL with parity tests ... It stands alone, and is the first cut to take even
|
||||
if the rest stays parked"). Nothing about that reasoning changed. The "planned
|
||||
Asura campaign" in the operator's words is that document: the tapes campaign,
|
||||
whose first consumer named there is Aetheria's volumetric raymarching. Asura
|
||||
itself does not need intervals (`cultmath-tape-target.md:103`).
|
||||
if the rest stays parked"). The "planned Asura campaign" in the operator's
|
||||
words is that document; its first named consumer is Aetheria's volumetric
|
||||
raymarching, and Asura itself does not need intervals
|
||||
(`cultmath-tape-target.md:103`).
|
||||
|
||||
The site speaks WebGL2 GLSL ES 3.00, not HLSL. Three routes were weighed:
|
||||
The site speaks WebGL2 GLSL ES 3.00. Three routes were weighed:
|
||||
|
||||
| Route | What | Why not |
|
||||
| --- | --- | --- |
|
||||
| A | Hand-port the needed functions to GLSL inside gamecult-site | A second noise authority with no parity test; the exact thing Aetheria's `GPU Noise` copies are (follow-up `aetheria-snoise-pin-bump`). |
|
||||
| B | `dxc -spirv` then SPIRV-Cross to GLSL ES, using the existing `tools/compile-hlsl-spirv.ps1` chain | SPIRV-Cross emits one whole shader with mangled names; there is no library a site shader can call `cultmath_snoise` from. |
|
||||
| C | **A textual lowering HLSL → GLSL in CultLib, with a complete, enumerated transformation list, the same discipline as the HLSL → C# mirror (`packages/cultmath/docs/design.md:92-134`)** | Chosen. |
|
||||
| C | **A textual lowering HLSL → GLSL in CultLib with a complete, enumerated transformation list, the discipline of the HLSL → C# mirror (`packages/cultmath/docs/design.md:92-134`)** | Chosen. |
|
||||
|
||||
Under C, `shaders/CultMath.glsl` is a committed generated artifact, like
|
||||
`Swizzles.g.cs`. One implementation of the lowering lives in the test project,
|
||||
a test pins the committed file equal to the lowering of `CultMath.hlsl`, and
|
||||
the same test regenerates it when asked. GLSL's stricter typing (no implicit
|
||||
`int`/`uint` → `float`, `step(vec4, float)` does not exist) is not patched in
|
||||
the lowering: the HLSL is written in the common subset, which is legal HLSL
|
||||
and bit-identical, and glslang compiles the lowered text in CultLib's tooling
|
||||
so it stays there. Parity then runs in two legs, each with one owner:
|
||||
|
||||
- HLSL ⇔ C#: the existing bit-for-bit mirror test, automatic for every new
|
||||
function.
|
||||
- GLSL ⇔ C#: a committed golden fixture (sample points and expected bit
|
||||
patterns, exported from C#) that the site's Hands evaluates on WebGL2 in the
|
||||
browser pane and reads back. CultLib has no GPU harness (follow-up
|
||||
`gpu-snoise-parity`); the browser pane is one. This cut does not close that
|
||||
follow-up, which asks for FXC-on-D3D11, but it gives it a second device.
|
||||
|
||||
The site vendors `CultMath.glsl` with a header naming the CultLib commit it was
|
||||
copied from. The site is a xenos consumer of CultLib at that boundary; it does
|
||||
not take a package.
|
||||
`Swizzles.g.cs`. One implementation of the lowering lives in the test project;
|
||||
a test pins the committed file equal to the lowering of `CultMath.hlsl` and
|
||||
regenerates it when asked. GLSL's stricter typing is not patched in the
|
||||
lowering: the HLSL is written in the common subset (legal, bit-identical) and
|
||||
glslang compiles the lowered text in CultLib's tooling. Parity runs in two
|
||||
legs, each with one owner: HLSL ⇔ C# by the existing bit-for-bit mirror;
|
||||
GLSL ⇔ C# by a committed golden fixture evaluated on WebGL2 in the browser
|
||||
pane and read back. CultLib has no GPU harness (follow-up
|
||||
`gpu-snoise-parity`); the browser pane is one. The site vendors
|
||||
`CultMath.glsl` with the CultLib commit in its header.
|
||||
|
||||
### Campaign
|
||||
|
||||
Recommend a **new campaign, `cultmath-tapes`**, repos `GameCult/CultLib` and
|
||||
`GameCult/gamecult-site`, target doc `docs/cultmath-tape-target.md` unparked
|
||||
for step 1 only (Q1). Not `asura`: Asura's target names intervals as not
|
||||
needed, and its invariants are about planets. Not `site-masthead`: that
|
||||
campaign is one repo and its target lists "the shader (a separate campaign if
|
||||
ruled)" under `not_in_scope` (`masthead-campaign.md:822`). The site cut cites
|
||||
`site-masthead:ruling:operator-chromeless-column` and depends on
|
||||
`wash-home-only` landing first, because it rewrites the same six lines.
|
||||
`cultmath-tapes` (Q1), repos `GameCult/CultLib` and `GameCult/gamecult-site`,
|
||||
target doc `docs/cultmath-tape-target.md` unparked for step 1 only. The site
|
||||
cuts cite `site-masthead:ruling:operator-chromeless-column` and depend on
|
||||
`wash-home-only` landing first, because they rewrite the same lines.
|
||||
|
||||
### Owner, consumers, invariant
|
||||
|
||||
- Owner: CultMath owns interval arithmetic and the noise bounds, in C# and
|
||||
HLSL, with GLSL as a derived lowering. The site owns its density field, its
|
||||
march, its TAA and its budget controller. CSS tokens on `body` own the
|
||||
ground's colours, lights and intensity; the shader derives its uniforms from
|
||||
them.
|
||||
- Consumers: the site ground shader now; Aetheria's nebula raymarch at its
|
||||
CultMath pin bump; the tapes campaign's HLSL interpreter later.
|
||||
- Protected invariant, **`intervals-enclose`**: every interval function
|
||||
returns a `[lo, hi]` that contains `f(x)` for every `x` in its input
|
||||
interval or ball, in C#, HLSL and GLSL alike. A consumer that skips a region
|
||||
because `hi < cutoff` has skipped nothing that was there. This is what the
|
||||
site proves and what the tapes campaign inherits.
|
||||
- Owner: CultMath owns interval arithmetic and the noise bounds in C# and HLSL,
|
||||
GLSL derived. The site owns its field, flow, march, TAA, stardust, DoF and
|
||||
budget controller. `custom.scss` owns the brand palette the shader reads
|
||||
(four nebula tokens) and the seed values of the ground tokens. `ground.js`
|
||||
owns the sampled ground tokens while it runs.
|
||||
- Consumers: the site ground now; Aetheria's nebula raymarch at its CultMath
|
||||
pin bump; the tapes campaign's HLSL interpreter later.
|
||||
- Protected invariant, **`intervals-enclose`**: every interval function returns
|
||||
a `[lo, hi]` containing `f(x)` for every `x` in its input region, in C#, HLSL
|
||||
and GLSL alike. A consumer that skips a region because `hi < cutoff` has
|
||||
skipped nothing that was there. The domain warp does not weaken it: a warped
|
||||
point lies in the ball enlarged by the warp bound, and a test says so.
|
||||
|
||||
## The authority map (all three cuts)
|
||||
## The authority map (all four cuts)
|
||||
|
||||
- Owner: `packages/cultmath` for `iv_*` and `iv_snoise_ball`/`iv_fbm_ball`;
|
||||
`GlslLowering` (test project) for `shaders/CultMath.glsl`; `custom.scss`
|
||||
`body` tokens for ground colour, light colour/alpha/position/radius and
|
||||
`--gamecult-wash`; `ground.js` for scheduling, budget and TAA; the two site
|
||||
GLSL files for the field and the resolve.
|
||||
- Inputs: CultMath reads nothing new. The lowering reads `CultMath.hlsl` and
|
||||
`CultMath.Phacelle.hlsl` only. `ground.js` reads the computed `body` tokens,
|
||||
`prefers-reduced-motion`, `visibilityState`, canvas size and the measured
|
||||
GPU frame time. The field reads uniforms only.
|
||||
- Owner: `packages/cultmath` for `iv_*` and the ball bounds; `GlslLowering`
|
||||
(test project) for `shaders/CultMath.glsl`; `custom.scss` for the four
|
||||
nebula input tokens, `--gamecult-wash`, and the seed values of the fifteen
|
||||
ground tokens; `ground.js` for the flow texture, scheduling, budget, TAA,
|
||||
the stardust and DoF passes, and the sampled ground tokens; the site GLSL
|
||||
files for the field, the particles and the resolve.
|
||||
- Inputs: CultMath reads nothing new. The lowering reads the two HLSL files.
|
||||
`ground.js` reads the nebula tokens, `--gamecult-wash`,
|
||||
`prefers-reduced-motion`, `visibilityState`, canvas size, device pixel
|
||||
ratio, the measured GPU frame time and its own 8×8 readback. The field
|
||||
reads uniforms and the flow texture only.
|
||||
- Outputs: `CultMath.glsl`; the interval functions; one canvas under the
|
||||
page; a per-session measurement log (console, debug flag only).
|
||||
- Derived state: the shader's palette, light geometry and intensity are
|
||||
derived from the CSS tokens; nothing in JS or GLSL holds a colour. The CSS
|
||||
wash is the same tokens rendered by CSS: it is the fallback, not a second
|
||||
ground. `CultMath.glsl` is derived from `CultMath.hlsl`; editing it by hand
|
||||
fails the pin test. The TAA history is a cache; a reset costs 32 frames and
|
||||
no correctness.
|
||||
- Forbidden writers: no colour literal, radius or position in `ground.js` or
|
||||
the GLSL; no user-agent, platform or touch-point sniffing anywhere; no
|
||||
class toggled on `body` by script; no second wash token; no page rule
|
||||
setting a radial alpha (carried from `wash-home-only`); no GLSL edited by
|
||||
hand; no `iv_*` function without a C# twin; no second noise copy in the
|
||||
site.
|
||||
- Shared paths: every ground on the site, the ritual essays included, paints
|
||||
through the same tokens, in CSS and in the shader. Every route to a
|
||||
reduced budget (slow device, hidden tab, reduced motion, missing WebGL2 or
|
||||
half-float) goes through the one controller in `ground.js`; there is no
|
||||
second "mobile" path.
|
||||
- Deletion line: in the site, the six literal radial colours and the three
|
||||
literal gradient stops become token references in the same commit the
|
||||
tokens are declared. In CultLib nothing is deleted; the GLSL file is new and
|
||||
the interval file is new. The subtraction this buys is upstream: at
|
||||
Aetheria's pin bump, `Assets/Plugins/GPU Noise/*.cginc` can go, and the
|
||||
tapes campaign's step 1 is already landed when it unparks.
|
||||
page; the fifteen ground tokens written on `:root` from the readback; a
|
||||
debug log behind a flag.
|
||||
- Derived state: the ground tokens are derived from the shader's result while
|
||||
it runs and are seed values otherwise; the CSS wash renders them; nothing
|
||||
reads them back into the shader. The flow texture is the one owner of
|
||||
motion: the warp, the TAA motion vectors and the stardust all read it.
|
||||
`CultMath.glsl` is derived from `CultMath.hlsl`. The TAA history is a cache.
|
||||
`S`, `dpr` and the particle count are derived from measurement within the
|
||||
session.
|
||||
- Forbidden writers: no colour literal in `ground.js` or any GLSL; the shader
|
||||
never reads a ground token; `ground.js` never reads the ground tokens except
|
||||
to seed before its first readback; no user-agent, platform or touch-point
|
||||
sniffing; no class on `body` or `html` by script; no second wash token; no
|
||||
second flow function (particles and clouds read one texture); no GLSL edited
|
||||
by hand; no `iv_*` without a C# twin; no noise defined in the site.
|
||||
- Shared paths: every page's ground, the ritual essays included, is one
|
||||
canvas and one token set, in CSS and in the shader. Every reduced budget
|
||||
(slow device, hidden tab, reduced motion, missing WebGL2 or half-float) goes
|
||||
through the one controller. The 8×8 readback is the one path from the
|
||||
shader to CSS.
|
||||
- Deletion line: in the site, the six literal radial colours and three
|
||||
literal stops become token references and the ritual block's own ground
|
||||
goes, in the same commit the tokens are declared. In CultLib nothing is
|
||||
deleted. The subtraction this buys is upstream: Aetheria's
|
||||
`Assets/Plugins/GPU Noise/*.cginc` at its pin bump, and the tapes campaign's
|
||||
step 1 already landed.
|
||||
|
||||
## The shader design
|
||||
|
||||
### Density field (site-owned, in `cloud.frag.glsl`)
|
||||
### What Aetheria contributes, and what the web version keeps
|
||||
|
||||
The subset of Aetheria taken: `Volumetric.cginc`'s structure of a fill term
|
||||
plus a coverage term shaped by noise, and `CloudShader.shader`'s transmittance
|
||||
integration (`IntegrateRaymarch`, `:83-104`). Not taken: the height-field
|
||||
textures, the fluid flow map, `triNoise3d`, the tint LOD lookup, the packed
|
||||
depth, and the three-pass Unity plumbing. `Clouds.shader`'s `1-abs(snoise)`
|
||||
envelope over an fBm is the shape of the detail term. The noise is CultMath
|
||||
`cultmath_snoise` via the lowering, and nothing else.
|
||||
| Aetheria | Mechanism | Web subset |
|
||||
| --- | --- | --- |
|
||||
| `Volumetric.cginc:165-221` density | fill + coverage, domain-warped by `flow()` in two half-period phases cross-faded with `tri2` (a flow map; the advection never accumulates) | kept, one coverage octave and one detail octave, CultMath noise |
|
||||
| `Volumetric.cginc:130-135` `globalFlow` | `Tri3D(p/scale − scroll) · amplitude` | replaced by two `snoise` at low frequency, rendered once per frame into a quarter-res flow texture |
|
||||
| `CloudShader.shader:83-104` | transmittance integration | kept |
|
||||
| `CloudShader.shader:261-314` TAA | reprojection by `_PrevVP`, γ=0.5 variance clip, fixed 5% blend, packed depth, the `density2` bug at `:311` | not kept; see TAA |
|
||||
| `Stardust.compute:118-147` | stateless particles: grid cell hashed to a spawn point, `position −= flow · lifetime · period`, size `parabola(lifetime, 2)`, colour from a hue ramp × nebula tint | kept as an instanced vertex shader reading the flow texture; `gl_InstanceID` is the cell |
|
||||
| `DepthOfField.hlsl:29-32` `FragCoC` | the CoC is read straight from a global `_DoFBlurTex` (its writer is not in this checkout; the operator recalls final transmittance), then the bokeh gather `FragBlur` `:150-201` | a gather on the stardust layer with radius ∝ cloud opacity at that pixel |
|
||||
|
||||
### Density field (site-owned, `cloud.frag.glsl`)
|
||||
|
||||
```text
|
||||
camera at the origin, looking +z, static; the slab z in [Z0, Z1] fills the view
|
||||
p(t) = dir * t + drift * time (drift slow: ~0.02 units/s)
|
||||
coverage = snoise(p * F0) (one octave, F0 ~ 0.35)
|
||||
occupancy = max(0, coverage - CUTOFF) * K (CUTOFF ~ 0.25: most space is empty)
|
||||
detail = 1 - abs(snoise(p * F0 * 4 + 17)) (one octave; the dune envelope)
|
||||
density = occupancy * mix(0.6, 1.0, detail)
|
||||
colour = sum_i light_i.rgb * light_i.alpha * falloff_i(screen uv) (the three CSS lights)
|
||||
flow(uv) = A · (snoise(q·Ff + o1), snoise(q·Ff + o2), 0) q = slab mid-plane point; quarter-res texture, once per frame
|
||||
phase_k(t) = fract(t / PERIOD + k/2), w_k = tri2(phase_k) k = 0,1
|
||||
warp_k(p) = p + flow · (phase_k − 0.5) · PERIOD
|
||||
coverage(p) = Σ_k w_k · snoise(warp_k(p) · F0) two snoise
|
||||
occupancy = max(0, coverage − CUTOFF) · K
|
||||
detail(p) = 1 − |snoise(p · 4F0 + 17)| one snoise; the dune envelope
|
||||
density = occupancy · mix(0.6, 1.0, detail)
|
||||
hue(uv) = mix of the three nebula colours by flow.xy / A free: the flow texture's own channels
|
||||
colour = hue · intensity
|
||||
```
|
||||
|
||||
Two `snoise` per dense sample, one per empty probe. The lights are the three
|
||||
CSS radials with the same centre, radius and colour; a point in the volume is
|
||||
"lit" by the radial falloff at its screen position, so the clouds are the wash
|
||||
made structured, with no 3D lighting.
|
||||
Three `snoise` per dense sample, one per empty probe. The pattern moves at
|
||||
velocity `−flow` everywhere, because both phases advance at the same rate;
|
||||
the cross-fade only changes content. That is what makes the motion vectors
|
||||
below exact.
|
||||
|
||||
### Interval culling along the ray
|
||||
### Interval culling along the ray, with the warp
|
||||
|
||||
`cultmath_iv_snoise_ball(c, r)` returns `[n(c) - L r, n(c) + L r] ∩ [-1, 1]`,
|
||||
where `L` is CultMath's committed Lipschitz constant for `snoise(float3)`. Over
|
||||
a ray segment `[t, t + Δ]` the ball is centred at `t + Δ/2` with radius `Δ/2`,
|
||||
scaled by `F0`. The march is adaptive subdivision, which is Fidget's region
|
||||
test on a 1-D region:
|
||||
`cultmath_iv_snoise_ball(c, r)` returns `[n(c) − L r, n(c) + L r] ∩ [−1, 1]`
|
||||
with `L` the committed Lipschitz constant. Over a segment `[t, t+Δ]` the
|
||||
unwarped ball is centred at `t + Δ/2` with radius `Δ/2`; the warp displaces
|
||||
any point by at most `D = |flow|max · PERIOD / 2`, so the ball `(c, Δ/2 + D)`
|
||||
contains both phases' warped points, and one evaluation bounds the whole
|
||||
coverage term. The march is adaptive subdivision, Fidget's region test on a
|
||||
1-D region:
|
||||
|
||||
```text
|
||||
Δ = Δ0; t = Z0/dir.z + jitter * Δ0
|
||||
Δ = Δ0; t = Z0/dir.z + jitter · Δ0
|
||||
while t < t_exit and evals < S:
|
||||
iv = iv_snoise_ball(p(t + Δ/2) * F0, F0 * Δ/2) -- 1 eval
|
||||
if iv.hi < CUTOFF: t += Δ; Δ = min(Δ * 2, ΔMAX) -- provably empty: skip, grow
|
||||
elif Δ > ΔDENSE: Δ = Δ / 2 -- maybe dense: refine
|
||||
else: integrate density(p(t + Δ/2)) over Δ; t += Δ -- 1 more eval
|
||||
iv = iv_snoise_ball(p(t + Δ/2) · F0, F0 · (Δ/2 + D)) 1 eval
|
||||
if iv.hi < CUTOFF: t += Δ; Δ = min(2Δ, ΔMAX) provably empty: skip, grow
|
||||
elif Δ > ΔDENSE: Δ = Δ / 2 maybe dense: refine
|
||||
else: integrate density(p(t + Δ/2)) over Δ; t += Δ 3 evals
|
||||
if transmittance < 0.02: break
|
||||
if t < t_exit: integrate one coarse sample over the rest -- the budget ran out; measured, must be rare
|
||||
if t < t_exit: integrate one coarse sample over the rest the budget ran out; measured, must be rare
|
||||
```
|
||||
|
||||
The `iv.hi < CUTOFF` test is the interval proof that the `0` branch of
|
||||
`max(0, coverage - CUTOFF)` wins on the whole segment: Fidget's `min`/`max`
|
||||
pruning rule at its smallest, applied to a two-clause expression by hand. Tape
|
||||
and automatic pruning stay parked; this cut proves the arithmetic they rest on.
|
||||
`iv.hi < CUTOFF` is the interval proof that the `0` branch of
|
||||
`max(0, coverage − CUTOFF)` wins on the whole segment: Fidget's `min`/`max`
|
||||
pruning at its smallest, applied by hand to a two-clause expression. Tape and
|
||||
automatic pruning stay parked; this proves the arithmetic they rest on.
|
||||
|
||||
### Step budget
|
||||
|
||||
`S` is the per-pixel, per-frame evaluation budget (each `snoise` call is one
|
||||
eval, so a dense sample costs 2). `S = 16` on desktop, `S = 8` on a phone,
|
||||
`S = 4` the floor; the controller sets it from measurement, not from the
|
||||
device. A debug mode writes `evals / S` and an "unfinished ray" flag into the
|
||||
colour so Hands can read back the mean and the unfinished fraction. The
|
||||
contract: at the chosen `S` on the shipped field, under 1% of rays are
|
||||
unfinished.
|
||||
`S` counts `snoise` evaluations per pixel per frame. `S = 16` desktop, `8`
|
||||
phone, `4` floor; the controller sets it from measurement. A debug mode writes
|
||||
`evals / S` and an unfinished flag into the colour. Contract: under 1% of rays
|
||||
unfinished at the chosen `S` on the shipped field.
|
||||
|
||||
### TAA
|
||||
### TAA, motion-aware
|
||||
|
||||
The camera never moves and the canvas is fixed, so there is no reprojection:
|
||||
history is the same pixel. What remains is temporal supersampling with
|
||||
rejection:
|
||||
The camera is static but the field moves, so history is reprojected by the
|
||||
known motion, which the flow texture owns:
|
||||
|
||||
- Jitter: a Halton(2,3) frame offset for the ray start, plus interleaved
|
||||
gradient noise per pixel, rotated per frame, so neighbours decorrelate.
|
||||
- Accumulation: `α = max(1 / frames, 1/32)`: the first frames converge fast,
|
||||
then settle. The accumulated value is colour plus transmittance.
|
||||
- Rejection: a 3×3 min/max box of the current frame clamps the history
|
||||
(AABB clamp, not variance clipping). The field's drift is slow enough that
|
||||
the clamp only bites where the field really changed.
|
||||
- Reset on resize, on a token change (route change: Quartz has `enableSPA:
|
||||
false` at `quartz.config.ts:15`, so a route change is a page load and the
|
||||
reset is free) and on the first frame.
|
||||
- Buffers: two RGBA16F history targets at canvas size. 8-bit history stalls
|
||||
convergence at `α = 1/32` for a ground this dark, so half-float is required;
|
||||
if `EXT_color_buffer_half_float` is missing, the controller falls back to
|
||||
the CSS wash.
|
||||
- Motion vector: `mv = project(−flow(uv) · dt)` in pixels; history is fetched
|
||||
at `uv − mv`. Exact for the pattern between cross-fades (see above); the
|
||||
cross-fade and the detail term are handled by rejection.
|
||||
- Jitter: Halton(2,3) frame offset on the ray start plus interleaved gradient
|
||||
noise per pixel, rotated per frame.
|
||||
- Accumulation: `α = max(1/frames, 1/32)` on colour and transmittance.
|
||||
- Rejection: a 3×3 min/max box of the current frame clamps the reprojected
|
||||
history (AABB clamp, not variance clipping); out-of-bounds fetches take
|
||||
`α = 1`.
|
||||
- Reset on resize, on the first frame and on a budget step.
|
||||
- Buffers: two RGBA16F history targets at canvas size (8-bit stalls
|
||||
convergence at `α = 1/32` on a ground this dark); missing half-float falls
|
||||
back to the CSS wash through the controller.
|
||||
|
||||
What is not copied from Aetheria's `CloudShader.shader` pass 2: variance
|
||||
clipping at `γ = 0.5` on an undersampled input (`:303-308`), a fixed 5% blend
|
||||
with out-of-bounds forced to 100% (`:311`), the packed depth/density float
|
||||
(`:181`, `:270`), and the bug at `:311`, which blends the alpha with
|
||||
`density2`, the last 3×3 neighbour's density from the loop above, not the
|
||||
pixel's own.
|
||||
### Stardust (cut `site-stardust`)
|
||||
|
||||
### Resolution
|
||||
Stateless, as Aetheria's: instance `i` is a cell of an `N×N` grid over the
|
||||
slab's mid-plane, hashed by `cultmath_pcg3d` to a spawn point and a lifetime
|
||||
offset; `lifetime = fract(t/PERIOD_P + offset)`; position = spawn − flow(uv
|
||||
of spawn) · lifetime · PERIOD_P, read from the flow texture in the vertex
|
||||
shader; size = `parabola(lifetime, 2)` × a hashed size; colour = the nebula
|
||||
hue at its screen position × a hashed brightness. Rendered as instanced
|
||||
billboards (one 6-vertex quad, `gl_InstanceID`) with additive blending into a
|
||||
half-resolution RGBA16F stardust layer, with the `powerPulse` falloff of
|
||||
`Stardust.shader:74-78`. Count: 16,384 desktop, 4,096 phone, controller-scaled
|
||||
with `S`. The particles and the clouds move as one because they read the same
|
||||
texture; there is no second flow.
|
||||
|
||||
Full device pixels: `canvas.width = innerWidth * devicePixelRatio`. The
|
||||
controller's steps are in order: lower `S` (16 → 8 → 4), then render at CSS
|
||||
pixels (`dpr = 1`), then the CSS wash. The second step is Q2.
|
||||
### Depth of field driven by the raymarch (cut `site-stardust`)
|
||||
|
||||
### Palette and intensity
|
||||
Aetheria's hack: the CoC is a raymarch output, so clouds blur what is behind
|
||||
them. Web version: in the resolve pass, the stardust layer is gathered with an
|
||||
8-tap Poisson disk whose radius is `RMAX · opacity(uv)`, where
|
||||
`opacity = 1 − T` is the TAA-accumulated cloud transmittance at that pixel
|
||||
(the stable, converged value, which is why it is read from history and not
|
||||
from the current frame). Composite: `ground + clouds + stardust_blurred · T`.
|
||||
All stardust is treated as behind the whole cloud layer; a per-depth
|
||||
transmittance would cost a second march and is not in scope.
|
||||
|
||||
Fifteen tokens on `body` in `custom.scss`, read by CSS and by `ground.js`:
|
||||
### Sampled wash: the CSS reads the shader
|
||||
|
||||
```text
|
||||
--gamecult-ground-0/1/2 three gradient stop colours (#03070d #07111a #09141f)
|
||||
--gamecult-light-N "255 138 42" (N = 0..2)
|
||||
--gamecult-light-N-a 0.18
|
||||
--gamecult-light-N-at "78% 14%"
|
||||
--gamecult-light-N-r "18%" (or a length; the ritual ground uses rem)
|
||||
--gamecult-wash from wash-home-only: 1 on /, 0.5 elsewhere
|
||||
```
|
||||
After the resolve, a tiny pass downsamples the final frame to 8×8 (RGBA8).
|
||||
`ground.js` reads it back asynchronously: `readPixels` into a
|
||||
`PIXEL_PACK_BUFFER`, a `fenceSync`, then `getBufferSubData` on a later frame
|
||||
once the fence has signalled. Never a synchronous readback. At most twice a
|
||||
second, and only written when a value moved by more than 1/255:
|
||||
|
||||
The CSS radials and the linear gradient reference the tokens. The ritual essays'
|
||||
block overrides the tokens instead of painting its own ground, which deletes
|
||||
its three literal radials. `ground.js` resolves `%` radii as CSS does (circle,
|
||||
farthest corner of the body's background positioning area) and lengths through
|
||||
the root font size. The parity test: with density forced to 0 the canvas equals
|
||||
the CSS wash within 2/255 per channel at 16 probe points, home and ritual.
|
||||
Intensity multiplies the cloud term; at 0 both paths show the plain gradient.
|
||||
- `--gamecult-ground-0/1/2`: the mean of rows 0–1, 3–4 and 6–7.
|
||||
- `--gamecult-light-N`, `-a`: the colour at the seed light's centre texel,
|
||||
split into the hue (normalised) and the excess over the ground stop there.
|
||||
- `--gamecult-light-N-at`, `-r`: the seed values; positions are not fitted.
|
||||
- `--gamecult-ground-mean`: the 8×8 mean, for any tinted UI.
|
||||
|
||||
These are written to `:root`'s inline style, so they beat the stylesheet's
|
||||
seed declarations. The seeds live on `:root` in `custom.scss` (today's
|
||||
literal wash), with the ritual essays' seeds under
|
||||
`:root:has(body[data-slug="Blog/..."])`. The body background and the ritual
|
||||
ground read the tokens. When WebGL2 is absent or the controller removes the
|
||||
canvas, the seeds are what paints. Across page loads the last sample may be
|
||||
carried as a cache (Q5).
|
||||
|
||||
### Palette input
|
||||
|
||||
The shader reads four input tokens from `custom.scss`, which is where the
|
||||
brand says colour is defined: `--gamecult-nebula-0/1/2` (today orange
|
||||
`255 138 42`, violet `109 96 255`, sky `89 183 255`) and
|
||||
`--gamecult-nebula-base` (`#07111a`), plus `--gamecult-wash` as intensity.
|
||||
Inputs and outputs are different tokens, so there is no cycle (Q4).
|
||||
|
||||
### Resolution and the slow ladder (Q2)
|
||||
|
||||
Full device pixels. The controller's ladder: `S` 16 → 8 → 4, then `dpr → 1`
|
||||
(the stardust layer and DoF follow the canvas), then the CSS wash. Never a
|
||||
step up within a session.
|
||||
|
||||
### Pausing and fallbacks
|
||||
|
||||
- `visibilityState !== "visible"`: no frames. On return, keep history.
|
||||
- `prefers-reduced-motion: reduce`: drift is 0, render until 32 accumulated
|
||||
frames, then stop; resize re-renders. The clouds are still there, still.
|
||||
- No WebGL2, context lost, no half-float: canvas removed, CSS wash shows.
|
||||
- Too slow: measured, see below. Never a user-agent branch.
|
||||
- The canvas is `position: fixed; inset: 0; z-index: -1; pointer-events: none;
|
||||
aria-hidden`. The root background paints first, so the canvas covers the CSS
|
||||
wash only once it has drawn; until then, and if it never does, the wash is
|
||||
what the reader sees.
|
||||
- `visibilityState !== "visible"`: no frames; on return, history is kept and
|
||||
the motion vector uses the real elapsed `dt`, clamped to one frame.
|
||||
- `prefers-reduced-motion: reduce`: time is frozen (phase, flow and particle
|
||||
lifetimes fixed), render until 32 accumulated frames, stop; resize
|
||||
re-renders. The clouds and stardust are there, still.
|
||||
- No WebGL2, context lost, no half-float: canvas removed; the seeds paint.
|
||||
- Too slow: measured; see below.
|
||||
- The canvas is `position: fixed; inset: 0; z-index: -1; pointer-events:
|
||||
none; aria-hidden`. The root background paints first, so the canvas covers
|
||||
the CSS wash only once drawn.
|
||||
|
||||
### Text contrast
|
||||
|
||||
The shader clamps its output so relative luminance never exceeds 0.035, about
|
||||
the current orange hotspot (`#07111a` plus `rgba(255,138,42,.18)` is roughly
|
||||
`(53, 42, 34)`, luminance 0.025). Body text `#b7c7d9` (luminance 0.55) keeps a
|
||||
contrast ratio of at least 7:1 against any ground pixel. A readback test
|
||||
asserts the max over the canvas after 32 frames on `/` at intensity 1.
|
||||
The resolve clamps relative luminance at 0.035 (about today's orange hotspot,
|
||||
`#07111a` + `rgba(255,138,42,.18)` ≈ `(53, 42, 34)`, luminance 0.025), after
|
||||
stardust and DoF are composited. Body text `#b7c7d9` (0.55) keeps ≥ 7:1 on any
|
||||
ground pixel. A readback test asserts the max after 32 frames on `/`.
|
||||
|
||||
## Budget and how it is measured
|
||||
|
||||
| Device | Pixels | GPU per frame at 30 fps | Proxy |
|
||||
| --- | --- | --- | --- |
|
||||
| Desktop, GTX 1070 class, 2560×1440 | 3.7 M | ≤ 1.0 ms | 3% GPU duty |
|
||||
| Laptop iGPU, Iris Xe, 1920×1080 at dpr 1.25 | 3.2 M | ≤ 3.0 ms | 9% duty |
|
||||
| Mid-range phone, Pixel 6a / Galaxy A54 class, 1080×2400 | 2.6 M | ≤ 4.0 ms | ≤ 12% duty; no upward drift of the median over 5 minutes (a thermal detector by measurement, not a sensor) |
|
||||
| Desktop, GTX 1070 class, 2560×1440 | 3.7 M | ≤ 2.0 ms | 6% GPU duty |
|
||||
| Laptop iGPU, Iris Xe, 1920×1080 at dpr 1.25 | 3.2 M | ≤ 3.5 ms | 10% duty |
|
||||
| Mid-range phone, Pixel 6a / Galaxy A54 class, 1080×2400 | 2.6 M | ≤ 4.0 ms | ≤ 12% duty; no upward drift of the median over 5 minutes |
|
||||
|
||||
The cost model that sizes `S`: one 3D simplex is ~150 flops; a phone GPU
|
||||
delivers ~1 TFLOP; 2.6 M × 8 evals × 150 is 3.1 GFLOP, about 3 ms, plus ~1 ms
|
||||
for the resolve pass (10 texel reads per pixel). Those are estimates; the
|
||||
measurements replace them.
|
||||
Cost model for the full stack on the phone (one `snoise` ≈ 150 flops, ~1
|
||||
TFLOP available; estimates, replaced by measurement):
|
||||
|
||||
Measurement, in this order of preference, all in `ground.js` behind a debug
|
||||
flag and used by the controller:
|
||||
| Pass | Work | ms |
|
||||
| --- | --- | --- |
|
||||
| Flow texture, quarter res | 160 k texels × 2 snoise | 0.05 |
|
||||
| March at `S = 8` | 2.6 M × 8 snoise | 3.1 |
|
||||
| TAA resolve + DoF gather | ~19 texel reads per pixel | 1.0 |
|
||||
| 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 |
|
||||
|
||||
1. `EXT_disjoint_timer_query_webgl2` where present (Chrome desktop):
|
||||
per-frame GPU time of the march pass and the resolve pass.
|
||||
2. A `fenceSync` after the resolve pass, polled; the time from issue to
|
||||
signalled is the GPU time when the queue is otherwise empty, which a
|
||||
background page's queue is. Available on every WebGL2, including Safari
|
||||
and phones.
|
||||
3. `requestAnimationFrame` cadence as the last signal: if frames are
|
||||
delivered under 24 fps for 30 consecutive frames while the page is idle,
|
||||
the controller steps down.
|
||||
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.
|
||||
|
||||
Hands proves it in the browser pane: desktop numbers from the pane with the
|
||||
timer query, phone numbers from the pane's mobile viewport emulation for the
|
||||
pixel count and from the operator's phone for the real GPU time (operator
|
||||
verification). Each landed number goes into the cut report, not into prose.
|
||||
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
|
||||
desktop) per pass; else a `fenceSync` after the resolve, polled (universal on
|
||||
WebGL2, phones and Safari included; the queue of a background page is
|
||||
otherwise empty); else `requestAnimationFrame` cadence. Hands proves it in the
|
||||
browser pane: desktop numbers with the timer query, the phone's pixel count
|
||||
from the mobile preset, the phone's GPU time from the operator's phone.
|
||||
|
||||
## The proving-ground contract (what Asura and the tapes campaign inherit)
|
||||
## The proving-ground contract (what the tapes campaign inherits)
|
||||
|
||||
1. **Enclosure tests in C#** (and so in HLSL by the mirror): for every `iv_*`
|
||||
op, random intervals and random points inside them, `f(x) ∈ op(I)`. For
|
||||
`iv_snoise_ball` and `iv_fbm_ball`, 2,000 random balls with 64 points each.
|
||||
A mutant that shrinks any bound dies.
|
||||
2. **The Lipschitz constant has provenance**: `CULTMATH_SNOISE_LIPSCHITZ` is
|
||||
the max of `|snoise_grad|` over 10⁶ random points refined by gradient
|
||||
ascent, times a stated margin (1.10). The test that measures it is
|
||||
committed and the constant is pinned by a test that fails if any sampled
|
||||
gradient exceeds it. It is empirical with a margin, not a proof; the map
|
||||
says so, and the enclosure tests are the defence.
|
||||
3. **Tightness is a number**: the mean ratio of interval width to the true
|
||||
range over the sampled balls, printed by a committed test and quoted in
|
||||
the cut report, so a later affine-arithmetic cut has something to beat.
|
||||
4. **The saving is a number**: a C# test marches 1,000 random rays through a
|
||||
representative coverage field with and without interval skipping and
|
||||
asserts at least a 2× reduction in evaluations; the real field's mean
|
||||
evals per pixel and unfinished-ray fraction come from the browser readback.
|
||||
5. **Three runtimes agree**: the GLSL golden fixture (256 points, bit
|
||||
patterns from C#) passes on WebGL2 within 1 ulp for the integer-hash paths
|
||||
and a stated tolerance for the transcendental ones, with the tolerance and
|
||||
the device named in the report.
|
||||
1. **Enclosure tests in C#** (and HLSL by the mirror): every `iv_*` op over
|
||||
random intervals and points; `iv_snoise_ball`/`iv_fbm_ball` over 2,000
|
||||
balls × 64 points. A mutant that shrinks any bound dies.
|
||||
2. **The warp is enclosed**: for random flows bounded by `D`, every point
|
||||
`warp_k(p)` over the segment lies in the enlarged ball and `snoise` of it
|
||||
lies in `iv_snoise_ball(c, r + D)`. This is the test the site's march rests
|
||||
on.
|
||||
3. **The Lipschitz constant has provenance**: the max of `|snoise_grad|` over
|
||||
10⁶ points refined by gradient ascent, × 1.10, measured by a committed
|
||||
test and pinned by another. Empirical with a margin, not a proof; the
|
||||
enclosure tests are the defence.
|
||||
4. **Tightness is a number**: mean interval width over true range, printed
|
||||
by a committed test, for a later affine cut to beat.
|
||||
5. **The saving is a number**: a C# test marches 1,000 rays through a
|
||||
representative warped coverage field with and without interval skipping
|
||||
and asserts ≥ 2× fewer evaluations at equal transmittance; the shipped
|
||||
field's evals per pixel and unfinished fraction come from the browser.
|
||||
6. **Three runtimes agree**: the GLSL golden fixture passes on WebGL2, with
|
||||
tolerance class and device named in the report.
|
||||
|
||||
What this cut does **not** prove, said plainly: tape evaluation, automatic
|
||||
pruning, the `min`/`max` choice tracking, and interval ops over anything but
|
||||
plain `[lo, hi]`. Those unpark with steps 2–4.
|
||||
Not proved here, said plainly: tape evaluation, automatic pruning, `min`/`max`
|
||||
choice tracking, affine arithmetic. Those unpark with steps 2–4.
|
||||
|
||||
## Cut order
|
||||
|
||||
```text
|
||||
glsl-lowering (CultLib) ─┐
|
||||
interval-ops (CultLib) ─┴─> site-ground (gamecult-site, after wash-home-only)
|
||||
interval-ops (CultLib) ─┴─> site-ground (clouds, flow, TAA, sampled wash) ─> site-stardust (particles, DoF)
|
||||
^ after site-masthead wash-home-only
|
||||
```
|
||||
|
||||
Three cuts, not one. One cut would be three repos' worth of concerns and
|
||||
three Soul vocabularies (a text transformer, numerical bounds, a browser
|
||||
budget); CultMath cuts have overrun 2× when they carried two concerns
|
||||
(`Asura cut-map.md:131`). `glsl-lowering` and `interval-ops` are independent
|
||||
and can run in parallel worktrees; `interval-ops` gets its GLSL for free from
|
||||
the lowering test once both are on `main`. No CultMath release is in this
|
||||
sequence: the site consumes a commit, not a package. The next CultMath release
|
||||
carries the intervals to Unity consumers.
|
||||
Four cuts. The two CultLib cuts are as in pass 1, except `interval-ops` gains
|
||||
the warp enclosure test. `site-ground` grew (flow texture, reprojection, the
|
||||
readback and token restructure); `site-stardust` is split out because its
|
||||
Soul vocabulary is different (particle statistics, blur quality, the composite)
|
||||
and because the ground must be measured on its own before the stardust is
|
||||
charged against the same budget. No CultMath release is in the sequence; the
|
||||
site consumes a commit.
|
||||
|
||||
Specs: `docs/cultmath-interval-ground-cut-glsl-lowering.spec.json`,
|
||||
`docs/cultmath-interval-ground-cut-interval-ops.spec.json`,
|
||||
`docs/cultmath-interval-ground-cut-site-ground.spec.json`.
|
||||
`docs/cultmath-interval-ground-cut-site-ground.spec.json`,
|
||||
`docs/cultmath-interval-ground-cut-site-stardust.spec.json`.
|
||||
|
||||
## Standing design decisions (means; Self may overrule)
|
||||
|
||||
- D1. Intervals are `float2(lo, hi)`. Affine arithmetic is not taken; the
|
||||
tightness number from the contract says whether it is ever worth it.
|
||||
- D2. No `iv_*` function has a value-only or gradient twin. The bounds take
|
||||
the point value only; `snoise_grad` is used by the Lipschitz test, not by
|
||||
the bound.
|
||||
- D3. The site vendors `CultMath.glsl` by CultLib commit, with the SHA in the
|
||||
file header and a check in the site cut that the bytes equal CultLib's at
|
||||
that SHA. A build-time fetch would make the site's build depend on another
|
||||
repo being reachable.
|
||||
- D4. The ground shader is opaque and reproduces the gradient and the lights
|
||||
from the tokens. A transparent canvas over the CSS wash would need the CSS
|
||||
radials hidden by a class (two ground states, a flash on load), and parsing
|
||||
the computed `background-image` string would make the palette authority a
|
||||
serializer's whim.
|
||||
- D5. The history reset on token change is a page load, because SPA is off. If
|
||||
tightness number says whether it is ever worth it.
|
||||
- D2. No `iv_*` function has a value-only or gradient twin; the bounds take
|
||||
the point value only.
|
||||
- D3. The site vendors `CultMath.glsl` by CultLib commit, SHA in the header, a
|
||||
drift check in the site cut.
|
||||
- D4. The flow texture is the one owner of motion. Warp, motion vectors, hue
|
||||
and particles read it; nothing evaluates flow a second way.
|
||||
- D5. Routes are page loads (`enableSPA: false`, `quartz.config.ts:15`); if
|
||||
SPA is ever enabled, `ground.js` listens for Quartz's `nav` event and
|
||||
re-reads the tokens; the spec names the hook so it is not forgotten.
|
||||
- D6. The budget controller holds state for the session only (no storage).
|
||||
Measuring costs under a second and a stored verdict would outlive a driver
|
||||
update.
|
||||
re-reads the input tokens.
|
||||
- D6. The budget verdict is session-only; a stored verdict would outlive a
|
||||
driver update.
|
||||
- D7. Inputs and outputs are different tokens: four nebula inputs, fifteen
|
||||
ground outputs with seeds. The shader never reads an output; CSS never reads
|
||||
an input except through the shader's result.
|
||||
- D8. The ground tokens are written on `:root` inline, so the seed in the
|
||||
stylesheet loses to the sample without a class or a second rule.
|
||||
- D9. The DoF blurs all stardust by the final opacity; per-depth transmittance
|
||||
is out of scope.
|
||||
- D10. Stardust and DoF land after the ground has been measured alone, so the
|
||||
report can say what each costs.
|
||||
|
||||
## Questions
|
||||
|
||||
**Q1. Campaign home.**
|
||||
- (a) New campaign `cultmath-tapes`, target `docs/cultmath-tape-target.md`
|
||||
unparked for step 1, repos CultLib and gamecult-site. **Recommended.** It is
|
||||
the campaign the operator called "planned", and the site is its first
|
||||
consumer.
|
||||
- (b) File the CultLib cuts under `asura` as 2a-iii and the site cut under
|
||||
`site-masthead`. Rejected: both targets say this work is not theirs.
|
||||
**Q4. Where the shader's palette input comes from.**
|
||||
- (a) Four new input tokens in `custom.scss` (`--gamecult-nebula-0/1/2`,
|
||||
`--gamecult-nebula-base`), seeded with today's wash colours.
|
||||
**Recommended.** The nebula palette can be tuned without moving the UI
|
||||
accent, and the brand doc's rule (colour is defined in `custom.scss`) holds.
|
||||
- (b) Reuse `--secondary` and `--tertiary` from `quartz.config.ts` and promote
|
||||
the violet to a config token. Fewer tokens; ties the nebula to the UI accent
|
||||
for good.
|
||||
|
||||
**Q2. The second step of the slow-device ladder.**
|
||||
- (a) After `S` hits its floor, render at CSS pixels (`dpr = 1`) before giving
|
||||
up to the CSS wash. **Recommended.** It is still a measured response, and a
|
||||
2× pixel cut is a 2× GPU cut, which keeps clouds on more phones.
|
||||
- (b) Never drop below device pixels; a device that fails at `S = 4` gets the
|
||||
CSS wash. Purer to the order, but it loses the phones one notch too slow.
|
||||
|
||||
**Q3. Ground authority in CSS.**
|
||||
- (a) Fifteen tokens on `body`, referenced by the CSS gradients and read by
|
||||
the shader; the ritual block overrides tokens. **Recommended.** One
|
||||
definition of the ground, two renderers.
|
||||
- (b) Keep the literals in CSS and duplicate the palette in `ground.js`.
|
||||
Rejected: the brand doc says the brand is defined in `custom.scss`; a copy
|
||||
in JS is a second authority that drifts.
|
||||
**Q5. Carrying the last sampled wash across page loads.**
|
||||
- (a) `ground.js` keeps the last written token set in `sessionStorage` and
|
||||
applies it before the first paint on the next page; the seeds apply when
|
||||
nothing is stored. A cache, never authority. **Recommended.** Without it,
|
||||
every navigation shows the seed wash for the first frames, then the shader.
|
||||
- (b) No storage; the seed paints first on every page. Simpler; a visible
|
||||
shift on every navigation.
|
||||
|
||||
## Unsettled
|
||||
|
||||
- The Lipschitz constant is empirical with a margin. If a reader wants a
|
||||
proof, the Ashima kernel's gradient is a bounded polynomial per simplex
|
||||
cell and a proof is possible; it is not in this cut.
|
||||
- The ritual ground's radii are `rem` lengths and its gradient has three
|
||||
stops but the first has no position (`custom.scss:124`). Hands normalises
|
||||
it to three positioned stops when tokenising; the appearance is unchanged.
|
||||
- Whether GameCult-Quartz's bundler lets a component's `afterDOMLoaded` do a
|
||||
dynamic `import()` of a static module, or whether the loader must be a
|
||||
classic script tag in `Head`. Hands reads the engine; both are one line.
|
||||
- The Lipschitz constant is empirical with a margin; a per-cell polynomial
|
||||
proof is possible and not in this cut.
|
||||
- `_DoFBlurTex`'s writer is not in the Aetheria checkout; the web DoF takes the
|
||||
operator's recollection (final transmittance) as the driver. If stills of
|
||||
the video arrive, Hands can match the blur radius against them, but the
|
||||
mechanism does not depend on it.
|
||||
- The ritual ground's radii are `rem` lengths and its first gradient stop is
|
||||
unpositioned (`custom.scss:124`); Hands normalises it when tokenising.
|
||||
- Whether GameCult-Quartz's bundler permits a dynamic `import()` from a
|
||||
component's `afterDOMLoaded` or needs a script tag; one line either way.
|
||||
- `:root:has(body[data-slug])` for the ritual seeds: supported in every
|
||||
current browser; a browser without `:has` gets the default seeds, which is
|
||||
acceptable for a fallback of a fallback.
|
||||
|
|
|
|||
Loading…
Reference in a new issue