如何在 Threejs 中使用外部和内部网格对象渲染带有剪裁平面和模板的帽子

How to render caps with clipping planes and stencil in Threejs with outer and inner mesh objects

我是 three.js 和 Whosebug 的新手。我正在尝试对已渲染的 three.js 个对象进行剪辑和加盖,以便我可以在对象中来回移动 helperPlane 以查看其内部。里面有一个物体。我要做的类似于此处对 OpenGL 中高级裁剪技术的描述:More OpenGL Game Programming - Bonus - Advanced Clip Planes。那么,如果这可以在 OpenGL 中完成,那么一定有某种方法可以在 WebGL 中完成吗?

我改编了 threejs ( webgl - clipping stencil ) 中的 clipping_stencil 示例,只要我不移动 helperPlanes,一切看起来都是正确的。当 helperPlanes 移动时,较大网格的一些帽面消失,有一些渲染伪影 - 我认为这是 z-fighting - 并且帽可能没有在所需位置渲染。

为网格设置 renderingOrder 属性 是让内部网格在场景中渲染的大技巧,但我不知道如何处理 z-fighting?当我移动滑块上的剪裁平面时。

我也在 discourse.threejs. Everything is on a JSFiddle 上发布了这个。任何帮助将不胜感激。

import * as THREE from 'three';
        import Stats from 'https://threejs.org/examples/jsm/libs/stats.module.js';
        import {GUI} from 'https://threejs.org/examples/jsm/libs/lil-gui.module.min.js';
        import { OrbitControls } from 'https://threejs.org/examples/jsm/controls/OrbitControls.js';

        let camera, scene, renderer, object, object2, stats;
        let planes, planeObjects, planeObjects2, planeHelpers;
        let clock;
        
        const params = {

            animate: false,
            planeX: {

                constant: 1,
                negated: false,
                displayHelper: false

            },
            planeY: {

                constant: 1,
                negated: false,
                displayHelper: false

            },
            planeZ: {

                constant: 0,
                negated: false,
                displayHelper: false

            }


        };
        
        init();
        animate();
        
        function createPlaneStencilGroup( geometry, plane, renderOrder ) {

            const group = new THREE.Group();
            const baseMat = new THREE.MeshBasicMaterial();
            baseMat.depthWrite = false;
            baseMat.depthTest = false;
            baseMat.colorWrite = false;
            baseMat.stencilWrite = true;
            baseMat.stencilFunc = THREE.AlwaysStencilFunc;

            /* Subtract the mask created from the front-facing image 
            from the mask created from the back-facing image, we get 
            a new mask that represents the area where the clip edge 
            would be. Set the stencil buffer operation to increment 
            when rederering back-facing polygons and decrement on 
            front-facing polygons. This results in the desired mask 
            stored in the stencil buffer : http://glbook.gamedev.net/GLBOOK/glbook.gamedev.net/moglgp/advclip.html */

            // back faces
            const mat0 = baseMat.clone();
            mat0.side = THREE.BackSide;
            mat0.clippingPlanes = [ plane ];
            mat0.stencilFail = THREE.IncrementWrapStencilOp;
            mat0.stencilZFail = THREE.IncrementWrapStencilOp;
            mat0.stencilZPass = THREE.IncrementWrapStencilOp;
            
            //mat0.depthFunc = THREE.LessDepth;  // See reference above

            const mesh0 = new THREE.Mesh( geometry, mat0 );
            mesh0.renderOrder = renderOrder;
            group.add( mesh0 );

            // front faces
            const mat1 = baseMat.clone();
            mat1.side = THREE.FrontSide;
            mat1.clippingPlanes = [ plane ];
            mat1.stencilFail = THREE.DecrementWrapStencilOp;
            mat1.stencilZFail = THREE.DecrementWrapStencilOp;
            mat1.stencilZPass = THREE.DecrementWrapStencilOp;
            
            //mat1.depthFunc = THREE.LessDepth;

            const mesh1 = new THREE.Mesh( geometry, mat1 );
            mesh1.renderOrder = renderOrder;

            group.add( mesh1 );

            return group;

        }
        
        function init(){
            //clock
            clock = new THREE.Clock();
        
            // scene
            scene = new THREE.Scene();
        
            // camera
            camera = new THREE.PerspectiveCamera(36, window.innerWidth/window.innerHeight, 1,100);
            camera.position.set(2,2,2);
            
            // Lights
            
            scene.add(new THREE.AmbientLight(0xffffff, 0.5));
            
            const 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);
            
            //Clipping planes
            planes = [
                new THREE.Plane( new THREE.Vector3( - 1, 0, 0 ), 1 ),
                new THREE.Plane( new THREE.Vector3( 0, - 1, 0 ), 1 ),
                new THREE.Plane( new THREE.Vector3( 0, 0, - 1 ), 0 )
            ];
            
            planeHelpers = planes.map( p => new THREE.PlaneHelper( p, 2, 0xffffff ) );
            planeHelpers.forEach( ph => {

                ph.visible = false;
                scene.add( ph );

            } );

            //Inner Cube        
            const geometry = new THREE.BoxGeometry( 0.5,0.5,0.5 );

            
            //Outer Cube    
            const geometry2 = new THREE.BoxGeometry( 1,1,1 );
            
            object = new THREE.Group();
            scene.add(object);
            
            //Set up clip plane rendering
            
            /*
            See https://discourse.threejs.org/t/capping-two-clipped-geometries-using-two-planes-which-are-negated-to-each-other/32643
            
            Object 1
            Render order 1: Draw front face / back face clipped and front face 
                            / back face not clipped (4 meshes)
            Render order 2: Draw planar clip cap
            
            Object 2
            Render order 3: Draw front face / back face clipped and front face 
                            / back face not clipped (4 meshes)
            Render order 4: Draw planar clip cap
            */
        
            planeObjects = [];
            planeObjects2 = [];
            const planeGeom = new THREE.PlaneGeometry( 4, 4 );

            for ( let i = 0; i < 3; i ++ ) {

                const poGroup = new THREE.Group();
                const poGroup2 = new THREE.Group()
                
                const plane = planes[ i ];
                
                // Object 1
                const stencilGroup = createPlaneStencilGroup( geometry, 
                plane, i + 4 ); // Render after first group
                
                // Object 2
                const stencilGroup2 = createPlaneStencilGroup( geometry2, 
                plane, i + 1 ); // Render this first
                
                // PLANAR CLIP CAP
                // plane is clipped by the other clipping planes
                const planeMat =
                    new THREE.MeshStandardMaterial( {

                        color: 0xfff000, // inner torus colour
                        metalness: 0.1,
                        roughness: 0.75,
                        clippingPlanes: planes.filter( p => p !== plane ),
                        
                        //depthFunc: THREE.LessDepth,
                        
                        stencilWrite: true,
                        stencilRef: 0,
                        stencilFunc: THREE.NotEqualStencilFunc,
                        stencilFail: THREE.ReplaceStencilOp,
                        stencilZFail: THREE.ReplaceStencilOp,
                        stencilZPass: THREE.ReplaceStencilOp,

                    } );
                
                const planeMat2 =
                    new THREE.MeshStandardMaterial( {

                        color: 0xff0000, // inner torus colour
                        metalness: 0.1,
                        roughness: 0.75,
                        clippingPlanes: planes.filter( p => p !== plane ),

                        //depthFunc: THREE.LessDepth,

                        stencilWrite: true,
                        stencilRef: 0,
                        stencilFunc: THREE.NotEqualStencilFunc,
                        stencilFail: THREE.ReplaceStencilOp,
                        stencilZFail: THREE.ReplaceStencilOp,
                        stencilZPass: THREE.ReplaceStencilOp,

                    } );
                
                const po = new THREE.Mesh( planeGeom, planeMat );
                const po2 = new THREE.Mesh( planeGeom, planeMat2 );
                
                
                po.onAfterRender = function ( renderer ) {

                    renderer.clearStencil();

                };
                
                po2.onAfterRender = function ( renderer ) {

                    renderer.clearStencil();

                };

                // Draw Planar Clip Cap
                po.renderOrder = i + 4.1; // Render last (slightly)
                po2.renderOrder = i + 1.1; // Render slightly after first group

                object.add( stencilGroup );
                object.add( stencilGroup2 );
                
                
                poGroup.add( po );
                poGroup2.add( po2 );
                
                
                planeObjects.push( po );
                planeObjects2.push( po2 );
                
                
                scene.add( poGroup );
                scene.add( poGroup2 );

            }
            
            // Object 1
            const material = new THREE.MeshStandardMaterial( {

                color: 0xfff000,  // outer torus colour
                metalness: 0.1,
                roughness: 0.75,
                clippingPlanes: planes,
                clipShadows: true,
                shadowSide: THREE.DoubleSide,

            } );
            
            // add the color
            const clippedColorFront = new THREE.Mesh( geometry, material );
            clippedColorFront.castShadow = true;
            clippedColorFront.renderOrder = 6;
            object.add( clippedColorFront );

            // Object 2
            const material2 = new THREE.MeshStandardMaterial( {

                color: 0xff0000,  // outer colour
                metalness: 0.1,
                roughness: 0.75,
                side: THREE.DoubleSide,
                clippingPlanes: planes,
                clipShadows: true,
                shadowSide: THREE.DoubleSide,

            } );
            
            // add the color
            const clippedColorFront2 = new THREE.Mesh( geometry2, material2 );
            clippedColorFront2.castShadow = true;
            clippedColorFront2.renderOrder = 3;
            object.add( clippedColorFront2 );
            
            //Ground
            const ground = new THREE.Mesh(
                new THREE.PlaneGeometry(9,9,1,1),
                new THREE.MeshPhongMaterial({color:0x999999, opacity:0.25, side:THREE.DoubleSide})
            );
            
            ground.rotation.x = - Math.PI/2; // rotates x/y to x/z
            ground.position.y = -1;
            ground.receiveShadow = true;
            scene.add(ground);

            //Stats
            stats = new Stats();
            document.body.appendChild(stats.dom);

            //Renderer
            renderer = new THREE.WebGLRenderer({antialias:true});
            renderer.shadowMap.enabled = true;
            renderer.setPixelRatio(window.devicePixelRatio);
            renderer.setSize(window.innerWidth, window.innerHeight);
            renderer.setClearColor( 0x263238 );
            window.addEventListener('resize',onWindowResize);
            document.body.appendChild(renderer.domElement);
            
            renderer.localClippingEnabled = true;
            
            const controls = new OrbitControls(camera, renderer.domElement);
            controls.minDistance = 2;
            controls.maxDistance = 20;
            controls.update();
            
            //GUI
            const gui = new GUI();
            gui.add(params, 'animate');
            
            const planeX = gui.addFolder( 'planeX' );
            planeX.add( params.planeX, 'displayHelper' ).onChange( v => planeHelpers[ 0 ].visible = v );
            planeX.add( params.planeX, 'constant' ).min( - 1 ).max( 1 ).onChange( d => planes[ 0 ].constant = d );
            planeX.add( params.planeX, 'negated' ).onChange( () => {

                planes[ 0 ].negate();
                params.planeX.constant = planes[ 0 ].constant;

            } );
            planeX.open();

            const planeY = gui.addFolder( 'planeY' );
            planeY.add( params.planeY, 'displayHelper' ).onChange( v => planeHelpers[ 1 ].visible = v );
            planeY.add( params.planeY, 'constant' ).min( - 1 ).max( 1 ).onChange( d => planes[ 1 ].constant = d );
            planeY.add( params.planeY, 'negated' ).onChange( () => {

                planes[ 1 ].negate();
                params.planeY.constant = planes[ 1 ].constant;

            } );
            planeY.open();

            const planeZ = gui.addFolder( 'planeZ' );
            planeZ.add( params.planeZ, 'displayHelper' ).onChange( v => planeHelpers[ 2 ].visible = v );
            planeZ.add( params.planeZ, 'constant' ).min( - 1 ).max( 1 ).onChange( d => planes[ 2 ].constant = d );
            planeZ.add( params.planeZ, 'negated' ).onChange( () => {

                planes[ 2 ].negate();
                params.planeZ.constant = planes[ 2 ].constant;

            } );
            planeZ.open();

        
        }
        
        function onWindowResize() {

            camera.aspect = window.innerWidth / window.innerHeight;
            camera.updateProjectionMatrix();

            renderer.setSize( window.innerWidth, window.innerHeight );

        }
        
        
        function animate() {

            const delta = clock.getDelta();

            requestAnimationFrame( animate );

            if ( params.animate ) {

                object.rotation.x += delta * 0.5;
                object.rotation.y += delta * 0.2;

            }

            for ( let i = 0; i < planeObjects.length; i ++ ) {

                const plane = planes[ i ];
                
                // Planar clip cap for object 1
                const po = planeObjects[ i ];
                plane.coplanarPoint( po.position );
                
                // planar clip cap for object 2
                const po2 = planeObjects[ i ];
                plane.coplanarPoint( po2.position );
                
                // planar clip cap for object 1
                po.lookAt(
                    po.position.x - plane.normal.x,
                    po.position.y - plane.normal.y,
                    po.position.z - plane.normal.z,
                );
                
                // planar clip cap for object 2
                po2.lookAt(
                    po2.position.x - plane.normal.x,
                    po2.position.y - plane.normal.y,
                    po2.position.z - plane.normal.z,
                );

            }
            
            stats.begin();
            renderer.render( scene, camera );
            stats.end();

        }
        
        

我开始做的事情取得了一些成功。这是更新后的 JSFiddle。我能够使用剪裁和模板实现将一个对象覆盖在另一个对象内。我将拖动和轨道控制以及 gui 添加到 select 平面 (x,y,z) 以进行剖切。我注意到在根据对象位置和相机旋转渲染盖子时出现一些奇怪的行为。

  • 我需要将对象移到离相机更远的地方才能看到在 x 和 y 平面而不是 z 平面中切片时渲染的端盖
  • 如果我将相机从正 x 旋转到负 x,盖子似乎像滑动门一样消失了

所以我认为上限与裁剪平面在同一位置渲染,深度测试无法在某些相机点区分两者。我认为,当我移动相机时,沿着垂直于平面的矢量以一定公差将封口从裁剪平面移开将使封口以更多角度渲染。我在我的动画函数中试过这个:

    innerCap.translateOnAxis(clipPlane.normal, -1.5);

这使得瓶盖在负 x 方向上呈现多一点的角度。我认为这个公差是物体到相机距离的函数,但我不确定如何实现它。感谢您的帮助。