Unity 里的局部坐标和世界坐标:Transform 背后的矩阵

如果说前面几篇还偏“基础知识”,那从这一篇开始,矩阵就彻底进入 Unity 日常开发现场了。

我自己觉得,矩阵对客户端开发最先产生价值的地方,不是在 Shader,而是在你第一次认真排查父子层级、挂点和骨骼跟随的时候。

平时我们最常写的是:

  • transform.position
  • transform.localPosition
  • transform.rotation
  • transform.localScale

这些字段看起来很直观,但真正把它们串起来的,是矩阵。

一、局部空间和世界空间的差别,本质上是参照系不同

这是所有问题的起点。

一个子物体的 localPosition = (0, 1, 0),意思不是“它在世界里 y=1”。

它的意思是:

相对于父节点的局部坐标系,它离父节点原点向上 1 个单位。

如果父节点旋转了、缩放了、平移了,这个子物体在世界里的最终位置就会一起变。

所以:

  • localPosition 是局部描述
  • position 是世界描述

它们不是两个独立真相,只是同一个点在不同坐标系下的两种写法。

二、Transform 本质上就是一个 TRS 变换

Unity 里单个节点最核心的三组信息就是:

  • Translation 位移
  • Rotation 旋转
  • Scale 缩放

把它们合起来,就是一个局部到父级空间的变换矩阵。

如果继续往上乘父节点、祖先节点的矩阵,就会得到最终的 localToWorldMatrix

也就是说,一个物体的世界位置不是“单独算出来的”,而是整条父子层级矩阵连乘的结果。

三、为什么父节点一动,子节点世界坐标全变

因为子节点的局部坐标,不是直接挂在世界空间里,而是先挂在父节点坐标系里。

父节点的局部 x 轴、y 轴、z 轴一旦转了、缩了、挪了,子节点的解释方式也会跟着变。

这就是层级系统最核心的地方:

父节点定义了子节点的参考坐标系。

所以你看到的不是“子节点自己在乱动”,而是它所依附的坐标空间在变化。

四、TransformPoint 为什么这么常用

假设你有一个局部挂点:

1
Vector3 localOffset = new Vector3(0.2f, 1.1f, 0f);

你要得到这个挂点在世界里的实际位置,最稳的方式通常就是:

1
Vector3 worldPos = transform.TransformPoint(localOffset);

它本质上等价于:

用当前物体的局部到世界矩阵,把一个“点”乘到世界空间。

这也是为什么它会受平移、旋转、缩放共同影响。

五、TransformDirectionTransformVector 不要混用

这两个 API 很多人会随手用,但语义上还是有区别。

一般理解上:

  • TransformPoint 处理位置点
  • TransformDirection 更强调方向
  • TransformVector 更强调长度也参与缩放

最常见的 bug 是:

你本来在处理“朝向”,却用成了点变换,结果把平移也算进去了。

或者你本来想保留纯方向,却无意间吃进了缩放影响。

所以一旦你在做射线、武器发射方向、特效朝向、骨骼挂点时出现偏差,先检查这里。

六、worldToLocalMatrix 是排查问题的利器

很多时候我们不是把局部点转世界,而是反过来:

我有一个世界坐标,想知道它相对于某个物体的局部坐标是多少。

例如:

  • 判断敌人是否进入角色前方扇形区域
  • 计算点击点在某个局部网格里的坐标
  • 把世界里的命中点映射回模型局部空间

这个时候 InverseTransformPointworldToLocalMatrix 就很好用。

因为它做的正是逆变换。

例如做“目标是不是在角色前方”的判断时,我一般不会先上点乘,而是先转回局部空间:

1
2
3
4
Vector3 localTarget = transform.InverseTransformPoint(target.position);

bool isInFront = localTarget.z > 0f;
bool isOnRight = localTarget.x > 0f;

这类写法的好处是语义很直接,后面要改成扇形范围、矩形攻击框或者棋盘局部坐标判断,也很容易往下扩。

七、一个父子层级的典型心智模型

可以把每个 Transform 理解成“在父坐标系里再搭一层新坐标系”。

比如:

  • 角色是第一层
  • 手臂骨骼是第二层
  • 武器挂点是第三层
  • 枪口火焰位置是第四层

最终枪口点位为什么能一直跟手臂、身体、朝向保持一致?

不是因为每一帧都在人工修正,而是因为这一连串矩阵关系天然保证了它们同步。

八、非等比缩放为什么在层级里很危险

如果父节点有非等比缩放,子节点的局部轴往往会变得不那么“干净”。

在工程里,这会引出很多连锁问题:

  • 朝向判断怪怪的
  • 圆形碰撞看起来被拉扁
  • 特效跟随位置没错,但方向不对
  • 法线或光照表现异常

所以很多团队会约束:

运行时骨架层、逻辑层尽量避免乱用非等比缩放。

因为这不是不能用,而是用了以后,你就必须更清楚矩阵里到底发生了什么。

九、Unity 中几个很值得背后的接口

我建议不要只背用法,而是连同语义一起记:

1
2
3
4
5
6
transform.localToWorldMatrix
transform.worldToLocalMatrix
transform.TransformPoint(localPoint)
transform.InverseTransformPoint(worldPoint)
transform.TransformDirection(localDir)
transform.InverseTransformDirection(worldDir)

这几组接口基本覆盖了大部分坐标转换场景。

十、一个很实用的调试建议

当你怀疑坐标不对时,不要只打印 position

建议一起打印:

1
2
3
Debug.Log(transform.localPosition);
Debug.Log(transform.position);
Debug.Log(transform.localToWorldMatrix);

再配合 Scene 视图里的 Gizmos 画出局部轴方向,通常很快就能定位问题到底出在:

  • 父节点层级
  • 旋转顺序
  • 缩放污染
  • 局部/世界空间搞混

十一、总结

Transform 从表面上看只是几个字段,实际上它是 Unity 空间系统最核心的封装。

只要你把“每个节点都在定义一个局部坐标系”这个认知建立起来,很多层级、挂点、骨骼、朝向问题都会清楚很多。

下一篇继续往下走,专门讲 Matrix4x4.TRS 的顺序问题,以及项目里什么时候值得自己手组矩阵。