Skip to content

feat!: sky grades — authored time-of-day LUT, editor, gradients, night ambient; looks removed - #57

Merged
DennisSmolek merged 1 commit into
mainfrom
feat/sky-grade
Oct 8, 2026
Merged

DennisSmolek merged 1 commit into
mainfrom
feat/sky-grade

Conversation

@DennisSmolek

Copy link
Copy Markdown
Member

What

A designer asked to author the sky rather than tweak gradient overrides, and reported harsh banding at sunset with the looks. This replaces looks with sky grades: an artist-authored lookup table over the physical sky, keyed on the time of day, plus a split-screen editor to make one.

  • SkyGrade — keyframes on sun elevation. Each grades the physical sky per region of the dome (zenith, horizon, sun side, away side, sun glow, below the horizon), with whole-sky controls (exposure, saturation, temperature, tint, hue), a gradient layer (amount swaps hue and keeps physical brightness; replace makes the gradient the sky — fixed or solid skies), a display-referred night fill, and keyed ambient light. Save/load as versioned JSON.
  • Helpers — horizonToZenith(a, b), gradientGrade(stops), solidSky(color) return an editable one-keyframe grade.
  • sky.setGrade(...) accepts a SkyGrade, a name, a definition or saved JSON; React <Sky grade={...} />.
  • SkyAmbient (sky.createAmbient()) — hemisphere night fill that fades in after sunset, or follows the grade's ambient keyframes. Fixes "the world goes black at night".
  • SkyGradePreview — the flattened sky dome (physical / graded / split / grade-only) for grading UIs, on any canvas.
  • ditherOutput — tone-map + encode + ±1 LSB triangular dither. The sunset/dusk banding was 8-bit quantisation of dark gradients; nothing in three's WebGPU output dithers.
  • Editor — examples/vanilla/25-sky-grade-editor.html: scene on top; timeline with keyframes and a whole-day strip; dome view with zone guides (click the sky to jump to its zone); keyframe inspector with auto-key; undo/redo, autosave, Save / Load / Copy JSON, bypass (B).

How it works

Each keyframe bakes on the CPU to one slice of a 32×64×16 RGBA half-float 3D table (colorize target × amount, gain, fill). The sun's place between keyframes is a JS-computed depth coordinate, so linear filtering in depth is the keyframe blend: a sun move writes two uniforms; an edit costs ~1.3 ms CPU plus the usual cube + IBL re-bake (never the atmosphere LUTs).

The operator is linear in the light — max(M·c, 0), colorize keeping the channel average (not luminance: a saturated blue at the luminance of a bright sunset needs a ~14× blue channel, which is what clipped into bands), scalar gain, plus an affine fill / replace term. So aerial-perspective inscatter and the light-shaft deficit are graded by the same table, with the affine part weighted by haze opacity: silhouettes stay seamless and fully hazed geometry lands on the graded sky.

Verification

  • pnpm test: 181 pass (grade bake/JSON/interpolation/linearity/gradients, SkyAmbient, Sky.setGrade integration, React grade prop).
  • scripts/verify-grade.mjs (new): GPU grade vs SkyGrade.apply over 504 dome samples, at a keyframe and between two — worst 2.8 / 255.
  • With no grade assigned, sky, haze and baked pages are pixel-identical to main (geometry-free regions; the haze demo's random mountains shift because three's UUIDs consume Math.random).
  • Shaders shrank vs main: sky mesh 31.3 → 29.1 KB WGSL, haze 48.1 → 47.3 KB.
  • Banding: on a dark dusk gradient the longest 8-bit flat run went 57 px → 2 px with ditherOutput.
  • Editor: 13 scripted interaction checks (select, edit, undo/redo, auto-key, load, autosave, shortcuts, zone pick).
  • verify-parity passes. verify-sky-fog fails only no-console-errors, from the pre-existing "update() before compile" warning on 24-sky-fog.html (that page never awaits compileAsync; unrelated).

Breaking

Looks are removed (setLook, setLookTrack, registerLook*, resolveLook, sampleLook, …, React look / lookTrack / lookTrackOverrides, baker.setLook, createHazeOutputNode({ lookUniforms })). baker.sky.lookUniforms.displayScale → baker.sky.displayScale. Migration table in docs/guides/upgrading.mdx: a look is gradientGrade(stops, { amount: chroma, replace: value }) with stop at in degrees. The ghibli and baked demos are converted; the looks demo and its verify scripts are gone (superseded by the editor and verify-grade).

Docs

New: docs/guides/sky-grades.mdx, docs/guides/grade-editor.mdx. Updated: upgrading (0.5 → 0.6), API (Sky, baker, React), haze, night sky, tuning (Unreal-parity knobs moved here), README, CLAUDE.md (grade architecture, dither, CanvasTarget resize trap).

🤖 Generated with Claude Code

…, with an editor; looks removed

Adds `SkyGrade`: keyframes on sun elevation that grade the physical sky per
region of the dome (zenith, horizon, sun side, away side, glow, below), with
whole-sky controls, a colour gradient layer (`amount` swaps hue and keeps the
physical brightness, `replace` makes the gradient the sky), a display-referred
night fill and keyed ambient light. Each keyframe bakes to one slice of a
32×64×16 half-float 3D table; the sun's place between keyframes is a uniform,
so a sun move costs two uniform writes and an edit ~1.3 ms of CPU plus the
usual cube re-bake. The operator is linear in the light, so aerial-perspective
haze and the shadow deficit are graded by the same table and silhouettes stay
seamless.

- `sky.setGrade(grade | name | definition | JSON)`, React `grade` prop
- helpers `horizonToZenith`, `gradientGrade`, `solidSky` for fixed /
  gradient / solid skies
- `SkyAmbient` (`sky.createAmbient()`): a hemisphere night fill that fades in
  after sunset or follows the grade's ambient keyframes
- `SkyGradePreview`: the flattened dome (physical / graded / split / grade)
  for grading UIs, on any canvas or target
- `ditherOutput`: tone-map, encode and dither — fixes 8-bit contour banding in
  dusk and night gradients (57 px flat runs → 2 px)
- `examples/vanilla/25-sky-grade-editor.html`: split-screen editor (scene,
  timeline, dome view, keyframe inspector), undo, autosave, save/load JSON
- `scripts/verify-grade.mjs`: GPU grade vs `SkyGrade.apply` (≤ 3/255)
- docs: Sky grades guide, Grade editor and demos guide, 0.6 upgrade notes

BREAKING CHANGE: looks are removed — `setLook`, `setLookTrack`,
`registerLook`, `registerLookTrack`, `resolveLook`, `sampleLook` and the rest
of `looks.ts`, the React `look` / `lookTrack` / `lookTrackOverrides` props,
`baker.setLook` and `createHazeOutputNode({ lookUniforms })`. Use sky grades:
a look becomes `gradientGrade(stops, { amount: chroma, replace: value })` with
stop `at` in degrees (`asin(at) × 180 / π`), a look track a `SkyGrade` with a
gradient per keyframe. `baker.sky.lookUniforms.displayScale` is now
`baker.sky.displayScale`. See docs/guides/upgrading.mdx.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@DennisSmolek
DennisSmolek merged commit 12c80a8 into main Oct 8, 2026
2 checks passed
@DennisSmolek
DennisSmolek deleted the feat/sky-grade branch October 8, 2026 12:09
@github-actions github-actions Bot mentioned this pull request Oct 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant