CRT post-process
Source: src/gpu/pipelines/crt.ts
The final full-screen pass stamps a CRT character onto the composited scene before it reaches the swap chain: chromatic aberration → tone-map → scanlines. When crt={false}, this is swapped for a plain passthrough blit.
1. Chromatic aberration
Section titled “1. Chromatic aberration”Real CRTs (and cheap lenses) don’t focus all wavelengths to the same point. We fake it by sampling the red and blue channels with a small horizontal offset while green stays centered:
aberration is in pixels (default 1.0), converted to uv space by dividing by width. At 0 there’s no fringing.
2. Tone-map — clamp, not Reinhard
Section titled “2. Tone-map — clamp, not Reinhard”The composite is HDR (bloom can push values past 1.0). Mapping it to the displayable 0..1 range is tone-mapping. A common operator is Reinhard, — but that’s for true HDR (values ≫ 1). Our signal is mostly in [0,1] (heads clamp at 1.0; bloom adds a modest overshoot), so Reinhard would crush midtones — a 1.0 head would map to 0.5, darkening everything. Instead we clamp:
Clamping lets bloom’s >1.0 highlights blow out to white — the desirable glow — and is identical to the implicit clamp the swap chain would apply anyway. (If heads ever emit genuine HDR, revisit with a real operator.)
3. Scanlines
Section titled “3. Scanlines”A horizontal brightness ripple — a sine in screen-space :
The frequency is tied to the canvas height so band spacing stays constant in device pixels at any size:
i.e. one full bright→bright period every 4 device pixels. scanlineStrength (default 0.3) controls only the depth of the ripple, not its spacing — multiply the color by scan.
Why it’s the last pass
Section titled “Why it’s the last pass”It reads the fully-composited scene (reusing the single-texture blit binding) and writes the swap chain directly — so it’s where HDR finally becomes the 8-bit image you see. Everything upstream stayed in HDR offscreen targets precisely so this step has real range to clamp and glow from. See the Pipeline overview.