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)
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.
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 );
on
(default value)specialMesh
to transparentPixels
(material will set gl_FragColor
to vec4(0.0, 0.0, 0.0, 0.0)
for any position on object)specialMesh
and camera
renderLayer to 0 (default value)transparentPixels.dephTest = true
)off
to draw only specialMesh
on topspecialMaterial
) uniform value so need just update it, .canvasChanged()
is custom function added to THREE.DataTexture
object)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)
)specialMesh
and camera
renderLayer to 1 to render only object on top of everythingspecialMesh
(specialMaterial.dephTest = false
)and here is 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>