javascriptthree.jspost-processingbloom

Threejs / EffectComposer - Interactively masking an UnrealBloomPass


So, I am trying to dynamically mask an UnrealBloomPass with the EffectComposer and I am getting unexpected results. I am not sure if I am missing a key concept here, or if I should be trying to achieve this in a different way. Any input would be appreciated.

The composer is set up into these layers:

I've been following tutorials and examples as closely as possible and still no dice. Thanks in advance.

 <!DOCTYPE html>
<html lang="en">
    <head>
        <title>BloomShader Mask</title>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
        <style>
            body {
                color: #fff;
                font-family:Monospace;
                font-size:13px;
                text-align:center;
                background-color: #fff;
                margin: 0px;
                overflow: hidden;
            }
            #info {
                position: absolute;
                top: 0px;
                width: 100%;
                padding: 5px;
            }
            #info p {
                max-width: 600px;
                margin-left: auto;
                margin-right: auto;
                padding: 0 2em;
            }
            a {
                color: #2983ff;
            }
        </style>
    </head>
    <body>
        <div id="container"></div>
        <script src="./build/three.js"></script>
        <script src="js/libs/stats.min.js"></script>
        <script src="js/libs/dat.gui.min.js"></script>
        <script src="js/controls/OrbitControls.js"></script>
        <script src="js/loaders/GLTFLoader.js"></script>
        <script src="js/postprocessing/EffectComposer.js"></script>
        <script src="js/postprocessing/RenderPass.js"></script>
        <script src="js/postprocessing/ShaderPass.js"></script>
        <script src="js/postprocessing/MaskPass.js  "></script>
        <script src="js/shaders/CopyShader.js"></script>
        <script src="js/shaders/LuminosityHighPassShader.js"></script>
        <script src="js/postprocessing/UnrealBloomPass.js"></script>
    <script id="gradientShader" type="shader-code">
      uniform vec2 resolution;
      uniform vec2 mousePosition;
      void main() {
        vec2 pos = (gl_FragCoord.xy - mousePosition.xy * 1.2) / resolution.xy;
        gl_FragColor = vec4(1.0, pos.x, pos.y, 1.0);
      }
    </script>
    <script>
      var clock = new THREE.Clock();
      var stats;
      var gui;
      var sceneHexGradient;
      var sceneMask;
      var camera; 
      var orbitControls;
      var pointLight;
      var composer;
      var mixer;
      var maskerMesh;
      var bloomPass;
      var width = 0;
      var height = 0;
      var cameraZoom = 120;
      var mousePositionY = 0;

      var params = {
          exposure: 2,
          bloomStrength: .81,
          bloomThreshold: 0,
          bloomRadius: .05,
          cameraZoom: cameraZoom
      };

      function init() {
        // Init vars (DOM needed)
        gui = initGui();
        stats = initStats();
        width = window.innerWidth;
        height = window.innerHeight;
        pixelRatio = window.devicePixelRatio;

        // Renderer
        var webGLRenderer = new THREE.WebGLRenderer({ antialias: true });
        webGLRenderer.setClearColor(new THREE.Color(0x000000, 1.0));
        webGLRenderer.setPixelRatio(pixelRatio);
        webGLRenderer.setSize(width , height);
        webGLRenderer.toneMapping = THREE.ReinhardToneMapping;
        webGLRenderer.shadowMap.enabled = true;
        appendChild(webGLRenderer.domElement);

        // Scene
        sceneHexGradient = new THREE.Scene();
        sceneMasker = new THREE.Scene();

        // Camera
        camera = new THREE.OrthographicCamera(-width, width, height, -height, -1000, 1000);
        camera.zoom = cameraZoom;
        camera.lookAt(new THREE.Vector3(0, 0, 0));
        camera.updateProjectionMatrix();

        sceneHexGradient.add(camera);
        sceneMasker.add(camera);

        // Orbit
        orbitControls = new THREE.OrbitControls(camera, webGLRenderer.domElement);
        orbitControls.maxPolarAngle = Math.PI * 0.5;
        orbitControls.minDistance = 1;
        orbitControls.maxDistance = 10;

        // Draw onto Scenes
        drawGradientToScene(sceneHexGradient);
        drawHexToScene(sceneHexGradient);
        drawMaskerToScene(sceneMasker);

        // ShaderPass
        var hexgradientPass = new THREE.RenderPass(sceneHexGradient, camera);
        hexgradientPass.clear = false;

        var effectCopyPass = new THREE.ShaderPass(THREE.CopyShader);
        effectCopyPass.renderToScreen = true;

        var maskingPass = new THREE.MaskPass(sceneMasker, camera);
        maskingPass.clear = true;
        // var maskingPass = new THREE.RenderPass(sceneMasker, camera);
        // maskingPass.renderToScreen = true;

        bloomPass = new THREE.UnrealBloomPass(new THREE.Vector2(width, height), 1.5, 0.4, 0.85);
        bloomPass.renderToScreen = true;
        bloomPass.threshold = params.bloomThreshold;
        bloomPass.strength = params.bloomStrength;
        bloomPass.radius = params.bloomRadius;

        var clearMaskPass = new THREE.ClearMaskPass();

        composer = new THREE.EffectComposer(webGLRenderer);
        composer.renderTarget1.stencilBuffer = true;
        composer.renderTarget2.stencilBuffer = true;
        composer.setSize(width, height);
        composer.addPass(hexgradientPass); 
        composer.addPass(maskingPass);
        // composer.addPass(maskingVerticalBlurPass);
        composer.addPass(bloomPass);
        composer.addPass(clearMaskPass);
        composer.addPass(effectCopyPass);

        render();

        function initStats() { // Debug only 
          var stats = new Stats();
          stats.setMode(0);
          stats.domElement.style.position = 'absolute';
          stats.domElement.style.left = '0px';
          stats.domElement.style.top = '0px';
          appendChild(stats.dom);
          return stats;
        }

        function initGui() { // Debug only
                gui = new dat.GUI();

          gui.add(params, 'exposure', 0.1, 2 ).onChange((value) => {
            webGLRenderer.toneMappingExposure = Math.pow(value, 4.0);
          });

          gui.add(params, 'bloomThreshold', 0.0, 1.0).onChange((value) => {
            bloomPass.threshold = Number(value);
          });

          gui.add(params, 'bloomStrength', 0.0, 3.0).onChange((value) => {
            bloomPass.strength = Number(value);
          });

          gui.add(params, 'bloomRadius', 0.0, 1.0).step(0.01).onChange((value) => {
            bloomPass.radius = Number(value);
          });

          gui.add(params, 'cameraZoom', 100, 150).onChange((value) => {
            camera.zoom = Number(value);
            camera.updateProjectionMatrix();
          });

          return gui;
        }

        function appendChild(domElement) {
          var container = document.getElementById('container');
          if (container) {
            return container.appendChild(domElement);
          }
          return false;
        }

        function drawGradientToScene(scene) {
          var gradientUniforms = {};
          gradientUniforms["resolution"] = { type:'v2', value:new THREE.Vector2(width, height)};
          gradientUniforms['mousePosition'] = { type:'v2', value:new THREE.Vector2(0, 0) };
          var shaderCode = document.getElementById('gradientShader').innerHTML;
          var material = new THREE.ShaderMaterial({ uniforms:gradientUniforms, fragmentShader:shaderCode });
          var geometry = new THREE.PlaneBufferGeometry(width, height);
          var mesh = new THREE.Mesh(geometry, material);
          scene.add(mesh);
        }

        function drawHexToScene(scene) {
          var hexes = [];
          var colCount = 100;
          var rowCount = 100;
          var hexDiameter = 1;
          var xStart = -(colCount) * hexDiameter * 0.5;
          var rowSpace = Math.sqrt(3) * (hexDiameter * 0.5);
          var yStart = (rowCount - 1) * (rowSpace * 0.5);

          var hexGeom = new THREE.CylinderGeometry(hexDiameter * .55, hexDiameter * .55, 0.0625, 6, 1);
          hexGeom.rotateX(Math.PI * 0.5);
          for (let j = 0; j < rowCount; j++) {
            for (let i = 0; i < colCount + (j % 2 === 0 ? 0 : 1); i++) {
              let hex = new THREE.Mesh(hexGeom, new THREE.MeshBasicMaterial({
                color: 0x000000,
                wireframe: false,
              }));
              var x = (xStart + i * hexDiameter + (j % 2 === 0 ? 0.5 * hexDiameter : 0));
              var y = (yStart - j * rowSpace);
              hex.position.set(x, y, 0);
              hexes.push(hex);
              scene.add(hex);
            }
          }
        }

        function drawMaskerToScene(scene) {
          var boxGeometry = new THREE.BoxGeometry(width, height, 10);
          var basicMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff });
          maskerMesh = new THREE.Mesh(boxGeometry, basicMaterial);
          maskerMesh.position.y = -height;
          scene.add(maskerMesh);
        }  

        function onResze() {
          camera.position.set(0, 0, -15);
          camera.zoom = cameraZoom;
          camera.updateProjectionMatrix();
                  composer.setSize(width, height);
        }

        function onMouseMove(event) {
          mousePositionY = event.clientY; 
        }

        function render() {
          webGLRenderer.autoClear = false;
          stats.update();

          var delta = clock.getDelta();
          // orbitControls.update(delta);

          requestAnimationFrame(render);

          maskerMesh.position.y = mousePositionY;
          // composer.setSize(window.innerWidth, window.innerHeight);
          composer.render();
        }

        window.addEventListener('mousemove', onMouseMove);
    } // End - init 
    // Listeners
    window.onload = init;
    window.onresize = init.onResze;
        </script>
    </body>
</html>

Solution

  • It was actually a combination of issues. The main issue was setting the flags on the passes, to clear or to render to screen. This wasn't an easy thing to debug:

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <title>BloomShader Mask</title>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, user-scalable=no, 
        minimum-scale=1.0, maximum-scale=1.0">
        <style>
            body {
                color: #fff;
                font-family:Monospace;
                font-size:13px;
                text-align:center;
                background-color: #fff;
                margin: 0px;
                overflow: hidden;
            }
            #info {
                position: absolute;
                top: 0px;
                width: 100%;
                padding: 5px;
            }
            #info p {
                max-width: 600px;
                margin-left: auto;
                margin-right: auto;
                padding: 0 2em;
            }
            a {
                color: #2983ff;
            }
        </style>
    </head>
    <body>
        <div id="container"></div>
        <script src="./build/three.js"></script>
        <script src="js/libs/stats.min.js"></script>
        <script src="js/libs/dat.gui.min.js"></script>
    <script src="js/controls/OrbitControls.js"></script>
        <script src="js/loaders/GLTFLoader.js"></script>
        <script src="js/postprocessing/EffectComposer.js"></script>
    <script src="js/postprocessing/ClearPass.js"></script>
        <script src="js/postprocessing/RenderPass.js"></script>
    <script src="js/postprocessing/ShaderPass.js"></script>
    <script src="js/postprocessing/MaskPass.js  "></script>
        <script src="js/shaders/CopyShader.js"></script>
        <script src="js/shaders/LuminosityHighPassShader.js"></script>
    <script src="js/postprocessing/UnrealBloomPass.js"></script>
    <script id="gradientShader" type="shader-code">
      uniform vec2 resolution;
      uniform vec2 mousePosition;
      void main() {
        vec2 pos = ((gl_FragCoord.xy - mousePosition.xy * 1.3) / resolution.xy);
        gl_FragColor = vec4(1.0, pos.x, pos.y, 1.0);
      }
    </script>
        <script>
      var clock = new THREE.Clock();
      var stats;
      var gui;
      var sceneHexGradient;
      var sceneBloomMasker;
      var sceneMasker;
      var camera; 
      var orbitControls;
      var pointLight;
      var composer;
      var mixer;
      var maskerMesh;
      var bloomPass;
      var width = 0;
      var height = 0;
      var cameraZoom = 120;
      var mousePositionY = 0;
    
            var params = {
                exposure: 2, //1.1
                bloomStrength: 1.75,
                bloomThreshold: 0,
        bloomRadius: .16,
        cameraZoom: cameraZoom
            };
    
      function init() {
        // Init vars (DOM needed)
        gui = initGui();
        stats = initStats();
        width = window.innerWidth;
        height = window.innerHeight;
        pixelRatio = window.devicePixelRatio;
    
        // Camera
        camera = new THREE.OrthographicCamera(-width, width, height, -height, -1000, 1000);
        camera.zoom = cameraZoom;
        camera.lookAt(new THREE.Vector3(0, 0, 0));
        camera.updateProjectionMatrix();
    
        // Renderer
        var webGLRenderer = new THREE.WebGLRenderer({ antialias: true });
        webGLRenderer.setClearColor(new THREE.Color(0x000000, 1.0));
        webGLRenderer.setPixelRatio(pixelRatio);
        webGLRenderer.setSize(width , height);
        webGLRenderer.toneMapping = THREE.ReinhardToneMapping;
        webGLRenderer.shadowMap.enabled = true;
        webGLRenderer.autoClear = false;
        appendChild(webGLRenderer.domElement);
    
        // Scene
        sceneHexGradient = new THREE.Scene();
        sceneBloom = new THREE.Scene();
        sceneMasker = new THREE.Scene();
    
        sceneHexGradient.add(camera);
        sceneBloom.add(camera);
        sceneMasker.add(camera);
    
        // Lights (are they needed ? ) 
        sceneBloom.add(new THREE.AmbientLight(0xffffff));
    
        // Orbit
        orbitControls = new THREE.OrbitControls(camera, webGLRenderer.domElement);
        orbitControls.maxPolarAngle = Math.PI * 0.5;
        orbitControls.minDistance = 1;
        orbitControls.maxDistance = 10;
    
        // Draw onto Scenes
        drawGradientToScene(sceneHexGradient);
        drawHexToScene(sceneHexGradient);
        drawMaskerToScene(sceneMasker);
    
        bloomPass = new THREE.UnrealBloomPass(new THREE.Vector2(width, height), 1.5, 0.4, 0.85);
        bloomPass.threshold = params.bloomThreshold;
        bloomPass.strength = params.bloomStrength;
        bloomPass.radius = params.bloomRadius;
    
        // ShaderPasses
        var clearPass       = new THREE.ClearPass();
        var clearMaskPass   = new THREE.ClearMaskPass();
        var hexgradientPass = new THREE.RenderPass(sceneHexGradient, camera);
        var maskingPass     = new THREE.MaskPass(sceneMasker, camera);
        var outputPass      = new THREE.ShaderPass(THREE.CopyShader);
    
                outputPass.renderToScreen = true;
    
        var parameters = {
                    minFilter: THREE.LinearFilter,
                    magFilter: THREE.LinearFilter,
                    format: THREE.RGBFormat,
                    stencilBuffer: true
                };
    
                var renderTarget = new THREE.WebGLRenderTarget(width, height, parameters);
    
        composer = new THREE.EffectComposer(webGLRenderer, renderTarget);
    
        composer.addPass(clearPass);
        composer.addPass(hexgradientPass); 
        composer.addPass(maskingPass);
        composer.addPass(bloomPass);
        composer.addPass(clearMaskPass);
        composer.addPass(outputPass);
    
        function initStats() { // Debug only 
          var stats = new Stats();
          stats.setMode(0);
          stats.domElement.style.position = 'absolute';
          stats.domElement.style.left = '0px';
          stats.domElement.style.top = '0px';
          appendChild(stats.dom);
          return stats;
        }
    
        function initGui() { // Debug only
                gui = new dat.GUI();
    
          gui.add(params, 'exposure', 0.1, 2 ).onChange((value) => {
            webGLRenderer.toneMappingExposure = Math.pow(value, 4.0);
          });
    
          gui.add(params, 'bloomThreshold', 0.0, 1.0).onChange((value) => {
            bloomPass.threshold = Number(value);
          });
    
          gui.add(params, 'bloomStrength', 0.0, 3.0).onChange((value) => {
            bloomPass.strength = Number(value);
          });
    
          gui.add(params, 'bloomRadius', 0.0, 1.0).step(0.01).onChange((value) => {
            bloomPass.radius = Number(value);
          });
    
          gui.add(params, 'cameraZoom', 100, 150).onChange((value) => {
            camera.zoom = Number(value);
            camera.updateProjectionMatrix();
          });
    
          return gui;
        }
    
        function appendChild(domElement) {
          var container = document.getElementById('container');
          if (container) {
            return container.appendChild(domElement);
          }
          return false;
        }
    
        function getRandomNumber(min = 1, max = 100) {
          return Math.floor(Math.random() * (+max - +min)) + +min; 
        }
    
        function getArrayOfRandomNumbers(count = 1, min = 1, max = 100) {
          const outArr = [];
          for( i = 0; i < count; ++i) {
            outArr.push(getRandomNumber(min, max));
          }
          return outArr.sort();
        }
    
        function drawBlackoutMask(scene) {
          // var x = 0, y = 0;
          // var hexDiameter = 2;
          // var shapeWidth = 10;
    
          // var facets = getRandomNumber(1, shapeWidth);
          // var xValuesArr = getArrayOfRandomNumbers(facets, 0, shapeWidth);
          // var yValuesArr = getArrayOfRandomNumbers(facets, 0, shapeWidth);
    
          // var hexGeom = new THREE.CylinderGeometry(hexDiameter * .55, hexDiameter * .55, 0.0625, 6, 1);
          // hexGeom.rotateX(Math.PI * 0.5);
          // var boxGeometry = new THREE.BoxGeometry(10, 10, 10);
          // var mesh = new THREE.Mesh(boxGeometry, new THREE.MeshBasicMaterial({ color: 0xff0000 }));
          // mesh.position.x = 0;
          // mesh.position.y = 0;
          // scene.add(mesh);  
    
          // for(var i = 0; i < facets; i++) {
          //   var hexGeom = new THREE.CylinderGeometry(hexDiameter * .55, hexDiameter * .55, 0.0625, 6, 1);
          //   var mesh = new THREE.Mesh(hexGeom, new THREE.MeshBasicMaterial({
          //     color: 0xff0000,
          //     wireframe: false,
          //   }));
          //   mesh.position.x = xValuesArr[i];
          //   mesh.position.y = yValuesArr[i];
          //   scene.add(mesh);
          // }          
        }
    
        function drawMaskerToScene(scene) {
          var boxGeometry = new THREE.CylinderBufferGeometry(width, height, 10);
          // var basicMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff });
          boxGeometry.computeBoundingBox();
          var material = new THREE.ShaderMaterial({
            uniforms: {
              color1: {
                value: new THREE.Color(0xffffff)
              },
              color2: {
                value: new THREE.Color(0x000000)
              },
              bboxMin: {
                value: boxGeometry.boundingBox.min
              },
              bboxMax: {
                value: boxGeometry.boundingBox.max
              }
            },
            vertexShader: `
              uniform vec3 bboxMin;
              uniform vec3 bboxMax;
    
              varying vec2 vUv;
    
              void main() {
                vUv.y = (position.y - bboxMin.y) / (bboxMax.y - bboxMin.y);
                gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0);
              }
            `,
            fragmentShader: `
              uniform vec3 color1;
              uniform vec3 color2;
    
              varying vec2 vUv;
    
              void main() {
                gl_FragColor = vec4(mix(color1, color2, vUv.y), 1.0);
              }
            `,
            wireframe: false
          });
          maskerMesh = new THREE.Mesh(boxGeometry, material); //basicMaterial);
          // maskerMesh.scale.set(1,1,1);
          // maskerMesh.position.y = -height;
          scene.add(maskerMesh);
        }  
    
        function drawGradientToScene(scene) {
          var gradientUniforms = {};
          gradientUniforms["resolution"] = { type:'v2', value:new THREE.Vector2(width, height)};
          gradientUniforms['mousePosition'] = { type:'v2', value:new THREE.Vector2(0, 0) };
          var shaderCode = document.getElementById('gradientShader').innerHTML;
          var material = new THREE.ShaderMaterial({ uniforms:gradientUniforms, fragmentShader:shaderCode });
          var geometry = new THREE.PlaneBufferGeometry(width, height);
          var mesh = new THREE.Mesh(geometry, material);
          scene.add(mesh);
        }
    
        function drawHexToScene(scene) {
          var hexes = [];
          var colCount = 100;
          var rowCount = 100;
          var hexDiameter = 1;
          var xStart = -(colCount) * hexDiameter * 0.5;
          var rowSpace = Math.sqrt(3) * (hexDiameter * 0.5);
          var yStart = (rowCount - 1) * (rowSpace * 0.5);
    
          var hexGeom = new THREE.CylinderGeometry(hexDiameter * .55, hexDiameter * .55, 0.0625, 6, 1);
          hexGeom.rotateX(Math.PI * 0.5);
          for (let j = 0; j < rowCount; j++) {
            for (let i = 0; i < colCount + (j % 2 === 0 ? 0 : 1); i++) {
              let hex = new THREE.Mesh(hexGeom, new THREE.MeshBasicMaterial({
                color: 0x000000,
                wireframe: false,
              }));
              var x = (xStart + i * hexDiameter + (j % 2 === 0 ? 0.5 * hexDiameter : 0));
              var y = (yStart - j * rowSpace);
              hex.position.set(x, y, 0);
              hexes.push(hex);
              scene.add(hex);
            }
          }
        }
    
        function onResze() {
          camera.position.set(0, 0, -15);
          camera.zoom = cameraZoom;
          camera.updateProjectionMatrix();
                  composer.setSize(width, height);
        }
    
        function onMouseMove(event) {
          mousePositionY = event.clientY; 
        }
    
        function render() {
          webGLRenderer.autoClear = false;
          stats.update();
    
          var delta = clock.getDelta();
    
          // orbitControls.update(delta);
    
          requestAnimationFrame(render);
    
          maskerMesh.position.y = mousePositionY;
          webGLRenderer.clear();
          composer.render(delta);
        }
    
        window.addEventListener('mousemove', onMouseMove);
        render();
    } // End - init 
    // Listeners
    window.onload = init;
    window.onresize = init.onResze;
        </script>
    </body>