☰
万向节锁是什么?从欧拉角到四元数的旋转避坑指南
2026/9/30 15:41:49 网站建设 项目流程

有没有在 3D 软件里遇到过这种见鬼的事:把一个物体绕 X 轴转 90°,再去拖 Y 轴和 Z 轴滑条,两个滑条的效果几乎一样,第三个方向怎么都拧不出来。我当年第一次撞上万向节锁(Gimbal Lock)时,第一反应是软件出 bug 了,后来看了很多资料反而更懵,直到自己坐下来把矩阵算了一遍,才真正明白是怎么回事。

这篇东西打算用白开水的方式讲:不堆术语,不搞上来就吓人的数学公式,但也不会绕开核心计算。你只要能看懂三维直角坐标,能忍受一点点矩阵乘法,我保证你能从根上理解万向节锁为什么存在、什么时候触发、怎么绕开它。这篇文章同样适合做游戏、做动画、搞机器人或者玩飞控的朋友,因为这几个领域几乎每天都在跟这个坑打交道。

1. 滑条失灵背后的真相:到底是谁被锁死了

1.1 一个让所有 3D 工具使用者崩溃的瞬间

你先回忆一下那个场景:建模软件里放了一个立方体,旋转面板上有 X、Y、Z 三个角度输入框。你先把 X 改成 90°,然后开始拖动 Y。按道理说,物体应该绕 Y 轴“乖乖”转动,但屏幕上的模型却在绕着一个斜轴翻滚。你再拖 Z,发现效果和拖 Y 差不多。三个自由度好像凭空少了一个,怎么操作都别扭。

很多人会以为是自己手残,或者软件抽风,于是重启软件再试一次,结果一样。其实这不是软件 bug,而是欧拉角表示姿态时的一个数学奇点,名字就叫万向节锁。为什么叫“万向节”?因为机械陀螺仪和万向支架早就用到了同一个物理模型,三环嵌套,各管一个旋转方向。

1.2 万向节锁到底锁的是什么

关键点来了:万向节锁锁住的不是物体本身,而是控制物体的那套参数。

物体作为一个刚体,在三维空间里可以转到任意姿态。这个事实不会因为任何数学表示而改变。你用手把那个立方体拿起来,想怎么摆就怎么摆,它永远有三个旋转自由度。问题出现在你用“欧拉角”这种工具去描述它的时候——欧拉角用三个数描述三个自由度,听起来挺合理,但在某些姿态下,这三个数会互相纠缠,导致两个输入旋钮实际控制同一个方向的转动。

这就好比你面前有三个水龙头,本来一个管冷水、一个管热水、一个管温水,结果管道内部接错了,拧其中两个水龙头出来的都是温水,你想调温度就只能同时动两个开关,非常难受。不是说水压出了问题,而是“管道拓扑”在某些状态下退化成了两个有效阀门。

数学上更准确的说法是:欧拉角这种三维参数表示无法覆盖所有三维旋转而不出现奇点。这是被拓扑学论证过的结论,简单理解就是“三个数想老老实实描述三维旋转的所有可能,必然在某个点上崩溃”。这个崩溃点就是万向节锁。

1.3 白开水版一句话定义

好了,给你一句话版本的答案:

万向节锁 = 用三次顺序旋转表示姿态时,第一个旋转轴和第三个旋转轴在某一个姿态下恰好重合,于是三个旋钮只剩两个独立效果,其中一个自由度丢失。

“第一次”和“第三次”的轴为什么会重合?因为第二次旋转把其中一个轴“搬”到了另一个轴的位置上。这是理解整个问题的钥匙。

举个例子。假设你的旋转顺序是“先绕 X 轴转,再绕 Y 轴转,最后绕 Z 轴转”。当中间的 Y 轴旋转 90° 时,原本的 X 轴会被“搬到”Z 轴的位置,或者说第三个旋转轴 Z 与第一次旋转的 X 轴变成了同一条直线。第三次旋转只是在第一次旋转的基础上做重复,三次旋转实际上退化成了“两次旋转”,其中一个自由度从参数层面消失了。

2. 欧拉角的三次旋转,顺序为什么不能乱

2.1 欧拉角是怎么定姿态的

要真正理解万向节锁,你得先把欧拉角本身摸清楚。欧拉角的核心思想是:任何一个姿态,都能通过三次绕坐标轴的旋转拼出来。比如你说的“先抬头 30°,再向左转 45°,最后躺倒 20°”,这就是一组欧拉角。

听起来很简单,但有个隐藏问题:旋转不满足交换律。

日常生活中你也知道,先向左转 90° 再抬头的朝向,和先抬头再向左转 90° 的朝向完全不同。如果你不信,可以站起来试试:先脸朝东,向左转 90° 后面朝北,再抬头看天花板;反过来,先抬头看天花板,再向左转 90°,脸还是朝北但视线还是看向天花板。你看,同样的两个动作,顺序不同,结果就不同。所以欧拉角必须规定一个严格的旋转顺序:哪条轴先转、哪条轴后转,不能含糊。

常见顺序有好几种,比如 Z-X-Z、X-Y-X 这种首尾轴相同的经典欧拉角,还有 Z-Y-X、X-Y-Z 这种三个轴两两都不相同的泰特-布莱恩角。飞机和无人机上常用的偏航-俯仰-滚转,也就是 yaw-pitch-roll,一般对应 Z-Y-X 这类顺序:先绕 Z 轴确定机头朝向,再绕 Y 轴抬头低头,最后绕 X 轴翻身滚转。这里的绕轴“先、后”你已经体会过,不能换。

2.2 内旋、外旋与旋转顺序:一大半混乱从这里来

还有一个让初学者反复踩坑的点:旋转到底是绕世界固定轴转,还是绕物体自身轴转。

如果每次旋转都绕世界坐标系里固定的轴操作,叫外旋。比如“先绕世界 Y 轴转 30°,再绕世界 X 轴转 20°”,这两个轴不会因为物体转动而改变方向。如果第一次旋转之后,第二次绕的是“物体身上的局部轴”,叫内旋。比如飞机执行“先抬头,再左滚转”,左滚转是绕飞机机体纵轴的,机体纵轴已经被前一次抬头带到了新方向。

内旋和外旋并不是两种完全不同的东西,它们之间可以互相转换:内旋按某顺序转,等于外旋把这个顺序反过来转。比如内旋 Z→Y→X,等价于外旋 X→Y→Z。所以你看资料的时候,如果人家用的是内旋你用的是外旋,或者反过来,同一组角度会产生不同的姿态,锁死轴也可能长得不一样。这不是理论矛盾,只是约定不同。

我后面推导用的约定明确一下:采用外旋 Z-Y-X,也就是先绕固定 X 轴转,再绕固定 Y 轴转,最后绕固定 Z 轴转。这个约定在数学上写起来最清晰,锁死的本质结论和外旋内旋没有区别。

2.3 不同顺序的锁死条件对照表

那么具体是哪个姿态触发锁死?这取决于旋转顺序。总结成一张表:

旋转顺序类型触发锁死的中间角重合的轴
Z-X-Z经典欧拉角绕 X 的中间角为 0° 或 180°第一个 Z 轴与第三个 Z 轴重合
Z-Y-X(偏航-俯仰-滚转)泰特-布莱恩角绕 Y 的中间角为 ±90°第一个 Z 轴与最后的 X 轴重合
X-Y-Z泰特-布莱恩角绕 Y 的中间角为 ±90°第一个 X 轴与最后的 Z 轴重合
X-Y-X经典欧拉角绕 Y 的中间角为 0° 或 180°第一个 X 轴与第三个 X 轴重合
Y-X-Y经典欧拉角绕 X 的中间角为 0° 或 180°第一个 Y 轴与第三个 Y 轴重合

你看规律没有?首尾轴相同的经典欧拉角,在中间角为 0° 或 180° 时锁死;首尾轴不同的泰特-布莱恩角,在中间角为 ±90° 时锁死。锁死的本质都一样:中间那一次旋转,把首轴与尾轴送到共线位置。

很多人只记住一句“俯仰角到 90° 锁死”,其实那只是 Z-Y-X 这种飞机顺序的特殊情况。换一种旋转顺序,锁死姿态就变了。这也是为什么你在 A 软件里设置 Euler XYZ,在 B 软件里设置 Euler ZYX,操作同一个物体却感觉旋转行为完全不同。

3. 把 90° 代入矩阵:三个旋钮如何变成一个

3.1 先约定旋转矩阵长什么样

光说“轴重合”还是有点抽象。我建议你跟我一起算一遍,这是当年让我“终于懂了”的时刻。我们不用复杂的工具,纸笔就能算,只是稍微长一点。

假设绕 X、Y、Z 轴的旋转矩阵分别是:

Rx(γ) = [1, 0, 0] [0, cosγ, -sinγ] [0, sinγ, cosγ] Ry(β) = [cosβ, 0, sinβ] [0, 1, 0] [-sinβ, 0, cosβ] Rz(α) = [cosα, -sinα, 0] [sinα, cosα, 0] [0, 0, 1]

公式不复杂,就是三维坐标旋转的标准表达。我们采用外旋 Z-Y-X 顺序:先绕 X 转滚转角 γ,再绕 Y 转俯仰角 β,最后绕 Z 转偏航角 α。合起来的旋转矩阵是:

R = Rz(α) * Ry(β) * Rx(γ)

注意乘法的顺序:最右边是先执行的旋转,因为矩阵作用在列向量上时,向量先经过最右边的矩阵。所以这里是先转 γ,再转 β,最后转 α。

3.2 中间角到 90° 的那一刻

现在把俯仰角 β 设为 90°,也就是 cosβ = 0,sinβ = 1。代入之后,先是中间那段:

Ry(90°) * Rx(γ) = [0, 0, 1] [1, 0, 0] [0, 1, 0] * [0, cosγ, -sinγ] [-1, 0, 0] [0, sinγ, cosγ]

算出来是:

[0, sinγ, cosγ] [0, cosγ, -sinγ] [-1, 0, 0]

然后再左乘 Rz(α),也就是最后的偏航旋转:

R = Rz(α) * Ry(90°) * Rx(γ)

我把结果写出来:

R = [0, sin(γ-α), cos(γ-α)] [0, cos(γ-α), -sin(γ-α)] [-1, 0, 0]

你看到了吗?最终矩阵只取决于 γ - α 这个差值,单独看 α 或 γ 都没有意义。这意味着什么?说明当你把俯仰角固定在 90° 后,无论你给偏航角 α 设多少、给滚转角 γ 设多少,只要它们的差值不变,物体姿态就完全一样。

更直白地说:三个输入旋钮,现在只控制一个数字(γ-α)。你单独拖偏航滑条,姿态变化;单独拖滚转滑条,姿态也在变化,但两者沿着同一个方向转。你的手指在动,姿态的自由度却少了一个。这就是万向节锁的数学真相。

3.3 用代码亲眼看一次

如果你不想手算,直接拿 Python 跑一下就知道了。下面这段代码用 numpy 手动构建矩阵,不依赖任何引擎封装:

import numpy as np def rx(t): return np.array([ [1, 0, 0], [0, np.cos(t), -np.sin(t)], [0, np.sin(t), np.cos(t)] ]) def ry(t): return np.array([ [np.cos(t), 0, np.sin(t)], [0, 1, 0], [-np.sin(t), 0, np.cos(t)] ]) def rz(t): return np.array([ [np.cos(t), -np.sin(t), 0], [np.sin(t), np.cos(t), 0], [0, 0, 1] ]) alpha = np.radians(30) # 偏航 beta = np.radians(90) # 俯仰,锁死角 gamma = np.radians(60) # 滚转 R1 = rz(alpha) @ ry(beta) @ rx(gamma) # 把 alpha 和 gamma 互换一下,但保持差值不变 R2 = rz(0) @ ry(beta) @ rx(np.radians(30)) print(np.round(R1, 4)) print(np.round(R2, 4))

如果你跑一下这两组结果,会发现只要 γ - α 相同,得到的旋转矩阵就完全相同。你在界面上把偏航从 30° 改成 0°、把滚转从 60° 改成 30°,物体纹丝不动。这个现象看起来像 bug,但它是数学系统正常工作时的必然奇点。

3.4 飞机、云台和杯盖:同一个数学模型的三种肉身

理解了上面的矩阵,你再看生活中的例子就通了。

飞机的偏航-俯仰-滚转:当机头垂直向上,也就是俯仰角达到 90° 时,飞机的机体纵轴与世界坐标系的垂直轴重合。这时候你推偏航杆,飞机绕垂直轴转;你压滚转杆,飞机还是绕垂直轴转。两个操作杆给出的是同一种运动。飞行员在特技飞行中接近这个姿态时会非常难受,因为你想让机头往左偏,结果飞机却在原地“旋螺丝”,怎么操纵都不够直接。

手机或摄像头的三轴云台是另一个典型。云台有三个电机,分别管偏航、俯仰、滚转,结构上就是三环嵌套的万向节。当拍摄对象被抬到接近 90° 俯仰时,外环和内环的转动轴会跑到同一条线上,云台就“卡住”了:你让外环电机转,负载只是在原地转圈,没办法独立改变朝向。

甚至你拿一个杯盖在桌上滚也会有类似直觉:杯盖平放时你能让它翻转、滚动、旋转三个方向,但当你把它立起来保持垂直时,想调整某个方向,就会发现自己只能同时动两个轴才能达到目标。物理直觉和数学结构在这里是统一的。

4. 旋转矩阵和四元数:为什么这种表示天然免疫

4.1 旋转矩阵:用“地图”代替“仪表盘”

避开万向节锁的第一个办法,就是不再用三个欧拉角描述姿态,改用旋转矩阵。

旋转矩阵本质上是一张地图:三行三列,分别记录了物体局部坐标系的三个轴在世界坐标系里的方向。不管物体怎么转,这三个方向向量始终彼此垂直、长度为一,所以矩阵永远保持正交性。物体转成什么样,矩阵就是什么样,不存在“三个角度仪表在某个读数组合下失灵”的问题。

用矩阵做旋转合成也很简单:连续旋转就是矩阵连乘,一次旋转就是矩阵乘向量。数值计算稳定,表达唯一,没有奇点。四元数、欧拉角都能轻易转成矩阵,所以矩阵在各个图形库里是最底层的“通用语”。

但矩阵也有缺点:9 个数字只表达 3 个自由度,冗余很多,占内存大,而且直接对两个矩阵做线性插值,中间结果往往不是一个合法的旋转矩阵,会出现拉伸或切变,需要用奇异值分解之类的方法重新正交化。这就导致它不适合做平滑动画插值。

4.2 四元数的直觉:把“轴加角度”打包成四维坐标

四元数才是现代 3D 引擎内部最常用的旋转表示。

它的直觉来源是轴-角。任意三维旋转都能看成“绕某个轴转某个角度”,但轴加角这种五维描述不好直接做合成运算:绕 a 轴转 30° 再接绕 b 轴转 40°,结果不是绕某个轴转 70° 这么简单。四元数干的事,就是把轴和角打包成四个数,然后让“旋转合成”变成简单的四元数乘法。

具体定义是这样:绕单位轴向量 (ux, uy, uz) 旋转角度 θ,对应的四元数是:

q = (cos(θ/2), ux·sin(θ/2), uy·sin(θ/2), uz·sin(θ/2))

四个分量通常写作 (w, x, y, z)。两个旋转先后的合成,不是把欧拉角相加,而是把两个四元数做乘法。四元数乘法看起来式子多,但全是加减乘除,非常适合计算机运算。

为什么四元数没有万向节锁?因为它从来不做“把姿态拆成三次旋转”这件事。它把一个姿态直接编码成一个单位四维向量,这个向量在一个四维单位球面上,每个点都对应一个明确定义的旋转,没有一个“读数组合”会导致两个旋转轴撞在一起。物体转一圈,四元数在球面上画出一条连续曲线,没有撕裂和滑脱。

再说一个有意思的细节:同一个旋转对应两个四元数,q 和 -q。因为它们相差一个整体符号,表示同样的旋转。这个特点叫“双覆盖”,虽然会让人偶尔被符号绕晕,但它恰恰是四元数能避免奇点的原因之一——它用更高维度的空间“包裹”住了旋转群,不存在需要用一个局部坐标覆盖全部流形的那种勉强。

4.3 工程里怎么选:从存储到插值的一笔账

实际工程里三种表示各有分工,我的建议很明确:

  • 给人看、给配置文件用:用欧拉角,直观。但凡是涉及连续旋转、插值、控制反馈,别拿欧拉角直接算。
  • 给向量做变换、和底层图形 API 对接:用旋转矩阵,效率高、稳定。但别拿矩阵直接做关键帧插值。
  • 给内部状态、动画插值、姿态融合:用四元数,平滑、无奇点、存储小。需要向量变换时再转成矩阵。

动画里最常用的操作是球面线性插值(SLERP)。这个操作可以让两个姿态之间的过渡保持匀速且沿最短路径旋转,不会像欧拉角那样出现中间姿态乱转。你拿四元数做,代码很短,效果稳定;你拿欧拉角做,哪怕两个关键帧都没到 90°,也可能出现“明明只转了 20°,中间却甩了一个大圈”的怪事。

一个容易踩的坑:四元数 q 和 -q 表示同一旋转,插值之前如果符号不一致,SLERP 会走“远路”。所以工程上拿到四元数序列后,通常要做符号统一:检查相邻四元数点积,如果为负就翻转其中一个。这个细节不处理好,动画会莫名其妙多转一圈。

5. 工程实战:软件、机械臂、云台里怎么避开这些坑

5.1 3D 软件和游戏引擎里的检查清单

如果你在 Blender、3ds Max、Unity 或 Unreal 里做旋转动画,先记住一个原则:编辑器里看到的角度、输入框里的欧拉角,只是“显示层”;引擎内部的旋转存储往往是四元数或矩阵,动画插值也会走四元数。问题是,很多脚本和美术资源会把欧拉角当成关键帧数据直接导出,插值的时候又回头用欧拉角算,问题就来了。

我建议你按这个清单排查:

  1. 动画关键帧之间有没有出现“旋转绕远路”的现象?如果有,优先切换到四元数旋转模式。Blender 里物体旋转模式改成 Quaternion(WXYZ),Unity 里用 Quaternion.Slerp 做插值。
  2. 旋转顺序有没有统一?Blender 里 Rotation Order 默认是 XYZ 还是 ZYX,不同版本不太一样,团队协作时第一时间约定好,否则导出的模型姿态会“劈叉”。
  3. 避免在接近锁死角的位置调节关键帧数值。美术经常会遇到“我明明只改了 Y 轴,结果 X 和 Z 也一起动了”,这就是到了锁死点附近,角度参数对位姿的映射变得极敏感。此时改关键帧不如重新调姿态,或者直接切换到欧拉角以外的模式编辑。

5.2 机械臂和云台的“物理锁死”与“数学锁死”不是一回事

这里要特别区分一个概念:机械云台和机械臂上的万向节锁,跟我们上面讨论的欧拉角数学奇点,有关联但不完全等同。

物理万向支架由三个嵌套环组成,最容易出现的硬件问题就是环与环转动到共面时,外层电机无法再把负载“带到”新的方向。这是真实的机械自由度丧失,靠软件改表示方法救不了,因为它属于执行机构的结构奇异。解决办法通常是加冗余自由度、限制工作空间、或者在轨迹规划里绕开奇异位形。

但很多软件控制场景里的“锁死”并不是硬件真卡住了,而是控制器还在用欧拉角做反馈计算。比如云台当前俯仰角接近 90° 时,如果控制器计算误差用的是“期望欧拉角减当前欧拉角”,角度差会突然跳变,或者某个轴的控制增益近乎无穷大,导致电机猛抽一下。这就是数学奇点混进了控制回路。

正确的做法是:姿态解算用四元数(比如 IMU/AHRS 输出的就是四元数),控制误差也用四元数相对旋转来表达,只有在显示给操作员看时才转成欧拉角。这样哪怕显示界面上 pitch 显示成 89.9°,内部控制依然平滑稳定。

5.3 我的踩坑笔记:什么时候可以继续用欧拉角

最后分享一点个人经验。很多人一听欧拉角有万向节锁,就恨不得把所有代码里的欧拉角全干掉。其实不必矫枉过正。

如果你的对象只在有限范围内运动,并且明确不会接近锁死角,欧拉角完全够用。比如安防摄像头的俯仰范围只有 -30° 到 +30°,偏航范围 0° 到 360°,用欧拉角当控制输入十分直观。又比如人形角色的头部旋转,脖子活动范围有限,只要在锁死点前后留出安全边界,用欧拉角写逻辑反而更清晰。

真正要警惕的是:你的对象可以自由旋转、姿势是任意朝向时,一定别用欧拉角做内部状态和插值。我现在做任何游戏原型,默认就用四元数存姿态,界面输入用欧拉角,输入完立刻转四元数,所有运算都在四元数下完成,只在最后显示时转回欧拉角。这样一劳永逸,不会突然在某个奇怪角度踩雷。

有一次我在做一个无人机模拟器的相机跟随,发现飞机倒飞时相机姿态会突然“翻”一下。排查了半天,问题不在相机算法,而在旧代码里把四元数转成了欧拉角传给摇杆映射,俯仰跨过 ±90° 后 roll 直接跳了 180°。把这一层欧拉角去掉,换成四元数误差再映射,瞬间就好了。

所以我的最终建议是:欧拉角适合给人看,四元数适合给机器算。万向节锁不是三维世界的诅咒,而是“用三个数强行描述姿态”的代价。理解了这一点,你就不会再被那些看起来玄乎的旋转问题折磨了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询