使用 THREE.js 进行包围盒碰撞检测

本文介绍了如何使用 **Three.js 库实现包围盒和球体之间的碰撞检测**。阅读本文前,假设您已阅读过我们的 3D 碰撞检测 入门文章,并具备 Three.js 的基础知识。

使用 Box3Sphere

Three.js 拥有表示 **数学体积**和形状的对象——对于 3D AABB 和包围球,我们可以使用 **Box3** 和 **Sphere** 对象。实例化后,它们提供了可用于对其他体积进行相交测试的方法。

实例化盒子

要创建 **Box3 实例**,我们需要提供盒子的 **下界和上界**。通常,我们希望此 AABB 与 3D 世界中的对象(如角色)“关联”。在 Three.js 中,Geometry 实例具有一个 boundingBox 属性,该属性包含对象的 minmax 边界。请记住,为了定义此属性,您需要事先手动调用 Geometry.computeBoundingBox

js
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 实体的 **变换以及任何子网格**。

js
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 属性。

js
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 的位置,我们需要手动更新包围球。例如

js
knot.scale.set(2, 2, 2);
knotBSphere.radius = knot.geometry.radius * 2;

相交测试

点与 Box3 / Sphere

Box3Sphere 都有一个 **containsPoint** 方法来执行此测试。

js
const point = new THREE.Vector3(2, 4, 7);
knotBBox.containsPoint(point);

Box3Box3

可以使用 **Box3.intersectsBox** 方法执行此测试。

js
knotBbox.intersectsBox(otherBox);

注意:这与 Box3.containsBox 方法不同,后者检查 Box3 是否完全包裹另一个 Box3

SphereSphere

与之前类似,可以使用 **Sphere.intersectsSphere** 方法执行此测试。

js
knotBSphere.intersectsSphere(otherSphere);

SphereBox3

不幸的是,Three.js 中没有实现此测试,但我们可以修补 Sphere 以实现 球体与 AABB 相交 算法。

js
// 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;
};

演示

我们准备了一些 实时演示 来演示这些技术,并提供 源代码 供您参考。

A knot object, a large sphere object and a small sphere object in 3-D space. Three vectors are drawn on the small sphere. The vectors point in the directions of the three axes that define the space. Text at the bottom reads: Drag the ball around.

使用 BoxHelper

作为使用原始 Box3Sphere 对象的替代方案,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

js
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 的形状和位置。

js
const box3 = new THREE.Box3();
box3.setFromObject(knotBoxHelper);

如果我们更改 Mesh 的位置、旋转、缩放等,我们需要调用 update() 方法,以便 BoxHelper 实例与其关联的 Mesh 匹配。我们还需要再次调用 setFromObject 以使 Box3 遵循 Mesh

js
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 对象。

js
// box vs. box
box3.intersectsBox(otherBox3);
// box vs. point
box3.containsPoint(point.position);

演示

您可以在我们的 实时演示页面 上查看 **两个演示**。 第一个 演示了使用 BoxHelper 进行点与盒碰撞。 第二个 演示了盒与盒测试。

A knot object, a sphere object and a cube object in 3-D space. The knot and the sphere are encompassed by a virtual bounding box. The cube is intersecting the bounding box of the sphere. Text at the bottom reads: Drag the cube around. Press Esc to toggle B-Boxes.