使用 THREE.js 进行包围盒碰撞检测
本文介绍了如何使用 **Three.js 库实现包围盒和球体之间的碰撞检测**。阅读本文前,假设您已阅读过我们的 3D 碰撞检测 入门文章,并具备 Three.js 的基础知识。
使用 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
是否完全包裹另一个 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
,该 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);