THREE.js によるバウンディングボリューム衝突検出
この記事では、Three.js ライブラリーを使用してバウンディングボックスとバウンディングスフィアの間の衝突検出を実装する方法を示します。 これを読む前に、まず三次元の衝突検出の紹介記事を読み、かつ Three.js に関する基本的な知識があることを前提としています。
Box3
と Sphere
の使用
ボックスのインスタンス化
Box3
インスタンスを作成するには、ボックスの下と上の境界(対角線の 2 つの座標)を指定する必要があります。 通常、この AABB を 3D ワールドのオブジェクト(文字など)に「リンク」する必要があります。 Three.js では、Geometry
インスタンスにオブジェクトの min
と max
の境界を持つ boundingBox
プロパティがあります。 このプロパティを定義するには、事前に 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
プロパティは、Mesh
ではなく、Geometry
自体を参照として使用します。 したがって、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
が別のものを 完全に 包んでいるかどうかをチェックする Box3.containsBox
メソッドとは異なります。
Sphere
対 Sphere
前のものと同様の方法で、このテストを実行するための Sphere.intersectsSphere
メソッドがあります。
knotBSphere.intersectsSphere(otherSphere);
Sphere
対 Box3
残念ながら、このテストは Three.js には実装されていませんが、Sphere
にパッチを適用して球対 AABB の交差アルゴリズムを実装できます。
// THREE.js の Sphere を展開して、対 Box3 衝突テストをサポートします。
// チェックのたびに Vector3 の新しいインスタンスが生成されないように、
// メソッドスコープ外でベクターを作成しています。
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
が作成されます。 これは、バウンディングボックスの形状を示し、Mesh
と一致するバウンディングボックスを作成するために、前に見た setFromObject
メソッドに渡すことができます。
BoxHelper
は、Three.js のバウンディングボリュームとの 3D 衝突を処理するための推奨される方法です。 球のテストをもらすことになりますが、このトレードオフにはそれだけの価値があります。
このヘルパーを使用する利点は次のとおりです。
- リンクした
Mesh
が回転したり、その寸法を変更した場合に、バウンディングボックスのMesh
のサイズを変更し、その位置を更新するupdate()
メソッドがあります。 - バウンディングボックスのサイズを計算するときに子メッシュが考慮されるため、元のメッシュとそのすべての子が包まれます。
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
と一致するようにする必要があります。 Box3
を Mesh
に従わせるには、setFromObject
を再度呼び出す必要もあります。
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);