3D 碰撞检测

本文介绍了在 3D 环境中实现碰撞检测所使用的不同包围体技术。后续文章将介绍在特定 3D 库中的实现。

轴对齐包围盒

与 2D 碰撞检测一样,**轴对齐包围盒** (AABB) 是确定两个游戏实体是否重叠的最快算法。这包括将游戏实体包裹在未旋转(因此轴对齐)的盒子中,并检查这些盒子在 3D 坐标空间中的位置,以查看它们是否重叠。

Two 3-D non-square objects floating in space encompassed by virtual rectangular boxes.

**轴对齐约束** 出于性能原因而存在。两个未旋转的盒子之间的重叠区域仅可以通过逻辑比较来检查,而旋转的盒子则需要额外的三角函数运算,这些运算计算速度较慢。如果您有将要旋转的实体,您可以修改包围盒的尺寸以使其仍然包裹对象,或者选择使用其他包围几何类型,例如球体(它们对旋转不变)。下面的动画 GIF 显示了一个 AABB 的图形示例,它调整其大小以适应旋转的实体。该盒子不断改变尺寸以紧密地贴合包含在其中的实体。

Animated rotating knot showing the virtual rectangular box shrink and grow as the knots rotates within it. The box does not rotate.

**注意:**查看使用 Three.js 的包围体文章以了解此技术的实际实现。

点与 AABB

检查一个点是否在 AABB 内部非常简单——我们只需要检查点的坐标是否落在 AABB 内部;分别考虑每个轴。如果我们假设PxPyPz是点的坐标,并且BminXBmaxXBminYBmaxYBminZBmaxZ是 AABB 每个轴的范围,我们可以使用以下公式计算这两个之间是否发生碰撞

f ( P , B ) = ( P x B m i n X P x B m a x X ) ( P y B m i n Y P y B m a x Y ) ( P z B m i n Z P z B m a x Z ) f(P, B)= (P_x \ge B_{minX} \wedge P_x \le B_{maxX}) \wedge (P_y \ge B_{minY} \wedge P_y \le B_{maxY}) \wedge (P_z \ge B_{minZ} \wedge P_z \le B_{maxZ})

或者在 JavaScript 中

js
function isPointInsideAABB(point, box) {
  return (
    point.x >= box.minX &&
    point.x <= box.maxX &&
    point.y >= box.minY &&
    point.y <= box.maxY &&
    point.z >= box.minZ &&
    point.z <= box.maxZ
  );
}

AABB 与 AABB

检查一个 AABB 是否与另一个 AABB 相交类似于点测试。我们只需要对每个轴进行一次测试,使用盒子的边界。下图显示了我们在 X 轴上执行的测试——基本上,范围AminXAmaxXBminXBmaxX是否重叠?

Hand drawing of two rectangles showing the upper right corner of A overlapping the bottom left corner of B, as A's largest x coordinate is greater than B's smallest x coordinate.

从数学上讲,这将如下所示

f ( A , B ) = ( A m i n X B m a x X A m a x X B m i n X ) ( A m i n Y B m a x Y A m a x Y B m i n Y ) ( A m i n Z B m a x Z A m a x Z B m i n Z ) f(A, B) = (A_{minX} \le B_{maxX} \wedge A_{maxX} \ge B_{minX}) \wedge ( A_{minY} \le B_{maxY} \wedge A_{maxY} \ge B_{minY}) \wedge (A_{minZ} \le B_{maxZ} \wedge A_{maxZ} \ge B_{minZ})

在 JavaScript 中,我们将使用此代码

js
function intersect(a, b) {
  return (
    a.minX <= b.maxX &&
    a.maxX >= b.minX &&
    a.minY <= b.maxY &&
    a.maxY >= b.minY &&
    a.minZ <= b.maxZ &&
    a.maxZ >= b.minZ
  );
}

包围球

使用包围球检测碰撞比 AABB 稍微复杂一些,但仍然相当快速。球体的主要优点是它们对旋转不变,因此如果包裹的实体旋转,包围球仍然保持不变。它们的主要缺点是,除非它们包裹的实体实际上是球形的,否则包裹通常不是一个很好的匹配(例如,用包围球包裹一个人会导致很多误报,而 AABB 将是更好的匹配)。

点与球体

要检查球体是否包含一个点,我们需要计算点与球体中心的距离。如果此距离小于或等于球体的半径,则该点位于球体内部。

Hand drawing of a 2D projection of a sphere and a point in a Cartesian coordinate system. The point is outside of the circle, to the lower right of it. The distance is denoted by a dashed line, labeled D, from the circle's center to the point. A lighter line shows the radius, labeled R, going from the center of the circle to the border of the circle.

考虑到两点AB之间的欧几里得距离为 ( A x B x ) 2 + ( A y B y ) 2 + ( A z B z ) 2 \sqrt{(A_x - B_x)^2 + (A_y - B_y)^2 + (A_z - B_z)^2} ,我们点与球体碰撞检测的公式将如下所示

f ( P , S ) = S r a d i u s ( P x S x ) 2 + ( P y S y ) 2 + ( P z S z ) 2 f(P,S) = S_{radius} \ge \sqrt{(P_x - S_x)^2 + (P_y - S_y)^2 + (P_z - S_z)^2}

或者在 JavaScript 中

js
function isPointInsideSphere(point, sphere) {
  // we are using multiplications because is faster than calling Math.pow
  const distance = Math.sqrt(
    (point.x - sphere.x) * (point.x - sphere.x) +
      (point.y - sphere.y) * (point.y - sphere.y) +
      (point.z - sphere.z) * (point.z - sphere.z),
  );
  return distance < sphere.radius;
}

**注意:**上面的代码包含一个平方根,计算起来可能很昂贵。避免它的一个简单的优化方法是将平方距离与平方半径进行比较,因此优化的方程将改为包含distanceSqr < sphere.radius * sphere.radius

球体与球体

球体与球体测试类似于点与球体测试。我们需要在这里测试的是球体中心的距离是否小于或等于其半径之和。

Hand drawing of two partially overlapping circles. Each circle (of different sizes) has a light radius line going from its center to its border, labeled R. The distance is denoted by a dotted line, labeled D, connecting the center points of both circles.

从数学上讲,这看起来像

f ( A , B ) = ( A x B x ) 2 + ( A y B y ) 2 + ( A z B z ) 2 A r a d i u s + B r a d i u s f(A,B) = \sqrt{(A_x - B_x)^2 + (A_y - B_y)^2 + (A_z - B_z)^2} \le A_{radius} + B_{radius}

或者在 JavaScript 中

js
function intersect(sphere, other) {
  // we are using multiplications because it's faster than calling Math.pow
  const distance = Math.sqrt(
    (sphere.x - other.x) * (sphere.x - other.x) +
      (sphere.y - other.y) * (sphere.y - other.y) +
      (sphere.z - other.z) * (sphere.z - other.z),
  );
  return distance < sphere.radius + other.radius;
}

球体与 AABB

测试球体和 AABB 是否发生碰撞稍微复杂一些,但仍然简单快捷。一种逻辑方法是检查 AABB 的每个顶点,对每个顶点执行点与球体测试。但是,这有点过头了——测试所有顶点是不必要的,因为我们可以只计算 AABB 的最近点(不一定是顶点)与球体中心的距离,看看它是否小于或等于球体的半径。我们可以通过将球体中心钳位到 AABB 的限制来获得此值。

Hand drawing of a square partially overlapping the top of a circle. The radius is denoted by a light line labeled R. The distance line goes from the circle's center to the closest point of the square.

在 JavaScript 中,我们将执行以下测试

js
function intersect(sphere, box) {
  // get box closest point to sphere center by clamping
  const x = Math.max(box.minX, Math.min(sphere.x, box.maxX));
  const y = Math.max(box.minY, Math.min(sphere.y, box.maxY));
  const z = Math.max(box.minZ, Math.min(sphere.z, box.maxZ));

  // this is the same as isPointInsideSphere
  const distance = Math.sqrt(
    (x - sphere.x) * (x - sphere.x) +
      (y - sphere.y) * (y - sphere.y) +
      (z - sphere.z) * (z - sphere.z),
  );

  return distance < sphere.radius;
}

使用物理引擎

**3D 物理引擎**提供碰撞检测算法,其中大多数也基于包围体。物理引擎的工作方式是创建**物理体**,通常附加到其视觉表示。该物体具有速度、位置、旋转、扭矩等属性,以及**物理形状**。此形状是在碰撞检测计算中考虑的形状。

我们准备了一个实时碰撞检测演示(带源代码),您可以查看它以了解此类技术的实际应用——它使用了开源 3D 物理引擎cannon.js

另请参阅

MDN 上的相关文章

外部资源