我最近在學習人物動畫的方面,做個總結,由于剛剛接觸這個方面,所以有什么問題請大家指出。
在這篇日志里面,你可以獲得這些信息:
1 人物動畫的框架
2 骨骼動畫及蒙皮技術
3 doom 3和quake 4中模型和動畫格式md5及原理
4 可能的擴展
先來看一下人物動畫的幾種方法:
一、簡單關鍵禎的動畫 像quake3中就是用的這樣的方法。這種方法最簡單,缺點就是空間上的浪費。由于每個關鍵禎中都要儲存整個網格的幾何信息,所以用這種方法生成的動畫文件相當的龐大,在游戲進行中也會占用大量的內存。現在的游戲,一般都不用了。
二、簡單的骨骼動畫及蒙皮技術 現在的很多游戲,都是用的這種方法,具體的原理后面再解釋。這種方法可以節省大量的空間,對于美工來說,工作量也相對較小(可以利用動作捕捉的數據),真實性方面,簡單的應用中也表現得比較好。
三、改進的蒙皮方法和基于物理的骨骼動畫 改進的蒙皮方法可以避免簡單的蒙皮中產生的“糖紙失真現象”;
基于物理的骨骼動畫,已經有很多的游戲、物理引擎支持這一特性了,但是還有很多的技術問題需要處理。這是次時代游戲引擎必須很好實現技術之一。
基本的蒙皮原理拿md5格式為例,來簡單的解釋一下蒙皮的原理。在doom3和quake4中md5mesh文件,用來記錄一個人物的靜態模型。有這樣幾個結構:
Joint: 用來記錄骨骼的關節的信息;
Weight: 用來記錄頂點相對于關節的權值;
Vertex: 頂點信息,和一般的頂點不同,這里的頂點不直接的包含幾何坐標信息,而是記錄了對應的Weight;
現在就來解釋一下這三者之間的關系:
Joint(關節)是會動的,而皮膚上的頂點是會隨著頂點做相應的運動。我們保持皮膚上面的各個頂點和它相對應的關節的“位置關系”,就可以通過旋轉關節,使得真個皮膚跟著旋轉。這個“位置關系”,就是Weight。在運動的過程中,我們獲得當前的骨骼的幾何信息,也就是每個關節的幾何位置,然后在根據每個頂點對于這些關節的權值,分別計算每個頂點的實際幾何位置,這樣,整個人物網格就計算出來了。
很顯然,這其中有一個預處理過程和兩個關鍵的步驟。預處理就是需要由靜態的模型(美工做出的人物模型)和骨骼來計算得到一組Weight;兩個關鍵的步驟是,1、獲得的整個骨骼的幾何信息(有可能從關鍵禎混合得到);2、由頂點對應的Weight來計算出每個頂點的實際幾何信息。
了解這些基本的概念,下面就來介紹人物動畫系統的框架。
骨骼蒙皮基本框架基本的類型:
關節信息:
typedef struct _CharJoint
{
Vector3 pos;
Vector4 startPoint;
int parentID;
char name[32];
} CharJoint;
其中,parentID為父關節,pos為對應父關節的偏移值,startPoint為旋轉角度;
權值信息:
typedef struct _CharWeight
{
Vector3 pos;
int jointID;
float bias;
} CharWeight;
其中,pos為偏移量,jiontID為對應的joint,bias偏向值;
頂點信息:
typedef struct _CharVert
{
float u, v;
int startWeight;
int weightCount;
} CharVert;
其中,startWeight為該頂點對應的Weight在Weight列表中的偏移地址,weightCount記錄該頂點對應多少個權值;對于簡單的頂點,比如頭頂上的某個點,動畫的時候涉及到的變化并不多,所以,對應的權值數也就少,可以只有一個;對于動畫中涉及變化比較復雜的點,比如手肘區域的頂點,可能由較多的權值(4個或更多),這樣才能夠很好的表示運動中對于多個關節的相對位置。
大概還涉及到這樣一些類:
CharSkeleton: 記錄整個骨骼的信息,包含了關節的鏈表;
CharMesh: 記錄整個人物模型的靜態信息,包括頂點,權值,關節等;
CharBlender: 基類,根據CharMesh和CharSkeleton來計算出實際的網格,基本成員函數為Blender,用CPU來計算蒙皮,可以被子類Blend覆蓋(比如可以寫一個用Vertex Shader實現的Blender);
CharAnimation: 每個CharAnimation實例對應一個動作序列,比如“人物蹲下動作”;動作序列保存的是人物骨骼動畫的關鍵禎,也就是在某一禎時,骨骼中各個關節的幾何信息;注意這里的禎的概念并不是平常說的渲染的禎,在動畫中,為了進一步節省空間,一般設定了一個動作為幾個格,就像動漫制作過程中的“故事板”,只是整個過程中的幾個縮略圖,在后期制作過程中,在“填滿”中間缺省的圖片;這里的骨骼動畫關鍵禎也是如此,文件中只保存了間斷的幾個狀態,在渲染的時候,還是要實時的生成中間的某個狀態,來把整個動作序列“填滿”;
CharAnimCtrl: 這個類的作用就是完成上面所說的,將動作序列“填滿”的功能,輸入是CharAnimation和時間,輸出是一個基本的骨架,也就是CharSkeleton(當然這是靠傳引用參數進行輸出);
解決關鍵問題 剛才提到了,整個系統中由三個關鍵的問題:一個預處理過程,關鍵禎混合以及從權值計算出實際頂點。預處理過程,基本上是編寫一個建模工具導出插件的工作,這里就不討論了。
關鍵禎混合:
簡單的辦法,就是直接用線性的方法混合,比如現在的動畫時間標識為40,而我只有標識為20和50的兩個關鍵禎,于是:40-20 = 20,50-40 = 10;而20:10 = 2:1;所以,我們現在的狀態離關鍵禎20的差異,以及離關鍵禎50的差異,這兩個差異的比,就是2:1;好了,所以現在很自然地,我們取倒數,1:2;于是,我們做混合的時候,用“1份”關鍵禎20的骨骼,和“2份”50關鍵禎的骨骼,然后相加兩者的結果(也就是“混合”過程)最后,除以3,得到最終的“1份”關鍵禎為40的骨骼。恩,就這么簡單。(不過注意不要把這個比值的含義搞反了);
際應用中還有其他的混合形式,后面再來介紹。
計算實際頂點:
我們看一下軟件的(用CPU做蒙皮)Blend過程:
void CharBlender::Blend( Mesh &outputMesh, PE::CharMesh &inputMesh, PE::CharSkeleton &inputSk )
{
if ( outputMesh.GetNumVertices() < inputMesh.GetNumVerts() )
return;
CharOutVert *pOutVerts = ( CharOutVert* )outputMesh.LockVertexBuffer();
int numVerts = inputMesh.GetNumVerts();
int numTris = inputMesh.GetNumTris();
for ( int i = 0; i < numVerts; i++ )
{
const CharVert *pVert = inputMesh.GetVertAt( i );
pOutVerts->x = pOutVerts->y = pOutVerts->z = 0.0f;
/* u v initial */
pOutVerts->u0 = pOutVerts->u1 = pOutVerts->u2 = pVert->u;
pOutVerts->v0 = pOutVerts->v1 = pOutVerts->v2 = pVert->v;
for ( int j = 0; j < pVert->weightCount; j++ )
{
const CharWeight *pWeight = inputMesh.GetWeightAt( pVert->startWeight + j );
int index = pWeight->jointID;
const CharJoint *pJoint = & ( inputSk.GetJointAt( pWeight->jointID ) );
vec3_t wv;
Quat_rotatePoint( &pJoint->startPoint.x, &pWeight->pos.x, wv );
pOutVerts->x += ( pJoint->pos[0] + wv[0] ) * pWeight->bias;
pOutVerts->y += ( pJoint->pos[1] + wv[1] ) * pWeight->bias;
pOutVerts->z += ( pJoint->pos[2] + wv[2] ) * pWeight->bias;
}
}
outputMesh.UnlockVertexBuffer();
CharTri *pOutTri = ( CharTri* )outputMesh.LockIndexBuffer();
for ( int i = 0; i < numTris; i++ )
{
const CharTri *pTri = inputMesh.GetTriAt( i );
pOutTri->index[0] = pTri->index[0];
pOutTri->index[1] = pTri->index[1];
pOutTri->index[2] = pTri->index[2];
}
outputMesh.UnlockIndexBuffer();
}
其中黑體的部分,就是關鍵的代碼,應該很容易看懂。其中,Quat_rotatePoint函數的作用就是將點進行旋轉,得到新的坐標。
關于md5anim文件 Doom3和Quake4中的動畫文件都是用md5anim文件保存的。md5anim文件只含有該動作所涉及到的骨骼關節的動畫信息。也就是所,文件中關鍵禎的關節列表,是它所對應的md5mesh文件中基本關節列表的一個子集;這樣做當然是有道理的,因為,有些動作,可能只涉及到身體的一個部分,比如眨眼,換彈夾等等,那么,把一個完整的骨骼框架放在mesh文件中,把若干不同的局部或者整體的關節序列放在不同的動畫文件中,這樣,可以最大限度的節省空間。
可能的擴展一、復雜動作的混合
有時候,我們需要將兩個動作混合,比如,一個人物同時的在做兩種動作,一邊向左平移,一邊向右方開槍;不可能為每種可能的混合動作做大量的美工工作,而且空間上,我們也不允許這樣做;可行的辦法是,混合兩個不同的動作序列,比如上半身動作和下半身動作的混合,這當然是最簡單的方式。還有很多比較麻煩的混合方式,比如,人物在行走時中了槍,需要混合“行走”和“中槍”兩個動作,而簡單的線性混合是無法真實模擬的。
二、基于物理的動畫
這不再僅是圖形方面的問題了,這其中涉及到了大量的物理模型,這個,我也不懂。。。可以從第三方的物理引擎獲得幫助,ODE好像就支持了;
三、基于GPU的蒙皮
原理和CPU蒙皮的原理一致,只是用了Shader,會比CPU蒙皮的效率快很多。在前面的代碼中,只需實現CharBlender的子類就可以了。
四、非常流行的“換裝”系統
這在RPG游戲里面簡直就是不可少的一條。就現在的框架來說,還不能達到隨意“換裝”的要求。修改CharMesh以及Character的底層,需要能夠添加和刪除基本的骨架,支持多層皮膚(衣服)(多個Mesh的開關)。還可以更換不同的武器(底層實現還是通過添加骨架完成)。。。