OpenGL ES 從零開始系列9:動畫基礎和關鍵幀動畫

最初這篇教程我並不打算作為第9章發布,原計劃是第10章。在深入了解Opengl ES 2.0 和著色器之前,我想討論下更基礎的:動畫。注意:你可以在這裡找到這篇教程的配套代碼,新版本的代碼已經在西部時間10:14更新了,更新的代碼裡面修正了一個不能動畫的錯誤。目前為止,想必你已經看過了opengles最基本的動畫形式。通過隨時間改變rotate, translate, scale(旋轉、移動和縮放)等,我們就可以使物體「動起來」。我們的第一個項目 the spinning icosahedron就是這種動畫的一個例子。我們把這種動畫叫做簡單動畫。然而,不要被「簡單動畫」這個名稱迷糊,你可以實現複雜的動畫,只需要隨時間改變一下矩陣變換。但是,如何掌握更加複雜的動畫呢?比如說你想讓一個人物行走或者表現一個被擠壓正要反彈的球。實際上這並不困難。在OpenGL了裡面有兩種主要實現方法:關鍵幀動畫和骨骼動畫。在這章裡面我們談論關於幀動畫的話題,下一章(#9b)裡面,我們將要談論的是骨骼動畫。Interpolation & Keys動畫只不過是隨著時間改變每個頂點的位置。這是是動畫的本質。當你移動、旋轉或縮放一個物體的時候,你實際上是移動了一個物體的所有頂點。如果你想讓一個物體有一個更複雜、精細的動畫,你需要一個方法按設置時間移動每個頂點。兩種動畫的基本原理是存儲物體關鍵位置的每一個頂點。在關鍵幀動畫中,我們存儲獨立關鍵位置的每一個頂點。而骨骼動畫,我們存儲虛擬骨骼的位置信息,並且用一些方法指定哪個骨骼會影響動作中的哪些頂點。那麼什麼是關鍵幀?如果要最簡單的方法說明他們,我們還得回到他們的起源,傳統逐格動畫,如經典的迪斯尼和華納兄弟的卡通。早期的動畫,一個小的團隊就能完成所有的繪畫工作。但是隨著產品的慢慢變大,那變得不可能,他們不得不進行分工。比較有經驗的漫畫師成為lead animator(有時叫關key animator)。這些有經驗的畫師並不畫齣動畫的每一格,而是繪製更重要的幀。比如說一個極端的運動或姿勢,體現一個場景的本質。如果要表現一個人物投擲一個球的動畫,關鍵幀是手臂最後端時候的幀,手臂在弧線最頂端的幀,和人物釋放球體的幀。然後,key animator會轉移到新場景 而 另一個in-betweener(有時叫rough in-betweener)會算出關鍵幀之間的時間間隔,並完成這些關鍵幀之間幀的繪畫。比如一個一秒鐘的投擲動畫,每秒12幀,他們需要指出怎樣在首席動畫師繪製的關鍵幀中間完成剩下的9幀。三維關鍵幀動畫的概念也是一樣。你有動作中關鍵位置的頂點數據,然後插值演算法擔當rough in-betweener的角色。插值將是你在三維動畫裡面用到的最簡單的數學演算法。或許我們看一個實際的例子會更明白一點。讓我們只關注一個頂點。在第一個關鍵幀,假設是在原點(0 ,0, 0)。第二個關鍵幀,假設那是在(5、5、5),並且在這兩個關鍵幀之間的時間間隔是五秒(為了計算方便)。動畫的一秒鐘,我們只需要表現出這一秒前後兩個頂點在每個坐標軸上的變化。所以,在我們的例子中,兩個關鍵幀在x,y,z軸總共移動了5個單位(5減去0等於5)。一秒鐘的動畫走了1/5的路程,所以我們添加5的1/5到在第一關鍵幀的x,y,z軸上面,變成(1, 1, 1)。目前數值算出來的過程並不優雅,但是數學演算法是一樣的。算出總距離,算出與第一關鍵幀之間流逝的時間比例,兩種相乘再加上第一關鍵幀的坐標值。這是最簡單的插值,叫線性插值,適用於大部分情況。更加複雜的演算法,要權衡動畫的長度。例如在Core Animation中,提供了幾種"ease in", "ease out", or "ease in/out"等幾種選項。也許我們會在以後的文章中討論非線性插值。不過現在,為了保持簡單易懂,我們繼續討論線性插值。你可以通過改變關鍵幀的數量和它們的時間間隔,完成絕大多數動畫。Keyframe Animation in OpenGLES讓我們看一個OpenGL中簡單動畫的例子。當一個傳統的手工繪畫師被訓練以後,他們做的第一件事情就是做一個能夠被擠壓的而且正在反彈的小球。這同樣適合我們,程序會像下面這樣:

讓我們用Blender(或者任何你想用的3d程序,如果你有方法輸出vertex , normal data的數據用人工的方法。在這個例子裡面我會用Blender export script,它能生成一個有頂點數據的頭文件)創建一個球。我開始在原點創建一多面體,並且重新命名為Ball1,然後我保存這個文件。使用我的腳本渲染並且輸出ball1。你可以在這裡找到這個幀的渲染文件。

現在,我們按另存為(F2)保存一個Ball2.blend的副本。我重命名為Ball2以便於輸出腳本使用不同的名字命名數據類型。接著點擊 tab鍵進入編輯模式,點擊A移動和縮放球體上的點,直到球體被壓扁。保存壓扁的球然後輸出到Ball2.h。 你可以在這裡找到壓扁的球的資料。

到這裡,我們有兩個頭文件,每個文件裡面都包包含著我的動畫裡面要用到的每個幀的頂點數據。從my OpenGL ES template開始工作,我先在 GLViewControler.h定義了一些新的值,它能幫助我追蹤小球的運動。#define kAnimationDuration 0.3enum animationDirection {kAnimationDirectionForward = YES,kAnimationDirectionBackward = NO};typedef BOOL AnimationDirection;因為我將是球在2個關鍵幀直接來回移動,我需要記錄他的軌跡是向前或向後。我也設置一個值去控制兩個幀之間的運動速度。然後在 GLViewController.m裡面,我重複在兩個幀之間插值,如下(不要擔心,我會解釋的):- (void)drawView:(UIView *)theView{static NSTimeInterval lastKeyframeTime = 0.0;if (lastKeyframeTime == 0.0)lastKeyframeTime = [NSDate timeIntervalSinceReferenceDate];static AnimationDirection direction = kAnimationDirectionForward;glClearColor(1.0, 1.0, 1.0, 1.0);glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);glLoadIdentity();glTranslatef(0.0f,2.2f,-6.0f);glRotatef(-90.0, 1.0, 0.0, 0.0); // Blender uses Z-up, not Y-up like OpenGL ESstatic VertexData3D ballVertexData[kBall1NumberOfVertices];glColor4f(0.0, 0.3, 1.0, 1.0);glEnable(GL_COLOR_MATERIAL);NSTimeInterval timeSinceLastKeyFrame = [NSDate timeIntervalSinceReferenceDate]- lastKeyframeTime;if (timeSinceLastKeyFrame > kAnimationDuration) {direction = !direction;timeSinceLastKeyFrame = timeSinceLastKeyFrame - kAnimationDuration;lastKeyframeTime = [NSDate timeIntervalSinceReferenceDate];}NSTimeInterval percentDone = timeSinceLastKeyFrame / kAnimationDuration;VertexData3D *source, *dest;if (direction == kAnimationDirectionForward){source = (VertexData3D *)Ball1VertexData;dest = (VertexData3D *)Ball2VertexData;}else{source = (VertexData3D *)Ball2VertexData;dest = (VertexData3D *)Ball1VertexData;}for (int i = 0; i < kBall1NumberOfVertices; i++){GLfloat diffX = dest[i].vertex.x - source[i].vertex.x;GLfloat diffY = dest[i].vertex.y - source[i].vertex.y;GLfloat diffZ = dest[i].vertex.z - source[i].vertex.z;GLfloat diffNormalX = dest[i].normal.x - source[i].normal.x;GLfloat diffNormalY = dest[i].normal.y - source[i].normal.y;GLfloat diffNormalZ = dest[i].normal.z - source[i].normal.z;ballVertexData[i].vertex.x = source[i].vertex.x + (percentDone * diffX);ballVertexData[i].vertex.y = source[i].vertex.y + (percentDone * diffY);ballVertexData[i].vertex.z = source[i].vertex.z + (percentDone * diffZ);ballVertexData[i].normal.x = source[i].normal.x + (percentDone * diffNormalX);ballVertexData[i].normal.y = source[i].normal.y + (percentDone * diffNormalY);ballVertexData[i].normal.z = source[i].normal.z + (percentDone * diffNormalZ);}glEnableClientState(GL_VERTEX_ARRAY);glEnableClientState(GL_NORMAL_ARRAY);glVertexPointer(3, GL_FLOAT, sizeof(VertexData3D), &Ball2VertexData[0].vertex);glNormalPointer(GL_FLOAT, sizeof(VertexData3D), &Ball2VertexData[0].normal);glDrawArrays(GL_TRIANGLES, 0, kBall1NumberOfVertices);glDisableClientState(GL_VERTEX_ARRAY);glDisableClientState(GL_NORMAL_ARRAY);}首先,有一些初始化設置。我創建了一個靜態變數來追蹤當前幀是否是最後一幀,這用來判定當前流逝的時間。首先我們初始化當前的時間,然後聲明變數來追蹤我們的動畫是向前還是向後的。static NSTimeInterval lastKeyframeTime = 0.0;if (lastKeyframeTime == 0.0)lastKeyframeTime = [NSDate timeIntervalSinceReferenceDate];static AnimationDirection direction = kAnimationDirectionForward;然後是一些OpenGL ES一般設置。唯一需要注意的是我把x軸旋轉了-90°。我們知道OpenGL ES使用Y軸向上的坐標體系,同樣的我們旋轉為Z軸向上。glClearColor(1.0, 1.0, 1.0, 1.0);glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);glLoadIdentity();glTranslatef(0.0f,2.2f,-6.0f);glRotatef(-90.0, 1.0, 0.0, 0.0); // Blender uses Z-up, not Y-up like OpenGL ES接下來,聲明一個靜態數組來存儲插值數據:static VertexData3D ballVertexData[kBall1NumberOfVertices];為了簡單,我設置了一個顏色並且開啟了color materials。我不想使使用texture(紋理)或者materials(材質)使這個例子變得更加混亂。glColor4f(0.0, 0.3, 1.0, 1.0);glEnable(GL_COLOR_MATERIAL);現在我計算出上一個幀過去到現在的時間,如果這個時間大於動畫時長,改變動畫的運動方向。NSTimeInterval timeSinceLastKeyFrame = [NSDate timeIntervalSinceReferenceDate]- lastKeyframeTime;if (timeSinceLastKeyFrame > kAnimationDuration) {direction = !direction;timeSinceLastKeyFrame = timeSinceLastKeyFrame - kAnimationDuration;lastKeyframeTime = [NSDate timeIntervalSinceReferenceDate];}NSTimeInterval percentDone = timeSinceLastKeyFrame / kAnimationDuration;為了適應雙向動畫,我聲明了兩個指針指向源幀和目的幀的數據,並且根據當前的方向指向適當的數據數組。VertexData3D *source, *dest;if (direction == kAnimationDirectionForward){source = (VertexData3D *)Ball1VertexData;dest = (VertexData3D *)Ball2VertexData;}else{source = (VertexData3D *)Ball2VertexData;dest = (VertexData3D *)Ball1VertexData;}最後,對於插值。正是我們前面談論到的是一個相當普遍的線性插值:for (int i = 0; i < kBall1NumberOfVertices; i++){GLfloat diffX = dest[i].vertex.x - source[i].vertex.x;GLfloat diffY = dest[i].vertex.y - source[i].vertex.y;GLfloat diffZ = dest[i].vertex.z - source[i].vertex.z;GLfloat diffNormalX = dest[i].normal.x - source[i].normal.x;GLfloat diffNormalY = dest[i].normal.y - source[i].normal.y;GLfloat diffNormalZ = dest[i].normal.z - source[i].normal.z;ballVertexData[i].vertex.x = source[i].vertex.x + (percentDone * diffX);ballVertexData[i].vertex.y = source[i].vertex.y + (percentDone * diffY);ballVertexData[i].vertex.z = source[i].vertex.z + (percentDone * diffZ);ballVertexData[i].normal.x = source[i].normal.x + (percentDone * diffNormalX);ballVertexData[i].normal.y = source[i].normal.y + (percentDone * diffNormalY);ballVertexData[i].normal.z = source[i].normal.z + (percentDone * diffNormalZ);}清理環境glEnableClientState(GL_VERTEX_ARRAY);glEnableClientState(GL_NORMAL_ARRAY);glVertexPointer(3, GL_FLOAT, sizeof(VertexData3D), &Ball2VertexData[0].vertex);glNormalPointer(GL_FLOAT, sizeof(VertexData3D), &Ball2VertexData[0].normal);glDrawArrays(GL_TRIANGLES, 0, kBall1NumberOfVertices);glDisableClientState(GL_VERTEX_ARRAY);glDisableClientState(GL_NORMAL_ARRAY);}不太難吧?只是些除法,乘法和加法。比起我們前面的經歷,這算不了什麼。這是基本技術的應用,例如在Id的老遊戲裡面使用的MD2文件格式。和我這裡所作的一樣,每個動畫都使用了關鍵幀動畫。Milkshape之後的版本支持其它文件格式,同樣可以使用關鍵幀做複雜的動畫。如果你想檢查這個彈球,你可以下載Xcode project親自運行。並不是所有的3 D動畫都是用關鍵幀實現的,但是插值是複雜動畫的基本原理。請繼續關注part 9 b,我們將要使用插值實現一個被稱為骨骼動畫的更複雜的動畫。在進入下一篇關於骨骼動畫的文章之前,讓我們先花點時間來了解一個馬上會使用到的新數據類型:四元數[譯者注:關於四元數的概念可以參考這個鏈接:點我]。我們用四元數存儲單一骨骼在3個軸線上的旋轉信息,換句話說,存儲的是骨骼指向的方向。在下一部分介紹的模擬骨骼動畫中,你將會看到,模型的頂點是同一個或多個骨骼相關聯的,當骨骼移動時它們也會隨之變化。相對於將歐拉角信息存儲在3個GLfloats變數或一個 Vector3D 變數里來說, 使用四元數有2個優點:1.四元數不會造成萬向節死鎖(gimbal lock),但是歐拉角容易造成萬向節死鎖,使用四元數能夠讓我們的3D模型能夠全方位的移動。2.相比於給每個歐拉角做矩陣旋轉轉換計算,使用四元數結合多角度旋轉可以顯著的減少計算量。從某些方面來看,四元數極其複雜且難於理解。它們是高級數學:完全瘋狂的符咒。幸運的是,你不需要完全理解它們背後的數學含義。但是,我們現在需要使用它們來完成骨骼動畫,所以還是值得我們花費些時間來討論下它們的概念和怎麼使用它們。Discovery探索從數學上講,四元數是複數的一個擴展延伸,於1843年由Sir William Rowan Hamilton發現。技術上講,四元數表現為實數之上的4維正規可除代數。Zoiks!更簡單的講,四元數被認為是第四維度用來計算笛卡爾坐標中的3個坐標值。好吧,一切可能不那麼簡單,對吧?先別怕,如果你不精通高等數學,四元數可能會讓你頭疼。但是,如我之前所說,如果你只是使用它們,完全不必深入了解。這玩意和你見過的一些概念是非常類似的。不知你是否還能想起我們在3維空間里涉及到的4X4矩陣的矩陣轉換。當我們使用已轉換的數據的時候,忽略了第4個值。我們可以把這裡的第四個值當成四元數,為計算提供了一個位置。數學範疇內,請不要跟我說——過度簡化有助於凡人在四元數世界裡佔有一席之地,有所作為。四元數在探索時代里被認為是相當創新的,但最繁榮的時期卻如此短暫。在1880中期,向量微積分開始在計算領域取代四元數理論,因為它用了一種更為容易理解和描述的概念描述了同樣的現象。Not Quite Dead Yet!雖死猶生但在20世紀,四元數又重新獲寵。正如我們在part 7里討論的,有一個被稱為gimbal lock 的現象,當你在每個軸線單獨做旋轉轉換的時候就會發生,此現象的危害就是可能導致在三個軸中的一個軸上停止旋轉。儘管事實是四元數源於複數和理論數學,但它們都有實際應用。其中一個實際應用是三軸線上旋轉角的展現。由於四元數用四個維度展示了笛卡爾(或三軸)旋轉,此展現不會導致gimbal lock,而且你可以在四元數和旋轉矩陣之間,四元數和歐拉角之間進行無損轉換。這使得存儲某些對象的旋轉信息相當完美,比如。。。骨骼框架中的單獨骨骼?不需要存貯3軸的角信息,而是存儲一個單獨的四元數。四元數和矩陣一樣,可以相乘,且存儲於不同四元數中的旋轉值通過相乘來合併計算。四元數乘積和2個旋轉矩陣乘積的結果是完全一樣的,考慮到減少計算量,這意味著除了要避免gimbal lock,還要減少每次程序循環運行的FLOPS(每秒浮點運算次數)。和矩陣乘法相比,四元數乘法不僅步驟少,而且可以通過一個四元數表達3軸所有數據。如果通過Vector3D 或3個GLfloats來存儲旋轉信息,我們經常不得不做3次矩陣乘法——每軸都要算一次。結論是,通過把存儲旋轉的獨立角信息存為四元數,可以帶來可觀的性能提升。The Quaternion Struct 四元數結構體從數據上看來,四元數只不過是比Vector 3D多加了一個GLfloat,經常把它當成w欄位。所以對我們來說一個四元數就象這樣:typedef struct {GLfloat x;GLfloat y;GLfloat z;GLfloat w;} Quaternion3D;Normalizing a Quaternion 四元數歸一化這個十分簡單。四元數代表空間里的一個方向,就像Vector3Ds,實際距離的值並不在意,並且在進行一些計算之前/之後使他們正常下降到1.0。完成這些我們可以這樣做:static inline void Quaternion3DNormalize(Quaternion3D *quaternion){GLfloat magnitude;magnitude = sqrtf((quaternion->x * quaternion->x) +(quaternion->y * quaternion->y) +(quaternion->z * quaternion->z) +(quaternion->w * quaternion->w));quaternion->x /= magnitude;quaternion->y /= magnitude;quaternion->z /= magnitude;quaternion->w /= magnitude;}Creating a Quaternion from a Rotation matrix 從一個旋轉矩陣中創建一個四元數如果我們沒法在四元數同其他的對象轉換過程中保存旋轉角度信息的話,四元數對我們來說還是沒用的。首先我們從一個旋轉矩陣中創建一個四元數。代碼如下:static inline Quaternion3D Quaternion3DMakeWithMatrix3D(Matrix3D matrix){Quaternion3D quat;GLfloat trace, s;trace = matrix[0] + matrix[5] + matrix[10];if (trace > 0.0f){s = sqrtf(trace + 1.0f);quat.w = s * 0.5f;s = 0.5f / s;quat.x = (matrix[9] - matrix[6]) * s;quat.y = (matrix[2] - matrix[8]) * s;quat.z = (matrix[4] - matrix[1]) * s;}else{NSInteger biggest;enum {A,E,I};if (matrix[0] > matrix[5])if (matrix[10] > matrix[0])biggest = I;elsebiggest = A;elseif (matrix[10] > matrix[0])biggest = I;elsebiggest = E;switch (biggest){case A:s = sqrtf(matrix[0] - (matrix[5] + matrix[10]) + 1.0f);if (s > QUATERNION_TRACE_ZERO_TOLERANCE){quat.x = s * 0.5f;s = 0.5f / s;quat.w = (matrix[9] - matrix[6]) * s;quat.y = (matrix[1] + matrix[4]) * s;quat.z = (matrix[2] + matrix[8]) * s;break;}s = sqrtf(matrix[10] - (matrix[0] + matrix[5]) + 1.0f);if (s > QUATERNION_TRACE_ZERO_TOLERANCE){quat.z = s * 0.5f;s = 0.5f / s;quat.w = (matrix[4] - matrix[1]) * s;quat.x = (matrix[8] + matrix[2]) * s;quat.y = (matrix[9] + matrix[6]) * s;break;}s = sqrtf(matrix[5] - (matrix[10] + matrix[0]) + 1.0f);if (s > QUATERNION_TRACE_ZERO_TOLERANCE){quat.y = s * 0.5f;s = 0.5f / s;quat.w = (matrix[2] - matrix[8]) * s;quat.z = (matrix[6] + matrix[9]) * s;quat.x = (matrix[4] + matrix[1]) * s;break;}break;case E:s = sqrtf(matrix[5] - (matrix[10] + matrix[0]) + 1.0f);if (s > QUATERNION_TRACE_ZERO_TOLERANCE){quat.y = s * 0.5f;s = 0.5f / s;quat.w = (matrix[2] - matrix[8]) * s;quat.z = (matrix[6] + matrix[9]) * s;quat.x = (matrix[4] + matrix[1]) * s;break;}s = sqrtf(matrix[10] - (matrix[0] + matrix[5]) + 1.0f);if (s > QUATERNION_TRACE_ZERO_TOLERANCE){quat.z = s * 0.5f;s = 0.5f / s;quat.w = (matrix[4] - matrix[1]) * s;quat.x = (matrix[8] + matrix[2]) * s;quat.y = (matrix[9] + matrix[6]) * s;break;}s = sqrtf(matrix[0] - (matrix[5] + matrix[10]) + 1.0f);if (s > QUATERNION_TRACE_ZERO_TOLERANCE){quat.x = s * 0.5f;s = 0.5f / s;quat.w = (matrix[9] - matrix[6]) * s;quat.y = (matrix[1] + matrix[4]) * s;quat.z = (matrix[2] + matrix[8]) * s;break;}break;case I:s = sqrtf(matrix[10] - (matrix[0] + matrix[5]) + 1.0f);if (s > QUATERNION_TRACE_ZERO_TOLERANCE){quat.z = s * 0.5f;s = 0.5f / s;quat.w = (matrix[4] - matrix[1]) * s;quat.x = (matrix[8] + matrix[2]) * s;quat.y = (matrix[9] + matrix[6]) * s;break;}s = sqrtf(matrix[0] - (matrix[5] + matrix[10]) + 1.0f);if (s > QUATERNION_TRACE_ZERO_TOLERANCE){quat.x = s * 0.5f;s = 0.5f / s;quat.w = (matrix[9] - matrix[6]) * s;quat.y = (matrix[1] + matrix[4]) * s;quat.z = (matrix[2] + matrix[8]) * s;break;}s = sqrtf(matrix[5] - (matrix[10] + matrix[0]) + 1.0f);if (s > QUATERNION_TRACE_ZERO_TOLERANCE){quat.y = s * 0.5f;s = 0.5f / s;quat.w = (matrix[2] - matrix[8]) * s;quat.z = (matrix[6] + matrix[9]) * s;quat.x = (matrix[4] + matrix[1]) * s;break;}break;default:break;}}return quat;}好的,如果你想真正知道這裡是怎麼工作的。你需要了解矩陣運算、歐拉旋轉理論、特徵值和紋理。更不用說理解旋轉在矩陣里的表示方法。當你完成你的數學博士學位,你可以回過頭用它來解釋剩餘的usl。你會發現這個函數中使用的演算法都是Matrix的FAQ上用偽代碼列出來的。Creating a Rotation Matrix from a Quaternion 從一個四元數中創建旋轉矩陣這另外的一個方法相對簡單些。並且這個基本演算法來自於Matrix FAQ,雖然我需要把它轉換成行優先的順序。static inline void Matrix3DSetUsingQuaternion3D(Matrix3D matrix, Quaternion3D quat){matrix[0] = (1.0f - (2.0f * ((quat.y * quat.y) + (quat.z * quat.z))));matrix[1] = (2.0f * ((quat.x * quat.y) - (quat.z * quat.w)));matrix[2] = (2.0f * ((quat.x * quat.z) + (quat.y * quat.w)));matrix[3] = 0.0f;matrix[4] = (2.0f * ((quat.x * quat.y) + (quat.z * quat.w)));matrix[5] = (1.0f - (2.0f * ((quat.x * quat.x) + (quat.z * quat.z))));matrix[6] = (2.0f * ((quat.y * quat.z) - (quat.x * quat.w)));matrix[7] = 0.0f;matrix[8] = (2.0f * ((quat.x * quat.z) - (quat.y * quat.w)));matrix[9] = (2.0f * ((quat.y * quat.z) + (quat.x * quat.w)));matrix[10] = (1.0f - (2.0f * ((quat.x * quat.x) + (quat.y * quat.y))));matrix[11] = 0.0f;matrix[12] = 0.0f;matrix[13] = 0.0f;matrix[14] = 0.0f;matrix[15] = 1.0f;}Converting an Angle and Axis of Rotation to a Quaternion 把一個角度和旋轉軸轉換成一個四元數四元數可以做的另外一種轉換是,表示成在一個Vector3D表示的軸線上進行旋轉。這在骨骼動畫裡面是非常有用的,因為這種表現形式通過矩陣是很難做到的。創建一個基於角度和軸旋轉得四元數,我們可以這樣做:static inline Quaternion3D Quaternion3DMakeWithAxisAndAngle(Vector3D axis, GLfloat angle){Quaternion3D quat;GLfloat sinAngle;angle *= 0.5f;Vector3DNormalize(&axis);sinAngle = sinf(angle);quat.x = (axis.x * sinAngle);quat.y = (axis.y * sinAngle);quat.z = (axis.z * sinAngle);quat.w = cos(angle);return quat;}Extracting an Angle and Axis of Rotation from a Quaternion 從一個四元數中檢測角度和軸得旋轉反過來,我們也可以從四元數中取得旋轉的數據,包括旋轉角度和深度,就像這樣static inline void Quaternion3DExtractAxisAndAngle(Quaternion3D quat, Vector3D *axis, GLfloat *angle){GLfloat s;Quaternion3DNormalize(&quat);s = sqrtf(1.0f - (quat.w * quat.w));if (fabs(s) < 0.0005f) s = 1.0f;if (axis != NULL){axis->x = (quat.x / s);axis->y = (quat.y / s);axis->z = (quat.z / s);}if (angle != NULL)*angle = (acosf(quat.w) * 2.0f);}Quaternion Multiplication 四元數乘法為了合併兩種不同形式的四元數中得3D旋轉信息。我們只需要讓他們彼此相乘。好了繼續我們得代碼static inline void Quaternion3DMultiply(Quaternion3D *quat1, Quaternion3D *quat2){Vector3D v1, v2, cp;float angle;v1.x = quat1->x;v1.y = quat1->y;v1.z = quat1->z;v2.x = quat2->x;v2.y = quat2->y;v2.z = quat2->z;angle = (quat1->w * quat2->w) - Vector3DDotProduct(v1, v2);cp = Vector3DCrossProduct(v1, v2);v1.x *= quat2->w;v1.y *= quat2->w;v1.z *= quat2->w;v2.x *= quat1->w;v2.y *= quat1->w;v2.z *= quat1->w;quat1->x = v1.x + v2.x + cp.x;quat1->y = v1.y + v2.y + cp.y;quat1->z = v1.z + v2.z + cp.z;quat1->w = angle;}Inverting a Quaternion 四元數轉置我們通過做一個四元數的共軛運算來取得四元數的轉置。四元數做共軛運算其實就是將四元數中表示向量(x,y,z)的值取反。在這裡的實現中,我們把它[四元數轉置計算]作為四元數標準計算的一部分,而不是一個獨立的步驟:static inline void Quaternion3DInvert(Quaternion3D *quat){GLfloat length = 1.0f / ((quat->x * quat->x) +(quat->y * quat->y) +(quat->z * quat->z) +(quat->w * quat->w));quat->x *= -length;quat->y *= -length;quat->z *= -length;quat->w *= length;}Creating a Quaternion from Euler Angles 從歐拉角中創建四元數前面我說過在旋轉中最好不要使用歐拉角,但是有時候我們需要將歐拉角轉換成四元數,比如說用戶輸入的信息是歐拉角信息。轉換的步驟是,將歐拉軸用Vector3D表示出來,然後將Vector3D的值轉換成四元數,最後將四元數相乘來得到結果:static inline Quaternion3D Quaternion3DMakeWithEulerAngles(GLfloat x, GLfloat y, GLfloat z){Vector3D vx = Vector3DMake(1.f, 0.f, 0.f);Vector3D vy = Vector3DMake(0.f, 1.f, 0.f);Vector3D vz = Vector3DMake(0.f, 0.f, 1.f);Quaternion3D qx = Quaternion3DMakeWithAxisAndAngle(vx, x);Quaternion3D qy = Quaternion3DMakeWithAxisAndAngle(vy, y);Quaternion3D qz = Quaternion3DMakeWithAxisAndAngle(vz, z);Quaternion3DMultiply(&qx, &qy );Quaternion3DMultiply(&qx, &qz );return qx;}最後,最重要的部分來了:SLERPS 和 NLERPS。SLERPS 是 球麵線性插值 的縮寫,NLERPS是 歸一化線性插值 的縮寫,還記得我們在「Part 9a」是怎樣計算 代數的 線性插值 的嗎,我們由此計算出了每一個依賴於 過去了多長時間的點的準確位置。但是,在插值四元法中 通過這樣的 計算我們得不到想要的結果。想像一個球和一個窩關節圖:

知道哪裡用得到這樣的球窩關節嗎?在你的 胳膊肘 的地方。當然這只是為了描敘方便。站在鏡子的前面,將你的胳膊舉過頭頂,並保持你的胳膊豎直,然後將胳膊放下,放到你身體的一側。你的胳膊不是直線運動的,對吧?你的手是弧線運動的,並且比你的肘關節和你的手臂運動的要快,很有可能你的手並不是以一個恆定的速度運動。這就是我們在做動畫旋轉角 時想要模擬的基本的轉動方式。球麵線性插值 和 歸一化線性插值 是兩種最通用的方法,球麵線性插值 更常用,並且對現實提供了一個更好的模擬,但是它更慢,需要的配置高。歸一化線性插值 更快,因為它和我們以前用過的線性插值本質上是一樣的,僅僅是插值後作歸一化,這個完全可以從「歸一化線性插值」這個名字中體味出來。當動畫效果比對速度的要求更嚴格時,使用球麵線性插值,否則就用 歸一化線性插值,這兩種方法完全相同的參數,在這兩種方法之間轉換應該沒什麼問題,你可以嘗試看一下是否球麵線性插值 要更多處理器消耗。這裡是一個簡單的 歸一化線性插值的例子:static inline Quaternion3D Quaternion3DMakeWithNLERP(Quaternion3D *start, Quaternion3D *finish, GLclampf progress){Quaternion3D ret;GLfloat inverseProgress = 1.0f - progress;ret.x = (start->x * inverseProgress) + (finish->x * progress);ret.y = (start->y * inverseProgress) + (finish->y * progress);ret.z = (start->z * inverseProgress) + (finish->z * progress);ret.w = (start->w * inverseProgress) + (finish->w * progress);Quaternion3DNormalize(&ret);return ret;}現在,球麵線性插值的實現就有一點複雜了,它要能夠 計算 四元數的點積 ,這跟計算一個向量的點積 是一樣的:static inline Quaternion3D Quaternion3DMakeWithSLERP(Quaternion3D *start, Quaternion3D *finish, GLclampf progress){GLfloat startWeight, finishWeight, difference;Quaternion3D ret;difference = ((start->x * finish->x) + (start->y * finish->y) + (start->z * finish->z) + (start->w * finish->w));if ((1.f - fabs(difference)) > .01f){GLfloat theta, oneOverSinTheta;theta = acosf(fabsf(difference));oneOverSinTheta = (1.f / sinf(theta));startWeight = (sinf(theta * (1.f - progress)) * oneOverSinTheta);finishWeight = (sinf(theta * progress) * oneOverSinTheta);if (difference < 0.f)startWeight = -startWeight;} else{startWeight = (1.f - progress);finishWeight = progress;}ret.x = (start->x * startWeight) + (finish->x * finishWeight);ret.y = (start->y * startWeight) + (finish->y * finishWeight);ret.z = (start->z * startWeight) + (finish->z * finishWeight);ret.w = (start->w * startWeight) + (finish->w * finishWeight);Quaternion3DNormalize(&ret);return ret;}Finish Line結束語:現在我們有能力做一些模擬骨骼動畫了,下次我們還會講解這個東西,我已經用這些新的函數和數據類型更新了Xcode工程模板。註:1、因為我們的程序使用了浮點數,實際上我們會看到由於「有效位丟失」造成輕微變化的想像,這不是因為我們使用四元法,是因為我們使用了浮點數。
推薦閱讀:

如何繪製女性動畫形象?
講述梵高自殺之迷的動畫大片—至梵高的愛
觀世音騎龍動畫
中國動畫片:從舉國體制下的藝術品到賣身生涯

TAG:動畫 | OpenGL | 從零開始 | 開始 | 基礎 |