作者:qlwuu
相信這里沒有人沒玩過采用骨骼動畫技術(shù)的游戲,看看那些熱門的動作游戲,例如《波斯王子》、《分裂細(xì)胞》和《戰(zhàn)神》,你就知道骨骼動畫的威力了(我承認(rèn)是猜的)。骨骼動畫技術(shù)用來使我們的3D模型在屏幕上動起來,通過和動作捕捉技術(shù)結(jié)合,可以讓模型做出非常逼真的動作。而這樣一個(gè)極具威力的技術(shù),其原理卻相當(dāng)簡單。
假設(shè)我們要讓游戲主角做出一個(gè)動作,例如波斯王子拿彎刀往前一劈。最簡單的方法,就是讓模型師建一個(gè)動畫序列,然后在程序中逐幀播放,就像放電影一樣。不過這樣一來工作量就太大了,玩家也需要N個(gè)G的硬盤來安裝這個(gè)游戲。與此不同,骨骼動畫技術(shù)采用了一種很聰明的方式。首先,建模師完成一個(gè)標(biāo)準(zhǔn)姿勢的3D模型,通常是雙手沿著肩的方向伸展平放,雙腳打開。所有的后繼動作將由這個(gè)基礎(chǔ)動作演變得到。在完成這個(gè)基準(zhǔn)模型之后,建模師再建一個(gè)骨骼結(jié)構(gòu),一系列相互關(guān)聯(lián)的頂點(diǎn),就像一個(gè)骨架一樣,與人體模型各個(gè)關(guān)節(jié)匹配并且都會有一定數(shù)量的頂點(diǎn)與之關(guān)聯(lián)。想象一下人和人身上的骨骼就很容易知道我在說什么。之后,在我們想要完成的動畫序列中,挑選一些關(guān)鍵幀,對每個(gè)關(guān)鍵幀,將骨骼的位置與關(guān)鍵幀匹配。然后把這一系列的關(guān)鍵幀骨骼保存起來,除了骨骼的位置,同時(shí)保存的還有從基準(zhǔn)位置變換到當(dāng)前關(guān)鍵幀的旋轉(zhuǎn)、平移、縮放或者一個(gè)混合的坐標(biāo)變換矩陣。在我們引擎中,首先根據(jù)當(dāng)前時(shí)間查找這時(shí)候角色是處于哪兩個(gè)關(guān)鍵幀中間。找到之后以時(shí)間為參數(shù)在關(guān)鍵幀的坐標(biāo)變換矩陣之間求插值,用插值結(jié)果來決定骨骼當(dāng)前的位置。骨骼位置求出來后,所有和骨骼關(guān)聯(lián)的頂點(diǎn)的坐標(biāo)也可以相應(yīng)求出來了。通過使用骨骼動畫技術(shù),我們用相對較少的數(shù)據(jù)就可以播放很平滑的動畫!
了解了相關(guān)原理,來看看如何在directx中播放骨骼動畫。我的參考書是《Advanced Animation with DirectX》。
現(xiàn)在我們知道為了播放骨骼動畫,需要有骨骼 (bone)的數(shù)據(jù),模型(mesh)的數(shù)據(jù),關(guān)聯(lián)骨骼和模型上每個(gè)頂點(diǎn)的關(guān)聯(lián)數(shù)據(jù),以及關(guān)鍵幀的坐標(biāo)變換數(shù)據(jù)。所有這些數(shù)據(jù)必須以某種形式存在于某個(gè)地方供我們獲取才行。這里要介紹的MS的x文件格式以及從中獲取數(shù)據(jù)的方法。強(qiáng)烈建議大家都來學(xué)習(xí)一下x文件格式!你會發(fā)現(xiàn)它即簡單又強(qiáng)大,即使用來存放自定義數(shù)據(jù)也是相當(dāng)?shù)姆奖悖坏┱莆罩笪冶WC你會對它愛不釋手。
典型的x文件以數(shù)據(jù)模板和實(shí)際數(shù)據(jù)兩部分組成。數(shù)據(jù)模板類似c++中的結(jié)構(gòu)定義,不過更為靈活和開放。實(shí)際數(shù)據(jù)就是遵守模板定義的數(shù)據(jù)段。
看一個(gè)例子:
template Employee {
<3D82AB43-62DA-11cf-AB39-0020AF71E433> // 每個(gè)模板關(guān)聯(lián)唯一的GUID
STRING Name; // 姓名
DWORD Sex; // 性別
[ContactEntry] // 聯(lián)系方式, 另一個(gè)模板,模板可以嵌套
}
template ContactEntry {
<4C9D055B-C64D-4bfe-A7D9-981F507E45FF> // GUID
STRING PhoneNumber; // 電話號碼
STRING Address; // 地址
}
Employee David{
"David";
1;
ContactEntry{
"100-100000000";
"far far away";
}
}
從上面這個(gè)簡單的例子我們就可以看出x文件的大概模樣了,詳細(xì)的情況大家可以參考《Advanced Animation with DirectX》。下面我們看如何來讀取這樣一個(gè)x文件,借助下幾個(gè)對象:
ID3DXFile -- x文件格式文檔對象。例如Employee.x這樣一個(gè)文件。
ID3DXFileEnumObject -- 用來枚舉x文檔的頂級模板數(shù)據(jù)。所謂頂級模板數(shù)據(jù)是指那些沒有父模板的數(shù)據(jù),例如上面的David數(shù)據(jù)段。
ID3DXFILEDATA -- 模板數(shù)據(jù)。上面的David和他的聯(lián)系方式都是ID3DXFILEDATA對象,自包含。
下面看實(shí)際的分析函數(shù), 下面的代碼適用于DirectX 9.0 SDK Update (October 2004),原書的代碼有點(diǎn)過時(shí)了...
//-----------------------------------------------------------------------------
// 名稱 : Parse
// 描述 : 分析x文件格式文檔
//-----------------------------------------------------------------------------
bool Parse( char *filename, void **pData )
{
LPD3DXFILE lpD3DXFile;
LPD3DXFILEENUMOBJECT lpD3DXFileEnumObj;
LPD3DXFILEDATA lpD3DXFileData;
// 參數(shù)檢查
if( NULL == filename )
return false;
// 創(chuàng)建X文件對象
HRESULT hr = D3DXFileCreate( &lpD3DXFile );
if( FAILED( hr ) )
return false;
// 注冊標(biāo)準(zhǔn)模板
hr = lpD3DXFile->RegisterTemplates(
( LPVOID )D3DRM_XTEMPLATES, D3DRM_XTEMPLATE_BYTES );
if( FAILED( hr ) )
{
Release<LPD3DXFILE>( lpD3DXFile );
return false;
}
// 創(chuàng)建X文件枚舉對象
hr = lpD3DXFile->CreateEnumObject(
filename, D3DXF_FILELOAD_FROMFILE, &lpD3DXFileEnumObj );
if( FAILED( hr ) )
{
Release<LPD3DXFILE>( lpD3DXFile );
return false;
}
// 解析開始
bool parseResult = BeginParse( pData );
if( true == parseResult )
{
// 查詢頂級模板數(shù)
SIZE_T childCount = 0;
lpD3DXFileEnumObj->GetChildren( &childCount );
// 分析每個(gè)訂級模板
for( DWORD i=0; i<childCount; i++ )
{
// 獲取當(dāng)前模板
hr = lpD3DXFileEnumObj->GetChild( i, &lpD3DXFileData );
if( FAILED( hr ) )
break;
// 分析
parseResult = ParseObject( lpD3DXFileData, NULL, 0, pData );
// 釋放FileData對象
Release<LPD3DXFILEDATA>( lpD3DXFileData );
// 出現(xiàn)錯誤,中斷分析
if( false == parseResult )
break;
}
// 解析結(jié)束
if( parseResult )
parseResult = EndParse( pData );
}
// 釋放相關(guān)對象
Release<LPD3DXFILEENUMOBJECT>( lpD3DXFileEnumObj );
Release<LPD3DXFILE>( lpD3DXFile );
// 解析結(jié)束
return parseResult;
}
//-----------------------------------------------------------------------------
// 名稱 : ParseObject
// 描述 : 遞歸解析頂級模板
//-----------------------------------------------------------------------------
bool ParseObject(
LPD3DXFILEDATA pDataObj,
LPD3DXFILEDATA pParentDataObj,
DWORD depth,
void **pData )
{
LPD3DXFILEDATA pSubDataObj;
bool parseResult = true;
HRESULT hr;
// 獲取子模板數(shù)目
DWORD childCount;
pDataObj->GetChildren( &childCount );
// 遍歷模板并分析
for( DWORD i=0; i<childCount; i++ )
{
// 取子模板對象
hr = pDataObj->GetChild( i, &pSubDataObj );
if( FAILED( hr ) )
break;
// 分析子模板
parseResult = ParseObject( pSubDataObj, pDataObj, depth+1, pData );
// 釋放數(shù)據(jù)對象
Release<LPD3DXFILEDATA>( pSubDataObj );
// 出現(xiàn)錯誤,停止分析
if( false == parseResult )
break;
}
return parseResult;
}
就那么簡單,相信大家都看得明白。通過重載ParseObject方法,我們以判斷當(dāng)前分析的模板類型,然后創(chuàng)建實(shí)際的模板對象,從文檔中復(fù)制數(shù)據(jù)。有了上面的工具,我們就可以自己來讀取和解析x格式的骨骼動畫文件了。
下面我們就來看看如何重載 ParseObject方法來獲得我們感興趣的數(shù)據(jù),不要擔(dān)心,絕對簡單。仔細(xì)看代碼,你會發(fā)現(xiàn)只需要做一件事情,判斷當(dāng)前數(shù)據(jù)段的類型(通過GUID),分配對應(yīng)的結(jié)構(gòu)對象,然后從數(shù)據(jù)段拷貝數(shù)據(jù)(所有SDK自定義模板的GUID都在頭文件rmxfguid.h中定義, 你需要把它加入你的工程中。所有預(yù)定義模板在
這里可以找到)。先來看看如何獲取當(dāng)前數(shù)據(jù)段的GUID,
GUID objGUID;
pDataObj->GetType( &objGUID );
簡單吧,下面開始我們的分析之旅。x動畫文件中骨骼是用Frame模板定義的,
template Frame
{
< 3D82AB46-62DA-11cf-AB39-0020AF71E433 >
FrameTransformMatrix frameTransformMatrix; // 骨骼相對于父節(jié)點(diǎn)的坐標(biāo)變換矩陣
Mesh mesh; // 骨骼的Mesh
}
只有兩個(gè)字段。FrameTransformMatrix就是一個(gè)matrix。Mesh稍微復(fù)雜,詳細(xì)格式大家自己參考MSDN,我們也會有專門的代碼來加載Mesh,現(xiàn)在關(guān)注Frame。為了加載Frame,我們要在程序中定義一個(gè)和Frame模板對應(yīng)的數(shù)據(jù)結(jié)構(gòu),SDK中經(jīng)默認(rèn)提供了一個(gè),那就是 D3DXFRAME,
typedef struct _D3DXFRAME
{
LPSTR Name; // 骨骼名稱
D3DXMATRIX TransformationMatrix; // 相對與父節(jié)點(diǎn)的坐標(biāo)變換矩陣
LPD3DXMESHCONTAINER pMeshContainer; // LPD3DXMESHCONTAINER對象,用來
// 加載MESH,還有一些附加屬性,見SDK
struct _D3DXFRAME *pFrameSibling; // 兄弟節(jié)點(diǎn)指針,和下面的子節(jié)點(diǎn)指針
// 一塊作用構(gòu)成骨骼的層次結(jié)構(gòu)。
struct _D3DXFRAME *pFrameFirstChild; // 子節(jié)點(diǎn)指針.
} D3DXFRAME, *LPD3DXFRAME;
這樣一個(gè)結(jié)構(gòu)已經(jīng)足夠容納Frame模板中的數(shù)據(jù)并形成一個(gè)層次結(jié)構(gòu),不過為了我們程序的需要,我們還需要其他字段,為此我們通常會擴(kuò)展D3DXFRAME,
typedef struct _D3DXFRAME_EX : public D3DXFRAME
{
D3DXMATRIX matCombined; // 存儲當(dāng)前節(jié)點(diǎn)相對于根節(jié)點(diǎn)的位置偏移矩陣,沿著到
// 到根骨骼的路徑把所有的坐標(biāo)變換矩陣相乘得到。
D3DXMATRIX matOriginal; // 在播放動畫的時(shí)候有可能會改變原來結(jié)構(gòu)中的
// TransformationMatrix,因此我們聲名一個(gè)新的字段
// 將原來的坐標(biāo)變換矩陣保存起來以便在需要的時(shí)候恢
// 復(fù)回去。
... // 忽略一些方法定義
}
我知道有些人已經(jīng)按捺不住了,那么動手吧,
// 判斷當(dāng)前分析的是不是Frame節(jié)點(diǎn)
if( objGUID == TID_D3DRMFrame )
{
// 引用對象直接返回,不需要做分析。一個(gè)數(shù)據(jù)段實(shí)際定義一次后可以
// 被其他模板引用,例如后面的Animation動畫模板就會引用這里的Frame
// 節(jié)點(diǎn),標(biāo)識動畫關(guān)聯(lián)的骨骼。
if( pDataObj->IsReference() )
return true;
// 創(chuàng)建D3DXFRAME_EX結(jié)構(gòu),準(zhǔn)備拷貝數(shù)據(jù)
D3DXFRAME_EX *pFrame = new D3DXFRAME_EX();
// 拷貝名稱
pFrame->Name = GetObjectName( pDataObj );
// 注意觀察文件就可以發(fā)現(xiàn)一個(gè)Frame要么是根Frame,父節(jié)點(diǎn)不存在,
// 要么作為某個(gè)Frame的下級Frame而存在。
if( NULL == pData )
{
// 作為根節(jié)點(diǎn)的兄弟節(jié)點(diǎn)加入鏈表。
pFrame->pFrameSibling = m_pRootFrame;
m_pRootFrame = pFrame;
pFrame = NULL;
// 將自定義數(shù)據(jù)指針指向自己,供子
// 節(jié)點(diǎn)引用。
pData = ( void** )&m_pRootFrame;
}
else
{
// 作為傳入節(jié)點(diǎn)的子節(jié)點(diǎn)
D3DXFRAME_EX *pDataFrame = ( D3DXFRAME_EX* )( *pData );
pFrame->pFrameSibling = pDataFrame->pFrameFirstChild;
pDataFrame->pFrameFirstChild = pFrame;
pFrame = NULL;
pData = ( void** )&pDataFrame->pFrameFirstChild;
}
}
結(jié)束了!是不是很簡單,呵呵,記住我們只需要做一件事情,判斷類型,分配匹配的對象然后拷貝數(shù)據(jù),下面來分析Frame中的 matrix,
// frame的坐標(biāo)變換矩陣, 因?yàn)閙atrix必然屬于某個(gè)Frame所以pData 必須有效
else if( objGUID == TID_D3DRMFrameTransformMatrix && pData )
{
// 我們可以肯定pData指向某個(gè)Frame
D3DXFRAME_EX *pDataFrame = ( D3DXFRAME_EX* )( *pData );
// 先取得緩沖區(qū)大小,應(yīng)該是個(gè)標(biāo)準(zhǔn)的4x4矩陣
DWORD size = 0;
LPCVOID buffer = NULL;
hr = pDataObj->Lock( &size, &buffer );
if( FAILED( hr ) )
return false;
// 拷貝數(shù)據(jù)
if( size == sizeof( D3DXMATRIX ) )
{
memcpy( &pDataFrame->TransformationMatrix, buffer, size );
pDataObj->Unlock();
pDataFrame->matOriginal = pDataFrame->TransformationMatrix;
}
}
這樣大家應(yīng)該對其他類型的模板數(shù)據(jù)分析代碼都應(yīng)該大致猜的出來了。具體的代碼我就不在這里提供,只是簡單的介紹一下它們的作用和關(guān)系,大家可以參考最后附上的工程。
Frame --
骨骼。正如大家已經(jīng)看到的那樣,我們可以用pFrameSibling和pFrameFirstChild兩個(gè)字段來構(gòu)成骨骼的層次結(jié)構(gòu)。骨骼模板包含了當(dāng)前骨骼相對父骨骼的坐標(biāo)變換矩陣和骨骼對應(yīng)的模型
Mesh --
模型。角色的頂點(diǎn)數(shù)據(jù),包含vertex buffer, index buffer等。我們可以直接用普通的ID3DXMesh來加載其中的數(shù)據(jù)。除此之外,Mesh中還包含了SkinWeight模板。
SkinWeight --
骨骼關(guān)聯(lián)的頂點(diǎn)已經(jīng)該骨骼的坐標(biāo)變換對該頂點(diǎn)的權(quán)重。實(shí)際中我們并不需要特殊處理這類模板數(shù)據(jù),ID3DXMesh已經(jīng)包含了對應(yīng)的代碼。
AnimationSet --
動畫集合。例如角色的各種動作“Kill”,“Jump”等等,包含多個(gè)Animation。
Animation --
動畫。由對應(yīng)骨骼的名稱和一組AnimationKey組成。
AnimationKey --
動畫鍵。包含一組時(shí)間戳以及在對應(yīng)時(shí)間戳應(yīng)用到骨骼上的平移、縮放、旋轉(zhuǎn)向量或者復(fù)合的坐標(biāo)變換矩陣。
以上就是我們需要了解的全部了。至此,所有原料都已經(jīng)準(zhǔn)備齊全,各位大廚們下一步要做的就是骨骼動畫這道小菜啦!