四元數能夠看作是一個複數,因此咱們先要回顧一下複數。本章的主要目標是展現一個複數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有矩陣表達,任何旋轉矩陣均可以轉換成四元數用來表達旋轉;
咱們可使用球面插值來對兩個用單位四元數表示的方向進行插值。