Color management in three.js

Illustration of bad gamma.

A particularly clear example of gamma correct (left) and incorrect (right) rendering. Source.

Updated November 9, 2022: With three.js ≥r139, I contributed a more comprehensive guide to color management in the official three.js documentation. I've maintained and updated this post as a short tl;dr, but I recommend reading the full guide for a stronger understanding.

Definitions

Best practices

  1. Textures with color data (.map, .emissiveMap, …) should be configured with .encoding = sRGBEncoding. Non-color textures use LinearEncoding. Exceptions exist for some formats (like OpenEXR), which typically use LinearEncoding for color data.
  2. Vertex colors should be stored in Linear-sRGB.
  3. Materials and lights require RGB components in Linear-sRGB. Hexadecimal and CSS colors are generally sRGB, and must be converted.1
  4. Renderer should have .outputEncoding = sRGBEncoding when not using post-processing. With three.js default post-processing, use LinearEncoding and apply a GammaCorrectionShader as the last pass in post.2

THREE.GLTFLoader provides (1), (2), and (3) out of the box. Other file formats vary.

1 With three.js r139, THREE.ColorManagement.legacyMode = false addresses issue (3), by automatically converting hexadecimal (0xF0F0F0) and CSS-like ("crimson") colors from sRGB to Linear-sRGB. This option is enabled by default in React Three Fiber.

2 With React Three Fiber's post-processing, or the postprocessing package for vanilla three.js, use .outputEncoding = sRGBEncoding. An explicit GammaCorrectionShader pass is not required.


Motivation

The goal of all that is a "linear workflow" — lighting calculations are done on linear values. When values are in gamma space (or "gamma workflow"), instead, surfaces will respond inconsistently to lighting, appearing too bright in places and too dark in others, or somewhat washed out. And obviously, light and color values brought from other PBR programs would not match as expected. You can tune your lights carefully and eventually get consistent results with a gamma workflow, too, but the lighting will be more fragile to future changes.

John Novak describes the problem well:

[With gamma workflows, or incorrectly-configured linear workflows] the material and lighting parameters we would need to choose would be completely devoid of any physical meaning whatsoever; they’ll be just a random set of numbers that happen to produce an OK looking image for that particular scene, and thus not transferable to other scenes or lighting conditions. It’s a lot of wasted energy to work like that. ... It’s also important to point out that incorrect gamma handling in 3D rendering is one of the main culprits behind the “fake plasticky CGI look” in some (mostly older) games.

References