ACES tone mapping
Purpose
Real lighting produces values that go far above 1.0, your monitor can only display 0–1, and tone mapping is the S-curve that squashes the extra down smoothly instead of clipping it flat.
Key insight
When the lighting in a scene adds up — a specular highlight plus a sky color plus an emissive material — the red/green/blue value for that pixel might be 3.0 or 12.0 or 80.0. The monitor cannot show anything above 1.0. The naive answer is to clip — anything above 1 becomes pure white — but that destroys detail. The cloud gets a flat white patch instead of the sun; the metal reflects a flat white square instead of a highlight shape.
Tone mapping replaces clipping with a curve: near-linear in the mid-tones (where most of the image lives), with a gentle shoulder that rolls off highlights and a toe that handles shadows. ACES is the industry curve most engines ship with — it's the reason modern lighting looks "cinematic" instead of "CGI blown out." The curve doesn't change any geometry or signal; it changes how numbers become pixels.
Break it
Toggle ACES off (the button flips the renderer to raw clip: color = min(color, 1.0)). The brightest pixels — the sun, the specular highlights on the cube, the emissive sphere — become flat featureless white blobs. Color channels also clip independently, so a bright yellow highlight (red + green both at 1.5) clips to pure yellow, losing the slight red-shift a real camera would show. Turn ACES back on: the highlights regain shape, gain a slight filmic warmth, and the color shoulder does its job. Teaches: clipping destroys highlight detail and shifts color weirdly; ACES preserves highlight shape and handles color in a way that matches how cameras and film behave.
Now slide exposure up. With ACES on, the highlights don't smash into flat white — they compress smoothly into a warm shoulder. With ACES off, the same slide produces a flat-white ceiling.
Direct Claude
// Tone mapping in Three.js is a one-line switch on the renderer.
// It is applied by the OutputPass at the end of the composer chain,
// AFTER bloom has already done its HDR work.
// --- ACES on (default for this lesson) ---
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.0; // the `exposure` slider, in linear units
// --- ACES off (the "break it") ---
renderer.toneMapping = THREE.NoToneMapping;
// The OutputPass will now just clip: any channel above 1.0 becomes 1.0.
// The slider converts photographic stops to the linear multiplier:
// exposure (stops) → pow(2, stops)
// +1 stop = 2x brighter, -1 stop = 0.5x.
function setExposure(stops) {
renderer.toneMappingExposure = Math.pow(2, stops);
}
// NOTE on ordering:
// RenderPass (scene, HDR float)
// UnrealBloomPass (HDR, threshold + blur + add) ← bloom sees raw values
// OutputPass (applies tone mapping + converts to sRGB for the display)
// If you tone-map first, the bloom pass only sees 0-1 values and can't tell
// "genuinely bright" from "merely light." Order matters.