Shader Lab
33.ACES tone mapping

ACES tone mapping

Arc 7 · Post-Pipeline · Lesson 33 of 35
coordinate signal color
0.00

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

"more filmic" ACES on, neutral exposure "richer highlights" ACES on, exposure up (more scene in the shoulder) "softer highlights" ACES on (any curve beats clipping) "more contrast / more punch" ACES on, exposure down (scene into the toe) "cinematic grade" ACES on, then LUT on top (L34)
Meta-phrase you gain here: "tone-map before grade." Grading (next lesson) assumes its input already looks right in brightness. If you grade unclipped HDR or clipped SDR, the LUT cannot do its job. Always tone-map first.
Combines with: L32 (bloom) — bloom runs on HDR before tone-mapping; if you tone-map first you've already squashed the brights the bloom wanted. L34 (LUT) — tone-map first, then LUT. This ordering is non-negotiable and is the single most common brief-level mistake. Canonical order: SSAO → bloom → ACES → LUT.
the tone-mapping config running above
// 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.