three.jsshaderocclusion

Three.js - Color based on occlusion


I would like to have a mesh that is always drawn to the scene (depthTest = false, renderOrder = 999) but I want the occluded part of the mesh (the area covered by another object in front of it in the scene) to render in a different color (diagram for reference, purple shaded area is what I want to render in a different color) Reference

I have tried looking for shader solutions, and the answer seems to lie in a manual depth test but I cannot get a working implementation. I have resorted to checking per-vertex occlusion, and I have provided a demo at https://z3db0y.com/occlusion/ which uses a THREE.Raycaster and is not the most efficient way. Additionally, since the vertices make up triangles, you cannot really change the color of a custom area using them.


Solution

  • you can achieve this using 2 steps of rendering like:

        renderer.autoClear = true;
        specialMesh.material = transparentPixels;
        specialMesh.layers.set(0)
        camera.layers.set(0)
        renderer.render( scene, camera );
    
        renderer.autoClear = false;
        screenTexture.canvasChanged()
        specialMesh.material = specialMaterial
        specialMesh.layers.set(1)
        camera.layers.set(1)
        renderer.render( scene, camera );
    
    1. turn autoClear on (default value)
    2. set material of specialMesh to transparentPixels (material will set gl_FragColor to vec4(0.0, 0.0, 0.0, 0.0) for any position on object)
    3. set specialMesh and camera renderLayer to 0 (default value)
    4. render all objects and your specialMesh (transparentPixels.dephTest = true)
    5. turn autoClear off to draw only specialMesh on top
    6. read rendered frame pixels as texture (texture is already set as value for ShaderMaterial (specialMaterial) uniform value so need just update it, .canvasChanged() is custom function added to THREE.DataTexture object)
    7. switch specialMesh material to specialMaterial which will check if something is overlaping your mesh (if something is overlaping it then pixels of rendered frame under object wont be equal to vec4(0.0, 0.0, 0.0, 0.0))
    8. set specialMesh and camera renderLayer to 1 to render only object on top of everything
    9. render only specialMesh (specialMaterial.dephTest = false)

    and here is result: result also as you can notice it not only changes color when overlapped it can use color of object which is overlaping it and in this case it multiplies that object`s color vector on 0.5

    also full code here:

    Promise.all([
      import('three').then(m=>{window.THREE = m}),
      import('three/addons/controls/OrbitControls.js').then(m=>{window.OrbitControls = m.OrbitControls}),
    ]).then(()=>{
    
      function rgbaTextureFromCanvas(c) {
        const texture = new THREE.DataTexture(new Uint8Array(c.width*c.height*4), c.width, c.height, THREE.RGBAFormat)
        const gl = c.getContext('webgl2')
        texture.canvasChanged = function() {
          gl.readPixels(0, 0, c.width, c.height, gl.RGBA, gl.UNSIGNED_BYTE, this.source.data.data)
          // this.flipY = false;
          this.needsUpdate = true;
        }
        texture.canvasChanged()
        return texture
      }
    
      const camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 0.01, 1000 );
      camera.position.set(0,0,6.5)
    
      const renderer = new THREE.WebGLRenderer();
      renderer.setSize( window.innerWidth, window.innerHeight );
      const canvas = document.body.appendChild( renderer.domElement )
    
      const controls = new OrbitControls( camera, renderer.domElement );
    
      const scene = new THREE.Scene();
    
      const screenTexture = rgbaTextureFromCanvas(renderer.domElement)
      const specialMaterial = new THREE.ShaderMaterial({
        uniforms: {
          u_screenTexture: {value: screenTexture}
        },
        vertexShader:`
          void main() {
            gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
          }`,
        fragmentShader:`
          uniform sampler2D u_screenTexture;
          // uniform vec2 u_resolution;
    
          void main() {
    
            ivec2 iTextureSize = textureSize(u_screenTexture,0);
            vec2 fTextureSize = vec2( float(iTextureSize.x) , float(iTextureSize.y) );
    
            vec4 color = texture2D( u_screenTexture, gl_FragCoord.xy/fTextureSize );
    
            if ( all(equal(color,vec4(0.0))) ) {
              // not overlapped
              color = vec4(1.0, 0.0, 1.0, 1.0);
            } else {
              // overlapped by some other object
              color = vec4(color.rgb*0.5, 1.0);
              // color = vec4(1.0, 1.0, 0.0, 1.0);
            }
    
            gl_FragColor = color;
          }`
      });
      specialMaterial.depthTest = false
    
      const transparentPixels = new THREE.ShaderMaterial({
        uniforms: {},
        vertexShader:`
          void main() {
            gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
          }`,
        fragmentShader:`
          void main() {
            gl_FragColor = vec4(0.0);
          }`
      });
    
      const specialMesh = new THREE.Mesh(
         new THREE.BoxGeometry(1, 1, 1)
        ,transparentPixels
      )
    
      const mesh1 = new THREE.Mesh(
         new THREE.BoxGeometry(1, 1, 1)
        ,new THREE.MeshBasicMaterial({color: 0xff_ff_00})
      )
      mesh1.position.set(-1,0,2)
    
      const mesh2 = new THREE.Mesh(
         new THREE.BoxGeometry(1, 1, 1)
        ,new THREE.MeshBasicMaterial({color: 0x00_00_ff})
      )
      mesh2.position.set( 1,0,2)
    
      const mesh3 = new THREE.Mesh(
         new THREE.BoxGeometry(1, 1, 1)
        ,new THREE.MeshBasicMaterial({color: 0x00_ff_00})
      )
      mesh3.position.set(0,-1,2)
    
      const mesh4 = new THREE.Mesh(
         new THREE.BoxGeometry(1, 1, 1)
        ,new THREE.MeshBasicMaterial({color: 0x00_ff_ff})
      )
      mesh4.position.set(0, 1,2)
    
    
      for (mesh of [specialMesh,mesh1,mesh2,mesh3,mesh4]) {
        if ( !(mesh.geometry.boundingBox instanceof THREE.Box3) ) {
          mesh.geometry.computeBoundingBox()
        }
        mesh.add( new THREE.Box3Helper( mesh.geometry.boundingBox, 0x20_20_20 ) )
      }
    
      scene.add(
         specialMesh
        ,mesh1
        ,mesh2
        ,mesh3
        ,mesh4
        ,new THREE.AxesHelper( 5 )
      )
    
      function animate() {
        requestAnimationFrame( animate );
        // controls.update();
    
        renderer.autoClear = true;
        specialMesh.material = transparentPixels;
        specialMesh.layers.set(0)
        camera.layers.set(0)
        renderer.render( scene, camera );
    
        renderer.autoClear = false;
        screenTexture.canvasChanged()
        specialMesh.material = specialMaterial
        specialMesh.layers.set(1)
        camera.layers.set(1)
        renderer.render( scene, camera );
      };
      animate();
      
    })
    body { 
                margin: 0;
            }
    <script type="importmap">
        {
            "imports": {
                "three": "https://cdn.jsdelivr.net/npm/three@v0.170.0/build/three.module.js",
                "three/addons/": "https://cdn.jsdelivr.net/npm/three@v0.170.0/examples/jsm/"
            }
        }
        </script>