Command Palette

Search for a command to run...

Hectal
Course outline
Topic 5.6Intermediate ~20 min · step 6 of 7

Colour Management and Tone Mapping

In one line

Screens do not store brightness evenly (sRGB), but light maths needs evenly spaced numbers (linear). three.js converts between them for you, and breaks the picture if you tell it the wrong thing. Tone mapping then squeezes very bright light into what a screen can show.

0/7 · 0%

Think of it like this

A ruler marked in centimetres is even. A ruler marked in steps that each look equally big to the eye is not, because the eye notices small differences in dark things more than in bright ones. Screens use the second ruler (sRGB). The light maths needs the first (linear). Mixing the two rulers gives colours that are too bright or too dark.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

sRGB
The colour encoding screens and image files use. It spends more of its numbers on dark tones, which is how our eyes work.
linear
Numbers proportional to the amount of light. Light is added and multiplied correctly only in linear numbers.
colour space
The rule that says what a colour's numbers mean. three.js has SRGBColorSpace, LinearSRGBColorSpace and NoColorSpace.
tone mapping
A curve that squeezes very bright light into the 0 to 1 range a screen can show, without clipping.

Step by step

01What a colour really holds

The first example prints the middle grey #808080: three.js stores 0.2159, not 0.5020. A linear 0.5 written back as a CSS colour is #bcbcbc.

02Draw it two ways

The same flat grey is drawn with outputColorSpace set to sRGB and then to linear. sRGB gives the right 128. Linear gives 55: the picture is too dark because the number was not converted for the screen.

03Seven tone-mapping curves

The second example draws one very bright orange ball seven times, side by side, with each curve (a scissor box per tile). Look at the lit side: None has red clipped at 255, and the others keep the red below it.

04A texture with the wrong label

The third example paints a grey and blue canvas and uses it twice. With SRGBColorSpace the pixels come out as the file holds them. With the default label the grey is 188 and the blue is 113,206,250: washed out.

Try it yourself

  1. 1

    Darken with exposure

    In the tone-mapping example change toneMappingExposure = 1 to 0.5. None does not change (255,211,156); Linear falls to 255,155,114 and Neutral to 248,134,105. Exposure scales the light before the curve, and None has no curve.

  2. 2

    Fix the washed-out texture

    In the texture example change the second NoColorSpace to SRGBColorSpace. Both planes print the same: 128,128,128 and 42,157,244.

Code & diagrams

sRGB and linear: the same grey New tab

The canvas shows the grey drawn with the sRGB output. The table prints what is stored and what lands on the screen.

idle

Console

Run the scene to see its console.

Expected console output

ColorManagement.enabled: true
new Color("#808080") stores 0.2159 (not 0.5020)
getHex() gives it back as #808080
setRGB(0.5, 0.5, 0.5) linear is #bcbcbc on the screen
outputColorSpace srgb         -> pixel 128
outputColorSpace srgb-linear  -> pixel 55
Seven tone-mapping curves on a very bright ball New tab

Left to right: None, Linear, Reinhard, Cineon, ACESFilmic, AgX, Neutral, all at exposure 1. The scene has a light of intensity 9 on purpose.

idle

Console

Run the scene to see its console.

Expected console output

tone mapping   lit side (R,G,B)   lower left (R,G,B)
None          255,211,156        255,153,70
Linear        255,211,156        255,153,70
Reinhard      224,169,137        215,135,68
Cineon        240,200,167        235,164,69
ACESFilmic    255,217,188        255,186,107
AgX           242,193,173        239,168,132
Neutral       252,165,147        251,137,107
A texture with the wrong colour space New tab

Left plane: correct. Right plane: the colour label left at the default, so the image is read as linear and looks washed out.

idle

Console

Run the scene to see its console.

Expected console output

the file holds:                        grey 128,128,128 and blue 42,157,244
colorSpace = SRGBColorSpace (right)    grey 128,128,128  blue 42,157,244
colorSpace left at the default (wrong) grey 188,188,188  blue 113,206,250

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Output the wrong colour space

In the first example set renderer.outputColorSpace = THREE.LinearSRGBColorSpace for the final render.

terminal
$ Run
── what you'll see ──
outputColorSpace srgb-linear -> pixel 55
The grey `#808080` is drawn as `55`: far too dark.

Myth vs fact

Myth

A colour you wrote as #808080 is half as bright as white.

Fact

It is 21.6% of the light. A byte of 128 is mid-grey to the eye, not half the light.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Only mesh materials with toneMapped: true (the default) get tone mapping. Post-processing (Phase 15) has to apply the curve and the sRGB conversion exactly once, in its final output pass; doing it twice gives washed-out or over-dark results.

Remember this

  1. 1

    The problem this solves: two common 3D bugs are a washed-out texture and a scene that looks too dark or contrasty. Both are usually colour-space mistakes, not lighting mistakes.

  2. 2

    sRGB and linear. A colour you write as #808080 is sRGB: the byte 128 is a middle grey to the eye, but only 0.2159 of the light of white (the example prints it). three.js keeps colours as linear numbers inside (ColorManagement.enabled is true, the default), converts your CSS colours when you create them, and converts the final result back to sRGB when it draws to the screen. This last step is renderer.outputColorSpace, default THREE.SRGBColorSpace.

  3. 3

    Textures need the right label. An image holding colours (a photo, a paint texture) is sRGB: set texture.colorSpace = THREE.SRGBColorSpace so three.js converts it. An image holding data (normal, roughness, metalness maps) is not colour: leave it at the default NoColorSpace, which means linear. Forget the label on a colour map and it looks washed out: the third example prints 188 where the file says 128.

  4. 4

    Tone mapping. Light maths can produce numbers far above 1 (a bright sun). A screen cannot show above 1. Plain clipping makes bright orange turn yellow and white (the example's None row: red is stuck at 255 while green rises). Tone mapping is a curve that squeezes all brightness into 0 to 1 smoothly. Set it on the renderer: renderer.toneMapping.

  5. 5

    The options in r186. NoToneMapping (default: clip). LinearToneMapping (just exposure). ReinhardToneMapping and CineonToneMapping (older curves). ACESFilmicToneMapping (the film-like look: contrasty, shifts hues). AgXToneMapping (soft, desaturates highlights). NeutralToneMapping (added for product viewers: it keeps colours close to true until they get very bright). CustomToneMapping (your own function).

  6. 6

    Exposure. renderer.toneMappingExposure multiplies the light before the curve: below 1 darkens, above 1 brightens. It does nothing for NoToneMapping. Tone mapping and outputColorSpace only touch the final pixel: a MeshBasicMaterial with toneMapped: false skips the curve, which is right for pure colours such as UI.

Explain it without notes

01

A photo texture looks pale on a plane. What is the likely cause and the one-line fix?

02

Why does None tone mapping turn bright orange yellowish, and what does a tone-mapping curve do instead?

Practice

01

Make the showroom's renderer keep colours close to true under a bright key light. Write the two lines.

02

A roughness map is loaded with TextureLoader. What colour space does it get?

Done when you can

  • Say why #808080 is not half the light of white

  • Label colour textures sRGB and data textures with no colour space

  • Pick a tone-mapping curve and say how exposure changes it

  • Tell washed-out from too-dark and name the cause of each