Search code examples
three.jsmask

Hide a part of an object using masks (EffectComposer)


Description of the problem

I am currently discovering the EffectComposer of three.js and I am looking for the method to hide a part of an object by another using masks.

Supposing you have a simple scene : a cube between the camera and a cylinder. The cube will have the role of the mask and hide the cylinder behind it :

Cube hiding the cylinder behind it

Solutions tested

I have played with the following examples of ThreeJS and tried to tweak them to get the result I want, but without success :

The problem comes from the use of the passes I guess.

I tried two solutions (check the snippet below to know the type of the passes added) :

1- Add the maskPass and then the renderPass so that the render of my scene will be drawn only inside my mask

  composer = new THREE.EffectComposer(renderer, renderTarget);
  composer.addPass(maskPass1);
  composer.addPass(renderPass);
  composer.addPass(clearMaskPass);
  composer.addPass(outputPass);

2- Add the renderPass, then the inverted mask, and a clearPass to remove the pixels

  maskPass1.inverse = true;
  composer = new THREE.EffectComposer(renderer, renderTarget);
  composer.addPass(renderPass);
  composer.addPass(maskPass1);
  composer.addPass(clearPass);
  composer.addPass(clearMaskPass);
  composer.addPass(outputPass);

Below, you will find a code snippet showing what I've done until now. I used a DotScreenPass just to see the effect of the maskPass.

On the image below, you'll see the result I get using the snippet on the left side and the result I want on the right side

Mask

Code snippet

var composer, renderer;
var box, torus;

init();
animate();

function init() {

  // Setup renderer
  renderer = new THREE.WebGLRenderer({antialias: false});
  renderer.setClearColor(0xe0e0e0);
  renderer.setPixelRatio(window.devicePixelRatio);
  renderer.setSize(window.innerWidth, window.innerHeight);
  renderer.autoClear = false;
  document.body.appendChild(renderer.domElement);

  // Setup scenes
  var camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 1, 1000);
  camera.position.z = 10;

  var scene1 = new THREE.Scene();
  var scene2 = new THREE.Scene();

  // Add objects
  box = new THREE.Mesh(new THREE.BoxGeometry(4, 4, 4));
  box.rotateY(Math.PI / 6);
  box.rotateX(-Math.PI / 6);
  scene1.add(box);

  torus = new THREE.Mesh(new THREE.TorusGeometry(3, 1, 16, 32), new THREE.MeshBasicMaterial({
    color: 0xff0000
  }));
  scene2.add(torus);

  // Create passes for composer
  var clearPass = new THREE.ClearPass();
  var clearMaskPass = new THREE.ClearMaskPass();

  var maskPass1 = new THREE.MaskPass(scene1, camera);
  var maskPass2 = new THREE.MaskPass(scene2, camera);
  maskPass1.inverse = true
  var renderPass = new THREE.RenderPass(scene2, camera);

  var screenDotPass = new THREE.DotScreenPass();

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

  var renderTarget = new THREE.WebGLRenderTarget(window.innerWidth, window.innerHeight, {
    minFilter: THREE.LinearFilter,
    magFilter: THREE.LinearFilter,
    format: THREE.RGBFormat,
    stencilBuffer: true
  });

  // Create composer and add passes
  composer = new THREE.EffectComposer(renderer, renderTarget);
  composer.addPass(renderPass);
  composer.addPass(maskPass1);
  composer.addPass(screenDotPass);
  composer.addPass(clearMaskPass);
  composer.addPass(outputPass);

}

function animate() {

  requestAnimationFrame(animate);

  var time = performance.now() * 0.001;
  renderer.clear();
  composer.render(time);

}
body
{
  background-color: #000;
  margin: 0px;
  overflow: hidden;
}
<div id="container"></div>

<script src="https://cdn.rawgit.com/mrdoob/three.js/master/build/three.js"></script>

<script src="https://cdn.rawgit.com/mrdoob/three.js/master/examples/js/shaders/CopyShader.js"></script>
<script src="https://cdn.rawgit.com/mrdoob/three.js/master/examples/js/shaders/DotScreenShader.js"></script>

<script src="https://cdn.rawgit.com/mrdoob/three.js/master/examples/js/postprocessing/EffectComposer.js"></script>
<script src="https://cdn.rawgit.com/mrdoob/three.js/master/examples/js/postprocessing/ClearPass.js"></script>
<script src="https://cdn.rawgit.com/mrdoob/three.js/master/examples/js/postprocessing/TexturePass.js"></script>
<script src="https://cdn.rawgit.com/mrdoob/three.js/master/examples/js/postprocessing/ShaderPass.js"></script>
<script src="https://cdn.rawgit.com/mrdoob/three.js/master/examples/js/postprocessing/MaskPass.js"></script>
<script src="https://cdn.rawgit.com/mrdoob/three.js/master/examples/js/postprocessing/DotScreenPass.js"></script>
<script src="https://cdn.rawgit.com/mrdoob/three.js/master/examples/js/postprocessing/RenderPass.js"></script>
<script src="https://cdn.rawgit.com/mrdoob/three.js/master/examples/js/postprocessing/DotScreenPass.js"></script>

<script src="https://cdn.rawgit.com/mrdoob/three.js/master/examples/js/Detector.js"></script>
<script src="https://cdn.rawgit.com/mrdoob/three.js/master/examples/js/libs/stats.min.js"></script>


Solution

  • After further researches, I found two solutions, but they do not use EffectComposer (which is not a problem for me).

    1st one : Depth buffer (Z-buffer)

    The idea is the following :

    1. Create two scenes, the first containing the objects playing the role of the mask, the other with your normal scene
    2. Prevent the renderer from writing into color buffer, only in the depth buffer (z-buffer) and "render" your mask scene. The z-buffer now contains the depth information of your mask scene
    3. Enable back the writing into the color buffer, and render your normal scene. Thanks to the depth comparison, some fragments of your normal scene won't be rendered

    Thanks you @WestLangley this question : How to write to zbuffer only with three.js

    var composer, renderer;
    var box, sphere;
    var scene1, scene2;
    var camera;
    
    init();
    animate();
    
    function init() {
    
      // Setup renderer
      renderer = new THREE.WebGLRenderer({
        antialias: false
      });
      renderer.setClearColor(0xe5e5e5);
      renderer.setPixelRatio(window.devicePixelRatio);
      renderer.setSize(window.innerWidth, window.innerHeight);
    
      // VERY IMPORTANT
      renderer.autoClear = false ;
    
      document.body.appendChild(renderer.domElement);
    
      // Setup scenes
      camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 1, 1000);
      camera.position.z = 15;
    
      scene1 = new THREE.Scene();
      scene2 = new THREE.Scene();
    
      // Add objects
      box = new THREE.Mesh(new THREE.BoxGeometry(4, 4, 4), new THREE.MeshBasicMaterial({
        color: 0x008800
      }));
      box.rotateY(Math.PI / 6);
      box.rotateX(-Math.PI / 6);
      box.position.z += 1;
      scene1.add(box);
    
      sphere = new THREE.Mesh(new THREE.SphereGeometry(4, 16, 16), new THREE.MeshBasicMaterial({
        color: 0xff0000
      }));
      scene2.add(sphere);
    }
    
    function animate() {
    
      requestAnimationFrame(animate);
    
      // Manually clear the renderer
      renderer.clear();
    
      // Sets which color components to enable or to disable when drawing or rendering to a WebGLFramebuffer
      renderer.context.colorMask(false, false, false, false); // R, G, B, A
      renderer.render(scene1, camera);
    
      // Enable back the writing into the color and alpha component
      renderer.context.colorMask(true, true, true, true);
      renderer.render(scene2, camera);
    
    }
    

    2nd solution : Stencil buffer

    I didn't tested this solution, but the jsfiddle seems nice !

    See this question : Three.js usage with stencil buffer

    And the associated jsfiddle : http://jsfiddle.net/g29k91qL/21/