Effect Composer Gamma Output Difference

I’ve run into this problem before when dealing with post-processing, and I confess, even after several days of studying color spaces and the tone mapping code in three.js, the only way I’ve been able to solve it is by guesswork.

I do think that there are some bugs in the way tone mapping is handled in the post-processing code, or if not, then the workflow needs to be improved and have much better documentation because it’s very confusing at the moment.

In the end, I solved this by disabling all color correction in three.js and adding my own final pass which does tone mapping/brightness/contrast.

EDIT: looks like I should also add gamma correction (or rather, sRGB transform) into this, after tone mapping and before brightness/contrast.

/*
 * Combined post-processing pass
 *
 * ACESFilmic Tone mapping
 * Brightness
 * Contrast
 *
*/

const CombinedShader = {

  uniforms: {

    tDiffuse: { value: null },
    toneMappingExposure: { value: 1.0 },
    brightness: { value: 0 },
    contrast: { value: 0 },

  },

  vertexShader:

  /* glsl */`
    varying vec2 vUv;

    void main() {

      vUv = uv;
      gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );

    }`,

  fragmentShader:

  /* glsl */`
    #define saturate(a) clamp( a, 0.0, 1.0 )

    uniform sampler2D tDiffuse;

    uniform float toneMappingExposure;

    uniform float brightness;
    uniform float contrast;

    varying vec2 vUv;

    vec3 ACESFilmicToneMapping( vec3 color ) {

      color *= toneMappingExposure;
      return saturate( ( color * ( 2.51 * color + 0.03 ) ) / ( color * ( 2.43 * color + 0.59 ) + 0.14 ) );

    }

    void main() {

      gl_FragColor = texture2D( tDiffuse, vUv );
      gl_FragColor.rgb = ACESFilmicToneMapping( gl_FragColor.rgb );

      gl_FragColor.rgb += brightness;

      if (contrast > 0.0) {
        gl_FragColor.rgb = (gl_FragColor.rgb - 0.5) / (1.0 - contrast) + 0.5;
      } else {
        gl_FragColor.rgb = (gl_FragColor.rgb - 0.5) * (1.0 + contrast) + 0.5;
      }

    } `,

};

export default CombinedShader;

I intend to add color correction to this as well, I’ve held off on that as I’m not sure whether it does before or after tonemapping.

2 Likes

Some more considerations for post processing:

  • some passes need to be in linear space (before tone-mapping) and some need to be done after. Some can be done before or after but you’ll get different results (e.g. vignette).
  • Outline effect looks like it has tone mapping built in (probably incorrect?)

Before tone mappings: bloom, lensflare etc
Then, in order: 1. tonemapping 2. gamma correction
After tone mapping: brightness, contrast, FXAA, SMAA, color correction
Order not important (or, choose what looks best to you): DOF, vignette, film grain etc.

I’m not sure where where exposure comes into this - either before all passes, or just before tone mapping?

Thoughts:

  • We should remove all tone mapping except for ACESFilmic/NoToneMapping. ACES Filmic is the industry standard and as far as I know the only kind of tone mapping Unreal and Unity support. I have so far not seen any arguments for having other kinds of tone mapping.
  • It would be great if a final tone mapping pass was automatic when using the EffectComposer. Even better, combine color correction/tonemapping/brightness/contrast as the final pass (again, that’s what Unreal does I think) (see below).
  • have post processing built into the core so it’s a first class three.js citizen - see Takahirox’s PR here:
3 Likes

Wow, thanks @looeee! This is some great, useful info I haven’t seen anywhere else :eyes:

That would be the dream scenario :heart_eyes:
but removing built in color corrections inside some passes would be a great start. Its simple to just add toneMapping, gamma etc at the end of the composer chain, but what complicates things is selfish passes with corrections built in, that break the chain.
Would Takahirox’s PR fix that issue?

Also Im not sure I got an answer for this, but why would renderPass and renderPass + copyPass give different colorspace renders? (or different results at all?) This actually seems to happen with 1 or more passes added after renderPass, regardless of the pass type (but a basic copyPass shows it clearly).
And why does it suddenly stop respecting the renderer.outputEncoding? :thinking: :face_with_raised_eyebrow:

1 Like

AFAIK, no. It’s just the idea to move post-processing into the core.

In any event, I don’t think I would add tonemapping or gamma correction automatically at the end of the pass chain in EffectComposer. At least there should be a flag that controls this behavior.

That happens because WebGLRenderer.outputEncoding is only respected when rendering to screen (or default framebuffer). When you rendering to a render target, the encoding from it’s texture property is evaluated. The internal render targets of EffectComposer use the default setting LinearEncoding (which makes sense since a render pass should always be in linear color space for further processing).

3 Likes

Hmm, yeah on further consideration this suggestion is contradictory with the need to do some passes before tone mapping and some after.

However, as far as I’m aware, you will always (at least on LDR screens) need to do tone mapping/gamma correction at some point in the post processing chain so it would make sense to have a combined pass that does these and clearly demonstrate in docs or examples how and where to use it.

2 Likes

Well then, what would be the reason for the ugly effect on the second image? Because color-wise it seems like it is converted well, it’s just that those pixely (almost 8-bit looking?) artifacts are showing, almost as if precision was lost?

Im not very knowledgeable in the internals of THREE.WebGLRenderer. Is there a difference between it’s gamma correction and post processing’s shader pass? If not, then I must be missing some additional conversion pass in my composer chain. :face_with_monocle:

So in other words: how can I achieve the first image output using the EffectComposer? Im particularly looking for doing selective bloom, but I might be adding other effects later on as well.

Um, I don’t know so far why these strange artifacts occur. This is definitely worth investigating…

@DolphinIQ unfortunately I can’t get your example to run.

THREE.WebGLRenderer: WEBGL_compressed_texture_astc extension not supported.
THREE.WebGLRenderer: WEBGL_compressed_texture_etc1 extension not supported.
THREE.WebGLRenderer: WEBGL_compressed_texture_pvrtc extension not supported.
THREE.WebGLRenderer: WEBKIT_WEBGL_compressed_texture_pvrtc extension not supported.

Loading Complete!

CompressedTexture {uuid: "8E71EAFF-6413-44CB-980E-645891D3A45B", name: "", image: {…}, mipmaps: Array(1), mapping: 300, …}

three.module.js:20960 THREE.WebGLState: TypeError: Failed to execute 'texImage2D' on 'WebGLRenderingContext': No function was found that matched the signature provided.
    at Object.texImage2D (three.module.js:20956)
    at setTextureCube (three.module.js:21576)
    at WebGLTextures.safeSetTextureCube (three.module.js:22344)
    at SingleUniform.setValueT6 [as setValue] (three.module.js:17002)
    at Function.WebGLUniforms.upload (three.module.js:17421)
    at setProgram (three.module.js:25213)
    at WebGLRenderer.renderBufferDirect (three.module.js:23935)
    at renderObject (three.module.js:24687)
    at renderObjects (three.module.js:24657)
    at WebGLRenderer.render (three.module.js:24436)

Hi @looeee, could you please replace one line in three.module.js from this PR

Mugen made it possible to build a CubeTexture out of compressed images. After that, it should work. Sorry for the confusion

It seems the issue can be fixed if the internal render targets of EffectComposer are created differently. If you enhance the following section:

by this type: FloatType, the result is as expected. The default type is UnsignedByteType by the way.

I’m not yet sure why this removes the artifacts but this might be related to a precision issue.

1 Like

So it was related to precision after all :hushed:
It reminded me a little of those old space games arts

Thanks a lot guys! This was a really instructive thread :pray: :bowing_man:
Managed to get selective bloom working for my game!

Intrstingly enough, the precision still wasn’t as good as when using the renderer’s default encoding. Perhaps that one uses 64-bit floats? Anyway, I lowered the renderer.gammaFactor to 1.7 to get the best of both worlds and all looks well now :+1:

2 Likes

Nope, double-precision floating-point is not supported as a data type of texel data.

1 Like

Well, then Im beat :sweat_smile:

1 Like

What is the status of this issue? Has a bug been reported on GitHub for a proper fix?

We’re having a similar issue since upgrading to v112 where gamma correction is doesn’t seem to be applied when using Effect Composer but otherwise is.
Adding type: FloatType in EffectComposer (and optionally to all the various passes as well) or even replacing the default type in Texture didn’t fix it.

Can you please demonstrate this with a live example or some code? There might be an issue in your app elsewhere.

I made a jsfiddle to exhibit the problem: https://jsfiddle.net/neptilo/femq6j93/4/
There, I copied the EffectComposer and added type: FloatType.
I added gamma-related parameters to the renderer.
The passes added to the effect composer are simply a render pass and a CopyShader pass.
When useComposer is false, gamma correction is applied. When true, it isn’t.

As mentioned earlier in this topic, you have to use a gamma correction pass if you use post processing. WebGLRenderer.outputEncoding is only evaluated when you directly render to the default frambebuffer or screen. Besides when you pass in a predefined render target to EffectComposer, you already have to define the correct type (THREE.FloatType ) for this render target. Otherwise do it like in the below fiddle and pass no render target to EffectComposer.

https://jsfiddle.net/xkgcz3ar/

OK, I hadn’t realized GammaCorrectionShader referred to an external class that had to be imported manually and was a necessary addition to apply gamma correction when using EffectComposer . It is a bit surprising to require an additional non-built-in file when none was required before v112.
In any case, thank you!

Before R112, most devs performed gamma correction at the wrong place. In most scenarios, it should be performed at the end of the pass chain, not when producing the render pass. So working with GammaCorrectionShader is actually more correct. And it’s worth the migration effort.

1 Like

An alternative that worked for me is to perform gamma correction at the end of each one of my geometry shaders so I get the correct final result regardless of whether I’m rendering to canvas or to a RenderTarget. This lets me deal with post-processing shaders in linear space, without needing a final color-correction pass. And I can also turn post on/off at will.

1 Like