Color management and post-processing confusion - need help

I am confused how color management, the output colorspace and post-processing with EffectComposer is supposed to work. I have gone through the documentation, and the behavior does not match what I was expecting based on the documentation.

I would like to ask for your help to improve my understanding, and maybe to identify whether the documentation can be clarified in some points.

I have created a minimal working example that showcases / experiments with some rendering settings. The fiddle runs different renderers in parallel with a simple emissive material (to keep it extremely simple). Currently the selected color is a bright orange: #f58231 Color Hex

I would expect some colorspace settings to reproduce the hex color correctly (i.e. sRGB in - sRGB out) and some not, but the current outcome is different than I expected. Below are the specific parts that I am confused about.


OutputPass has no effect

As you can see in the fiddle, adding an OutputPass as the last pass in the EffectComposer has no effect. The displayed color remains the same whether using RenderPass only, or RenderPass + OutputPass, independently of the colorspace used in the renderer. I was expecting to need the OutputPass to convert from linear colorspace to sRGB.

The manual states (under Workflow):

OutputPass is usually the last pass in the chain which performs sRGB color space conversion and tone mapping.

But the OutputPass seems to do literally nothing (if I’m not using tone mapping):

  • If the renderer colorspace is set to LinearSRGBColorSpace, the output color is wrong (i.e. not converted to sRGB) regardless of whether OutputPass is present.
  • If the renderer colorspace is set to SRGBColorSpace, then the colors are displayed correctly, but this is even the case without OutputPass, so what is it for?

What is the correct colorspace and post-processing chain?

The older color management migration guide states:

If using three.js-provided post-processing (not pmndrs/postprocessing), set renderer.outputColorSpace = THREE.LinearSRGBColorSpace and enable a GammaCorrectionShader pass.

But this clearly results in the wrong color as seen in the fiddle. Note that GammaCorrectionShader has been superseded by the OutputPass, but is this migration step now wrong?

In the current manual I cannot find much about what colorspace to use with post-processing, and whether the OutputPass is always necessary or not.

The color management manual states only:

Output to a display device, image, or video may involve conversion from the open domain Linear-sRGB working color space to another color space. This conversion may be performed in the main render pass (WebGLRenderer.outputColorSpace), or during post-processing.

renderer.outputColorSpace = THREE.SRGBColorSpace; // optional with post-processing

Why does the manual say “optional with post-processing”? Since SRGBColorSpace is the default for the renderer anyway, setting it is clearly also optional without post-processing.


Many thanks for improving my understanding on this topic!

1 Like

I think the documentation about setting renderer.outputColorSpace = LinearSRGBColorSpace is out of date now that OutputPass exists. With OutputPass (unlike GammaCorrectionShader) the setting of renderer.outputColorSpace defines what the OutputPass does.

In your example I think you can just use default settings without changing either of these…

renderer.outputColorSpace = SRGBColorSpace

ColorManagement.enabled = true

… and then with or without OutputPass post-processing, the color should appear as expected. If using post-processing, you should either use OutputPass or implement the color space conversion manually yourself.

I’ve edited my forum post and opened Docs: Fix incorrect note on color management in post-processing by donmccurdy · Pull Request #30281 · mrdoob/three.js · GitHub accordingly.

So would it be correct to paraphrase you that if you have an EffectComposer pipeline, the OutputPass is only necessary if you want tone mapping? Because the colorspace comes out correctly whether or not you are using the OutputPass.

But then I have a follow-up question:
My understanding was that the advantage of linear workflow is to have “better” calculations of colors over the whole pipeline. If I have multiple post-processing passes, I wouldn’t want to have the intermediate colorspaces to be sRGB, instead retaining the linear space until the last output pass.

But this doesn’t seem to be the case? If I have only a RenderPass in my pipeline, the output is still already in sRGB (if renderer.outputColorSpace is set accordingly), so the intermediate space is now sRGB??

I also added a comment to your PR, because it seems to have a slightly different conclusion than here.

Sorry, fixed my typo above. You should use OutputPass when using post-processing. renderer.outputColorSpace controls the output of the color conversion in OutputPass, when post-processing is being used.

If you only have a RenderPass in a post-processing pipeline, then output would be Linear-sRGB (working color space). Conversion to sRGB, or anything else, requires an OutputPass. You could still put more passes after the OutputPass if you wish.

Thanks for trying to clarify this for me!

Based on your statements and because they didn’t match my results in the fiddle, I went to look into the renderer source and I think I found what is happening and this has cleared my confusion.

To reiterate, the fiddle clearly shows that even without OutputPass, I was getting the correct conversion to sRGB, which contradicted your insistence that OutputPass is needed.

It turns out that the output color space is set in the WebGLRenderer shader to the renderer output color space if the renderer renders to the screen, and kept at Linear-SRGB otherwise (i.e. if there is a RenderTarget).

When using EffectComposer with only a RenderPass and no any additional pass, the RenderPass will render directly to the screen and not to a render target, and so there is an implicit sRGB conversion there.

If I add any other pass after RenderPass, the results are as you describe, i.e. the correct color only appears when the OutputPass is present.

This can be seen in this modified fiddle where I added a GlitchPass to show this: https://jsfiddle.net/s5yLpd6r/3/

The selection of the colorspace for the shader can actually be found here in WebGLPrograms.js (L202)

So basically what you were telling me was correct in 99%+ of cases, because if we are using EffectComposer, we do it because we want multiple passes. But in my experimentation in trying to understand the color management, I fell into the one exception, where EffectComposer with RenderPass as the last pass does not require OutputPass for a correct color output.

I don’t know if you want to clarify this in the manual or rather not.


Also in this light, I don’t know if this WebGLRenderer.outputColorSpace manual entry is really fully correct:

If a render target has been set using .setRenderTarget then renderTarget.texture.colorSpace will be used instead.

It seems to me that linear-srgb being used and not the texture colorspace if there is a render target, except in XR environments, where the texture colorspace is indeed being used.

Hmm, I wasn’t aware of that exception. I’m not sure whether it should be documented, or changed for greater consistency.

It seems to me that linear-srgb being used and not the texture colorspace …

When a render target is configured with .colorSpace = SRGBColorSpace, colors written to it are sRGB-encoded automatically, and colors read from it are decoded automatically from sRGB to Linear-sRGB. So it doesn’t change the working color space of the following pass. This happens in the WebGL API, it isn’t explicitly part of any shader program. WebGL allows this sRGB encode/decode only for 8-bit render targets.

1 Like