使用 THREE.js 进行包围盒碰撞检测
本文将介绍如何使用 Three.js 库来实现包围盒和球体之间的碰撞检测。假设您在阅读本文之前已经阅读了我们的3D 碰撞检测入门文章,并对 Three.js 有基本的了解。
使用 Box3 和 Sphere
Three.js 提供了表示数学体和形状的对象——对于 3D AABB 和包围球,我们可以使用Box3 和Sphere 对象。一旦实例化,它们就有可用的方法来与其他体进行相交测试。
实例化包围盒
要创建 Box3 实例,我们需要提供包围盒的最小和最大边界。通常我们会希望这个 AABB “链接”到我们 3D 世界中的一个对象(例如角色)。在 Three.js 中,Geometry 实例有一个 boundingBox 属性,其中包含对象的 min 和 max 边界。请注意,为了使此属性生效,您需要提前手动调用 Geometry.computeBoundingBox。
const knot = new THREE.Mesh(
new THREE.TorusKnotGeometry(0.5, 0.1),
new MeshNormalMaterial({}),
);
knot.geometry.computeBoundingBox();
const knotBBox = new Box3(
knot.geometry.boundingBox.min,
knot.geometry.boundingBox.max,
);
注意: boundingBox 属性以 Geometry 本身为参考,而不是 Mesh。因此,在计算包围盒时,应用于 Mesh 的任何变换(如缩放、位置等)都将被忽略。
一个更简单的替代方法可以解决上述问题,即稍后使用 Box3.setFromObject 设置这些边界,它将计算尺寸,同时考虑 3D 实体的变换以及任何子网格。
const knot = new THREE.Mesh(
new THREE.TorusKnotGeometry(0.5, 0.1),
new MeshNormalMaterial({}),
);
const knotBBox = new Box3(new THREE.Vector3(), new THREE.Vector3());
knotBBox.setFromObject(knot);
实例化球体
实例化 Sphere 对象也很相似。我们需要提供球体的中心和半径,这些可以添加到 Geometry 中可用的 boundingSphere 属性中。
const knot = new THREE.Mesh(
new THREE.TorusKnotGeometry(0.5, 0.1),
new MeshNormalMaterial({}),
);
const knotBSphere = new Sphere(
knot.position,
knot.geometry.boundingSphere.radius,
);
不幸的是,Sphere 实例没有等同于 Box3.setFromObject 的方法。因此,如果我们对 Mesh 应用变换或更改其位置,我们需要手动更新包围球。例如
knot.scale.set(2, 2, 2);
knotBSphere.radius = knot.geometry.radius * 2;
相交测试
点与 Box3 / Sphere
Box3 和 Sphere 都有一个 containsPoint 方法来进行此测试。
const point = new THREE.Vector3(2, 4, 7);
knotBBox.containsPoint(point);
Box3 与 Box3
提供了 Box3.intersectsBox 方法来执行此测试。
knotBbox.intersectsBox(otherBox);
注意:这与 Box3.containsBox 方法不同,后者检查一个 Box3 是否完全包含另一个。
Sphere 与 Sphere
与之前类似,有一个 Sphere.intersectsSphere 方法来执行此测试。
knotBSphere.intersectsSphere(otherSphere);
Sphere 与 Box3
不幸的是,Three.js 中没有实现此测试,但我们可以通过补丁 Sphere 来实现一个球体与 AABB 的相交算法。
// expand THREE.js Sphere to support collision tests vs. Box3
// we are creating a vector outside the method scope to
// avoid spawning a new instance of Vector3 on every check
THREE.Sphere.__closest = new THREE.Vector3();
THREE.Sphere.prototype.intersectsBox = function (box) {
// get box closest point to sphere center by clamping
THREE.Sphere.__closest.set(this.center.x, this.center.y, this.center.z);
THREE.Sphere.__closest.clamp(box.min, box.max);
const distance = this.center.distanceToSquared(THREE.Sphere.__closest);
return distance < this.radius * this.radius;
};
演示
我们准备了一些实时演示来展示这些技术,并提供源代码供您查看。

使用 BoxHelper
作为使用原始 Box3 和 Sphere 对象的替代方案,Three.js 有一个有用的对象可以更轻松地处理包围盒:BoxHelper(以前是 BoundingBoxHelper,已弃用)。此辅助工具会接收一个 Mesh 并为其计算一个包围盒体(包括其子网格)。这会生成一个新的包围盒 Mesh 来表示包围盒的形状,并且可以传递给之前看到的 setFromObject 方法,以获得与 Mesh 匹配的包围盒。
BoxHelper 是在 Three.js 中处理带包围体的 3D 碰撞的推荐方法。您会错过球体测试,但权衡是值得的。
使用此辅助工具的优点是
- 它有一个
update()方法,如果链接的Mesh旋转或改变尺寸,它会调整其包围盒Mesh的大小,并更新其位置。 - 在计算包围盒大小时,它会考虑子网格,因此原始网格及其所有子网格都会被包含在内。
- 我们可以通过渲染
BoxHelper创建的Mesh来轻松调试碰撞。默认情况下,它们是使用LineBasicMaterial材料创建的(一种用于绘制线框风格几何体的 three.js 材料)。
主要缺点是它只创建包围盒体,因此如果您需要球体与 AABB 的测试,则需要创建自己的 Sphere 对象。
要使用它,我们需要创建一个新的 BoxHelper 实例,并提供几何体和可选的颜色,该颜色将用于线框材质。我们还需要将新创建的对象添加到 three.js 场景中进行渲染。我们假设我们的场景变量名为 scene。
const knot = new THREE.Mesh(
new THREE.TorusKnotGeometry(0.5, 0.1),
new THREE.MeshNormalMaterial({}),
);
const knotBoxHelper = new THREE.BoxHelper(knot, 0x00ff00);
scene.add(knotBoxHelper);
为了同时拥有我们的实际 Box3 包围盒,我们创建一个新的 Box3 对象,并使其采用 BoxHelper 的形状和位置。
const box3 = new THREE.Box3();
box3.setFromObject(knotBoxHelper);
如果我们更改 Mesh 的位置、旋转、缩放等,我们需要调用 update() 方法,以便 BoxHelper 实例与其链接的 Mesh 匹配。我们还需要再次调用 setFromObject 以使 Box3 跟随 Mesh。
knot.position.set(-3, 2, 1);
knot.rotation.x = -Math.PI / 4;
// update the bounding box so it stills wraps the knot
knotBoxHelper.update();
box3.setFromObject(knotBoxHelper);
执行碰撞测试的方式与上一节所述相同——我们以与上述相同的方式使用我们的 Box3 对象。
// box vs. box
box3.intersectsBox(otherBox3);
// box vs. point
box3.containsPoint(point.position);
演示
我们可以在实时演示页面上查看两个演示。第一个使用 BoxHelper 展示了点与包围盒的碰撞。第二个执行包围盒与包围盒的测试。
