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

本文将介绍如何使用 Three.js 库来实现包围盒和球体之间的碰撞检测。假设您在阅读本文之前已经阅读了我们的3D 碰撞检测入门文章,并对 Three.js 有基本的了解。

使用 Box3Sphere

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

实例化包围盒

要创建 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 是否完全包含另一个。

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 来表示包围盒的形状,并且可以传递给之前看到的 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.