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。
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 轴转,但意义不同。α 决定「方向」,γ 决定「自转」。
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));
}
eulerAngles() 方法,它内部处理了这些边界情况。
4.6 知识体系总览
下面这张图总结了欧拉角的核心逻辑:
4.7 总结与建议
欧拉角是个好工具,但用对场景很重要。我个人总结了几条原则:
- 人机交互用欧拉角:直观,容易理解
- 内部计算用四元数或旋转矩阵:避免万向锁,插值平滑
- 数据存储用欧拉角:节省空间,三个数就够了
- 通信协议用欧拉角:便于调试和日志记录
你想想看,如果整个系统都用四元数,调试时看到四个数(w, x, y, z),你能一眼看出姿态对不对?反正我不能。但看到 (yaw=30°, pitch=10°, roll=5°),我马上就知道车头朝哪。
所以我的做法是:内部用四元数做所有运算,只在输入输出时转成欧拉角。这样既保证了计算的稳定性,又保留了人机交互的直观性。
公众号:蓝海资料掘金营,微信deep3321