使用 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.