Weird EffectComposer result

Hi, I hope someone can spot what stupid mistake I’m making here… :slight_smile:

Part of a bigger feature, I need to render the main scene at a specific point and apply some effects to it, but I’m getting weird results. I stripped it down to basically a copy shader, but I can’t seem to be able to get the same output as when I just normally render the scene using App.renderer.render(App.scene, App.camera);.

Here is the stripped down setup:

const uniforms = {
  uBgTexture: { value: null }
};

/**
  * App.renderer.render(...) is not called in this case, RenderPass renders the scene, 
  * then it gets passed to the CustomPass, which is forced to render to screen.
  */
const composer = new EffectComposer(App.renderer);
composer.renderToScreen = false;
composer.addPass(new RenderPass(App.scene, App.camera)); // renders to buffer
composer.addPass(new CustomPass(uniforms)); // renders to screen

and in CustomPass:

export class CustomPass extends Pass {
  protected material: ShaderMaterial;
  protected fsQuad: FullScreenQuad;

  constructor(uniforms: { [uniform: string]: IUniform }) {
    super();

    this.material = new ShaderMaterial({
      uniforms,
      vertexShader,
      fragmentShader
    });

    this.fsQuad = new FullScreenQuad(this.material);
  }

  render(renderer: WebGLRenderer, writeBuffer: WebGLRenderTarget, readBuffer: WebGLRenderTarget) {
    this.material.uniforms.uBgTexture.value = readBuffer.texture;
    renderer.setRenderTarget(null);
    renderer.clear(); // makes no difference
    this.fsQuad.render(renderer);
  }

  dispose() {
    this.material.dispose();
    this.fsQuad.dispose();
  }
}

in the fragment shader I simply copy the whole scene:

void main()
{
    vec4 bg = texture(uBgTexture, vUv);
    gl_FragColor = bg;
    #include <tonemapping_fragment>
    #include <colorspace_fragment>
}

This is what it should look like (using App.renderer.render(...)):

This is what I get with the EffectComposer setup (more opaque, and maybe saturated):

And finally, if I comment out either the colorspace_fragment or both includes in the shader, I get this:

Adding an OutputPass to the end doesn’t change anything.
I’m pretty sure I’m missing something trivial :thinking:

Normally you do not have to include the tone mapping and color space conversion chunks into a custom pass. Ideally, you let OutputPass do this step.

Yes, I get that, and as I mentioned, I did try with OutputPass and removed the chunks from the shader, but I get the 3rd (dark) version :confused:

Any chances to demonstrate what you are doing with a live example? three.js dev template - module - JSFiddle - Code Playground

Hi @Mugen87, took me a bit, but finally I managed to reproduce it in a fiddle.

I put a 2s timeout to switch from the default renderer to the composer.
By default (with the OutputPass added), you can see the scene turning “brighter”.
If you comment it out (line 594) and rerun it, it will turn darker.
But it will never reproduce the original result.

Forgive the bloated setup in the beginning, I needed to build something similar to what I have.
And really appreciate you looking into it, thank you!

@electric.cicada are you still having this issue, and is the JSFiddle still the current code you’d like help debugging?

Maybe this is a bit easier to read (but it’s basically the same): fiddle

I tried everything I could think of, enabling/disabling the color conversion chunks fully/partly, changing material/texture color space, added sRGBTransferOETF as you suggested (hopefully I did it correctly…). I have this feeling the I’m missing the forest for the trees, but at this point maybe only a second set of eyes can help :slight_smile:

As a starting point, I would work backwards to make things match by disabling the transparency.

  1. In the MeshBasicMaterial declaration let’s comment out these properties, so we’re just dealing with opaque surfaces: transparent, opacity, depthWrite, blending
  2. In the CustomPass ShaderMaterial declaration, we’ll update the main function to look like this:
void main() {
  gl_FragColor = texture(uBgTexture, vUv);        
  #include <tonemapping_fragment>
  #include <colorspace_fragment>
}

At this point the result should look identical with or without EffectComposer – do you see the same? If not, let’s stop and figure that out before bringing the transparency back in.


Now if we re-enable the MeshBasicMaterial transparency-related properties, you’ll again see a (smaller) difference between the render with and without EffectComposer. This is (like the other thread) because of the difference in alpha blending in sRGB vs. Linear-sRGB space.

In general the way EffectComposer handles this is correct — we’d do the same without post-processing if it were possible in WebGL, but it isn’t. So if you can get results you like that way, that’s what I’d recommend. But if it’s not acceptable for whatever reason, let us know!

1 Like

First of all, thank you for taking the time to check it!

Yes, I can confirm that by removing transparency the two are identical, I have tried that before to verify that it was because of the transparency, sorry I forgot to mention it.
I also tried to pass a RenderTarget to the composer and play around with its props, but no luck.

Unfortunately for me even this slight difference is a big issue, because I have much more going on in the actual scene, and it really looks “blown out” once the composer kicks in :confused:
I don’t mind going full custom on this, as long as there’s a way to achieve the same colors.

In that case, I think the next things to try would be:

  1. Set renderer.outputColorSpace = THREE.LinearSRGBColorSpace
  2. Set .map.colorSpace = THREE.LinearSRGBColorSpace on sphere-cube material

This appears to be enough for the JSFiddle, but if there’s other content in the scene it may need similar adjustments.

If that works, then the OutputPass and these two chunks …

  #include <tonemapping_fragment>
  #include <colorspace_fragment>

… are no longer doing anything and could be removed.

I switched everything to LinearSRGBColorSpace and it surely solved the problem with the composer. Now I just have to go through everything again, readjust the colors everywhere and see if it still looks good :smiley:

Is the only drawback of linear the possible banding in dark areas, or is there anything else maybe?

Thank you for the suggestion again, @donmccurdy

The naming is misleading, what we’re really doing by setting .outputColorSpace = LinearSRGBColorSpace is disabling conversions so that rendering and blending are done in the sRGB color space, and then output directly to the canvas. If you find you’re having to re-adjust colors everywhere, then setting THREE.ColorManagement.enabled = false might handle the input side of that, but it wasn’t a factor in the JSFiddle so I’m not sure.

I don’t like the term, but this is sometimes called a “gamma workflow.” If you’re using lights and lit materials, the major downside here is that we’re baking a bunch of wrong assumptions into the rendering pipeline and the art direction, as noted in What every coder should know about gamma. If you’re not doing lit rendering (just blending, overlays, etc.), the tradeoffs are less clear to me. I don’t expect banding to necessarily be any worse, and dithering can still address that if so.

Yes, ColorManagement.enabled = false did the trick! :partying_face:
It still looks darker than it was before, but I’ll fiddle with it until it looks the same again.
I don’t use any lights and I either use MeshBasicMaterial or my custom shaders, so lucky me.

The naming is misleading, what we’re really doing by setting .outputColorSpace = LinearSRGBColorSpace is disabling conversions so that rendering and blending are done in the sRGB color space, and then output directly to the canvas.

Ah, got it, that’s good to know! Thanks for all the help, very much appreciated!

1 Like