四元数旋转:从一维到四维
Main
1、前言
本文讨论四元数与三维空间中的旋转关系,也就是四元数从四维空间对三维空间里的向量起了什么作用。众所周知,四元数存在于四维中,这也让它蒙上了一层神秘的色彩;很多文章把它描述得更加神秘,而有些文章为了使其“去神秘化”又写得太过简短,导致很多四元数的细节都被略过了。网上关于四元数的优秀的文献有很多,复制粘贴的也非常多,更有甚者连偏手性都没声明;本人在使用四元数的过程中也产生过一些疑惑,在得到解决后也想写一篇总结性的文章。
以上种种,成为本文的写作动机。三维空间中的旋转其实比想象中要复杂得多,比如二维旋转的次序无关紧要而三维的则不然(数学上的表达就是不满足交换律);再比如我们生活中习以为常的“前后左右”等概念是相对的和模糊的(对此,数学上的严格表达就是偏手性和多坐标系);还有像使用欧拉角导致的万向节锁定的问题存在(四元数则没有这个问题),等等。正因为三维旋转具有这些特性,本文的论述从一维空间开始,逐步升维,在当前维度中阐述对应的数学概念,这有利于更好地理解旋转的本质。由于四元数就是升维了的复数,通过对升维的阐述,把前后相关的知识联系起来。
本文用通俗易懂的方式论述,仅需最基本的线性代数知识即可阅读。但由于是应用于游戏、图形学方向,本文更强调的是四元数(以及其他维度中的数学概念)带来的几何意义;再加上笔者本人才疏学浅的事实,所以如果您是想从更高层次的抽象代数来阅读的话,那么本文并不适合您。本文的结构为:
- 前面的部分,用一些基础的线性代数和几何学知识,来推导和理解四元数的乘积公式;
- 后面的部分,用可视化的方式,看看四元数究竟是怎么旋转三维中的向量的。由于四元数存在于四维空间中,对三维的我们来说已经不可见,但是我们可以借用“球极投影”的方法,看看它在三维中的直观投影。有了前置的知识,更有利于这一部分的理解;
- 最后的部分,我们让四元数在代码中实现。
- 另外,为了加速计算过程,我将省略一些公式的推导或者证明过程。大多数时候即使不清楚它们的推导过程也不会有什么问题,但如果读者感兴趣,可以移步到扩展阅读部分阅读。
重要约定
偏手性
偏手性和左右手定则大多数时候只是数学上的惯用约定,实际上可以自由组合。由于unity大多数的坐标空间是左手坐标系,因此本文将采用:
- 左手坐标系;
- 左手定则。这意味着,从对着旋转轴的方向看,向量顺时针旋转;影响叉乘方向。
符号约定
- 向量:使用小写粗体字母,如:
- 四元数:使用小写细体字母,如:
- 纯四元数:使用小写细体字母,如:
- 矩阵:使用大写粗体字母,如:
- 集合:使用黑板报粗体字母,如:
2、一维
一维中只有点和线,可以用一根实数轴和依附于上面的单位刻度来表示数,如下图所示:
加法可以看作是把 0 向左或右移动到相加的结果的位置,比如
乘法可以看作是把 0 固定住,并以它为原点将 1 拉伸(缩放)到相乘的结果的位置,比如
比起代数运算结果,我们更应该关注的是运算带来的移动、缩放的动作本身,它就是抽象结果在具象化的过程。在其他的维度空间也有着类似的这种动作。
3、二维
由于二维空间是个平面,到了这里终于可以有旋转的存在了。我们可以像上一个章节那样构造一个由两个数轴围起来的平面,在这里我们用复平面来表示,也就是一个实轴和一个虚轴。复平面我们放到复数的后面来论述,在此之前先看下二维旋转的分解。
旋转的分解
假设向量
如下图所示:
旋转矩阵
也可以利用三角函数构造旋转矩阵。设旋转前的向量为
而对于旋转后的向量
根据三角公式可以变形为:
用原向量的
用矩阵乘法来表达就是:
因此,这个旋转矩阵就是
如下图所示:
在二维旋转中,施加旋转的次序是无关紧要的。比如说原向量
旋转矩阵表示的旋转,与式子
复数
数学上用
模长:
乘法运算:
请注意,本文将不会对复数进行详细地阐述,只会对一些跟旋转有关以及与前后文有联系的部分进行论述。如果想了解更多复数的细节,可以到网上参考更多资料。
类似于在一维中构造数轴的方式,可以用复数来构造一个二维平面,如下图所示,横轴代表实轴,而竖轴则代表虚轴:
可以看到,复数
再来看下
观察上图,可以看出
这个式子解释了复数乘法与二维旋转的关系。观察式子的右边,它跟我们在上个章节的式子
如果计算一下,会发现
通过观察可以得出这个旋转矩阵的复数形式是
复平面
复数在几何上有非常形象的解释,如下图所示:
由于规定了
我们还可以参照一维那个章节里展示的思想,构造一张网格,代表复平面中的复数。加法可以看作是平移整张网格,并把
4、三维
万向节锁定的本质
与二维旋转不同,三维的旋转顺序是不能变换的,数学上的表达就是运算不满足交换律。比如一个六面不同颜色的魔方,按照 unity 场景视图中的坐标轴为旋转轴,按照
使用欧拉角有万向节锁定的风险。在 unity 的场景视图中就可以“人为制造”一个万向锁。具体的做法是,在场景中放置一个立方体,然后在 Inspector 窗口的 Transform 中把 Rotation 的 x 设为
对于万向节锁定的解释,有两种比较流行的说法。一种是从旋转矩阵出发,用代数计算的方式可以提供一种优雅而严谨的证明,这种解释需要构造三维旋转矩阵,有兴趣的读者可以在扩展阅读部分中阅读
-
当我们尝试绕
轴旋转时,由于 轴旋转已经改变了物体的朝向,原本应该垂直于 轴的 轴旋转,现在看起来像是在绕 轴旋转。 -
同样地,当我们尝试绕
轴旋转时,它实际上会表现出类似绕 轴的效果,因为 轴旋转已经使 轴和 轴的旋转效果变得难以区分。
虽然我们在直觉上认为我们是在绕不同的轴旋转,但由于每次旋转都是相对于初始姿态
三维旋转的分解
我们可以借鉴二维里分解旋转向量的思想,把三维向量也同样分解成对应的分量,从而用平面的方式解决问题。如下图所示:
图中是同一个轴角系统的两个不同视角,右边是俯视图。设三维向量
罗德里格旋转公式
为了求出
根据上一节的向量分解,可得:
根据
而
再看
将以上变量代入式子
将这个式子整理一下可得:
这就是罗德里格旋转公式,以法国数学家 Benjamin Olinde Rodrigues 的名字命名的。我们在后面论述四元数的章节里将会证明四元数乘法与此处的式子是等价的。
构造三维复数?
在
为什么用两个虚轴和一个实轴无法描述三维的旋转呢?这个问题有很多种说法,这里用一种几何上的解释。如下图所示:
图中的球体是单位球,由于在二维中复数的旋转路径是一个圆,所以在三维里“三维复数”的则是一个球体。现在假设它是存在的,让球面(二维)上的点乘以一个单位“三维复数”
请想象一下,乘法运算之后球面上所有的箭头(这里只画了几个)都会指向自身的唯一方向,除了两处:那就是上下两个极点。这两处的方向是不确定的,这就好比地球的经纬度描述在两个极点处会失效一样,用数学语言来描述就是:使用三个参数无法完整地覆盖三维旋转的参数空间。拓扑学上的“毛球定理”
5、四维
既然“三维复数”不行,哈密顿转而寻找四维的数。1843年10月16日,在前往爱尔兰皇家科学院参会的路上,他灵光一闪,觉得应该使用三个虚数(
四元数基本运算
对于四元数
注:上述表格乘法顺序按照“列_行”的顺序进行。
除了
其中的
由于三维坐标系中三个轴都被替代为虚数轴,而第四个轴,即实数轴,它存在于四维空间,以我们无法理解的方式同时正交于这三个本身互相正交的虚数轴。所以,四元数是以
请注意,本文不会介绍四元数的矩阵形式、对数型、指数型、点乘和叉乘等概念,只会简单地介绍一些接下来要用到的基本运算。以下是四元数的一些基本计算方式:
- 标准乘法:(由于
和 这种变量名会让计算结果显得非常凌乱,此处用 和 来代替)
使用表格
设
- 模长:
- 共轭(记法为
):
- 逆(记为
):
的计算
这里到了最关键的环节。我们同样用
由式子
由于
将
使用分配律变形上式,可得:
设
上式
将式子
设
到这一步,四元数对向量(或者说纯四元数)的旋转作用已经非常直观了:对于
但是数学公式追求一种最简洁的美,上式还可以继续变形。
因为
又因为
至此,得出了上式形式的四元数一般性的乘法公式,非常简洁。
总结一下,设
与罗德里格旋转公式的比较
以上的四元数乘法,其结果等价于之前论述的罗德里格旋转公式(式子
证明:
令
由于罗德里格旋转公式是旋转后的向量表示,对应于四元数里的虚部。因此,只需要计算式子
其中,
根据向量三重积公式
将
因为
将
利用三角公式化简
则所得结果与罗德里格旋转公式一致。
6、可视化
四元数藏匿于四维空间,想实现真正的可视化是不现实的,我们只能通过其在三维空间中的投影来建立起对它的几何直觉。关于四元数可视化的这部分,建议观看 3Blue 1Brown 的视频,更直观:四元数的可视化_哔哩哔哩_bilibili 。但是应该提醒自己,关注点不是作者的酷炫动画展示,而是他在视频中提到的左乘和右乘的不同影响。直观去感受四元数旋转在三维中的投影变化方式对理解它没有什么大的帮助,因此本文将补充一点视频中没提到的部分。以下部分图片来源于他视频中的截图。
通常采用“球极投影”(Stereographic Projection)的方法来实现,如下图所示:
从球面的极点出发引一条射线,穿过球面上的某一个点最后投射到平面上,则球面上的点被投影到平面。如上图黄色的线是投影线,
视频中作者从
如上图所示:通过
三维球体投影到二维平面上保持不变的部分是一个圆(赤道),而四维超球体投影到我们三维空间中,保持不变的部分则是一个同时穿过
让我们先考虑三个虚轴上的情景。当一个单位四元数左乘一个四元数时,如
但是如果是右乘,则是相反的操作,此时遵循左手定则。
其他的两个虚轴也是类似的。
除了三个虚轴,还可以扩展到任意轴:
现在回到四元数乘法公式上:
当右乘
以上的左乘和右乘对点(向量)的影响,才是四元数可视化中最重要的地方。它在三维中炫酷的展开方式只是一种几何直觉,对理解四元数没有多大的帮助。并且,从视频里也能看出,当进行四元数乘法计算时,每个点都在移动(旋转),这也说明其不会遇到
如果以上图中展示的方法还是感觉太麻烦,还有一种十分简便的方法来理解:用您的双手,保持每只手的大拇指都和其余四指垂直,弯曲四指,然后让双手的小拇指弯曲的形状重合。此时,两个大拇指的方向是相反的,因此,大拇指方向的旋转互相抵消,而四指弯曲方向的旋转叠加了。
7、代码实现
得益于四元数乘法公式的简洁性,它在代码中实现非常地简单,比较需要注意的地方是在归一化的操作中要注意避免除零的计算风险。在本节代码中只计算纯旋转的变换,也没有提供四元数的求负、加减等操作,而且没有涉及多个四元数的复合变换,也没有提供双倍覆盖的处理方式。所以这并不是一个很健壮的代码。本文没有对四元数插值进行论述(也许以后有机会会写一写这方面的文章),因此本节代码也将不加入插值的函数。
using System;
public struct Vector3
{
public float X, Y, Z;
//Vector3构造函数
public Vector3(float x, float y, float z)
{
X = x;
Y = y;
Z = z;
}
}
public class Quaternion
{
//四元数实部、虚部属性
public double W { get; private set; }
public double X { get; private set; }
public double Y { get; private set; }
public double Z { get; private set; }
//Quaternion构造函数
public Quaternion(double w, double x, double y, double z)
{
W = w;
X = x;
Y = y;
Z = z;
Normalize();
}
//轴角对的计算函数
public static Quaternion FromAxisAngle(Vector3 axis, double angleInRadians)
{
double halfAngle = angleInRadians / 2; //注意公式qvq*中一个q的角度是θ/2
double sinHalfAngle = Math.Sin(halfAngle);
return new Quaternion(
Math.Cos(halfAngle),
axis.X * sinHalfAngle,
axis.Y * sinHalfAngle,
axis.Z * sinHalfAngle
);
}
//归一化四元数
private void Normalize()
{
double lengthSquared = W * W + X * X + Y * Y + Z * Z;
// 检查是否为零向量
if (Math.Abs(lengthSquared) < 1e-6) //判断模长平方的绝对值是否小于0.000001
{
// 如果是零向量,可以选择不同的处理方式。
// 这里简单粗暴地将其设为单位四元数 (1, 0, 0, 0)
W = 1.0;
X = Y = Z = 0.0;
return;
}
double length = Math.Sqrt(lengthSquared);
// 检查长度是否接近1
if (Math.Abs(length - 1.0) > 1e-6) //大于1必须初始化
{
double invLength = 1.0 / length;
W *= invLength;
X *= invLength;
Y *= invLength;
Z *= invLength;
}
}
//共轭,也就是q*
public Quaternion Conjugate()
{
return new Quaternion(W, -X, -Y, -Z);
}
//执行四元数乘法公式:qvq*
public Vector3 RotateVector(Vector3 v)
{
// 将向量转换成纯四元数
Quaternion qv = new Quaternion(0, v.X, v.Y, v.Z);
Quaternion qConj = this.Conjugate();
// 应用旋转: q * v * q*
Quaternion result = Multiply(Multiply(this, qv), qConj);
// 提取旋转后的向量部分
return new Vector3((float)result.X, (float)result.Y, (float)result.Z);
}
//四元数标准乘法
private static Quaternion Multiply(Quaternion a, Quaternion b)
{
return new Quaternion(
a.W * b.W - a.X * b.X - a.Y * b.Y - a.Z * b.Z,
a.W * b.X + a.X * b.W + a.Y * b.Z - a.Z * b.Y,
a.W * b.Y - a.X * b.Z + a.Y * b.W + a.Z * b.X,
a.W * b.Z + a.X * b.Y - a.Y * b.X + a.Z * b.W
);
}
}
使用示例:
// 定义一个要旋转的向量
Vector3 vectorToRotate = new Vector3(1, 0, 0);
// 创建一个绕Y轴旋转90度的四元数
Quaternion rotation = Quaternion.FromAxisAngle(new Vector3(0, 1, 0), Math.PI / 2);
// 应用旋转
Vector3 rotatedVector = rotation.RotateVector(vectorToRotate);
Console.WriteLine($"Rotated Vector: ({rotatedVector.X}, {rotatedVector.Y}, {rotatedVector.Z})");
8、扩展阅读
推导部分
[1] 向量投影公式的推导
以其中
现在的问题就是找到这个标量
将
把这个
根据三角函数的定义可得
将其上下同时乘以
[2] 复数满足交换律
可见两者的结果是一致的。
[3] 首先构造三维旋转矩阵。在
首先是
同理可以构造
假设现在
可见表达式中丢失了
[4] 毛球定理(Hairy Ball Theorem)是拓扑学中的一个定理,它指出在二维球面上,不存在连续的、非零的切向量 场。换句话说,如果你试图将毛发均匀地分布在球体表面(想象成在球上刷油漆或梳理毛球),总会有一个地方 毛发会竖起来或者聚集在一起,形成所谓的“旋涡”或“牛眼”。
具体来说,如果我们尝试定义一个从实数到
[5]
两边同时左乘
两边同时右乘
同理,由(1)可以继续变形:
同理,由(2)可以继续变形:
[6] 逆定义为
[7] 设
因为
可知乘一次
[8]
设
[9]
为简便起见,设
可见两者结果是一致的。
[10]
同样设
可见两者结果是一致的。
参考资料
[1] Ian Parberry, Fletcher Dunn. 3D数学基础:图形与游戏开发(第2版)[M]. 北京: 清华大学出版社, 2020-05.
[2] Eric Lengyel. 3D游戏与计算机图形学中的数学方法(第3版)[M]. 北京: 清华大学出版社, 2016-05.
[3] HMMNRST. Unityで3Dモデルの法線を反転させる方法[EB/OL].[2019-05-22].https://qiita.com/HMMNRST/items/0a4ab86ed053c770ff6a.
[4] 视频:四元数的可视化_哔哩哔哩_bilibili
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· spring官宣接入deepseek,真的太香了~