这是对Foundation of 3D Computer Graphics第7章的翻译,本章讲解了方位插值、四元数表达(quaternion representation)、slerp(球体线性插值)/lerp操作等基础知识,重点是如何使用四元数替换旋转矩阵。本书内容仍在不断的学习中,因此本文内容会不断的改进。若有任何建议,请不吝赐教ninetymiles@icloud.com。
注:文章中相关内容归原作者所有,翻译内容仅供学习参考。
另:Github项目CGLearning中拥有相关翻译的完整资料、内容整理、课程项目实现。
四元数(Quaternions)
本章中,我们会探讨将旋转的四元数表达作为对旋转矩阵的代替
针对四元数,我们的主要用处是协助我们以自然的方式在方位间插值。对于驱动从空中飞过的物体十分有用。如果你不打算针对实际应用插值出旋转,那么这种表达不是必须的。
7.1 插值(Interpolation)
假设我们拥有一个想要的物体帧(object frame),在时刻“time=0”:
一种思路是定义

Figure 7.1:
另一种思路,不值得过于深入探究,是以某种方式将
我们真正想做的是首先生成一个过渡矩阵
同时设置
如此,我们会得到一系列帧,当我们增加
这其中困难的部分为分解一个过渡矩阵,比如
7.1.1 循环(Cycles)
此处我们需要澄清一个细节。事实上,矩阵
7.1.2 不变性(Invariance)
相关于方程式(7.1)的围绕单轴,常量角速率方式的运动,存在很多自然的事物。举一个例子,一个飞过空中的物体在没有外力施加于其上时会让其质心沿着直线运动,同时其方位围绕着一个固定轴旋转。另外,这种方位插值满足左侧和右侧不变性,这一点我们现在就来解释。
假设我们有一个可替换的世界帧

Figure 7.2: 当我们将
我们可以看到使用方程(7.1)的插值满足左侧不变性,其证明如后面所述。“过渡”仿射变换映射一个帧
右侧不变性,在另一方面,意味着一个物体的插值确实改变了,即便是我们改变了被使用的物体帧。实际上,假如我们固定一个“右侧”旋转矩阵

Figure 7.3: 这里我们改变为新的物体帧(蓝色),但是借助新的物体坐标在时刻time=0和time=1绘制相同的正方形。如果插值满足右侧不变性,我们会得到正方形的相同动画。
我们能够直接看出使用方程(7.1)的插值满足右侧不变性如下
7.2 表达(The Representation)
一个四元数(quaternion)只是实数的四元组,我们会很快在其上定义合适的操作。
我们把一个四元数写作
此处
要表达围绕一个单位长度(unit length)轴
除以2的除法看起来有一点儿让人吃惊,但是正是它让四元数操作可以正确工作,后面会讲述。注意围绕
在我们随后定义幂操作符时,这种怪异将会让事情更复杂一点。
四元数
表达了同一旋转矩阵(identity rotation matrix)。
四元数
表达了围绕轴
任何这种形式的四元数
都拥有值为1(四个条目的平方和的平方根)的态(norm)。反过来说,任何这样的单位态的四元数(和其负值一起)都可以被解读为一个唯一的旋转矩阵。
7.3 各种操作(Operations)
一个四元数(不必然是单位态(unit norm))乘以一个标量的乘法被定义如下
两个四元数(不必然是单位态)间的乘法借助下列有着奇怪外观的操作被定义
此处
一个单位态四元数的乘法反转(倒数)为
这个四元数只是围绕相同轴旋转了
更重要的是,我们能够借助四元数操作针对一个坐标实例应用旋转变换。假设我们拥有4部件坐标矢量
这里最终的4部件坐标矢量为这种形式
让我们拿3部件坐标矢量
接着我们执行下面的三重四元数乘法:
这个公式可以通过一系列不是特别直观的计算验证出这种三重四元数的乘积事实上为下面形式的四元数
此处
因此,四元数一方面明确地封装了旋转轴和角度,而另一方面又具备允许我们像旋转矩阵一样实现所有必需的操作的特征。
7.4 幂(Power)
给出一个表达一个旋转的单位态四元数,我们能够将其提升为幂
当
如果
7.4.1 球体线性插值和简单线性插值(Slerp and Lerp)
把所有这些汇总,如果存在两个帧通过旋转矩阵
那么我们只需要计算四元数:
这种插值操作经常被称作球体线性插值或者由于后面的原因直接称为"slerping"。单位态四元数只是平方和为1的实数的4元组,从几何角度,我们因此可以把这些四元数作为4维空间中单位球体上的点。这可以被展示为(参看下面),如果你以两个单位态四元数开始并且借助方程式(7.4)插值,那么实际上在
在任何维度
此处
此处
从这种观点,我们可以明白使用
因为这种插值不再是一个单位态四元数,我们必须随后标准化计算的结果,但是这很容易做到。重要的是,这种插值处理,它被称为lerping,并且勾勒出了更复杂的slerp所经历的(通过一个单一的固定轴旋转)完全相同的四元数路径,虽然这种旋转角度不再随着

Figure 7.4: 我们的旋转插值行为可以被解读为采用了一种常量速度的路径,这种路径沿着连接
lerp操作也是左侧和右侧都不变的。例如,左侧不变性可以证明如下
因为标量乘法跨四元数乘法可交换,并且四元数乘法在和之上分布。相似地,我们从这种形式能直接看到,方程式(7.6)也都是左侧和右侧不变的。仅有的较难的部分为执行计算以证明角度
Lerp可以比slerp更有效率地被实现。更重要地是,它非常轻松地泛化了在
基于幂和球体的Slerps的等价性(选修)(Equivalence of Power and Sphere Based Slerps)
我这里勾勒出需要的步骤以证实基于幂和球体的旋转插值的等价性。
- 可以证实方程式(7.6)是左侧不变的(正如我们上面所讲过的)。注意我们更早时候针对方程式(7.4)已经证实了左侧不变性。由于两种插值方法的左侧不变性,我们可以在不损失通用性的前提下,只考虑
是同一(identity)变换的情形。
- 假设
是同一变换,方程式(7.4)中基于幂的插值体给出了我们
为
- 因为
是同一变换,初始四元数为
。把这个插入方程(7.6)中,我们可以验证这个表达也和方程式(7.7)一致。
- 三角学参数可以被用于证实方程式(7.6)几何方式对应于沿着球体表面的插值。
7.5 编码(Code)
一个四元数类可以非常简单地编码。
我们定义Quat
为一个实数的四元组。随后我们按照方程式(7.2)定义乘法(q1 * q2)
。假定一个单位态四元数,Quat q
,我们定义inv(q)
。假定一个单位态四元数,Quat q
,和一个Cvec4 c
,我们定义(q * c)
,这会对坐标矢量$mathbf{c}$应用旋转操作,就如方程式(7.3),同时返回坐标矢量
我们也会写代码实现MakeRotation
系列函数,就如我们对矩阵类所做的。
给出一个单位态四元数q和一个实数alpha,我们定义幂操作符:pow(q,alpha)
。给出两个四元数q0和q1,我们可以定义出插值的四元数:slerp(q0,q1,alpha)
。记住,当实现slerp时,在应用幂操作之前,为了在“短路径”上插值,只要第一个坐标为负,我们就需要负化四元数(q1 * inv(q0))
。
7.6 放回平移(Putting Back the Translations)
目前为止,我们已经讨论了四元数对于表达旋转是如何起作用的,但是忽略了平移。现在我们讨论怎样使用四元数和平移矢量一起协同来表达刚体变换(rigid body transformations)。
一个刚体变换,或RBT,可以通过合成一个平移和一个旋转被表示。
因此,我们可以将此表示为一个对象:
class RigTForm{
Cvec4 t;
Quat r;
};
记住,因为t表示一个平移矢量,其第4坐标一定是0.
7.6.1 插值(Interpolation)
给出两个RBT
注意到这种RBT插值体不是右侧不变的,这一点很重要。如果你改变了物体帧并且使用这种方式在它们之间插值,新的原点会以直线方式行进,而旧的原点会勾勒出一个曲线路径。(参考图示

Figure 7.5: 这里我们改变物体帧(从绿色变为蓝色),但是在时刻time=0和time=1借助新的物体坐标绘制相同的正方形。我们的RBT插值不满足右侧不变性。针对中间值的
7.6.2 操作(Operations)
返回到小节6.2中我们的绘制代码中,我们现在可以借助RigTForm
数据类型而不是Matrix4
类型来表达eyeRBT
和objRBT
。
要生成有效的坚固形体(刚体)变换,我们需要下列的操作
RigTForm identity();
RigTForm makeXRotation(const double ang);
RigTForm makeYRotation(const double ang);
RigTForm makeZRotation(const double ang);
RigTForm makeTranslation(const double tx, const double ty, const double tz);
我们需要编码RigTForm A
和Cvec4 c
的乘积,它会返回结果A.r * c + A.t
。
接着,我们需要编码两个RigTForm
的乘积。要理解如何完成这个计算,让我们观察两个这样的刚体变换的乘积。
从这种表达我们看到结果为一个新的刚体变换拥有平移
接着,我们需要为这种数据类型编码倒数(inverse)操作符。如果我们观察一个刚体变换的倒数,我们会看到
因此,我们看到结果为一个新的刚体变换,拥有平移
给出这种基础结构,我们能够借助我们的新数据类型重新编码函数doQtoOwrtA(RigTForm Q, RigTForm 0, RigTForm A)
。
最终,为了和顶点着色器沟通,需要使用$4times4$矩阵,我们需要一个程序Matrix4 makeRotation(quat q)
实现方程式(2.5)。然后,用于刚体变换的矩阵可以被计算为
matrix4 makeTRmatrix(const RigTForm& rbt){
matrix4 T = makeTranslation(rbt.t);
matrix4 R = makeRotation(rbt.r);
return T * R;
}
从而,我们的绘制代码以下列代码开始
Matrix4 MVM = makeTRmatrix(inv(eyeRbt) * objRbt);
can right multiply scales here
Matrix4 NMVM = normalMatrix(MVM);
sendModelViewNormalMatrix(MVM,NMVM);
注意,我们构造计算的方式,我们不再需要任何代码,这些代码会接受一个矩阵并将其转换为一个Quat
。
除了从Matrix4
数据类型切换到RigTForm
类型,我们的其余代码,这些代码保持不停地追踪多个刚体帧,不需要被改变。在新实现中,伸缩仍然要被Matrix4
类型表达。