一句话:欧拉角就是把任意一个旋转,拆成"绕 3 根轴依次转 3 次"。记录的只是 3 个角度,真正旋转物体时,会把它们变成 3 个旋转矩阵按顺序相乘。
还是按直觉 → 原理 → 代码 → 坑的顺序,尽量讲简单。
一、直觉:飞机的三个动作
想象你在开飞机,所有姿态都能由三个动作组合出来:
| 动作 | 英文 | 绕哪根轴 | Unity 中 |
|---|---|---|---|
| 左右转头(机头左右摆) | Yaw 偏航 | 竖直轴 | Y |
| 抬头低头(机头上下摆) | Pitch 俯仰 | 左右轴 | X |
| 左右翻滚(机翼一高一低) | Roll 翻滚 | 前后轴 | Z |
Y(偏航) ↑ │ │ ●──────→ X(俯仰) ╱ ╱ ↙ Z(翻滚,机头方向)Inspector 里 Rotation 显示的(30, 45, 0),就是"绕 X 转 30°、绕 Y 转 45°、绕 Z 转 0°"。
优点:人类非常好理解,美术、策划直接填数字就行。
二、原理第一步:绕一根轴怎么转?
1. 先看 2D:平面上转一个点
一个点(x, y)绕原点转 θ 角,新位置是:
x' = x·cosθ − y·sinθ y' = x·sinθ + y·cosθ不用死记,看两个特殊点就懂了:
y ↑ (−sinθ, cosθ) ● ● (cosθ, sinθ) ← (1,0) 转 θ 后到这里 ╲ │ ╱ ╲ │ ╱ θ ╲ │ ╱ ──────────●───────────→ x(1, 0)转 θ 后到了(cosθ, sinθ)(0, 1)转 θ 后到了(−sinθ, cosθ)
任意点(x, y)= x 个(1,0)+ y 个(0,1),旋转后就是 x 个新位置 + y 个新位置,展开就是上面的公式。
2. 推广到 3D:绕哪根轴,哪根轴的坐标不变
绕 Z 轴转,z 坐标不动,只有 x、y 在平面里转——就是上面的 2D 旋转。其他两根轴同理:
绕 X 轴(x 不变) 绕 Y 轴(y 不变) 绕 Z 轴(z 不变) ┌ 1 0 0 ┐ ┌ cos 0 sin ┐ ┌ cos −sin 0 ┐ │ 0 cos −sin │ │ 0 1 0 │ │ sin cos 0 │ └ 0 sin cos ┘ └ −sin 0 cos ┘ └ 0 0 1 ┘ Rx Ry Rz看规律:不变的那根轴所在的行和列是 1 和 0,剩下 4 个格子就是 2D 旋转。
三、原理第二步:三次旋转按顺序合并
1. 顺序非常重要
拿起手机试一下(屏幕朝你):
方案 A:先绕 X 转 90°(向后倒,屏幕朝天)→ 再绕 Y 转 90° 方案 B:先绕 Y 转 90°(屏幕朝右) → 再绕 X 转 90°两次动作完全一样,只是顺序反了,手机最终的朝向完全不同。
数学上也一样:矩阵乘法不满足交换律,Rx × Ry ≠ Ry × Rx。
所以每个引擎都必须规定一个固定顺序。
2. Unity 的顺序
Unity 文档的说法:先绕 Z,再绕 X,最后绕 Y(绕的是世界坐标轴)。写成矩阵:
R = Ry × Rx × Rz ↑ 最右边的最先作用在点上 新位置 = Ry × Rx × Rz × 原位置为什么这么选?因为它等价于按飞机自身的轴理解成:先转头(Y)→ 再抬头(X)→ 最后翻滚(Z)。这最符合角色和相机的操作习惯:先决定朝哪个方向,再决定抬头多少。
| 引擎 / 软件 | 默认顺序(各家描述方式不同) |
|---|---|
| Unity | ZXY(世界轴)= Y→X→Z(自身轴) |
| Unreal | Yaw-Pitch-Roll |
| Maya / Blender | 默认 XYZ,可在属性里切换 |
从一个软件导出动画到另一个软件,旋转"全乱了",很多时候就是顺序不一致。
3. 代码:手写欧拉角转矩阵(和 Unity 结果一致)
usingUnityEngine;publicstaticclassEulerMath{staticMatrix4x4RotX(floatdeg){floatr=deg*Mathf.Deg2Rad,c=Mathf.Cos(r),s=Mathf.Sin(r);varm=Matrix4x4.identity;m.m11=c;m.m12=-s;m.m21=s;m.m22=c;returnm;}staticMatrix4x4RotY(floatdeg){floatr=deg*Mathf.Deg2Rad,c=Mathf.Cos(r),s=Mathf.Sin(r);varm=Matrix4x4.identity;m.m00=c;m.m02=s;m.m20=-s;m.m22=c;returnm;}staticMatrix4x4RotZ(floatdeg){floatr=deg*Mathf.Deg2Rad,c=Mathf.Cos(r),s=Mathf.Sin(r);varm=Matrix4x4.identity;m.m00=c;m.m01=-s;m.m10=s;m.m11=c;returnm;}// Unity 的顺序:R = Ry * Rx * RzpublicstaticMatrix4x4EulerToMatrix(Vector3e){returnRotY(e.y)*RotX(e.x)*RotZ(e.z);}}// 验证:两者结果应该相同// Matrix4x4 a = EulerMath.EulerToMatrix(new Vector3(30, 45, 60));// Matrix4x4 b = Matrix4x4.Rotate(Quaternion.Euler(30, 45, 60));四、反过来:从旋转矩阵算回欧拉角
把Ry × Rx × Rz乘开(cx 表示 cos(x),sx 表示 sin(x),依此类推),挑出几个简单的格子:
m12 = −sx → 只和 x 有关 m02 = sy·cx, m22 = cy·cx → 相除后只剩 y m10 = cx·sz, m11 = cx·cz → 相除后只剩 z于是:
publicstaticVector3MatrixToEuler(Matrix4x4m){floatx=Mathf.Asin(Mathf.Clamp(-m.m12,-1f,1f));floaty=Mathf.Atan2(m.m02,m.m22);floatz=Mathf.Atan2(m.m10,m.m11);returnnewVector3(x,y,z)*Mathf.Rad2Deg;}注意看:算 y 和 z 时,分子分母都带着cx。当x = ±90°时cx = 0,就变成Atan2(0, 0)——无法确定 y 和 z。这就是下面要讲的万向节死锁,在数学上露出的马脚。
五、欧拉角最大的问题:万向节死锁(Gimbal Lock)
1. 直觉
还是开飞机:把机头拉到正上方(俯仰 90°)。
↑ 机头朝天 ┃ ┃ ═╋═ 机翼 ┃这时候:
- “左右转头”(绕竖直轴)→ 飞机原地绕着朝天的机身打转
- “左右翻滚”(绕机身轴)→ 飞机原地绕着朝天的机身打转
两个操作效果一模一样!三个旋转轴里有两个重合了,原本 3 个自由度只剩 2 个。此时无论怎么调 Y 和 Z,都没法让机头往"侧面"偏,这个方向的旋转被"锁死"了。
2. 数学上的表现
当 x = 90° 时,Ry(a) × Rx(90°) × Rz(b)乘出来的结果只和 a、b 的组合有关(一个加一个减),比如(90, 30, 0)和(90, 0, -30)是同一个姿态。两个参数,只控制了一个效果。
3. 在游戏里会造成什么
| 现象 | 场景 |
|---|---|
| 相机抬头到顶时突然乱转、翻转 | 自由视角相机没有限制俯仰角 |
| 动画在某帧突然"甩一圈" | 骨骼动画用欧拉角插值,经过 90° 附近 |
| 飞行游戏竖直爬升时操作失灵 | 飞机姿态用欧拉角存储 |
注意:死锁不是"物体真的卡住了",而是"用欧拉角这种描述方式表达不了某些连续变化"。物体本身完全可以那样转,只是这 3 个数字说不清楚。
六、欧拉角的其他坑
1. 同一个姿态,有多种写法
transform.eulerAngles=newVector3(180,0,180);Debug.Log(transform.eulerAngles);// 可能输出 (0, 180, 0)(180, 0, 180)和(0, 180, 0)是完全相同的朝向。Unity 内部用四元数存储,读回欧拉角时只会给出其中一种,所以写进去的值和读出来的值可能不一样。
2. 插值会"绕远路"
从 350° 转到 10°,实际只差 20° 直接 Lerp(350, 10) → 350 → 300 → ... → 10,倒着转了 340° ❌3. 不要单独累加读回的分量
// ❌ 错误:读回的 x 可能跳变(例如超过 90° 后被换成另一种写法),导致抖动Vector3e=transform.eulerAngles;e.x+=1f;transform.eulerAngles=e;七、实际项目中怎么用
原则:欧拉角用来"给人看、给人填",计算和存储交给四元数。
| 场景 | 推荐做法 |
|---|---|
| Inspector 填旋转、配置表填角度 | 欧拉角 ✅ |
| 物体持续旋转 | transform.Rotate(...)或四元数相乘 |
| 两个朝向之间平滑过渡 | Quaternion.Slerp/RotateTowards |
| 朝向某个目标 | Quaternion.LookRotation |
| 第一/第三人称相机 | 自己保存yaw、pitch 两个变量,限制 pitch,再组合 |
标准的鼠标视角相机写法
usingUnityEngine;publicclassMouseLook:MonoBehaviour{publicfloatsensitivity=3f;floatyaw,pitch;// 自己维护,不从 transform 读回voidUpdate(){yaw+=Input.GetAxis("Mouse X")*sensitivity;pitch-=Input.GetAxis("Mouse Y")*sensitivity;pitch=Mathf.Clamp(pitch,-89f,89f);// 不到 90°,避开死锁transform.rotation=Quaternion.Euler(pitch,yaw,0f);}}这段代码同时避开了三个坑:
- 角度自己存,不读回
eulerAngles→ 不会出现写法跳变 - 俯仰限制在 ±89° → 不会进入万向节死锁
- 最终转成四元数赋值 → 计算稳定
一句话总结
欧拉角 = 绕 3 根轴依次旋转的 3 个角度。实现上把每个角度变成一个旋转矩阵(绕哪根轴,哪根轴不动),再按固定顺序相乘(Unity 是
Ry × Rx × Rz),顺序不同结果就不同。它直观好用,但当中间那次旋转到90°时,另外两根轴会重合,丢失一个自由度,这就是万向节死锁。所以实际项目里:人填欧拉角,程序算四元数。