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

asc_palette

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.

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

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.

detectordefinitionbudget
magentaPctgreen below both other channels, value ≥ 500.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 1now
mid-tone neutral (value 96–159, sat < 0.10) — the brief's own test9.2–26.4 %0.00 % at all 16 timestamps
magenta family3.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 ≥ 6026.4 % at H40.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.

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_palette.module — ASCENSION: the §5 palette → display seam. §5 authors the part in sRGB hex and says what every phrase's sky READS AS on screen. For an emissive surface that fills the frame, landing on those hexes is arithmetic, not taste: the value the dome hands the renderer has to be the one that arrives at the display as the authored hex after the whole known post chain. This module is that arithmetic, in one place, so no aspect re-derives it and no aspect guesses. It owns three things and decides nothing about the picture: 1. THE SPACE THE RAMP IS EVALUATED IN. §5 gives a phrase two colours, a cold zenith and a warm horizon, and rule 1 forbids a third hue between them. A straight line between them in ANY linear space passes within a few units of the achromatic axis — the line from #2E4560 to #FFC98A crosses r = g = b at u = 0.30, which is the literal #808080 §5 names as proof the grade has failed. The ramp therefore travels an OkLCh geodesic with a chroma FLOOR, so it bows away from the neutral axis, and with the hue arc taken DESCENDING (blue → teal → green → amber), which makes the magenta family geometrically unreachable rather than swept out afterwards. 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. So the brightest display value the dome may ask for is fixed by the threshold and by this frame's exposure: `ceilFor(E)` is the display value that lands on SKY_SCENE_CAP linear, and SKY_SCENE_CAP sits under 1.4 by construction. Nothing here is swept. 3. THE INVERSE OF THE CHAIN. `invAces` is the exact inverse of the engine's Narkowicz operator (read out of `@builtin::shaders.post. tonemap`), and `unSaturate` the exact inverse of the colorGrade pass's saturation term (`@builtin::tools.pp.shared`, priority 52, which runs BEFORE the tonemap at 60). Multiplicative exposure and brightness are cancelled by the dome's own `exposure` property. The chain, in the order the engine runs it, is the whole reason this module exists: 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 * 2^expEV) vignette 70 display-space multiply demo_fade 200 letterbox + the part's fadeIn/fadeOut asc_endcard_point 210 `verify` replays that chain against the shader's exact blend law and returns the worst case, which is how the gates below stop being an intention.

toLinear(c: number) → number

argtypedescription
cnumber

toDisplay(v: number) → number

argtypedescription
vnumber

hexD(s: string) → void

A §5 hex as the display triple it names: sRGB fractions in 0..1.

argtypedescription
sstring

hexL(s: string) → void

The same hex as linear rgb, for anything lit rather than emissive.

argtypedescription
sstring

hexOf(d: { number }) → string

argtypedescription
d{ number }

q(v: number) → number

argtypedescription
vnumber

aces(x: number) → number

argtypedescription
xnumber

invAces(y: number) → number

The scene-linear value the operator maps onto display value `y`. The fit is y = x(2.51x + 0.03) / (x(2.43x + 0.59) + 0.14), so x is the non-negative root of (2.51 − 2.43y)x² + (0.03 − 0.59y)x − 0.14y = 0. The constant term is ≤ 0 and the leading term > 0 for every y under the curve's 1.0329 ceiling, so the discriminant is never negative and the root always exists.

argtypedescription
ynumber

ceilFor(sceneExposure: number) → number

The brightest display value the dome may ask for, given this frame's total pre-tonemap multiplier. Derived from §2's bloom threshold via SKY_SCENE_CAP — there is no constant to sweep.

argtypedescription
sceneExposurenumber

cbrt(x: number) → number

argtypedescription
xnumber

okFromLinear(c: { number }) → void

linear rgb → OkLab

argtypedescription
c{ number }

linearFromOk(o: { number }) → void

OkLab → linear rgb

argtypedescription
o{ number }

okFromDisplay(d: { number }) → void

argtypedescription
d{ number }

displayFromOk(o: { number }) → void

argtypedescription
o{ number }

toLCh(o: { number }) → void

OkLab ↔ OkLCh. Hue in degrees.

argtypedescription
o{ number }

fromLCh(L: number, c: number, h: number) → void

argtypedescription
Lnumber
cnumber
hnumber

_arc(hz: number, hh: number) → number

argtypedescription
hznumber
hhnumber

gamutFit(L: number, C: number, h: number) → void

The crossover colour §5's two ends need between them, as a display triple. Lightness and chroma are read off the two ends; the hue is rotated along the DESCENDING arc, which is the half of the circle that does not contain magenta, so that family is unreachable by construction rather than swept out afterwards. The most chroma an (L, h) can carry and still be a real sRGB colour. Asking for more does not give more: `displayFromOk` clips the out-of-range channel, and clipping a channel BOTH loses the chroma and rotates the hue — which is how a chroma request of 1.35× landed a mid at r = 0 with its hue pushed off the arc. Halving down to the gamut keeps the hue exactly and gives the most saturated colour that actually exists at that lightness.

argtypedescription
Lnumber
Cnumber
hnumber

inside(c: number) → boolean

argtypedescription
cnumber

midColor(zenD: { number }, horD: { number }) → void

THE CROSSOVER CARRIES NO THIRD HUE. It is the WARM end's own hue at a lightness and chroma read off both ends, which is exactly what the dome's own note asks for: "the phrase's own horizon colour at a third of its value". The rotation this replaces is the retired sunset's, and it is retired with it. §3's desert palette is a hue PAIR — sand ochre at 26–40° and dust brown-violet at 250–278° — and there is no arc between those two that is legal. Descending (the sunset's, chosen because it avoids magenta) runs 285° → 26° straight through blue, cyan and green: at t = 16.6 it landed the mid on `#002A56` and at t = 28.6 on `#091E2A`, which is the banned cyan lane, on screen, in the sky. Ascending is 101° the other way and lands on `#335`-ish magenta, which is banned lane 3. §3's own answer is that there is no bridge hue: the sand's hue darkens upward and the violet arrives from the zenith term, so the crossover takes `Hh` and the arc is not travelled at all. `arc` is left in place unreached; the aspect that owns this file decides whether a future palette wants it back.

argtypedescription
zenD{ number }
horD{ number }

unSaturate(c: { number }, s: number) → void

argtypedescription
c{ number }
snumber

saturate(c: { number }, s: number) → void

argtypedescription
c{ number }
snumber

hueOf(px: { number }) → number

Hue in degrees of an 8-bit triple. Needed because §3's banned lanes are stated as hue RANGES and two of them (green, cyan) cannot be separated from the authored dust brown-violet by comparing channels.

argtypedescription
px{ number }

toBytes(d: { number }, vign: number) → void

One display sample → the 8-bit triple the frame carries, through the rest of the chain: the display ceiling, then the vignette multiply.

argtypedescription
d{ number }
vignnumber

verify(zenD: { number }, horD: { number }, skyCeil: number, vign: number?, sunGlow: number?, midOverride: { number }?) → void

Sample the whole ramp for both rules and return the worst offender. `vign` is the vignette's darkest factor to test at (1.0 = frame centre). `zenD` / `horD` are the triples the DOME receives — already folded toward the void by skyMix if the caller is checking a specific instant — so what is verified is what is drawn. `sunGlow` is the dome's own glow term.

argtypedescription
zenD{ number }
horD{ number }
skyCeilnumber
vignnumber?
sunGlownumber?
midOverride{ number }?

scaleCeil(d: { number }) → void

argtypedescription
d{ number }

err(a: { number }, b: { number }) → number

argtypedescription
a{ number }
b{ number }

Sub-parts

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

3items
This part has no composite children. See the Files segment for its leaf payloads.
backing path · preseed/asc/asc_palette.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. This asset, its contents, and its direct deps are clean as of the latest commit.
⌬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.