一句话:四元数用 4 个数记录"绕哪根轴、转多少度",把一次旋转当成一个整体来存。它不会死锁、插值平滑、计算便宜,所以引擎内部几乎所有旋转都用它。Unity 的
transform.rotation本身就是四元数。
延续上一篇:先讲它是什么、为什么好用,再讲游戏里的具体用法。
一、四元数是什么?
1. 直觉:任何旋转都等于"绕某根轴转一次"
不管物体被转了多少次、怎么转,最终的姿态都可以用一根轴 + 一个角度一步到位。这是数学上的定理(欧拉旋转定理)。
欧拉角:先绕 Y 转 → 再绕 X 转 → 再绕 Z 转(3 步) 四元数:绕这根斜着的轴,一次转 60°(1 步)2. 4 个数怎么来
假设绕单位轴(ax, ay, az)转 θ 角:
x = ax · sin(θ/2) y = ay · sin(θ/2) z = az · sin(θ/2) w = cos(θ/2)x, y, z:记录轴的方向w:记录转了多少- 4 个数平方和恰好等于 1(这种叫单位四元数,只有它表示旋转)
| 四元数 (x, y, z, w) | 含义 |
|---|---|
| (0, 0, 0, 1) | 没有旋转(Quaternion.identity) |
| (0, 0.707, 0, 0.707) | 绕 Y 轴转 90° |
| (0, 1, 0, 0) | 绕 Y 轴转 180° |
为什么是半角 θ/2?因为用四元数旋转一个向量时,计算公式里四元数会被"用两次"(
q · v · q⁻¹),两个半角合起来正好是 θ。记住结论就行,日常开发完全不需要手算这 4 个数。
3. 为什么它没有万向节死锁?
欧拉角的问题出在"分 3 步、按顺序转",中间那步转到 90° 时,前后两根轴重合了。
四元数不分步骤,直接记录最终的"轴 + 角",没有"中间那根轴",也就不存在两根轴重合的问题。
4. 和欧拉角、矩阵对比
| 欧拉角 | 旋转矩阵 | 四元数 | |
|---|---|---|---|
| 存储 | 3 个数 | 9 个数 | 4 个数 |
| 人能看懂 | ✅ 很直观 | ❌ | ❌ |
| 万向节死锁 | ❌ 有 | ✅ 没有 | ✅ 没有 |
| 平滑插值 | ❌ 会绕远路、抖动 | ❌ 插值后会变形 | ✅ 天然平滑 |
| 组合旋转 | 麻烦 | 矩阵相乘(较贵) | 四元数相乘(便宜) |
分工:人填欧拉角 → 程序用四元数计算 → 渲染时转成矩阵交给 GPU。
二、最常用的 4 个操作
记住这 4 个,游戏里 90% 的旋转需求都能解决。
1. 旋转一个向量:q * v
Quaternionq=Quaternion.AngleAxis(90,Vector3.up);// 绕 Y 转 90°Vector3v=q*Vector3.forward;// (0,0,1) → (1,0,0)顺序必须是四元数在左、向量在右,写反了编译不通过。
2. 组合两次旋转:q2 * q1
Quaterniontotal=q2*q1;// 先转 q1,再转 q2(从右往左读)和矩阵一样,顺序不能交换。在 Unity 里有一个非常实用的规律:
Quaterniondelta=Quaternion.Euler(0,10,0);transform.rotation=transform.rotation*delta;// 绕【自身】的 Y 轴转transform.rotation=delta*transform.rotation;// 绕【世界】的 Y 轴转delta 放右边 → 自身坐标系(像飞机按自己的机身转) delta 放左边 → 世界坐标系(像地球的竖直方向)3. 反向旋转:Quaternion.Inverse(q)
把转过去的再转回来。常用于求两个朝向之间的差:
// 从 a 朝向转到 b 朝向,需要多转多少?Quaterniondiff=Quaternion.Inverse(a)*b;4. 插值:Slerp/Lerp
在两个朝向之间平滑过渡,这是四元数最大的价值,下一节详细讲。
三、游戏中的实际应用
应用 1:让物体朝向目标 ——LookRotation
敌人看向玩家、炮塔瞄准、角色面朝移动方向:
Vector3dir=player.position-transform.position;transform.rotation=Quaternion.LookRotation(dir);// 让 Z 轴(前方)指向 dir只转水平方向(角色不该因为玩家在高处就仰起身子):
dir.y=0;if(dir.sqrMagnitude>0.0001f)// 方向为零时不要调用transform.rotation=Quaternion.LookRotation(dir);应用 2:平滑转向 ——Slerp
直接设置朝向会"瞬间转过去",很生硬。用插值让它慢慢转:
Quaterniontarget=Quaternion.LookRotation(dir);transform.rotation=Quaternion.Slerp(transform.rotation,target,5f*Time.deltaTime);为什么用四元数插值就不会出问题?
欧拉角插值:350° → 10°,可能倒着转 340°;还会在 90° 附近乱甩 四元数插值:永远沿着【最短的那条弧】转过去,速度均匀、不抖动| 方法 | 特点 | 适用 |
|---|---|---|
Quaternion.Slerp | 角速度完全均匀 | 镜头、需要精确的动画 |
Quaternion.Lerp | 计算更快,中间速度略有不均 | 大量物体、差异肉眼难辨 |
小提醒:
Slerp(当前, 目标, k * deltaTime)是"越接近越慢"的缓动效果,在不同帧率下手感略有差异。追求严格一致时可以用1 - Mathf.Exp(-k * Time.deltaTime)作为插值系数。
应用 3:限速转向 ——RotateTowards
炮塔、坦克炮管通常有固定的最大转速,不是缓动:
publicfloatturnSpeed=90f;// 每秒最多转 90°voidUpdate(){Quaterniontarget=Quaternion.LookRotation(enemy.position-turret.position);turret.rotation=Quaternion.RotateTowards(turret.rotation,target,turnSpeed*Time.deltaTime);// 判断是否瞄准完成(两个朝向之间相差的角度)if(Quaternion.Angle(turret.rotation,target)<2f)Fire();}Quaternion.Angle直接给出两个朝向相差多少度,判断"瞄准了没有""视野内没有"都很方便。
应用 4:第三人称环绕相机
publicTransformtarget;publicfloatdistance=5f;floatyaw,pitch;voidLateUpdate(){yaw+=Input.GetAxis("Mouse X")*3f;pitch-=Input.GetAxis("Mouse Y")*3f;pitch=Mathf.Clamp(pitch,-30f,70f);Quaternionrot=Quaternion.Euler(pitch,yaw,0);// 人能理解的角度 → 四元数transform.rotation=rot;transform.position=target.position-rot*Vector3.forward*distance;// ↑ 用四元数把"前方"转到相机朝向,再往后退}这里同时用到了"欧拉角输入"和"四元数旋转向量",是两者分工的典型例子。
应用 5:贴合地面 ——FromToRotation
车辆开上斜坡、角色站在球形星球上,需要让"头顶方向"对齐地面法线:
if(Physics.Raycast(transform.position,Vector3.down,outRaycastHithit,2f)){// 求出"把当前上方转到地面法线"所需的旋转Quaternionalign=Quaternion.FromToRotation(transform.up,hit.normal);Quaterniontarget=align*transform.rotation;// 放左边:世界空间叠加transform.rotation=Quaternion.Slerp(transform.rotation,target,10f*Time.deltaTime);}FromToRotation(a, b):把方向 a 转到方向 b 的最短旋转,非常实用。
应用 6:骨骼动画与动画混合
角色每根骨骼的旋转都以四元数存储。走路切换到跑步时,两套动画要按比例混合:
最终旋转 = Slerp(走路姿势, 跑步姿势, 混合比例)如果用欧拉角混合,手臂可能在中间某帧突然甩一圈;用四元数则过渡自然。这也是动画系统(Animator、Timeline)底层都用四元数的原因。
应用 7:物理与陀螺仪
// 刚体旋转:应该用 MoveRotation,而不是直接改 transformrb.MoveRotation(rb.rotation*Quaternion.Euler(0,90f*Time.fixedDeltaTime,0));// 手机陀螺仪直接给出四元数,用于 AR、体感瞄准Quaterniongyro=Input.gyro.attitude;应用 8:网络同步压缩
多人游戏要每秒同步大量物体的朝向,四元数可以压得很小:
4 个 float = 16 字节 ↓ 利用"平方和 = 1",丢掉最大的那个分量,接收方再算回来 3 个分量 × 10 位 + 2 位标记丢的是哪个 = 32 位 = 4 字节这叫Smallest Three压缩,很多网络框架都在用。欧拉角很难这样压,因为它存在多种写法和死锁问题。
四、常见的坑
| 坑 | 说明 | 正确做法 |
|---|---|---|
直接改rotation.x | x、y、z、w 不是角度,改了会导致四元数失效、物体变形 | 用Euler、AngleAxis等函数构造 |
| 乘法顺序写反 | a * b和b * a结果不同 | 记住:右边先转;增量放右边 = 自身轴,放左边 = 世界轴 |
LookRotation(Vector3.zero) | 方向为零,会报警告且结果无意义 | 调用前检查长度 |
方向几乎竖直时LookRotation抖动 | 前方与默认上方Vector3.up平行,无法确定"上" | 手动传入合适的up参数 |
q和-q是同一个旋转 | 自己写混合时若不处理,可能绕远路转 | 若Quaternion.Dot(a, b) < 0,先把 b 取反(Unity 的 Slerp 已自动处理) |
| 多次相乘后误差累积 | 长时间累乘可能不再是单位四元数 | 必要时q.Normalize() |
五、常用 API 速查
| API | 作用 | 典型用途 |
|---|---|---|
Quaternion.Euler(x, y, z) | 欧拉角 → 四元数 | 把配置/输入角度转成旋转 |
Quaternion.AngleAxis(角度, 轴) | 绕任意轴旋转 | 风车、绕斜轴转动 |
Quaternion.LookRotation(方向) | 朝向某方向 | 看向目标、面朝移动方向 |
Quaternion.FromToRotation(a, b) | 把 a 转到 b | 贴合地面、对齐法线 |
Quaternion.Slerp / Lerp | 平滑插值 | 转向、动画混合 |
Quaternion.RotateTowards | 限速转向 | 炮塔、追踪导弹 |
Quaternion.Angle(a, b) | 两朝向的夹角 | 瞄准判断、视野判断 |
Quaternion.Inverse(q) | 反向旋转 | 求相对旋转 |
q * v | 旋转一个向量 | 计算偏移位置、射击方向 |
一句话总结
四元数 = 用 4 个数记录"绕哪根轴转多少度",把旋转当成一个整体。因为不分步骤,所以没有万向节死锁;因为能沿最短弧插值,所以转向和动画过渡都很平滑。
实际开发中基本不用关心那 4 个数的值,只要会用:
LookRotation看向目标、Slerp平滑转向、RotateTowards限速转、FromToRotation对齐方向、q * v旋转向量、a * b组合旋转,就能覆盖绝大多数需求。