一、先从最直观的问题说起:怎么描述一个物体"转了多少"

想象你手里拿着一架玩具飞机,飞机在空间里的朝向(姿态)可以用三个互相垂直的坐标轴 X,Y,ZX, Y, ZX,Y,Z 来表示——这三根轴一起固定在飞机机身上,随着飞机一起转动,叫做"机体坐标系"。
现在问题来了:怎么用几个简单的数字,把飞机从"最初朝向"转到"现在朝向"这件事描述清楚?
欧拉(Euler)给出的办法是:把一次任意复杂的三维旋转,拆成三次绕坐标轴的简单旋转。每次旋转只绕一根轴转过一个角度,转三次以后,就能把物体转到空间中任意想要的朝向。这三个角度,就叫做欧拉角
这个想法非常朴素:三维空间中一个物体的朝向本来就有三个自由度(可以绕 XXX 转、绕 YYY 转、绕 ZZZ 转),所以用三个角度、三次旋转,理论上刚好能表示出所有可能的朝向。

二、旋转顺序可以随便选,但选了就不能变

图片开头这句话是理解欧拉角最关键的一句:

可以自由选择三次旋转的顺序:XZX,XYX,XYZ,ZYX…XZX, XYX, XYZ, ZYX \dotsXZX,XYX,XYZ,ZYX
但是不同旋转顺序结果互不相同:旋转群不满足交换律
拆开来看两层意思:
第一层:顺序是可以自由约定的。 你可以规定"先绕 XXX 转,再绕 ZZZ 转,再绕 XXX 转"(写作 XZXXZXXZX),也可以规定"先绕 XXX 转,再绕 YYY 转,再绕 ZZZ 转"(写作 XYZXYZXYZ)。因为每次转完之后坐标轴本身也跟着动了,所以到底选哪三根轴、按什么顺序转,一共有很多种合法的组合方式,都能达到"三次旋转覆盖三个自由度"的效果。
第二层:但是顺序一旦选定,就必须固定下来,不能中途换,也不能颠倒。 因为矩阵乘法(也就是旋转的复合)不满足交换律:先转 AAA 再转 BBB,和先转 BBB 再转 AAA,通常会得到两个完全不同的最终姿态。这一点在下面的 PDF 图(见图2)中有非常直观的例子:同样是"绕 XXX90∘90^\circ90" 加上"绕 ZZZ90∘90^\circ90",仅仅是顺序换了一下,最终朝向就从 +X+X+X 方向变成了 −Y-YY 方向,完全不同。
这也是为什么,只要一提到"欧拉角",就必须同时说清楚"是哪种旋转顺序"——脱离了顺序单独说"欧拉角是 (30°, 45°, 60°)" 是没有意义的,必须说成"按 ZYXZYXZYX 顺序的欧拉角是 (30°, 45°, 60°)" 才是完整的描述。

在这里插入图片描述

在这里插入图片描述

三、两大类欧拉角:Proper Euler Angles 与 Tait–Bryan Angles

图片里的表格其实分成了左右两大类,这是欧拉角里非常重要的一个分类标准:第一根轴和第三根轴是不是同一根轴

  • Proper Euler angles(本征欧拉角):第一次和第三次旋转绕的是同一根轴,比如 X1Z2X3X_1 Z_2 X_3X1Z2X3(先绕 XXX,再绕 ZZZ,最后又绕 XXX)、Z1Y2Z3Z_1 Y_2 Z_3Z1Y2Z3(先绕 ZZZ,再绕 YYY,最后又绕 ZZZ)。一共有 6 种组合:XZX,XYX,YXY,YZY,ZYZ,ZXZXZX, XYX, YXY, YZY, ZYZ, ZXZXZX,XYX,YXY,YZY,ZYZ,ZXZ
  • Tait–Bryan angles(卡尔丹角 / 泰特-布莱恩角):三次旋转分别绕三根不同的轴,比如 X1Z2Y3X_1 Z_2 Y_3X1Z2Y3X1Y2Z3X_1 Y_2 Z_3X1Y2Z3。一共也是 6 种组合:XZY,XYZ,YXZ,YZX,ZYX,ZXYXZY, XYZ, YXZ, YZX, ZYX, ZXYXZY,XYZ,YXZ,YZX,ZYX,ZXY。飞行器姿态里常说的"横滚-俯仰-偏航"(Roll-Pitch-Yaw)用的就是这一类。
    两类角在应用上各有偏好:Proper Euler angles 常见于陀螺仪、机械臂关节这类"轴对称"结构;Tait–Bryan angles 则更常见于飞行器、航天器的姿态描述(比如常说的横滚角、俯仰角、偏航角)。

四、下面这张表在讲什么:从"三个角度"算出"旋转矩阵"

图中第一张大表,本质上是在回答这样一个问题:

已知三个欧拉角 (θ1,θ2,θ3)(\theta_1,\theta_2,\theta_3)(θ1,θ2,θ3)(表中记作对应的正弦 s1,s2,s3s_1,s_2,s_3s1,s2,s3 和余弦 c1,c2,c3c_1,c_2,c_3c1,c2,c3),如果按某种指定顺序(比如 Z1Y2X3Z_1Y_2X_3Z1Y2X3)依次旋转,那么最终得到的旋转矩阵 RRR 长什么样?
这里做一个符号约定,后面统一使用:
c1=cos⁡θ1,s1=sin⁡θ1,c2=cos⁡θ2,s2=sin⁡θ2,c3=cos⁡θ3,s3=sin⁡θ3 c_1=\cos\theta_1,\quad s_1=\sin\theta_1,\quad c_2=\cos\theta_2,\quad s_2=\sin\theta_2,\quad c_3=\cos\theta_3,\quad s_3=\sin\theta_3 c1=cosθ1,s1=sinθ1,c2=cosθ2,s2=sinθ2,c3=cosθ3,s3=sinθ3
其中 θ1\theta_1θ1 是第一次旋转的角度,θ2\theta_2θ2 是第二次,θ3\theta_3θ3 是第三次。

4.1 矩阵是怎么来的:三个基础旋转矩阵相乘

无论是哪一种顺序,做法都完全一样,分三步:
第一步,写出绕单一坐标轴旋转的三个"基础旋转矩阵"(这是一切的地基,请务必先记住这三个):
RX(θ)=[1000cos⁡θ−sin⁡θ0sin⁡θcos⁡θ],RY(θ)=[cos⁡θ0sin⁡θ010−sin⁡θ0cos⁡θ],RZ(θ)=[cos⁡θ−sin⁡θ0sin⁡θcos⁡θ0001] R_X(\theta)=\begin{bmatrix}1&0&0\\0&\cos\theta&-\sin\theta\\0&\sin\theta&\cos\theta\end{bmatrix},\quad R_Y(\theta)=\begin{bmatrix}\cos\theta&0&\sin\theta\\0&1&0\\-\sin\theta&0&\cos\theta\end{bmatrix},\quad R_Z(\theta)=\begin{bmatrix}\cos\theta&-\sin\theta&0\\\sin\theta&\cos\theta&0\\0&0&1\end{bmatrix} RX(θ)= 1000cosθsinθ0sinθcosθ ,RY(θ)= cosθ0sinθ010sinθ0cosθ ,RZ(θ)= cosθsinθ0sinθcosθ0001
规律很好记:转哪根轴,对应那一行那一列就保持"单位矩阵"的样子(比如绕 XXX 转,第一行第一列就是 1,0,01,0,01,0,0),剩下的 2×22\times22×2 子块就是标准的二维旋转矩阵 [cos⁡−sin⁡sin⁡cos⁡]\begin{bmatrix}\cos&-\sin\\\sin&\cos\end{bmatrix}[cossinsincos],只是嵌入的位置、正负号会因为坐标轴顺规(右手系)略有不同。
第二步,把三次旋转依次"复合"起来。注意复合的方向:如果旋转顺序是先绕 ZZZ(用 θ1\theta_1θ1),再绕新的 YYY(用 θ2\theta_2θ2),再绕新的 XXX(用 θ3\theta_3θ3),那么写成矩阵乘法时,最先发生的旋转矩阵写在最右边
R=RZ(θ1) RY(θ2) RX(θ3) R = R_Z(\theta_1)\,R_Y(\theta_2)\,R_X(\theta_3) R=RZ(θ1)RY(θ2)RX(θ3)
这是因为矩阵作用在列向量上是从右往左依次生效的,最右边的矩阵先作用在向量上,等价于"最先发生"。这一点新手很容易搞反,一定要记牢。
第三步,把上面三个矩阵老老实实地按矩阵乘法展开、合并同类项,用 c1,s1,c2,s2,c3,s3c_1,s_1,c_2,s_2,c_3,s_3c1,s1,c2,s2,c3,s3 表示出来,就得到了表格里那一大块 3×33\times33×3 矩阵。表格已经把 12 种组合(6种 Proper Euler + 6种 Tait-Bryan)全部算好了,你只需要根据自己用的旋转顺序,直接去表里查对应的矩阵,不需要每次都重新推导。
举例:查表格中的 Z1Y2X3Z_1Y_2X_3Z1Y2X3(属于 Tait–Bryan 一类),矩阵是:
R=[c1c2c1s2s3−c3s1s1s3+c1c3s2c2s1c1c3+s1s2s3c3s1s2−c1s3−s2c2s3c2c3] R=\begin{bmatrix} c_1c_2 & c_1s_2s_3-c_3s_1 & s_1s_3+c_1c_3s_2\\ c_2s_1 & c_1c_3+s_1s_2s_3 & c_3s_1s_2-c_1s_3\\ -s_2 & c_2s_3 & c_2c_3 \end{bmatrix} R= c1c2c2s1s2c1s2s3c3s1c1c3+s1s2s3c2s3s1s3+c1c3s2c3s1s2c1s3c2c3
这正是航空航天里最常用的"偏航-俯仰-横滚"(yaw-pitch-roll,先绕机体 ZZZ 偏航,再绕 YYY 俯仰,再绕 XXX 横滚)对应的旋转矩阵。

4.2 图1解读:旋转的动态过程

前面提到的整个"三步复合",可以对照本文档配套的 PDF 文件中的图1 来看(位置提示:请在这里插入"图1")。图1画的是 Z1Y2X3Z_1Y_2X_3Z1Y2X3 这一种旋转顺序(对应第一步绕 Z0Z_0Z0、第二步绕新的 Y1Y_1Y1、第三步绕新的 X2X_2X2)的完整动态过程:

  • 第一步:坐标系还是原始状态 X0,Y0,Z0X_0,Y_0,Z_0X0,Y0,Z0
  • 第二步:绕 Z0Z_0Z0 轴转过 α\alphaα 角后,X,YX,YX,Y 轴变成了新的 X1,Y1X_1,Y_1X1,Y1,但 ZZZ 轴没变(Z1=Z0Z_1=Z_0Z1=Z0);
  • 第三步:绕新的 Y1Y_1Y1 轴转过 β\betaβ 角后,X,ZX,ZX,Z 轴又变成了新的 X2,Z2X_2,Z_2X2,Z2,而 YYY 轴不变(Y2=Y1Y_2=Y_1Y2=Y1);
  • 第四步:绕最新的 X2X_2X2 轴转过 γ\gammaγ 角后,得到最终坐标系 X3,Y3,Z3X_3,Y_3,Z_3X3,Y3,Z3
    关键点在于:每一次旋转都是绕着"上一步转完之后的新轴"来转的,而不是一直绕着最初、固定不动的世界坐标轴转。这种方式叫做"内旋"(intrinsic rotation),也是图中默认表示的方式(这与另一种"外旋"/extrinsic 方式——每次都绕固定不动的世界坐标轴转——是欧拉角里另一个容易混淆的知识点,两者能通过顺序颠倒互相转换,这里先不展开)。

五、为什么旋转顺序不能乱换:一个具体的例子

这一点光看文字公式不够直观,最好的办法是拿两个具体的 90∘90^\circ90 旋转来对比一下,对应本文档配套 PDF 中的图2位置提示:请在这里插入"图2")。
假设物体一开始朝向 +Z+Z+Z 方向。做两组实验:

顺序 第一次旋转 第二次旋转 最终朝向
顺序一 XXX 轴转 90∘90^\circ90 ZZZ 轴转 90∘90^\circ90 +X+X+X
顺序二 ZZZ 轴转 90∘90^\circ90 XXX 轴转 90∘90^\circ90 −Y-YY

两组实验用的是完全相同的两个旋转动作,仅仅是先后顺序换了一下,最终朝向却截然不同(+X+X+X 对比 −Y-YY)。这就是"旋转群不满足交换律"最直观的体现:用数学语言写出来就是
RZ(90∘) RX(90∘)  ≠  RX(90∘) RZ(90∘) R_Z(90^\circ)\,R_X(90^\circ) \;\neq\; R_X(90^\circ)\,R_Z(90^\circ) RZ(90)RX(90)=RX(90)RZ(90)
正因为如此,凡是使用欧拉角的场合(游戏引擎、机器人学、航天姿态控制、CAD软件……),第一件事永远是先把旋转顺序的约定搞清楚,否则同一组角度数字,放到不同软件、不同顺规下,会指向完全不同的姿态,这也是欧拉角在工程实践中最容易踩坑的地方。

六、右边小表格在讲什么:反过来,从旋转矩阵"反推"欧拉角

图片右边那张小表格,做的是和第4节相反的事情:

已知一个旋转矩阵 RRR(矩阵里的每个元素记作 RijR_{ij}Rij,表示第 iii 行第 jjj 列),如果知道这个矩阵是按某种指定顺序(比如 X1Z2Y3X_1Z_2Y_3X1Z2Y3)旋转得到的,怎么把三个欧拉角 α,β,γ\alpha,\beta,\gammaα,β,γ 反过来算出来?
这个过程叫做"欧拉角提取"(Euler angle extraction),在实际工程中同样重要——比如传感器直接给出的姿态往往是旋转矩阵或四元数形式,但人更容易理解"绕某轴转了多少度",所以经常需要把矩阵转换回欧拉角。
以表格中的 X1Z2Y3X_1Z_2Y_3X1Z2Y3(Proper Euler 分类)为例:
α=arctan⁡ ⁣(R31R21),β=arccos⁡(R11),γ=arctan⁡ ⁣(R13−R12) \alpha=\arctan\!\left(\frac{R_{31}}{R_{21}}\right),\qquad \beta=\arccos(R_{11}),\qquad \gamma=\arctan\!\left(\frac{R_{13}}{-R_{12}}\right) α=arctan(R21R31),β=arccos(R11),γ=arctan(R12R13)
以及 Tait–Bryan 分类中的 X1Z2Y3X_1Z_2Y_3X1Z2Y3
α=arctan⁡ ⁣(R32R22),β=arcsin⁡(−R12),γ=arctan⁡ ⁣(R13R11) \alpha=\arctan\!\left(\frac{R_{32}}{R_{22}}\right),\qquad \beta=\arcsin(-R_{12}),\qquad \gamma=\arctan\!\left(\frac{R_{13}}{R_{11}}\right) α=arctan(R22R32),β=arcsin(R12),γ=arctan(R11R13)
这些公式是怎么来的? 思路其实很朴素:第4节里已经把每一种旋转顺序对应的矩阵,用 c1,s1,c2,s2,c3,s3c_1,s_1,c_2,s_2,c_3,s_3c1,s1,c2,s2,c3,s3 展开写出来了。既然矩阵每个位置的表达式已知,那么反过来,只要在矩阵里找到某个位置恰好等于单独的 sin⁡θ2\sin\theta_2sinθ2cos⁡θ2\cos\theta_2cosθ2(这样的位置通常只有一个,因为 θ2\theta_2θ2 是"中间那次旋转",在矩阵展开式里最容易单独出现),就可以先用 arcsin⁡\arcsinarcsinarccos⁡\arccosarccos 解出中间角 β\betaβ(即 θ2\theta_2θ2);再找两个只包含 θ1\theta_1θ1 或只包含 θ3\theta_3θ3 的比值组合(通常是形如 sin⁡θ1cos⁡θ2cos⁡θ1cos⁡θ2\dfrac{\sin\theta_1\cos\theta_2}{\cos\theta_1\cos\theta_2}cosθ1cosθ2sinθ1cosθ2 这样一约分,cos⁡θ2\cos\theta_2cosθ2 就消掉了,只剩 tan⁡θ1\tan\theta_1tanθ1),用 arctan⁡\arctanarctan 分别解出 α\alphaα(即 θ1\theta_1θ1)和 γ\gammaγ(即 θ3\theta_3θ3)。
这里有一个所有欧拉角方法都逃不掉的"坑":当中间角 β\betaβ 恰好等于 ±90∘\pm90^\circ±90 时(比如 cos⁡θ2=0\cos\theta_2=0cosθ2=0),第一次和第三次旋转的轴会重合到一起,导致丢失一个自由度,此时 α\alphaαγ\gammaγ 无法被唯一确定——这就是著名的"万向节死锁"(Gimbal Lock)问题,是欧拉角这种表示方法与生俱来的缺陷(这也是为什么工程上很多时候更偏爱四元数来表示旋转,但四元数不在这张图的讨论范围内,这里只做一个提醒)。

七、内容小结

  • 欧拉角把一次任意的三维旋转,拆成三次绕坐标轴的简单旋转,用三个角度描述姿态。
  • 旋转顺序可以在 12 种组合(6 种 Proper Euler + 6 种 Tait–Bryan)里自由选择,但一旦选定必须严格固定,因为旋转的复合不满足交换律,顺序不同、结果不同。
  • Proper Euler angles 的特点是第一轴和第三轴相同(如 ZYZZYZZYZ);Tait–Bryan angles 的特点是三轴互不相同(如 ZYXZYXZYX)。
  • 从欧拉角推旋转矩阵:把三个基础旋转矩阵按"从右到左"的顺序依次相乘、展开合并即可,最先发生的旋转写在最右边。
  • 从旋转矩阵反推欧拉角:先用矩阵中单独出现 sin⁡/cos⁡(θ2)\sin/\cos(\theta_2)sin/cos(θ2) 的位置解出中间角,再用比值消去 cos⁡θ2\cos\theta_2cosθ2、通过 arctan⁡\arctanarctan 解出另外两个角;当中间角为 ±90∘\pm90^\circ±90 时会遇到万向节死锁问题。
    (本文档配套的 PDF 文件包含两张 TikZ 绘制的示意图:图1展示了 Z1Y2X3Z_1Y_2X_3Z1Y2X3 顺序下坐标系三次旋转的完整过程;图2用具体的 90∘90^\circ90 旋转对比,直观展示了旋转顺序不满足交换律这一核心结论。)

欧拉角(Euler Angle)从零理解

1. 为什么需要欧拉角

在三维空间里,一个物体(比如飞机、机械臂末端、摄像机)不仅有位置,还有姿态(朝向)。姿态的数学本质是一个旋转矩阵 RRR,它是一个 3×33\times 33×3 的矩阵,一共有 9 个数字,但由于旋转矩阵必须满足"正交且行列式为1"这个约束,真正独立的自由度只有 3 个
欧拉角就是用三个角度(记作 ψ,θ,ϕ\psi, \theta, \phiψ,θ,ϕ,分别对应偏航 yaw、俯仰 pitch、横滚 roll,或者不同教材里叫 α,β,γ\alpha,\beta,\gammaα,β,γ)来描述这三个自由度。它的思路很朴素:把一次复杂的三维旋转,拆成三次绕单一坐标轴的简单旋转,依次执行,最后叠加起来就等于任意一个三维旋转。
拆分的方式不是唯一的,图中展示的是最常见的两种理解方式——内旋外旋。它们用的是完全相同的三个角度,但对"绕哪根轴转"的理解不同,所以最终算出来的旋转矩阵的乘法顺序是相反的。这正是这份讲义要讲清楚的核心问题。

2. 预备知识:单轴旋转矩阵

在讲内旋外旋之前,先复习一下"绕一根固定坐标轴转某个角度"对应的矩阵长什么样。设某个向量在旋转前坐标是 vvv,旋转后坐标是 v′v'v,则 v′=Rvv' = R vv=Rv
zzz 轴转角度 ψ\psiψx,yx,yx,y 平面内旋转):
Rz(ψ)=[cos⁡ψ−sin⁡ψ0sin⁡ψcos⁡ψ0001] R_z(\psi) = \begin{bmatrix} \cos\psi & -\sin\psi & 0 \\ \sin\psi & \cos\psi & 0 \\ 0 & 0 & 1 \end{bmatrix} Rz(ψ)= cosψsinψ0sinψcosψ0001
yyy 轴转角度 θ\thetaθ
Ry(θ)=[cos⁡θ0sin⁡θ010−sin⁡θ0cos⁡θ] R_y(\theta) = \begin{bmatrix} \cos\theta & 0 & \sin\theta \\ 0 & 1 & 0 \\ -\sin\theta & 0 & \cos\theta \end{bmatrix} Ry(θ)= cosθ0sinθ010sinθ0cosθ
xxx 轴转角度 ϕ\phiϕ
Rx(ϕ)=[1000cos⁡ϕ−sin⁡ϕ0sin⁡ϕcos⁡ϕ] R_x(\phi) = \begin{bmatrix} 1 & 0 & 0 \\ 0 & \cos\phi & -\sin\phi \\ 0 & \sin\phi & \cos\phi \end{bmatrix} Rx(ϕ)= 1000cosϕsinϕ0sinϕcosϕ
这三个矩阵是所有后续推导的"零件",记住它们的含义(绕哪个轴转、正方向按右手定则),后面就只是"怎么把零件组装起来"的问题。

3. 内旋(intrinsic rotations):绕"不断更新的"动坐标轴转

看原图的第一行。整个过程分三步,每一步转完之后,坐标轴都会跟着物体一起转动,下一步是绕新的坐标轴转的:

  • 第一步:绕世界坐标系的 zzz 轴转 ψ\psiψ,得到新的坐标系 (x′,y′,z′)(x', y', z')(x,y,z)(此时 z′=zz'=zz=z,因为是绕 zzz 转的,zzz 轴本身不动)。
  • 第二步:绕新坐标系自己的 y′y'y 轴(不是原来的 yyy 轴!)转 θ\thetaθ,得到 (x′′,y′′,z′′)(x'', y'', z'')(x′′,y′′,z′′)
  • 第三步:绕最新坐标系自己的 x′′x''x′′ 轴(不是原来的 xxx 轴!)转 ϕ\phiϕ,得到最终坐标系 (xf,yf,zf)(x_f, y_f, z_f)(xf,yf,zf)
    核心特征:每次旋转的转轴都"跟着物体走",是"动坐标系"的轴,所以叫内旋。图中的 x′,y′,z′x', y', z'x,y,z 用不同颜色(蓝→橙→绿)画出来,就是在强调"坐标轴每一步都在换"。

3.1 内旋矩阵怎么推导

推导内旋矩阵的关键技巧是:把每一步的旋转都换算回"世界坐标系"里表示,这样才能把三步合并成一个矩阵乘法。
设向量在世界坐标系下的坐标是 vvv
第一步:绕世界 zzz 轴转 ψ\psiψ,这是标准的 Rz(ψ)R_z(\psi)Rz(ψ),直接得到:
v1=Rz(ψ) vv_1 = R_z(\psi)\, vv1=Rz(ψ)v
第二步:绕新的 y′y'y 轴转 θ\thetaθ。这根轴不是世界坐标系的 yyy 轴,而是"zzz 轴转过 ψ\psiψ 之后"的 yyy 轴。数学上可以证明:绕一个"已经被旋转过的轴"做旋转,等价于——先把当前坐标"变换回旋转前的样子"(乘 Rz(ψ)−1R_z(\psi)^{-1}Rz(ψ)1),绕标准 yyy 轴转 θ\thetaθ(乘 Ry(θ)R_y(\theta)Ry(θ)),再变换回来(乘 Rz(ψ)R_z(\psi)Rz(ψ))。写成公式:
v2=Rz(ψ) Ry(θ) Rz(ψ)−1⋅v1=Rz(ψ) Ry(θ) Rz(ψ)−1⋅Rz(ψ) v=Rz(ψ) Ry(θ) vv_2 = R_z(\psi)\, R_y(\theta)\, R_z(\psi)^{-1} \cdot v_1 = R_z(\psi)\, R_y(\theta)\, R_z(\psi)^{-1} \cdot R_z(\psi)\, v = R_z(\psi)\, R_y(\theta)\, vv2=Rz(ψ)Ry(θ)Rz(ψ)1v1=Rz(ψ)Ry(θ)Rz(ψ)1Rz(ψ)v=Rz(ψ)Ry(θ)v
注意中间的 Rz(ψ)−1Rz(ψ)R_z(\psi)^{-1} R_z(\psi)Rz(ψ)1Rz(ψ) 抵消了,结果非常简洁:只是在原来的矩阵右边多乘了一个 Ry(θ)R_y(\theta)Ry(θ)
第三步同理,绕最新的 x′′x''x′′ 轴转 ϕ\phiϕ,经过同样的抵消,结果是在右边再乘一个 Rx(ϕ)R_x(\phi)Rx(ϕ)
v3=Rz(ψ) Ry(θ) Rx(ϕ) vv_3 = R_z(\psi)\, R_y(\theta)\, R_x(\phi)\, vv3=Rz(ψ)Ry(θ)Rx(ϕ)v
于是内旋的总旋转矩阵是:
R内旋=Rz(ψ) Ry(θ) Rx(ϕ) \boxed{R_{\text{内旋}} = R_z(\psi)\, R_y(\theta)\, R_x(\phi)} R内旋=Rz(ψ)Ry(θ)Rx(ϕ)
记忆技巧:内旋——矩阵按"转动发生的先后顺序",从左到右排列,依次右乘。第一个转的 ψ\psiψ 对应的矩阵在最左边。

4. 外旋(extrinsic rotations):绕"始终不变的"世界坐标轴转

看原图第二行。同样是三步,但这次每一步都是绕最初就固定好的世界坐标轴转,坐标轴本身从头到尾都不动:

  • 第一步:绕固定的 zzz 轴转 ψ\psiψ
  • 第二步:绕固定的 yyy 轴(还是最初的 yyy,不是转过的)转 θ\thetaθ
  • 第三步:绕固定的 xxx 轴(还是最初的 xxx)转 ϕ\phiϕ
    图中三次旋转都画在同一套黑色坐标轴 (x,y,z)(x,y,z)(x,y,z) 上,物体(橙色/绿色箭头)自己转动,坐标系背景不变,这就是外旋和内旋在图上最直观的区别。

4.1 外旋矩阵怎么推导

这个推导比内旋简单,因为不需要"变换回去再变换回来"的技巧——外旋的轴本来就是固定的世界坐标轴,直接按旋转发生的先后顺序,把矩阵左乘上去即可(这是旋转矩阵作用在向量上的基本规则:后发生的旋转,矩阵写在左边)。
第一步:v1=Rz(ψ) vv_1 = R_z(\psi)\, vv1=Rz(ψ)v
第二步(绕固定 yyy 轴):v2=Ry(θ) v1=Ry(θ) Rz(ψ) vv_2 = R_y(\theta)\, v_1 = R_y(\theta)\, R_z(\psi)\, vv2=Ry(θ)v1=Ry(θ)Rz(ψ)v
第三步(绕固定 xxx 轴):v3=Rx(ϕ) v2=Rx(ϕ) Ry(θ) Rz(ψ) vv_3 = R_x(\phi)\, v_2 = R_x(\phi)\, R_y(\theta)\, R_z(\psi)\, vv3=Rx(ϕ)v2=Rx(ϕ)Ry(θ)Rz(ψ)v
于是外旋的总旋转矩阵是:
R外旋=Rx(ϕ) Ry(θ) Rz(ψ) \boxed{R_{\text{外旋}} = R_x(\phi)\, R_y(\theta)\, R_z(\psi)} R外旋=Rx(ϕ)Ry(θ)Rz(ψ)
记忆技巧:外旋——矩阵按"转动发生的先后顺序",从右到左排列,依次左乘。第一个转的 ψ\psiψ 对应的矩阵在最右边(离原始向量 vvv 最近)。

图1(见配套 PDF:欧拉角_内旋外旋合成顺序图.pdf):这张图用流程框的方式,把内旋"轴不断更新、矩阵右乘"和外旋"轴始终不变、矩阵左乘"这两条规律并排画出来,并在底部给出了矩阵合成公式,建议对照原始的坐标轴图一起看。

在这里插入图片描述

5. 作业解答:内旋与外旋的关系是什么?

对比第 3 节和第 4 节推出的两个矩阵:
R内旋(ψ,θ,ϕ)=Rz(ψ) Ry(θ) Rx(ϕ)R_{\text{内旋}}(\psi,\theta,\phi) = R_z(\psi)\, R_y(\theta)\, R_x(\phi)R内旋(ψ,θ,ϕ)=Rz(ψ)Ry(θ)Rx(ϕ)
R外旋(ψ,θ,ϕ)=Rx(ϕ) Ry(θ) Rz(ψ)R_{\text{外旋}}(\psi,\theta,\phi) = R_x(\phi)\, R_y(\theta)\, R_z(\psi)R外旋(ψ,θ,ϕ)=Rx(ϕ)Ry(θ)Rz(ψ)
这两个矩阵长得几乎一样,只是矩阵出现的顺序被整体颠倒了Rz,Ry,RxR_z, R_y, R_xRz,Ry,Rx 变成了 Rx,Ry,RzR_x, R_y, R_zRx,Ry,Rz)。这不是巧合,而是一个具有一般性的规律:

结论:以某个顺序做内旋(例如先绕 zzz,再绕新 y′y'y,最后绕新 x′′x''x′′,角度依次是 ψ,θ,ϕ\psi,\theta,\phiψ,θ,ϕ),得到的旋转矩阵,与以完全相反的轴顺序完全相反的角度顺序做外旋(先绕固定 xxx 轴转 ϕ\phiϕ,再绕固定 yyy 轴转 θ\thetaθ,最后绕固定 zzz 轴转 ψ\psiψ),得到的旋转矩阵完全相同。反过来也成立。
用公式表达就是:
R内旋(轴顺序 z,y,x; 角度 ψ,θ,ϕ)  =  R外旋(轴顺序 x,y,z; 角度 ϕ,θ,ψ) R_{\text{内旋}}(\text{轴顺序 } z,y,x;\ \text{角度 } \psi,\theta,\phi) \;=\; R_{\text{外旋}}(\text{轴顺序 } x,y,z;\ \text{角度 } \phi,\theta,\psi) R内旋(轴顺序 z,y,x; 角度 ψ,θ,ϕ)=R外旋(轴顺序 x,y,z; 角度 ϕ,θ,ψ)
也就是说,同一串数字(比如 “先 ψ\psiψ、再 θ\thetaθ、再 ϕ\phiϕ,轴序 z-y-x”)如果按内旋方式理解和按外旋方式理解,算出来的姿态是不一样的(这也是为什么原图里外旋部分最终画出来的姿态,和内旋部分不同);但如果你愿意把顺序整体倒过来读,两种旋转方式就能互相转换、互相等价。

5.1 为什么会这样:一个更本质的原因

这个规律的本质原因是:“绕动轴转"这件事,本身就等价于"用相反顺序绕定轴转”。直观理解:

  • 想象你在做内旋的最后一步——绕最新的 x′′x''x′′ 轴转 ϕ\phiϕ。这根 x′′x''x′′ 轴,其实就是"世界坐标系的 xxx 轴,先后被 Rz(ψ)R_z(\psi)Rz(ψ)Ry(θ)R_y(\theta)Ry(θ) 转过之后"的样子。
  • 而"先绕动轴 A 转 α\alphaα,再绕动轴 B(是 A 转完之后的新轴)转 β\betaβ",从整体效果上看,与"先绕固定轴 B 转 β\betaβ,再绕固定轴 A 转 α\alphaα"是同一个旋转——因为内旋的每一步都会把"抵消项"消掉(就是第 3.1 节推导里出现的 R−1RR^{-1}RR1R 抵消),抵消之后剩下的矩阵乘积顺序,正好和外旋反过来。
    所以只要记住第 3、4 节里两个加框的公式,这道作业题的答案就是"顺序整体颠倒,内旋外旋等价"这一句话,加上上面的公式即可。

6. 用具体数字验证一下

为了确认公式没有理解错,取一组简单角度代入验证矩阵乘法顺序确实不同(也就是内旋和外旋,如果轴序和角度都不颠倒,结果一般不相等)。
ψ=90°,θ=90°,ϕ=0°\psi = 90°, \theta = 90°, \phi = 0°ψ=90°,θ=90°,ϕ=ϕ=0\phi=0ϕ=0Rx(ϕ)R_x(\phi)Rx(ϕ) 是单位矩阵,方便手算):

  • Rz(90°)=[0−10100001]R_z(90°) = \begin{bmatrix}0&-1&0\\1&0&0\\0&0&1\end{bmatrix}Rz(90°)= 010100001 Ry(90°)=[001010−100]R_y(90°) = \begin{bmatrix}0&0&1\\0&1&0\\-1&0&0\end{bmatrix}Ry(90°)= 001010100
    内旋:R内旋=Rz(90°)Ry(90°)=[0−10100001][001010−100]=[0−10001−100]R_{\text{内旋}} = R_z(90°)R_y(90°) = \begin{bmatrix}0&-1&0\\1&0&0\\0&0&1\end{bmatrix}\begin{bmatrix}0&0&1\\0&1&0\\-1&0&0\end{bmatrix} = \begin{bmatrix}0&-1&0\\0&0&1\\-1&0&0\end{bmatrix}R内旋=Rz(90°)Ry(90°)= 010100001 001010100 = 001100010
    外旋:R外旋=Ry(90°)Rz(90°)=[001010−100][0−10100001]=[0011000−10]R_{\text{外旋}} = R_y(90°)R_z(90°) = \begin{bmatrix}0&0&1\\0&1&0\\-1&0&0\end{bmatrix}\begin{bmatrix}0&-1&0\\1&0&0\\0&0&1\end{bmatrix} = \begin{bmatrix}0&0&1\\1&0&0\\0&-1&0\end{bmatrix}R外旋=Ry(90°)Rz(90°)= 001010100 010100001 = 010001100
    可以看到两个矩阵确实不同——这说明"同一组角度、同一个轴序名字(先 z 后 y)“,用内旋理解和用外旋理解,结果是两种不同的姿态。这正是为什么使用欧拉角时必须明确标注"这是内旋还是外旋、轴的顺序是什么”,否则同一串数字会指向两个完全不同的姿态。
    如果按第 5 节的规律,把外旋的顺序颠倒(先绕固定 zzz90°90°90°,再绕固定 yyy90°90°90°,即 Ry(90°)Rz(90°)R_y(90°)R_z(90°)Ry(90°)Rz(90°),这正好是我们上面算的"外旋"那一项!)会发现它恰好等于"先绕动 zzz90°90°90°、再绕动 y′y'y90°90°90°"的内旋结果吗?读者可以自己验证:把内旋公式里的角度顺序也倒过来代入,即可看到两者吻合,从而验证第 5 节结论。

7. 内旋与外旋对比总结


对比项 内旋(intrinsic) 外旋(extrinsic)
旋转轴 绕"动坐标系"自身的轴,每一步都更新 绕"世界坐标系"固定轴,从不更新
矩阵合成方向 按时间顺序从左到右、右乘R=R1R2R3R=R_1 R_2 R_3R=R1R2R3 按时间顺序从右到左、左乘R=R3R2R1R=R_3 R_2 R_1R=R3R2R1
常见应用 飞行器姿态(先偏航,再俯仰,再横滚,都是绕机体轴) 把一个固定编队/固定坐标系下的多次转动叠加
与对方的关系 顺序与角度整体颠倒后,等于外旋 顺序与角度整体颠倒后,等于内旋

8. 常见误区提醒

  1. 不要以为"轴序名字一样,内旋外旋结果就一样":第 6 节的算例已经证明,同样是"先 z 后 y",内旋和外旋算出来的矩阵是不同的两个矩阵。
  2. 内旋记忆口诀:“轴跟着物体转,矩阵往右加”;外旋记忆口诀:“轴焊死在世界上,矩阵往左加”。
  3. 在机器人学、航空航天、计算机图形学中,不同软件库(比如某些引擎用 ZYX 内旋,某些用 XYZ 外旋)默认约定不同,使用欧拉角前一定要先确认对方用的是哪一种约定,否则姿态会算错。

欧拉角的另一种叫法:俯仰(Pitch)/横滚(Roll)/航向(Yaw)

1. 这三个词是什么意思

上一份讲义里讲的欧拉角,用的是比较抽象的符号 ψ,θ,ϕ\psi,\theta,\phiψ,θ,ϕ。但在飞行器、机器人、游戏摄像机、人物角色控制这些实际场景里,大家更习惯给这三个转动角度起一个"看得懂"的名字,这样描述起来更直观:

  • 横滚(Roll):绕机身"从尾到头"的那根轴转动,效果是机身像拧麻花一样左右滚动,两侧机翼一高一低。可以想象一架飞机在天上"翻滚"的动作。
  • 俯仰(Pitch):绕左右机翼方向的轴转动,效果是机头抬起或低下,就像点头动作(抬头看天、低头看地)。
  • 航向(Yaw):绕垂直于地面的轴转动,效果是机头左右摆动,就像摇头动作(左右转向),飞行里也叫"偏航"。
    把这三个词和人的身体动作对应起来记,会更好理解:横滚像"歪头"(耳朵贴向肩膀),俯仰像"点头"(下巴上下动),航向像"摇头"(左右转脸)。游戏里角色转身面向另一个方向,对应的就是航向角变化;角色抬头看天上的东西,对应的就是俯仰角变化。

2. 三根轴的精确定义(配合图1理解)

三根轴都穿过飞行器的重心,并且两两互相垂直,具体方向是:

  • Roll Axis(横滚轴):沿着机身纵向,指向机头方向。
  • Pitch Axis(俯仰轴):垂直于机身,指向机翼延伸的方向(也就是与横滚轴垂直、并且大致水平的那根轴)。
  • Yaw Axis(航向轴):垂直于机身所在的水平面,指向地面(按照右手定则,指向下方)。

图1(见配套 PDF:欧拉角_俯仰横滚航向三轴图.pdf)

在这里插入图片描述

图中用黑色箭头画出了这三根轴,橙色小圆弧箭头表示每根轴对应的正向转动方向,黄色小圆点是旋转中心(重心)。图下方的结论框把这三根轴和上一份讲义里的内旋公式对应了起来,放在完全空白、不遮挡飞机图形的位置。

2.1 正方向怎么确定:右手定则

三根轴的正转方向都遵守右手定则:大拇指指向轴的正方向,四指弯曲的方向就是这根轴的正转方向。比如航向轴指向地面,那么右手大拇指指向地面时,四指弯曲方向是"从机头往右转",所以航向角增大,对应机头向右偏。同理可以自己用右手比划出俯仰和横滚的正方向。

3. 三轴和上一份讲义里内旋公式的关系

细心观察会发现一个重要事实:这三根轴不是固定在地面上的,而是牢牢固定在机身上的——飞机怎么转,这三根轴就跟着怎么转。这正好呼应了上一份讲义里讲的**内旋(绕动坐标轴)**的定义!
按照飞行器姿态描述里最常用的约定(工程上常称为 “Z-Y-X 欧拉角” 或 “yaw-pitch-roll 顺序”),三步旋转依次是:

  1. 先绕航向轴(此时航向轴和世界坐标系的竖直轴重合)转 ψ\psiψ(航向角);
  2. 再绕转过一次之后的俯仰轴θ\thetaθ(俯仰角);
  3. 最后绕转过两次之后的横滚轴ϕ\phiϕ(横滚角)。
    这和上一份讲义里"绕 zzz 轴转 ψ\psiψ → 绕新的 y′y'y 轴转 θ\thetaθ → 绕最新的 x′′x''x′′ 轴转 ϕ\phiϕ"完全是同一件事,只是把 z,y,xz,y,xz,y,x 换成了更好记的名字:z↔z \leftrightarrowz 航向轴,y↔y \leftrightarrowy 俯仰轴,x↔x \leftrightarrowx 横滚轴。所以旋转矩阵的写法也完全照搬上一份讲义的结论,只是把角标换成好记的名字:
    R机体姿态=Ryaw(ψ) Rpitch(θ) Rroll(ϕ) R_{\text{机体姿态}} = R_{\text{yaw}}(\psi)\, R_{\text{pitch}}(\theta)\, R_{\text{roll}}(\phi) R机体姿态=Ryaw(ψ)Rpitch(θ)Rroll(ϕ)
    矩阵右乘的顺序(先发生的旋转矩阵写在最左边)和上一份讲义完全一致,因为这仍然是内旋。

4. 具体数值例子:一次完整的姿态变化

假设一架飞机(或者一个游戏角色的朝向)依次做了三个动作:

  1. 先向右转身(航向角)ψ=30°\psi = 30°ψ=30°
  2. 再抬头(俯仰角)θ=15°\theta = 15°θ=15°
  3. 最后向右侧倾斜身体(横滚角)ϕ=10°\phi = 10°ϕ=10°
    按第 3 节的公式,最终姿态矩阵是:
    R=Rz(30°) Ry(15°) Rx(10°) R = R_z(30°)\, R_y(15°)\, R_x(10°) R=Rz(30°)Ry(15°)Rx(10°)
    把第一份讲义里给出的三个单轴旋转矩阵代入,依次用 cos⁡30°≈0.866, sin⁡30°=0.5\cos30°\approx0.866,\ \sin30°=0.5cos30°0.866, sin30°=0.5cos⁡15°≈0.966, sin⁡15°≈0.259\cos15°\approx0.966,\ \sin15°\approx0.259cos15°0.966, sin15°0.259cos⁡10°≈0.985, sin⁡10°≈0.174\cos10°\approx0.985,\ \sin10°\approx0.174cos10°0.985, sin10°0.174
    Rz(30°)=[0.866−0.500.50.8660001],Ry(15°)=[0.96600.259010−0.25900.966],Rx(10°)=[10000.985−0.17400.1740.985] R_z(30°)=\begin{bmatrix}0.866&-0.5&0\\0.5&0.866&0\\0&0&1\end{bmatrix},\quad R_y(15°)=\begin{bmatrix}0.966&0&0.259\\0&1&0\\-0.259&0&0.966\end{bmatrix},\quad R_x(10°)=\begin{bmatrix}1&0&0\\0&0.985&-0.174\\0&0.174&0.985\end{bmatrix} Rz(30°)= 0.8660.500.50.8660001 ,Ry(15°)= 0.96600.2590100.25900.966 ,Rx(10°)= 10000.9850.17400.1740.985
    先算 Ry(15°)Rx(10°)R_y(15°)R_x(10°)Ry(15°)Rx(10°)
    Ry(15°)Rx(10°)=[0.9660.0450.25500.985−0.174−0.2590.1680.952] R_y(15°)R_x(10°)=\begin{bmatrix}0.966&0.045&0.255\\0&0.985&-0.174\\-0.259&0.168&0.952\end{bmatrix} Ry(15°)Rx(10°)= 0.96600.2590.0450.9850.1680.2550.1740.952
    再左乘 Rz(30°)R_z(30°)Rz(30°),得到最终姿态矩阵(保留三位小数):
    R≈[0.837−0.4510.3120.4830.898−0.017−0.2590.1680.952] R \approx \begin{bmatrix}0.837&-0.451&0.312\\0.483&0.898&-0.017\\-0.259&0.168&0.952\end{bmatrix} R 0.8370.4830.2590.4510.8980.1680.3120.0170.952
    这个矩阵就是"先转身、再抬头、再侧倾"这一整套动作叠加后的最终朝向。读者可以把它作用在一个初始朝向向量(比如机头初始朝向 (1,0,0)(1,0,0)(1,0,0))上,用 R⋅(1,0,0)TR \cdot (1,0,0)^TR(1,0,0)T 算出最终机头指向的方向向量,直观感受角度叠加的效果。

5. 一个值得了解的坑:万向节死锁(Gimbal Lock)

这不是这次作业要求的内容,但了解一下会让你对欧拉角有更完整的认识:当俯仰角 θ\thetaθ 恰好转到 90°90°90°(比如飞机垂直朝天飞)时,航向轴和横滚轴会重合成同一根轴,这时候航向角和横滚角就分不清了——转航向和转横滚变成了同一个动作,少了一个自由度。这个现象叫万向节死锁,是欧拉角表示法一个公认的缺点,工程上有时会改用四元数(quaternion)来避免这个问题。这里只是提一下背景,不展开推导。

6. 总结对照表


轴名称 英文 对应欧拉角符号 穿过的方向 动作比喻 对应上一讲的轴
横滚轴 Roll Axis ϕ\phiϕ 沿机身指向机头 像"歪头",两翼一高一低 内旋第三步 x′′x''x′′
俯仰轴 Pitch Axis θ\thetaθ 垂直机身、沿机翼方向 像"点头",机头抬起或低下 内旋第二步 y′y'y
航向轴 Yaw Axis ψ\psiψ 垂直向下 像"摇头",机头左右摆动 内旋第一步 zzz

三根轴始终固定在机身上、随机身一起转动,这正是它属于内旋的原因;旋转矩阵按"航向→俯仰→横滚"这个发生顺序,从左到右依次右乘,就得到最终姿态:R=Ryaw(ψ)Rpitch(θ)Rroll(ϕ)R = R_{\text{yaw}}(\psi)R_{\text{pitch}}(\theta)R_{\text{roll}}(\phi)R=Ryaw(ψ)Rpitch(θ)Rroll(ϕ)

Blender 里的欧拉角旋转模式怎么理解

1. 面板里那个"模式"下拉框是在选什么

打开 Blender 物体属性面板的"旋转"部分,会看到三个角度输入框(旋转 X、Y、Z),下面还有一个"模式"下拉框,里面列了一大串选项:
四元数运算(WXYZ)、XYZ欧拉、XZY欧拉、YXZ欧拉、YZX欧拉、ZXY欧拉、ZYX欧拉、坐标轴角度
这个下拉框本质上是在回答一个问题:“旋转 X、Y、Z 这三个角度值,到底应该按什么顺序、绕哪几根轴依次转?”
前面两份讲义已经讲过,同一组角度数字,如果转动的先后顺序不同,最终转出来的姿态是完全不一样的。Blender 的做法是把"三个角度值"和"转动顺序"拆开成两件事分别设置:角度值填在 X/Y/Z 三个输入框里,顺序则通过"模式"这个下拉框单独选择。选中的六个"字母组合欧拉"选项,就是欧拉角三次旋转轴的六种排列方式;另外两个选项(四元数运算、坐标轴角度)根本不是欧拉角,而是另外两套完全不同的姿态表示方法,第 5 节会简单介绍。

2. "XYZ欧拉"到底是怎么转的

以截图里选中的默认模式"XYZ欧拉"、面板数值 X=60°60°60°、Y=20°20°20°、Z=0°0° 为例。模式名字里字母的顺序,就是三次旋转真正发生的先后顺序,并且都是绕物体"自身不断更新的轴"转(也就是前两份讲义讲过的内旋):

  1. 第一步:绕物体自身的 XXX 轴转 60°60°60°
  2. 第二步:绕转过一次之后YYY 轴转 20°20°20°(注意这已经不是世界坐标系的 yyy 轴了);
  3. 第三步:绕转过两次之后ZZZ 轴转 0°0°(因为这个例子里 Z 填的是 0°0°,所以这一步实际上什么都没转)。

图1(见配套 PDF:欧拉角_Blender旋转模式示意图.pdf)

在这里插入图片描述

图中用流程框展示了这三步的先后关系,并在下方给出了这组数值代入后算出的具体旋转矩阵,结论框放在图形完全空白的下方区域,不遮挡任何流程内容。

2.1 对应的旋转矩阵

按照前两份讲义总结的内旋规律(矩阵按发生顺序从左到右依次右乘),"XYZ欧拉"模式对应的旋转矩阵是:
RXYZ欧拉=Rx(X) Ry(Y) Rz(Z) R_{\text{XYZ欧拉}} = R_x(X)\, R_y(Y)\, R_z(Z) RXYZ欧拉=Rx(X)Ry(Y)Rz(Z)
把截图里的具体数值 X=60°, Y=20°, Z=0°X=60°,\ Y=20°,\ Z=0°X=60°, Y=20°, Z= 代入(因为 Z=0°Z=0°Z=Rz(0°)R_z(0°)Rz() 是单位矩阵,相当于没转),算出来大约是:
R≈[0.94000.3420.2960.5−0.814−0.1710.8660.470] R \approx \begin{bmatrix} 0.940 & 0 & 0.342\\ 0.296 & 0.5 & -0.814\\ -0.171 & 0.866 & 0.470 \end{bmatrix} R 0.9400.2960.17100.50.8660.3420.8140.470
这个矩阵就是截图左侧视口里那个六边形网格物体,从"没有任何旋转"的初始状态,变成当前歪斜姿态所经历的完整旋转变换。

3. 六种"字母组合欧拉"模式的通用规律

Blender 一共提供了 6 种欧拉角顺序:XYZ、XZY、YXZ、YZX、ZXY、ZYX,对应三个坐标轴的全部排列组合。它们的原理完全一样,区别只是"哪根轴先转、哪根轴后转"。按照第 2 节的规律,可以直接写出全部 6 种模式对应的旋转矩阵公式(角度仍然用面板里对应轴的输入值表示,比如 “X” 代表旋转X输入框里的数值):

模式名称 旋转发生顺序 对应旋转矩阵
XYZ欧拉 先X,再Y,最后Z R=Rx(X) Ry(Y) Rz(Z)R = R_x(X)\,R_y(Y)\,R_z(Z)R=Rx(X)Ry(Y)Rz(Z)
XZY欧拉 先X,再Z,最后Y R=Rx(X) Rz(Z) Ry(Y)R = R_x(X)\,R_z(Z)\,R_y(Y)R=Rx(X)Rz(Z)Ry(Y)
YXZ欧拉 先Y,再X,最后Z R=Ry(Y) Rx(X) Rz(Z)R = R_y(Y)\,R_x(X)\,R_z(Z)R=Ry(Y)Rx(X)Rz(Z)
YZX欧拉 先Y,再Z,最后X R=Ry(Y) Rz(Z) Rx(X)R = R_y(Y)\,R_z(Z)\,R_x(X)R=Ry(Y)Rz(Z)Rx(X)
ZXY欧拉 先Z,再X,最后Y R=Rz(Z) Rx(X) Ry(Y)R = R_z(Z)\,R_x(X)\,R_y(Y)R=Rz(Z)Rx(X)Ry(Y)
ZYX欧拉 先Z,再Y,最后X R=Rz(Z) Ry(Y) Rx(X)R = R_z(Z)\,R_y(Y)\,R_x(X)R=Rz(Z)Ry(Y)Rx(X)

记忆规律只有一句话:模式名字从左到右读出来的字母顺序,就是旋转矩阵从左到右的排列顺序,也是三次旋转实际发生的先后顺序。这六种模式全部都是内旋(绕自身不断更新的轴),Blender 官方文档里把这套坐标轴体系形象地称为"欧拉云台"(Euler gimbal)——三根轴像老式陀螺仪里一层套一层的圆环,最外层的环带着中间层转,中间层又带着最内层转。

4. 为什么会有 6 种选择,以及万向节死锁

既然 6 种模式本质上是同一件事的不同排列,为什么 Blender 要都提供出来?原因是不同的物体、不同的动画场景,转动时"更自然"的轴顺序不一样——比如一个人的手臂,先抬肩、再转肘、最后转手腕会更符合关节的实际约束;而一架飞机则更适合按航向-俯仰-横滚(也就是 ZYX 顺序)来描述。选对顺序能让动画曲线更平滑、更好编辑。
这 6 种模式还有一个共同的缺点,也是欧拉角这种表示方法本身固有的问题——万向节死锁(Gimbal Lock)。以"XYZ欧拉"为例:当中间那个字母对应的轴(这里是 YYY 轴)转到 ±90°\pm 90°±90° 附近时,第一个字母和第三个字母对应的轴(XXX 轴和 ZZZ 轴)会重合成同一根轴,此时绕 XXX 轴转和绕 ZZZ 轴转会产生完全相同的效果,实际上就丢失了一个自由度,导致这个方向上的旋转再也调不出来。这也是截图里下拉菜单每个模式后面都标注"容易发生万向节死锁"的原因。

5. 另外两个选项:四元数与坐标轴角度

下拉菜单里除了 6 种欧拉角,还有两个完全不同的姿态表示方式,都是为了解决万向节死锁问题而存在的:

  • 四元数运算(WXYZ):用一个四维向量 (w,x,y,z)(w,x,y,z)(w,x,y,z) 来表示旋转,不依赖任何"先转哪根轴"的顺序,因此天生不会有万向节死锁问题,是游戏引擎和动画系统底层最常用的旋转表示方式,缺点是这四个数字本身不直观,人很难直接读懂"转成这样"具体是什么姿态。
  • 坐标轴角度:直接指定一根旋转轴(一个三维方向向量)和绕这根轴转动的角度,是"轴角表示法"(axis-angle)。这种方式在几何直觉上很清晰(一次性绕一根轴转一个角度就搞定),也不会有万向节死锁,但同样不方便像欧拉角那样分别控制"抬头"“转身”"侧倾"这种独立的动作。
    这两种方式的具体数学定义和推导,超出了本讲义"从欧拉角入手"的范围,这里只作为背景知识,帮助理解 Blender 为什么在下拉菜单里把它们和欧拉角放在一起——它们解决的是同一个问题(怎么描述三维旋转),只是各有各的取舍。

6. 小结


要点 内容
"模式"下拉框的作用 决定欧拉角三次旋转的轴顺序,或改用四元数/轴角表示
字母顺序的含义 从左到右就是旋转发生的先后顺序,也是矩阵从左到右的乘法顺序
本质 6 种"字母组合欧拉"模式全部都是内旋(绕自身更新的轴)
共同缺点 都存在万向节死锁:中间字母对应的轴转到 ±90°\pm90°±90° 附近时丢失一个自由度
不会死锁的替代方案 四元数运算(WXYZ)、坐标轴角度(轴角表示法)

万向节死锁(Gimbal Lock)从零理解

1. 先看实物:万向节长什么样

万向节(gimbal)是一种机械结构,由三个互相嵌套的圆环组成:最外层的环固定在机身或支架上,中间层的环挂在最外层环内部、可以绕一根轴转动,最内层的环又挂在中间层环内部、也能绕另一根轴转动。三层环各自的转轴依次对应航向(yaw,最外层)→ 俯仰(pitch,中间层)→ 横滚(roll,最内层),最内层环的中心固定着飞行器(或者任何需要描述姿态的物体)。
正常情况下,三层环的转轴两两之间保持不同方向,转动任意一层环,物体姿态的变化效果都是独一无二的,互不重复——这时候三个自由度(航向、俯仰、横滚)都能被自由、独立地调节。
但当中间层的俯仰环转到 90°90°90°(也就是 π/2\pi/2π/2 弧度)时,会出现一个很反直觉的现象:最外层的航向环转轴和最内层的横滚环转轴,会变成完全平行、甚至重合的两根轴。这时候不管你转的是最外层环还是最内层环,物体姿态的变化效果都变得完全一样——原本三个独立可调的自由度,在这一瞬间只剩下两个能用了,第三个自由度"丢失"了,这就是万向节死锁

图1(见配套 PDF:欧拉角_万向节死锁示意图.pdf)

在这里插入图片描述

左边画的是正常状态,三根轴(紫色航向、绿色俯仰、蓝色横滚)两两方向不同;右边画的是俯仰角转到 90°90°90° 之后的死锁状态,航向轴和横滚轴几乎贴在一起变成了同一根轴,用红色"Lock!"箭头强调这一点。

2. 为什么俯仰轴转到 90° 会导致这个问题

结合前面几份讲义讲过的知识,飞行器姿态最常用的欧拉角约定是按航向 ψ\psiψ → 俯仰 θ\thetaθ → 横滚 ϕ\phiϕ 的顺序做内旋,对应的旋转矩阵是:
R=Rz(ψ) Ry(θ) Rx(ϕ) R = R_z(\psi)\, R_y(\theta)\, R_x(\phi) R=Rz(ψ)Ry(θ)Rx(ϕ)
注意这三步旋转里,俯仰这一步排在中间:先转航向、再转俯仰、最后转横滚。正是因为俯仰排在中间,它转动的角度会同时影响"航向轴接下来指向哪"和"横滚轴接下来指向哪"这两件事——这也是为什么俯仰角是这三个角里唯一会引发死锁的那个(这一点其实和之前 Blender 讲义里"模式名字中间那个字母最容易出问题"的规律是同一件事)。

2.1 完整的旋转矩阵展开式

把三个单轴旋转矩阵按顺序相乘(这是一个纯粹的矩阵乘法运算,可以自己动手验证),展开后得到:
R=[cos⁡ψcos⁡θcos⁡ψsin⁡θsin⁡ϕ−sin⁡ψcos⁡ϕcos⁡ψsin⁡θcos⁡ϕ+sin⁡ψsin⁡ϕsin⁡ψcos⁡θsin⁡ψsin⁡θsin⁡ϕ+cos⁡ψcos⁡ϕsin⁡ψsin⁡θcos⁡ϕ−cos⁡ψsin⁡ϕ−sin⁡θcos⁡θsin⁡ϕcos⁡θcos⁡ϕ] R = \begin{bmatrix} \cos\psi\cos\theta & \cos\psi\sin\theta\sin\phi - \sin\psi\cos\phi & \cos\psi\sin\theta\cos\phi + \sin\psi\sin\phi\\ \sin\psi\cos\theta & \sin\psi\sin\theta\sin\phi + \cos\psi\cos\phi & \sin\psi\sin\theta\cos\phi - \cos\psi\sin\phi\\ -\sin\theta & \cos\theta\sin\phi & \cos\theta\cos\phi \end{bmatrix} R= cosψcosθsinψcosθsinθcosψsinθsinϕsinψcosϕsinψsinθsinϕ+cosψcosϕcosθsinϕcosψsinθcosϕ+sinψsinϕsinψsinθcosϕcosψsinϕcosθcosϕ
这是一个关于 ψ,θ,ϕ\psi,\theta,\phiψ,θ,ϕ 三个独立变量的矩阵——正常情况下,任意改变这三个角度中的一个,矩阵都会发生不同的变化。

2.2 代入 θ=90°\theta = 90°θ=90° 之后发生了什么

现在把 θ=90°\theta = 90°θ=90°(即 π/2\pi/2π/2)代入,此时 cos⁡θ=0, sin⁡θ=1\cos\theta = 0,\ \sin\theta = 1cosθ=0, sinθ=1。把这两个值代进上面的矩阵,很多项会因为乘上 cos⁡θ=0\cos\theta=0cosθ=0 而直接消失:
R(θ=90°)=[0cos⁡ψsin⁡ϕ−sin⁡ψcos⁡ϕcos⁡ψcos⁡ϕ+sin⁡ψsin⁡ϕ0sin⁡ψsin⁡ϕ+cos⁡ψcos⁡ϕsin⁡ψcos⁡ϕ−cos⁡ψsin⁡ϕ−100] R(\theta{=}90°) = \begin{bmatrix} 0 & \cos\psi\sin\phi - \sin\psi\cos\phi & \cos\psi\cos\phi + \sin\psi\sin\phi\\ 0 & \sin\psi\sin\phi + \cos\psi\cos\phi & \sin\psi\cos\phi - \cos\psi\sin\phi\\ -1 & 0 & 0 \end{bmatrix} R(θ=90°)= 001cosψsinϕsinψcosϕsinψsinϕ+cosψcosϕ0cosψcosϕ+sinψsinϕsinψcosϕcosψsinϕ0
矩阵里剩下的这些项,形式正好是三角函数的和差角公式
cos⁡ψsin⁡ϕ−sin⁡ψcos⁡ϕ=−sin⁡(ψ−ϕ),cos⁡ψcos⁡ϕ+sin⁡ψsin⁡ϕ=cos⁡(ψ−ϕ) \cos\psi\sin\phi - \sin\psi\cos\phi = -\sin(\psi-\phi), \qquad \cos\psi\cos\phi + \sin\psi\sin\phi = \cos(\psi-\phi) cosψsinϕsinψcosϕ=sin(ψϕ),cosψcosϕ+sinψsinϕ=cos(ψϕ)
把所有项都换成 (ψ−ϕ)(\psi-\phi)(ψϕ) 的形式,最终化简为:
R(θ=90°)=[0−sin⁡(ψ−ϕ)cos⁡(ψ−ϕ)0cos⁡(ψ−ϕ)sin⁡(ψ−ϕ)−100] \boxed{R(\theta{=}90°) = \begin{bmatrix} 0 & -\sin(\psi-\phi) & \cos(\psi-\phi)\\ 0 & \cos(\psi-\phi) & \sin(\psi-\phi)\\ -1 & 0 & 0 \end{bmatrix}} R(θ=90°)= 001sin(ψϕ)cos(ψϕ)0cos(ψϕ)sin(ψϕ)0
这一步是整个万向节死锁问题的数学核心:可以看到,化简后的矩阵里,ψ\psiψϕ\phiϕ 不再是各自独立出现的,它们只以 (ψ−ϕ)(\psi-\phi)(ψϕ) 这一个组合的形式出现在矩阵里。这意味着:只要 ψ−ϕ\psi-\phiψϕ 的值不变,不管 ψ\psiψϕ\phiϕ 单独取多少,算出来的旋转矩阵都完全相同!原本三个独立参数 (ψ,θ,ϕ)(\psi,\theta,\phi)(ψ,θ,ϕ),在 θ=90°\theta=90°θ=90° 这一点上,实际能影响姿态的只剩下两个独立的量:固定的 θ=90°\theta=90°θ=90°,加上一个组合量 (ψ−ϕ)(\psi-\phi)(ψϕ)——这就是"三个自由度退化成两个"的精确数学含义。

2.3 用数字验证一下

取三组不同的 ψ,ϕ\psi,\phiψ,ϕ,但保持 ψ−ϕ=20°\psi - \phi = 20°ψϕ=20° 不变(θ\thetaθ 固定为 90°90°90°),代入验证:

ψ\psiψ ϕ\phiϕ ψ−ϕ\psi-\phiψϕ 算出的旋转矩阵
30°30°30° 10°10°10° 20°20°20° [0−0.3420.94000.9400.342−100]\begin{bmatrix}0&-0.342&0.940\\0&0.940&0.342\\-1&0&0\end{bmatrix} 0010.3420.94000.9400.3420
50°50°50° 30°30°30° 20°20°20° [0−0.3420.94000.9400.342−100]\begin{bmatrix}0&-0.342&0.940\\0&0.940&0.342\\-1&0&0\end{bmatrix} 0010.3420.94000.9400.3420
80°80°80° 60°60°60° 20°20°20° [0−0.3420.94000.9400.342−100]\begin{bmatrix}0&-0.342&0.940\\0&0.940&0.342\\-1&0&0\end{bmatrix} 0010.3420.94000.9400.3420

三组完全不同的 (ψ,ϕ)(\psi,\phi)(ψ,ϕ),只要差值一样,算出来的矩阵逐个数字都完全相同。这也正好验证了原图文字说的那句话——“改变横滚角的效果与改变航向角相同”:既然只有差值 ψ−ϕ\psi-\phiψϕ 才影响结果,那么把 ϕ\phiϕ(横滚)增大一点,和把 ψ\psiψ(航向)减小同样的量,效果是完全等价的,两者变成了"绑在一起"、没法再单独控制的同一个自由度。

3. 一句话理解"奇点"是什么意思

原图里提到"俯仰角等于 π/2\pi/2π/2 是一个奇点(singularity)",这里的"奇点"不是指某个具体的数值坏掉了,而是指:(ψ,θ,ϕ)(\psi,\theta,\phi)(ψ,θ,ϕ) 这三个数字去描述所有可能的三维旋转,这种描述方法本身在 θ=±90°\theta=\pm 90°θ=±90° 这个特殊位置上失效了——在其他任何角度,给定一个旋转姿态,都能唯一地反推出对应的 (ψ,θ,ϕ)(\psi,\theta,\phi)(ψ,θ,ϕ);但在 θ=90°\theta=90°θ=90° 时,无数组不同的 (ψ,ϕ)(\psi,\phi)(ψ,ϕ)(只要差值相同)都对应着同一个旋转矩阵,"角度数字"和"实际姿态"不再是一一对应的关系了。这种"描述方法在某个特殊点上失效、多个参数值对应同一个结果"的现象,在数学上就统称为奇点。

4. 怎么避开这个问题

结合之前 Blender 讲义里提到的内容,工程上主要有两种思路来彻底避免万向节死锁:

  • 换一种旋转顺序:欧拉角有 6 种轴顺序可选,死锁只会在"中间那根轴转到 90°90°90°"时发生,所以可以根据具体应用场景,选择一个物体在正常工作范围内不太可能转到 90°90°90° 的那根轴放在中间,从而在实际使用中避开这个奇点(但这只是"绕开",理论上死锁依然存在,只是出现的角度不一样)。
  • 换一种表示方法:从根本上放弃欧拉角,改用**四元数(quaternion)轴角表示法(axis-angle)**来描述旋转。这两种方式用更多的数字(四元数用 4 个数、轴角用 4 个数:一根轴方向 + 一个角度)换来了"处处光滑、没有奇点"的好处,是游戏引擎、机器人学、航天姿态控制里更常用的底层实现方式。

5. 小结


要点 内容
发生条件 三步内旋(航向→俯仰→横滚)中,中间那一步的角度 θ\thetaθ 转到 ±90°\pm 90°±90°
直观表现 最外层轴(航向)和最内层轴(横滚)方向重合,转其中一个等于转另一个
数学本质 化简后的旋转矩阵只依赖组合量 ψ−ϕ\psi-\phiψϕ,3 个独立参数退化成 2 个
专业说法 欧拉角参数空间到旋转姿态之间的映射,在 θ=±90°\theta=\pm 90°θ=±90° 处出现奇点
解决办法 更换轴顺序(治标);改用四元数或轴角表示法(治本)

轴角表示(Axis-Angle)从零理解

在这里插入图片描述

配套图形见 PDF 文件《轴角表示_图解.pdf》,其中:

  • 图1(a) 对应下文"直观理解"部分;
  • 图1(b) 对应下文"数学推导"部分,图中标注的每一个符号(u,v,v∥,v⊥,w,vrot,θu, v, v_\parallel, v_\perp, w, v_{\mathrm{rot}}, \thetau,v,v,v,w,vrot,θ)都和文字中的记号完全一致,可以对照着看。

一、先建立直觉:什么是"轴角"

想象一头玩具奶牛,我们想让它转个身。最自然的描述方式是:找一根穿过奶牛的轴,然后让奶牛绕着这根轴转过某个角度
这就是"轴角表示"的全部思想——用两个东西描述一次三维旋转:

  • 一根转轴:告诉你"绕哪个方向转";
  • 一个角度:告诉你"转了多少"。
    (对应 图1(a):物体绕轴 uuu 转过角度 θ\thetaθ
    这和我们熟悉的"旋转矩阵"(一个 3×33\times33×3 的矩阵,9个数字)比起来,轴角表示只需要 4 个数字(轴的3个分量 + 1个角度,因为轴是单位向量,实际上只有2个自由度描述方向),信息更紧凑,也更符合人的直觉——没有人会说"我把这个物体用某个 3×33\times33×3 矩阵变换了一下",但会说"我把它绕着这根轴转了45度"。

二、数学定义

给定一次三维旋转,可以用下面两个量唯一(在 θ∈(−π,π]\theta\in(-\pi,\pi]θ(π,π] 范围内几乎唯一,除了 θ=0\theta=0θ=0θ=π\theta=\piθ=π 的特殊情况)确定它:

  • 旋转轴u∈R3u \in \mathbb{R}^3uR3,且 ∥u∥=1\|u\|=1u=1(必须是单位向量,否则"转多少度"这件事就没有统一的度量标准);
  • 旋转角度θ∈R\theta \in \mathbb{R}θR(正负号约定:从轴的正方向看过去,逆时针为正,遵循右手定则)。
    把这两个信息合并成一个向量,就得到了旋转向量(rotation vector):
    θ=θu∈R3 \boldsymbol{\theta} = \theta u \in \mathbb{R}^3 θ=θuR3
    这个向量非常巧妙:它的方向就是转轴方向,它的长度就是旋转的角度。一个向量同时装下了"轴"和"角"两个信息,这也是它在工程上(比如机器人学、SLAM、计算机视觉里的位姿优化)被大量使用的原因——三个自由度、无约束、可以直接做加减法参与优化。

三、推导:轴角是怎么把一个向量转到另一个位置的

现在的问题是:已知转轴 uuu、转角 θ\thetaθ,以及空间中任意一个向量 vvv,转完之后的新向量 vrotv_{\mathrm{rot}}vrot 该怎么算?这就是**罗德里格斯旋转公式(Rodrigues’ Rotation Formula)**要回答的问题。推导思路其实很朴素,分三步走。

第一步:把 vvv 拆成"跟轴平行"和"跟轴垂直"两部分

任何一个向量 vvv,都可以唯一地分解成两部分:一部分和转轴 uuu 平行(记作 v∥v_\parallelv),另一部分和转轴 uuu 垂直(记作 v⊥v_\perpv):
v=v∥+v⊥ v = v_\parallel + v_\perp v=v+v
其中 v∥v_\parallelv 就是 vvvuuu 方向上的投影,用点积公式直接写出来:
v∥=(u⋅v) u v_\parallel = (u \cdot v)\, u v=(uv)u
u⋅vu\cdot vuv 是一个标量,表示投影的长度;乘上 uuu 把这个长度变回一个沿 uuu 方向的向量)
剩下的部分自然就是垂直分量:
v⊥=v−v∥ v_\perp = v - v_\parallel v=vv
(对应 图1(b) 中:红色实线箭头 vvv,被拆成沿轴向上的红色箭头 v∥v_\parallelv,以及躺在与轴垂直的平面内的红色虚线箭头 v⊥v_\perpv,图中的虚线是辅助构造线,表示 v=v∥+v⊥v = v_\parallel + v_\perpv=v+v 这个向量加法的平行四边形)
为什么要这样拆? 因为绕轴 uuu 旋转的时候,凡是躺在轴上的部分根本不会动(就像地球自转,地轴上的点永远在原地),会动的只有垂直于轴的那一部分。所以:
vrot=v∥+(v⊥ 旋转 θ 角度后的结果) v_{\mathrm{rot}} = v_\parallel + (v_\perp \text{ 旋转 } \theta \text{ 角度后的结果}) vrot=v+(v 旋转 θ 角度后的结果)
问题就转化成了:v⊥v_\perpv 这个向量,在垂直于 uuu 的那个平面内,转过 θ\thetaθ 角之后变成什么?

第二步:在垂直平面内,借助叉乘构造一个"帮手"向量

要在一个平面内描述"转过 θ\thetaθ 角",最简单的办法是找到这个平面内的一组正交基(两个互相垂直、长度相等的向量),然后用高中学过的二维旋转公式(cos⁡θ\cos\thetacosθsin⁡θ\sin\thetasinθ 的组合)去表示旋转结果。
v⊥v_\perpv 本身就是这个平面内的一个基方向。另一个方向,可以通过叉乘构造出来:
w=u×v w = u \times v w=u×v
(因为 u×v∥=0u \times v_\parallel = 0u×v=0,两个平行向量叉乘结果是零向量,所以 u×vu\times vu×v 其实等于 u×v⊥u\times v_\perpu×v,写成 u×vu\times vu×v 只是省去了一步分解)
这个 www 有两个很好的性质:

  1. www 垂直于 v⊥v_\perpv:因为叉乘的结果必然垂直于参与叉乘的两个向量,所以 w=u×v⊥w=u\times v_\perpw=u×v 同时垂直于 uuuv⊥v_\perpv;既然 www 垂直于 uuu,它自然也躺在那个与 uuu 垂直的平面里,和 v⊥v_\perpv 在同一个平面内;
  2. wwwv⊥v_\perpv 长度相等:叉乘的模长公式是 ∥u×v⊥∥=∥u∥ ∥v⊥∥sin⁡ϕ\|u\times v_\perp\| = \|u\|\,\|v_\perp\|\sin\phiu×v=uvsinϕ,这里 ϕ\phiϕuuuv⊥v_\perpv 的夹角。因为 v⊥⊥uv_\perp\perp uvu,夹角 ϕ=90°\phi=90°ϕ=90°sin⁡ϕ=1\sin\phi=1sinϕ=1;又因为 ∥u∥=1\|u\|=1u=1,所以 ∥w∥=∥v⊥∥\|w\| = \|v_\perp\|w=v
    一句话总结:www 就是把 v⊥v_\perpv 在这个垂直平面内原地转 90 度得到的向量。有了 v⊥v_\perpvwww 这一组等长且正交的基,这个平面内任意角度的旋转都可以写出来了。
    (对应 图1(b) 中的蓝色箭头 w=u×vw=u\times vw=u×v,以及底部那条从 v⊥v_\perpv 转到某一点、标注了 θ\thetaθ 的圆弧——这条圆弧画在垂直于 uuu 的单位圆上,直观地表示"转过角度 θ\thetaθ"这件事到底发生在哪个平面里)

第三步:套用二维旋转公式,再把平行分量加回来

有了 v⊥v_\perpvwww 这组正交基,v⊥v_\perpv 转过 θ\thetaθ 角之后的结果,就是标准的二维旋转公式:
v⊥,rot=cos⁡θ v⊥+sin⁡θ w v_{\perp,\mathrm{rot}} = \cos\theta\, v_\perp + \sin\theta\, w v,rot=cosθv+sinθw
这一步的直觉和平面直角坐标系里"一个点绕原点转 θ\thetaθ 角"完全一样:新位置等于"原方向乘 cos⁡θ\cos\thetacosθ"加上"垂直方向乘 sin⁡θ\sin\thetasinθ"。
最后,把不参与旋转的平行分量加回来:
vrot=v∥+v⊥,rot=v∥+cos⁡θ v⊥+sin⁡θ w v_{\mathrm{rot}} = v_\parallel + v_{\perp,\mathrm{rot}} = v_\parallel + \cos\theta\, v_\perp + \sin\theta\, w vrot=v+v,rot=v+cosθv+sinθw
v⊥=v−v∥v_\perp = v-v_\parallelv=vvw=u×vw=u\times vw=u×v 代进去展开:
vrot=v∥+cos⁡θ (v−v∥)+sin⁡θ (u×v)=cos⁡θ v+(1−cos⁡θ) v∥+sin⁡θ (u×v) \begin{aligned} v_{\mathrm{rot}} &= v_\parallel + \cos\theta\,(v-v_\parallel) + \sin\theta\,(u\times v)\\ &= \cos\theta\, v + (1-\cos\theta)\, v_\parallel + \sin\theta\,(u\times v) \end{aligned} vrot=v+cosθ(vv)+sinθ(u×v)=cosθv+(1cosθ)v+sinθ(u×v)
再把 v∥=(u⋅v)uv_\parallel=(u\cdot v)uv=(uv)u 代进去,就得到了最终的罗德里格斯旋转公式
vrot=cos⁡θ v+(1−cos⁡θ)(u⋅v) u+sin⁡θ (u×v) \boxed{v_{\mathrm{rot}} = \cos\theta\, v + (1-\cos\theta)(u\cdot v)\,u + \sin\theta\,(u\times v)} vrot=cosθv+(1cosθ)(uv)u+sinθ(u×v)
(对应 图1(b) 中蓝色箭头 vrotv_{\mathrm{rot}}vrot,它同样可以分解成"沿轴方向的 v∥v_\parallelv"和"垂直平面内旋转后的分量"两部分,图中用虚线画出了这个分解的平行四边形,和 vvv 的分解方式完全对称)
这个公式的意义在于:只用一个转轴 uuu 和一个角度 θ\thetaθ,就能把任意向量 vvv 转到旋转之后的位置 vrotv_{\mathrm{rot}}vrot,而且全程只用到了点积和叉乘,不需要先构造出完整的 3×33\times33×3 旋转矩阵。
得到这个公式后,其实还可以借助一个向量恒等式,把它改写成另一种更适合推出"旋转矩阵"的等价形式——这就是下一节要做的事。

四、等价形式与旋转矩阵

一个好用的向量恒等式:u×(u×v)=(u⋅v)u−vu\times(u\times v)=(u\cdot v)u-vu×(u×v)=(uv)uv

三维向量的二重叉乘有一个很经典的恒等式(常被称为"BAC-CAB"公式),对任意向量 a,b,ca,b,ca,b,c 都成立:
a×(b×c)=b(a⋅c)−c(a⋅b) a\times(b\times c) = b(a\cdot c) - c(a\cdot b) a×(b×c)=b(ac)c(ab)
a=b=ua=b=ua=b=u(单位向量)、c=vc=vc=v 代进去:
u×(u×v)=u(u⋅v)−v(u⋅u) u\times(u\times v) = u(u\cdot v) - v(u\cdot u) u×(u×v)=u(uv)v(uu)
因为 uuu 是单位向量,u⋅u=∥u∥2=1u\cdot u = \|u\|^2 = 1uu=u2=1,所以:
u×(u×v)=(u⋅v) u−v u\times(u\times v) = (u\cdot v)\,u - v u×(u×v)=(uv)uv
这个恒等式的意义在于:它把"点积构造出的平行分量 (u⋅v)u(u\cdot v)u(uv)u"和"两次叉乘"联系了起来,这样一来,罗德里格斯公式里就不需要点积了,全部可以用叉乘表示,这在计算机里实现起来更统一(只需要一个叉乘操作就够了)。

用恒等式改写罗德里格斯公式

把上面的恒等式变形一下,可以反解出:
(u⋅v) u=u×(u×v)+v (u\cdot v)\,u = u\times(u\times v) + v (uv)u=u×(u×v)+v
代入第三节推出的罗德里格斯公式:
vrot=cos⁡θ v+(1−cos⁡θ)(u⋅v) u+sin⁡θ (u×v) v_{\mathrm{rot}} = \cos\theta\, v + (1-\cos\theta)(u\cdot v)\,u + \sin\theta\,(u\times v) vrot=cosθv+(1cosθ)(uv)u+sinθ(u×v)
(u⋅v)u(u\cdot v)u(uv)u 替换掉:
vrot=cos⁡θ v+(1−cos⁡θ)[u×(u×v)+v]+sin⁡θ (u×v)=[cos⁡θ+(1−cos⁡θ)] v+(1−cos⁡θ) u×(u×v)+sin⁡θ (u×v) \begin{aligned} v_{\mathrm{rot}} &= \cos\theta\, v + (1-\cos\theta)\big[u\times(u\times v) + v\big] + \sin\theta\,(u\times v)\\ &= \big[\cos\theta + (1-\cos\theta)\big]\, v + (1-\cos\theta)\,u\times(u\times v) + \sin\theta\,(u\times v) \end{aligned} vrot=cosθv+(1cosθ)[u×(u×v)+v]+sinθ(u×v)=[cosθ+(1cosθ)]v+(1cosθ)u×(u×v)+sinθ(u×v)
注意中括号里 cos⁡θ+(1−cos⁡θ)=1\cos\theta + (1-\cos\theta) = 1cosθ+(1cosθ)=1,正好把 vvv 前面的系数化简成了 111,于是得到一个同样成立、但形式更简洁的等价公式
vrot=v+(1−cos⁡θ) u×(u×v)+sin⁡θ (u×v) \boxed{v_{\mathrm{rot}} = v + (1-\cos\theta)\, u\times(u\times v) + \sin\theta\,(u\times v)} vrot=v+(1cosθ)u×(u×v)+sinθ(u×v)
这个式子和第三节的原始公式完全等价(只是代数变形),好处是全程只需要"叉乘"这一种运算,不再需要单独算点积,写代码或者写成矩阵形式时会更整齐。

从等价公式直接得到旋转矩阵

叉乘可以写成矩阵乘法:任取向量 aaaa×va\times va×v 总能写成某个矩阵乘以 vvv,这个矩阵记作 [a][a][a](有的书里写作 [a]×[a]_\times[a]×),叫做由 aaa 生成的反对称矩阵(也叫斜对称矩阵,因为它满足 [a]⊤=−[a][a]^\top=-[a][a]=[a])。对于 u=(ux,uy,uz)u=(u_x,u_y,u_z)u=(ux,uy,uz),具体写出来是:
[u]=[0−uzuyuz0−ux−uyux0] [u] = \begin{bmatrix} 0 & -u_z & u_y \\ u_z & 0 & -u_x \\ -u_y & u_x & 0 \end{bmatrix} [u]= 0uzuyuz0uxuyux0
有了这个矩阵,u×vu\times vu×v 就等于 [u] v[u]\,v[u]v,而"叉乘两次" u×(u×v)u\times(u\times v)u×(u×v) 自然就等于把矩阵 [u][u][u] 连续乘两次,也就是 [u]2v[u]^2 v[u]2v。把这两个替换代入上面的等价公式:
vrot=v+(1−cos⁡θ) [u]2v+sin⁡θ [u] v=[I+(1−cos⁡θ) [u]2+sin⁡θ [u]] v v_{\mathrm{rot}} = v + (1-\cos\theta)\,[u]^2 v + \sin\theta\,[u]\,v = \Big[I + (1-\cos\theta)\,[u]^2 + \sin\theta\,[u]\Big]\, v vrot=v+(1cosθ)[u]2v+sinθ[u]v=[I+(1cosθ)[u]2+sinθ[u]]v
方括号里的这一整块就是把"轴角 (u,θ)(u,\theta)(u,θ)"转换成"旋转矩阵"的公式,记作 R(u,θ)R(u,\theta)R(u,θ)
R(u,θ)=I+(1−cos⁡θ) [u]2+sin⁡θ [u] \boxed{R(u,\theta) = I + (1-\cos\theta)\,[u]^2 + \sin\theta\,[u]} R(u,θ)=I+(1cosθ)[u]2+sinθ[u]
也就是说,只要按上面的公式先把 uuu 组装成反对称矩阵 [u][u][u],再代入这个式子,算出来的 R(u,θ)R(u,\theta)R(u,θ) 就是一个标准的 3×33\times33×3 旋转矩阵,之后直接用 vrot=R(u,θ) vv_{\mathrm{rot}}=R(u,\theta)\,vvrot=R(u,θ)v 就能对任意向量做旋转,不需要每次都重新走一遍点积、叉乘的推导过程——这在需要对同一个旋转反复作用在很多个点上时(比如旋转一整个点云)效率更高,因为矩阵只需要算一次。
这个从轴角推出旋转矩阵的过程,也是后面理解李群 SO(3)SO(3)SO(3) 上的指数映射、四元数与旋转矩阵相互转换的基础,这里先记住结论即可。

五、用一个具体数字例子过一遍(和图1(b)完全对应)

为了把公式落到实处,这里给出一组具体数字,图1(b) 就是严格按照这组数字画出来的,可以逐个数字对照图形理解:

  • 转轴:u=(0,0,1)u=(0,0,1)u=(0,0,1)(沿 zzz 轴)
  • 待旋转向量:v=(1,0,0.6)v=(1,0,0.6)v=(1,0,0.6)
  • 转角:θ=60°\theta=60°θ=60°
    一步步算:
  • v∥=(u⋅v) u=0.6×(0,0,1)=(0,0,0.6)v_\parallel = (u\cdot v)\,u = 0.6\times(0,0,1) = (0,0,0.6)v=(uv)u=0.6×(0,0,1)=(0,0,0.6)
  • v⊥=v−v∥=(1,0,0)v_\perp = v - v_\parallel = (1,0,0)v=vv=(1,0,0)
  • w=u×v=(0,1,0)w = u\times v = (0,1,0)w=u×v=(0,1,0)(可以验证 ∥w∥=1=∥v⊥∥\|w\|=1=\|v_\perp\|w=1=v,两者长度确实相等)
  • v⊥,rot=cos⁡60° (1,0,0)+sin⁡60° (0,1,0)=(0.5, 0.866, 0)v_{\perp,\mathrm{rot}} = \cos60°\,(1,0,0) + \sin60°\,(0,1,0) = (0.5,\ 0.866,\ 0)v,rot=cos60°(1,0,0)+sin60°(0,1,0)=(0.5, 0.866, 0)
  • vrot=v∥+v⊥,rot=(0.5, 0.866, 0.6)v_{\mathrm{rot}} = v_\parallel + v_{\perp,\mathrm{rot}} = (0.5,\ 0.866,\ 0.6)vrot=v+v,rot=(0.5, 0.866, 0.6)

六、计算流程一览

下面用一张流程图梳理"输入轴角和向量,算出旋转结果"的完整步骤,和上面的推导一一对应:

输入:轴 u(单位向量)
角度 θ
待旋转向量 v

计算平行分量
v_∥ = (u·v) u

计算垂直分量
v_⊥ = v - v_∥

构造正交基向量
w = u × v

在垂直平面内转 θ 角
v_⊥rot = cosθ v_⊥ + sinθ w

加回平行分量
v_rot = v_∥ + v_⊥rot

输出:v_rot
= cosθ v + (1-cosθ)(u·v) u + sinθ (u×v)

七、C++ 代码实现

下面给出一份完整、可以直接编译运行的 C++ 代码。分成两部分:第一部分是第三节推出的"直接对向量做旋转"的罗德里格斯公式;第二部分是第四节推出的"先算旋转矩阵 R(u,θ)R(u,\theta)R(u,θ),再用矩阵乘向量"的写法,并验证两种写法算出的结果完全一致。代码不依赖任何第三方库,只用了标准库的 <cmath>

#include <iostream>
#include <cmath>
// 用一个简单的结构体表示三维向量,只实现旋转计算需要用到的运算
struct Vec3 {
    double x, y, z;
    // 向量加法:对应分量相加
    Vec3 operator+(const Vec3& other) const {
        return {x + other.x, y + other.y, z + other.z};
    }
    // 向量减法:对应分量相减
    Vec3 operator-(const Vec3& other) const {
        return {x - other.x, y - other.y, z - other.z};
    }
    // 向量数乘:每个分量都乘上同一个标量 s
    Vec3 operator*(double s) const {
        return {x * s, y * s, z * s};
    }
    // 点积(内积):对应分量相乘后求和,结果是一个标量
    // 几何意义:反映两个向量方向的接近程度,也用来算投影长度
    double dot(const Vec3& other) const {
        return x * other.x + y * other.y + z * other.z;
    }
    // 叉积(外积):结果是一个新向量,同时垂直于参与运算的两个向量
    // 这里用的是标准的行列式展开公式
    Vec3 cross(const Vec3& other) const {
        return {
            y * other.z - z * other.y,
            z * other.x - x * other.z,
            x * other.y - y * other.x
        };
    }
    // 向量的模长(长度):勾股定理的三维版本
    double norm() const {
        return std::sqrt(x * x + y * y + z * z);
    }
    // 归一化:把向量方向不变,长度缩放成1,变成单位向量
    Vec3 normalized() const {
        double n = norm();
        return {x / n, y / n, z / n};
    }
};
// 核心函数:用罗德里格斯公式把向量 v 绕单位轴 u 旋转 theta 弧度
// 数学公式: v_rot = cosθ*v + (1-cosθ)*(u·v)*u + sinθ*(u×v)
Vec3 rotateByAxisAngle(const Vec3& v, const Vec3& u_raw, double theta) {
    // 第一步:把传入的轴强制归一化,保证 ||u|| = 1
    // 公式推导时用到了 ||u||=1 这个前提(比如叉乘模长公式里少乘了一个 ||u||),
    // 如果轴不是单位向量,直接代公式会得到错误结果
    Vec3 u = u_raw.normalized();
    double cosTheta = std::cos(theta);
    double sinTheta = std::sin(theta);
    // 第一项:v 整体按 cosθ 缩放
    // 这是当 theta 很小或者 v 本身就在轴上时的"主项"
    Vec3 term1 = v * cosTheta;
    // 第二项:(1-cosθ)(u·v) u
    // u·v 先算出 v 在轴方向上的投影长度,乘上 (1-cosθ) 是在补偿
    // "平行分量本不该被 cosθ 缩放"这件事(因为平行分量根本不参与旋转)
    double u_dot_v = u.dot(v);
    Vec3 term2 = u * (u_dot_v * (1.0 - cosTheta));
    // 第三项:sinθ (u×v)
    // u×v 就是推导过程中的"帮手向量" w,代表垂直分量旋转后
    // 新增出来的、原来的 v 里完全没有的那个方向分量
    Vec3 term3 = u.cross(v) * sinTheta;
    // 三项相加,就是旋转后的向量
    return term1 + term2 + term3;
}
// 打印一个向量,方便观察结果
void printVec(const char* name, const Vec3& v) {
    std::cout << name << " = (" << v.x << ", " << v.y << ", " << v.z << ")\n";
}
// ------------------------------------------------------------
// 下面是第四节推导的"旋转矩阵"写法:先把轴角组装成一个3x3矩阵R,
// 再用 v_rot = R * v 做旋转。用一个简单的3x3矩阵结构体来实现。
// ------------------------------------------------------------
struct Mat3 {
    double m[3][3]; // m[行][列]
    // 矩阵乘向量:标准的矩阵乘法定义,第i个分量 = 第i行 点乘 v
    Vec3 multiply(const Vec3& v) const {
        return {
            m[0][0]*v.x + m[0][1]*v.y + m[0][2]*v.z,
            m[1][0]*v.x + m[1][1]*v.y + m[1][2]*v.z,
            m[2][0]*v.x + m[2][1]*v.y + m[2][2]*v.z
        };
    }
    // 矩阵乘矩阵:用来计算 [u]的平方,即 [u]乘以[u]自己
    Mat3 multiply(const Mat3& other) const {
        Mat3 result{};
        for (int i = 0; i < 3; ++i) {
            for (int j = 0; j < 3; ++j) {
                double sum = 0.0;
                for (int k = 0; k < 3; ++k) {
                    sum += m[i][k] * other.m[k][j];
                }
                result.m[i][j] = sum;
            }
        }
        return result;
    }
    // 矩阵数乘:每个元素都乘上标量s
    Mat3 scale(double s) const {
        Mat3 result{};
        for (int i = 0; i < 3; ++i)
            for (int j = 0; j < 3; ++j)
                result.m[i][j] = m[i][j] * s;
        return result;
    }
    // 矩阵加法:对应元素相加
    Mat3 add(const Mat3& other) const {
        Mat3 result{};
        for (int i = 0; i < 3; ++i)
            for (int j = 0; j < 3; ++j)
                result.m[i][j] = m[i][j] + other.m[i][j];
        return result;
    }
};
// 单位矩阵 I:对角线是1,其余是0
Mat3 identity3() {
    Mat3 I{};
    I.m[0][0] = I.m[1][1] = I.m[2][2] = 1.0;
    return I;
}
// 由向量u构造反对称矩阵 [u],满足对任意v都有 [u]*v == u.cross(v)
Mat3 skewMatrix(const Vec3& u) {
    Mat3 S{};
    S.m[0][0] = 0;    S.m[0][1] = -u.z; S.m[0][2] =  u.y;
    S.m[1][0] = u.z;  S.m[1][1] = 0;    S.m[1][2] = -u.x;
    S.m[2][0] = -u.y; S.m[2][1] = u.x;  S.m[2][2] = 0;
    return S;
}
// 核心函数:由轴角 (u, theta) 构造旋转矩阵 R(u,theta)
// 数学公式: R(u,θ) = I + (1-cosθ)*[u]^2 + sinθ*[u]
Mat3 axisAngleToMatrix(const Vec3& u_raw, double theta) {
    Vec3 u = u_raw.normalized();      // 同样要先保证是单位向量
    Mat3 U = skewMatrix(u);           // 反对称矩阵 [u]
    Mat3 U2 = U.multiply(U);          // [u]的平方,对应 u×(u×v) 这个操作
    double cosTheta = std::cos(theta);
    double sinTheta = std::sin(theta);
    // 按公式把三项加起来: I + (1-cosθ)[u]^2 + sinθ[u]
    Mat3 R = identity3()
                .add(U2.scale(1.0 - cosTheta))
                .add(U.scale(sinTheta));
    return R;
}
int main() {
    // ---- 例1:对应文档第四节的数值例子,方便和图1(b)对照 ----
    Vec3 u1 = {0.0, 0.0, 1.0};   // 转轴:z轴方向
    Vec3 v1 = {1.0, 0.0, 0.6};   // 待旋转向量
    double theta1 = 60.0 * M_PI / 180.0;  // 60度转成弧度
    Vec3 v1_rot = rotateByAxisAngle(v1, u1, theta1);
    std::cout << "===== 例1:与图1(b)对应的数值例子 =====\n";
    printVec("v", v1);
    printVec("v_rot", v1_rot);
    std::cout << "理论结果应为 (0.5, 0.866, 0.6),可以看到数值基本吻合\n\n";
    // ---- 例2:绕z轴转90度,验证结果符合直觉 ----
    // x轴方向的向量绕z轴转90度,应该正好转到y轴方向
    Vec3 u2 = {0.0, 0.0, 1.0};
    Vec3 v2 = {1.0, 0.0, 0.0};
    double theta2 = M_PI / 2.0;
    Vec3 v2_rot = rotateByAxisAngle(v2, u2, theta2);
    std::cout << "===== 例2:验证直角旋转 =====\n";
    printVec("v", v2);
    printVec("v_rot", v2_rot);
    std::cout << "理论结果应为 (0, 1, 0)\n\n";
    // ---- 例3:用旋转矩阵方法重算例1,验证两种写法结果一致 ----
    Mat3 R = axisAngleToMatrix(u1, theta1);   // 先把轴角组装成矩阵R
    Vec3 v1_rot_by_matrix = R.multiply(v1);   // 再用矩阵乘向量
    std::cout << "===== 例3:矩阵法与向量公式法对比(应与例1结果相同) =====\n";
    printVec("v_rot(旋转矩阵法)", v1_rot_by_matrix);
    printVec("v_rot(向量公式法,例1结果)", v1_rot);
    return 0;
}

代码逻辑讲解

整段代码分成三块:
Vec3 结构体:把三维向量需要的基本运算(加、减、数乘、点积、叉积、归一化)都封装成成员函数,这样后面写公式的时候可以像写数学表达式一样直接写 v * cosThetau.cross(v),代码和公式基本一一对应,不容易出错。
rotateByAxisAngle 函数:这是整段代码的核心,严格按照推导出的三项公式实现:

  • 先对轴做 normalized() 归一化,这一步容易被忽略,但公式的正确性完全依赖 ∥u∥=1\|u\|=1u=1 这个前提,如果直接传入一个没有归一化的轴,算出来的结果会是错的;
  • term1term2term3 三个变量分别对应公式里的三项,命名和顺序都和数学公式保持一致,方便对照检查;
  • 最后把三项相加返回,就是旋转后的向量。
    Mat3 结构体:表示一个 3×33\times33×3 矩阵,实现了矩阵乘向量(multiply(const Vec3&))、矩阵乘矩阵(multiply(const Mat3&))、矩阵数乘(scale)、矩阵加法(add)四个基本操作,第四节推出的矩阵公式里出现的运算,这里都能找到对应的函数。
    skewMatrix 函数:按第四节给出的公式,把向量 uuu 组装成反对称矩阵 [u][u][u]。这个矩阵的作用等价于叉乘:对任意向量 vvvskewMatrix(u).multiply(v) 算出来的结果和 u.cross(v) 应该完全一样,这是理解后面矩阵公式的关键一步。
    axisAngleToMatrix 函数:这是矩阵形式的核心,严格对应公式 R(u,θ)=I+(1−cos⁡θ)[u]2+sin⁡θ[u]R(u,\theta)=I+(1-\cos\theta)[u]^2+\sin\theta[u]R(u,θ)=I+(1cosθ)[u]2+sinθ[u]
  • 先对轴归一化(原因和向量法一样,公式依赖 ∥u∥=1\|u\|=1u=1);
  • skewMatrix 算出 [u][u][u],再用矩阵乘矩阵算出 [u]2[u]^2[u]2(对应恒等式里"叉乘两次"的操作);
  • 最后按公式把单位矩阵、(1−cos⁡θ)[u]2(1-\cos\theta)[u]^2(1cosθ)[u]2sin⁡θ[u]\sin\theta[u]sinθ[u] 三部分加在一起,返回旋转矩阵 RRR
    main 函数:一共给了三个测试例子。例1直接用文档里推导用的数字,算出来的结果应该约等于 (0.5,0.866,0.6)(0.5, 0.866, 0.6)(0.5,0.866,0.6),可以和图1(b)里的 vrotv_{\mathrm{rot}}vrot 对上;例2是一个更容易靠直觉验证的例子——把 xxx 轴方向的向量绕 zzz 轴转 90 度,结果理应正好落在 yyy 轴上;例3用矩阵法重新计算了一遍例1的场景,用来验证"直接对向量套公式"和"先算矩阵再乘向量"这两种写法算出来的结果完全一致,从代码层面印证第四节的推导没有问题。
    编译运行方式(Linux/Mac 下):
g++ -O2 -o rodrigues rodrigues.cpp
./rodrigues

八、轴角表示在旋转描述家族里的位置

三维旋转常见的几种描述方式各有优劣,简单对比一下,方便理解轴角表示适合用在什么场景:

表示方法 自由度 优点 缺点
旋转矩阵 9个数(受6个约束) 直接可用于变换点,无奇异性 参数冗余,不便于优化和插值
欧拉角 3个数 直观,符合日常语言习惯 存在万向锁(Gimbal Lock)奇异性
轴角/旋转向量 3个数 紧凑,无冗余,适合参与优化(比如非线性最小二乘) 角度接近 0002π2\pi2π 附近时轴的方向不稳定
四元数 4个数(受1个约束) 无奇异性,插值(球面线性插值)效果好 不直观,理解门槛稍高

轴角表示的最大优势在于只用三个自由度就完整描述了一次旋转(转轴用两个自由度描述方向,加上一个角度),既不像旋转矩阵那样冗余,也不像欧拉角那样存在万向锁问题,所以在 SLAM、三维重建、机器人位姿估计等需要对旋转做优化迭代的场景里非常常用,比如著名的 Ceres Solver、g2o 等优化库内部都大量使用旋转向量作为旋转的优化变量。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐