文章摘要
坐标变换=在不同“参考系”里看同一个点/物体。
具体干的三件事:
- 平移:整体挪个位置;
- 旋转:整体转个角度;
- 缩放:整体放大缩小。
在数学里,这是解析几何 + 线性代数的核心;
在游戏里(Unity / UE / 自研引擎),
你看到的一切移动、转身、镜头、骨骼动画、相机跟随,
背后都是无数次“坐标变换”的组合。
下面我用大白话 + 具体游戏例子,把这件事从直觉讲到公式,再讲到代码和实际用法。
一、先弄清楚:为啥要“坐标变换”?
1.1 同一个东西,在不同“参考系”里,坐标是不一样的
想象一个简单场景:
- 你站在房间中间;
- 房间在地图上的位置也有个坐标;
- 摄像机在天花板上看你;
- 小地图也在俯视整个房间。
问题来了:
“你这个人”,到底坐标是多少?
要看是谁在说:
- 以“整个地图”为参考:
- 你的位置是“世界坐标”:例如 (100, 0, 50);
- 以“房间”为参考:
- 你的位置也可以写成“相对于这个房间”的局部坐标:例如 (3, 0, 2);
- 以“你自己”为参考:
- 你的手、脚的位置,是“相对于你的身体”的局部坐标,比如手在 (0.5, 1.2, 0)。
同一个点在不同坐标系下的坐标值是不一样的。
坐标变换干的,就是在这些坐标系之间“翻译”。
1.2 游戏里的典型场景:父子节点 + 世界坐标
在 Unity 里:
- 一个 GameObject 有 transform:
- localPosition / localRotation / localScale(相对父物体);
- position / rotation / lossyScale(世界坐标下)。
你经常会做的事:
- 把一个武器挂在角色手上(武器坐标相对手骨骼);
- 再把角色放到世界任意一个位置(角色坐标相对世界);
- 角色再旋转、移动,武器会自然跟着走、跟着转;
这背后就是父子层级的坐标变换:
- 子物体坐标 = 在父坐标系下的局部坐标;
- 要渲染、做物理时,必须把这些局部坐标转换成世界坐标。
二、三大基本操作:平移、旋转、缩放,用生活话说一遍
2.1 平移:整体搬家
平移 = 整体挪个位置,不改变方向和大小。
类比一下:
- 你把一张画从桌子上拿到地上;
- 画上的所有点的位置都变了,但图案形状没变。
数学上:
- 原点 P=(x, y, z);
- 平移向量 T=(tx, ty, tz);
- 平移后的点 P’ = P + T。
Unity 里:
transform.position+=newVector3(1,0,0);// 向世界 x 方向平移 1这就是一次最直观的坐标变换:
加一个向量。
2.2 旋转:绕某个点/轴转一圈
旋转 = 改变方向,但不改变长度。
生活里:
- 你手里拿着一支笔,笔尖在空中画圆;
- 你转动手机,屏幕画面旋转 90°;
- 你开车转弯,从面朝北变成面朝东。
数学上:
在 2D:绕原点旋转 θ,
点 P(x, y) 变成 P’(x’, y’):x' = x cosθ - y sinθ y' = x sinθ + y cosθ这可以写成矩阵乘法(后面讲矩阵会说,但你现在只要知道“旋转就是变换公式”)。
在 3D:绕 x/y/z 轴旋转,有对应的 3×3 矩阵,或者用四元数。
Unity 里你写:
transform.Rotate(0,90,0);就是在把物体绕 y 轴旋转 90 度。
2.3 缩放:放大 / 缩小
缩放 = 改变大小,不改变方向(如果缩放比例为正)。
- 点 P(x, y, z);
- 缩放比例 S=(sx, sy, sz);
- 缩放后 P’=(sx * x, sy * y, sz * z)。
如果 sx=sy=sz,这叫“等比缩放”,物体不会变形,只是整体变大/变小。
Unity 里:
transform.localScale=newVector3(2,2,2);// 放大 2 倍这会让物体在三个轴上都放大两倍。
三、从点的视角看:坐标怎么一步步被“变换”?
想象有一个点 P,它经历了这几件事:
- 在原点附近画出来;
- 整体往右挪了 3 米(平移);
- 绕原点旋转了 90 度(旋转);
- 再整体放大 2 倍(缩放)。
每动一次,它的坐标就会变一次:
- 原始:P0(x, y);
- 平移后:P1 = P0 + T;
- 旋转后:P2 = R * P1;
- 缩放后:P3 = S * P2。
坐标变换 = 对坐标做一系列算术操作:加、乘矩阵、乘比例。
四、再具体一点:用矩阵把平移+旋转+缩放统一管理(略带点线性代数)
不用矩阵也能理解坐标变换,但在游戏/图形这行,你迟早要面对矩阵。
我用最少的数学,把这件事说清楚。
4.1 为什么要矩阵?——因为变换多了,手动算会疯
你可以这样想:
- 平移:P’ = P + T;
- 旋转:P’ = R * P;
- 缩放:P’ = S * P;
问题来了:
- 如果你对一个点先缩放,再旋转,再平移,怎么写?
- 如果场景里有 10 个祖孙层级物体,每层都有自己的平移、旋转、缩放,你要怎么把最里层点的最终世界坐标算出来?
手动一层层代入,会很噩梦。
矩阵的作用就是:
把“平移+旋转+缩放”这些操作统一写成一个 4×4 矩阵 M,
点用“齐次坐标”写成 4×1 列向量,
这样所有变换都可以写成:
P’ = M * P。多次变换叠加,就是多个矩阵相乘:
M_total = M3 * M2 * M1。
Unity 中transform.localToWorldMatrix就是一个典型的 4×4 变换矩阵。
4.2 不用死记矩阵形式,只要知道:矩阵=“打包变换”
你现在不需要把每个矩阵的元素背下来,只要记住一句关键话:
在图形/游戏世界里:
一个“4×4 变换矩阵”通常就是:
“先缩放、再旋转、再平移”的打包结果。
因此,只要你有了一个 transform(里边有 position/rotation/scale),
引擎就能帮你生成一个矩阵 M:
- 把任何子点的“局部坐标”乘上这个矩阵 M,就能得到“世界坐标”。
通俗点说:
矩阵 = 一个物体“从自己空间到世界空间”的翻译官。
五、游戏案例 1:角色移动——本地坐标 vs 世界坐标
5.1 问题:按 W 键,角色到底往哪走?
在第三人称 / FPS 游戏里:
- 你按 W 键,角色“向前走”;
- 如果角色没转身,“前”是世界的某个方向;
- 如果角色已经转向了,“前”是角色面朝的方向,而不是世界的 Z 轴。
这里就涉及一个核心概念:
“前”是哪个方向?
要以谁为参考?
5.2 以世界为参考:简单粗暴的移动
最简单的写法:
// 世界坐标系下,向 z 轴正方向移动transform.position+=newVector3(0,0,1)*speed*Time.deltaTime;这表示:
- 不管你角色面朝哪,只要按 W,就朝世界 z 正方向走。
- 适合“固定摄像机”或者“顶视 RTS”类游戏,但不适合“角色跟随摄像机转身”的 3D 动作游戏。
5.3 以角色自己为参考:本地坐标移动
大多数 3D 动作/FPS,需要的是:
角色按 W,一直朝自己的“前方”移动。
在 Unity 里:
transform.forward:表示角色在世界坐标系中的“前方方向”(一个单位向量)。
所以移动写成:
transform.position+=transform.forward*speed*Time.deltaTime;这里,transform.forward是一个典型的“坐标变换”结果:
- 在角色自己的局部坐标里,“前”是 (0, 0, 1);
- 通过角色的旋转矩阵,把 (0, 0, 1) 转换到世界坐标系中,就得到了一个新的向量
transform.forward; - 再用它来移动,就相当于:在世界空间里沿着“角色的前方方向”移动。
你可以这样理解:
“局部方向”+“旋转变换” → “世界方向”;
这是最核心的方向坐标变换。
六、游戏案例 2:武器绑定手上——父子坐标变换
6.1 需求:手动一摆,武器跟着走
在角色身上挂武器,常见做法:
- 在角色的骨骼上,有一个“Hand_R”骨骼节点;
- 武器作为 Hand_R 的子物体;
- 设置武器的
localPosition和localRotation来调整握持姿势; - 之后角色播放动画时,骨骼变换会带动武器一起动。
6.2 数学视角:局部坐标 + 父矩阵 = 世界坐标
设:
- 武器在手坐标系中的局部坐标(位置和旋转) = Pw_local;
- 手骨骼的世界变换矩阵 = M_hand;
- 武器自身的局部变换矩阵 = M_weapon_local;
- 武器的世界矩阵 = M_weapon_world;
则:
M_weapon_world = M_hand * M_weapon_local再换成点的视角:
P_weapon_world = M_hand * P_weapon_local理解成日常话就是:
“武器在手心这里相对偏一点点(局部),
再加上手整体在世界的位置和旋转,
就能算出武器到底在世界什么位置、朝向哪里。”
Unity 里你几乎不用管这些矩阵细节,只要:
weapon.transform.parent=handTransform;weapon.transform.localPosition=offsetPosition;weapon.transform.localRotation=offsetRotation;之后所有坐标变换会自动完成。
七、游戏案例 3:摄像机跟随与绕点旋转
7.1 摄像机跟着角色走:平移 + 旋转的组合
典型的第三人称视角:
- 摄像机永远在角色后上方一段距离;
- 角色前进,摄像机跟着平移;
- 鼠标左右移动,摄像机绕角色旋转(角色也可跟着转身)。
设定:
- 角色位置:P;
- 角色朝向方向:forward;
- 摄像机相对角色的“局部偏移”:
- 比如在角色背后 5 米,上方 3 米。
- 可以在角色坐标系中写成 offset_local = (0, 3, -5)。
真正的相机位置要在世界坐标系里算:
// 把局部偏移变换到世界空间Vector3offset_world=characterTransform.TransformDirection(offset_local);// 或更严格:TransformPoint,把原点也平移进来camera.position=characterTransform.position+offset_world;这里用到的 Unity 函数:
TransformDirection(vec):把一个“局部方向向量”变换到世界方向(只考虑旋转和缩放,不加平移);TransformPoint(point):把局部坐标点变换到世界坐标(包括平移)。
你可以理解为:
offset_local 是“在角色自己的轴里,摄像机该在哪”;
TransformDirection/TransformPoint 做了一次“从角色空间到世界空间”的坐标变换;
得到真正的世界坐标位置。
7.2 绕角色旋转相机:以角色为中心旋转坐标系
鼠标左右移动时,你希望相机绕角色转,而不是角色绕相机转。
简化版逻辑:
记录一个“相机相对角色的球坐标”(极坐标):
- 距离 r;
- 水平角 yaw;
- 俯仰角 pitch;
鼠标左右动修改 yaw,鼠标上下动修改 pitch;
再把 (r, yaw, pitch) 转回 3D 直角坐标(就是一个坐标变换),得到 offset_local;
再像刚才那样,把 offset_local 转成世界坐标。
这个过程,本质就是:
我们用了一种更适合描述“绕着一个点转圈”的坐标系(极坐标/球坐标),
再把它转换回直角坐标系,用来画图、渲染。
八、游戏案例 4:缩放——角色变大变小、UI 自适应、攻击判定
缩放看起来简单,但实战用途挺多。
8.1 Boss 变大:整体缩放模型
有些游戏里,Boss 激怒后会突然变大:
boss.transform.localScale=newVector3(2,2,2);// 放大 2 倍效果:
- 模型坐标系里的所有点,都乘以 2;
- 所有子物体、武器、挂在身上的特效,也跟着放大。
有几点需要注意:
碰撞体(Collider)是否同步缩放?
- Unity 大部分 Collider 会受 localScale 影响(Box、Sphere 等),
但有时需要手动调整参数以保证准确。
- Unity 大部分 Collider 会受 localScale 影响(Box、Sphere 等),
攻击范围 / 判定圆 / 触发器是否同步放大?
常常在代码里根据 scale 调整半径:
floatscaledRadius=baseRadius*transform.lossyScale.x;// 假设等比缩放
8.2 UI 缩放:屏幕分辨率变了,UI 如何适配?
UI 系统里,坐标一般在“屏幕空间”坐标系下工作:
- 屏幕宽度从 1920 变 2560;
- UI 元素的 anchor / pivot 决定它如何缩放、对齐;
- RectTransform 的坐标变换里,也隐含着平移+缩放。
比如一个血条在屏幕左上角:
- 可用相对屏幕宽高的比例来确定它的位置;
- 当分辨率变化时,通过 Canvas Scaler + RectTransform 自动做缩放和平移的坐标变换,让它保持在视觉上的“同一位置”。
本质还是:
“屏幕像素坐标系”和“UI 布局坐标系”之间的变换。
九、父子层级里的“坐标变换链”:骨骼动画的本质
再稍微往进阶一点,这个思想在骨骼动画里体现得淋漓尽致。
9.1 每块骨骼都有自己的局部变换
一个角色骨骼层级大致是:
- Root
- Spine
- Shoulder
- Arm
- Forearm
- Hand
- Weapon
- Hand
- Forearm
- Arm
- Shoulder
- Spine
每个骨骼都有:
- localPosition(相对父骨骼);
- localRotation;
- localScale。
当你播放一段跑步动画时:
- 动画系统在每一帧更新每块骨骼的局部变换;
- 然后从 Root 开始,一层层做“局部→父→世界”的矩阵乘法;
- 得到每块骨骼在世界坐标系下的最终变换;
- 再驱动蒙皮网格(SkinMesh)里的每个顶点做相应的坐标变换。
这是一条典型的“坐标变换链”:
P_world = M_root * M_spine * M_shoulder * M_arm * M_forearm * M_hand * P_local你不需要在代码里手写这堆运算,引擎会帮你做完;
但理解这件事有助于你:
- 调试骨骼问题;
- 做一些 IK(反向动力学)效果(比如脚贴地);
- 实现攻击判定(通过骨骼位置计算武器路径)。
十、再往“数学一点”:组合变换顺序很重要
平移 + 旋转 + 缩放的组合,有一个非常关键的坑:
变换顺序不一样,结果会不一样。
简单举个 2D 例子:
- 先平移再旋转 VS 先旋转再平移。
10.1 先平移再旋转:绕世界原点转圈
假设点 P0=(1,0),操作:
- 平移 T=(1,0) → P1 = (2,0);
- 再绕原点旋转 90° → P2 = (0,2)。
画出来你会发现:
这个点是在“先从右边往外挪一点,再绕原点转”的。
视觉效果:
绕世界原点旋转。
10.2 先旋转再平移:绕自身局部原点平移
同样起点 P0=(1,0),操作:
- 先绕原点旋转 90° → P1=(0,1);
- 再平移 T=(1,0) → P2=(1,1)。
视觉效果:
先把形状转好,再整体平移。
这两个结果不同。
在矩阵里表现为:
- M_total = T * R ≠ R * T。
在游戏中,这也极其常见:
- 有时候你想“绕物体自身中心旋转再移动”;
- 有时候你想“先移动到某个位置,再绕世界原点旋转”。
理解顺序的重要性,能帮你避免很多“相机绕圈绕错中心”“物体旋转古怪”的 BUG。
十一、把整篇压成一张“坐标变换小抄”
最后,用一张“脑内小抄”的形式,把关键点帮你捋一下。以后做游戏/看代码/学数学,都能快速想起来。
11.1 概念层面
坐标系 = 给点编号码的规则:
- 世界坐标系:整个场景的统一参考系;
- 局部坐标系:某个物体/骨骼自己的参考系。
坐标变换 = 在不同坐标系之间翻译:
- 局部 → 世界;
- 世界 → 摄像机;
- 屏幕 → UI;
- 等等。
三大基本操作:
- 平移:加一个向量(位置变化);
- 旋转:乘一个旋转矩阵/四元数(方向变化);
- 缩放:乘一个缩放矩阵(大小变化)。
11.2 数学/实现层面
平移:
P' = P + T旋转(2D 例子):
x' = x cosθ - y sinθ y' = x sinθ + y cosθ缩放:
P' = (sx * x, sy * y, sz * z)组合变换:
- 用 4×4 矩阵一次性打包;
- 多层变换通过矩阵相乘叠加;
- 顺序很重要:M_total = M3 * M2 * M1。
11.3 游戏应用层
角色移动:
- 世界方向移动:直接加
(0,0,1); - 角色“前方向”移动:加
transform.forward。
- 世界方向移动:直接加
武器绑定手上:
- 武器的 localPosition/localRotation 相对于手骨骼;
- 手骨骼变换再通过矩阵链传到世界。
摄像机跟随 / 绕角色旋转:
- 使用“局部偏移 + 坐标变换”得出世界位置。
缩放:
- Boss 变大、技能范围变大;
- UI 自适应屏幕;
- 碰撞范围依据 scale 调整。
骨骼动画、IK:
- 一连串父子变换链;
- 每帧把局部变换算到世界空间。
十二、一句真正能记住的话收尾
把这篇东西浓缩到一句最大白话里:
所谓“坐标变换:平移 + 旋转 + 缩放”,
就是告诉你:
同一个点/物体,在不同“参考系”下坐标不一样,
我们用平移、旋转、缩放的组合,
把这些坐标在各种坐标系之间来回“翻译”。数学上,这是解析几何 + 矩阵运算;
游戏里,它就是:
- 角色能走能转;
- 武器能跟着手一起挥;
- 摄像机能跟着你转圈;
- UI 能适配不同分辨率;
- 骨骼动画能让模型活起来。
当你下次写出:
transform.position+=transform.forward*speed*Time.deltaTime;weapon.parent=hand;camera.position=character.position+character.TransformDirection(offset);可以在心里补一句:
“哦,这几行,就是在做坐标变换——
平移 + 旋转 + 缩放的组合,让虚拟世界动起来。”