THREE.js によるバウンディングボリューム衝突検出

この記事では、Three.js ライブラリーを使用してバウンディングボックスとバウンディングスフィアの間の衝突検出を実装する方法を示します。 これを読む前に、まず三次元の衝突検出の紹介記事を読み、かつ Three.js に関する基本的な知識があることを前提としています。

Box3Sphere の使用

Three.js には、数学的なボリューム(mathematical volumes)と形状を表すオブジェクトがあります。 3D の AABB とバウンディングスフィアには、Box3 オブジェクトと Sphere オブジェクトを使用できます。 インスタンス化されると、他のボリュームに対して交差テストを実行するために使用できるメソッドがあります。

ボックスのインスタンス化

Box3 インスタンスを作成するには、ボックスの下と上の境界(対角線の 2 つの座標)を指定する必要があります。 通常、この AABB を 3D ワールドのオブジェクト(文字など)に「リンク」する必要があります。 Three.js では、Geometry インスタンスにオブジェクトの minmax の境界を持つ boundingBox プロパティがあります。 このプロパティを定義するには、事前に 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 プロパティは、Mesh ではなく、Geometry 自体を参照として使用します。 したがって、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 が別のものを 完全に 包んでいるかどうかをチェックする Box3.containsBox メソッドとは異なります。

SphereSphere

前のものと同様の方法で、このテストを実行するための Sphere.intersectsSphere メソッドがあります。

js
knotBSphere.intersectsSphere(otherSphere);

SphereBox3

残念ながら、このテストは Three.js には実装されていませんが、Sphere にパッチを適用して球対 AABB の交差アルゴリズムを実装できます。

js
// 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 と呼ばれると仮定します。

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 と一致するようにする必要があります。 Box3Mesh に従わせるには、setFromObject を再度呼び出す必要もあります。

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);

デモ

ライブデモのページで確認できる 2 つのデモがあります。 1 つ目は、BoxHelper を使用した点対ボックスの衝突を示しています。 2 つ目は、ボックス対ボックスのテストを実行します。