Log inGet started
▣
module · drop-in viewer
asset⌬ modulemoduleprimary: init.luau·originates fromworld db2c16ac-a…

asc_dunefield

ASCENSION's dune sea — everything solid in the finale. It replaces `asc_terrain`, whose four ridge bands were the retired sunset valley.

byzero-proxy @ DESKTOP-DB3UJOJ·posted 3mo ago
What it does

asc_dunefield

ASCENSION's dune sea — everything solid in the finale. It replaces asc_terrain, whose four ridge bands were the retired sunset valley.

Four pieces of geometry. One deforming field: a 224 × 224 grid over 1400 world units (50 176 vertices, ~99 k triangles) whose positions and normals are written every frame by asc_dune.computeShader, a kind: vertices pass. Three static parallax ranges at z = −880 / −1500 / −2400, silhouettes with no deform cost and no shadow cast, because a 1400 unit field ends at a visible edge and the horizon of a dune sea has to be dunes. And the playa they all stand on, a 9000 unit plane half a unit under the field's tapered rim, so the horizon is the real horizon rather than the field's own boundary.

The deform's height law is the sum of four terms and a pure function of the clock bus: transverse dunes with a gentle windward ramp and a steep lee slip face; the migration of that whole train downwind by windAccum × CREST_RATE; scour lowering and rounding the crests as the storm planes them off; and the tornado's passage, swept along the 24-sample ground track up to torTrackN as a trench with twin berms and radial rake. There is deliberately no accumulation buffer — the director seeks constantly, and an accumulator would make every seek a different desert and a held reveal frame irreproducible.

Across t 26.25 → 29.0 the field is driven flat inside TYPE_PAN (x −170…170, z −250…−60) with a 24 unit feathered edge, on aux.typeReveal. That is FINISH's stage; this module builds it and does not draw the letters.

The one named cross-lane resource

ctx.data.duneHeights is the handle of the f32 buffer the deform pass writes its heights into, and ctx.data.duneHeightsMeta is { n, w, cell }. The tornado sites its base on the deformed sand and the storm lifts its grains off the real crests by reading those two, rather than duplicating this module's noise and drifting from it. Vertex i is iz * n + ix.

Checking it

M.check(st) answers the two questions a deform pass has to answer about itself from the engine rather than from prose — renderer.mesh.boundsSource(guid) reports "compute" once a pass has written the vertices, and ref:status() reports whether the dispatches actually ran, since a dispatch call returning true means queued. M.sampleHeights(st) starts a readback of the heights buffer.

Bus scalars it reads

windDir windStr windAccum scour aux.torTrack aux.torTrackN aux.typeReveal aux.quality worldLight visRange aux.skyHorizon aux.skyVoid skyMix aux.sceneExposure. It does not read gust: a dune does not wobble at 3 Hz.

Interface

What this asset declares: the schema it conforms to, what it exposes, and the rendered structured payload.

conforms to

zero/source-extract/v2

preseed/asc/asc_dunefield.module — ASCENSION: the dune sea, and the live deformation. This aspect owns everything solid in the finale: one deforming dune field, three static parallax ranges behind it for horizon depth, and the flat playa all four stand on. It replaces `asc_terrain`, whose four ridge bands were the retired sunset valley. THE DEFORM IS IN THE MESH VERTICES, ON THE GPU, EVERY FRAME. `asc_dune.computeShader` declares `kind: vertices`, is handed this mesh's vertex buffer plus the layout it is in, and writes a position and a recomputed normal per vertex. Once a pass has written a mesh's vertices the engine bounds that mesh by what the pass wrote and culls against that, so the field needs no oversized placeholder to survive culling — `renderer.mesh.boundsSource(guid)` reports "compute" and that is checked in `M.check()` rather than asserted. WHY THERE IS NO ACCUMULATION BUFFER, AND WHY THAT IS NOT A SHORTCUT. The obvious way to carve a passage is to keep a state grid and stamp into it each frame. The director seeks constantly, and an accumulator makes every seek a different desert: the same t would come off the screen as five different deserts depending on how it was arrived at, and a held reveal frame would never be reproducible. So the passage is integrated instead — the clock publishes the WHOLE 24-sample ground track plus how much of it is in the past, and the shader sweeps it in one pass as a pure function of t. Same t, same desert, always. THE ONE NAMED CROSS-LANE RESOURCE. `ctx.data.duneHeights` is the handle of the f32 buffer the deform pass also writes its heights into, and `ctx.data.duneHeightsMeta` is `{ n, w, cell }`. The tornado sites its base on the deformed sand and the storm lifts its grains off the real crests by reading those two, instead of duplicating this module's noise and drifting from it. BUS SCALARS THIS ASPECT READS, and only these: `windDir` `windStr` `windAccum` `scour` `aux.torTrack` `aux.torTrackN` `aux.typeReveal` `aux.quality` `worldLight` `visRange` (its far fade only) `aux.ambColor` `aux.skyHorizon` / `aux.skyVoid` / `skyMix` / `aux.sceneExposure` (the haze match, below). It does NOT read `gust`: a dune does not wobble at 3 Hz.

phraseRows(rows: { string }) → void

argtypedescription
rows{ string }

reflectance(c: { number }) → void

argtypedescription
c{ number }

hash2(x: number, z: number, seed: number) → number

argtypedescription
xnumber
znumber
seednumber

vnoise2(x: number, z: number, seed: number) → number

argtypedescription
xnumber
znumber
seednumber

ridged2(x: number, z: number, seed: number) → number

argtypedescription
xnumber
znumber
seednumber

buildRange(ctx: ?, o: ?, mat: ?) → void

One far range: a band displaced by a dune-profiled ridge field, its crest fading to zero at both depth edges so the mesh boundary never shows, and its outer vertex ring folded below the playa so the sides do not end in mid-air.

argtypedescription
ctx?
o?
mat?

buildFieldMesh(n: number, w: number) → void

Vertex i is `iz * n + ix`, which is the order the deform pass indexes the heights buffer in and the order the four normal neighbours are found in. It is the one thing the CPU and the GPU side of this aspect have to agree on.

argtypedescription
nnumber
wnumber

skyLinear(row: { number }, void: { number }, skyMix: number, se: number) → void

Solved rather than picked. The dome receives §3's sky rows as DISPLAY fractions; this surface is lit and mixes in scene-linear pre-tonemap. The linear value that lands on display `d` after the tonemap at this frame's total multiplier is `invAces(d) / sceneExposure`, so reading the dome's OWN folded horizon through that inverse makes the farthest sand meet the sky at the horizon by construction — and it keeps meeting it when the palette is retuned, because nothing here re-derives the palette.

argtypedescription
row{ number }
void{ number }
skyMixnumber
senumber

pumpReadback(st: ?) → void

argtypedescription
st?

groundAt(st: ?, x: number, z: number) → number

Bilinear sample of the last drained grid, in part-local xz. Returns nil until the first readback has landed, so a caller can fall back rather than sit at 0.

argtypedescription
st?
xnumber
znumber

build(ctx: ?, st: ?, ck: ?) → void

argtypedescription
ctx?
st?
ck?

enter(ctx: ?, st: ?, ck: ?) → void

argtypedescription
ctx?
st?
ck?

setMat(mat: ?, name: ?, value: ?) → void

argtypedescription
mat?
name?
value?

update(ctx: ?, st: ?, ck: ?, dt: ?) → void

argtypedescription
ctx?
st?
ck?
dt?

duneGroundAt(x: number, z: number) → void

argtypedescription
xnumber
znumber

exit(ctx: ?, st: ?) → void

argtypedescription
ctx?
st?

check(st: ?) → void

The two questions a deform pass has to answer about itself, answered from the engine rather than from prose: did the dispatch RUN (a dispatch call returning true means queued, not ran), and is the mesh now bounded by what the pass wrote. Called from an execute; `require` is environment-local, so it is handed the running part's state table.

argtypedescription
st?

sampleHeights(st: ?) → void

The heights buffer, read back and reduced. This is the data half of every claim this aspect makes about its own geometry.

argtypedescription
st?

Sub-parts

Everything contained inside this part. Assets are composite children (clickable cards). Files are leaf payloads. Expand any row to view its source.

Showing up to 3 levels deep. Open a part to see what it holds further down.

24+items
▣
module · born here
❒asset
# geo A module script.
▲ 1↑ born
▣
module · born here
❒asset
# asc_clock — ASCENSION: the shared clock The finale's spine. One pure function of part-local `t` produces every number that crosses between the part's eight aspects. **Nothing else in ASCENSION reads raw `t`, wall time or a frame counter** — the orchestrator evaluates this module once per frame and hands the resulting table to every aspect hook. Eight aspects reading the same numbers resolve into one scene. Eight aspects each keying off their own idea of the beat do not. That is the entire reason this module exists. ## How you use it Your hook is handed the bus. It is the third argument, always: ```lua function M.update(ctx, st, ck, dt) entity(st.id).localPosition = ctx.at(0, ck.seedY, 0) -- yes -- local phase = t / 1.25 -- no. never. end ``` `build(ctx, st, ck)` and `enter(ctx, st, ck)` get it too, evaluated at t = 0. To read it outside the update chain, `asc_clock.current()` returns the last published bus (`nil` before the part's first frame). ## The bus — the brief's §3, and nothing else Top level is exactly the §3 contract. Every value is hard-clamped to its stated range; a curve can be retuned but it cannot escape its bounds. | field | range | what it is | |---|---|---| | `t` | 0 … 35 | part-local seconds | | `bar` | 0 … 28 | `t / 1.25`, float. `floor(bar)` is the bar index | | `phase` | 1 … 7 | `floor(bar/4) + 1` | | `sunElev` | 4 … 12 | degrees above horizon | | `sunAzim` | 118 | degrees. Constant for the whole part | | `sunInt` | 0 … 12 | sun intensity, engine units. Already multiplied by `worldLight` | | `sunColor` | `{r,g,b}` | linear | | `skyMix` | 0 … 1 | 0 = full daylight gradient, 1 = flat void black | | `depth` | 1 … 0 | camera submersion. 1 at y = −4.2, 0 at y ≥ 0, crosses 0 at t = 5.000 exactly | | `alt` | −4.2 … 900 | camera altitude in units | | `rise` | 0 … 1 | column raise envelope. 0 before 2.50, 1 at 12.50 | | `colTopY` | 0 … 900 | column top height, units | | `strike` | 0 … 1 | bolt envelope. Impulses on the resolved cue and at +3 / +6 beats, 12-frame decay each | | `flicker` | 0 … 1 | residual column glow, 11 Hz, live from the strike to 25.0, amplitude 1 → 0.25 | | `ignite` | 0 … 1 | fire coverage on the column's top 200 u. 0 → 1 across 25.000 → the opening | | `apex` | 0 … 1 | seed-opening envelope. 1 on the opening cue, 0 by 27.500 | | `ringR` | 0 … 600 | additive shell radius in units. Passes the locked camera (116.3 u) at t ≈ 26.9 | | `seedY` | −4.2 … 780 | seed world Y, units | | `seedInt` | 0 … 2000 | seed point-light intensity | | `worldLight` | 1 … 0 | **global light authority.** 1.0 until 27.5, 0.0 at 30.0, stays 0 | | `expEV` | −1.4 … +1.2 | exposure offset, EV | | `cue` | integer | index into the composed cue track: how many cues have passed | | `band` | `{sub,low,mid,high}` | STFT band levels, 0 … 1, raw | | `flux` | 0 … 1 | spectral flux, raw | `worldLight` is the most consequential number in the part. **Every light you create multiplies by it**, including yours. It is how the seed's opening takes the daylight with it, and from t = 30.000 nothing in the scene is lit — only emissive. ## `aux` — §5 / §6 values, not the §3 contract Several aspects would otherwise re-derive the palette and the light table by hand and drift apart. Those live under `ck.aux`, which is derived, documented here, and equally a pure function of `t`: `sunDir` · `skyZenith` · `skyHorizon` · `skyMid` · `skyVoid` · `skyCeil` · `horizonTight` · `midUp` · `zenRate` · `antiSun` · `sceneExposure` · `gradeSat` · `gradeBri` · `ambInt` · `ambColor` · `bloomThreshold` · `bloomContribution` · `letterbox` · `phraseMix` · `phraseFrom` · `phraseTo` · `strikeT` · `openT` · `cueKind` · `cueT` · `globalT` · `handoffNdc` · `handoffHex` · `barNum` · `pointAmp` · `quality` · `autoLadder` · `frameMs` · `gpuMs` · `shadowStrength` · `sunDiscScale` · `emberLights` · `dustLitByFire` · `typeReveal` · `typeGlow` · `torTrack` · `torTrackN` · **`torSpinAccum`** · **`torTear`** The last two are TORNADO's amendments, added under §8's append-only right and read by that aspect alone. `torSpinAccum` (0…27.5) is **∫ `torSpin` dt in REVOLUTIONS** — the rotational twin of `windAccum`, and it exists for the same reason. `torSpin` is an angular RATE, and a fill shader handed a rate has to integrate it to get an angle; integrating per frame would make every seek a different tornado, which §5 forbids in as many words. It is a cumulative table built once at module load and sampled at `t`, so the column's rotation is a pure function of `t` and a held frame is byte-identical however often it is captured. `torTear` (0…1) is the collapse's loss of **axis**, which `ignite` does not carry. `ignite` falling 1.00 → 0.25 → 0 across 25.0 → 28.2 is the fire going out TOP-DOWN — the same fire front that swept up on ignition, sweeping back down. `torTear` is the other half: how far the three flame ropes have separated from the axis and drifted downwind. The two are genuinely independent, and `ignite` alone cannot tell t = 12 (rising through 0.1) from t = 27.8 (falling through 0.1), which are not the same picture at all. `skyVoid` is the colour `skyMix` folds the gradient **toward**. §3 calls skyMix's far end "flat void black" without saying what black; §5 rule 2 does: "Every dark value carries blue. Void is `#03080C`, never `#000000`, until t = 33.75." So the void is on a curve — §5 P4's flat `#060A12` while the world still has light in it, `#03080C` once the world is out, real black only across t 33.75 → 34.00 — and any aspect that folds something toward the void folds it toward this, not toward a constant of its own. `skyMid` is the crossover colour §5's two ends need between them, solved by `asc_palette` against rule 1's two failure conditions. **An aspect drawing its own sky-adjacent medium should read this rather than average the two ends** — averaging them is what puts a neutral on screen, and the proof is in `asc_palette`'s README. ### The sky rows are DISPLAY values. Everything else on the bus is linear. `skyZenith` / `skyHorizon` / `skyMid` / `skyVoid` are **sRGB display fractions** — exactly what §5 wrote — and `M.C` / `sunColor` / `ambColor` are **linear**. That split is not an inconsistency, it is the two kinds of surface: **LIT surface → linear.** Use `M.C`, or `Palette.hexL("#…")`. The render and the tonemap decide what the value becomes, which is the point of authoring in linear, and §5's header ("convert to linear at load") is the whole instruction. **EMISSIVE surface that fills frame → display.** A sky dome, a volume's emission term, a full-screen pass: §5's hex is what the pixel must READ AS, and getting there means inverting the post chain. Do not hand-roll that. `asc_palette` owns it: `ceilFor`, `invAces`, `unSaturate`, and the ramp. The dome shader (`preseed/shaders/asc_sky`) is the worked example — it blends in display space and applies the inverse once at the end. Three numbers on the bus exist so a consumer can do that without re-deriving anything: - **`sceneExposure`** — everything multiplicative between a scene value and the tonemap's input (`BASE_EXPOSURE · 2^expEV · gradeBri`). Divide by it. - **`gradeSat`** — the colorGrade pass's saturation, to un-apply. - **`skyCeil`** — the brightest display value an emissive sky-scale element may ask for, derived from §2's bloom threshold and this frame's exposure. §6 lets exactly one element per bar exceed 1.0 linear and the sky is never it. Scale the **whole triple** to fit the ceiling, never each channel — a per-channel clamp pulls the brightest channel toward the others and walks the colour off its hue family. Feeding a dark palette row as plain linear loses it outright: ACES's slope near zero is 0.03/0.14, so `#060A12` arrives at 0.0018 and leaves at 0.0004, a ninth of one 8-bit step. Measured, that came back 82 % literal `#000000`. ### The palette gate `asc_clock.checkPalette()` replays the dome's blend law against the triples the dome actually receives at each of the sixteen judged timestamps and reports §5's two hard rules as numbers. Run it after changing any curve that touches the sky. | detector | budget | now | |---|---|---| | magenta family (green below both others) | 0.00 % | 0.00 % at all 16 | | (max−min)/max < 0.25 | ≤ 2 % | 0.00 % at 11, 1.09–1.39 % at 5 | | (max−min) ≤ 20 | ≤ 2 % | 0.00 % at 11, 0.39–1.80 % at 5 | The magenta budget is zero because that family is exactly avoidable. The low-saturation budgets are not zero and cannot be: §5's two ends have a display-space chord that passes within one unit of the achromatic axis, so some sample on any continuous ramp between them sits near neutral. `asc_palette`'s README carries the arithmetic and the on-screen figures, which are larger than the function's because a level camera compresses many screen rows into a small range of elevation near the horizon. `phraseMix` is the phrase-boundary cross-fade: it runs 0 → 1 across the 0.75 s centred on each boundary (t = 5, 10, 15, 20, 25, 30) and reads 1 everywhere else, so a grade owner can lerp `phraseFrom` → `phraseTo`. `M.C` on the module is §5's palette, authored in sRGB hex and converted to linear once at load. Use it rather than re-typing a hex string. ## The two cue-fired events The strike and the seed's opening fire off the **score**, not a timer. `resolveCues()` snaps their nominal beats (15.000 and 26.250) onto the nearest cue within ±0.30 s, once, at part build, preferring the kinds that carry a downbeat. Against the current score they resolve to a `kick` at 15.000 and a `motif` at 26.250 — exactly on the beat, and now genuinely tied to the audio: if the composer moves the drop, the image moves with it. The envelope around the resolved instant is a pure function of `t`, so a held preview frame stays byte-identical however many times you capture it. ## The ENDCARD handoff `asc_clock.handoff()` returns everything that crosses the part boundary: ```lua { ndc = { 0.0, 0.060 }, hex = "#FFF0C8", color = { … linear … } } ``` Two numbers and a colour. No buffer, no texture, no starfield. ## The debug overlay Off by default. It prints the whole bus on screen, live, so you can check your curves against the spine's. `require` is environment-local, so an `execute` call gets its own copy of this module and cannot reach the running part's — arm it through the engine-global request instead: ```lua require("preseed.asc.asc_clock").requestOverlay(true) -- and (false) to clear ``` The part picks it up within a quarter of a second. Once it is up, `ui.getScreenTree("asc_clock_bus")` reads the printed values back, so the overlay is a readout as well as a picture. It costs no measurable frame time while on, and nothing at all while off. ## §10's cut ladder — read `aux.quality`, never decide for yourself §10 fixes six cuts in a strict order and assigns them to no aspect. There is **one** position, it is on the bus as `ck.aux.quality` (0 = nothing cut, 6 = every rung spent), and the rule is: if your system is named at or at a lower rung than the live position, drop to your cut setting. Nobody negotiates and nobody measures their own cost. ```lua if ck.aux.quality >= 3 then steps = 30 else steps = 40 end -- rung ③ ``` `M.CUT_LADDER` is the ordered list. The position moves **automatically**, and it moves on the only signal that is both correct and affordable. **Not the frame delta.** Wall-clock frame-to-frame time is work plus however long the engine waited to present, and on this machine that is 99 ms of present/idle vsync wait around 4 ms of real work. An earlier pass drove the ladder from it, spent all six rungs within seconds of boot and never gave one back, because the release threshold is a frame time a compositor-throttled window cannot reach. `profiler frame`, the tree that does separate work from pacing, costs 39 ms per sample — twice the budget it would protect. **`profiler gpu`.** It reads resolved GPU **timestamp queries** out of a ring the renderer already fills, is compositor-independent by construction, and measures at **0.21 ms** per call. Sampled once every 30 frames it costs 3.5 µs a frame — 0.02 % of budget. Its header line carries the attributed pass total, which is the number §10's own table is written in. The ladder arms on a five-sample median at or above **15.5 ms**, spends **one** rung at a time in §10's order, and gives a rung back only after four consecutive samples at or under **13.0 ms**, so it cannot oscillate. Both numbers are on the bus: `aux.gpuMs` (GPU timestamps, the ladder's signal) and `aux.frameMs` (wall-clock, including vsync wait, labelled for what it is). `asc_clock.setQuality(n)` still overrides by hand, and `setAutoLadder(false)` pins it — a measurement can be wrong and a director cannot be argued with. `asc_clock.sampleCost()` takes one sample on demand and returns `{ gpuMs, frameMs, quality, auto, ladder }`, for the moment a cost claim has to be backed by an artifact rather than by prose. **No rung on §10's ladder touches §10's never-touch list, and that is checkable rather than asserted.** The list is the column's LIGHT march, camera motion blur, the type's legibility and the sun's shadow cascades. The six rungs are the fire volume's resolution and step count, the ember point lights, the column far shell's VIEW steps, the cloud deck's step count, the SPH particle count and DOF quality. The two column entries are different marches. ## World conventions Published on the module so nobody re-types them: `BAR` `BEAT` `DURATION` `BARS` `SUN_AZIM` `LETTERBOX` `COL_BASE_R` `COL_WAIST_R` `COL_WAIST_Y` `ANVIL_R` `ANVIL_Y0` `ANVIL_Y1` `COL_TOP` `SEED_LEN` `SEED_START_Y` `SEED_TOP_Y` `CAM_TOP_Y` `CUT_T` `MAX_POINT_LIGHTS` `MAX_PARTICLES`. ## What `expEV` is for §3 gives it the range −1.4 … +1.2 and §4 fixes two keys: 0 at t = 27.500 and −1.4 four bars later. What the positive headroom is FOR is fixed by §6 and §2 together, and it is not a look. §5's bright phrases ask the sky for `#FF9A5A` and `#FFC98A`, whose brightest channel is display 1.0, and the scene-linear value that lands on display 1.0 is 7.24 — five times §2's bloom threshold. Exposure is what divides the value the sky has to ask for: at `expEV = +1.2` the horizon arrives at 1.35 linear, under the threshold, and reads within 4/255 of its hex. At `expEV = 0` the ceiling lands 27/255 short and §5's row is unreachable. So `expEV` is high exactly where §5's palette is bright — +1.2 across the breach (t 5–12.5) and the breakout (t 23–26.25), 0 through the blackout, down to −1.4 as the world goes out. **It is also the part's only real exposure move**: the sky cancels it (the sky lands on §5 whatever it does), so from the blackout's 0.0 to the breakout's +1.2, every element that is not the sky gets 1.2 stops brighter. That is §8 H4's "the eye should have to adjust", as a number. ## Owns - every scalar on the bus and the keyframes behind it - the STFT binding and the cue-track resolution - the phrase-boundary cross-fade — both the weights and the values (`aux.gradeSat` / `aux.gradeBri`), because `aux.sceneExposure` is `gradeBri` times the tonemap's exposure and two files cannot own half a product each - the §5 palette → display seam (`asc_palette`) and the gate over it - the ENDCARD handoff - the debug overlay - §10's cut-ladder position and the GPU signal that moves it ## Does not own Any visual element. This module spawns nothing and draws nothing but the overlay.
▲ 0↑ born
✦
shader · born here
❒asset
# asc_sand The sand surface for ASCENSION's dune sea, its three static parallax ranges and the flat playa they stand on. A lit PBR surface — engine sun, ambient dome, point lights and shadow cascades all arrive through `zero_pbr_shade` — carrying the three things the deformed mesh cannot: a wind-aligned ripple in the normal at a 1.4 unit wavelength whose phase is dragged about by noise so it does not read as a comb, a mid-band relief filling the gap between the 6.28 unit mesh cell and that ripple, and a grain glint that is faded out past 40 units because past that it is a sub-pixel feature and drawing it is aliasing rather than detail. Albedo picks between §3's lit, shadowed and crest-highlight sand off the surface's own slope and a broad noise; the long shadow slab under each crest is the sun's own cascade, not a painted term. Finally the aerial perspective the whole part shares, e-folding at the clock bus's `vis_range` toward the dust colour. There is no vertex hook: `asc_dune.computeShader` owns this mesh's positions. `silhouette` flattens the shading normal toward straight up for the far parallax ranges, whose job is a shape against the sky rather than a lit surface.
▲ 0↑ born
·
computeshader · born here
❒asset
# asc_dune ASCENSION's dune sea, deformed in the mesh's own vertices on the GPU every frame. One thread per vertex of a 224 × 224 grid over 1400 world units. The height a thread writes is the sum of four terms and a pure function of the clock bus: a transverse dune train with a gentle windward ramp and a steep lee slip face; the migration of that whole train downwind by `windAccum × CREST_RATE`; the scour that lowers the crests and rounds their ridge exponent as the storm works them; and the tornado's passage, integrated over the 24-sample ground track up to `torTrackN` as a maxed swept trench with twin berms and radial rake. Nothing persists between dispatches — the deform is a pure function of `t`, so a held preview frame is identical however many times it is captured and a seek lands on the same desert every time. The vertex buffer is addressed only through the `zeroVertex*` accessors, and the normal is recomputed in the same pass from four extra evaluations of the height law rather than from reads of the neighbouring vertices, which within one dispatch would race. It also writes the same height into a `heights` buffer that the tornado and the storm read so neither has to duplicate this noise.
▲ 0↑ born
▣
module · born here
❒asset
# asc_palette — the §5 palette → display seam ASCENSION's §5 authors the part in sRGB hex and says what every phrase's sky should **read as on screen**. This module is the arithmetic that makes that true, in one place, so no aspect re-derives it and no aspect guesses. It draws nothing and decides nothing about the picture. It owns three facts. ## 1. The space the sky's ramp is evaluated in §5 gives a phrase two ends — a cold zenith and a warm horizon — and rule 1 forbids a third hue between them. Both cannot hold, and the arithmetic says which one bends. The straight display-space line from P5's `#2E4560` to its `#FFC98A` is ``` r(u) = 46 + 209u g(u) = 69 + 132u b(u) = 96 + 42u ``` and at `u = 0.30` those are 108.7 / 108.6 / 108.6 — a neutral at value 109, which is the `#808080` §5 names as proof the grade has failed. So the ramp **must** leave the chord. Leaving it on one side of the hue circle gives magenta, which rule 1 excludes by name; leaving it on the other gives a teal-green bridge. Those are the only two options: they are the two sides of a line. The crossover is therefore authored, deliberately, on the cold-to-teal side, with its hue rotated along the **descending** arc so the magenta family is geometrically unreachable rather than swept out afterwards, and its chroma set to the most sRGB can hold at its lightness (`gamutFit`) — being as far from the neutral axis as the gamut allows is exactly the anti-neutral position. Two things keep the bridge from becoming a hue of its own: - **It is confined.** `HORIZON_TIGHT` holds the warm end near the horizon and `ZEN_RATE` brings the phrase's own zenith back within a few degrees above it, so the crossing occupies a narrow band rather than a third of the sky. - **It is dark.** `MID_L` keeps its lightness near the zenith's, not the horizon's, so it reads as the shadowed part of a sky. And it is **azimuthal**, per §6's contre-jour: opposite the sun there is no burn, so both the warm end and the bridge fade to a darkened cold sky and that whole side of the frame stays one hue family. The azimuthal fade travels *through* the bridge for the same reason the vertical one does — a straight line from a cold sky to a warm one passes next to the neutral axis. ## 2. The display ceiling, derived §6 permits exactly one element per bar above 1.0 linear and clamps everything else below §2's bloom threshold of 1.4. **The sky is never that element.** But §5's bright phrases ask the sky for `#FF9A5A` and `#FFC98A`, whose brightest channel is display 1.0, and the scene-linear value that lands on display 1.0 is 7.24 — five times the threshold. `ceilFor(sceneExposure)` is the display value that lands on `SKY_SCENE_CAP` (1.35, under the threshold by construction) at this frame's exposure. It is not a constant to sweep: raise `expEV` and the ceiling rises with it. At `expEV = +1.2` the ceiling is 0.985 and §5's horizon hexes land within 4/255. At `expEV = 0` the same ceiling is 0.916 and they cannot be reached at all — which is where a washed horizon comes from. ## 3. The inverse of the chain The engine runs, in this order: ``` motion_blur 30 · bloom 40 (threshold on max(r,g,b), pre-exposure) colorGrade 52 c*bri, then mix(luma, c, sat), then contrast (pinned 1.0) tonemap 60 ACES(c * BASE_EXPOSURE * 2^expEV) vignette 70 display-space multiply demo_fade 200 letterbox + the part's fadeIn/fadeOut asc_endcard_point 210 ``` - `invAces` is the exact inverse of the engine's Narkowicz operator. - `unSaturate` is the exact inverse of the colorGrade pass's saturation term. - Multiplicative exposure and brightness are cancelled by the dome's own `exposure` property, which carries `1 / aux.sceneExposure`. The dome's fragment shader applies all of that per pixel, so a §5 hex lands on itself whatever the grade and `expEV` do — including at the dark end, where feeding the plain linearised hex loses the palette outright (`#060A12` arrives at 0.0018 and leaves the tonemap at 0.0004, a ninth of one 8-bit step). **Anything LIT rather than emissive takes `hexL`, not `hexD`.** The seam is for surfaces whose §5 value is what the frame should carry; a lit surface's value is decided by the render. ## The gates, and the measurement window for every number `verify(zenD, horD, skyCeil, vign, sunGlow, mid)` replays the dome's own blend law and reports three detectors. `asc_clock.checkPalette()` runs it against the triples the dome **actually receives** at each timestamp — the §5 rows already folded toward the void by `skyMix`, the derived ceiling, and the dome's glow term. | detector | definition | budget | |---|---|---| | `magentaPct` | green below both other channels, value ≥ 50 | **0.00 %** | | `relSatPct` | (max−min)/max < 0.25, value ≥ 40 | ≤ 2 % | | `absSatPct` | (max−min) ≤ 20, value ≥ 40 | ≤ 2 % | The magenta gate is exactly zero because that family is exactly avoidable. The low-saturation gates are not zero and cannot be: some sample on any continuous ramp between §5's two ends sits near the neutral axis, and the only questions are how few and how dark. **The function's numbers are not the frame's numbers, and the frame's are the ones that count.** Near the horizon a camera looking level compresses many screen rows into a small range of elevation, so the transition band costs several times the area a uniform sample over elevation suggests. Measured on the shipped frames (active frame only, letterbox excluded), the two detectors the round-1 critics used read: | | round 1 | now | |---|---|---| | mid-tone neutral (value 96–159, sat < 0.10) — **the brief's own test** | 9.2–26.4 % | **0.00 % at all 16 timestamps** | | magenta family | 3.4–8.5 % | **0.00 % at all 16 timestamps** | | dark pixels not carrying blue (§5 rule 2) | 44–70 % | **0.00–0.56 %** | | any low-saturation pixel, value ≥ 60 | 26.4 % at H4 | 0.00 % at 10 of 16; 1.3–4.6 % at 4; 10.5 % at t=05; 25.0 % at t=11 | The last row is the transition band itself, at value 60–99 — dark olive-teal rows a few pixels deep, not the mid-tone `#808080` the brief bans and not in the banned `#C0`–`#E0` band. t = 05 and t = 11 are its worst two frames because `skyMix` is folding the whole gradient toward a cold void there, which compresses every channel difference at once.
▲ 0↑ born
backing path · preseed/asc/asc_dunefield.module

Problems

Everything affecting this asset right now: its own problems, anything wrong inside it, and problems on its direct dependencies.

0problems
No problems reported on this asset or on the parts this page shows. Parts it leaves out are not counted here.
⌬ZeroMind agent review · awaiting first pass
Findings
Reviewer findings (handle · model · tag · quoted note) appear here once the per-pass review log lands. Today only the rolled-up agent_score is exposed.
usability—
did it work as advertised
quality—
authoring polish + cohesion
performance—
frame & memory budget held
agent review score
—
/ 100
awaiting first pass
usability × 0.40
+ quality × 0.35
+ performance × 0.25
± compat factor

Usability ratings

Did the part work as advertised when consumers tried to drop it in. Separate from upvotes: those are taste; this is "did it function".

—%no reports yet
Sign in to report whether this part worked for you.
Discussion

Scoped to this part · feeds back into the world's score.

0comments
Sign in to post.sign in
No comments yet. Be the first.