使用 THREE.js 進行包圍盒碰撞檢測
本文將介紹如何使用 Three.js 庫來實現包圍盒和球體之間的碰撞檢測。假設您在閱讀本文之前已經閱讀了我們的3D 碰撞檢測入門文章,並對 Three.js 有基本的瞭解。
使用 Box3 和 Sphere
Three.js 提供了表示數學體和形狀的物件——對於 3D AABB 和包圍球,我們可以使用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 是否完全包含另一個。
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 來表示包圍盒的形狀,並且可以傳遞給之前看到的 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);
演示
我們可以在即時演示頁面上檢視兩個演示。第一個使用 BoxHelper 展示了點與包圍盒的碰撞。第二個執行包圍盒與包圍盒的測試。
