本篇将要介绍的是在 three.js 中如何使用二元操作来自由组合物体。为此,我们需要引入一个 three.js 的扩展 ThreeBSP.js 库,你可以从网上找到这个库,譬如从 http://download.youkuaiyun.com/download/zhulx_sz/10202730 中下载,这个资源里面提供的这个 ThreeBSP.js 库,已经针对 three.js r8x 进行了小小的修复,包括一些警告信息都消除了,建议从这里下载。当然,你可以从官方下载,但必须自己来编译,这样才能引入到你的工程中使用,网址为 https://github.com/skalnik/threebsp。
所谓的二元操作是指,将各种标准的几何体(如 THREE.BoxGeometry、THREE.SphereGeometry 等)组合在一起,并通过 subtract(相减)、intersect(相交)、union(联合)等运算操作后创建出一个新的几何体。下面我们给出这几种运算操作的大致定义:
操作 | 描述 |
---|---|
intersect(相交) | 此运算操作可以在两个几何体的交集上创建出新的几何体。两个几何体相互交叠的部分就是新的几何体 |
subtract(相减) | 此运算操作可以在第一个几何体中减去两个几何体交叠的部分,剩下的部分就是新的几何体 |
union(联合) | 此运算操作可以将两个几何体联合在一起,从而创建出新的几何体 |
<!DOCTYPE html>
<html>
<head>
<title>示例 06.08 - Binary Operations Geometry</title>
<script src="../build/three.js"></script>
<script src="../build/js/controls/OrbitControls.js"></script>
<script src="../build/js/libs/stats.min.js"></script>
<script src="../build/js/libs/dat.gui.min.js"></script>
<script src="../jquery/jquery-3.2.1.min.js"></script>
<script src="../build/js/ThreeBSP.js"></script>
<script src="../build/js/libs/spin.js"></script>
<style>
body {
/* 设置 margin 为 0,并且 overflow 为 hidden,来完成页面样式 */
margin: 0;
overflow: hidden;
}
/* 统计对象的样式 */
#Stats-output {
position: absolute;
left: 0px;
top: 0px;
}
</style>
</head>
<body>
<!-- 用于 WebGL 输出的 Div -->
<div id="webgl-output"></div>
<!-- 用于统计 FPS 输出的 Div -->
<div id="stats-output"></div>
<!-- 运行 Three.js 示例的 Javascript 代码 -->
<script type="text/javascript">
var scene;
var camera;
var render;
var webglRender;
//var canvasRender;
var controls;
var stats;
var guiParams;
var ground;
var sphere1;
var sphere2;
var cube;
var result;
var spinner;
var ambientLight;
var spotLight;
var axesHelper;
//var cameraHelper;
$(function() {
stats = initStats();
scene = new THREE.Scene();
webglRender = new THREE.WebGLRenderer( {antialias: true, alpha: true} ); // antialias 抗锯齿
webglRender.setSize(window.innerWidth, window.innerHeight);
webglRender.setClearColor(0xeeeeee, 1.0);
webglRender.shadowMap.enabled = true; // 允许阴影投射
render = webglRender;
camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.01, 2147483647); // 2147483647
camera.position.set(-45.5, 68.2, 90.9);
var target = new THREE.Vector3(10, 0 , 0);
controls = new THREE.OrbitControls(camera, render.domElement);
controls.target = target;
camera.lookAt(target);
$('#webgl-output')[0].appendChild(render.domElement);
window.addEventListener('resize', onWindowResize, false);
// 加入一个坐标轴:X(橙色)、Y(绿色)、Z(蓝色)
axesHelper = new THREE.AxesHelper(60);
scene.add(axesHelper);
ambientLight = new THREE.AmbientLight(0x0c0c0c);
scene.add(ambientLight);
spotLight = new THREE.SpotLight(0xffffff);
spotLight.position.set(0, 260, 230);
spotLight.shadow.mapSize.width = 5120; // 必须是 2的幂,默认值为 512
spotLight.shadow.mapSize.height = 5120; // 必须是 2的幂,默认值为 512
spotLight.castShadow = true;
scene.add(spotLight);
//cameraHelper = new THREE.CameraHelper(spotLight.shadow.camera);
//scene.add(cameraHelper);
// sphere1
sphere1 = createMesh(new THREE.SphereGeometry(5, 20, 30));
sphere1.position.x = -2;
// sphere1
sphere2 = createMesh(new THREE.SphereGeometry(5, 20, 30));
sphere2.position.set(3, 0, 0);
// cube
cube = createMesh(new THREE.BoxGeometry(5, 5, 5));
cube.position.x = -7;
// 加入到场景中
scene.add(sphere1);
scene.add(sphere2);
scene.add(cube);
/** 用来保存那些需要修改的变量 */
guiParams = new function() {
this.rotationSpeed = 0.02;
//this.actionSphere = 'none';
//this.actionCube = 'none';
this.showResult = function() {
redrawResult();
};
this.rotateResult = false;
this.hideWireframes = false;
this.sphere1 = {
posx: 3,
posy: 0,
posz: 0,
scale: 1
}
this.sphere2 = {
posx: 3,
posy: 0,
posz: 0,
scale: 1,
actionSphere: 'subtract'
}
this.cube = {
posx: 3,
posy: 0,
posz: 0,
scalex: 1,
scaley: 1,
scalez: 1,
actionCube: 'none'
}
}
/** 定义 dat.GUI 对象,并绑定 guiParams 的几个属性 */
var gui = new dat.GUI();
var folder = gui.addFolder('sphere1');
//folder.open();
folder.add(guiParams.sphere1, "posx", -15, 15, 0.1).onChange(function(e){
sphere1.position.set(guiParams.sphere1.posx, guiParams.sphere1.posy, guiParams.sphere1.posz);
});
folder.add(guiParams.sphere1, "posy", -15, 15, 0.1).onChange(function(e){
sphere1.position.set(guiParams.sphere1.posx, guiParams.sphere1.posy, guiParams.sphere1.posz);
});
folder.add(guiParams.sphere1, "posz", -15, 15, 0.1).onChange(function(e){
sphere1.position.set(guiParams.sphere1.posx, guiParams.sphere1.posy, guiParams.sphere1.posz);
});
folder.add(guiParams.sphere1, "scale", -15, 15, 0.1).onChange(function(e){
sphere1.scale.set(guiParams.sphere1.scale, guiParams.sphere1.scale, guiParams.sphere1.scale);
});
folder = gui.addFolder('sphere2');
folder.open();
folder.add(guiParams.sphere2, "posx", -15, 15, 0.1).onChange(function(e){
sphere2.position.set(guiParams.sphere2.posx, guiParams.sphere2.posy, guiParams.sphere2.posz);
});
folder.add(guiParams.sphere2, "posy", -15, 15, 0.1).onChange(function(e){
sphere2.position.set(guiParams.sphere2.posx, guiParams.sphere2.posy, guiParams.sphere2.posz);
});
folder.add(guiParams.sphere2, "posz", -15, 15, 0.1).onChange(function(e){
sphere2.position.set(guiParams.sphere2.posx, guiParams.sphere2.posy, guiParams.sphere2.posz);
});
folder.add(guiParams.sphere2, "scale", -15, 15, 0.1).onChange(function(e){
sphere2.scale.set(guiParams.sphere2.scale, guiParams.sphere2.scale, guiParams.sphere2.scale);
});
folder.add(guiParams.sphere2, "actionSphere", ['subtract', 'intersect', 'union', 'none']);
folder = gui.addFolder('cube');
folder.open();
folder.add(guiParams.cube, "posx", -15, 15, 0.1).onChange(function(e){
cube.position.set(guiParams.cube.posx, guiParams.cube.posy, guiParams.cube.posz);
});
folder.add(guiParams.cube, "posy", -15, 15, 0.1).onChange(function(e){
cube.position.set(guiParams.cube.posx, guiParams.cube.posy, guiParams.cube.posz);
});
folder.add(guiParams.cube, "posz", -15, 15, 0.1).onChange(function(e){
cube.position.set(guiParams.cube.posx, guiParams.cube.posy, guiParams.cube.posz);
});
folder.add(guiParams.cube, "scalex", -15, 15, 0.1).onChange(function(e){
cube.scale.set(guiParams.cube.scalex, guiParams.cube.scaley, guiParams.cube.scalez);
});
folder.add(guiParams.cube, "scaley", -15, 15, 0.1).onChange(function(e){
cube.scale.set(guiParams.cube.scalex, guiParams.cube.scaley, guiParams.cube.scalez);
});
folder.add(guiParams.cube, "scalez", -15, 15, 0.1).onChange(function(e){
cube.scale.set(guiParams.cube.scalex, guiParams.cube.scaley, guiParams.cube.scalez);
});
folder.add(guiParams.cube, "actionCube", ['subtract', 'intersect', 'union', 'none']);
gui.add(guiParams, "showResult");
gui.add(guiParams, "rotateResult");
gui.add(guiParams, "hideWireframes").onChange(function(e){
if (e) {
sphere1.visible = false;
sphere2.visible = false;
cube.visible = false;
} else {
sphere1.visible = true;
sphere2.visible = true;
cube.visible = true;
}
});
renderScene();
});
/** 渲染场景 */
function renderScene() {
stats.update();
rotateMesh(); // 旋转物体
requestAnimationFrame(renderScene);
render.render(scene, camera);
}
/** 初始化 stats 统计对象 */
function initStats() {
stats = new Stats();
stats.setMode(0); // 0 为监测 FPS;1 为监测渲染时间
$('#stats-output').append(stats.domElement);
return stats;
}
/** 当浏览器窗口大小变化时触发 */
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
render.setSize(window.innerWidth, window.innerHeight);
}
/** 旋转物体 */
var step = 0;
function rotateMesh() {
step += guiParams.rotationSpeed;
scene.traverse(function(mesh) {
if (mesh === result && guiParams.rotateResult) {
//mesh.rotation.x = step;
mesh.rotation.y = step;
//mesh.rotation.z = step;
}
});
}
function createMesh(geom) {
var wireFrameMat = new THREE.MeshBasicMaterial({
//transparency: true,
opacity: 0.5,
wireframe: true,
wireframeLinewidth: 0.5
});
return new THREE.Mesh(geom, wireFrameMat);
}
function redrawResult() {
showSpinner();
setTimeout(function() {
scene.remove(result);
var sphere1BSP = new ThreeBSP(sphere1);
var sphere2BSP = new ThreeBSP(sphere2);
var cubeBSP = new ThreeBSP(cube);
var resultBSP;
// 第一步:球体
switch(guiParams.sphere2.actionSphere) {
case 'subtract':
resultBSP = sphere1BSP.subtract(sphere2BSP);
break;
case 'intersect':
resultBSP = sphere1BSP.intersect(sphere2BSP);
break;
case 'union':
resultBSP = sphere1BSP.union(sphere2BSP);
break;
case 'none':
// 无操作
break;
}
// 第二步:方块
if (!resultBSP) resultBSP = sphere1BSP;
switch(guiParams.cube.actionCube) {
case 'subtract':
resultBSP = resultBSP.subtract(cubeBSP);
break;
case 'intersect':
resultBSP = resultBSP.intersect(cubeBSP);
break;
case 'union':
resultBSP = resultBSP.union(cubeBSP);
break;
case 'none':
// noop
break;
}
if (guiParams.sphere2.actionSphere === 'none' && guiParams.cube.actionCube === 'none') {
// 无操作
} else {
result = resultBSP.toMesh();
result.geometry.computeFaceNormals();
result.geometry.computeVertexNormals();
scene.add(result);
}
hideSpinner(spinner);
}, 100);
}
/** 显示等待画面 */
function showSpinner() {
var opts = {
lines: 13, // The number of lines to draw
length: 20, // The length of each line
width: 10, // The line thickness
radius: 30, // The radius of the inner circle
corners: 1, // Corner roundness (0..1)
rotate: 0, // The rotation offset
direction: 1, // 1: clockwise, -1: counterclockwise
color: '#000', // #rgb or #rrggbb or array of colors
speed: 1, // Rounds per second
trail: 60, // Afterglow percentage
shadow: false, // Whether to render a shadow
hwaccel: false, // Whether to use hardware acceleration
className: 'spinner', // The CSS class to assign to the spinner
zIndex: 2e9, // The z-index (defaults to 2000000000)
top: 'auto', // Top position relative to parent in px
left: 'auto' // Left position relative to parent in px
};
var target = $('#webgl-output')[0];
//var target = document.getElementById('webgl-output');
spinner = new Spinner(opts).spin(target);
return spinner;
}
/** 隐藏等待画面 */
function hideSpinner(spinner) {
spinner.stop();
}
</script>
</body>
</html>
在这个示例中,我们首先添加了三个物体:一个立方体和两个球体。初始场景里中间那个 sphere1 球体,所有操作都会在这个对象上进行。它的右边是 sphere2 球体,左边是 cube 立方体。读者可以在 sphere2 及 cube 上指定四种操作中的一种,即:subtract、intersect、union 和 none(无操作)。这些操作都是基于 sphere1 的。
其中的 showSpinner() 函数主要作用是在耗时的二元操作期间显示一个等待画面,以便向用户提示操作正在进行中。用到的一个 js 扩展库可以从这里下载 http://download.youkuaiyun.com/download/zhulx_sz/10202976
其中最核心的部分是 showResult() 函数,在这个函数中,我们首先将各种待运算操作的网格对象包装成 ThreeBSP 对象,只有这样才能进行 subtract、intersect 和 union 操作。随后我们在操作结果对象 resultBSP 上调用 toMesh() 函数,并通过调用 computeFaceNormals() 和 computeVertexNormals() 函数以确保所有的法向量可以正确的计算出来。之所以要调用这两个函数,是因为在执行二元操作后,几何体中顶点和面的法向量可能会被改变,而 three.js 在着色时会用到面法向量和顶点法向量。所以显式调用这两个函数可以保证新生成的对象着色光滑、渲染正确,并能加入到场景 scene 中。
特别地,这种 union 操作的方法并不是很好的,我们后面将会给出一种更好的 three.js 的合并方法。因为此处这种方法,你在旋转时会发现,中心还是在原来 sphere1 那个球上,而不在 union 后的新几何体的中心。其实另外两种操作也存在类似的问题。
未完待续···