四元数能够看作是一个复数,因此咱们先要回顾一下复数。本章的主要目标是展现一个复数P乘以一个单位复数,获得的是旋转后的P。数组
有不少种方法介绍复数,咱们采用下面的方法,将它想象成一个2D点或者向量。
一对排序的实数z = (a, b)是一个复数。第一个部分叫实数部分,第二个部分是虚数部分。而且加减乘除定义以下:
而且很容易证实实数的算术性质对复数也依然有效(交换律,结合律,分配率)(练习1);markdown
若是一个复数形式是(x, 0),那么它就是经过实数x定义,而后写成(x, 0);那么任何实数均可以是一个复数,其虚数部分是0;观察一个实数乘以一个复数x(a, b) = (x, 0)(a, b) = (xa, xb) = (a, b)(x, 0) = (a, b)x,这个形式可让咱们回忆起变量-向量的乘法。
咱们定义单位虚数i = (0, 1),而后使用定义的复数乘法,i^2 = (0, 1)(0, 1) = (−1, 0) = −1,因此i是方程x^2 = −1的解。
复数z = (a, b)的复数共轭表示为z‾\overline{z}z,而且z‾\overline{z}z= (a, −b);一个简单的记住复数除法公式的方法是:分子和分母乘以分母的共轭,这样分母就成为了一个实数:
ide
下面展现一个复数(a, b)能够写为a + ib,咱们有a = (a, 0), b = (b, 0)和i = (0, 1),因此:
函数
使用a + ib形式,咱们能够重作加减乘除:
而且在这种形式下,z = a + ib的共轭复数为a - ib。oop
咱们将复数a + ib = (a, b)理解为几何上的2D向量或者点(在复平面);复数相加匹配到向量的相加:
复数的绝对值或者长度由向量的长度来定义:
咱们将长度为1的复数称之为单位复数:
学习
由于复数能够看作是2D复平面的点或者向量,因此咱们也能够将它表示到极坐标:
后面的等式就是复数a + ib的极坐标表示。
令z1 = r1(cosθ1 + isinθ1), z2 = (cosθ2 + isinθ2),那么:
这里使用了三角定理:
因此几何上,z1z2的乘积表示长度为r1r2的向量旋转了θ1 + θ2角度;若是其中r2为1,那么乘积就表示将z1旋转了θ2角度。如上图,因此复数乘以单位复数,表示把前面的复数旋转。动画
四个有序实数q = (x, y, z, w) = (q1, q2, q3, q4)是一个四元数,一般简写为q = (u, w) = (x, y, z, w),而且咱们称u = (x, y, z)为虚数部分,w为实数部分。那么加减乘除定义以下:
乘法的定义看起来会比较奇怪,可是这些运算是定义,因此咱们能够定义为咱们想要的形式,而且这些形式颇有用。矩阵的乘法定义一开始也看起来很奇怪,可是结果是它们颇有用。
令p = (u, p4) = (p1, p2, p3, p4)而且q = (v, q4) = (q1, q2, q3, q4),那么u × v =(p2q3 – p3q2, p3q1 – p1q3, p1q2 – p2q1)而且u·v = p1q1 + p2q2 + p3q3。那么组件形式下,四元数的乘积r = pq是:
能够写成矩阵乘法形式:
若是你偏心行向量形式,取其转置矩阵:
this
令i = (1, 0, 0, 0), j = (0, 1, 0, 0), k = (0, 0, 1, 0)为四元数,那么咱们有一些特殊乘积,会让咱们回忆起叉积:
这些等式是直接从四元数乘积公式得来的,好比:
atom
四元数的乘法不具有交换律,下图证实ij = −ji。四元数乘积具备结合律;因此四元数能够联想为矩阵的乘积,具备结合律,不具备交换律。四元数e = (0, 0, 0, 1)用以乘法单位:
四元数具备乘法分配律p(q + r) = pq + pr 和 (q + r)p = qp + rp。spa
咱们将实数、向量与四元数经过下面的方式来关联。令s是实数x,u = (x, y, z)是向量,那么:
能够说任意实数是一个有0向量的四元数,任意向量是一个具备0实数的四元数;另外单位四元数为1 = (0, 0, 0, 1);一个四元数具备0实数称之为纯四元数(pure quaternion)。
使用四元数乘法的定义,一个实数乘以四元数是标量相乘,具备交换律:
四元数q = (q1, q2, q3, q4) = (u, q4)的共轭由q定义:
也就是直接将虚数部位取反;相比于共轭复数,它具备下面特性:
其中q + q和qq* = q*q等于实数。
四元数的范数(长度),定义为:
范数为1的四元数为单位四元数,范数具备下面的特性:
特性2表示2个单位四元数相乘,依然是单位四元数;若是||p|| = 1,那么||pq|| = ||q||。
共轭和范数的特性能够直接经过定义推导出来,好比:
由于矩阵和四元数乘法不具备交换律,因此不能直接定义除法运算。可是每一个非0四元数具备逆,令q = (q1, q2, q3, q4) = (u, q4)是一个非0的四元数,那么它的逆经过q−1q^{-1}q−1来定义:
很容易证实它是复数的逆:
能够看出若是q是单位四元数,那么∣∣q∣∣2=1||q||^2 = 1∣∣q∣∣2=1,而且q−1=q∗q^{-1} = q^*q−1=q∗。
而且符合下面的特性:
若是q = (q1, q2, q3, q4) = (u, q4)是单位四元数,那么:
上图表示,对于θ∈[0, π],q4 = cosθ,根据三角定义sin2θ + cos2θ = 1:
因此
如今求单位向量:
因此u = sinθn,如今咱们能够写出单位四元数q = (u, q4)的极坐标表达,其中n是单位向量:
若是咱们将−θ带入等式中的θ,只是取反了向量部分:
下节将会介绍,n表示旋转的轴向。
令q = (u, w)是一个单位四元数而且令v是一个3D点或者向量,而后咱们认为v是一个纯四元数p = (v, 0)。当q是一个单位四元数时,q−1=q∗q^{−1} = q^*q−1=q∗。回顾四元数乘法公式:
如今考虑下面的乘法:
对其长度稍做简化,咱们把实数部分和向量部分分开计算。咱们作下面的符号替换:
实数部分:
其中u · (v × u) = 0,由于根据叉积的定义,(v × u)是和u正交的。
虚数部分:
其中对u × (u × v)应用了乘法定义:a × (b × c) = (a · c)b − (a · b)c。因此:
计算的结果是一个向量或者点,其实数部分为0。因此随后的等式中,咱们放弃实数部分。
由于q是一个单位四元数,因此能够写为:
带入上面公式后:
为了进一步简化,咱们带入三角定义:
对比第三章的旋转公式,咱们发现它和旋转公式基本一致,它将向量v沿着n轴旋转2θ度。
因此咱们定义四元数旋转运算:
因此若是你要沿着n轴旋转θ度,那么你能够构建对于的旋转四元数:
而后应用到旋转公式中Rq(v)R_q(v)Rq(v)。
令q = (u, w) = (q1, q2, q3, q4)是一个单位四元数,根据以前的公式,能够获得:
上面公式的三个部分能够分别写出矩阵形式:
将它们相加:
根据单位四元数的特性(各分组件平方的和为1),作下面的简化:
最后矩阵能够写为:
给出一个旋转矩阵:
咱们但愿找到四元数q = (q1, q2, q3, q4),咱们的策略是,设置矩阵以下:
而后求解q1, q2, q3, q4;
首先将对角线上的元素相加(最终一个矩阵):
而后组合对角相反的元素来求解q1, q2, q3:
若是q4 = 0,那上面这些公式就无心义,因此咱们要找到R的最大对角元素来除,而且选择矩阵元素的其余组合。加入R11是最大的对角:
若是假设R22 或者 R33为最大对角线,计算模式相似。
假设p和q是单位四元数,而且对于旋转运算为Rp 和 Rq,令v′=Rp(v)v^{'} = R_p(v)v′=Rp(v),那么组合:
由于p和q都是单位四元数,pq的乘积也是单位四元数||pq|| = ||p||||q|| = 1;因此pq也表示旋转;也就是说说获得的旋转为:Rq(Rp(v))R_q(R_p(v))Rq(Rp(v))。
由于四元数是由4个实数组成的,因此几何上,能够把它当作是一个4D向量,单位四元数是4D在单位4D球体表面,除了叉积(只定义了3D向量)。特别的,点积也支持四元数,令p = (u, s)而且q = (v, t),那么:
其中θ是两个四元数之间的夹角,若是p和q是单位四元数,那么p·q = cosθ,因此点积能够帮助咱们考虑2个四元数之间的夹角。
出于动画考虑,咱们须要在两个方向之间进行插值,为了插值四元数,咱们须要在单位球体上进行弧度差值,因此也须要在单位四元数上差值。为什么推导出公式,以下图所示:咱们须要在a和b中间差值tθ。咱们须要找到权重c1和c2支持p = c1a + c2b,其中||p|| = ||a|| = ||b||。咱们对两个未知项建立两个等式:
而后能够导出下面的矩阵:
考虑到上面的矩阵等式Ax = b,其中A是可逆的,因此根据克莱姆法则xi=detAidetAx_i = \frac{detA_i}{detA}xi=detAdetAi,其中AiA_iAi是经过交换A中第i列的向量到b,因此:
根据三角毕达哥斯拉定义和加法公式,咱们能够得出:
因此:
而且:
因此咱们定义出球体差值公式:
若是将单位四元数当作4D向量的话,咱们就能够求解四元数之间的夹角:θ = arccos(a · b)。
若是a和b之间的夹角趋近于0,sinθ趋近于0,那么上面公式中的除法就会引起问题,会致使无限大的结果。这种状况下,对两个四元数进行线性差值,并标准化结果,就是对小θ的一个很好的近似:
观察下图,线性差值是经过将四元数差值投影回单位球体,其结果是一个非线性速率的旋转。因此若是你对大角度使用线性差值的话,旋转的速度会时快时慢。
咱们如今支持一个四元数有趣的特性,(sq)= sq而且标量-四元数的乘积是具备交换律的,因此咱们能够得出:
咱们得出q和-q表示的相同的旋转,也能够经过其余方式来证实,若是
Rq表示围绕n旋转θ,R-q表示围绕-n旋转2π − θ。在几何上,一个在4D单位球体上的单位四元数和它的极坐标相反值−q表明的是相同的方向。下图能够看出,这两个旋转到了相同的位置,只是一个旋转了小角度,另外一个旋转了大的角度:
因此b和-b表达了相同的方向,咱们有2个选择来差值:slerp(a, b, t) 或者 slerp(a, −b, t)。其中一个是从更小的角度直接旋转;另外一个是从更大的角度来旋转。以下图所示,选择哪一个旋转基于哪一个旋转在单位球体上的弧度:选择小弧度表明选择了更直接的路径,选择更长的弧度表明对物体有额外更多的旋转[Eberly01]。
[Watt92]若是要在单位球面上找到四元数最短旋转弧度,咱们能够比较||a – b||2和||a – (−b)||2 = ||a + b||2。若是 ||a + b||2 < ||a – b||2咱们就选择-b,由于-b更接近a:
// Linear interpolation (for small theta). public static Quaternion LerpAndNormalize(Quaternion p, Quaternion q, float s) { // Normalize to make sure it is a unit quaternion. return Normalize((1.0f - s)*p + s*q); } public static Quaternion Slerp(Quaternion p, Quaternion q, float s) { // Recall that q and -q represent the same orientation, but // interpolating between the two is different: One will take the // shortest arc and one will take the long arc. To find // the shortest arc, compare the magnitude of p-q with the // magnitude p-(-q) = p+q. if(LengthSq(p-q) > LengthSq(p+q)) q = -q; float cosPhi = DotP(p, q); // For very small angles, use linear interpolation. if(cosPhi > (1.0f - 0.001)) return LerpAndNormalize(p, q, s); // Find the angle between the two quaternions. float phi = (float)Math.Acos(cosPhi); float sinPhi = (float)Math.Sin(phi); // Interpolate along the arc formed by the intersection of the 4D // unit sphere and the plane passing through p, q, and the origin of // the unit sphere. return ((float)Math.Sin(phi*(1.0- s))/sinPhi)*p + ((float)Math.Sin(phi*s)/sinPhi)*q; }
DirectX数学库支持四元数。由于四元数的数据是4个实数,因此使用XMVECTOR类型类保存四元数。下面是通用的函数:
// Returns the quaternion dot product Q1·Q2. XMVECTOR XMQuaternionDot(XMVECTOR Q1, XMVECTOR Q2); // Returns the identity quaternion (0, 0, 0, 1). XMVECTOR XMQuaternionIdentity(); // Returns the conjugate of the quaternion Q. XMVECTOR XMQuaternionConjugate(XMVECTOR Q); // Returns the norm of the quaternion Q. XMVECTOR XMQuaternionLength(XMVECTOR Q); // Normalizes a quaternion by treating it as a 4D vector. XMVECTOR XMQuaternionNormalize(XMVECTOR Q); // Computes the quaternion product Q1Q2. XMVECTOR XMQuaternionMultiply(XMVECTOR Q1, XMVECTOR Q2); // Returns a quaternions from axis-angle rotation representation. XMVECTOR XMQuaternionRotationAxis(XMVECTOR Axis, FLOAT Angle); // Returns a quaternions from axis-angle rotation representation, where the axis // vector is normalized—this is faster than XMQuaternionRotationAxis. XMVECTOR XMQuaternionRotationNormal(XMVECTOR NormalAxis,FLOAT Angle); // Returns a quaternion from a rotation matrix. XMVECTOR XMQuaternionRotationMatrix(XMMATRIX M); // Returns a rotation matrix from a unit quaternion. XMMATRIX XMMatrixRotationQuaternion(XMVECTOR Quaternion); // Extracts the axis and angle rotation representation from the quaternion Q. VOID XMQuaternionToAxisAngle(XMVECTOR *pAxis, FLOAT *pAngle, XMVECTOR Q); // Returns slerp(Q1, Q2, t) XMVECTOR XMQuaternionSlerp(XMVECTOR Q0, XMVECTOR Q1, FLOAT t);
本章中的Demo,咱们在简单的场景中运动一个骷髅头。位置、方形和缩放都作动画。咱们用四元数来表达骷髅的方向,而后使用球面差值来对方向差值。使用线性差值对位置和缩放差值。它是对下一章中的角色动画作预热。
咱们使用关键帧系统对骷髅作动画:
struct Keyframe { Keyframe(); ˜Keyframe(); float TimePos; XMFLOAT3 Translation; XMFLOAT3 Scale; XMFLOAT4 RotationQuat; };
动画是一些列经过实践来排序的关键帧:
struct BoneAnimation { float GetStartTime()const; float GetEndTime()const; void Interpolate(float t, XMFLOAT4X4& M)const; std::vector<Keyframe> Keyframes; };
GetStartTime函数用来返回第一个帧的时间;GetEndTime函数返回最后一个关键帧的时间。它对于动画何时结束颇有用,咱们能够中止动画。
如今有了一个关键帧列表,对于每两个帧之间使用插值计算:
void BoneAnimation::Interpolate(float t, XMFLOAT4X4& M)const { // t is before the animation started, so just return the first key frame. if( t <= Keyframes.front().TimePos ) { XMVECTOR S = XMLoadFloat3(&Keyframes.front().Scale); XMVECTOR P = XMLoadFloat3(&Keyframes.front().Translation); XMVECTOR Q = XMLoadFloat4(&Keyframes.front().RotationQuat); XMVECTOR zero = XMVectorSet(0.0f, 0.0f, 0.0f, 1.0f); XMStoreFloat4x4(&M, XMMatrixAffineTransformation(S, zero, Q, P)); } // t is after the animation ended, so just return the last key frame. else if( t >= Keyframes.back().TimePos ) { XMVECTOR S = XMLoadFloat3(&Keyframes.back().Scale); XMVECTOR P = XMLoadFloat3(&Keyframes.back().Translation); XMVECTOR Q = XMLoadFloat4(&Keyframes.back().RotationQuat); XMVECTOR zero = XMVectorSet(0.0f, 0.0f, 0.0f, 1.0f); XMStoreFloat4x4(&M, XMMatrixAffineTransformation(S, zero, Q, P)); } // t is between two key frames, so interpolate. else { for(UINT i = 0; i < Keyframes.size()-1; ++i) { if( t >= Keyframes[i].TimePos && t <= Keyframes[i+1].TimePos ) { float lerpPercent = (t - Keyframes[i].TimePos) / (Keyframes[i+1].TimePos - Keyframes[i].TimePos); XMVECTOR s0 = XMLoadFloat3(&Keyframes[i].Scale); XMVECTOR s1 = XMLoadFloat3(&Keyframes[i+1].Scale); XMVECTOR p0 = XMLoadFloat3(&Keyframes[i].Translation); XMVECTOR p1 = XMLoadFloat3(&Keyframes[i+1].Translation); XMVECTOR q0 = XMLoadFloat4(&Keyframes[i].RotationQuat); XMVECTOR q1 = XMLoadFloat4(&Keyframes[i+1].RotationQuat); XMVECTOR S = XMVectorLerp(s0, s1, lerpPercent); XMVECTOR P = XMVectorLerp(p0, p1, lerpPercent); XMVECTOR Q = XMQuaternionSlerp(q0, q1, lerpPercent); XMVECTOR zero = XMVectorSet(0.0f, 0.0f, 0.0f, 1.0f); XMStoreFloat4x4(&M, XMMatrixAffineTransformation(S, zero, Q, P)); break; } } }
下图展现了两个关键帧之间插值的结果:
插值事后,咱们构造了变换的矩阵,由于在着色器中咱们最终使用矩阵作变换。XMMatrixAffineTransformation函数定义以下:
XMMATRIX XMMatrixAffineTransformation( XMVECTOR Scaling, XMVECTOR RotationOrigin, XMVECTOR RotationQuaternion, XMVECTOR Translation);
如今咱们简单的动画系统已经完成,下一步是定义一些关键帧:
// Member data float mAnimTimePos = 0.0f; BoneAnimation mSkullAnimation; // // In constructor, define the animation keyframes // void QuatApp::DefineSkullAnimation() { // // Define the animation keyframes // XMVECTOR q0 = XMQuaternionRotationAxis(XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f), XMConvertToRadians(30.0f)); XMVECTOR q1 = XMQuaternionRotationAxis(XMVectorSet(1.0f, 1.0f, 2.0f, 0.0f), XMConvertToRadians(45.0f)); XMVECTOR q2 = XMQuaternionRotationAxis(XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f), XMConvertToRadians(-30.0f)); XMVECTOR q3 = XMQuaternionRotationAxis(XMVectorSet(1.0f, 0.0f, 0.0f, 0.0f), XMConvertToRadians(70.0f)); mSkullAnimation.Keyframes.resize(5); mSkullAnimation.Keyframes[0].TimePos = 0.0f; mSkullAnimation.Keyframes[0].Translation = XMFLOAT3(-0.0f, 0.0f); mSkullAnimation.Keyframes[0].Scale = XMFLOAT3(0.25f, 0.25f, 0.25f); XMStoreFloat4(&mSkullAnimation.Keyframes[0].RotationQuat, q0); mSkullAnimation.Keyframes[1].TimePos = 2.0f; mSkullAnimation.Keyframes[1].Translation = XMFLOAT3(0.0f, 2.0f, 10.0f); mSkullAnimation.Keyframes[1].Scale = XMFLOAT3(0.5f, 0.5f, 0.5f); XMStoreFloat4(&mSkullAnimation.Keyframes[1].RotationQuat, q1); mSkullAnimation.Keyframes[2].TimePos = 4.0f; mSkullAnimation.Keyframes[2].Translation = XMFLOAT3(7.0f, 0.0f, 0.0f); mSkullAnimation.Keyframes[2].Scale = XMFLOAT3(0.25f, 0.25f, 0.25f); XMStoreFloat4(&mSkullAnimation.Keyframes[2].RotationQuat, q2); mSkullAnimation.Keyframes[3].TimePos = 6.0f; mSkullAnimation.Keyframes[3].Translation = XMFLOAT3(0.0f, 1.0f, -10.0f); mSkullAnimation.Keyframes[3].Scale = XMFLOAT3(0.5f, 0.5f, 0.5f); XMStoreFloat4(&mSkullAnimation.Keyframes[3].RotationQuat, q3); mSkullAnimation.Keyframes[4].TimePos = 8.0f; mSkullAnimation.Keyframes[4].Translation = XMFLOAT3(-0.0f, 0.0f); mSkullAnimation.Keyframes[4].Scale = XMFLOAT3(0.25f, 0.25f, 0.25f); XMStoreFloat4(&mSkullAnimation.Keyframes[4].RotationQuat, q0); }
最后一步使根据时间进行插值操做:
void QuatApp::UpdateScene(float dt) { … // Increase the time position. mAnimTimePos += dt; if(mAnimTimePos >= mSkullAnimation.GetEndTime()) { // Loop animation back to beginning. mAnimTimePos = 0.0f; } // Get the skull’s world matrix at this time instant. mSkullAnimation.Interpolate(mAnimTimePos, mSkullWorld); … }
如今骷髅的世界矩阵每一帧都根据动画来更新。
一个有序的4个实时q = (x, y, z, w) = (q1, q2, q3, q4)是一个四元数,通常都简写成q = (u, w) = (x, y, z, w),而且咱们将u = (x, y, z)称为虚向量部分,w为实数部分,进一步它的加减乘除定义为:
四元数乘法不知足交换律,可是知足结合律,四元数e = (0, 0, 0, 1)用以恒等式。四元数支持乘法分配律p(q + r) = pq + pr和(q + r)p = qp + rp;
咱们能够将任意实数写成四元数s = (0, 0, 0, s),也能够将任意向量转换成四元数u = (u, 0)。实数部分为0的四元数为纯四元数。四元数能够和标量相乘:s(p1, p2, p3, p4) = (sp1, sp2, sp3, sp4) = (p1, p2, p3, p4)s,特殊的地方在于标量和四元数的乘法支持交换律;
共轭四元数和四元数范式的定义;
逆四元数的定义和计算;
单位四元数能够写成极向量表达q = (u, q4),其中n是单位向量;
若是q是一个单位四元数,那么q = (sinθn,cosθ) for ||n|| = 1 and θ ∈ [0, π],旋转运算为Rq(v)=qvq−1=qvq∗R_q(v) = qvq^{-1} = qvq^*Rq(v)=qvq−1=qvq∗表示将点/向量围绕n旋转2θ。Rq有矩阵表达,任何旋转矩阵均可以转换成四元数用来表达旋转;
咱们可使用球面插值来对两个用单位四元数表示的方向进行插值。