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



我是three.js和stackoverflow的新手。我正在尝试对三个已经渲染的.js对象进行剪裁和封顶,这样我就可以在对象中来回移动helperPlane来查看它的内部。它内部有一个对象。我想做的与OpenGL中高级剪裁技术的描述类似:更多OpenGL游戏编程-奖金-高级剪裁平面。所以,如果这可以在OpenGL中实现,那么在WebGL中也必须有一些方法来实现?

我从threejs(webgl-clipping steel(中修改了clipping_stencil示例,只要我不移动helperPlanes,一切看起来都是正确的。当helperPlanes移动时,较大网格的一些帽面会消失,存在一些渲染瑕疵——我认为这是z冲突——帽可能不会渲染到所需的位置。

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

我也在discourse.threejs上发布了这篇文章。一切都在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。我能够用剪切和模板将一个对象覆盖在另一个对象中。我包括了拖动和动态观察控件,以及选择要沿其剖面的平面(x,y,z(的gui。我注意到,根据对象位置和相机的旋转,在渲染帽子时出现了一些奇怪的行为。

  • 我需要将对象移动到离相机更远的位置,以查看在x和y平面中进行剖切时渲染的盖帽,而不是z
  • 如果我把相机从正x旋转到负x,帽子就像滑动门一样消失了

所以我认为帽子在剪辑平面的同一个地方渲染,深度测试在某些相机点无法区分两者。我认为,当我移动相机时,沿着垂直于平面的向量将帽子从剪裁平面移开一定的公差,会使帽子以更多的角度渲染。我在我的动画功能中尝试了这个:

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

这样可以使帽子在负x方向上多渲染一个角度。我认为这个公差是从对象到相机距离的函数,但我不知道如何实现。谢谢你的帮助。

最新更新