免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

三维旋转的四种表示法:旋转矩阵、欧拉角、四元数与插值详解

三维旋转的四种表示法:旋转矩阵、欧拉角、四元数与插值详解 1. 项目概述三维旋转的四种“语言”在三维图形、机器人学、惯性导航和游戏开发这些领域里我们每天都在和“旋转”打交道。无论是让一个3D模型转头还是让无人机在空中调整姿态亦或是让游戏角色做出一个流畅的动作其核心都是如何用数学精确地描述和计算这个旋转过程。这就好比我们要描述一个物体的位置可以用笛卡尔坐标、极坐标或者经纬度描述旋转同样有多种“语言”其中最核心、最常用的四种就是旋转矩阵、欧拉角、四元数以及基于四元数的插值。这四种表示法各有各的脾气和适用场景。旋转矩阵像个“全能手”9个数摆开能直接用于坐标变换但冗余且不直观欧拉角则像“导航仪”用三个角度俯仰、偏航、滚转告诉你“头抬多高、脸朝哪边、身体侧倾多少”非常符合人类直觉但有个致命的“万向节死锁”问题四元数则是个“数学奇才”用四个数一个实部三个虚部优雅地解决了死锁并且计算效率高特别适合做连续旋转的合成与插值。而四元数插值尤其是球面线性插值则是实现平滑动画和姿态过渡的“秘密武器”。如果你正在学习计算机图形学、开发机器人控制算法或者想深入理解游戏引擎中的旋转逻辑那么彻底搞懂这四种表示法之间的区别、联系与转换就是绕不开的基本功。我在这上面踩过不少坑比如曾经因为欧拉角的顺序问题导致模型旋转错乱也曾在四元数插值时因为没做归一化而得到奇怪的结果。接下来我就结合这些实际经验把这套“旋转语言”的语法、语义以及它们之间的“翻译”规则给你彻底讲明白。2. 核心概念深度解析与对比2.1 旋转矩阵三维空间的“变换算子”旋转矩阵是一个3x3的正交矩阵。所谓“正交”意味着它的逆矩阵等于它的转置矩阵这保证了用它进行变换时向量的长度和夹角保持不变即刚体旋转。一个绕单位轴 $\mathbf{u} (u_x, u_y, u_z)$ 旋转 $\theta$ 角的旋转矩阵可以通过罗德里格斯旋转公式直接求得$$ \mathbf{R} \mathbf{I} \sin\theta \mathbf{K} (1-\cos\theta)\mathbf{K}^2 $$其中$\mathbf{I}$ 是3x3单位矩阵$\mathbf{K}$ 是由旋转轴 $\mathbf{u}$ 构成的叉乘矩阵 $$ \mathbf{K} \begin{bmatrix} 0 -u_z u_y \ u_z 0 -u_x \ -u_y u_x 0 \end{bmatrix} $$注意罗德里格斯公式非常实用它直接建立了旋转轴-角与旋转矩阵之间的联系。在编程实现时要特别注意旋转轴 $\mathbf{u}$ 必须是单位向量否则计算结果会出错。旋转矩阵的优势非常明显表达无歧义变换直接。给定一个空间点 $\mathbf{v}$左乘旋转矩阵 $\mathbf{R}$ 就能得到旋转后的点 $\mathbf{v’} \mathbf{R}\mathbf{v}$。多个旋转也可以直接通过矩阵乘法连续作用$\mathbf{R}{\text{total}} \mathbf{R}_2 \mathbf{R}_1$注意顺序通常是右乘先发生的旋转。但它的问题也同样突出冗余性9个参数只表达了3个自由度旋转的三个维度存在6个约束条件行/列向量均为单位向量且两两正交。数值误差累积在多次矩阵乘法后由于浮点数精度限制矩阵可能逐渐失去正交性需要定期进行“重新正交化”处理。不直观看着一堆数字很难想象出具体的旋转姿态。2.2 欧拉角符合直觉的“导航参数”欧拉角用三个绕特定坐标轴依次旋转的角度来描述姿态例如航空航天领域常用的“Z-Y-X”顺序即偏航Yaw-俯仰Pitch-滚转Roll。这非常直观偏航角 $\psi$ 描述左右转头俯仰角 $\theta$ 描述抬头低头滚转角 $\phi$ 描述侧倾身体。然而欧拉角有一个著名的万向节死锁问题。当第二个旋转角在Z-Y-X顺序中就是俯仰角 $\theta$为 $\pm90^\circ$ 时第一次旋转和第三次旋转的旋转轴会重合丢失一个自由度。此时系统只能表示绕竖直轴和侧倾轴的旋转而无法表示绕“前后”轴的旋转实际上这个轴与第一个旋转轴重合了。在死锁点附近微小的角度变化会导致第一个和第三个角发生剧烈跳变这在控制系统中是灾难性的。实操心得在游戏开发中我们常用欧拉角给美术或策划设置初始旋转因为好理解。但在内部逻辑和动画插值中一定要尽快转换成四元数或矩阵来运算绝对避免对欧拉角进行直接插值或复杂运算否则死锁和角度跳变会让你抓狂。此外欧拉角还有顺序依赖性。旋转顺序“ZYX”和“XYZ”得到的结果完全不同。在读取或设置欧拉角时必须明确约定顺序不同引擎、不同传感器厂商的默认顺序可能不同这是数据对接时一个常见的坑。2.3 四元数优雅的“数学工具”四元数可以看作复数的扩展形式为 $q w xi yj zk$也常写作 $q [w, (x, y, z)]$其中 $w$ 是实部$(x, y, z)$ 是虚部对应旋转轴。一个表示绕单位轴 $\mathbf{u}$ 旋转 $\theta$ 角的单位四元数为 $$ q \left[\cos\frac{\theta}{2}, \ \sin\frac{\theta}{2} \mathbf{u} \right] $$四元数的核心优势解决万向节死锁因为其描述是基于旋转轴和半角不存在欧拉角那样的奇点。计算高效合成两个旋转四元数乘法只需要16次乘加运算而矩阵乘法需要27次。对于需要每秒处理成千上万次旋转的图形或物理引擎这个优势是巨大的。平滑插值四元数提供了完美的球面线性插值方法可以实现旋转间的最短路径平滑过渡。紧凑且无冗余4个参数表示3个自由度仅有一个单位化约束 $w^2x^2y^2z^21$比矩阵更简洁。它的缺点是不够直观。看到一个四元数[0.707, 0, 0.707, 0]你很难立刻想象出它对应的姿态。2.4 四元数插值平滑动画的“灵魂”线性插值两个向量很简单但直接对两个单位四元数进行线性插值结果得到的四元数不再是单位四元数对应的旋转会不均匀且速度会变化。我们需要在四维单位球面上进行插值这就是球面线性插值。给定两个单位四元数 $q_0$ 和 $q_1$以及插值参数 $t \in [0, 1]$SLERP公式为 $$ \text{Slerp}(q_0, q_1; t) \frac{\sin((1-t)\Omega)}{\sin\Omega} q_0 \frac{\sin(t\Omega)}{\sin\Omega} q_1 $$ 其中 $\Omega \arccos(q_0 \cdot q_1)$ 是 $q_0$ 与 $q_1$ 之间的夹角。重要技巧计算点积 $q_0 \cdot q_1$ 时如果结果为负可以将其中一个四元数取反$q_1 -q_1$。因为四元数 $q$ 和 $-q$ 代表相同的旋转旋转角相差 $2\pi$。这样做可以确保我们总是沿最短路径进行插值。3. 表示法之间的转换与实现细节在实际系统中我们经常需要在不同表示法之间转换。例如从传感器如IMU得到四元数需要转换成欧拉角给人看或者转换成旋转矩阵用于图形渲染。3.1 四元数到旋转矩阵这是非常常用的转换。给定单位四元数 $q [w, x, y, z]$对应的旋转矩阵 $\mathbf{R}$ 为 $$ \mathbf{R} \begin{bmatrix} 1 - 2y^2 - 2z^2 2xy - 2wz 2xz 2wy \ 2xy 2wz 1 - 2x^2 - 2z^2 2yz - 2wx \ 2xz - 2wy 2yz 2wx 1 - 2x^2 - 2y^2 \end{bmatrix} $$实现注意事项在计算前务必确保输入四元数是单位四元数。如果不是先进行归一化$q q / |q|$。矩阵的9个元素中有大量重复计算项如 $2xy$、$2wz$ 等。好的实现会先计算这些中间变量避免重复运算。这个公式推导自四元数旋转向量的公式 $\mathbf{v’} q \mathbf{v} q^{-1}$这里 $\mathbf{v}$ 是纯虚四元数。记住四元数乘法不满足交换律。3.2 旋转矩阵到四元数从矩阵反求四元数需要小心处理。一种稳健的方法是检查矩阵的迹对角线之和 设 $m_{ij}$ 为矩阵元素计算 $$ w \frac{1}{2}\sqrt{1 m_{11} m_{22} m_{33}} \ x \frac{m_{32} - m_{23}}{4w} \ y \frac{m_{13} - m_{31}}{4w} \ z \frac{m_{21} - m_{12}}{4w} $$ 当迹最大时这个方法数值最稳定。更健壮的算法会先比较 $m_{11}, m_{22}, m_{33}, \text{tr}(\mathbf{R})$ 四个值选择最大的一个来作为计算的分母避免除零或精度损失。3.3 四元数到欧拉角以Z-Y-X顺序为例这是需求很大但陷阱也很多的一步。从四元数 $[w, x, y, z]$ 转换到偏航($\psi$)、俯仰($\theta$)、滚转($\phi$)的公式为 $$ \begin{aligned} \phi \text{atan2}(2(wx yz), 1 - 2(x^2 y^2)) \ \theta \arcsin(2(wy - zx)) \ \psi \text{atan2}(2(wz xy), 1 - 2(y^2 z^2)) \end{aligned} $$这里有几个大坑奇异点处理当俯仰角 $\theta$ 接近 $\pm90^\circ$即 $|2(wy - zx)| \approx 1$时$\arcsin$ 函数输入接近±1计算出的 $\theta$ 是可靠的但此时万向节死锁发生$\phi$ 和 $\psi$ 的求解会退化。通常的应对策略是在死锁附近指定一个默认值如 $\psi 0$然后只用 $\phi$ 和 $\theta$或 $\psi$ 和 $\theta$来表示姿态。很多数学库如Eigen的转换函数内部已经做了处理。角度范围$\text{atan2}$ 返回的是 $(-\pi, \pi]$ 的角度$\arcsin$ 返回的是 $[-\pi/2, \pi/2]$。你需要根据应用场景决定是否将角度转换到 $[0, 2\pi)$ 或其他范围。顺序一致性这个公式对应的是“Z-Y-X”外旋顺序即先绕Z轴转$\psi$再绕新的Y轴转$\theta$最后绕新的X轴转$\phi$。务必确认你的欧拉角定义顺序与此一致。3.4 欧拉角到四元数这个转换相对直接就是将三个基本旋转对应的四元数按顺序乘起来。对于Z-Y-X顺序 $$ q_{\text{yaw}} [\cos(\psi/2), 0, 0, \sin(\psi/2)] \ q_{\text{pitch}} [\cos(\theta/2), 0, \sin(\theta/2), 0] \ q_{\text{roll}} [\cos(\phi/2), \sin(\phi/2), 0, 0] \ q q_{\text{roll}} \otimes q_{\text{pitch}} \otimes q_{\text{yaw}} $$ 注意乘法顺序最先发生的旋转放在最右边。4. 姿态解算中的核心应用与问题排查4.1 姿态解算的基本流程在惯性导航或无人机姿态估计中我们常使用IMU惯性测量单元它包含陀螺仪测角速度和加速度计测比力。姿态解算的核心是利用陀螺仪数据进行姿态预测并用加速度计、磁力计等数据进行校正。四元数因其计算效率和无奇点特性成为姿态表示的首选。一个简化的基于互补滤波的四元数姿态更新步骤如下读取传感器数据获取陀螺仪角速度 $[\omega_x, \omega_y, \omega_z]$单位弧度/秒以及加速度计数据用于校正俯仰和滚转。角速度积分根据当前姿态四元数 $q_k$ 和角速度计算下一时刻的预测四元数 $q_{\text{pred}}$。常用一阶龙格-库塔法 $$ \dot{q} \frac{1}{2} q \otimes [0, \omega_x, \omega_y, \omega_z] \ q_{\text{pred}} q_k \dot{q} \cdot \Delta t $$ 然后对 $q_{\text{pred}}$ 进行归一化。加速度计校正将重力向量 $[0, 0, 1]^T$ 用当前预测姿态四元数 $q_{\text{pred}}$ 旋转到机体坐标系得到理论重力向量。与加速度计测量值已归一化做叉乘得到误差向量。这个误差向量反映了预测姿态与“重力告诉我们的水平姿态”之间的偏差。互补滤波融合将角速度积分的预测结果与加速度计校正的误差进行融合。误差以PI控制器的形式反馈到角速度上形成一个闭环系统 $$ \omega_{\text{corrected}} \omega_{\text{gyro}} K_p \times \text{error} K_i \times \int \text{error} , dt $$ 然后用校正后的角速度重新进行步骤2的积分。输出得到融合后的四元数 $q_{k1}$可以根据需要转换为欧拉角或旋转矩阵输出。4.2 常见问题与排查技巧实录在实际工程中你会遇到各种各样奇怪的现象。下面是我总结的一些典型问题及其排查思路现象可能原因排查与解决方法姿态解算发散数值爆炸1. 四元数未归一化。2. 陀螺仪数据未校准存在零偏。3. 积分步长 $\Delta t$ 不稳定或过大。1.强制归一化在每次四元数更新乘法、加法后立即执行 $q q / |q|$。2.校准陀螺仪静止状态下采集数据计算均值作为零偏在读数中减去。3.使用稳定计时确保用于积分的 $\Delta t$ 来自稳定的系统时钟且频率高于陀螺仪带宽。俯仰/滚转角在静止时有缓慢漂移加速度计校正不足或过冲。互补滤波参数 $K_p, K_i$ 设置不当。1.调整滤波参数增大 $K_p$ 可以加强加速度计的修正作用但过大会引入振动噪声增大 $K_i$ 可以消除静态误差但过大会导致超调。通常 $K_p$ 在0.5-2.0$K_i$ 在0.001-0.01量级需要实测调整。2.检查加速度计数据确保加速度计在静止时读数稳定且模长接近1g9.8 m/s²。偏航角Yaw持续漂移无法锁定这是纯惯性解算的固有问题。陀螺仪积分误差累积且没有外部参考如磁力计进行校正。1.引入磁力计使用磁力计测量地磁场方向与已知的当地磁场向量比较为偏航角提供绝对参考。注意硬铁和软铁干扰的校准。2.使用GPS或视觉里程计在更高层次的导航滤波中如卡尔曼滤波用外部绝对位置/速度信息间接约束偏航角漂移。从四元数转换得到的欧拉角发生跳变如从179°跳到-181°这是角度跨越 $\pm180^\circ$ 边界时的正常现象$\text{atan2}$ 函数输出范围是 $(-\pi, \pi]$。进行角度解缠绕。记录上一帧的角度如果当前帧与上一帧的角度差大于 $\pi$则认为发生了一次跨越对当前角度加减 $2\pi$使其与上一帧角度连续。公式current round((prev - current) / (2*PI)) * 2*PI。同一个星敏输入两组四元数结果不一致1. 四元数顺序约定不同可能是标量在前[w, x, y, z]或标量在后[x, y, z, w]。2. 参考坐标系定义不同NED还是ENU机体系是前右下还是前左上。3. 两组四元数本身代表不同的旋转可能来自不同时刻或不同处理算法。1.统一数据格式明确与数据提供方约定四元数的存储顺序。2.统一坐标系明确所有输入输出使用的坐标系定义并在必要时进行转换。这是多传感器融合中最常见的错误来源之一。3.检查时间戳和算法确认两组数据是否对应同一时刻、同一物理量。进行SLERP插值时旋转路径奇怪或抖动1. 输入的四元数不是单位四元数。2. 没有处理四元数的“双覆盖”特性即 $q$ 和 $-q$ 代表同一旋转但点积为负会导致插值走长路径。1.输入归一化插值前对 $q_0$ 和 $q_1$ 分别归一化。2.点积校正计算dot q0·q1。如果dot 0则将q1 -q1且dot -dot。这保证了插值沿最短球面路径进行。欧拉旋转顺序与初始四元数设定不符在初始化系统时如果给了欧拉角但转换成四元数时用的旋转顺序与实际解算或显示时用的顺序不一致。建立严格的转换规范在项目初期就明确并文档化整个系统中唯一使用的欧拉角顺序例如Unity是Z-X-YROS/无人机常用Z-Y-X。所有初始化、显示、转换函数都必须遵循这一规范。写一个全局配置参数来定义这个顺序。4.3 高级话题四元数求导与误差状态卡尔曼滤波在更高级的姿态估计中如用于无人车的ESKF我们通常不直接对四元数进行卡尔曼滤波因为四元数的单位约束会带来麻烦。取而代之的是使用误差状态卡尔曼滤波。其核心思想是名义状态用一个单位四元数 $\mathbf{q}$ 表示最优估计的姿态。误差状态用一个三维小角度向量 $\delta\boldsymbol{\theta}$相当于旋转向量表示名义姿态与真实姿态之间的微小偏差。这个误差状态是无约束的可以直接用在卡尔曼滤波的协方差矩阵中。更新过程卡尔曼滤波在误差状态 $\delta\boldsymbol{\theta}$ 上进行预测和更新。更新后将误差状态作为一个小的旋转四元数 $\delta\mathbf{q} \approx [1, \frac{1}{2}\delta\boldsymbol{\theta}]$然后通过四元数乘法修正名义状态$\mathbf{q} \leftarrow \mathbf{q} \otimes \delta\mathbf{q}$最后重置误差状态为零。这种方法既利用了四元数在全局表示上的优点又避免了在滤波中处理其约束条件是目前高精度姿态估计的主流方法。5. 工具选型与代码实现建议5.1 数学库的选择除非有极致的性能或尺寸要求否则强烈建议使用成熟的数学库而不是自己从头实现所有转换和运算。这能避免大量低级错误。CEigen库是绝对的首选。它提供了高度优化的Quaternion,AngleAxis,EulerAngles等类转换函数齐全且稳定如eulerAngles方法已处理死锁API设计优雅。Armadillo、GLM也是不错的选择。PythonSciPy的scipy.spatial.transform.Rotation类功能非常完整支持所有表示法及其转换并且经过了充分测试。numpy也可以但需要自己组合函数。MATLAB/Simulink内置函数如quat2eul,eul2quat,quat2dcm等非常方便注意指定旋转顺序。游戏引擎Unity 的Quaternion类和 Unreal Engine 的FQuat类都封装了所有常用操作如LookRotation,Slerp,Euler等直接使用即可。个人体会早期项目我曾自己手写四元数运算后来发现边界条件处理如死锁、归一化非常繁琐且易错。切换到 Eigen 后代码简洁性和可靠性大幅提升。除非是学习目的否则不要重复造轮子。5.2 关键代码片段示例这里给出一个用 C (Eigen) 实现的包含异常处理的四元数到欧拉角转换函数Z-Y-X顺序#include Eigen/Geometry #include cmath // 将单位四元数转换为Z-Y-X欧拉角偏航Yaw, 俯仰Pitch, 滚转Roll // 返回角度单位为弧度 Eigen::Vector3d quaternionToEulerZYX(const Eigen::Quaterniond q) { // 确保输入是单位四元数安全起见 Eigen::Quaterniond q_normalized q.normalized(); double w q_normalized.w(); double x q_normalized.x(); double y q_normalized.y(); double z q_normalized.z(); // 计算俯仰角 (pitch) double sinp 2.0 * (w * y - z * x); double pitch; // 处理浮点数精度问题防止asin参数略大于1或小于-1 if (std::fabs(sinp) 1.0) { pitch std::copysign(M_PI / 2.0, sinp); // 使用 copysign 处理正负90度 } else { pitch std::asin(sinp); } // 万向节死锁判断俯仰角接近正负90度 const double epsilon 1e-6; if (std::fabs(pitch) (M_PI / 2.0 - epsilon)) { // 处于死锁区域滚转和偏航退化通常设定偏航为0只计算滚转 double roll std::atan2(2.0 * (w * x y * z), 1.0 - 2.0 * (x * x y * y)); double yaw 0.0; // 或根据实际情况选择其他策略 return Eigen::Vector3d(yaw, pitch, roll); } else { // 正常情况 double roll std::atan2(2.0 * (w * x y * z), 1.0 - 2.0 * (x * x y * y)); double yaw std::atan2(2.0 * (w * z x * y), 1.0 - 2.0 * (y * y z * z)); return Eigen::Vector3d(yaw, pitch, roll); } }这个函数增加了死锁区域的特殊处理避免了在奇异点附近计算不稳定的atan2。在实际应用中你需要根据对死锁时姿态的定义来调整yaw的赋值策略。5.3 性能优化小技巧避免频繁转换在核心循环中尽量保持数据在一种表示法下运算。例如姿态解算循环内部全程使用四元数只在需要输出日志或显示时才转换成欧拉角。预先计算对于固定的旋转如坐标系之间的变换预先计算好对应的四元数或矩阵而不是每次使用时都从欧拉角转换。使用近似函数在一些对精度要求不高的实时图形应用中可以使用快速近似算法来计算三角函数如sin,cos,atan2或者使用查找表。向量化如果使用 Eigen 或 numpy利用其向量化运算能力一次性处理多个四元数如一整条运动轨迹比循环调用标量函数快得多。理解旋转矩阵、欧拉角、四元数和四元数插值就像是掌握了三维旋转世界的语法。矩阵是底层的机器指令欧拉角是人类可读的脚本而四元数则是高效、稳定的编译中间代码。在实际项目中我的策略通常是用欧拉角做初始化和用户交互用四元数做内部运算和存储用矩阵做最终的顶点变换。明确每种“语言”的边界并在它们之间建立正确、稳健的转换桥梁是避免各种灵异现象的关键。最后多写测试用例特别要测试边界情况如俯仰角接近90度、四元数接近非单位化这些地方往往是bug的藏身之所。
返回列表