Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to blend multiple layers with opacity?

Using three.js, I would like to apply separate post-processing effects to individual scenes and then combine all these scenes into a final render. To do this I am using the three.js Effects Composer.

const radialBlurShader = {
	uniforms: {
		"tDiffuse": { value: null },
	},
	vertexShader: `
		varying vec2 vUv;
		void main() {
			vUv = uv;
			gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
		}
	`,

	fragmentShader: `
		uniform sampler2D tDiffuse;
		varying vec2 vUv;

    const float strength = 0.7;
    const int samples = 50;

		void main() {
      vec4 col = vec4(0);

      vec2 dir = vec2(0.5) - vUv;
      for (int i = 0; i < samples; i++) {
        float amount = strength * float(i) / float(samples);

        vec4 sample = 
          texture2D(tDiffuse, vUv + dir * amount) + 
          texture2D(tDiffuse, vUv - dir * amount);

        col += sample;
      }

			gl_FragColor = 0.5 * col / float(samples);
		}
	`
};

const renderWidth = window.innerWidth;
const renderHeight = window.innerHeight;

const renderer = new THREE.WebGLRenderer({
  antialias: true,
});

renderer.setSize(renderWidth, renderHeight);
document.body.appendChild(renderer.domElement);

const camera = new THREE.PerspectiveCamera(45, renderWidth / renderHeight, 0.1, 1000);
camera.position.z = 10;

var geometry = new THREE.PlaneGeometry(1, 1);
var material = new THREE.MeshBasicMaterial({ 
  color: 0x0000ff,
  transparent: true
});

function makeEC(scene) {
  const ec = new THREE.EffectComposer(renderer);
  const rp = new THREE.RenderPass(scene, camera);
  const sp = new THREE.ShaderPass(radialBlurShader);

  rp.clearColor = 0xffffff;
  rp.clearAlpha = 0;

  sp.renderToScreen = true;
  sp.material.transparent = true;
  sp.material.blending = THREE.CustomBlending;

  ec.addPass(rp);
  ec.addPass(sp);
  return ec;
}

const scene1 = new THREE.Scene();
const mesh1 = new THREE.Mesh(geometry, material);
mesh1.position.x = 2;
scene1.add(mesh1);
ec1 = makeEC(scene1);

const scene2 = new THREE.Scene();
const mesh2 = new THREE.Mesh(geometry, material);
mesh2.position.x = -2;
scene2.add(mesh2);
ec2 = makeEC(scene2);

renderer.setClearColor(0xffffaa, 1);
renderer.autoClear = false;
renderer.clear();
ec1.render();
ec2.render();
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r82/three.js"></script>
<script src="https://rawgit.com/mrdoob/three.js/r82/examples/js/shaders/CopyShader.js"></script>
<script src="https://rawgit.com/mrdoob/three.js/r82/examples/js/postprocessing/EffectComposer.js"></script>
<script src="https://rawgit.com/mrdoob/three.js/r82/examples/js/postprocessing/RenderPass.js"></script>
<script src="https://rawgit.com/mrdoob/three.js/r82/examples/js/postprocessing/ShaderPass.js"></script>

I create a separate Effects Composer instance for each post-processing shader I want to apply. (In this example I am using the same radial blur shader twice for simplicity.)

function makeEC(scene) {
    const ec = new THREE.EffectComposer(renderer);
    const rp = new THREE.RenderPass(scene, camera);
    const sp = new THREE.ShaderPass(radialBlurShader);

    rp.clearColor = 0xffffff;
    rp.clearAlpha = 0;

    sp.renderToScreen = true;
    sp.material.transparent = true;

    // defaults to SRC_ALPHA, ONE_MINUS_SRC_ALPHA
    sp.material.blending = THREE.CustomBlending;

    ec.addPass(rp);
    ec.addPass(sp);
    return ec;
}

const ec1 = makeEC(scene1);
const ec2 = makeEC(scene2);

As you see, I clear the render pass to a transparent background. Then the shader pass will draw to the renderbuffer using the typical SRC_ALPHA, ONE_MINUS_SRC_ALPHA blend.

My render code then looks like this

renderer.setClearColor(0xffffaa, 1);
renderer.autoClear = false;
renderer.clear();
ec1.render();
ec2.render();

However, this process does not blend the layers together correctly

This is what I get

enter image description here

First pass before blending (correct)

enter image description here

Second pass before blending (correct)

enter image description here

Both passes blended as described (incorrect) Too dark

enter image description here

premultipliedAlpha disabled (incorrect) Too transparent

Why are the squares too dark when both layers are blended together?

Why are the squares too transparent when premultipliedAlpha is disabled?

How can I blend both layers together so that they look the same as before blending?

like image 737
Lucas C Avatar asked Nov 17 '16 07:11

Lucas C


1 Answers

Changing the Shader Pass blendSrc to ONE fixes the problem. e.g.

sp.material.blending = THREE.CustomBlending;
sp.material.blendSrc = THREE.OneFactor;

I believe this works because of how the radial blur shader functions in particular. First the render pass color buffer is cleared to a black transparent color and an opaque square is drawn inside of it. The radial blur shader then blurs these opaque pixels with the transparent pixels around it. That has the effect of premultiplying the alpha of any non-transparent pixels.

At that point drawing the texture to the buffer doesn't require multiplying the source pixels by the alpha channel anymore. Is this the correct explanation?

like image 57
Lucas C Avatar answered Oct 22 '22 18:10

Lucas C