math について
mathは、Three.jsの3D数学タスクに関するガイダンススキルです。組み込みクラスの選び方に加え、座標系、回転、ビルボード、アニメーションモデルの境界測定を説明します。
主な機能
- 01球面座標のヘルパー
- 02EulerとQuaternionの使い分け
- 03ワールド空間でのビルボード配置
- 04境界ボックス測定の注意点
- 05スキンメッシュのサイズ測定法
活用シーン
- 01カメラの軌道位置の計算
- 02回転表現の選択
- 03ラベルをカメラに向ける
- 04アニメーションキャラクターの測定
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ drawcall-ai/ skills --skill mathインストール後、Claude Code で/mathを入力して読み込まれたか確認します。
詳細
カスタム実装よりも、Three.js の数学クラス(Vector3、Euler、Quaternion、Matrix4)を優先してください。十分にテストされており、エッジケースにも対応し、Three.js オブジェクトとシームレスに連携できます。Box3 のような測定用プリミティブや、Spherical のような座標ヘルパーも同様です。重要なのは、ワールド空間とローカル空間という基準座標系です。これを意識しないと、組み込み機能でさえ誤解を招くことがあります。
このドキュメントでは、4 つの領域を扱います。球面座標と三角関数では、手動の sin/cos を置き換えます。回転では、Euler と Quaternion の使い分けを説明します。ビルボードでは、ワールド空間で向きを設定する必要があるカメラ向きの四角形を扱います。**オブジェクトのサイズの測定(Box3)**では、スキン付き/アニメーション付きのキャラクターを setFromObject で測定すると値が不正になる理由と、代わりに何をすべきかを説明します。
球面座標と三角関数
球面座標や sin/cos を使う処理には、手作業の三角関数計算ではなく、Euler、Vector3、Quaternion を使用してください。軌道上の位置を求める場合は、Spherical を作成して、その値をベクトルに読み込みます。累積した角度から方向を求める場合は、Euler を作成して前方ベクトルに適用します。
// Instead of manual sin/cos for orbit position:
const spherical = new Spherical(radius, polarAngle, azimuthalAngle)
position.setFromSpherical(spherical)
// For direction from angles:
const euler = new Euler(pitch, yaw, 0, 'YXZ')
const direction = new Vector3(0, 0, -1).applyEuler(euler)
回転
回転は慎重に扱う必要があり、用途に応じて表現を選びます。ピッチとヨーを別々に累積する軌道カメラや FPS カメラには、'YXZ' の順序で Euler を使用してください。一般的な回転、補間(slerp)、ジンバルロックの回避には Quaternion を使用してください。表現を変換する際は、Euler の順序を考慮してください。単一の表現を使い続けると、バグを減らせます。Three.js オブジェクトには .rotation(Euler)と .quaternion(Quaternion)の両方があり、自動的に同期されます。
ビルボード(カメラを向くラベル、ヘルスバー、アイコン)
ビルボードの四角形は、ワールド空間で向きを設定する必要があります。回転するオブジェクト(敵や砲塔など)の子にするとき、カメラの向きをそのオブジェクトのローカル回転にコピーするのは誤りです。親オブジェクトのワールド回転がさらに適用されるため、四角形が親と一緒に傾いたり回転したりします。このローカル回転の誤りは、ヘルスバーの向きが不正になる一般的な原因です。
修正方法 A:親オブジェクトのワールド回転を補正しながら、ワールド空間で向きを設定します。修正方法 B(より簡単):ラベルを移動するオブジェクトの子にせず、ワールド空間に置いたまま、位置を直接設定します。
// A — orient in world space, compensating for the parent's world rotation:
mesh.quaternion.copy(camera.quaternion) // desired WORLD rotation
const parentWorld = mesh.parent.getWorldQuaternion(new Quaternion())
mesh.quaternion.premultiply(parentWorld.invert()) // local = parentWorld⁻¹ · desiredWorld
// B (simpler) — don't parent it to the moving object; keep it in world space:
scene.add(label)
label.position.copy(targetWorldPosition).add(offset)
label.quaternion.copy(camera.quaternion)
THREE.Sprite は常に自動的にカメラを向くため、この問題を完全に回避できます。シンプルなヘルスバーやアイコンには、こちらを優先してください。
オブジェクトのサイズを測定する(Box3)— スキニング/リグ付きメッシュに注意
new Box3().setFromObject(object) は静的メッシュには正しい方法です。しかし、キャラクターなどのスキニング済み、リグ付き、またはアニメーション中のオブジェクトには不適切で、結果が安定しません。理由は2つあります。まず、可視の本体から大きく離れた位置にあるスケルトンのボーンや、キャラクターシステムが保持している参照用またはクローンのサブオブジェクトも含め、すべての子孫をまとめて計測します。次に、バインドポーズのジオメトリまで範囲に含めるため、ボーンのポーズが変わるたびにボックスが変化します。
その結果、可視の本体の高さの2~3倍(あるいはそれ以上)になることがよくあり、その値を基準にサイズを決めるものがすべて、気付かないうちに壊れます。典型的な失敗例は、計測した高さを合わせるためにキャラクターをスケーリングした結果、巨大化するケースです。計測したキャラクターは動作中で(値が膨らんで)いた一方、もう一方は読み込み直後で(正しく)計測されていたためです。
正しい方法は次の3つです。
SkinnedMeshのgeometry.boundingBoxを計測します。これは安定したバインドポーズのメッシュ範囲です(mesh.geometry.computeBoundingBox()を実行し、mesh.geometry.boundingBoxを読み取ります)。注意点として、このボックスはローカルジオメトリ空間のものです。ワールド単位で高さを求めるには、メッシュのワールド行列で変換します。mesh.geometry.boundingBox.clone().applyMatrix4(mesh.matrixWorld)とすると、親ノードのスケールも反映されます。通常、glTF のインポートスケールはここに含まれています。値をそのまま読み取ったり、mesh.scaleだけを掛けたりすると、スケーリングされたキャラクターの計測を誤ります。- リグ付け、アニメーション、ラップを行う前の読み込み時に、元のモデルを一度だけ計測します。
- 計測自体を避け、決めておいた固定値(目標とするキャラクターの高さ)を基準にします。
動作中のキャラクターに毎フレーム setFromObject を実行して、その高さを信頼してはいけません。