Updates to Color Management in three.js r152

The upcoming release r152 will contain changes affecting color and lighting. These updates enable a “linear workflow” by default, for better image quality in both physically-based and non-physically-based scenes. By “linear workflow”, we mean that any sRGB input colors (as found in most textures, color pickers, CSS, and HTML) are converted from sRGB to a linear working color space for rendering, and the rendered image is converted back to sRGB for display.


What does that mean for me?

These changes provide a better lighting workflow. However, it’s essential that inputs (like textures) are assigned the correct color space.

Changes to property names:

  1. THREE.WebGLRenderer property .outputEncoding renamed to .outputColorSpace
  2. THREE.Texture property .encoding renamed to .colorSpace
  3. sRGBEncoding renamed to SRGBColorSpace
  4. LinearEncoding renamed to LinearSRGBColorSpace

Changes to defaults:

  1. THREE.WebGLRenderer property .outputColorSpace now defaults to sRGB (THREE.SRGBColorSpace)
  2. THREE.ColorManagement.enabled now defaults to true

Read on to learn what to expect from these changes.


Migrating

Updating to three.js r152 can go two ways, depending on whether you’re already using renderer.outputEncoding = sRGBEncoding or not.

I'm still using renderer.outputEncoding = LinearEncoding (previous default)

This migration has few required steps, but there will be some differences in scene lighting as you’re moving to a linear workflow. Typically shading will appear softer, with smoother transitions between light and dark.

Recommended steps:

  1. Ensure that any material textures containing color data (like material.map) are assigned a color space. Typically that will be texture.colorSpace = THREE.SRGBColorSpace. This is not set by default, as it does not apply to textures used for normal maps, roughness maps, or other non-color data.
  2. HDR color textures (.exr, .hdr) should typically use THREE.LinearSRGBColorSpace instead, and will already have this set by default.
  3. Ensure that any THREE.ShaderMaterial instances include output color space encoding and tone-mapping, after setting gl_FragColor.
#include <tonemapping_fragment>
#include <encodings_fragment>
  1. If using three.js-provided post-processing (not pmndrs/postprocessing), set renderer.outputColorSpace = THREE.LinearSRGBColorSpace and enable a GammaCorrectionShader pass use OutputPass.

Be aware that your input colors (like 0xFF0000) are likely already sRGB, and there’s no need to convert them yourself. three.js now recognizes CSS and hex colors as sRGB. If you’re working with other color syntax, without conversions, you may now need to tell three.js when the values are sRGB.

material.color.setRGB( r, g, b, THREE.SRGBColorSpace );
material.color.setHSL( h, s, l, THREE.SRGBColorSpace );

These sRGB inputs are converted automatically to the working color space, Linear-sRGB, required for lighting and other operations. Components of THREE.Color objects and vertex colors are always in Linear-sRGB.

The linear workflow corrects bugs in previous workflows, and generally gives better results faster in new projects. Lighting tends to be softer and less harsh. However, if you’ve already fine-tuned lighting exactly to your preference in an existing scene with the old workflow, you may need to adjust lighting or tone mapping to get more contrast now. Increasing strength of directional lighting or adding tone-mapping may help.

I'm already using renderer.outputEncoding = sRGBEncoding

This migration is lossless. The appearance of your scene should not change; you are already using a linear workflow.

  1. Update your code to use renderer.outputColorSpace and texture.colorSpace instead of the older encoding-based names. Use the new values, SRGBColorSpace and LinearSRGBColorSpace. Non-color textures (normal maps, etc.) should use NoColorSpace, the default.
  2. Be aware that three.js now interprets CSS and hexadecimal as sRGB colors by default (as in CSS and HTML), and automatically converts them to Linear-sRGB:
// before
material.color.setHex( 0x112233 ).convertSRGBToLinear();

// after
material.color.setHex( 0x112233 );

If you weren’t already using convertSRGBToLinear(), then you should assume that your input colors were Linear-sRGB. Either tell three.js the value is already Linear-sRGB, or switch to sRGB color values if you want consistency with CSS and HTML.

// before
material.color.setHex( 0x808080 );

// after (option 1)
material.color.setHex( 0x808080, THREE.LinearSRGBColorSpace );

// after (option 2)
material.color.setHex( 0xbbbbbb );

Finally, remember that CSS and hexadecimal colors are being converted from sRGB to Linear, but other properties and methods of the THREE.Color class are still Linear. As a result, 0x800000 is not the same thing as color.r = 0.5. Most setters and getters in the THREE.Color API accept a color space parameter to specify:

color.setRGB( 0.5, 0.5, 0.5 );
console.log( color ); // → r = .5, g = .5, b = .5 (linear)

color.setRGB( 0.5, 0.5, 0.5, THREE.SRGBColorSpace ); // (srgb → linear)
console.log( color ); // → r = .22, g = .22, b = .22

color.getHex(); // → 0x808080 (linear → srgb)
color.getHex( THREE.LinearSRGBColorSpace ); // → 0x373737 (linear)

Vertex colors and the RGB components of THREE.Color instances are expected to be Linear-sRGB.

Can I opt out?

Yes. Although we recommend using the default workflow in new projects, you can still opt out of these defaults:

import * as THREE from 'three';

THREE.ColorManagement.enabled = false;
renderer.outputColorSpace = THREE.LinearSRGBColorSpace;

Note that THREE.ColorManagement must be disabled before you initialize THREE.Color instances, or CSS and hexadecimal values assigned to colors will be converted to Linear-sRGB.

NOTE: If you’re using three.js with React Three Fiber, A-Frame, or Threlte, then you’re probably already using these defaults — property names have changed but little else.


Motivation

We’re confident these changes move the three.js project in the right direction. As WebGL and WebGPU APIs begin adding support for wide-gamut and high dynamic range (HDR) color, having a color workflow that properly separates linear and non-linear colors is critical. While our legacy workflow was similar to what was once common in older games and even 3D authoring tools, the “plasticky CGI” look was always a problem.

The advantages of a linear workflow are well-documented and uncontroversial at this point — see Two Wrongs Don’t Make a Right for a clear illustration of the issues with our previous workflow.

With all of that said, we understand this is a large change. We intend to keep color APIs easy to use, but cannot (and should not) completely hide the distinction between the sRGB and Linear-sRGB color spaces. Any questions are welcome, and we hope to improve documentation further over time.

Questions

For general questions, please reply to this thread. If you would like help updating some existing code, consider starting a new thread with the full context, then including a link in this thread. Refer to the color management guide for deeper technical background.

38 Likes

amazing, thanks to everyone involved!

2 Likes

Thanks for this. We were just migrating to r151 and given the changes in colorspaces we noticed we had to get rid of convertSRGBToLinear() calls in several of our apps.
Good to read confirmation on this course of action.

Would be brilliant if with every update, a bash script is included which modifies code accordingly. So people only need to run that script with a filename as argument and bamm… that file is v152.

I don’t think it’s worth implementing a script for something that editors and IDEs can do in a much more clear manner.

Besides, some potential migration tasks like color space conversion can’t be done my search and replace anyway.

I hope these new changes will be worth the effort if there will be migrations and coding changes from r151 to the new upcoming release r152.

If you develop an 3D engine from scratch, you want to ensure a proper color management workflow right from the beginning (see Motivation section of Don’s first post). So it really makes sense to change the defaults in three.js since more apps will get better, more consistent visuals.

4 Likes

@hermann Sorry, this topic is about the upcoming color management changes and not about scripts for automating migration tasks. Feel free to create a separate topic but please stop posting about it here.

See previous semver discussion. Unfortunately, holding all breaking changes for “multiple-of-x” releases would have all the costs of semver, and then a few more. I’d prefer to keep this thread on the topic of color management, as people will definitely have questions about it— but please feel free to raise old or new threads for release scheduling.

3 Likes

Is there a way to access r151 of the three.js editor? Our art team has been using three.js editor for their asset import process but this change has broken the pipeline since it no longer exports the “encoding” field. We can’t update our engine to r152 right away, so in the meantime we’re stuck.

Are there specific examples that now use ColorSpace, or is that a work in progress?
I am using the “skyboxsun25deg” from the examples and things seem darker.
But perhaps that is to be expected because the sun is only 25 degrees above the horizon.

FYI
For a live example of a comparison between program using r152 and one using r150, see this message. I think r152 is a vast improvement and has made the iFFT generated ocean waves appear sharper and more detailed.

Yes, use this link: three.js editor

5 Likes

Thanks, that should keep us going until we are able to update the engine.

1 Like

i’m guessing all of the official examples should be updated and using the latest version of three, postprocessing_3dlut on inspection is definitely using references to colorSpace
image

1 Like

Thanks!
One question: the “color management guide” indicates that you should set ColorManagement to “true”, e.g.:
THREE.ColorManagement.enabled = true;

However, the example does not contain that command.
Is ColorManagement a default setting that is automatically set to true?

to answer your question, as outlined above, it seems so, yes…

apart from the postprocessing_3dlut example, i haven’t yet come across another example that demonstrates the use of colorManagement, although there’s likely a few, most postprocessing examples ( such as three.js webgl - postprocessing ) seem to have been set to THREE.ColorManagement.enabled = false;

3 Likes

Examples were revisited before r152 was released. But we do not explicitly set default values so you will not see in every example reference to Texture.colorSpace or ColorManagement.

Yes. The default of THREE.ColorManagement.enabled is now true. That is explained in this topic and the migration guide (Migration Guide · mrdoob/three.js Wiki · GitHub).

3 Likes

Every time i set a color on a material using hex values, i used to do color.convertLinearToSRGB() or color.convertSRGBToLinear()to get the correct hex representation

this step also won’t be necessary anymore right ?

How did your renderer setup look like so far? Did you already use renderer.outputEncoding = THREE.sRGBEncoding?

2 Likes