GAMES001 Rotation学习:欧拉角(Euler Angle)详细讲解
一、先从最直观的问题说起:怎么描述一个物体"转了多少"
想象你手里拿着一架玩具飞机,飞机在空间里的朝向(姿态)可以用三个互相垂直的坐标轴 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)中有非常直观的例子:同样是"绕 XXX 转 90∘90^\circ90∘" 加上"绕 ZZZ 转 90∘90^\circ90∘",仅仅是顺序换了一下,最终朝向就从 +X+X+X 方向变成了 −Y-Y−Y 方向,完全不同。
这也是为什么,只要一提到"欧拉角",就必须同时说清楚"是哪种旋转顺序"——脱离了顺序单独说"欧拉角是 (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_3X1Z2Y3、X1Y2Z3X_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θ0−sinθcosθ
,RY(θ)=
cosθ0−sinθ010sinθ0cosθ
,RZ(θ)=
cosθsinθ0−sinθcosθ0001
规律很好记:转哪根轴,对应那一行那一列就保持"单位矩阵"的样子(比如绕 XXX 转,第一行第一列就是 1,0,01,0,01,0,0),剩下的 2×22\times22×2 子块就是标准的二维旋转矩阵 [cos−sinsincos]\begin{bmatrix}\cos&-\sin\\\sin&\cos\end{bmatrix}[cossin−sincos],只是嵌入的位置、正负号会因为坐标轴顺规(右手系)略有不同。
第二步,把三次旋转依次"复合"起来。注意复合的方向:如果旋转顺序是先绕 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=
c1c2c2s1−s2c1s2s3−c3s1c1c3+s1s2s3c2s3s1s3+c1c3s2c3s1s2−c1s3c2c3
这正是航空航天里最常用的"偏航-俯仰-横滚"(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-Y−Y |
两组实验用的是完全相同的两个旋转动作,仅仅是先后顺序换了一下,最终朝向却截然不同(+X+X+X 对比 −Y-Y−Y)。这就是"旋转群不满足交换律"最直观的体现:用数学语言写出来就是
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θ2 或 cosθ2\cos\theta_2cosθ2(这样的位置通常只有一个,因为 θ2\theta_2θ2 是"中间那次旋转",在矩阵展开式里最容易单独出现),就可以先用 arcsin\arcsinarcsin 或 arccos\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ψ0−sinψ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θ0−sinθ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ϕ0−sinϕ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(ψ)−1⋅v1=Rz(ψ)Ry(θ)Rz(ψ)−1⋅Rz(ψ)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}RR−1R 抵消),抵消之后剩下的矩阵乘积顺序,正好和外旋反过来。
所以只要记住第 3、4 节里两个加框的公式,这道作业题的答案就是"顺序整体颠倒,内旋外旋等价"这一句话,加上上面的公式即可。
6. 用具体数字验证一下
为了确认公式没有理解错,取一组简单角度代入验证矩阵乘法顺序确实不同(也就是内旋和外旋,如果轴序和角度都不颠倒,结果一般不相等)。
取 ψ=90°,θ=90°,ϕ=0°\psi = 90°, \theta = 90°, \phi = 0°ψ=90°,θ=90°,ϕ=0°(ϕ=0\phi=0ϕ=0 时 Rx(ϕ)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°)=
010−100001
,Ry(90°)=[001010−100]R_y(90°) = \begin{bmatrix}0&0&1\\0&1&0\\-1&0&0\end{bmatrix}Ry(90°)=
00−1010100
内旋: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°)= 010−100001 00−1010100 = 00−1−100010
外旋: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°)= 00−1010100 010−100001 = 01000−1100
可以看到两个矩阵确实不同——这说明"同一组角度、同一个轴序名字(先 z 后 y)“,用内旋理解和用外旋理解,结果是两种不同的姿态。这正是为什么使用欧拉角时必须明确标注"这是内旋还是外旋、轴的顺序是什么”,否则同一串数字会指向两个完全不同的姿态。
如果按第 5 节的规律,把外旋的顺序颠倒(先绕固定 zzz 转 90°90°90°,再绕固定 yyy 转 90°90°90°,即 Ry(90°)Rz(90°)R_y(90°)R_z(90°)Ry(90°)Rz(90°),这正好是我们上面算的"外旋"那一项!)会发现它恰好等于"先绕动 zzz 转 90°90°90°、再绕动 y′y'y′ 转 90°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. 常见误区提醒
- 不要以为"轴序名字一样,内旋外旋结果就一样":第 6 节的算例已经证明,同样是"先 z 后 y",内旋和外旋算出来的矩阵是不同的两个矩阵。
- 内旋记忆口诀:“轴跟着物体转,矩阵往右加”;外旋记忆口诀:“轴焊死在世界上,矩阵往左加”。
- 在机器人学、航空航天、计算机图形学中,不同软件库(比如某些引擎用 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 顺序”),三步旋转依次是:
- 先绕航向轴(此时航向轴和世界坐标系的竖直轴重合)转 ψ\psiψ(航向角);
- 再绕转过一次之后的俯仰轴转 θ\thetaθ(俯仰角);
- 最后绕转过两次之后的横滚轴转 ϕ\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. 具体数值例子:一次完整的姿态变化
假设一架飞机(或者一个游戏角色的朝向)依次做了三个动作:
- 先向右转身(航向角)ψ=30°\psi = 30°ψ=30°;
- 再抬头(俯仰角)θ=15°\theta = 15°θ=15°;
- 最后向右侧倾斜身体(横滚角)ϕ=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°)
把第一份讲义里给出的三个单轴旋转矩阵代入,依次用 cos30°≈0.866, sin30°=0.5\cos30°\approx0.866,\ \sin30°=0.5cos30°≈0.866, sin30°=0.5,cos15°≈0.966, sin15°≈0.259\cos15°\approx0.966,\ \sin15°\approx0.259cos15°≈0.966, sin15°≈0.259,cos10°≈0.985, sin10°≈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.50−0.50.8660001 ,Ry(15°)= 0.9660−0.2590100.25900.966 ,Rx(10°)= 10000.9850.1740−0.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.9660−0.2590.0450.9850.1680.255−0.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.483−0.259−0.4510.8980.1680.312−0.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°0° 为例。模式名字里字母的顺序,就是三次旋转真正发生的先后顺序,并且都是绕物体"自身不断更新的轴"转(也就是前两份讲义讲过的内旋):
- 第一步:绕物体自身的 XXX 轴转 60°60°60°;
- 第二步:绕转过一次之后的 YYY 轴转 20°20°20°(注意这已经不是世界坐标系的 yyy 轴了);
- 第三步:绕转过两次之后的 ZZZ 轴转 0°0°0°(因为这个例子里 Z 填的是 0°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=0° 代入(因为 Z=0°Z=0°Z=0°,Rz(0°)R_z(0°)Rz(0°) 是单位矩阵,相当于没转),算出来大约是:
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.296−0.17100.50.8660.342−0.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°)=
00−1cosψ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°)=
00−1−sin(ψ−ϕ)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} 00−1−0.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} 00−1−0.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} 00−1−0.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}^3u∈R3,且 ∥u∥=1\|u\|=1∥u∥=1(必须是单位向量,否则"转多少度"这件事就没有统一的度量标准);
- 旋转角度:θ∈R\theta \in \mathbb{R}θ∈R(正负号约定:从轴的正方向看过去,逆时针为正,遵循右手定则)。
把这两个信息合并成一个向量,就得到了旋转向量(rotation vector):
θ=θu∈R3 \boldsymbol{\theta} = \theta u \in \mathbb{R}^3 θ=θu∈R3
这个向量非常巧妙:它的方向就是转轴方向,它的长度就是旋转的角度。一个向量同时装下了"轴"和"角"两个信息,这也是它在工程上(比如机器人学、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∥ 就是 vvv 在 uuu 方向上的投影,用点积公式直接写出来:
v∥=(u⋅v) u v_\parallel = (u \cdot v)\, u v∥=(u⋅v)u
(u⋅vu\cdot vu⋅v 是一个标量,表示投影的长度;乘上 uuu 把这个长度变回一个沿 uuu 方向的向量)
剩下的部分自然就是垂直分量:
v⊥=v−v∥ v_\perp = v - v_\parallel v⊥=v−v∥
(对应 图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 有两个很好的性质:
- www 垂直于 v⊥v_\perpv⊥:因为叉乘的结果必然垂直于参与叉乘的两个向量,所以 w=u×v⊥w=u\times v_\perpw=u×v⊥ 同时垂直于 uuu 和 v⊥v_\perpv⊥;既然 www 垂直于 uuu,它自然也躺在那个与 uuu 垂直的平面里,和 v⊥v_\perpv⊥ 在同一个平面内;
- www 和 v⊥v_\perpv⊥ 长度相等:叉乘的模长公式是 ∥u×v⊥∥=∥u∥ ∥v⊥∥sinϕ\|u\times v_\perp\| = \|u\|\,\|v_\perp\|\sin\phi∥u×v⊥∥=∥u∥∥v⊥∥sinϕ,这里 ϕ\phiϕ 是 uuu 和 v⊥v_\perpv⊥ 的夹角。因为 v⊥⊥uv_\perp\perp uv⊥⊥u,夹角 ϕ=90°\phi=90°ϕ=90°,sinϕ=1\sin\phi=1sinϕ=1;又因为 ∥u∥=1\|u\|=1∥u∥=1,所以 ∥w∥=∥v⊥∥\|w\| = \|v_\perp\|∥w∥=∥v⊥∥。
一句话总结:www 就是把 v⊥v_\perpv⊥ 在这个垂直平面内原地转 90 度得到的向量。有了 v⊥v_\perpv⊥ 和 www 这一组等长且正交的基,这个平面内任意角度的旋转都可以写出来了。
(对应 图1(b) 中的蓝色箭头 w=u×vw=u\times vw=u×v,以及底部那条从 v⊥v_\perpv⊥ 转到某一点、标注了 θ\thetaθ 的圆弧——这条圆弧画在垂直于 uuu 的单位圆上,直观地表示"转过角度 θ\thetaθ"这件事到底发生在哪个平面里)
第三步:套用二维旋转公式,再把平行分量加回来
有了 v⊥v_\perpv⊥ 和 www 这组正交基,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⊥=v−v∥、w=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θ(v−v∥)+sinθ(u×v)=cosθv+(1−cosθ)v∥+sinθ(u×v)
再把 v∥=(u⋅v)uv_\parallel=(u\cdot v)uv∥=(u⋅v)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+(1−cosθ)(u⋅v)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)=(u⋅v)u−v
三维向量的二重叉乘有一个很经典的恒等式(常被称为"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(a⋅c)−c(a⋅b)
把 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(u⋅v)−v(u⋅u)
因为 uuu 是单位向量,u⋅u=∥u∥2=1u\cdot u = \|u\|^2 = 1u⋅u=∥u∥2=1,所以:
u×(u×v)=(u⋅v) u−v u\times(u\times v) = (u\cdot v)\,u - v u×(u×v)=(u⋅v)u−v
这个恒等式的意义在于:它把"点积构造出的平行分量 (u⋅v)u(u\cdot v)u(u⋅v)u"和"两次叉乘"联系了起来,这样一来,罗德里格斯公式里就不需要点积了,全部可以用叉乘表示,这在计算机里实现起来更统一(只需要一个叉乘操作就够了)。
用恒等式改写罗德里格斯公式
把上面的恒等式变形一下,可以反解出:
(u⋅v) u=u×(u×v)+v (u\cdot v)\,u = u\times(u\times v) + v (u⋅v)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+(1−cosθ)(u⋅v)u+sinθ(u×v)
把 (u⋅v)u(u\cdot v)u(u⋅v)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+(1−cosθ)[u×(u×v)+v]+sinθ(u×v)=[cosθ+(1−cosθ)]v+(1−cosθ)u×(u×v)+sinθ(u×v)
注意中括号里 cosθ+(1−cosθ)=1\cos\theta + (1-\cos\theta) = 1cosθ+(1−cosθ)=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+(1−cosθ)u×(u×v)+sinθ(u×v)
这个式子和第三节的原始公式完全等价(只是代数变形),好处是全程只需要"叉乘"这一种运算,不再需要单独算点积,写代码或者写成矩阵形式时会更整齐。
从等价公式直接得到旋转矩阵
叉乘可以写成矩阵乘法:任取向量 aaa,a×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]=
0uz−uy−uz0uxuy−ux0
有了这个矩阵,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+(1−cosθ)[u]2v+sinθ[u]v=[I+(1−cosθ)[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+(1−cosθ)[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∥=(u⋅v)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⊥=v−v∥=(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=cos60° (1,0,0)+sin60° (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)
六、计算流程一览
下面用一张流程图梳理"输入轴角和向量,算出旋转结果"的完整步骤,和上面的推导一一对应:
七、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 * cosTheta、u.cross(v),代码和公式基本一一对应,不容易出错。rotateByAxisAngle 函数:这是整段代码的核心,严格按照推导出的三项公式实现:
- 先对轴做
normalized()归一化,这一步容易被忽略,但公式的正确性完全依赖 ∥u∥=1\|u\|=1∥u∥=1 这个前提,如果直接传入一个没有归一化的轴,算出来的结果会是错的; term1、term2、term3三个变量分别对应公式里的三项,命名和顺序都和数学公式保持一致,方便对照检查;- 最后把三项相加返回,就是旋转后的向量。
Mat3结构体:表示一个 3×33\times33×3 矩阵,实现了矩阵乘向量(multiply(const Vec3&))、矩阵乘矩阵(multiply(const Mat3&))、矩阵数乘(scale)、矩阵加法(add)四个基本操作,第四节推出的矩阵公式里出现的运算,这里都能找到对应的函数。skewMatrix函数:按第四节给出的公式,把向量 uuu 组装成反对称矩阵 [u][u][u]。这个矩阵的作用等价于叉乘:对任意向量 vvv,skewMatrix(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+(1−cosθ)[u]2+sinθ[u]: - 先对轴归一化(原因和向量法一样,公式依赖 ∥u∥=1\|u\|=1∥u∥=1);
- 用
skewMatrix算出 [u][u][u],再用矩阵乘矩阵算出 [u]2[u]^2[u]2(对应恒等式里"叉乘两次"的操作); - 最后按公式把单位矩阵、(1−cosθ)[u]2(1-\cos\theta)[u]^2(1−cosθ)[u]2、sinθ[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个数 | 紧凑,无冗余,适合参与优化(比如非线性最小二乘) | 角度接近 000 或 2π2\pi2π 附近时轴的方向不稳定 |
| 四元数 | 4个数(受1个约束) | 无奇异性,插值(球面线性插值)效果好 | 不直观,理解门槛稍高 |
轴角表示的最大优势在于只用三个自由度就完整描述了一次旋转(转轴用两个自由度描述方向,加上一个角度),既不像旋转矩阵那样冗余,也不像欧拉角那样存在万向锁问题,所以在 SLAM、三维重建、机器人位姿估计等需要对旋转做优化迭代的场景里非常常用,比如著名的 Ceres Solver、g2o 等优化库内部都大量使用旋转向量作为旋转的优化变量。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)