Optional eye adaptation
src/render/exposure.ts owns the metric, luminance floor/clamp, spatial weight, target grey, exposure limits, asymmetric response rates, cut thresholds and marker cadence. It is game source, not engine configuration. enabled: false is the qualification default; no shipped picture changes merely because these files exist. The generated quality presets additionally keep autoExposureEnabled: false, avoiding GPU metering cost until the game explicitly opts in.
autoExposure.ts reduces the existing unexposed colour texture in 4×4 blocks and adapts in log2
space on separate 1×1 ping-pong targets. Odd-sized edge blocks are masked rather than repeated.
There is no extra scene draw, tonemapper, or render loop. Wire it ahead of bloom and the output
transform; never multiply both its absolute exposure and the old constant. applyExposure scales
RGB and preserves alpha: multiplying a whole vec4 darkens low-exposure output again when the
browser composites its reduced alpha onto the page.
For an existing WorldEnvironment chain, its baseColour callback already supplies the world pass:
const world = new WorldEnvironment({
...qualityPreset("high"),
autoExposureEnabled: true,
exposurePolicy: { ...exposureSettings, enabled: true },
});
const applied = world.apply(renderer, scene, camera);
const eye = applied.exposure;
// The installed graph owns the meter and its existing world pass.
applied.dispose?.();Import the classes/functions from this game's src/render/ files. If a baseColour composition
already exists (for example aerial perspective), meter that scene-referred composition using its
existing texture; do not create a second scene pass. Keep the existing chain's output cleanup.
eye.setEnabled(false)keeps measuring and tracking the hypothetical adaptation while returning exactly the supplied constant. The graph must remain attached, so application opt-out never silences measurement. The separateautoExposureEnabled: falsecost flag omits the graph entirely.quality.tsdecides when to apply the convention; this file decides its look.eye.reset()adopts the next measured target;eye.reset(value)seeds a known cut. Reset only for an authored cut/level load. Resizing changes meter targets but preserves the 1×1 history.eye.getObservation()andTN_AUTO_EXPOSUREreport actual GPU luminance, applied stops, target stops, settled state andapplied. Before the first readback, or on invalid readback,measuredis false; it never fabricates a zero reading. Diagnostic readbacks are bounded to one in flight.- A replaced chain owns a replaced node. Dispose the old pair; do not leave a second history graph
alive. Continuous quality-rebuild history transfer is not yet integrated with
setupPost.
Qualify an authored policy using the same camera in dark and bright poses, cuts in both directions, known resets, viewport/resolution changes and disabled measurement. Inspect actual canvas captures and the named adapter. A software-adapter image proves pixels, not native support or hardware cost.