一、基本原理
骨骼動畫的基本原理就是首先控制各個骨骼和關節,再使附在上面的蒙皮(Mesh)與其相匹配。
一個角色由作為皮膚的單一網格模型和按照一定層次組織起來的骨骼組成。
骨骼層次描述了角色的結構:相鄰的骨骼通過關節連接,並且可以做出相對的運動。這里要注意的是,骨骼間是具有“父子”關系的,比如,右前臂是右上臂的子節點,同時又是右手的父節點。通過改變相鄰骨骼間的位移、夾角,就可以做出不同的動作,實現不同的動畫效果。
皮膚作為一個網格蒙在骨骼之上,規定角色的外觀。這里的皮膚不死固定不變的剛性mesh,而是可以在骨骼影響下變化的一個可變性mesh。組成皮膚的每一個頂點都會受到一個或多個骨骼的影響(就人體來說,一個網格頂點最多受到4塊骨骼的影響),不同的骨骼按照與頂點的幾何、物理關系確定對該頂點的影響權重,通過計算影響該頂點的不同骨骼加權值之和就可以得到該頂點在世界坐標系中的正確位置。
動畫文件中的關鍵幀一般保存着骨骼的位置、朝向等信息。通過在動畫序列中相鄰的兩個關鍵幀之間插值可以確定某一時刻各個骨骼的新位置和新朝向(或者不插值,直接從一個關鍵幀變到另一個關鍵幀,作為簡單版本實現,本例中就沒有插值),這樣就可以計算出一個骨骼變換矩陣。然后皮膚網格的每個頂點根據影響它的骨骼及相應的權重計算它們的加權和,就可以計算出這個頂點在世界坐標系(強調)中的新位置。如此,便實現了在骨骼驅動下的單一皮膚網格變形動畫(這就是簡單版本的骨骼蒙皮動畫)。
二、實現
下面主要討論技術細節。
在一個典型的骨骼蒙皮動畫模型中,會保存如下信息:網格信息、骨骼信息和動畫信息。
網格信息是角色的多邊形模型。該多邊形模型一般由三角形面片組成,每個三角形面片有三個指向模型的頂點表的索引(通過該索引,可以確定這個三角形的三個定點的坐標)。頂點表中的每一頂點除了帶有位置、法向量、材質、紋理等基本信息外,還會指出有哪些骨骼影響了該頂點,影響權重又是多少。
骨骼信息包括全部去骨骼的數量和每個骨骼的具體信息。每一根骨骼包括該骨骼在父骨骼坐標系中的變換矩陣(可能為了方便會計算2個變換矩陣:絕對變換矩陣和相對變換矩陣),通過該變換矩陣可以在確定父骨骼位置的前提下,確定子骨骼的位置。另外,為了方便,常常計算出每根骨骼相對於世界坐標系的“顯示變換矩陣”(它其實就是通過父骨骼的顯示矩陣乘以子骨骼相對於父骨骼的平移+旋轉變換矩陣得到的)總之,每一個關鍵幀指出了每一個骨骼在該時刻相對於父骨骼的變換矩陣或者說該骨骼相對於父骨骼的位移、旋轉等操作。
關鍵的計算公式:
1. 子骨骼的位置=父骨骼的位置*子骨骼相對於父骨骼的平移矩陣*子骨骼相對於父骨骼的旋轉矩陣
2. mesh頂點的位置=最初狀態頂點的位置*初始狀態骨骼逆變換*骨骼本地坐標系下的新變換
(有必要解釋一下,“初始狀態骨骼你變換”其實就是“世界坐標系到骨骼本地坐標系”的變換矩陣,之所以叫“逆矩陣”,是因為默認情況下,我們認為“骨骼本地坐標到世界坐標系”的變換為“正”--最常用的意思,更多解釋見http://www.cnblogs.com/neoragex2002/archive/2007/09/13/Bone_Animation.html)
本例中的3個矩陣含義:
“顯示變換矩陣”可以直接從世界坐標系變換到特定骨骼的本地坐標系(世界坐標系的標准矩陣乘以顯示矩陣);“絕對變換矩陣”是相對於世界坐標系的原點而言的,不包含子骨骼相對於父骨骼的平移;“相對變換矩陣”是子骨骼相對於父骨骼的本地坐標系而言的,同樣不包含平移,只含旋轉操作。定義他們是有目的的:“絕對變換矩陣”是mesh頂點需要的,“相對變換矩陣”則是“絕對變換矩陣”需要的。
三、設計
一個模型類(BoneModel)包含一個骨骼類(Bone)和一個網格定點類(MeshVertex)。
骨骼類的屬性:
/*骨骼類*/ class Bone { public: Bone(); /*設置骨骼數據*/ void SetBone(int parent,float length); void SetBone(int parent,float length,M3DMatrix44f rela,M3DMatrix44f abso); unsigned int m_parent; /*父節點索引*/ float m_length; /*骨骼長度*/ /*變換矩陣:絕對變換矩陣和相對變換矩陣*/ M3DMatrix44f m_abso; M3DMatrix44f m_rela; };
網格頂點類的屬性:
/*蒙皮頂點類*/ class MeshVertex { public: MeshVertex(); /*設置蒙皮頂點數據*/ void SetMeshVertex(M3DVector3f pos,M3DVector3f normal,int b1,int b2,int b3,int b4, float w1,float w2,float w3,float w4,float red,float green,float blue,float alpha,int nNumBone); M3DVector3f m_pos; /*頂點位置*/ M3DVector3f m_normal; /*法線坐標*/ int m_arrBoneIdx[4]; /*影響頂點的骨骼索引,做多4個*/ float m_arrWeight[4]; /*各骨骼影響頂點的權重*/ int m_nNumBone; /*實際影響頂點的骨骼數目*/ float m_red; float m_green; /*頂點顏色*/ float m_blue; float m_alpha; };
骨骼模型類的屬性:
/*定義一些宏*/ #define MAX_BONES 2 #define MAX_MESHES 3 #define MAX_VERTICES_PER_MESH 4 class BoneModel { public: BoneModel(); /*自定義初始化骨骼和蒙皮頂點數據*/ void InitData(); Bone m_bones[MAX_BONES]; /*所有的骨骼索引*/ M3DMatrix44f m_DispMat[MAX_BONES]; /*各個骨骼的顯示矩陣*/ MeshVertex m_ModelPoints[MAX_MESHES*MAX_VERTICES_PER_MESH]; };
更新骨骼位置的函數:
/*更新所有的骨骼變換矩陣*/ void UpdateBones() { /*用於保存平移和旋轉矩陣*/ M3DMatrix44f XRotMat,zRotMat,InvTransMat; /*循環更新骨骼*/ for(int i=0;i<MAX_BONES;++i) { //檢查是否根骨骼 if(pObjModel->m_bones[i].m_parent == -1) { /*初始化根骨骼相對 絕對 顯示矩陣*/ m3dTranslationMatrix44(pObjModel->m_bones[i].m_rela,0.0f,0.0f,1.0f);//根骨骼的位置在DrawModel()中指定 m3dCopyMatrix44(pObjModel->m_bones[i].m_abso,pObjModel->m_bones[i].m_rela); m3dCopyMatrix44(pObjModel->m_DispMat[i],pObjModel->m_bones[i].m_rela); } else { //在父骨骼的基礎上平移,這要求在顯示的時候父子骨骼之間不能彈堆棧 //移動到父骨骼的位置 m3dTranslationMatrix44(pObjModel->m_bones[i].m_rela,0.0f,pObjModel->m_bones[pObjModel->m_bones[i].m_parent].m_length,0.0f); /*更新旋轉矩陣*/ m3dRotationMatrix44(XRotMat,m3dDegToRad(xRot),1.0f,0.0f,0.0f); m3dRotationMatrix44(zRotMat,m3dDegToRad(zRot),0.0f,0.0f,1.0f); /*保存相對矩陣的逆矩陣(此時的相對矩陣(平移矩陣)只包含平移操作,無旋轉操作)*/ m3dInvertMatrix44(InvTransMat,pObjModel->m_bones[i].m_rela); /*子骨骼顯示矩陣=父骨骼絕對矩陣*子骨骼平移矩陣*子骨骼旋轉矩陣*/ m3dMatrixMultiply44(pObjModel->m_DispMat[i],pObjModel->m_bones[pObjModel->m_bones[i].m_parent].m_abso,pObjModel->m_bones[i].m_rela); m3dMatrixMultiply44(pObjModel->m_DispMat[i],pObjModel->m_DispMat[i],XRotMat); m3dMatrixMultiply44(pObjModel->m_DispMat[i],pObjModel->m_DispMat[i],zRotMat); /*計算真正的相對變換矩陣(不含平移)*/ m3dMatrixMultiply44(pObjModel->m_bones[i].m_rela,pObjModel->m_bones[i].m_rela,XRotMat); m3dMatrixMultiply44(pObjModel->m_bones[i].m_rela,pObjModel->m_bones[i].m_rela,zRotMat); m3dMatrixMultiply44(pObjModel->m_bones[i].m_rela,pObjModel->m_bones[i].m_rela,InvTransMat); /*計算絕對變換矩陣(不含平移--相對於原點)*/ m3dMatrixMultiply44(pObjModel->m_bones[i].m_abso,pObjModel->m_bones[pObjModel->m_bones[i].m_parent].m_abso,pObjModel->m_bones[i].m_rela); } } }
繪制網格頂點的函數(注意乘的是abso函數):
void DrawMesh() { UpdateBones(); M3DMatrix44f mat; int nIdx=0; //glPolygonMode(GL_FRONT,GL_LINE); /*渲染網格中的頂點*/ for(int j=0;j<MAX_MESHES;++j) { glBegin(GL_QUADS); for(int i=0;i<MAX_VERTICES_PER_MESH;++i) { float vx=0.0f,vy=0.0f,vz=0.0f; /*mesh頂點坐標*/ float vnx=0.0f,vny=0.0f,vnz=0.0f; /*mesh頂點法線坐標*/ float tmp1=0.0f,tmp2=0.0f,tmp3=0.0f;/*在這里vnx,vny,vnz要初始化為0,因為在下層循環中vnx vny vnz要不斷權重累加*/ /*根據權值計算頂點的pos normal坐標*/ nIdx=i+j*MAX_VERTICES_PER_MESH; for(int k=0;k < pObjModel->m_ModelPoints[nIdx].m_nNumBone;++k) { m3dCopyMatrix44(mat,pObjModel->m_bones[pObjModel->m_ModelPoints[nIdx].m_arrBoneIdx[k]].m_abso);//這里用的是絕對矩陣 /*加權計算骨骼對頂點位置的影響*/ tmp1=(pObjModel->m_ModelPoints[nIdx].m_pos)[0]*mat[0]+ (pObjModel->m_ModelPoints[nIdx].m_pos)[1]*mat[4]+ (pObjModel->m_ModelPoints[nIdx].m_pos)[2]*mat[8]+mat[12]; tmp2=(pObjModel->m_ModelPoints[nIdx].m_pos)[0]*mat[1]+ (pObjModel->m_ModelPoints[nIdx].m_pos)[1]*mat[5]+ (pObjModel->m_ModelPoints[nIdx].m_pos)[2]*mat[9]+mat[13]; tmp3=(pObjModel->m_ModelPoints[nIdx].m_pos)[0]*mat[2]+ (pObjModel->m_ModelPoints[nIdx].m_pos)[1]*mat[6]+ (pObjModel->m_ModelPoints[nIdx].m_pos)[2]*mat[10]+mat[14]; vx+=tmp1*(pObjModel->m_ModelPoints[nIdx].m_arrWeight[k]); vy+=tmp2*(pObjModel->m_ModelPoints[nIdx].m_arrWeight[k]); vz+=tmp3*(pObjModel->m_ModelPoints[nIdx].m_arrWeight[k]); /*在這里我犯了一個愚蠢的錯誤(就這么一個簡單的測試程序就花了2天時間來調試):用vx代替了tmp1,vy--tmp2,vz--tmp3,huh,FOOLISH.*/ tmp1=(pObjModel->m_ModelPoints[nIdx].m_normal)[0]*mat[0]+ (pObjModel->m_ModelPoints[nIdx].m_normal)[1]*mat[4]+ (pObjModel->m_ModelPoints[nIdx].m_normal)[2]*mat[8]+mat[12]; tmp2=(pObjModel->m_ModelPoints[nIdx].m_normal)[0]*mat[1]+ (pObjModel->m_ModelPoints[nIdx].m_normal)[1]*mat[5]+ (pObjModel->m_ModelPoints[nIdx].m_normal)[2]*mat[9]+mat[13]; tmp3=(pObjModel->m_ModelPoints[nIdx].m_normal)[0]*mat[2]+ (pObjModel->m_ModelPoints[nIdx].m_normal)[1]*mat[6]+ (pObjModel->m_ModelPoints[nIdx].m_normal)[2]*mat[10]+mat[14]; vnx+=tmp1*(pObjModel->m_ModelPoints[nIdx].m_arrWeight[k]); vny+=tmp2*(pObjModel->m_ModelPoints[nIdx].m_arrWeight[k]); vnz+=tmp3*(pObjModel->m_ModelPoints[nIdx].m_arrWeight[k]); } /*渲染網格頂點*/ glColor4f(pObjModel->m_ModelPoints[nIdx].m_red,pObjModel->m_ModelPoints[nIdx].m_green, pObjModel->m_ModelPoints[nIdx].m_blue,pObjModel->m_ModelPoints[nIdx].m_alpha); glNormal3f(vnx,vny,vnz); glVertex3f(vx,vy,vz); } glEnd(); } }
繪制整個模型的函數:
/*根據骨骼的位置變化,繪制骨骼和蒙皮*/ void DrawModel() { glLoadIdentity();//???? glTranslatef(0.0f,-4.0f,-15.0f); DrawMesh(); /*繪制骨骼*/ for(int i=0;i<MAX_BONES;++i) { glPushMatrix(); glMultMatrixf(pObjModel->m_DispMat[i]); glColor3f(1.0f,0.0f,0.0f); glBegin(GL_LINES); /*繪制線段組成的模擬骨骼*/ glVertex3f(-0.4f, 0.0f, -0.4f); glVertex3f(0.4f, 0.0f, -0.4f); glVertex3f(0.4f, 0.0f, -0.4f); glVertex3f(0.4f, 0.0f, 0.4f); glVertex3f(0.4f, 0.0f, 0.4f); glVertex3f(-0.4f, 0.0f, 0.4f); glVertex3f(-0.4f, 0.0f, 0.4f); glVertex3f(-0.4f, 0.0f, -0.4f); glVertex3f(-0.4f, 0.0f, -0.4f); glVertex3f(0.0f, pObjModel->m_bones[i].m_length, 0.0f); glVertex3f(0.4f, 0.0f, -0.4f); glVertex3f(0.0f, pObjModel->m_bones[i].m_length, 0.0f); glVertex3f(0.4f, 0.0f, 0.4f); glVertex3f(0.0f, pObjModel->m_bones[i].m_length, 0.0f); glVertex3f(-0.4f, 0.0f, 0.4f); glVertex3f(0.0f, pObjModel->m_bones[i].m_length, 0.0f); glEnd(); glPopMatrix(); } }
運行效果:
圖1 父骨骼旋轉一定角度后的效果
圖2 初始狀態
ref:<<OpenGL編程精粹>>