unity-3d-math について
unity-3d-mathは、Unityの空間処理や3D数学コード向けのリファレンススキルです。Transform、回転、ベクトル、カメラ、境界に関するよくある誤りと適切なAPIの使い方をまとめています。
主な機能
- 01座標空間変換の指針
- 02クォータニオン回転と補間のパターン
- 03方向と角度の安全な計算
- 04浮動小数点比較の指針
- 05カメラと平面交差のパターン
- 06境界と距離判定の指針
活用シーン
- 01空間間で位置・方向・ベクトルを変換
- 02無効なクォータニオンを避けて対象へ向ける
- 03ゲームロジックで距離と角度を判定
- 04画面やビューポートの座標をワールド空間へ変換
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ nice-wolf-studio/ unity-claude-skills --skill unity-3d-mathインストール後、Claude Code で/unity-3d-mathを入力して読み込まれたか確認します。
同梱ファイルreferences/spatial-math-deep-dive.md
詳細
3D 数学と空間推論 — 正確性のパターン
前提となるスキル:
unity-scripting(Vector3/Quaternion API)、unity-foundations(Transform、GameObject)、unity-physics(レイキャストの基礎)
これらのパターンは、Claude が最も陥りやすい危険な失敗、つまり一見もっともらしくコンパイルも通るものの、誤った結果を生む数学的処理を対象としています。各パターンでは、Claude がデフォルトで起こす間違いと、Unity での正しい方法を説明します。
パターン:座標空間 — TransformPoint と TransformDirection の使い分け
WHEN: ローカル空間とワールド空間を変換するとき
WRONG (Claude default):
// Using TransformDirection for a position offset
Vector3 worldPos = transform.TransformDirection(localOffset);
RIGHT:
// Positions: TransformPoint (applies position + rotation + scale)
Vector3 worldPos = transform.TransformPoint(localOffset);
// Directions: TransformDirection (applies rotation only, ignores scale)
Vector3 worldDir = transform.TransformDirection(localDir);
// Vectors: TransformVector (applies rotation + scale, no position)
Vector3 worldVec = transform.TransformVector(localVec);
GOTCHA: TransformDirection はスケールを無視します。親に非一様スケールがあり、方向にもスケールを適用する必要がある場合は、TransformVector を使用してください。逆変換には、InverseTransformPoint、InverseTransformDirection、InverseTransformVector を使用します。
パターン:Quaternion の乗算順序
WHEN: 回転を組み合わせるとき(例:ワールド回転にローカル回転を適用するとき)
WRONG (Claude default):
// "First rotate by A, then rotate by B"
transform.rotation = rotA * rotB; // Actually applies B first, then A
RIGHT:
// Quaternion multiplication applies RIGHT operand first
// "Apply B in A's space" = A * B
// Parent-then-child: parent * child
transform.rotation = worldRotation * localRotation;
// Example: rotate 45 degrees around Y, then tilt 30 degrees around local X
Quaternion yaw = Quaternion.AngleAxis(45f, Vector3.up);
Quaternion pitch = Quaternion.AngleAxis(30f, Vector3.right);
transform.rotation = yaw * pitch; // yaw is applied in world, pitch in yaw's local space
GOTCHA: これは行列の乗算順序の読み方とは逆です。「A の後に B」と考える場合は、A * B と記述します。右オペランドは左オペランドのローカル空間で適用されます。
パターン:オイラー角の補間(ジンバルロック)
WHEN: 2 つの姿勢の間を滑らかに回転させるとき
WRONG (Claude default):
// Interpolating euler angles directly
Vector3 currentEuler = Vector3.Lerp(startEuler, endEuler, t);
transform.eulerAngles = currentEuler;
RIGHT:
// Always interpolate quaternions, never euler angles
transform.rotation = Quaternion.Slerp(startRot, endRot, t);
// For small angles where performance matters, Lerp is acceptable
transform.rotation = Quaternion.Lerp(startRot, endRot, t); // Slightly faster, less accurate for large arcs
GOTCHA: オイラー角はピッチが 90 度のときにジンバルロックが発生し、不連続性もあります(例:359 度から 1 度へ移動すると、2 度ではなく 358 度分回転します)。Quaternion.Slerp は常に最短経路を通ります。Quaternion.Lerp は角度差が小さい(< 45 度)場合にのみ使用してください。
パターン:Quaternion.LookRotation にゼロベクトルを渡さない
WHEN: オブジェクトをターゲットの方向に向けるとき
WRONG (Claude default):
Vector3 dir = target.position - transform.position;
transform.rotation = Quaternion.LookRotation(dir);
RIGHT:
Vector3 dir = target.position - transform.position;
if (dir.sqrMagnitude > 0.0001f) // Guard against zero/near-zero vector
{
transform.rotation = Quaternion.LookRotation(dir, Vector3.up);
}
GOTCHA: LookRotation(Vector3.zero) は NaN のクォータニオンを生成し、Transform を気付かないうちに破損させます。forward パラメーターは正規化する必要はありません(Unity が内部で正規化します)が、必ずゼロ以外でなければなりません。また、forward が up と完全に平行な場合も失敗します。第 2 パラメーターのデフォルトは Vector3.up なので、ターゲットが真上または真下にある場合に問題が起きます。
パターン:Quaternion の各成分を直接変更しない
WHEN: 特定の回転軸をゼロにしたり変更したりするとき
WRONG (Claude default):
// "Remove the X rotation"
Quaternion rot = transform.rotation;
rot.x = 0f;
transform.rotation = rot;
RIGHT:
// Extract euler, modify, rebuild
Vector3 euler = transform.eulerAngles;
euler.x = 0f;
transform.rotation = Quaternion.Euler(euler);
// Or use Quaternion factory methods
// Keep only Y rotation:
transform.rotation = Quaternion.Euler(0f, transform.eulerAngles.y, 0f);
GOTCHA: Quaternion の x/y/z/w はオイラー角ではありません。4 次元の単位クォータニオンを構成する成分です。.x = 0 を設定すると、単位クォータニオンではなくなり、動作が未定義になります。必ずファクトリーメソッド Quaternion.Euler()、Quaternion.AngleAxis()、Quaternion.LookRotation() を使用してください。
パターン:浮動小数点数の比較
WHEN: 位置、距離、または浮動小数点値を比較するとき
WRONG (Claude default):
if (transform.position == targetPosition) { /* arrived */ }
if (distance == 0f) { /* overlapping */ }
RIGHT:
// For positions: use sqrMagnitude with epsilon
if ((transform.position - targetPosition).sqrMagnitude < 0.0001f) { /* arrived */ }
// For single floats: use Mathf.Approximately
if (Mathf.Approximately(distance, 0f)) { /* close enough */ }
// For custom tolerance:
const float epsilon = 0.01f;
if (Mathf.Abs(a - b) < epsilon) { /* within tolerance */ }
GOTCHA: Unity の Vector3 == Vector3 は内部で近似比較(イプシロンは約 1e-5)を行いますが、ゲームプレイのロジックには厳しすぎることがよくあります。ゲームに必要な精度に合わせて、明示的なしきい値を使用してください。誤差が累積した計算済みの浮動小数点値には、決して == を使用しないでください。
パターン:Vector3.Angle の値は常に正
WHEN: 回転方向(左か右か、時計回りか反時計回りか)を判定するとき
WRONG (Claude default):
float angle = Vector3.Angle(transform.forward, dirToTarget);
// angle is always 0-180, cannot tell left from right
RIGHT:
// SignedAngle returns -180 to +180 relative to the specified axis
float signedAngle = Vector3.SignedAngle(transform.forward, dirToTarget, Vector3.up);
// Positive = target is to the right, Negative = target is to the left (when axis is up)
GOTCHA: 符号は axis パラメーターによって決まります。軸に Vector3.up を指定した場合、上から見て時計回りが正になります。回転する平面に合った軸を選んでください。XY 平面を使う 2D ゲームでは、軸に Vector3.forward を使用してください。
パターン:外積の順序と座標系の左右
WHEN: 法線、垂直ベクトル、または頂点の回り順を計算するとき
WRONG (Claude default):
// Assuming right-hand rule
Vector3 normal = Vector3.Cross(edge1, edge2);
RIGHT:
// Unity uses a LEFT-handed coordinate system (Y-up, Z-forward)
// Cross product follows LEFT-hand rule:
// Cross(right, forward) = UP (not down)
Vector3 normal = Vector3.Cross(edge1, edge2);
// If normal points wrong way, swap operand order:
Vector3 flippedNormal = Vector3.Cross(edge2, edge1);
GOTCHA: Unity は左手系(Y が上、X が右、Z が前)です。OpenGL/Blender は右手系です。右手系の資料から数式を移植する場合は、外積の順序を逆にするか、いずれかの軸の符号を反転する必要があります。Unity では、三角形の頂点が時計回りの場合、正面を向いていると判定されます。
パターン:距離の比較には sqrMagnitude を使用
使用場面:パフォーマンスが重要なコード(Update ループ、多数のオブジェクト)で距離を比較する場合
誤った例(Claude のデフォルト):
// Vector3.Distance computes a square root every call
if (Vector3.Distance(a, b) < detectionRange)
{
// detected
}
正しい例:
// Compare squared distances -- avoids sqrt
float sqrRange = detectionRange * detectionRange;
if ((a - b).sqrMagnitude < sqrRange)
{
// detected
}
注意点:sqrRange はループの外でキャッシュしてください。反復ごとに range * range を再計算しないでください。1 フレームあたり N 個のオブジェクトを調べる場合、この最適化は重要です(O(N) の平方根計算が発生します)。1 回だけ確認する場合は Vector3.Distance で問題ありません。1 回限りの呼び出しを細かく最適化する必要はありません。
パターン:Camera.ScreenToWorldPoint の Z 深度
使用場面:画面上の位置(マウス、タッチ)をワールド上の位置に変換する場合
誤った例(Claude のデフォルト):
// z=0 gives a point ON the camera's near plane, not in the scene
Vector3 worldPos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
正しい例:
// Set z to the desired distance from the camera
Vector3 screenPos = Input.mousePosition;
screenPos.z = desiredDistance; // Distance from camera along its forward axis
Vector3 worldPos = Camera.main.ScreenToWorldPoint(screenPos);
注意点:入力ベクトルの z 成分は、カメラの前方向に沿ったワールド単位でのカメラからの距離です。パースペクティブカメラでは、z=0 を指定するとカメラの位置の点が返されます。オーソグラフィックカメラでは、z は x/y に影響しませんが、深度は設定します。地面にオブジェクトを配置するには、代わりに Camera.ScreenPointToRay と Physics.Raycast を使用してください。
パターン:Camera.ViewportToWorldPoint
使用場面:ビューポートの端(HUD の境界、画面の端)にオブジェクトを配置する場合
誤った例(Claude のデフォルト):
// Missing z depth -- returns a point at the camera
Vector3 topRight = Camera.main.ViewportToWorldPoint(new Vector3(1f, 1f, 0f));
正しい例:
// z = distance from camera where you want the world point
float distFromCamera = 10f;
Vector3 topRight = Camera.main.ViewportToWorldPoint(new Vector3(1f, 1f, distFromCamera));
Vector3 bottomLeft = Camera.main.ViewportToWorldPoint(new Vector3(0f, 0f, distFromCamera));
注意点:ビューポート座標は正規化されています。(0,0)は左下、(1,1)は右上です。z 値は Z ワールド座標ではなく、カメラからの距離です。距離に応じて視錐台が広がるパースペクティブカメラでは、特に重要です。
パターン:Plane.Raycast の動作
使用場面:光線が数学的な平面と交差する位置を求める場合
誤った例(Claude のデフォルト):
Plane groundPlane = new Plane(Vector3.up, 0f);
float enter;
groundPlane.Raycast(ray, out enter);
Vector3 hitPoint = ray.GetPoint(enter); // Using enter without checking return value
正しい例:
Plane groundPlane = new Plane(Vector3.up, 0f); // Normal=up, distance=0 (XZ plane at origin)
float enter;
if (groundPlane.Raycast(ray, out enter))
{
Vector3 hitPoint = ray.GetPoint(enter);
}
// If returns false, the ray points away from the plane (enter is negative)
注意点:Plane.Raycast が true を返すのは、光線が平面の表側(法線が向いている側)と交差する場合のみです。光線の始点が平面の裏側にある場合や、平面から遠ざかる方向を向いている場合は、false を返し、enter は負の値になります。Plane のコンストラクター new Plane(normal, distance) では、distance は法線に沿った原点からの符号付き距離です。new Plane(Vector3.up, 5f) は y = 5 ではなく、y = -5 の平面を作成します。y = 5 の平面には new Plane(Vector3.up, new Vector3(0, 5, 0)) を使用してください。
パターン:Bounds は常に軸平行(AABB)
使用場面:回転したオブジェクトの空間的な重なりや内包を確認する場合
誤った例(Claude のデフォルト):
// Assuming bounds rotates with the object
if (renderer.bounds.Contains(point))
{
// This is an AABB check, not an OBB check
}
正しい例:
// renderer.bounds is an AXIS-ALIGNED bounding box in WORLD space
// It expands to encompass the rotated mesh, making it larger than the actual object
Bounds aabb = renderer.bounds;
// For oriented checks, use the collider or manual OBB:
// Option 1: Use a collider (accurate to shape)
Collider col = GetComponent<Collider>();
Vector3 closest = col.ClosestPoint(point);
bool inside = (closest - point).sqrMagnitude < 0.0001f;
// Option 2: Transform point to local space for local-space AABB check
Vector3 localPoint = transform.InverseTransformPoint(point);
Bounds localBounds = meshFilter.sharedMesh.bounds; // Local-space bounds
bool containsLocal = localBounds.Contains(localPoint);
注意点:Renderer.bounds はワールド空間の AABB を返します。オブジェクトを回転させると AABB は大きくなります(回転した立方体の AABB は立方体自体より大きくなります)。Mesh.bounds はローカル空間です。Bounds.Intersects(other) を使用する場合、両方が同じ空間にある必要があります。Collider.bounds も AABB です。正確な形状チェックには Physics.ComputePenetration または Collider.ClosestPoint を使用してください。
パターン:Transform の階層におけるスケールの継承
使用場面:不均一なスケールを持つ親子の Transform を扱う場合
誤った例(Claude のデフォルト):
// Assuming scale is independent
transform.localScale = Vector3.one; // Visually one-to-one? Not if parent is scaled
正しい例:
// lossyScale gives the approximate world-space scale (read-only)
Vector3 worldScale = transform.lossyScale;
// To set a specific world-space scale on a child:
Vector3 parentScale = transform.parent.lossyScale;
transform.localScale = new Vector3(
desiredWorldScale.x / parentScale.x,
desiredWorldScale.y / parentScale.y,
desiredWorldScale.z / parentScale.z
);
注意点:階層に回転と不均一なスケール(スキュー)が含まれる場合、lossyScale は近似値にすぎません。Unity は Transform のスキューをサポートしていないため、lossyScale は見た目のスケールを正確に表さないことがあります。回転した子を持つ親には、不均一なスケールを使用しないでください。SetParent(parent, true) はワールド位置・回転・スケールを維持し、SetParent(parent, false) はローカル値を維持します。意図に合わせて選択してください。
よく使うパターンのクイックリファレンス
| 必要な処理 | メソッド | 空間 |
|---|---|---|
| ローカルからワールドへの位置変換 | TransformPoint | ローカル -> ワールド |
| ワールドからローカルへの位置変換 | InverseTransformPoint | ワールド -> ローカル |
| ローカルからワールドへの方向変換 | TransformDirection | 回転のみ |
| ワールドからローカルへの方向変換 | InverseTransformDirection | 回転のみ |
| ローカルからワールドへのベクトル変換 | TransformVector | 回転 + スケール |
| 2つのベクトル間の角度 | Vector3.Angle | 符号なし、0〜180 |
| 符号付き角度 | Vector3.SignedAngle | -180〜+180 |
| 内積 > 0 | ベクトルは同じ半球を向いています | |
| 内積 == 0 | ベクトルは垂直です | |
| 内積 < 0 | ベクトルは反対方向を向いています | |
| マウス位置からワールド空間へのレイ | Camera.ScreenPointToRay | スクリーン -> ワールド |
| マウス位置からワールド空間への点 | ScreenToWorldPoint + z 深度 | スクリーン -> ワールド |
| ビューポート端からワールド空間への点 | ViewportToWorldPoint + z 深度 | ビューポート -> ワールド |
関連スキル
- unity-scripting -- Vector3、Quaternion、Time API リファレンス
- unity-foundations -- Transform API、親子階層
- unity-physics -- レイキャスト、コライダーの空間クエリ
- unity-physics-queries -- 物理クエリの正確性に関するパターン