4. 欧拉角:从直觉到陷阱

欧拉角这东西,我第一次接触时觉得挺直观的。不就是绕三个轴转嘛,谁不会呢?

但真正在项目里用起来,才发现坑不少。尤其是做机械臂运动规划时,一个万向锁就能让规划器直接罢工。今天咱们就把欧拉角彻底聊透。

4.1 什么是欧拉角?

说白了,欧拉角就是用三个角度来描述刚体在空间中的姿态。每次绕一个坐标轴旋转,三次旋转组合起来,就能把物体转到任意方向。

关键问题在于:旋转顺序。绕轴顺序不同,结果完全不同。这就引出了两种最常见的约定:ZYX 和 ZYZ。

4.2 ZYX 欧拉角

ZYX 欧拉角,也叫作 RPY 角(Roll-Pitch-Yaw)。顺序是:先绕 Z 轴转(偏航 Yaw),再绕新的 Y 轴转(俯仰 Pitch),最后绕新的 X 轴转(翻滚 Roll)。

我习惯用 ZYX 做移动机器人的姿态描述。为什么?因为它跟人的直觉最接近:

  • Yaw(偏航):车头朝哪边
  • Pitch(俯仰):车头抬多高
  • Roll(翻滚):车身侧倾多少

旋转矩阵的表达式是这样的(注意顺序是右乘):

R = Rz(yaw) * Ry(pitch) * Rx(roll)

展开后:

R = [
  [cψ cθ,  cψ sθ sφ - sψ cφ,  cψ sθ cφ + sψ sφ],
  [sψ cθ,  sψ sθ sφ + cψ cφ,  sψ sθ cφ - cψ sφ],
  [-sθ,    cθ sφ,              cθ cφ]
]

其中 ψ = yaw, θ = pitch, φ = roll。c 表示 cos,s 表示 sin。

我的小技巧:实际编程时,别手写这个矩阵。用现成的库函数,比如 Eigen 的 AngleAxisd 组合,或者 ROS 的 tf::createQuaternionFromRPY。手写容易写错符号。

4.3 ZYZ 欧拉角

ZYZ 欧拉角在工业机器人里更常见。顺序是:先绕 Z 轴转,再绕新的 Y 轴转,最后再绕新的 Z 轴转。

为什么会有这种看起来重复的约定?因为有些场景下,绕同一个轴转两次反而更方便。比如描述一个机械臂末端执行器的朝向时,ZYZ 能更自然地表达「工具指向」和「工具旋转」。

旋转矩阵:

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

展开后:

R = [
  [cα cβ cγ - sα sγ,  -cα cβ sγ - sα cγ,  cα sβ],
  [sα cβ cγ + cα sγ,  -sα cβ sγ + cα cγ,  sα sβ],
  [-sβ cγ,             sβ sγ,              cβ]
]

α 和 γ 都是绕 Z 轴转,但意义不同。α 决定「方向」,γ 决定「自转」。

关键区别:ZYX 适合描述「载体姿态」(如无人机、车辆),ZYZ 适合描述「工具朝向」(如机械臂末端)。

4.4 万向锁问题

嗯,这里要重点说。万向锁是欧拉角最让人头疼的问题。

什么叫万向锁?简单说:当第二个旋转角达到 ±90° 时,第一个和第三个旋转轴会重合,导致丢失一个自由度。

举个例子,用 ZYX 欧拉角:

  • 当 pitch = 90° 时,yaw 和 roll 的旋转轴都指向同一个方向
  • 此时无论怎么调 yaw 和 roll,效果都一样——只能绕一个轴转
  • 你失去了一个旋转自由度

我曾经在调试一个六轴机械臂时,遇到末端执行器突然「卡住」的情况。查了半天,发现是某个关节角度组合触发了万向锁。规划器算出来的轨迹在奇异点附近剧烈抖动,差点把工件甩出去。

避坑指南:如果你用欧拉角做插值或规划,一定要检测并避开万向锁区域。否则会出现:
  • 姿态突变(角度跳变 180°)
  • 关节速度超限
  • 轨迹不平滑
我建议在规划层就用四元数或旋转矩阵,只在人机交互时转成欧拉角显示。

4.5 欧拉角与旋转矩阵的转换

这部分是基本功。咱们分两个方向说。

4.5.1 从欧拉角到旋转矩阵

上面已经给了 ZYX 和 ZYZ 的公式。实际编程时,我推荐用链式乘法:

// C++ 伪代码 - ZYX 欧拉角转旋转矩阵
Eigen::Matrix3d eulerToMatrix(double yaw, double pitch, double roll) {
    Eigen::AngleAxisd rotZ(yaw, Eigen::Vector3d::UnitZ());
    Eigen::AngleAxisd rotY(pitch, Eigen::Vector3d::UnitY());
    Eigen::AngleAxisd rotX(roll, Eigen::Vector3d::UnitX());
    return (rotZ * rotY * rotX).toRotationMatrix();
}

注意顺序:Z * Y * X。这个顺序对应「先绕 X 转,再绕 Y 转,最后绕 Z 转」的物理过程。

4.5.2 从旋转矩阵到欧拉角

这个稍微麻烦点,因为有多个解。以 ZYX 为例:

// 从旋转矩阵提取 ZYX 欧拉角
double pitch = asin(-R(2, 0));  // 注意符号

// 处理万向锁
if (abs(pitch - M_PI/2) < 1e-6) {
    // 万向锁情况:pitch = 90°
    yaw = 0;  // 任意值
    roll = atan2(R(0, 1), R(0, 2));
} else if (abs(pitch + M_PI/2) < 1e-6) {
    // 万向锁情况:pitch = -90°
    yaw = 0;
    roll = atan2(-R(0, 1), -R(0, 2));
} else {
    yaw = atan2(R(1, 0), R(0, 0));
    roll = atan2(R(2, 1), R(2, 2));
}
经验之谈:提取欧拉角时,一定要处理万向锁分支。很多开源库的早期版本在这里有 bug,导致在某些姿态下返回 NaN。我建议用 Eigen 的 eulerAngles() 方法,它内部处理了这些边界情况。

4.6 知识体系总览

下面这张图总结了欧拉角的核心逻辑:

欧拉角知识体系 欧拉角定义 三次旋转,绕轴顺序是关键 两种常见约定 ZYX (RPY) 与 ZYZ 万向锁问题 第二个角 ±90° 时自由度丢失 ZYX:Yaw-Pitch-Roll 应用:移动机器人、无人机 ZYZ:α-β-γ 应用:工业机械臂末端 原因:旋转轴重合 后果:自由度丢失、轨迹突变 欧拉角 ↔ 旋转矩阵 双向转换 注意:提取时需处理万向锁分支,存在多解 建议:规划用四元数,交互用欧拉角

4.7 总结与建议

欧拉角是个好工具,但用对场景很重要。我个人总结了几条原则:

  1. 人机交互用欧拉角:直观,容易理解
  2. 内部计算用四元数或旋转矩阵:避免万向锁,插值平滑
  3. 数据存储用欧拉角:节省空间,三个数就够了
  4. 通信协议用欧拉角:便于调试和日志记录

你想想看,如果整个系统都用四元数,调试时看到四个数(w, x, y, z),你能一眼看出姿态对不对?反正我不能。但看到 (yaw=30°, pitch=10°, roll=5°),我马上就知道车头朝哪。

所以我的做法是:内部用四元数做所有运算,只在输入输出时转成欧拉角。这样既保证了计算的稳定性,又保留了人机交互的直观性。

一句话总结:欧拉角是给人类看的,四元数是给计算机算的。别搞反了。

公众号:蓝海资料掘金营,微信deep3321