关于 unity-3d-math
unity-3d-math 是面向 Unity 空间与 3D 数学代码的参考技能,整理常见错误及正确 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、GameObjects)、unity-physics(射线检测基础)
这些模式针对 Claude 最危险的失败模式:看起来合理、能够编译,但会产生错误结果的数学计算。每种模式都会说明 Claude 默认会犯的错误,以及正确的 Unity 做法。
模式:坐标空间 —— TransformPoint 与 TransformDirection
适用场景:在本地空间与世界空间之间转换
错误(Claude 的默认做法):
// Using TransformDirection for a position offset
Vector3 worldPos = transform.TransformDirection(localOffset);
正确:
// 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);
注意:TransformDirection 会忽略缩放——如果父对象有非均匀缩放,且你需要方向也受缩放影响,请使用 TransformVector。对于逆向操作,请使用 InverseTransformPoint、InverseTransformDirection、InverseTransformVector。
模式:Quaternion 乘法顺序
适用场景:组合旋转(例如,在世界旋转基础上应用本地旋转)
错误(Claude 的默认做法):
// "First rotate by A, then rotate by B"
transform.rotation = rotA * rotB; // Actually applies B first, then A
正确:
// 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
注意:这与矩阵乘法的阅读顺序相反。如果你想的是“先执行 A,再执行 B”,就写 A * B——右侧操作数会在左侧操作数的本地空间中应用。
模式:欧拉角插值(万向锁)
适用场景:在两个朝向之间平滑旋转
错误(Claude 的默认做法):
// Interpolating euler angles directly
Vector3 currentEuler = Vector3.Lerp(startEuler, endEuler, t);
transform.eulerAngles = currentEuler;
正确:
// 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
注意:欧拉角在俯仰角为 90 度时会出现万向锁,也存在不连续问题(例如,从 359 度跳到 1 度时,会经过 358 度,而不是 2 度)。Quaternion.Slerp 始终沿最短路径旋转。仅当角度差较小时(< 45 度)才使用 Quaternion.Lerp。
模式:Quaternion.LookRotation 零向量
适用场景:旋转对象,使其朝向目标方向
错误(Claude 的默认做法):
Vector3 dir = target.position - transform.position;
transform.rotation = Quaternion.LookRotation(dir);
正确:
Vector3 dir = target.position - transform.position;
if (dir.sqrMagnitude > 0.0001f) // Guard against zero/near-zero vector
{
transform.rotation = Quaternion.LookRotation(dir, Vector3.up);
}
注意:LookRotation(Vector3.zero) 会生成 NaN 四元数,悄无声息地破坏 Transform。forward 参数无需归一化(Unity 会在内部进行归一化),但必须非零。如果 forward 与 up 完全平行,也会失败——第二个参数默认为 Vector3.up,而目标正上方或正下方时会导致问题。
模式:绝不直接修改 Quaternion 分量
适用场景:尝试将某个旋转轴归零或修改其值
错误(Claude 的默认做法):
// "Remove the X rotation"
Quaternion rot = transform.rotation;
rot.x = 0f;
transform.rotation = rot;
正确:
// 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);
注意:Quaternion 的 x/y/z/w 不是欧拉角,而是 4D 单位四元数的分量。设置 .x = 0 会生成非单位四元数,导致行为未定义。始终使用工厂方法:Quaternion.Euler()、Quaternion.AngleAxis()、Quaternion.LookRotation()。
模式:浮点数比较
适用场景:比较位置、距离或任何浮点值
错误(Claude 的默认做法):
if (transform.position == targetPosition) { /* arrived */ }
if (distance == 0f) { /* overlapping */ }
正确:
// 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 */ }
注意:Unity 中的 Vector3 == Vector3 确实会在内部进行近似比较(epsilon 约为 1e-5),但对于游戏逻辑而言,这个阈值往往过于严格。请使用符合游戏精度需求的明确阈值。对于累积了误差的计算结果,绝不要使用 == 比较浮点数。
模式:Vector3.Angle 始终返回正值
适用场景:判断转向方向(左转与右转、顺时针与逆时针)
错误(Claude 的默认做法):
float angle = Vector3.Angle(transform.forward, dirToTarget);
// angle is always 0-180, cannot tell left from 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)
注意:正负号取决于 axis 参数。以 Vector3.up 为轴时,从上方观察,正值表示顺时针。请选择与旋转平面相符的轴。对于使用 XY 平面的 2D 游戏,请使用 Vector3.forward 作为轴。
模式:叉积顺序与坐标系手性
适用场景:计算法线、垂直向量或顶点绕序
错误(Claude 的默认做法):
// Assuming right-hand rule
Vector3 normal = Vector3.Cross(edge1, edge2);
正确:
// 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);
注意: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。检查每帧 N 个对象时(O(N) 次平方根运算),这项优化很重要。对于单次检查,Vector3.Distance 完全没问题,不要对一次性调用进行过早优化。
模式: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 | 旋转 + 缩放 |
| 向量之间的夹角 | 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 -- 物理查询的正确性模式