My project uses geometry where each face is its own mesh. I need to clip the geometry to cut away a portion of it and have a stenciled cap face cover the clipped edges. I examined and tinkered with the Three.js clipping stencil example and I understand how to use a stencil to cap trimmed solid geometry, but when I try it on collections of face geometries it doesn’t work. Here is some code I have been tinkering with, based on the example:
body { margin: 0; } canvas { display: block; }
<script type="module"> import * as THREE from 'https://unpkg.com/three@0.120.1/build/three.module.js'; import { OrbitControls } from 'https://unpkg.com/three@0.120.1/examples/jsm/controls/OrbitControls.js'; import { BufferGeometryUtils } from 'https://unpkg.com/three@0.120.1/examples/jsm/utils/BufferGeometryUtils.js'; var camera, scene, renderer; var planes, planeObjects; init(); animate(); function createPlaneStencilGroup( geometry, plane, renderOrder ) { var group = new THREE.Group(); var baseMat = new THREE.MeshBasicMaterial(); baseMat.depthWrite = false; baseMat.depthTest = false; baseMat.colorWrite = false; baseMat.stencilWrite = true; baseMat.stencilFunc = THREE.AlwaysStencilFunc; // back faces var mat0 = baseMat.clone(); mat0.side = THREE.BackSide; mat0.clippingPlanes = [ plane ]; mat0.stencilFail = THREE.IncrementWrapStencilOp; mat0.stencilZFail = THREE.IncrementWrapStencilOp; mat0.stencilZPass = THREE.IncrementWrapStencilOp; var mesh0 = new THREE.Mesh( geometry, mat0 ); mesh0.renderOrder = renderOrder; group.add( mesh0 ); // front faces var mat1 = baseMat.clone(); mat1.side = THREE.FrontSide; mat1.clippingPlanes = [ plane ]; mat1.stencilFail = THREE.DecrementWrapStencilOp; mat1.stencilZFail = THREE.DecrementWrapStencilOp; mat1.stencilZPass = THREE.DecrementWrapStencilOp; var mesh1 = new THREE.Mesh( geometry, mat1 ); mesh1.renderOrder = renderOrder; group.add( mesh1 ); return group; } function init() { scene = new THREE.Scene(); camera = new THREE.PerspectiveCamera( 36, window.innerWidth / window.innerHeight, 1, 100 ); camera.position.set( 2, 2, 2 ); initLights(); planes = [ new THREE.Plane( new THREE.Vector3( 0, - 1, 0 ), 0.42 ), new THREE.Plane( new THREE.Vector3( 0, 0, - 1 ), 0.25 ) ]; var material = new THREE.MeshStandardMaterial( { color: 0x00ff00, metalness: 0.1, roughness: 0.75, side: THREE.DoubleSide, clippingPlanes: planes, clipShadows: true, shadowSide: THREE.DoubleSide, } ); // Simple sphere geometry. Something I know works, for comparison. var sphereGeom = new THREE.SphereBufferGeometry( 0.5, 32, 32 ); sphereGeom.translate( -1.1, 0, 0 ); // Make a cube out of 6 planes and merge them together var planeGeoms = []; for(var i = 0; i < 6; i++) { planeGeoms.push( new THREE.PlaneBufferGeometry( 1, 1 ) ); } var mergedBufferGeom = BufferGeometryUtils.mergeBufferGeometries( planeGeoms ); // Set up clip plane rendering planeObjects = []; var planeGeom = new THREE.PlaneBufferGeometry( 4, 4 ); for ( var i = 0; i < 2; i ++ ) { var poGroup = new THREE.Group(); var plane = planes[ i ]; var stencilGroup_sphere = createPlaneStencilGroup( sphereGeom, plane, i + 1 ); var stencilGroup_Box = createPlaneStencilGroup( mergedBufferGeom, plane, i + 1 ) // plane is clipped by the other clipping planes var planeMat = new THREE.MeshStandardMaterial( { color: 0x0000ff, metalness: 0.1, roughness: 0.75, clippingPlanes: planes.filter( p => p !== plane ), stencilWrite: true, stencilRef: 0, stencilFunc: THREE.NotEqualStencilFunc, stencilFail: THREE.ReplaceStencilOp, stencilZFail: THREE.ReplaceStencilOp, stencilZPass: THREE.ReplaceStencilOp, } ); var po = new THREE.Mesh( planeGeom, planeMat ); po.onAfterRender = function ( renderer ) { renderer.clearStencil(); }; po.renderOrder = i + 1.1; plane.coplanarPoint( po.position ); po.lookAt( po.position.x - plane.normal.x, po.position.y - plane.normal.y, po.position.z - plane.normal.z, ); scene.add( stencilGroup_sphere ); scene.add( stencilGroup_Box ); poGroup.add( po ); planeObjects.push( po ); scene.add( poGroup ); } var sphereMesh = new THREE.Mesh( sphereGeom, material ); sphereMesh.renderOrder = 6; scene.add( sphereMesh ); var planeMeshes = []; for(var i = 0; i < 6; i++) { planeMeshes.push( new THREE.Mesh(planeGeoms[i], material) ); } planeMeshes[0].position.copy(new THREE.Vector3(.5, 0, 0)); planeMeshes[1].position.copy(new THREE.Vector3(0, .5, 0)); planeMeshes[2].position.copy(new THREE.Vector3(0, 0, .5)); planeMeshes[3].position.copy(new THREE.Vector3(-.5, 0, 0)); planeMeshes[4].position.copy(new THREE.Vector3(0, -.5, 0)); planeMeshes[5].position.copy(new THREE.Vector3(0, 0, -.5)); planeMeshes[0].lookAt(new THREE.Vector3(2, 0, 0)); planeMeshes[1].lookAt(new THREE.Vector3(0, 2, 0)); planeMeshes[2].lookAt(new THREE.Vector3(0, 0, 2)); planeMeshes[3].lookAt(new THREE.Vector3(-2, 0, 0)); planeMeshes[4].lookAt(new THREE.Vector3(0, -2, 0)); planeMeshes[5].lookAt(new THREE.Vector3(0, 0, -2)); for(var i = 0; i < 6; i++) scene.add( planeMeshes[i] ); // Renderer renderer = new THREE.WebGLRenderer( { antialias: true } ); renderer.shadowMap.enabled = true; renderer.setPixelRatio( window.devicePixelRatio ); renderer.setSize( window.innerWidth, window.innerHeight ); renderer.setClearColor( 0x263238 ); renderer.localClippingEnabled = true; window.addEventListener( 'resize', onWindowResize, false ); document.body.appendChild( renderer.domElement ); // Controls var controls = new OrbitControls( camera, renderer.domElement ); controls.minDistance = 2; controls.maxDistance = 20; controls.update(); } function initLights() { scene.add( new THREE.AmbientLight( 0xffffff, 0.5 ) ); var dirLight = new THREE.DirectionalLight( 0xffffff, 1 ); dirLight.position.set( 5, 10, 7.5 ); dirLight.castShadow = true; dirLight.shadow.camera.right = 2; dirLight.shadow.camera.left = - 2; dirLight.shadow.camera.top = 2; dirLight.shadow.camera.bottom = - 2; dirLight.shadow.mapSize.width = 1024; dirLight.shadow.mapSize.height = 1024; scene.add( dirLight ); } function onWindowResize() { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize( window.innerWidth, window.innerHeight ); } function animate() { requestAnimationFrame( animate ); renderer.render( scene, camera ); } </script>
It contains 2 clipping planes, a cube made from 6 separate PlaneGeometries, and a solid sphere for comparison. I made the stencil for the cube using an additional BufferGeometry made from merging the planes together into a single geometry object. The stencil for the cube appears to be the right shape and size, but only one cap face is drawn and it is not at the location of either of the clipping planes. Is there anything else I’m supposed to do with the stencil or the clipping plane beyond what the example already does to make it work on geometry of this type?
Advertisement
Answer
Turns out the PlaneBufferGeometries that were getting merged for the stencil were not in the same positions as the plane meshes that used those geometries. That is why the cap face wasn’t being drawn properly. I had not considered the fact that if you apply a transform to a Mesh, then get the Mesh’s geometry to use elsewhere, that geometry won’t reflect the transform applied to the Mesh. I got it to work by applying the transform matrices from the plane meshes to the PlaneBufferGeometries that needed to be merged.