一、
由于项目需要,我进行了模型剖切功能方面的研究,如果单纯实现模型剖切效果,我相信你会从这篇文章中得到收获。
二、
我对模型剖切功能的研究主要基于three.js中的webgl_clipping_stencil.html进行,该示例中被剖切的网格模型是由Three.js自身创建的几何体和材质构成,当我把导入的obj模型添加进去之后,始终没有剖切成功,更别说绘制剖切面了,这花费了我很长时间,还查过着色器相关的知识,后来随着对Three.js学习的深入,我认识到了我的错误。。。
实际上,最终的代码十分简单,导入的obj模型 和Three.js自身创建的网格模型本质上是一样的,所以上只要用obj模型合理的替代原本的网格模型,便可以实现剖切及剖切面的绘制,不过这样并没有实际上学习到模型剖切的原理,可以在研究场景渲染效果时加深对着色器的认识,再回过头分析剖切的原理。
三、
首先对源代码中的一些内容进行介绍:
1.创建三个剖切平面,gui用于调节剖切平面的位置
planes = [
new THREE.Plane( new THREE.Vector3( - 1, 0, 0 ), 0 ),
new THREE.Plane( new THREE.Vector3( 0, - 1, 0 ), 0 ),
new THREE.Plane( new THREE.Vector3( 0, 0, - 1 ), 0 )
];
2.创建 组object,将网格模型 clippedColorFront 和 剖切面stencilGroup 添加到object中,实际上分为两部分内容:将模型位于剖切一侧的部分剪切掉,添加到 组object中,此时只是实现剖切,但是没有剖切面;调用creatPlaneStencilGroup函数,设置几何体、平面、渲染顺序,生成剖切面的网格模型,同样添加到 组object中。
var camera, scene, renderer, object, stats;
//初始化组
object = new THREE.Group();
scene.add( object );
//添加网格模型
var clippedColorFront = new THREE.Mesh( geometry, material );
object.add( clippedColorFront );
//绘制剖切面
var stencilGroup = createPlaneStencilGroup( geometry, plane, i + 1 );
object.add( stencilGroup );
四、
接下来对代码进行修改,实现obj模型的剖切
1.在加载obj模型的函数中修改模型的材质,设置属性clippingPlanes,其参数对应先前创建的平面,实现模型的剪切。
obj.traverse(function(child){
if (child instanceof THREE.Mesh) {
child.material = new THREE.MeshStandardMaterial({
color: child.material.color,
clippingPlanes: planes,
clipShadows: true,
shadowSide: THREE.DoubleSide,
});
child.castShadow = true;
child.renderOrder = 6;
}
})
2.由于我使用的模型尺寸很大,设置模型缩放比例为0.001,同时为确保模型位于场景中心,模型位置为(x,y,z)。剖切面的绘制如代码所示,object为创建的组,object.children[0]代表加载的obj模型,其子对象为网格模型,具有几何体和材质。这里还需要设置geometry.translate和stencilGroup.scale,从而确保剖切面的比例和位置正确。
for (var w = 0; w < obj.children.length; w++) {
var geometry0 = object.children[0].children[w].geometry.clone();
geometry0.translate(x/0.001, y/0.001, z/0.001);
var stencilGroup = createPlaneStencilGroup(geometry0, plane, 1);
stencilGroup.scale.set(0.001,0.001,0.001)
object.add(stencilGroup);
}
3.还有一点需要注意,只有位于planeGeom范围内的模型才会被成功剖切,所以要设定合适的参数。
var planeGeom = new THREE.PlaneBufferGeometry( 4000, 4000 );
五、
下面为obj模型剖切的完整代码,为避免代码过长,仅定义了一个剖切平面。
<!DOCTYPE html>
<html lang="en">
<head>
<title>three.js webgl - clipping stencil</title>
<meta charset="utf-8">
</head>
<style>
body{
overflow:hidden;
background-color:#263238;
}
</style>
<body>
<script type="module">
import * as THREE from '../build/three.module.js';
import { OrbitControls } from './jsm/controls/OrbitControls.js';
import { GUI } from './jsm/libs/dat.gui.module.js';
import Stats from './jsm/libs/stats.module.js';
import { OBJLoader } from './jsm/loaders/OBJLoader.js';
import { MTLLoader } from './jsm/loaders/MTLLoader.js';
var camera, scene, renderer, object, stats;
var planes, planeObjects=[], planeHelpers;
var params = {
planeY: {
constant: 0,
negated: false,
displayHelper: false
},
};
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( 0, 0, 20 );
let ambientLight = new THREE.AmbientLight(0xcccccc,0.6);
scene.add(ambientLight);
var dirLight = new THREE.DirectionalLight( 0xffffff, 1 );
scene.add( dirLight );
planes = [
new THREE.Plane( new THREE.Vector3( 0, - 1, 0 ), 0 ),
];
planeHelpers = planes.map( p => new THREE.PlaneHelper( p, 10, 0xffffff ) );
planeHelpers.forEach( ph => {
ph.visible = false;
scene.add( ph );
} );
object = new THREE.Group();
scene.add( object );
var obj_loader = new OBJLoader();
var mtl_loader=new MTLLoader();
mtl_loader.load( 'models/obj/blue0.mtl', function (materials ) {
materials.preload();
obj_loader.setMaterials(materials);
obj_loader.load( 'models/obj/blue0.obj', function (obj ) {
obj.scale.set(0.001,0.001,0.001);
let bbox = new THREE.Box3().setFromObject(obj);
var x=-(bbox.max.x+bbox.min.x)/2;
var y=-(bbox.max.y+bbox.min.y)/2;
var z= -(bbox.max.z+bbox.min.z)/2;
obj.position.set(-(bbox.max.x+bbox.min.x)/2,
-(bbox.max.y+bbox.min.y)/2,
-(bbox.max.z+bbox.min.z)/2);
obj.traverse(function(child){
if (child instanceof THREE.Mesh) {
child.material = new THREE.MeshStandardMaterial({
color: child.material.color,
clippingPlanes: planes,
clipShadows: true,
shadowSide: THREE.DoubleSide,
});
child.castShadow = true;
child.renderOrder = 6;
}
})
console.log(obj)
object.add(obj);
var planeGeom = new THREE.PlaneBufferGeometry( 4000, 4000 );
var poGroup = new THREE.Group();
var plane = planes[ 0 ];
for (var w = 0; w < obj.children.length; w++) {
var geometry0 = object.children[0].children[w].geometry.clone();
geometry0.translate(x/0.001, y/0.001, z/0.001);
var stencilGroup = createPlaneStencilGroup(geometry0, plane, 1);
stencilGroup.scale.set(0.001,0.001,0.001)
object.add(stencilGroup);
}
var planeMat =
new THREE.MeshBasicMaterial( {
color: 0xffff00,
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 = 1.1;
poGroup.add( po );
planeObjects.push( po );
scene.add( poGroup );
} );
})
var ground = new THREE.Mesh(
new THREE.PlaneBufferGeometry( 9, 9, 1, 1 ),
new THREE.ShadowMaterial( { color: 0, 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 = new Stats();
document.body.appendChild( stats.dom );
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, false );
document.body.appendChild( renderer.domElement );
renderer.localClippingEnabled = true;
var controls = new OrbitControls( camera, renderer.domElement );
controls.update();
var gui = new GUI();
var planeY = gui.addFolder( 'planeY' );
planeY.add( params.planeY, 'displayHelper' ).onChange( v => planeHelpers[ 1 ].visible = v );
planeY.add( params.planeY, 'constant' ).min( - 6 ).max( 6 ).onChange( d => planes[ 0 ].constant = d );
planeY.add( params.planeY, 'negated' ).onChange( () => {
planes[ 0 ].negate();
params.planeY.constant = planes[ 0 ].constant;
} );
planeY.open();
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize( window.innerWidth, window.innerHeight );
}
function animate() {
requestAnimationFrame( animate );
for ( var i = 0; i < planeObjects.length; i ++ ) {
var plane = planes[ i ];
var po = planeObjects[ i ];
plane.coplanarPoint( po.position );
po.lookAt(
po.position.x - plane.normal.x,
po.position.y - plane.normal.y,
po.position.z - plane.normal.z,
);
}
stats.begin();
renderer.render( scene, camera );
stats.end();
}
</script>
</body>
</html>