参考资料:threejs中文网
threejs qq交流群:814702116
three.js八叉树Octree扩展库介绍
本章节学习八叉树Octree目的,是为了实现漫游的碰撞检测功能,比如遇到装障碍物被挡住、比如爬坡和上楼梯。
你可以打开本节课的工厂漫游案例体验测试。
本节课内容会比较多,不过大部分只是作为了解和扩展学习,你只需要你掌握下面3行代码即可。
js
// 引入八叉树扩展库
import { Octree } from 'three/examples/jsm/math/Octree.js';
const worldOctree = new Octree();
// 分割模型,生成八叉树节点
worldOctree.fromGraphNode(模型对象);
八叉树基本原理解释
下面给大家简单介绍下八叉树概念,初学者不要求记住具体细节,先有个印象就行。
通过前面基础内容2.3. 网格模型(三角形概念)的学习,大家都知道网格模型Mesh本质是由三角形构成,三角形由顶点构成,这些三角形和自己的顶点数据分布在3D空间中。
javascript
const geometry = new THREE.BoxGeometry(50,50,50);
console.log('顶点位置数据',geometry.attributes.position);
console.log('三角形顶点索引数据',geometry.index);
如果整个3D模型用一个长方体空间来表示,在三维空间xyz三个方向,都分割一次,这样就可以得到8个小的长方体子空间。
一个3D模型的三角形(顶点)分布在三维空间中,如果你用一个长方体来表示整个3d场景,当你分割为8个子空间的时候,每个子空间可以包含对应的三角形(顶点)数据。
每个子空间如果三角形(顶点)数量比较多,还可以继续分割,具体分割规则,你可以自定义,比如你可以规定,一个子空间包含的三角形数量只要大于8个就继续分割。这样一个个子空间可以构成一个树结构,整体来看,每个节点,分叉出来八个子节点。
项目引入Octree.js
Three.js在目录/examples/jsm/math/
下提供了一个八叉树相关的扩展库Octree.js
。 npm安装threejs情况下,Octree.js扩展库引入路径。
js
// 引入/examples/jsm/math/目录下八叉树扩展库
import { Octree } from 'three/examples/jsm/math/Octree.js';
在.html
文件中,你也可以配置为其它任意路径引入方式
html
<script type="importmap">
{
"imports": {
"three": "../../../three.js/build/three.module.js",
"three/addons/": "../../../three.js/examples/jsm/"
}
}
</script>
<script type="module">
import { Octree } from 'three/addons/math/Octree.js';
</script>
生成八叉树.fromGraphNode()
实例化一个八叉树对象。
js
const worldOctree = new Octree();
.fromGraphNode()
的参数是模型对象,比如一个mesh,或者多个mesh构成的层级模型。
js
const gltf = await loader.loadAsync("../地形.glb");
worldOctree.fromGraphNode(gltf.scene);
执行.fromGraphNode()
会对模型进行分割,分割为一个一个的小的长方体空间,构成一个八叉树。
执行.fromGraphNode()
会把一个3D模型,分割为8个子空间,每个子空间都包含对应的三角形或者说顶点数据,每个子空间还可以继续分割。
具体分割规则非常复杂,不要求掌握,如果你有兴趣可以阅读Octree.js
的源码,比如Octree.js
会根据三角形数量决定是否分割一个子空间,比如一个子空间包含的三角形数量小于等于8个就不在分割,当然你也可以修改规则,作为初学者,也不要求记住,先有个印象就行。
Octree.js文件中部分源码截取
js
if ( len > 8 && level < 16 ) {
subTrees[ i ].split( level + 1 );
}
浏览器控制台打印八叉树
浏览器控制台打印八叉树,查看分割的结果(不要求掌握,过一遍即可)。
js
console.log('查看八叉树结构', worldOctree);
.box
属性是包围盒Box3,描述当前分割的子空间位置和尺寸.subTrees
属性表示八叉树的子节点,类似threejs层级模型的children
属性- 查看叶子结点(最后一层没有子对象的节点)
.triangles
属性,可以看到包含的三角形数据
OctreeHelper
可视化八叉树
Three.js在目录/examples/jsm/helpers/
下提供了一个可视化八叉树相关的扩展库OctreeHelper.js
。
js
import { OctreeHelper } from 'three/examples/jsm/helpers/OctreeHelper.js';
js
//课程案例源码里面配置的路径
import { OctreeHelper } from 'three/addons/helpers/OctreeHelper.js';
js
const helper = new OctreeHelper( worldOctree );
scene.add( helper );
八叉树与胶囊Capsule交叉计算
Capsule
表示胶囊形状的几何体,具体说就是上面一个半球、中间一个圆柱、下面一个半球拼接构成的胶囊形状几何体。
下面给大家讲解胶囊形状几何体apsule与八叉树表示的3D模型进行交叉计算,你可以类比以前的射线交叉计算,虽然不同,都是都是交叉相关的计算。
项目引入胶囊碰撞体Capsule.js
Three.js在目录/examples/jsm/math/
下提供了一个胶囊形状的几何体Capsule.js
。
npm安装threejs情况下,Capsule.js扩展库引入路径。
js
// 引入/examples/jsm/math/目录下胶囊扩展库Capsule.js
import { Capsule } from 'three/examples/jsm/math/Capsule.js';
本课程案例源码.html
里面自定义了新的路径。
js
import { Capsule } from 'three/addons/math/Capsule.js';
创建胶囊几何体
创建胶囊几何体:让胶囊底部半球与y=0的平面刚好相切即可。
js
const R = 0.4;//胶囊半径
const H = 1.7;//胶囊总高度
const start = new THREE.Vector3(0, R, 0);//底部半球球心坐标
const end = new THREE.Vector3(0, H - R, 0);//顶部半球球心坐标
const capsule = new Capsule(start, end, R);
console.log('capsule', capsule);
练习:Mesh可视化上面胶囊几何体
js
// 可视化胶囊几何体
const capsuleHelper = CapsuleHelper(R, H);
model.add(capsuleHelper);
js
function CapsuleHelper(R, H) {
const group = new THREE.Group();
const material = new THREE.MeshLambertMaterial({
color: 0x00ffff,
transparent: true,
opacity: 0.5,
});
// 底部半球
const geometry = new THREE.SphereGeometry(R, 25, 25, 0, 2 * Math.PI, 0, Math.PI / 2);
geometry.rotateX(Math.PI);
const mesh = new THREE.Mesh(geometry, material);
mesh.position.y = R;
group.add(mesh)
// 顶部半球
const geometry2 = new THREE.SphereGeometry(R, 25, 25, 0, 2 * Math.PI, 0, Math.PI / 2);
const mesh2 = new THREE.Mesh(geometry2, material);
mesh2.position.set(0, H - R, 0)
group.add(mesh2)
// 中间圆柱
const h = H - 2 * R
const geometry3 = new THREE.CylinderGeometry(R, R, h,32,1,true);
geometry3.translate(0, h / 2+R,0)
const mesh3 = new THREE.Mesh(geometry3, material);
group.add(mesh3)
return group;
}
平移胶囊几何体
平移胶囊碰撞体,使底部半球位于y=0的平面以下。
js
capsule.translate(new THREE.Vector3(0, -R, 0));
可视化胶囊的模型对象同步平移
js
capsuleHelper.position.y += -R;
换一种平移方式
js
capsuleHelper.position.copy(capsule.start)
capsuleHelper.position.y -= R;
交叉计算
Octree.capsuleIntersect(capsule)
可以计算Octree表示的3D模型与胶囊几何体capsule是否重合交叉,如果有重合交叉,返回交叉相关的信息,具体说就是在某个方向上交叉重合的深度是多少。
js
// 碰撞检测:几何体交叉计算
// Octree表示的3D模型和Capsule交叉计算
const result = worldOctree.capsuleIntersect(capsule);
console.log('碰撞检测结果', result);
.depth
交叉重合的深度.normal
深度对应的方向
先与八叉树里面的包围盒子节点进行交叉计算,在与相交叉的包围盒包含的三角形进行交叉计算。借助八叉树,相比较,for循环所有模型所有三角形分别进行交叉计算,更节约时间。
胶囊放在斜面上
平移胶囊放在斜面上,查看交叉重合计算结果。
js
capsule.translate(new THREE.Vector3(0, 0, -3*H));
capsuleHelper.position.z += -3 * H;
你可以看到交叉方向不再是垂直于平面
.depth
交叉重合的深度.normal
深度对应的方向
根据交叉碰撞数据,平移碰撞体
根据交叉碰撞数据,平移碰撞体,让胶囊碰撞体不在于八叉树对应模型重合。
.depth
交叉重合的深度.normal
深度对应的方向
总结:.normal
数据的特点就是让胶囊沿着.normal
方向,平移.depth
距离,就能刚好确保交叉重合深度为0
js
// 根据碰撞结果平移胶囊碰撞体,使交叉重合深度为0
capsule.translate(result.normal.multiplyScalar(result.depth));
capsuleHelper.position.copy(capsule.start);
capsuleHelper.position.y -= R;
胶囊与楼梯交叉
js
// 根据碰撞结果平移胶囊碰撞体,使交叉重合深度为0
capsule.translate(result.normal.multiplyScalar(result.depth));
capsuleHelper.position.copy(capsule.start);
capsuleHelper.position.y -= R;
平移后,不在于楼梯交叉,上升偏离地面了(如果运动起来,这个特点可以让胶囊产生上楼梯的效果,下节课会讲解)
胶囊向上平移,与地面没有接触,查看交叉计算结果
胶囊向上平移,与地面没有接触
js
// 胶囊向上平移,与地面不交叉情况,查看计算结果
capsule.translate(new THREE.Vector3(0, R, 0));
capsuleHelper.position.y += R;
交叉计算结果返回值false
js
const result = worldOctree.capsuleIntersect(capsule);
console.log('碰撞检测结果', result);