Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 62 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -288,9 +288,9 @@ This bit `multiScatteringFactor` (2026-09-04): added to the TSL integrator,
verified "no change" in the browser, root cause was the missing WGSL half.
WGSL params are **positional fn args**, so adding one means (a) the fn
signature in `luts.wgsl.ts`, (b) the multiply site, (c) the named key in the
`backends/wgsl/luts.ts` wrapper call. The headless verify script
(`examples/vanilla/scripts/verify-looks.mjs`) is what caught it — a diff of
~0.0003 where >0 was expected. For any new atmosphere param: edit both twins,
`backends/wgsl/luts.ts` wrapper call. A headless verify script (since
removed with the looks) is what caught it — a diff of ~0.0003 where >0 was
expected. For any new atmosphere param: edit both twins,
then check `examples/vanilla/parity/` still agrees.

### Haze sky mask: test the raw depth, never `viewZ`/`linearDepth` against `far` (2026-09-26)
Expand Down Expand Up @@ -431,8 +431,10 @@ NoToneMapping + a tiny exposure, read the pixel back and divide by the
exposure" returns clipped garbage ×(1/exposure). That exact trick produced a
20× wrong measurement of the noon sky (read ~10.5, really ~0.5–1) and PR #10
recalibrated look `intensity` against it, which pushed every `value > 0` look
to white (fixed 2026-09-26: looks are now display-referred, ramp ÷
toneMappingExposure). To read linear HDR values, scale the source instead
to white (fixed 2026-09-26 by making looks display-referred, ramp ÷
toneMappingExposure; looks are gone since 0.6, and the grade's fill and
gradient `replace` are display-referred the same way, via the mesh's
`displayScale`). To read linear HDR values, scale the source instead
(e.g. `sky.setExposure(40 * k)` with a small k and NoToneMapping), or read a
render target directly. Sanity-check any readback against a tonemapped render
of the same pixel before trusting it.
Expand Down Expand Up @@ -540,6 +542,57 @@ is byte-identical across reloads, which Chrome's shader cache needs — it is):
the sky cube's mipmap pass are gone on WebGPU: `SkyPmrem` allocates the
PMREM target itself and fills the cube's listed mips from its own chain.

### Sky grades: keep the operator linear, and where each part lives (2026-10-08)

`src/grade.ts` (data, bake, JSON, CPU twin) → `src/sky/GradeUniforms.ts` (two
32×64×16 RGBA half `Data3DTexture`s, one depth slice per keyframe, allocated
once and rewritten in place) → `src/backends/tsl/grade.tsl.ts` (sampled in the
sky mesh and the haze pass). The sun's place between keyframes is a JS-side
uniform `w = (index + fraction + 0.5) / 16`, so linear filtering in depth _is_
the keyframe blend; the baker syncs it in `update()` from the sun's elevation
against the camera's local up (planet mode follows).

- **The operator must stay linear in the light** (`max(M·c, 0)`, colorize by
channel _average_, scalar gain). The haze grades AP inscatter and the shadow
deficit with the same table, and both rely on linearity; the fill is the one
affine term and is weighted by AP opacity on haze, 0 on the deficit. A new
control that is nonlinear in `c` belongs in the sky mesh only, or breaks
silhouettes.
- **Colorizing at constant luminance bands.** Normalising a saturated blue target to unit
luminance needs a blue channel ~14× the luminance, which ACES clips into flat
regions — the sunset artefact of the looks this replaced. The average costs ≤3×.
- `scripts/verify-grade.mjs` checks the GPU dome (`SkyGradePreview` `grade`
mode, NoToneMapping) against `SkyGrade.apply` (worst ~3 / 255).

### Banding is 8-bit output, fixed by `ditherOutput` — after the tonemapper

A dusk/night sky gradient spans a few dozen 8-bit levels; undithered, one
level covered 57 px in the editor (visible contours in blues). three's
WebGPU output has no dithering. `ditherOutput(node)` calls `renderOutput`
then adds ±1 LSB triangular noise; the pipeline needs
`outputColorTransform = false` or the transform runs twice. Dither added
before the tonemapper (or baked into the cube) is the wrong size or filtered
away.

### `CanvasTarget`: resize it while it is the active target

A second canvas on the same renderer (`renderer.setCanvasTarget(target)`,
r186) gets its depth buffer from the renderer, which resizes it from the
target's `resize` event — and only listens to the _active_ target. A
`setSize` from a ResizeObserver while the main canvas is active leaves the
depth at 300×150: "depth stencil attachment size does not match". Queue the
size and apply it right after `setCanvasTarget(target)` (the grade editor's
`lutSize`).

### Comparing renders across branches: three's UUIDs consume `Math.random`

Seeding `Math.random` does not make two branches build the same random
geometry if one creates more three objects first (each `generateUUID` draws
from it). Compare geometry-free regions (sky, a hazed ground strip), or
remove the randomness. And if a page warns `compileComputeAsync is not a
function`, `node_modules` still has three r185: `pnpm install`, then restart
Vite with `--force` (its dep cache pins the old build).

### Vite HMR + WebGPU shader edits

Editing a TSL helper while a page is open often leaves the previous shader
Expand Down Expand Up @@ -665,6 +718,9 @@ src/
├── sky/
│ ├── SkyAtmosphereBaker.js public API (setSun, setAtmosphereParams, update)
│ ├── SkyAtmosphereMesh.js visible sky, samples SkyView LUT
│ ├── GradeUniforms.ts sky grade tables (3D, slice per keyframe) + uniforms
│ ├── SkyGradePreview.ts flat dome view (physical/graded/split/grade) for UIs
│ ├── SkyAmbient.ts night fill HemisphereLight, follows grade keyframes
│ ├── AtmosphereUniforms.js create/updateAtmosphereUniforms (TSL uniform bundle)
│ ├── luts/
│ │ ├── TransmittanceLUT.js 256×64, on atmos change
Expand All @@ -678,6 +734,7 @@ examples/
├── 10-transmittance-lut.html fullscreen TLUT view + readback
├── 11-multiscatter-lut.html TLUT|MS split + ?debug=<mode> bisection
├── 12-skyview-lut.html fullscreen SkyView LUT view (40× scaled)
├── 25-sky-grade-editor.html split-screen grade editor (grade-editor/*.js)
└── parity/ WGSL-core vs TSL-twin numeric parity harness
├── 00-leaf-helpers.html phases, ray-sphere
└── 01-uv-maps.html SkyView UV maps, spherical dir
Expand Down
14 changes: 8 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ The atmosphere itself builds on Sébastien Hillaire's
(EGSR 2020), and is checked against his reference code and Bruneton's
precomputed scattering. Around it you also get aerial-perspective haze for
distant geometry (with optional light shafts) or a cheaper sky-coloured height
fog, a night sky with pixel-sharp stars and the Milky Way, and stylized looks.
fog, a night sky with pixel-sharp stars and the Milky Way, and sky grades: an
authored, time-of-day lookup table over the physical sky, with an editor.
Vanilla and React (R3F) entry points, one `update()` call per frame.

**[Full docs →](https://sky.docs.pmnd.rs)** · **[Demo gallery →](https://sky.docs.pmnd.rs/examples/)**
Expand Down Expand Up @@ -130,7 +131,7 @@ depends on is only pulled into bundles that actually need it.

`<Sky>` props mirror the facade: construction options (`preset`, `quality`,
…) rebuild the instance, the rest (`timeOfDay`, `exposure`, `sunColor`,
`look` / `lookTrack`, haze knobs, …) go through the setters live. See the
`grade`, haze knobs, …) go through the setters live. See the
[React reference](https://sky.docs.pmnd.rs/api/react) for the full props table.

To compose with your own pipeline, skip `<AutoHaze />` and grab the instance
Expand Down Expand Up @@ -160,10 +161,11 @@ function CustomPipeline() {
- **[Haze guide](https://sky.docs.pmnd.rs/guides/haze)** — aerial perspective, policies, known issues, height fog
- **[Planet-scale guide](https://sky.docs.pmnd.rs/guides/planet-scale)** — `planetCenter`, radial frames, flight controls
- **[Tuning the atmosphere](https://sky.docs.pmnd.rs/guides/tuning-atmosphere)** — `AtmosphereParams`, presets
- **[Stylized looks](https://sky.docs.pmnd.rs/guides/looks)** — colour ramps over the physical sky, elevation-keyed tracks
- **[Sky grades](https://sky.docs.pmnd.rs/guides/sky-grades)** — art-direct the sky across the day: keyframed grades, gradients, a night fill and ambient light, and the editor
- **[Grade editor and demos](https://sky.docs.pmnd.rs/guides/grade-editor)** — author a grade, export it, load it in your app
- **[Night sky](https://sky.docs.pmnd.rs/guides/night-sky)** — star sprites and the baked Milky Way
- **[React reference](https://sky.docs.pmnd.rs/api/react)** — `<Sky>` props, `<AutoHaze>`, `useSky()`
- **[Upgrading](https://sky.docs.pmnd.rs/guides/upgrading)** — what changed in 0.5 (three r186, background shader compiles) and 0.4
- **[Upgrading](https://sky.docs.pmnd.rs/guides/upgrading)** — what changed in 0.6 (looks replaced by sky grades), 0.5 (three r186) and 0.4

A condensed method table for the `Sky` facade:

Expand All @@ -176,7 +178,7 @@ A condensed method table for the `Sky` facade:
| `setExposure(n)` | Sky luminance scale (default 40) |
| `setSunDisc(boolean \| { angularDiameter })` | Disc visibility + size in radians |
| `setSunColor(color)` | Colour of the sun as a light (`'neutral'`, `'bruneton'`, hex, `Color`, `[r,g,b]`) |
| `setLook(look)` / `setLookTrack(track)` | Stylized colour ramp over the physical sky, fixed or following sun elevation |
| `setGrade(grade)` | Sky grade: keyframed colour over the physical sky, or a gradient / solid sky (`horizonToZenith`, `gradientGrade`, `solidSky`) |
| `setTurbidity(n)` | Mie scattering scalar (1 = Earth) |
| `setGroundAlbedo(n \| Vector3)` | Multi-scatter LUT input |
| `setMirrorBelowHorizon(boolean)` | Bake a Y-mirrored sky on the cube's lower hemisphere instead of lit-ground albedo (clean sky HDRI for reflective-floor scenes) |
Expand Down Expand Up @@ -226,7 +228,7 @@ LUT debug views, and the numbered scratch demos used during development.
## Status

Baked sky, aerial-perspective haze with light shafts, sky-coloured height fog,
planet-scale (ground→orbit) rendering, the night sky, and stylized looks are
planet-scale (ground→orbit) rendering, the night sky, and sky grades are
all functional. Volumetric clouds are out of scope.

## Contributing
Expand Down
Loading
Loading