ホーム < ゲームつくろー! < DirectX技術編 < xファイルによるポリゴンメッシュのアニメーション(取っ掛かり編)
その23 xファイルによるポリゴンメッシュのアニメーション(取っ掛かり編)
Direct3Dをある程度使えるようになると、どうしてもやりたくなってくるのが「アニメーション」です。もちろん、オブジェクトを移動させたり、回転させたりするのもアニメーションですが、多くの人が「やりたい!」っと熱望するのが「歩く」「走る」「飛ぶ」といったもっとダイナミックなアニメーションではないでしょうか?
ところが、DirectX9のマニュアルを見ても、そのチュートリアルは皆無です。表示の方法は手取り足取り教えてくれているのに、動きに関してはつっけんどん・・・。このように、ことアニメーションとなると、突然冷たくなるDirect3D。この章では、そんな情報不足の「アニメーション」を行う方法を模索していきます。ただ、私も執筆段階で知識は皆無です!!よって、まずは何らかの取っ掛かりを見つけて、とにかくアニメーションの糸口を掴むことに専念します。
@ まずは手がかりを・・・
本当に資料が少ない・・・。それが実感です。日本語サイトのWebにも殆どありません。比較的丁寧に説明してくれているのが本家マイクロソフトのオンラインマニュアルであるmsdnでCutting Edge DX9を連載されている川西 裕幸氏のページ(http://www.microsoft.com/japan/msdn/directx/japan/dx9/default.asp)です。ただ、バックボーンが不足している私にはちょっとまだ敷居が高い。手持ちの書籍ではGAME CODING Vol.1 DirectX/COM編(鎌田 茂雄著)の7章と18章にアニメーションについて触れられています。DirectX9のサンプルの中ではSkinned Meshが直接関係しそうです。実行すると人(tiny)がひょこひょこ歩いてます。やりたいことは正にこういうことですよね。個人的に「綺麗だなぁ」と思ったのがDolphinVS。これは本来頂点シェーダのサンプルなのですが、優雅に泳いでいるイルカはしっかりアニメーションしています。この辺りから手探りでまとめていくしかないみたいです。
A アニメーションとメッシュ
アニメーションをする上で出てくる用語に「メッシュ」というのがあります。これは、簡単に言うなら三角ポリゴンの塊の事です。1つのポリゴンオブジェクトが複数のメッシュに分割されている事があります。それは、オブジェクトが異なるパーツを持っていて、メッシュごとに色々な設定をしたいからです。例えば、RPGなどで出てくる盾のオブジェクトで、宝石が埋め込まれているものがあるとき、盾本体は金属的な質感を持つポリゴンで形成し、宝石はきらびやかに輝く質感を持たせる。そういう時にメッシュに分けるわけです。
書籍によると、アニメーションはメッシュを動かす事で実現されるようです。比較的簡単なのがメッシュの形を変えずにその位置や向きを変化させるアニメーションです。硬い物質の組み合わせである機械の動きをさせるときに用い、メッシュ・アニメーションと呼ばれます。それに対して、人の皮膚や衣服、恐竜の肌や動くスライムなど「軟質系」のアニメーションはメッシュ自体を変形させる必要が出てきます。そういうメッシュを「スキンメッシュ」と呼んでいます。スキンメッシュを使用したアニメーションは、たぶん「スキンメッシュ・アニメーション」と呼ぶのではないかと思います。
B サンプルプログラムSkinned Meshを読み解く!
アニメーションをするための取っ掛かりを掴むために、DirectXのサンプルプログラムである「Skinned Mesh」を出来る限り読み解いてみたいと思います。このサンプルは[SDKルート]\Samples\C++\Direct3D\Meshes\SkinnedMeshにあります(マニュアルが指している位置はちょっと違うので注意)。書籍からの情報でも良いのですが、これを見ている人も一緒に検討するにはサンプルの方が良いと判断しました。
サンプルのプロジェクトを起動させてコンパイルするともちろん通ります。後は、しょっぱなからステップ実行して行きます(F11を押すとしょっぱなから始まります)。
まずWinMain関数にある
d3dApp.Create( hInst )
という関数の内部に入ります。最初の方はウィンドウの初期かなので無視。ずっと下がっていって、Initialize3DEnvironment関数に入ります。
// Initialize the 3D environment for the app
if( FAILED( hr = Initialize3DEnvironment() ) )
関数名からして、Direct3D関連の初期化を全部しているようです。
初期化関数もしばらくはIDirec3DDevice9オブジェクトの生成やらを行っていますが、次の部分:
// Initialize the app's device-dependent objects
hr = InitDeviceObjects();
ここでオブジェクトの読み込みなどを行っています。中に突入です。
C xファイルから情報を取得(InitDeviceObjects関数内部)
この辺りから慎重に行きましょう。まず、
CAllocateHierarchy Alloc(this);
これは、ID3DXAllocateHierarchyというインターフェイスを継承しているクラスです。ID3DXAllocateHierarchyをマニュアルで調べてみます。
ID3DXAllocateHierarchyインターフェイス このインターフェイスは、アプリケーションによって実装され、フレームおよびメッシュ コンテナ オブジェクトの割り当てまたは解放を行う。ここに含まれるメソッドは、フレーム階層をロードおよび破棄する処理で呼び出される。
この説明によると、どうやらこのインターフェイスはフレームやメッシュをひとまとめにするコンテナ(箱)を生成したり解放したりしてくれるようです。メッシュは良いのですがフレームとは何でしょう。マニュアルで調べてみると、どうやらxファイルの中身のお話のようです。それだけだとちょっとイメージができなかったのですが、さらに検索に引っかかってきたD3DXFRAME構造体の中身を見ると、フレームというものが何となく想像できてきました:
D3DXFRAME構造体 typedef struct _D3DXFRAME {
LPTSTR Name;
D3DXMATRIX TransformationMatrix;
LPD3DXMESHCONTAINER pMeshContainer;
struct _D3DXFRAME *pFrameSibling;
struct _D3DXFRAME *pFrameFirstChild;
} D3DXFRAME, *LPD3DXFRAME;
Nameはフレームの名前です。
TransformationMatrixはトランスフォーム行列。何かを座標変換するのに使うはずです。
pMeshContainerはメッシュコンテナへのポインタとあります。
pFrameSiblingは兄弟フレームへのポインタ、
pFrameFirstChildは子フレームへのポインタです。
つまり、フレームというのはメッシュコンテナ(メッシュ情報が入った箱)を保持しており、他のフレームとのつながり(兄弟とか子とか)を持ち、変換行列によってメッシュの座標を変換する(のかな)、そういったメッシュに関連する複合情報の単位のようです。
ID3DXAllocateHierarchyインターフェイスは結局、このフレームのつながり、つまり階層を作成して管理してくれるマネージャのようなものだと想像します。良く見れば、Allocate(メモリ確保)Hierarchy(階層)という名前が正にそれを示していますね。これがどう使われるのか、もう少しサンプルをトレースしてみましょう。
次のm_pFontやm_pFontSmallというのは名前からしてフォント関連の設定でしょうから、あまり関係なさそうです。次の一塊は間違いなく鍵ですね。
// Load the mesh from the specified file
hr = DXUtil_FindMediaFileCb( strMeshPath, sizeof(strMeshPath), m_strMeshFilename );
if (FAILED(hr))
return hr;
hr = D3DXLoadMeshHierarchyFromX(strMeshPath, D3DXMESH_MANAGED, m_pd3dDevice, &Alloc, NULL, &m_pFrameRoot, &m_pAnimController);
if (FAILED(hr))
return hr;
hr = SetupBoneMatrixPointers(m_pFrameRoot);
if (FAILED(hr))
return hr;
hr = D3DXFrameCalculateBoundingSphere(m_pFrameRoot, &m_vObjectCenter, &m_fObjectRadius);
if (FAILED(hr))
return hr;
コメントが「特定のファイルからメッシュをロードする」とありますから、ここでメッシュを読み込んでいるはずです。
DXUtil_FindMediaFileCb関数はサンプル独自定義のユーティリティ関数で、指定のファイル名からそのフルパスとファイルの存在をチェックしてくれているようです。このプログラムでは「tiny.x」という人オブジェクトを格納してあるxファイルの振るパスを取得しています。
次のD3DXLoadMeshHierarchyFromX関数が核でしょう。名前の通り、xファイルからメッシュの階層を読み込む関数です。この関数の定義を見てみましょう。
D3DXLoadMeshHierarchyFromX関数 HRESULT D3DXLoadMeshHierarchyFromX(
LPCTSTR Filename,
DWORD MeshOptions,
LPDIRECT3DDEVICE9 pDevice,
LPD3DXALLOCATEHIERARCHY pAlloc,
LPD3DXLOADUSERDATA pUserDataLoader,
LPD3DXFRAME* ppFrameHeirarchy,
LPD3DXANIMATIONCONTROLLER* ppAnimController
);
Filenameにはxファイルを指定します。
MeshOptionというのはメッシュ作成のオプションです。D3DXMESH_MANAGEDを指定するのが一般的のようで、Direct3Dが管理するメモリ下に作成してくれます。
pDeviceはデバイスへのポインタです。これは問題ないですよね。
pAllocはID3DXAllocateHierarchyインターフェイスへのポインタを渡します。詳しい事はマニュアルに「これでもか!」というくらい載っているのですが、要は指定のxファイルからフレーム階層を構築してくれるようです。
pUserDataLoaderはxファイル内にユーザ定義部分があるとき、その情報をここに格納するようです。
ppFrameHeirarchyはD3DXFRAME構造体へのダブルポインタで、ここにフレーム階層の親分(ルートフレーム)へのポインタのポインタが返るようです。ダブルポインタを渡すという事は、明らかにフレームが配列化されているという事ですよね。このポインタを通して子フレームにアクセスできるようになるんでしょう。
ppAnimControllerはID3DXAnimationControllerインターフェイスへのポインタで、xファイル内のアニメーションに対応するアニメーションコントローラへのポインタが返るようです(マニュアルの受け売りです)。きっとここにアニメーションに関する情報が格納されるはずです!ID3DXAnimationControllerインターフェイスとは何者なのか、後々調べる必要がありそうですね。
ということで、D3DXLoadMeshHierarchyFromX関数は、xファイルからフレームやメッシュ、アニメーションに関する情報を抽出してくれるありがたい関数である事がわかりました。この関数はxファイルによるアニメーションをする上で必須になりそうです。
次はSetupBoneMatrixPointers関数です。これは、独自に作成された関数のようで、関数内部でルートフレームから子フレームを検索しながら、メッシュ情報を持つフレームを探し当て、そこに格納されている「ボーン」の情報を格納しているようです。これについては後述します。
D3DXFrameCalculateBoundingSphere関数という長い名前の関数は、ルートフレームに含まれるメッシュ情報からオブジェクトの境界球の大きさを計算してくれるようです。これは、メッシュごとではなくてルートフレームに所属するメッシュ全体での境界球です。衝突判定などで使いそうです。
どうやら、これでオブジェクト自体の読み込みは終わったようです。InitDeviceObjects関数内の最後にあるD3DXCreateEffectFromFile関数は頂点シェーダを読み込む部分なので、アニメーションとは直接関係しません。
少しずつですが概容がわかってきました。メッシュはフレーム単位で管理されていて、フレームは階層構造を成している。D3DXLoadMeshHierarchyFromX関数によってそれらをすべて格納できる。フレームを辿る事で、色々な情報へアクセスできる。おお、おぼろげにわかってきました(^-^)
D アニメーションはどうやっているのか?(FrameMove
関数内)
xファイルからアニメーション関連の情報を取得する方法はなんとなく見えてきました。では、実際にどうやってアニメーションを動かしているのか?それを知るにはプログラムの実行部分を見る必要があります。トレースは続きます。
WinMain関数内のRun関数の内部に入ります。ここはいわゆるメッセージループ部分に相当するのですが、見るべき点はただ一つ、Render3DEnvironment関数内です。他はすべてWindowsメッセージの処理部分になっています。この関数の内部にさらに入ります。
関数内部では、実時間を計算しているようです。そして真ん中下辺りにあるFrameMove関数の中で1フレーム動かしているようです。この関数の最初では、レンダリングパイプラインの基本であるワールド変換行列とビュー変換をデバイスに教えているのですが、注目はその後です。
if (m_pAnimController != NULL)
m_pAnimController->SetTime(m_pAnimController->GetTime() + m_fElapsedTime);
UpdateFrameMatrices(m_pFrameRoot, &matWorld);
なんとなくアニメーションを実行してそうな部分ですよね。m_pAnimControllerはID3DXAnimationControllerオブジェクトのようです。マニュアルで調べたたところ、このインターフェイスのSetTime関数に時間を表す数値(DWORD)を渡すと、フレーム階層に設定してある変換行列を更新してくれるのだそうです。時間を渡しているということは、その時間でのオブジェクトの動きを補間計算してくれているようなイメージがあります。具体的には、指定した時間にポリゴンメッシュの頂点が何処にあるか計算するための行列を「自動的に」更新してくれるといったところでしょうか。えらい便利です。
次のUpdateFrameMatrics関数はサンプル独自の関数で、引数にルートフレームとワールド座標を渡しています。この関数の中を見てみましょう。
UpdateFrameMatrices関数 void CMyD3DApplication::UpdateFrameMatrices(
LPD3DXFRAME pFrameBase,
LPD3DXMATRIX pParentMatrix
)
{
D3DXFRAME_DERIVED *pFrame =
(D3DXFRAME_DERIVED*)pFrameBase;
// フレームにある行列からワールド変換行列を生成
if (pParentMatrix != NULL)
D3DXMatrixMultiply(
&pFrame->CombinedTransformationMatrix,
&pFrame->TransformationMatrix,
pParentMatrix
);
else
pFrame->CombinedTransformationMatrix =
pFrame->TransformationMatrix;
// 兄弟のフレームの行列をワールド変換行列に
if (pFrame->pFrameSibling != NULL)
{
UpdateFrameMatrices(pFrame->pFrameSibling, pParentMatrix);
}
// 子フレームの行列をワールド変換行列に
if (pFrame->pFrameFirstChild != NULL)
{
UpdateFrameMatrices(pFrame->pFrameFirstChild,
&pFrame->CombinedTransformationMatrix);
}
日本語のコメントは私が付けました。つけながらやっている事が良く分かりました(^-^)。この関数では、先ほどのm_pAnimController->SetTime関数で更新された「相対変換行列」を「ワールド変換行列」に変換する作業をしています。最初に登場するD3DXFRAME_DERIVED構造体はDirect3DXでは定義されていません。これはサンプルが独自に定義しているようで、D3DXFRAME構造体からの派生構造体になっており、
D3DXMATRIXA16 CombinedTransformationMatrix;
のみ追加しています。Combined(結合された)TransformationMatrix(変換行列)ですから、ワールド変換行列として変換された行列をここに格納するはずです。確かにこうすると管理しやすいですね。そのワールド変換は、実はすぐ下の行列の掛け算(D3DXMatrixMultiply関数)で行っています。親フレームの変換作業はこれで終了です。
次に兄弟フレームの行列をワールド変換行列にするためにまたUpdateFrameMatrixs関数を再帰的に呼んでいます。兄弟の作業が終わったら、次は子の作業です。このように、この関数によってフレームに設定してあるすべてのローカル変換行列はワールド変換行列に変換されることになります。
これでFrameMove関数の仕事はおしまいです。この関数を抜けると、すぐにレンダリング作業に移ります。
E オブジェクトのレンダリング
行列の更新がすべて終了すると、レンダリングに入ります。これは、Render3DEnvironment関数内のRender関数内部で行われます。
Render関数内部ではバックバッファのクリア、ライトの設定、射影行列の設定など、通常のレンダリングに必要な作業を最初に行っています。実際のレンダリングはおなじみのIDirect3DDevice9::BegeinScene関数とEndScene関数の間で行われます。サンプルプログラムでフレームの描画はDrawFrame関数にまとめられているようです。
この関数の内部は次のようになっています。
DrawFrame関数 void CMyD3DApplication::DrawFrame(LPD3DXFRAME pFrame)
{
LPD3DXMESHCONTAINER pMeshContainer;
// フレーム内のメッシュコンテナを格納
pMeshContainer = pFrame->pMeshContainer;
// 連結するメッシュがなくなるまで描画
while (pMeshContainer != NULL)
{
// メッシュを描画
DrawMeshContainer(pMeshContainer, pFrame);
// 次のメッシュにスイッチ
pMeshContainer = pMeshContainer->pNextMeshContainer;
}
// 兄弟フレームを描画
if (pFrame->pFrameSibling != NULL)
{
DrawFrame(pFrame->pFrameSibling);
}
// 子フレームを描画
if (pFrame->pFrameFirstChild != NULL)
{
DrawFrame(pFrame->pFrameFirstChild);
}
}
構造が先ほどのUpdateFrameMatrixs関数と良く似ていますね。まず最初に、フレームに格納されているメッシュコンテナを格納し、ループによってメッシュを描画しています。pNextMeshContainerという変数からして、どうやら「1つのフレームには複数のメッシュが存在できる」ようです。重要な情報です。親フレームの描画が終了したら、次は兄弟フレームの描画に移っています。さらに子ふーレムも再帰的に関数を呼び出して描画しています。こう見ると、フレーム階層というのは統制が取れた見事な管理がされています。
さて実際に描画をしてくれているDrawMeshContainer関数の内部をさらに見てみましょう。この関数はCMyD3DApplicationクラスのメンバ関数として定義されています。で、この関数の中身なんですが・・・尋常じゃなく長い!。何だか良く分かりませんがやたらと長い条件分岐をしています。そのまま見ていると目がくらむので、とりあえず一番でっかいくくりになっている条件文だけを抜き出してみます。
DrawFrame関数 void CMyD3DApplication::DrawMeshContainer(
LPD3DXMESHCONTAINER pMeshContainerBase,
LPD3DXFRAME pFrameBase
)
{
// first check for skinning
if (pMeshContainer->pSkinInfo != NULL)
{
if (m_SkinningMethod == D3DNONINDEXED){
// ......}
else if (m_SkinningMethod == D3DINDEXED){
// ......}
else if (m_SkinningMethod == D3DINDEXEDVS){
// ......}
else if (m_SkinningMethod == D3DINDEXEDHLSLVS){
// ......}
else if (m_SkinningMethod == SOFTWARE){
// ......}
else // bug out as unsupported mode{
return;}
}
else // standard mesh, just draw it after setting material properties
{
// ......
}
}
一番外側の条件分岐は、メッシュが「スキンメッシュ」なのか「通常のメッシュ」なのかで描画を分けているようです。スキンメッシュだった場合、m_SkinningMethod変数の内容によって描画方法を細かく分けています。フラグは独自に定義しているようで、Direct3Dの中では定義されていません。マクロの名前から想像するに、
D3DNONINDEXED インデックス番号が付いていないメッシュの描画 D3DINDEXED インデックス番号が付いているメッシュの描画 D3DINDEXEDVS インデックス番号付き+頂点シェーダ描画 D3DINDEXEDHLSLVS インデックス番号つき+HLSLによる頂点シェーダ描画 SOFTWARE ソフトウェアによる描画
という条件分岐をしているようです。この章はアニメーションの概容をざっと見る目的がありますから、細かな中身についてはここでは触れない事にします。兎にも角にもこの場合分けによって、様々なスキンメッシュをエラー無く描画しているようです。
この関数を抜けると、描画がすべて終了し、ループが1回りします。いや〜、大変でした(^-^;;;
F ここまでで分かったこと、まとめます
非常に長いプロセスでしたが、じっくりと見ていったおかげで知識ゼロから随分と進歩したように思えます。間違いもあるかもしれませんが、ここまでわかったことをまとめてみることにします。
まず、xファイルからアニメーションメッシュを読み込むにはD3DXLoadMeshHierarchyFromX関数を用います。この関数に渡す変数のうち、
・ pAlloc(ID3DXAllocateHierarchyインターフェイスポインタ)
・ ppFrameHeirarchy(D3DXFRAME構造体へのダブルポインタ)
・ ppAnimContoroller(ID3DXAnimationControllerインターフェイスポインタ)
が肝で、pAllocにフレームの階層構造が、ppFrameHeirarchyにフレームの配列が、ppAnimControllerにアニメーションを担当するインターフェイスがそれぞれ格納されます。
フレームは単数・複数のメッシュを持ち、メッシュの座標系内での変換行列を定義し、また親・兄弟・子の階層関係を確立するリストを持ちます。フレームの変数はD3DXFRAME構造体に格納されます。初期化作業はこの確立と保持がメインのようです。
実際にアニメーションをするためには、ppAnimContorollerのSetTime関数に時間を指定してやります。多分、xファイル内部にキーフレームが設定されていて、任意の時間に対するメッシュの位置を補間計算して行列を設定してくれているんでしょう。ありがたいことです。ただ、もしIKをやるのであれば、この関数を呼ばずに行列を直接変更する事になるのかもしれません。
SetTime関数によって更新した行列はメッシュ空間でメッシュを動かす「ローカル変換行列」です。実際動いたメッシュはワールド座標に置かれる必要があるため、サンプルではこの変換を行う再帰関数を独自に作成していました。この実装は非常に参考になりますね。
行列をワールド行列に変換したら、あとはその行列の情報を用いてメッシュを実際に動かしてワールド空間に描画すると、アニメーションが完成します。この部分は非常に複雑でして、この章では触れていません。
Skinned Meshサンプルをじっくりと見ることにより、xファイルからフレーム階層をダイレクトに読み込み、行列を用いてアニメーションをする筋道が理解できました。ただ、描画の部分や細かな行列の計算などは曖昧にしか触れてきていません。また、ところどころにボーンも出てきていたのですが、それもはしょっています。それらについては次の章からの各論でまた詳しく考えていくことになります。どうやらしばらくはアニメーションに章を割くことになりそうです。