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