1. 从一次姿态数据对不上的问题说起
前阵子帮一个朋友调无人机飞控日志,他拿着两套代码来问我:为什么同一块IMU出来的原始数据,一套代码换算出的欧拉角在俯仰角到90度的时候会出现横滚角和偏航角突然乱跳,另一套代码就不会。我问他:你先告诉我,这两套代码用的旋转顺序是不是一样的?他愣了一下,翻了半天代码才确认,一套用的是ZYX顺序,另一套是YZX顺序。
这就是欧拉角最典型的坑——表面上大家都说"欧拉角就是绕三个轴转三次嘛",但转哪三个轴、按什么顺序转、在什么坐标系下转,这三件事只要有一个不一致,算出来的角度就完全不是一回事。很多做飞控、机器人、三维渲染、游戏开发的人,第一版程序跑出来的姿态跟预期对不上,十有八九不是数学算错了,而是欧拉角定义的某个细节没对齐。
这篇就打算把欧拉角的定义从头到尾掰开揉碎讲一遍,重点不是背公式,而是搞清楚每条定义背后的几何直觉和工程后果。适合那些人——刚接触姿态解算、刚学机器人学、做三维引擎或传感器融合、甚至只是被万向节锁这个概念吓到过的同学。把这篇文章看完,你会能清楚地回答三个问题:欧拉角到底在转什么、为什么旋转顺序会改变结果、万向节锁到底"锁"的是什么。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 欧拉角的原始定义:三次旋转背后的几何直觉
2.1 从"绕一个轴转"到"绕三个轴转"
欧拉角的起源可以追溯到18世纪欧拉(Leonhard Euler)对刚体旋转的研究。欧拉用一个著名的定理定下了基调:任何刚体在三维空间中的姿态变化,无论看起来多么复杂,都可以分解为绕某个固定轴的一次旋转。但这里有个实际问题——一次旋转虽然数学上简洁,工程上却很难用。
举个例子,你坐在飞机里,机头先朝东再抬头30度,然后向右倾斜20度。如果要用"绕某一个任意轴转一个角度"来描述这个姿态,你得先算出一个综合旋转轴和综合旋转角,完全不直观。而欧拉角的方式是:把这个姿态分解成三个绕标准坐标轴的旋转——先绕z轴转一个偏航角,再绕新的y轴转一个俯仰角,最后绕更新的x轴转一个横滚角。三个角度各管各的,跟飞行员的操作习惯完全一致,这才能在仪表盘上显示、在摇杆上控制。
不过这种"各管各的"是有代价的:它引入了旋转顺序问题。三维旋转不是加法的交换律——先横滚再俯仰,和先俯仰再横滚,最终机头朝向完全不同。这就回到了开头的那个场景:两套代码用了不同的旋转顺序,相同的角速度数据换算出来的角度当然就不一样。
2.2 基元旋转:构成欧拉角的积木
在理解三次旋转如何组合之前,先得明确"绕坐标轴的一次旋转"到底是什么。数学上,这是一个基元旋转矩阵。绕x轴转角度θ的矩阵是:
code复制R_x(θ) = [ 1 0 0 ]
[ 0 cos(θ) -sin(θ) ]
[ 0 sin(θ) cos(θ) ]
注意这里用的是右手定则:右手拇指指向旋转轴正方向,四指弯曲的方向就是正角度方向。所以绕x轴正转时,y轴会转向z轴。
绕y轴和绕z轴同理,形式略有不同:
code复制R_y(θ) = [ cos(θ) 0 sin(θ) ]
[ 0 1 0 ]
[ -sin(θ) 0 cos(θ) ]
R_z(θ) = [ cos(θ) -sin(θ) 0 ]
[ sin(θ) cos(θ) 0 ]
[ 0 0 1 ]
这三个矩阵是整个欧拉角体系的"积木"。任意一个欧拉角定义,本质上就是选三个基元旋转矩阵,按特定顺序相乘。矩阵乘法的顺序就是旋转施加的顺序。
2.3 内旋还是外旋:一个被低估的前提差异
这里必须花大篇幅说一个特别容易被忽略、却是工程中出问题最多的地方:内旋(intrinsic)与外旋(extrinsic)的区别。
外旋(extrinsic)的意思是:三次旋转都绕固定不动的参考坐标系的轴进行。比如说,先绕世界坐标系的z轴转yaw,再绕世界坐标系的y轴转pitch,再绕世界坐标系的x轴转roll。整个过程中参考系是始终不动的。
内旋(intrinsic)的意思是:第一次旋转绕参考系的某个轴转完之后,第二次要绕刚体自身坐标系(也就是旋转后的坐标系)的轴继续转,第三次再绕第二次旋转之后的新坐标系的轴转。
外旋非常好理解,但工程里绝大多数情况用的是内旋。为什么?因为传感器的读数、控制器的指令大多是在自身坐标系下表达的——无人机机头上的摄像头看到的是它自己坐标系的画面,惯性测量单元(IMU)测的是机体坐标系的角速度。你下指令说"向右倾斜",这个"右"是相对机头方向来说的右,不是相对地面坐标系的右。所以从工程直觉来说,内旋更贴近实际控制逻辑。
但这里有个数学上的等价关系:外旋按X-Y-Z顺序,等价于内旋按Z-Y-X顺序。就这么一个简单的等价关系,不知道坑了多少人。因为你在代码里写R = Rz(yaw) * Ry(pitch) * Rx(roll)的时候,如果三个矩阵全部用的是固定坐标系下的基元旋转,这实际上是一种外旋XYZ;如果用的是相对当前坐标系更新的旋转,这是内旋ZYX。两套代码长得一样、乘出来的矩阵却可能是转置关系。
我的建议是:写任何姿态相关代码的第一行注释,必须写明"我用的是内旋还是外旋,旋转顺序是什么,正方向是什么"。这不是形式主义,而是防止三个月后你自己回来读代码时陷入认知混乱。
3. 为什么"旋转顺序"能决定欧拉角的命运
3.1 十二种组合:从经典欧拉角到泰特-布莱恩角
细心的读者会发现,绕三个轴各转一次,轴的排列方式是可以变的。绕x、y、z三个轴,选每个轴一次,排列方式有3! = 6种:XYZ、XZY、YZX、YXZ、ZXY、ZYX。如果允许首尾两个轴相同(即第二个轴和另外两个轴都不同,但第一个和第三个可以重复),那就又多了6种:ZXZ、XYX、YZY、ZYZ、XZX、YXY。一共12种。
理论力学里把第一类(三个轴都不同,如ZYX)称为泰特-布莱恩角(Tait-Bryan angles),就是我们日常说的yaw-pitch-roll;把第二类(首尾同轴,如ZYZ)称为经典欧拉角(Classic Euler angles),常见于理论力学和分子模拟中。
从旋转变换的角度看,这12种没有本质上谁更正确的问题,都足以描述任意三维姿态。但从工程习惯来说,绝大多数机械臂、无人机、惯性导航领域采用的是ZYX(内旋)或XYZ(外旋)这种泰特-布莱恩角,因为三个轴分别对应"偏航-俯仰-横滚"的实际物理自由度,在0度附近小角度时,三个角跟三个轴的直接分量几乎一一对应,控制律设计起来也直观。
3.2 矩阵乘法与"不可交换"的本质
有人会问:我写成R = Rz(yaw) * Ry(pitch) * Rx(roll),为什么它就是ZYX顺序而不是XYZ顺序?关键在于矩阵乘法是不满足交换律的。如果先做roll(绕x轴),再做pitch(绕y轴),最后做yaw(绕z轴),那么从数学上应当写成:
code复制R_total = R_z(yaw) · R_y(pitch) · R_x(roll)
因为矩阵乘法是从右往左作用于向量的,所以最右边的矩阵是最先执行的旋转。这个顺序直接决定了输出的语义。
我见过很多初学者把这个顺序搞反。写了R = Rx(roll) * Ry(pitch) * Rz(yaw)还自认为是ZYX,其实它执行的是先偏航、再俯仰、最后横滚——如果又把原始向量乘在这个矩阵右侧,得到的结果跟你脑子里的"先横滚再俯仰再偏航"完全是两个姿态。调试姿态数据时最典型的现象就是:把传感器固定在桌上,让它只绕一个轴做已知角度转动,代码算出来的角度却是两个轴同时在变。出现这种状况,优先检查两个地方:一是矩阵右乘的旋转顺序;二是内旋外旋是否搞混。
3.3 如何在代码里验证你的欧拉角约定
为了避免"凭感觉写、写错不自知",我分享一个实际中最好用的验证办法:
第一步,把传感器或物体放在初始姿态(通常使参考系与机体系重合)。
第二步,让它先绕自己的x轴正方向转90度。如果代码返回的roll角约等于90度,pitch和yaw约等于0,说明至少你的第一个旋转轴定义正确。
第三步,复位后,先绕x轴转90度,再绕自身y轴转90度,盯着输出看pitch和yaw的变化——注意这里因为第一次旋转已经改变了机体坐标系的朝向,第二次的"自身y轴"在地面系里其实指向了原来的z方向。如果你的代码约定是内旋,输出会跟"后一次旋转作用在更新后的坐标轴上"的结果一致;如果是外旋,输出会跟"始终绕地面系轴"的结果一致。
这步操作能快速暴露约定不一致的问题。做惯性测量单元标定、机械臂运动学正解时,这个10分钟的自检能省下好几天的排错时间。
4. 万向节锁:大家都说它"锁死"了,但它到底锁的是什么
4.1 一次直觉实验:为什么会出现锁死
说到欧拉角,几乎所有人都会提到万向节锁(gimbal lock)。但很多人的理解停留在"俯仰角到90度时,横滚和偏航就锁死了"。到底怎么锁的?
拿一个经典的三环万向节来想:外环控制偏航,中环控制俯仰,内环控制横滚。当俯仰角转到正负90度时,中环会带着内外两个环转到同一个平面附近——此时外环和内环的旋转轴在同一条直线上,你转动外环和转动内环,实际效果变成了"同一个旋转轴的两次不同命名"。物理上自由度并没有消失,你依然可以通过同时调整两个环来达到目标姿态,但你无法单独通过某个环控制某一个方向的转动了。换句话说,独立可控的自由度从3个退化成了2个。
数学上用ZYX顺序来想:当俯仰角β = π/2时,余弦项cos(β)=0,正弦项sin(β)=1,整个旋转矩阵会退化成只与偏航角α和横滚角γ的和或差有关的式子。具体来说,矩阵中跟α和γ独立的项消失了,剩下的项里α和γ永远以α±γ的组合形式出现。这意味着:对于任意给定的最终姿态,存在无穷多组(α, γ)能通过调整它们的和或差来得到同一结果。姿态解算时,很小的姿态扰动就会导致α和γ在数值上剧烈跳变,系统看起来就像"失控"了一样。
4.2 万向节锁的三个常见误区
第一个误区是"锁死意味着物体不能动了"。不对。物体在物理上完全可以继续转动,锁死的是欧拉角表示法的独立性,不是物体的物理约束。就像一个坐标系里两条基向量重合了,你描述点的方式变得冗余,但点本身没有消失。
第二个误区是"只要避开90度就不会遇到问题"。这也不对。任何欧拉角方案都存在奇异点,只是发生的姿态不同。ZYX的奇异点出现在pitch = ±90°,但XYZ、ZYZ、ZXY等都有各自的奇异姿态。用欧拉角做全姿态解算,永远会有一条"临界姿态线"在那里等着你,不是不报,只是时候未到。
第三个误区是"万向节锁是欧拉角唯一的缺点"。其实它还有一个关联问题叫角度跳变。由于角度的周期性,偏航角从179度转到-179度,中间会经历一个180度的跨越。如果你用插值去平滑这段数据,会发现角度不是从179走到-179,而是莫名其妙地先跳到-181度或者是绕了一大圈。这在动画、轨迹规划里特别致命。
4.3 奇异点附近的数值表现与工程对策
奇异点在理论上是一条线,在数值上是一片区域。因为姿态传感器输出总带有噪声,当俯仰角接近90度时,即使俯仰角只偏移0.1度,反解出来的横滚角也可能在几千度的范围内跳变。控制回路看到这种跳变会直接饱和,姿态估计滤波器的协方差也会被污染。
因此实际工程里,几乎所有认真做全姿态系统的人都不会直接用欧拉角作为内部状态。常见的做法是:
- 在无人机和机器人领域,内部用四元数或旋转矩阵做姿态解算和控制,只在人机交互界面需要显示时,才把四元数转成欧拉角。
- 在需要连续转动的机械臂轨迹规划里,避免直接用欧拉角做插值,而是用四元数的球面线性插值(slerp),或者干脆用轴角表示法来规划旋转路径。
- 如果必须在接近奇异点的区域工作,比如某个摄影云台需要俯仰到接近90度,那就要在定义上选择一组"奇异点位于不太可能发生的姿态上"的欧拉角序列。例如把奇异点放到翻滚角为±90度而不是俯仰角为±90度,具体看设备的实际工作范围。
5. 从"绕轴转"到"姿态表示法选型":欧拉角、四元数与旋转矩阵的取舍
5.1 三种表示法在一个三维空间里的生存状态
欧拉角只是描述三维姿态的若干种语言之一。如果把三维姿态比作"说什么语言",那么欧拉角是英语——最普及、最直观,但语法复杂且有很多坑;旋转矩阵是"数学通用语言"——任何系统都能用,计算稳定,但体积大、冗余多;四元数则像"国际音标"——精确、高效、无冗余,但刚接触时很抽象、不直观。
它们的本质关系可以从维数看出来:三维旋转群SO(3)是一个三维流形,所以最少只需要三个参数就能描述任意姿态。欧拉角正是用三个角度,但代价是出现奇异性。旋转矩阵用了9个数,冗余了6个,却天然没有奇异性,代价是每次旋转更新要做9个元素的矩阵乘法,并且要不断做正交化修正来对抗数值漂移。四元数用4个数描述3自由度,冗余了1个,正好能同时避开奇异性和复杂度过高的问题,代价就是不太直观。
5.2 各表示法的典型工程场景选择
我需要给出有明确选型建议的清单,这是实战中反复论证过的经验:
| 表示法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 欧拉角 | 直观、物理意义明确 | 有万向节锁、有角度跳变、有旋转顺序歧义 | 人机交互显示、键盘鼠标遥控、飞行仪表盘、小角度姿态微调 |
| 旋转矩阵 | 无奇异点、形式统一 | 9个参数冗余、需要正交化修正 | 坐标变换链、视觉标定、刚体运动学正解、多坐标系级联 |
| 四元数 | 无奇异点、插值平滑、计算高效 | 理解门槛高、需归一化 | 姿态解算、姿态插值、无人机飞控、卫星姿态控制、三维动画骨骼插值 |
拿无人机来说,控制律内部用的是四元数,因为向量旋转可以用四元数乘法实现,既避免了万向节锁,又比旋转矩阵少几个乘法;地面站显示姿态时则转成欧拉角,因为只有欧拉角能直接在仪表盘上显示姿态。整个系统是"内四元数、外欧拉角"的结构,这个分层思路值得借鉴。
5.3 四元数并没有"取代"欧拉角,反而放大了定义的复杂度
很多人以为学会了四元数就可以把欧拉角扔进垃圾桶。但现实中,四元数经常只是在计算层帮你绕过了奇异点,而到了人机交互层、数据记录层、跨模块通信层,你还是会把四元数转回欧拉角。一旦做这个转换,旋转顺序、内旋外旋、角度范围这些问题一个都躲不掉。
这里尤其要强调一个行业共识:不同厂商、不同SDK、不同文件格式的欧拉角约定是五花八门的。游戏引擎Unity默认用的是ZYX内旋(先绕z,再绕y,最后绕x),且y轴是上方向;Unreal引擎也有自己的约定;ROS中的RPY通常是绕固定坐标轴定义的XYZ外旋;而一些惯性导航设备厂商输出的是ZYX内旋。如果你把一个模块输出的欧拉角直接喂给另一个模块而不做约定转换,轻则显示错误,重则控制发散。
我自己的习惯是:每个模块的接口文档里,必须有类似这样的配置描述:
code复制Rotation order: ZYX
Rotation type: intrinsic
Angle range: yaw [-pi, pi], pitch [-pi/2, pi/2], roll [-pi, pi]
Reference frame: local body frame, x-forward, y-right, z-down
这段描述,一行都不能省。它比任何代码注释都重要。因为姿态数据一旦脱离定义,就只是一堆无意义的数字。
6. 从定义到工程:欧拉角在不同领域的约定差异与排错经验
6.1 航空航天、机器人与游戏引擎的约定差异
航空航天领域的欧拉角使用非常统一:偏航角(yaw)是绕机体z轴,俯仰角(pitch)是绕机体y轴,横滚角(roll)是绕机体x轴。坐标系通常定义为x轴指向机头、y轴指向右翼、z轴垂直向下(北东地系)或垂直向上(东北天系)。旋转顺序几乎都是ZYX内旋。因为飞行员需要的操作顺序是:先对准航向(偏航),再抬头低头(俯仰),最后调整机翼倾斜(横滚)。
机器人领域就没这么统一了。URDF关节运动学里的旋转约定是绕固定坐标系的XYZ欧拉角(即外旋),而一些开源机械臂库用ZYX内旋,ROS机器人操作系统里的RPY消息类型定义明确是固定参考系下的旋转。所以如果你在ROS里看到odom到base_link的四元数转欧拉角,千万别默认它是ZYX内旋,要去看具体的tf文档。
游戏引擎的差异更大。Unity的Transform组件里,欧拉角的旋转顺序是ZXY内旋,而很多教程、网络代码里写的是"先绕X转、再绕Y转、再绕Z转",严格说这叫外旋XYZ,跟Unity的默认顺序完全对不上。做相机跟随、角色动画时,如果代码里直接对欧拉角分量做累加,很容易出现"转了两圈之后角度乱飞"的现象。游戏行业处理这个问题的通用做法是不直接暴露欧拉角给动画插值系统,需要用角度时就从四元数实时换算。
6.2 现场排错:一次欧拉角约定冲突的完整排查链路
我多次在社区里分享过这样一个真实案例,这里再完整复述一遍排查过程,因为它的每一步都有普遍意义。
一个机器人项目里,底盘上装了一个二维激光雷达,为了把激光点云从雷达坐标系变换到机器人基座标系,我需要一组欧拉角来描述雷达相对基座的姿态。供应商给的文档里写的是"rotation:roll=0.02,pitch=0.01,yaw=0.5,单位弧度,RPY order,extrinsic"。我没有细想,直接在代码里用ZYX内旋,然后乘上坐标变换矩阵,结果点云整体歪斜。
排错时我做了四步:
第一步,打印出雷达坐标系原点在基座坐标系下的投影,发现位置没问题,说明平移对、旋转错。
第二步,单独做一个测试程序:在雷达正前方放一个已知位置的目标,看算法算出来的目标在基座系里的坐标。这一步直接暴露出目标位置在y和z方向上明显偏转。
第三步,查供应商给的欧拉角到底是绕哪个参考系。原来文档里写的"extrinsic"是绕固定参考系的XYZ外旋,但我用的是机体坐标系内旋ZYX。
第四步,把旋转矩阵改成外旋XYZ的写法——即R = Rz(yaw) * Ry(pitch) * Rx(roll),并且三个基元矩阵都用固定参考系的版本——点云立刻对齐了。
这个案例最有价值的不是那句"改成外旋就好",而是确认了三个问题:谁定义了这个欧拉角?定义中坐标轴指向是什么?旋转顺序符不符合我的系统默认值?只要这三个问题有一项没对齐,姿态数据就一定是错的。
6.3 跨模块通信时的欧拉角规范化建议
在模块多、团队大的项目里,我建议在中间层强制统一姿态表示。具体做法是:
系统内部姿态统一用四元数传递,只在硬件驱动层(读传感器原始数据)和显示层(给用户看角度)做欧拉角转换。如果某个第三方库或传感器只能输出欧拉角,那么在接入层立即把它转换成四元数,并注释原始约定。
这么设计的逻辑很简单:把"欧拉角约定差异"这个风险隔离在系统的边界处。内部的四元数没有歧义,也不会有内旋外旋之争。等需要显示时,再按照界面需求转换到固定顺序的欧拉角。
7. 回到定义:欧拉角的"定义"为什么比"公式"更值钱
欧拉角的定义,表面上就是"用三个角度描述三维旋转",但深入讲,它就是一组坐标系约定、一组旋转顺序、一组角度取值范围的完整组合。忽略其中任何一环,你手上拿到的角度就只是一堆符号而非实际姿态。
我做姿态相关项目这几年,最大的感受是:公式可以从任何一本教科书里抄,手算示例也能对着书慢慢推,但真正让一个系统稳定跑起来的是每个人对"约定"的敬畏。你写的每一行旋转矩阵、每一个from_euler或to_euler调用、每一次把四元数转成欧拉角打印日志,背后都有一整套坐标系和旋转顺序的潜台词。把这些潜台词明文化,是避免那些"诡异bug"最便宜的方法。
如果你现在刚接触欧拉角,我的建议很直接:抄起手边任一个能显示姿态的小工具(手机陀螺仪App、小飞控地面站、甚至游戏引擎的Debug界面都行),做一次上文中说的"逐轴自检"——转一下x轴看是不是只有roll变,转到接近90度时再看另外两个角是不是变得异常不稳定。用身体和感官把这几个抽象概念"焊"进直觉里,比死记硬背定义有用十倍。
