Resolves Soul's five findings on cut-interval-ops.h2 and carries the rulings operator-wells-contract-vs-ceiling and operator-analytic-wall-body. glsl-lowering r2 and site-ground r3 now depend on interval-ops r3; the site-ground spec names the one shipped void with (Rc, e) derived from (Rh, S) and the body-start word. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1009 lines
61 KiB
Markdown
1009 lines
61 KiB
Markdown
# CultMath intervals and the gamecult.org ground: cut map
|
||
|
||
Status: Imagination pass 4, 2026-10-03. Pass 1's Q1–Q3 are answered (below);
|
||
the Self opened the `cultmath-tapes` campaign from pass 2; pass 3 revises the
|
||
two CultLib cuts after their first Hands reports and the operator's challenge
|
||
to the march, and changes the site field to envelope-then-noise; pass 4
|
||
(section "Pass 4") revises `interval-ops` to r3 after its second Hands report
|
||
and Soul's verdict, and names the one shipped void (Q9). Where a later pass and
|
||
an earlier section disagree, the later pass wins. Where this map and the Body
|
||
disagree, the Body wins and this map is stale.
|
||
|
||
Pinned HEADs (every `file:line` below is against these):
|
||
|
||
| Repo | Ref | SHA |
|
||
| --- | --- | --- |
|
||
| 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`, `Compute/Stardust/Stardust.{compute,shader,cs}`, `CustomDoF/DepthOfField.hlsl`, `Zone Display/VolumeCloudRenderer.cs` |
|
||
|
||
## The operator's words
|
||
|
||
1. "Can we replace the site's background with a fancy yet unobtrusive and
|
||
inexpensive shader thing? It's odd that my main site wouldn't have that,
|
||
considering my specialization. Would it be too much to have some optimized
|
||
subset of Aetheria's volumetric clouds shader in there?"
|
||
2. After the Self proposed a quarter-resolution, 8–16 step, static-on-phones
|
||
design: "map it as a cut that includes the GPU interval arithmetic we're
|
||
borrowing from Fidget for a planned Asura campaign. I'd like this shader to
|
||
be the proving ground for that - with the right math we can absolutely run
|
||
this fullscreen on mobile, per pixel as long as the sample count is low and
|
||
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"
|
||
|
||
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
|
||
|
||
### Where the interval math lives
|
||
|
||
**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"). 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. 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 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
|
||
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
|
||
|
||
`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,
|
||
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 four cuts)
|
||
|
||
- 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; 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
|
||
|
||
### What Aetheria contributes, and what the web version keeps
|
||
|
||
| 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`)
|
||
|
||
Pass 3 wraps this field in an envelope (a nebula floor with authored wells and
|
||
clear sky above it); `coverage` below becomes the noise that displaces the
|
||
floor, as in Aetheria. The flow, the phases and the hue stand.
|
||
|
||
```text
|
||
camera at the origin, looking +z, static; the slab z in [Z0, Z1] fills the view
|
||
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
|
||
```
|
||
|
||
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, with the warp
|
||
|
||
Pass 3 supersedes this section's march: the probe is per screen tile in a
|
||
pre-pass, the field has an analytic envelope the probe tests first, and the
|
||
loop below becomes the pre-pass's loop over cell ranges. The ball and the warp
|
||
enlargement stand.
|
||
|
||
`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
|
||
while t < t_exit and evals < S:
|
||
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
|
||
```
|
||
|
||
`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` 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, motion-aware
|
||
|
||
The camera is static but the field moves, so history is reprojected by the
|
||
known motion, which the flow texture owns:
|
||
|
||
- 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.
|
||
|
||
### Stardust (cut `site-stardust`)
|
||
|
||
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.
|
||
|
||
### Depth of field driven by the raymarch (cut `site-stardust`)
|
||
|
||
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.
|
||
|
||
### Sampled wash: the CSS reads the shader
|
||
|
||
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:
|
||
|
||
- `--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, 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 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 | ≤ 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 |
|
||
|
||
Cost model for the full stack on the phone (one `snoise` ≈ 150 flops, ~1
|
||
TFLOP available; estimates, replaced by measurement):
|
||
|
||
| Pass | Work | ms |
|
||
| --- | --- | --- |
|
||
| Flow texture, quarter res | 160 k texels × 2 snoise | 0.05 |
|
||
| Tile pre-pass (pass 3), 1/64 res, `N = 8` | 40 k tiles × ≈ 30 probes in the cavity (interior retired in a few, the wall's ≈ 15 cells at 1.5 probes each), envelope-only because the noise ball cannot prove anything at this scale; ≈ 0.1 snoise-equivalents per pixel; worst case ≤ 192 probes per tile | 0.1 (≤ 0.8) |
|
||
| March at `S = 8` | 2.6 M × 8 snoise per frame, every pixel marches a wall; LOD and the mask decide where the frame's samples land, not how many (the controller does) | 3.3 |
|
||
| TAA resolve + DoF gather | ~19 texel reads per pixel, plus the depth-history fetch and one matrix multiply for the camera reprojection | 1.1 |
|
||
| Sun self-shadowing sample (named, not taken) | one envelope-only transmittance sample per primary sample, ≈ +25% of the march | +0.8, not on the phone |
|
||
| Stardust layer, half res, 4,096 particles | fill-bound, small | 0.2 |
|
||
| 8×8 downsample + PBO readback at 2 Hz | tiny | 0.05 |
|
||
| **Total at `S = 8`, the cavity (pass 3)** | | **≈ 4.8** |
|
||
| Total at `S = 6` / `S = 4` | | ≈ 3.9 / 3.1 |
|
||
|
||
Said plainly: LOD does not change the per-frame cost, which the controller
|
||
fixes by `S`; it changes how much a frame's samples are worth. At `S = 8` the
|
||
cavity does not fit the 4 ms phone budget on the estimate (≈ 4.8); `S = 6`
|
||
sits at the line and `S = 4` is under it. What gives is `S`, and with the
|
||
mask and LOD that is nearly free: a converged wall needs about 12 snoise per
|
||
pixel in total, so `S = 4` converges in about three frames through the TAA
|
||
instead of thirty, and `S = 8` in two. Then `dpr`, as before. What gives, in order, is the
|
||
sample count, then the resolution; the stardust and the DoF are never dropped
|
||
before the clouds' samples; the self-shadowing sample is not taken on any
|
||
device until measured. On desktop at `S = 16` the stack is ≈ 1.9 ms.
|
||
|
||
Measurement, in this order, all in `ground.js` behind a debug flag and used by
|
||
the controller: `EXT_disjoint_timer_query_webgl2` where present (Chrome
|
||
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 the tapes campaign inherits)
|
||
|
||
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** (amended in pass 3): a C# test marches tiles of
|
||
rays through four scenarios, an envelope-then-noise field with authored
|
||
empty space among them, with the dense march and with the tile-amortized
|
||
interval march, and asserts ≥ 2× lower combined cost at equal
|
||
transmittance on the authored scenarios; the uniform slab is printed as
|
||
the worst case. 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.
|
||
|
||
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 (clouds, flow, TAA, sampled wash) ─> site-stardust (particles, DoF)
|
||
^ after site-masthead wash-home-only
|
||
```
|
||
|
||
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-stardust.spec.json`. Revision 2 of
|
||
the two CultLib cuts (pass 3):
|
||
`docs/cultmath-interval-ground-cut-interval-ops.r2.spec.json`,
|
||
`docs/cultmath-interval-ground-cut-glsl-lowering.r2.spec.json`. Revision 3 of
|
||
`interval-ops` (pass 4): `docs/cultmath-interval-ground-cut-interval-ops.r3.spec.json`;
|
||
`glsl-lowering` r2 and `site-ground` r3 now depend on it.
|
||
|
||
## Standing design decisions (means; Self may overrule)
|
||
|
||
- D1. Intervals are `float2(lo, hi)`. Affine arithmetic is not taken; the
|
||
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 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.
|
||
|
||
## Pass 3: the envelope, the scenarios, the tile pre-pass, the split Phacelle
|
||
|
||
Pinned: CultLib `main` `d0ea37f8`; `hands/cultmath-interval-ops` at
|
||
`c9bd003b` (report `cut-interval-ops.h1`); `hands/cultmath-glsl-lowering` at
|
||
`5ae207e8` (report `cut-glsl-lowering.h1`, branched from interval-ops).
|
||
Rulings: `operator-tile-amortized-march`, `operator-split-phacelle`,
|
||
`operator-compiler-downloads`, `self-exp-measure-in-site`,
|
||
`operator-ground-void-brush` (answers Q6; the void scene below).
|
||
|
||
### The operator's words
|
||
|
||
On the march: "I'm skeptical about the interval march, both in implementation
|
||
and measurement. Are we testing it in scenarios where we're actually giving
|
||
it empty space to skip? Surely if the pre distortion SDF represents a clear
|
||
sky with the camera above the fog and aimed up, the interval math can quickly
|
||
tell us not to bother marching very far, no? In Aetheria the levels are
|
||
authored to place deep gravity wells for the player to gaze across, with
|
||
dynamic action spaced around the well, and finding the empty space there is
|
||
the whole point of using intervals, no?"
|
||
|
||
He is right. r1's saving test marched one `snoise` octave against a cutoff
|
||
through a slab with coverage everywhere: the worst case for intervals, and
|
||
0.93x said nothing about real scenes.
|
||
|
||
### What r1 measured, and what was wrong in the march
|
||
|
||
| Finding (`NoiseBoundTests.cs:344-376` at `c9bd003b`) | Effect | r2 |
|
||
| --- | --- | --- |
|
||
| Every probe charged one `snoise` per ray | a failed probe cost a third of a dense sample | one probe per tile (the ruling) |
|
||
| Coarse probes attempted where the ball could never prove emptiness: `hi = n(c) + L r` clamps to 1, so `L r F0 ≥ 1 + cutoff` is a guaranteed failure; at `StepMax = 2`, `L r F0 = 3.7` | every coarse probe wasted; the 0.74x row | a noise probe is attempted only when `L r F0` leaves room to prove `hi < cutoff`; otherwise the level is "maybe" for free |
|
||
| Growth capped at `StepMax = 8 Step0` | clear space cost `Depth / StepMax` probes | ranges double to the grid's end |
|
||
| The dense sample evaluated the detail octave even at zero occupancy | the baseline was dearer than a sane shader | coverage first, detail only when density can be nonzero (Aetheria's `if (dist < _SafetyDistance)`) |
|
||
| The finest-level probe before each dense cell | fine; amortized it is `1/N²` for a chance at 3 snoise | kept |
|
||
| No field structure | nothing to skip | envelope-then-noise, below |
|
||
|
||
### The field, from Aetheria
|
||
|
||
`Volumetric.cginc:165-221` with `:91-102`, `Zone.cs:369-397`
|
||
(`GetHeight`), `Zone.cs:426` (`PowerPulse`), `Settings.asset`
|
||
(`DefaultEnvironment`, `PlanetSettings`):
|
||
|
||
```text
|
||
s(p) = p.y + h(p.xz) the pre-distortion SDF; h ≥ 0 the well map
|
||
h(xz) = PowerPulse(|xz| / 2R, 2) · 64 + Σ_b PowerPulse(|xz − c_b| / r_b, 16) · d_b
|
||
PowerPulse(x, e) = (1 − 4x²)^e on [0, ½], 0 beyond; r_b = 500 M^0.25, d_b = 30 M^0.175
|
||
fade(s) = 1 − smoothstep(0.75 S, S, s) S = SafetyDistance 30: no noise above it
|
||
s'(p) = s + A · fade(s) · n(warp(p)) n = the two-phase warped coverage + ½ detail
|
||
density = max(0, (F − s') / B) F = FloorOffset −20, B = FloorBlend 10
|
||
```
|
||
|
||
Above the safety distance the density is exactly 0 and the noise is never
|
||
evaluated, by the dense march too. The warp applies to the noise argument
|
||
only; the height is read at the unwarped `xz` (Aetheria reads
|
||
`_NebulaSurfaceHeight` at `pos.xz` and warps inside `triNoise3d`).
|
||
|
||
### The composed bound (invariant `intervals-enclose`, kept)
|
||
|
||
Over a tile's frustum slice `[z0, z1]`, every step an existing `iv_*` op:
|
||
|
||
- Envelope over the slice's exact box, not the ball: `y = iv_mul([z0, z1],
|
||
m_y)`, `x = iv_mul([z0, z1], m_x)` with the tile's slope intervals;
|
||
`iv_h` = Σ over bowls of `[pulse(far), pulse(near)]`, `near`/`far` the
|
||
least and greatest distance from the box to the bowl centre (monotone, so
|
||
exact per bowl). `iv_s = iv_add(y, iv_h)`; `iv_fade` its ordered endpoints.
|
||
- Noise over the ball: `iv_n = iv_snoise_ball(ball.xyz · F0, ball.w · F0)`,
|
||
`ball = iv_frustum_ball(m_c, z0, z1, N / (√2 f), D)`: centre on the tile's
|
||
central ray at mid-depth, radius `(Δ/2)·|(m_c, 1)| + z1·N/(√2 f) + D`.
|
||
The warp enlarges only this radius; the envelope's box takes none.
|
||
- `iv_s' = iv_add(iv_s, iv_scale(iv_mul(iv_fade, iv_n), A))`; the slice is
|
||
provably empty iff `iv_s'.lo ≥ F`. When `iv_fade.hi = 0` the noise term is
|
||
identically zero and `iv_snoise_ball` is not called: the envelope proves
|
||
the slice empty at no `snoise` cost. This is why tiles compound: one
|
||
envelope probe per tile can retire a whole ray length.
|
||
|
||
Two tests pin it: `TileBallEnclosesEveryRaySegment` (every ray's points,
|
||
sub-pixel jitter and warp included, inside the ball) and
|
||
`EnvelopeBoundEnclosesDensity` (the composed bound encloses `density` at
|
||
warped points of 2,000 slices, no tolerance).
|
||
|
||
### Scenarios and predictions (combined cost, `e = 0.2` snoise per envelope evaluation, `N = 8`)
|
||
|
||
| Scenario | Setup | Prediction |
|
||
| --- | --- | --- |
|
||
| (a) height fog | `h = 0`, camera at `y = S + 30`, rays level and up | > 50x: dense pays 256 envelope evaluations per ray; the tile about ten probes shared by 64 rays; 0 snoise either way |
|
||
| (b) inside the fog | `h = 0`, camera at `y = F − 10`, rays level | 0.8–1.0x: every ray saturates in a few cells; the pre-pass still probes the whole grid |
|
||
| (c) Aetheria-like | zone bowl `R = 2000` + four wells (masses 100, 1000, 10000, 1000), camera above the fog in the bowl, rays gazing across (`m_y ∈ [−0.25, 0.1]`), Aetheria's units and 256-sample quadratic grid | 2–4x: the clear cells before the fog surface collapse to a few probes per tile; the fog cells cost the same on both sides. The noise ball proves nothing at Aetheria's scale (`L r F0 ≥ 1.6` at the finest cell with `D = 60`), so it is not evaluated; the saving is the envelope's |
|
||
| (d) uniform slab | r1's field, kept as the worst case | 1.3–1.5x: the ceiling at `L = 10.099261` with the cheaper baseline |
|
||
| (e) the void, the shipped scene | the true cavity, hollow radius `Rh = 198`, ramp `S = 50`, camera 186 from the centre, low; every ray crosses interior and ends in a wall; three marches (fixed-step reference, masked fixed-step, masked LOD) | **steps per pixel ≈ 35 → 6 (5–8x fewer), combined cost 1.7x, snoise alone 1.25x**, from the model below. The noise ball proves nothing (`L r F0 ≥ 1.5`); the interior is retired by the envelope, the ramp is sampled at the content limit |
|
||
|
||
The 2x contract on combined cost is asserted on (a) and (c) at `N = 8`; (b)
|
||
and (d) are printed. (e) is the headline: its contract is steps per pixel
|
||
(below), and it prints cost and snoise beside it.
|
||
|
||
### Footprint LOD, and the cavity cost as a function
|
||
|
||
The operator's words: "Surely this depends on the size of the cavity and the
|
||
camera placement, no? The slope of the wall's density gradient offset
|
||
determines the region we have to sample in. How much we actually sample that
|
||
region depends on how close it is to the camera. A 10-wide gradient 1,000
|
||
units away only needs one raymarching step worth of detail, right?"
|
||
|
||
Right. Pass 3b costed every wall at a fixed step. The march is now
|
||
footprint-aware, in the site and in the saving test:
|
||
|
||
- `θ_px = 2 tan(fov/2) / H`; the footprint at distance `z` is `z θ_px`.
|
||
- Step `Δ(z) = max(k z θ_px, min(S/4, λ_fine(z)/2))`, `k = 4`: never finer
|
||
than four footprints, otherwise the finer of the two content limits (four
|
||
samples across the ramp, two per wavelength of the finest live octave).
|
||
- Octave `i` of wavelength `λ_i` carries weight `w_i(z) = 1 −
|
||
smoothstep(λ_i/2m, λ_i/m, z θ_px)`, `m = 4`: one smooth blend per octave,
|
||
no popping; TAA hides the per-frame sparsity.
|
||
- The weights are part of the field, so the bound encloses the truncated
|
||
field exactly: the noise interval over a slice is `Σ_i iv_mul([w_i(z1),
|
||
w_i(z0)], iv_snoise_ball(c F_i, r F_i))`, `w_i` monotone in `z`.
|
||
`intervals-enclose` holds for what the shader evaluates.
|
||
- The pre-pass says where each ray's ramp begins; LOD says how finely to
|
||
sample it once there. They compound. The reference a march that does not
|
||
know where the wall is must pay is the fixed step `min(10, S/4)` everywhere.
|
||
|
||
The cavity is parametrised by the hollow radius `Rh` and the ramp width `S`;
|
||
the brush `(1 − (d/Rc)²)^e` with `CARVE` gives `Rh = Rc √(1 − CARVE^(−1/e))`
|
||
and `S` from the exponent (`e = 4` gives `S ≈ 1.3 Rh`, `e = 0.25` gives
|
||
`S ≈ 0.12 Rh`), so `(Rh, S)` are the knobs and `(Rc, e)` derived. Shipped:
|
||
`Rh = 198`, `S = 50`, so `Rc = Rh + S = 248` and `e = −ln CARVE / ln(1 −
|
||
(Rh/Rc)²) = 0.40`, `K = 1/30` (pass 4 corrects this line's `e ≈ 0.6, Rc ≈
|
||
220`, which did not satisfy the formula; Q9 holds the look question).
|
||
|
||
Imagination's model (scratchpad `cavity_model.py`; the test confirms or
|
||
refutes), 1080p, `N = 8`, per pixel for a converged frame; "ref" is the
|
||
fixed-step reference's steps:
|
||
|
||
| `Rh` | camera | `S = 10` steps / snoise / cost ratio | `S = 50` | `S = 150` |
|
||
| --- | --- | --- | --- | --- |
|
||
| 100 | low | 6 / 12 / 2.6x | 6 / 12 / 1.6x | 14 / 36 / 1.3x |
|
||
| 200 | low (**shipped**, ref ≈ 35 steps) | 6 / 12 / 3.2x | **6 / 12 / 1.7x** | 14 / 36 / 1.4x |
|
||
| 400 | low | 6 / 12 / 4.0x | 6 / 12 / 1.9x | 14 / 36 / 1.5x |
|
||
| 1000 | low | 5 / 9 / 8.2x | 6 / 12 / 2.5x | 14 / 36 / 1.7x |
|
||
| 1000 | centred | 4.3 / 7 / 12.9x | 6 / 12 / 2.9x | 14 / 36 / 1.8x |
|
||
|
||
Centred and mid offsets differ from low by under 10% at every point. What
|
||
the table says: within the frustum the footprint is under 4 units, so the
|
||
step is set by the content limits (`S/4`, and the detail octave's 12.5-unit
|
||
half-wavelength), not by distance; the "10-wide gradient 1,000 units away"
|
||
row is where the footprint finally bites (4.3 steps, 7 snoise). The sample
|
||
count is nearly flat in `Rh` and camera offset and rises with `S` only once
|
||
the detail wavelength, not the ramp, sets the step. What makes the shipped
|
||
scene cheaper without losing the mood, in order: a lower per-frame sample
|
||
budget, which LOD and the mask make nearly lossless (`S_budget = 4` converges
|
||
in about three frames at the walls); a thinner ramp (`S = 10` is 3.2x on cost
|
||
but reads as a surface; 25–50 keeps the fog); a longer detail wavelength or
|
||
no detail octave at the far wall; a larger hollow, which changes the
|
||
composition and costs nothing per pixel.
|
||
|
||
**Q8 retired.** At the shipped point the shipped march takes 5–8x fewer steps
|
||
than the fixed-step reference and 1.7x less combined cost; the contract for
|
||
(e) is the number that scales with GPU time, steps per pixel: ≥ 2x fewer than
|
||
the fixed-step reference at the shipped parameters, with snoise per pixel and
|
||
combined cost printed beside it and promised nothing. The test asserts that;
|
||
the browser measures it (`?ground=debug` against `?ground=dense`). A shortfall skips the assertion naming `saving-2x-scenarios`; the
|
||
fields, grids and `e` are not tuned. The honest summary: at a Lipschitz bound
|
||
near 10 the noise ball is weak everywhere; what intervals buy on these fields
|
||
is the envelope's empty space, found exactly, and the tiles make finding it
|
||
nearly free.
|
||
|
||
### The site ground: the void (ruling `operator-ground-void-brush`)
|
||
|
||
The operator's words: "There was an old main menu visual with a purely
|
||
authored field, first thing I did all the way back when the shader was new. I
|
||
believe it was a single negative pseudo gaussian spherical brush creating a
|
||
void in the clouds, with a sun gently nestled at the bottom lighting the
|
||
cavity and orbiting along a track with the camera following low, very moody,
|
||
gives us lots of options for authoring the flow field and stardust
|
||
distribution"
|
||
|
||
**The original, found.** Aetheria `e6181c11` ("Main Menu", 2021-03-13):
|
||
`Assets/Scenes/Main Menu.unity`, the `Sun` prefab instance under a `Center`
|
||
pivot, `Materials/Brushes/Gravity Well.mat` and
|
||
`Main Menu Sun Fog Tint.mat`, `Fog Marching Main Menu.mat` on
|
||
`VolumeRender.shader` of that commit. What survives, in its units:
|
||
|
||
| Element | Original value |
|
||
| --- | --- |
|
||
| The brush | `PowerBrush` `(1 − x²)^16`, depth 40, radius 256 (half depth at r = 52; a Gaussian of σ ≈ 45); the sun's own gravity-well child. A boundary bowl (depth 75, power 0.25, radius 1500) is flat across the scene: a constant 75 |
|
||
| The floor | offset −16.46, blend 39.6: the fog top is `y = −91` outside the void, `−131` at the sun |
|
||
| The sun | sphere radius 12.5 at `y = −130`: the bottom of the void |
|
||
| The orbit | `Center` rotates 5°/s about y (72 s period); the sun at radius 175 |
|
||
| The camera | Cinemachine vcam following the sun (framing transposer, distance 150, damping 10, handheld noise amplitude 1 at 0.25 Hz), looking at `Look Target` `(0, −130, 0)`, the orbit centre at the sun's depth; saved at `y = −27` |
|
||
| The light | authored, not marched: the sun's tint brush `(1 − x²)^3`, depth 2, radius 128, orange `(1, .4, .1)`, plus a scene-wide blue ambient tint `(0.18, 0.59, 1)` × 0.01 |
|
||
| Stardust | 256² particles, spacing 2, floor −60, ceiling 0, height exponent `800 / (ceiling + wellDepth)`: more headroom where the well is deeper, so the void fills with stardust |
|
||
| Noise | strength 150, frequency 0.01, safety 50 |
|
||
|
||
Not surviving: the 2021 `d()` displacement details (the 2021-05-30 "Volumetric
|
||
Redux" rewrote it), the tint map's runtime contents, the video. The 2021
|
||
brush was radial in `xz` and carved a bowl in a cloud floor; the operator
|
||
ruled (Q7, `operator-void-true-cavity`) that the site's void is a true 3D
|
||
hollow inside a cloud volume that surrounds it on every side, including
|
||
above. Everything else recovered carries over.
|
||
|
||
**The site scene** (`site-ground` r3; the brush parameters in this paragraph,
|
||
`Rc = 640`, `e = 4`, are superseded by pass 4's one shipped void, `Rh = 198`,
|
||
`S = 50`, `Rc = 248`, `e = 0.40`, pending Q9): the original's units. Outer volume: an
|
||
unbounded uniform fill (chosen over a shell: every ray then ends in a wall
|
||
and the march terminates by opacity, never by a far plane; `Z1 = 2 Rc` bounds
|
||
the pre-pass range only). The carve: `d = |p − c_v|`, `g(d) = (1 −
|
||
(d/Rc)²)^4` for `d < Rc`, `Rc = 640`, `CARVE = 1.5`; `density = K · max(0,
|
||
1 − CARVE · g(d'))`, exactly 0 inside the hollow radius `Rh = Rc √(1 −
|
||
CARVE^(−1/4)) = 198`, half at `0.49 Rc`, full at `Rc`; `K = 1/30`, so a wall
|
||
is opaque within ≈ 150 units of entry. Noise displaces the wall only:
|
||
`fade(d) = smoothstep(Rh − S, Rh, d)`, `S = 50`, `d' = d + A · fade · n`,
|
||
`A = 20`, `F0 = 0.01`. Its bound over a tile's slice: `d`'s interval is the
|
||
box's least and greatest distance to `c_v` (exact); `g`, `fade` and the carve
|
||
are monotone in it; the slice is provably empty iff `d_hi + A · fade(d_hi) ·
|
||
n_hi ≤ Rh`, and deep inside (`fade = 0`) no noise is evaluated. `Rc` grew
|
||
from the brush's 256 because a 3D hollow must hold the camera.
|
||
|
||
The sun orbits the origin at radius 175, period 72 s; the cavity centre is
|
||
`c_v = sun + (0, Rh − 12.5, 0)`, so the sun sphere rests on the hollow's
|
||
bottom and the hollow moves with it. The camera follows 120 behind the sun
|
||
along the tangent and 45 above it: 186 from `c_v`, 12 inside the hollow, low,
|
||
looking at `sun + (0, 60, 0)` so the lit lower wall and the dark upper wall
|
||
share the frame. The 2021 follow distance 150 and height 25 do not fit a
|
||
hollow a camera can stand in; 120 and 45 are the re-derivation. There is no
|
||
sky: behind a fully transmissive pixel the resolve shows the base token, and
|
||
only an unfinished ray (budget exhausted before the wall, measured, under 1%)
|
||
reaches it. `worldAt(t)` in `ground.js` is the one
|
||
owner of sun, cavity and camera; nothing integrates motion. The flow is authored
|
||
around the void: a swirl `SWIRL · tangent · (1 − (r/Rw)²)²` (largest
|
||
mid-slope, zero at rim and centre) plus the global noise flow, rendered into a
|
||
world-space window of side `4 Rw` around the sun, so warp, hue, motion vectors
|
||
and the stardust read one texture (D4). The stardust (next cut) spawns in the
|
||
bowl with the original's height rule and is lit by the same pulse.
|
||
|
||
**Lighting, cheapest moody version:** the original's mechanism made 3D. At a
|
||
sample, `light = SUN · (1 − (d/Rl)²)^3` for `d = |p − sun| < Rl = Rh = 198`
|
||
(the original's 128 scaled with the hollow), plus the blue ambient; `colour =
|
||
density · (light + ambient) · hue(flow)`. Zero extra `snoise`; the lower wall
|
||
within `Rl` of the sun glows and the upper wall stays dark because the pulse
|
||
is zero beyond `Rl`, which is the moody split; the sun inside the hollow is
|
||
occluded by nothing, so it is a small additive disc in the resolve, occluded
|
||
only by the accumulated transmittance along its ray. A self-shadowing step (one envelope-only
|
||
transmittance sample toward the sun per primary sample, no noise, ≈ +25% on
|
||
the march) is named, not taken: desktop-only if measurement ever allows.
|
||
|
||
**TAA with a moving camera:** the main pass writes the depth-weighted mean
|
||
march distance to an `R16F` history channel; the resolve reconstructs the
|
||
world point at that depth, moves it back by the field motion (`p + flow·dt`),
|
||
projects it with the previous frame's view-projection and fetches history
|
||
there. Same 3×3 clamp, same `α`, same resets. Colour, transmittance and depth
|
||
are reprojected by the one motion vector.
|
||
|
||
**Fit with the tokens:** `--gamecult-nebula-0` (orange) is the sun's colour,
|
||
`--gamecult-nebula-2` (sky) the ambient, `--gamecult-nebula-1` the cloud hue's
|
||
second pole, the base the sky. Outputs as pass 2, with one change:
|
||
`--gamecult-light-0-at` is written from the sun's projected position, so the
|
||
CSS wash's main light follows the sun; its seed is the sun's mean screen
|
||
position, so the fallback is a still of the same composition.
|
||
|
||
**Soundness with a moving camera:** each frame's pre-pass evaluates
|
||
`iv_frustum_ball` from that frame's camera; no ball, mask or probe result is
|
||
carried across frames. The only cross-frame state is the TAA history, which
|
||
is colour, not a soundness input. The saving test draws each tile's camera
|
||
independently and says so.
|
||
|
||
The march: a **low-resolution pre-pass** at `W/8 × H/8` runs the tile loop
|
||
over the slab's cell ranges (the envelope first, the noise ball only when the
|
||
envelope leaves `fade.hi > 0` and `L r F0` leaves room) and writes a per-tile
|
||
skip mask of the dense cells not proven empty into an `RGBA32UI` texel (128
|
||
bits; 96 cells at the slab's fine step). The main pass reads one texel
|
||
(`texelFetch`) and places its `S` samples in unmasked cells only, stratified
|
||
over the unmasked length; masked cells contribute exactly zero, so this is
|
||
exact importance sampling, not an approximation. The fragment cannot share a
|
||
probe across pixels (derivatives reach a 2×2 quad at best), so the pre-pass
|
||
is the cheaper and the only sound option in WebGL2. Cost at `N = 8` is in the
|
||
budget table: ≤ 144 probes per tile worst case (nothing skipped, hierarchy
|
||
descended everywhere), mostly envelope-only, ≈ 0.4–2 snoise-equivalents per
|
||
pixel; `N = 16` quarters it but makes the ball's lateral term equal the fine
|
||
half-length at the slab's far plane. **Predicted `N = 8`.** The site's own
|
||
ratio is measured in the browser against a dense reference with the same
|
||
envelope gate (the proving-ground contract's second half).
|
||
|
||
### The split Phacelle and the compile checks (glsl-lowering r2)
|
||
|
||
Pass 4: the glsl branch rebases onto interval-ops **r3**'s head, because r3
|
||
changes `cultmath_iv_frustum_ball`'s radius and the fixture family with it.
|
||
|
||
- Transformation step 1 gains one licence-keyed entry: an include under its
|
||
own licence is lowered into its own file. `CultMath.glsl` (MIT: `CultMath.hlsl`
|
||
+ `CultMath.Interval.hlsl`) and `CultMath.Phacelle.glsl` (MPL-2.0,
|
||
concatenated after it only by consumers that call `cultmath_phacelle`; the
|
||
site does not). Both pinned; `THIRD-PARTY-NOTICES.md` lists the MPL files
|
||
and no longer calls `CultMath.glsl` MPL.
|
||
- `iv_exp` joins the fixture with a platform tolerance (1 ulp, the generating
|
||
OS named), because `self-exp-measure-in-site` has the site measure WebGL2
|
||
`exp` against it. The fixture is regenerated for `cultmath_iv_frustum_ball`.
|
||
- glslang 16.6.0 and dxc are downloaded pinned and run: the MIT-only wrapper,
|
||
the two-file wrapper, a `lerp(` negative control, and a `cultmath_phacelle`
|
||
call against the MIT-only wrapper that must fail (proves the split).
|
||
- The glsl branch rebases onto interval-ops r2's head; its first commit is the
|
||
regeneration alone.
|
||
|
||
Carried, still out of scope: the Unity `CultMath.dll` rebuild (no release in
|
||
the sequence); the fixture's ulp bounds on WebGL2 (site cut); Stryker's
|
||
timeouts are reported as timeouts, not kills.
|
||
|
||
## Pass 4: Soul's five findings, the analytic body, the one shipped void
|
||
|
||
Pinned: CultLib `main` `780d8cfa` (docs only since `d0ea37f8`);
|
||
`hands/cultmath-interval-ops` at `43183276` (report `cut-interval-ops.h2`,
|
||
verdict `cut-interval-ops.s1`, worktree `F:\Projects\CultLib-iv`). Rulings new
|
||
in this pass: `operator-wells-contract-vs-ceiling` ((c) is held to at least 90%
|
||
of its probe-free ceiling, not 2x: the ceiling is what intervals control; the
|
||
fog band's cost belongs to the field) and `operator-analytic-wall-body` (where
|
||
the envelope interval proves the density constant, one closed-form
|
||
Beer-Lambert step instead of sampling at the content limits). Spec:
|
||
`docs/cultmath-interval-ground-cut-interval-ops.r3.spec.json`.
|
||
|
||
### What r2 measured
|
||
|
||
| Scenario | r2 result | Pass 3 prediction |
|
||
| --- | --- | --- |
|
||
| (a) height fog | 16384x | > 50x |
|
||
| (b) inside the fog | 0.99x | 0.8–1.0x |
|
||
| (c) Aetheria wells | 1.41x at warp D (3.32x at warp 0); probe-free ceiling 1.42x (3.46x) | 2–4x |
|
||
| (d) uniform slab | 1.23x | 1.3–1.5x |
|
||
| (e) void, shipped | steps per pixel 36.34 → 16.45 (2.21x fewer), cost 1.34x, snoise 26.6 | 35 → 6 (5–8x), cost 1.7x |
|
||
|
||
Two things the pass 3 model got wrong, both now owned by r3: the footprint
|
||
step kept its content limits (`S/4`, the detail half-wavelength 12.5) inside
|
||
the wall's constant-density body, which the model had costed at two steps; and
|
||
the noise band does not end at `Rc`: with `A = 20` and the free noise interval
|
||
`[−1.5, 1.5]`, the density is provably `K` only from `d ≥ Rc + 30`. The ruling
|
||
answers the first; the second is content and stays.
|
||
|
||
### Soul's findings and what r3 does with each
|
||
|
||
| Finding | Severity | r3 |
|
||
| --- | --- | --- |
|
||
| `frustum-ball-float-crossing` | Medium | the radius is widened by a derived float32 bound (below); seven extreme families join the enclosure test; the strictness sentence leaves the C#, the HLSL and `design.md` |
|
||
| `aetheria-far-plane-miscited` | Low | `far = 2048`, the Main Camera's (`ARPG.unity:31777`, `:31830`); the Tint camera's 1000 was the wrong camera |
|
||
| `shipped-void-two-scenes` | Medium | one shipped void, `(Rh, S) = (198, 50)`, `(Rc, e) = (248, 0.40)` derived; the map, the site-ground spec and the test agree; the look question is Q9 |
|
||
| `lod-weights-never-exercised` | Low | configuration "LOD far" (`Rh = 4000`, `S = 50`, low camera, `f = 467`) reaches weights in `(0, 1)` and at 0; the `[w(z1), 1]` bound is recorded as a deviation with its soundness argument |
|
||
| `stryker-timeout-survivor` | Low | 2525 named equivalent; ball radii to `1e3` so `L/r` dies to an enclosure test; every timeout hand-run; the report claims only demonstrated kills |
|
||
|
||
### The float widening rule (finding 1)
|
||
|
||
`iv_frustum_ball` returns a float32 ball; its promise is that every exact
|
||
point `(m z, z)` of every ray of the tile, `m` in the footprint, `z ∈ [z0,
|
||
z1]`, moved by at most the warp, lies inside it. Soul showed the r2 ball misses
|
||
such points by up to `5.7e-4 r` when `z0 == z1`, `3.3e-4 r` at `z ≈ 1e7` and
|
||
19% of `r` at `|m_c| ≈ 1000`: the triangle inequality is an equality on the
|
||
degenerate slice and the centre's own rounding crosses it. The rule, in C# and
|
||
both HLSL copies, after the r2 radius:
|
||
|
||
```text
|
||
radius += (|c.x| + |c.y| + |c.z| + radius) · 2^-20 (9.5367431640625e-7, exact)
|
||
```
|
||
|
||
Derivation with `u = 2^-24`: the centre `c = (m_c z_m, z_m)` carries at most
|
||
two roundings per component (`z_m = fl(z0 + z1) · 0.5`, then the product), so
|
||
`|c_float − c_exact| ≤ 2u |c|₁`; the radius carries at most seven first-order
|
||
roundings (`z1 − z0`; the axis `sqrt(mx² + my² + 1)` at `3u`; the product; `z1
|
||
· footprint`; the sum; `+ warp`), so `r_exact ≤ r_float (1 + 7u)`; the
|
||
widening's own addition rounds once more. `2^-20 = 16u` covers `8u (|c|₁ + r)`
|
||
twice over. fma contraction removes roundings and only tightens, so dxc's HLSL
|
||
and any driver's GLSL are covered by the same bound. Cost at the shipped
|
||
scales: `|c|₁ ≈ 600`, `r ≈ 5` → `6e-4` units, 0.01% of the radius. The
|
||
enclosure test computes distances in double from the float ball and the float
|
||
ray, no tolerance, over the r2 domain and the extreme families (`z0 == z1`;
|
||
`z1 − z0 = 1e-6 z1`; `|m_c| ≤ 1000`; `z ≤ 1e7`; `z0 = 0, z1 ≥ 1e-3`; `f ≥ 1`;
|
||
warp to `1e6`), corners at both depth ends with the flow pointed outward. The
|
||
derivation pin becomes two-sided (`r_exact ≤ ball.w ≤ r_exact + 2^-19 (r_exact
|
||
+ |c|₁)`), so a ball that forgets the widening fails and one that grows past
|
||
the rule fails. The caller's world transform and its own point rounding remain
|
||
the caller's; the function says so in its doc instead of a false proof.
|
||
|
||
### The one shipped void (finding 3) and Q9
|
||
|
||
The map held two voids: the Footprint-LOD section's `(Rh, S) = (198, 50)`
|
||
(with an `e ≈ 0.6, Rc ≈ 220` that did not satisfy its own formula) and the site
|
||
section's `Rc = 640, e = 4` (a 442-wide ramp with a 50-wide fade band). Hands
|
||
implemented the first with the arithmetic corrected (`Rc = Rh + S = 248`, `e =
|
||
0.40`) and measured 2.21x; Soul measured the second at 1.93x. Under the
|
||
analytic body the second gains nothing: across a 442-wide ramp the optical
|
||
depth exceeds 4 before the density is provably constant, so rays end inside the
|
||
ramp and the body is never proven. The map's own rule says `(Rh, S)` are the
|
||
knobs, so the honest choice is the first; it changes the look relative to the
|
||
site section, hence Q9 (below). For `CARVE = 1.5` the ramp width is tied to the
|
||
exponent (`S/Rh = 1/√(1 − 1.5^(−1/e)) − 1`: `e = 0.4 → 0.25`, `e = 0.7 →
|
||
0.5`, `e = 1 → 0.73`, `e = 4 → 2.2`), so "pseudo-Gaussian" and "narrow" cannot
|
||
both be had without a third knob, and a narrow wall at `e = 4` needs
|
||
`CARVE ≈ 58`, which is a hard wall again. The ramp width is the look-cost knob.
|
||
|
||
### The analytic body (ruling `operator-analytic-wall-body`)
|
||
|
||
Consumer-side, no new CultMath function; the saving test and the site mirror
|
||
each other.
|
||
|
||
- **When it applies.** The consumer's `Bound` over the whole remaining range
|
||
`[z, z_end]`, gated, is a point interval with positive value: for the void,
|
||
the envelope with the free noise interval `[−1.5, 1.5]` proves `fade = 1`
|
||
and `d_lo + A · (fade n)_lo ≥ Rc`, so every point of every ray of the tile
|
||
over the rest of the grid has density exactly `K` for any noise value. For
|
||
(a)–(d) no bound is a positive point (the fog's density varies with `s'`),
|
||
so the step never applies there, and the test asserts it.
|
||
- **Pre-pass.** `MarchTile` returns `(dense cells, bodyStart, bodyDensity)`:
|
||
when a single cell is not provably empty and its own bound is a positive
|
||
point, one more probe over `[z_j, z_end]`; if that is a positive point too,
|
||
`bodyStart = j` and every remaining cell is appended dense without probing
|
||
(always sound; it also drops the body's probes). The site writes the cell in
|
||
the mask texel's fourth word, `0xFFFFFFFF` when none.
|
||
- **Main pass.** Unmasked cells before `bodyStart` as before; at `bodyStart`,
|
||
`T *= exp(−K · (z_end − z) · |dir|)`, one step, stop. The grid end for every
|
||
void configuration becomes `|camera − c_v| + Rc + 6/K` (was `5/K`): the
|
||
body starts at `d ≥ Rc + 30` plus box slack, so `6/K` leaves at least 4.9 of
|
||
optical depth and `T ≤ 0.0075`. No probe in the main pass: the constant is
|
||
the field's pointwise envelope value at that cell.
|
||
- **Pin.** `AnalyticBodyMatchesFineMarch`: wherever a body is reported, the
|
||
bound over the rest of the grid is a positive point and the full and the
|
||
truncated density at 64 sampled points of the body equal it exactly; the
|
||
analytic factor equals the fixed-step product over the same cells within
|
||
`1e-4` relative; no (a)–(d) tile reports a body. The masked fixed-step march
|
||
stays the mask's soundness pin (exact agreement with the reference); only
|
||
the LOD march takes the step. Hand mutation that must fail: in
|
||
`VoidField.Compose`, `CarveRadius → HollowRadius` in the lo test.
|
||
|
||
### Footprint LOD, exercised (finding 4)
|
||
|
||
At `f = 935` every octave weight is 1 inside every r2 grid (the fine weight
|
||
drops below 1 past `z = 2922`; the largest grid ends at 2240). `VoidField`
|
||
takes a focal length, and one configuration reaches the weights: `Rh = 4000`,
|
||
`S = 50`, low camera, `f = 467` (a 98° vertical FOV at 1080 rows): fine weight
|
||
below 1 past 1460 and 0 past 2920, coarse below 1 past 5840, grid to ≈ 7990;
|
||
the footprint term `4 z θ` also exceeds 12.5 past 1460, so `LodStep` is
|
||
footprint-set there. It joins the enclosure test's field list and is printed by
|
||
the saving test, not asserted. Deviation recorded: the bound takes each weight
|
||
over `[w(z1), 1]`, not `[w(z1), w(z0)]`, because one `Bound` serves the full
|
||
field (`w = 1`) and the truncated one (`w ∈ [w(z1), w(z0)]`); `[w(z1), 1]`
|
||
contains both and `iv_mul` is monotone in its interval argument, so the
|
||
composed interval encloses both fields; the slack is only in far tiles whose
|
||
step the footprint already sets. Hand mutation that must fail the truncated
|
||
check there: `w(z0)` for `w(z1)`.
|
||
|
||
### Contracts after r3 (asserted at `N = 8`)
|
||
|
||
- (a): at least 2x combined cost. Measured 16384x.
|
||
- (c): at least 0.90 of its probe-free ceiling (`Ratio / Ceiling`, the lower
|
||
of warp 0 and D). Measured 0.99 at warp D, 0.96 at warp 0.
|
||
- (e): at least 2x fewer LOD steps per pixel than the fixed-step reference at
|
||
the one shipped void; the masked march exact; the LOD march within 0.02.
|
||
- (b), (d), the (e) grid and LOD-far: printed only.
|
||
|
||
### Predictions (Imagination's rule, not a measurement)
|
||
|
||
From r2's printed rows: the LOD steps that evaluated noise are about
|
||
`snoise / 2.8` plus two pre-band steps; the rest were constant-body steps,
|
||
which the analytic step makes one. Ref/LOD steps:
|
||
|
||
| `Rh`, camera | `S = 10` r2 → r3 | `S = 50` r2 → r3 | `S = 150` r2 → r3 |
|
||
| --- | --- | --- | --- |
|
||
| 100, low | 2.1 → 5.3x | 1.8 → 2.5x | 1.6 → 1.6x |
|
||
| 200, low | 2.7 → 5.8x | 2.2 → 2.8x | 1.8 → 1.8x |
|
||
| 400, low | 3.4 → 6.2x | 2.9 → 3.5x | 2.2 → 2.2x |
|
||
| 1000, low | 6.8 → 10.9x | 4.5 → 5.2x | 3.3 → 3.3x |
|
||
| 1000, centred | 12.4 → 30x | | |
|
||
| **shipped** (198, low, 50) | | **2.21 → ≈ 2.9x (12.5 steps), cost 1.34 → 1.38x, snoise 26.6 unchanged** | |
|
||
|
||
At `S = 150` rays end inside the ramp before the body is proven, so nothing
|
||
changes there. If the shipped point lands under 2.6x, the report says which
|
||
assumption failed: the pre-band's 0-snoise steps, or a body start later than
|
||
`Rc + 30` by box slack.
|
||
|
||
### Authority map delta (against pass 3's)
|
||
|
||
- `iv_frustum_ball` is now the owner of its own float32 rounding: the
|
||
widening is part of the function. Consumers add nothing for it and remain
|
||
owners of their world transform.
|
||
- The body proof is derived from the same `Bound` the mask is derived from, by
|
||
one more probe; the main pass reads it and never decides it. The Beer-Lambert
|
||
step is the consumer's integration, not a CultMath function.
|
||
- `(Rc, e)` are no longer authored; they are derived from `(Rh, S)`. The site
|
||
section's `Rc = 640, e = 4` is no longer an owner of the look (Q9 pending).
|
||
- (c)'s contract is derived from its own run (0.90 of the ceiling); no absolute
|
||
target is written for (c).
|
||
- Deletion line: the false strictness sentence, the Tint camera citation, the
|
||
(c) 2x assertion and the second shipped void leave before their replacements
|
||
are written. Net in `src`: one line and a doc comment.
|
||
|
||
### Dependent specs
|
||
|
||
`glsl-lowering` r2 now depends on `interval-ops` r3 and rebases onto its head
|
||
(the fixture family for `cultmath_iv_frustum_ball` changes with the widening).
|
||
`site-ground` r3 now depends on `interval-ops` r3; its field item carries the
|
||
one shipped void with `(Rc, e)` derived, its pre-pass writes the body-start
|
||
cell in the texel's fourth word, its main pass takes the one analytic step,
|
||
and `Z1 = |camera − c_v| + Rc + 6/K` (614 at the shipped point, 62 cells of
|
||
10; the mask has 96 bits). Both files are edited in place on `main`; the
|
||
mind's copies need re-admission by Self.
|
||
|
||
## Questions
|
||
|
||
**Q9. The one shipped void** (pass 4; finding `shipped-void-two-scenes`).
|
||
Authored as `(Rh, S)` with `(Rc, e)` derived; `CARVE = 1.5`, `A = 20`,
|
||
`F0 = 0.01`, `K = 1/30`, the camera as site-ground r3.
|
||
- (a) `Rh = 198`, `S = 50` → `Rc = 248`, `e = 0.40`. The scene r2 measured
|
||
(2.21x; predicted ≈ 2.9x with the analytic body). A 50-wide noisy wall whose
|
||
outer edge has a kink (`e < 1`), softened by the ±20 noise displacement and
|
||
the TAA. **Recommended**: the map's own rule, the measured scene and the
|
||
ruling's analytic body all point at it.
|
||
- (b) `Rc = 640`, `e = 4` (`Rh = 198`, a 442-wide pseudo-Gaussian ramp, fade
|
||
band 50): the site section's soft wall. Measured 1.93x; the body is never
|
||
proven, so the analytic step never fires; the (e) contract fails unless it
|
||
is dropped to printed or the detail octave is removed where the ramp is
|
||
wide, both of which are the operator's to decide.
|
||
- (c) `Rh = 198`, `S = 100` → `Rc = 298`, `e = 0.70`: the compromise, a
|
||
softer 100-wide wall; predicted ≈ 2.4x; not in the r2 grid, so unmeasured.
|
||
|
||
Hands codes (a); another ruling moves only the shipped constants here, in the
|
||
r3 spec and in the site-ground spec.
|
||
|
||
**Q6. The site ground's envelope look.** Answered outside the options by
|
||
`cultmath-tapes:ruling:operator-ground-void-brush`: the old main menu's void
|
||
(pass 3, "The site ground: the void").
|
||
|
||
**Q7. What "spherical" means for the brush.** Ruled
|
||
`cultmath-tapes:ruling:operator-void-true-cavity`: a true 3D hollow inside a
|
||
cloud volume that surrounds it on every side, including above; not the 2021
|
||
bowl.
|
||
|
||
**Q8. `cavity-contract`.** Retired in pass 3d (see "Footprint LOD"): the
|
||
contract for (e) is steps per pixel against the fixed-step reference at the
|
||
shipped parameters, ≥ 2x fewer, predicted 5–8x; cost and snoise are printed
|
||
beside it and promised nothing.
|
||
|
||
Specs for pass 3: `docs/cultmath-interval-ground-cut-site-ground.r3.spec.json`
|
||
beside the two CultLib r2 specs.
|
||
|
||
**Q4. Where the shader's palette input comes from.**
|
||
- (a) Four new input tokens in `custom.scss` (`--gamecult-nebula-0/1/2`,
|
||
`--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.
|
||
|
||
**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; 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.
|