ホーム < ゲームつくろー! < DirectX技術編
その68 極短Xファイルスキンメッシュアニメーション
スキンメッシュアニメーションは3Dプログラマの一つの壁かなと思います。データ構造を含めすべて1から作り上げるのは、理屈を完璧に理解していなければ絶対に出来ませんし、理解していても構造が複雑で組むのに難儀します。そんな中でXファイルによるスキンメッシュアニメーションは、DirectXが提供するヘルパー関数を用いることで手間を物凄く省かせてくれます。とは言え、面倒な部分もあるのは確か。そして、コンパクトにまとまったソースコードもWeb上にはあまり見当たりません。
そこで、Xファイルを用いたこてこてのコードを組むとどうなるのか、改めて作ってみる事にしました。コードの細かい部分の説明は、マルペケのスキンメッシュアニメーションの記事に記載している事とかぶりますので、多くは省かせて頂きますが、これでスキンメッシュアニメーションの壁が少しでも低くなれば幸いです(^-^)
マルペケ恒例極短シリーズ、今回はスキンメッシュアニメーションです。
@ 1にも2にもID3DXAllocateHierarchyが面倒くさい…
Xファイルを用いたスキンメッシュアニメーションの入り口はD3DXLoadMeshHierarchyFromX関数です。この関数はスキンメッシュが入ったXファイルから情報を余すことなく引き出してくれて(パーシング)、ボーンの構成であるフレームツリー(D3DXFRAME)、FKアニメーション(ID3DXAnimationController)という2つのオブジェクトを返してくれます。
この関数にXファイルを投げると自動的にパースし膨大な結果を返してくれます。その結果を受けてフレームに格納する繋ぎ役になるのが「ID3DXAllocateHierarchy」インターフェイスです。このインターフェイスはメソッドだけが提供されていて実体がありません。この実装はプログラマ自身がやらなければならないのです。これがXファイルによるスキンメッシュアニメーションのいきなり大きな(そして面倒臭い)壁だったりします。
そこで今回は理屈抜き、極短コードを実現するために愚直に実装したID3DXAllocateHierarchyの実装を以下にどどんと公開します:
OX::OXAllocateHierarchyクラス #ifndef IKD_OX_OXALLOCATEHIERARVHY_H
#define IKD_OX_OXALLOCATEHIERARVHY_H
#include <d3dx9.h>
namespace OX {
struct OXD3DXFRAME : public D3DXFRAME {
DWORD id;
D3DXMATRIX offsetMatrix;
OXD3DXFRAME() : id(0xffffffff) {
D3DXMatrixIdentity( &offsetMatrix );
}
};
struct OXD3DXMESHCONTAINER : public D3DXMESHCONTAINER {
DWORD maxFaceInfl;
DWORD numBoneCombinations;
ID3DXBuffer *boneCombinationTable;
OXD3DXMESHCONTAINER() : maxFaceInfl(1), numBoneCombinations(0), boneCombinationTable(0) {}
};
class AllocateHierarchy : public ID3DXAllocateHierarchy {
char *copyName( const char* name ) {
char *n = 0;
if ( !name || name[0] == '\0' ) {
n = new char[1];
n[0] = '\0';
} else {
size_t len = strlen(name);
n = new char[ strlen(name) + 1 ];
strcpy_s( n, strlen(name) + 1, name );
}
return n;
}
public:
STDMETHOD(CreateFrame)(THIS_ LPCSTR Name, LPD3DXFRAME *ppNewFrame) {
OXD3DXFRAME *newFrame = new OXD3DXFRAME;
newFrame->Name = copyName( Name );
newFrame->pFrameFirstChild = 0;
newFrame->pFrameSibling = 0;
newFrame->pMeshContainer = 0;
D3DXMatrixIdentity( &newFrame->TransformationMatrix );
*ppNewFrame = newFrame;
return D3D_OK;
}
STDMETHOD(DestroyFrame)(THIS_ LPD3DXFRAME pFrameToFree) {
if ( pFrameToFree->pFrameFirstChild )
DestroyFrame( pFrameToFree->pFrameFirstChild );
if ( pFrameToFree->pFrameSibling )
DestroyFrame( pFrameToFree->pFrameSibling );
if ( pFrameToFree->pMeshContainer )
DestroyMeshContainer( pFrameToFree->pMeshContainer );
delete[] pFrameToFree->Name;
delete pFrameToFree;
return D3D_OK;
}
STDMETHOD(CreateMeshContainer)(THIS_
LPCSTR Name,
CONST D3DXMESHDATA *pMeshData,
CONST D3DXMATERIAL *pMaterials,
CONST D3DXEFFECTINSTANCE *pEffectInstances,
DWORD NumMaterials,
CONST DWORD *pAdjacency,
LPD3DXSKININFO pSkinInfo,
LPD3DXMESHCONTAINER *ppNewMeshContainer
) {
OXD3DXMESHCONTAINER *newCont = new OXD3DXMESHCONTAINER;
newCont->Name = copyName( Name );
newCont->pAdjacency = new DWORD[ pMeshData->pMesh->GetNumFaces() * 3 ];
memset( newCont->pAdjacency, 0, pMeshData->pMesh->GetNumFaces() * 3 * sizeof(DWORD) );
newCont->MeshData.Type = pMeshData->Type;
pSkinInfo->ConvertToBlendedMesh(
pMeshData->pMesh, 0, pAdjacency, newCont->pAdjacency, 0, 0, &newCont->maxFaceInfl,
&newCont->numBoneCombinations, &newCont->boneCombinationTable, &newCont->MeshData.pMesh
);
newCont->NumMaterials = NumMaterials;
newCont->pMaterials = new D3DXMATERIAL[ NumMaterials ];
memcpy( newCont->pMaterials, pMaterials, NumMaterials * sizeof(D3DXMATERIAL) );
newCont->pEffects = 0;
if ( pEffectInstances ) {
newCont->pEffects = new D3DXEFFECTINSTANCE;
newCont->pEffects->pEffectFilename = copyName( pEffectInstances->pEffectFilename );
newCont->pEffects->NumDefaults = pEffectInstances->NumDefaults;
newCont->pEffects->pDefaults = new D3DXEFFECTDEFAULT[ pEffectInstances->NumDefaults ];
for ( DWORD i = 0; i < pEffectInstances->NumDefaults; i++ ) {
D3DXEFFECTDEFAULT *src = pEffectInstances->pDefaults + i;
D3DXEFFECTDEFAULT *dest = newCont->pEffects->pDefaults + i;
dest->NumBytes = src->NumBytes;
dest->Type = src->Type;
dest->pParamName = copyName( src->pParamName );
dest->pValue = new char[src->NumBytes];
memcpy( dest->pValue, src->pValue, src->NumBytes );
}
}
newCont->pSkinInfo = pSkinInfo;
pSkinInfo->AddRef();
*ppNewMeshContainer = newCont;
return D3D_OK;
}
STDMETHOD(DestroyMeshContainer)(THIS_ LPD3DXMESHCONTAINER pMeshContainerToFree) {
OXD3DXMESHCONTAINER *m = (OXD3DXMESHCONTAINER*)pMeshContainerToFree;
m->MeshData.pMesh->Release();
delete[] m->Name;
delete[] m->pAdjacency;
if ( m->pEffects ) {
for ( DWORD i = 0; i < m->pEffects->NumDefaults; i++ ) {
D3DXEFFECTDEFAULT *d = m->pEffects->pDefaults + i;
delete[] d->pParamName;
delete[] d->pValue;
}
delete[] m->pEffects->pDefaults;
delete[] m->pEffects->pEffectFilename;
delete m->pEffects;
}
delete[] m->pMaterials;
if ( m->pSkinInfo )
m->pSkinInfo->Release();
if ( m->boneCombinationTable )
m->boneCombinationTable->Release();
delete m;
return D3D_OK;
}
};
}
#endif
このコード、かなり切り詰めました。切り詰めてもこんなです(^-^;。
ID3DXAllocateHierarchyインターフェイスはD3DXFRAMEの生成と破棄、D3DXMESHCONTAINERの生成と破棄の4つのメソッドから構成されます。ポイントがいくつかあります。まず今回スキンメッシュをより楽にできるようにD3DXFRAMEの派生クラスであるOXD3DXFRAMEクラスを作っています。派生クラスにはidとoffsetMatrixの2つのメンバを追加しました。idはそのD3DXFRAMEが担当するボーンIDです。このIDは、ボーン構造の入ったXファイルを解析すると出来るID3DXSkinInfoというオブジェクトが知っています。
D3DXFRAMEにはボーンの名前であるNameメンバが最初からいて、上のCreateFrameメソッドの引数にもそれが文字列で入ってきます。なのにどうしてわざわざIDを別途格納するのか?それは、D3DXFRAMEの名前(Name)はアニメーションを行うどの過程でも一切使われないからです!衝撃の事実…orz。一方のボーンIDはオフセット行列を指定する時やボーンの姿勢を更新する時、そして描画デバイスに各ボーンのワールド変換行列を渡す時などで使われる大切なキー(鍵)となってくれるんです。最初からID渡せ〜っと思ってしまいます(笑)。ただ一つ注意。Xファイルのフレーム構造には対応するボーンが無いNULLフレームが混じっています。そういう対応ボーンが無いフレームのIDは0xffffffffと統一する事にしました。
OXD3DXFRAMEに追加されているもう一つのメンバoffsetMatrixは、名前の通りボーンオフセット行列を格納します。これはフレームの初期姿勢となる大切な行列で、これも最初から渡せーと思う行列の一つです。
最強に面倒なのが次にあるCreateMeshContainerメソッドです。これはD3DXMESHCONTAINER構造体にデータを収めるのが主目的なメソッドなのですが、要素の数も種類も多く、また不定長データが多いため細かくnewしていくしかなくて吐きそうになります(^-^;。データ構造は決まっているので、静々とそれに従ってメモリを確保して格納しています。
このメソッドの中ではID3DXSkinInfo::ConvertBlendedMeshメソッドを呼び出しています。ここが最大に大切なポイントでして、これによってメッシュはスキンメッシュとして動くようになるんです。このメソッドが返すボーン影響数(maxFaceInfl)、ボーンコンビネーション構造体数(numBoneCombinations)そしてボーンコンビネーション構造体テーブル(boneCombinationTable)はスキンメッシュを行う時にすべて使いますので、OXD3DXMESHCONTAINERクラスでこれらを追加しています。
削除メソッド(DestroyFrame、DestroyMeshContainerメソッド)は確保した物を全部きれいさっぱり消しているだけです。メモリが細かく確保されているので、削除忘れがないか検討するのが一苦労する部分でもあります(^-^;
こんな感じで、極短とは言えID3DXAllocateHierarchyインターフェイスを愚直に実装するのはとにかく面倒なんです。
A 極短main.cpp公開します
@のOX::OXAllocateHierarchyクラスを使うとD3DXLoadMeshHierarchyFromX関数を使ってXファイルからすべての情報を引き出せます。ただ、フレームに格納するIDとかオフセット行列とか、いくつか足りない所もあります。それらは読み込みが終わった後に補ってあげる必要があります。
それらの作業を含めた、スキンメッシュアニメーションの描画まで行っているプログラム(main.cpp)を全公開します!コピペの用意はよろしいですか〜:
main.cpp // 極短 XFile Skin Mesh Animation!
#pragma comment(lib, "dxguid.lib")
#pragma comment(lib, "d3d9.lib")
#pragma comment(lib, "d3dx9.lib")
#include <windows.h>
#include <tchar.h>
#include <d3d9.h>
#include <d3dx9.h>
#include <map>
#include <string>
#include "oxallocatehierarchy.h"
TCHAR gName[] = _T("極短 XFile Skin Mesh Animation");
OX::OXD3DXMESHCONTAINER *getMeshContainer( D3DXFRAME *frame ) {
if ( frame->pMeshContainer )
return (OX::OXD3DXMESHCONTAINER*)frame->pMeshContainer;
if (frame->pFrameFirstChild) {
OX::OXD3DXMESHCONTAINER *mesh = getMeshContainer(frame->pFrameFirstChild);
if ( mesh != 0 )
return mesh;
if ( frame->pFrameSibling )
return getMeshContainer( frame->pFrameSibling );
return 0;
}
void setFrameId( OX::OXD3DXFRAME *frame, ID3DXSkinInfo *info ) {
std::map<std::string, DWORD> nameToIdMap;
for ( DWORD i = 0; i < info->GetNumBones(); i++ )
nameToIdMap[ info->GetBoneName( i ) ] = i;
struct create {
static void f( std::map<std::string, DWORD> nameToIdMap, ID3DXSkinInfo *info, OX::OXD3DXFRAME* frame ) {
if ( nameToIdMap.find( frame->Name ) != nameToIdMap.end() ) {
frame->id = nameToIdMap[ frame->Name ];
frame->offsetMatrix = *info->GetBoneOffsetMatrix( frame->id );
}
if ( frame->pFrameFirstChild )
f( nameToIdMap, info, (OX::OXD3DXFRAME*)frame->pFrameFirstChild );
if ( frame->pFrameSibling )
f( nameToIdMap, info, (OX::OXD3DXFRAME*)frame->pFrameSibling );
}
};
create::f( nameToIdMap, info, frame );
}
void updateCombMatrix( std::map<DWORD, D3DXMATRIX> &combMatrixMap, OX::OXD3DXFRAME *frame ) {
struct update {
static void f( std::map<DWORD, D3DXMATRIX> &combMatrixMap, D3DXMATRIX &parentBoneMatrix, OX::OXD3DXFRAME *frame ) {
D3DXMATRIX &localBoneMatrix = frame->TransformationMatrix;
D3DXMATRIX boneMatrix = localBoneMatrix * parentBoneMatrix;
if ( frame->id != 0xffffffff)
combMatrixMap[ frame->id ] = frame->offsetMatrix * boneMatrix;
if ( frame->pFrameFirstChild )
f( combMatrixMap, boneMatrix, (OX::OXD3DXFRAME*)frame->pFrameFirstChild );
if ( frame->pFrameSibling )
f( combMatrixMap, parentBoneMatrix, (OX::OXD3DXFRAME*)frame->pFrameSibling );
}
};
D3DXMATRIX iden;
D3DXMatrixIdentity( &iden );
update::f( combMatrixMap, iden, frame );
}
LRESULT CALLBACK WndProc(HWND hWnd, UINT mes, WPARAM wParam, LPARAM lParam){
if(mes == WM_DESTROY) {PostQuitMessage(0); return 0;}
return DefWindowProc(hWnd, mes, wParam, lParam);
}
int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow) {
// アプリケーションの初期化
MSG msg; HWND hWnd;
WNDCLASSEX wcex ={sizeof(WNDCLASSEX), CS_HREDRAW | CS_VREDRAW, WndProc, 0, 0, hInstance, NULL, NULL, (HBRUSH)(COLOR_WINDOW+1), NULL, (TCHAR*)gName, NULL};
if(!RegisterClassEx(&wcex))
return 0;
int sw = 600, sh = 600;
RECT r = {0, 0, sw, sh};
::AdjustWindowRect( &r, WS_OVERLAPPEDWINDOW, FALSE );
if(!(hWnd = CreateWindow(gName, gName, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, r.right - r.left, r.bottom - r.top, NULL, NULL, hInstance, NULL)))
return 0;
// Direct3Dの初期化
LPDIRECT3D9 g_pD3D;
LPDIRECT3DDEVICE9 g_pD3DDev;
if( !(g_pD3D = Direct3DCreate9( D3D_SDK_VERSION )) ) return 0;
D3DPRESENT_PARAMETERS d3dpp = {sw , sh, D3DFMT_X8R8G8B8, 0, D3DMULTISAMPLE_NONE, 0, D3DSWAPEFFECT_DISCARD, NULL, TRUE, TRUE, D3DFMT_D24S8, 0, 0, D3DPRESENT_INTERVAL_DEFAULT};
if( FAILED( g_pD3D->CreateDevice( D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWnd, D3DCREATE_HARDWARE_VERTEXPROCESSING, &d3dpp, &g_pD3DDev ) ) ) {
g_pD3D->Release();
return 0;
}
// スキンメッシュ情報をXファイルから取得
OX::AllocateHierarchy allocater;
OX::OXD3DXFRAME *pRootFrame = 0;
ID3DXAnimationController *controller = 0;
D3DXLoadMeshHierarchyFromX( _T("tiny.x"), D3DXMESH_MANAGED, g_pD3DDev, &allocater, 0, (D3DXFRAME**)&pRootFrame, &controller );
OX::OXD3DXMESHCONTAINER *cont = getMeshContainer( pRootFrame );
D3DXBONECOMBINATION *combs = (D3DXBONECOMBINATION*)cont->boneCombinationTable->GetBufferPointer();
// フレーム内にボーンIDとオフセット行列を埋め込む
setFrameId( pRootFrame, cont->pSkinInfo);
// テクスチャ作成
IDirect3DTexture9 *tex = 0;
D3DXCreateTextureFromFile( g_pD3DDev, _T("Tiny_skin.dds"), &tex );
// ビュー、射影変換行列設定
D3DXMATRIX world, view, proj;
D3DXMatrixIdentity( &world );
D3DXMatrixLookAtLH( &view, &D3DXVECTOR3( 0.0f, 0.0f, -1500.0f ), &D3DXVECTOR3( 0.0f, 0.0f, 0.0f ), &D3DXVECTOR3( 0.0f, 1.0f, 0.0f ) );
D3DXMatrixPerspectiveFovLH( &proj, D3DXToRadian( 30.0f ), 1.0f, 0.01f, 10000.0f );
g_pD3DDev->SetTransform( D3DTS_VIEW, &view );
g_pD3DDev->SetTransform( D3DTS_PROJECTION, &proj );
// ライトオフ
g_pD3DDev->SetRenderState( D3DRS_LIGHTING, FALSE );
ShowWindow(hWnd, nCmdShow);
// メッセージ ループ
std::map<DWORD, D3DXMATRIX> combMatrixMap;
do{
if( PeekMessage(&msg, NULL, 0, 0, PM_REMOVE) ){ DispatchMessage(&msg);}
else{
// 時間を進めて姿勢更新
controller->AdvanceTime( 0.016f, 0 );
updateCombMatrix( combMatrixMap, pRootFrame );
// Direct3Dの処理
g_pD3DDev->Clear( 0, NULL, D3DCLEAR_TARGET | D3DCLEAR_STENCIL | D3DCLEAR_ZBUFFER, D3DCOLOR_XRGB(255, 255, 255), 1.0f, 0 );
g_pD3DDev->BeginScene();
g_pD3DDev->SetTexture( 0, tex );
for ( DWORD i = 0; i < cont->numBoneCombinations; i++ ) {
DWORD boneNum = 0;
for( DWORD j = 0; j < cont->maxFaceInfl; j++ ) {
DWORD id = combs[i].BoneId[ j ];
if( id != UINT_MAX ) {
g_pD3DDev->SetTransform( D3DTS_WORLDMATRIX( j ), &combMatrixMap[ id ] );
boneNum++;
}
}
g_pD3DDev->SetRenderState( D3DRS_VERTEXBLEND, boneNum - 1 );
cont->MeshData.pMesh->DrawSubset( i );
}
g_pD3DDev->EndScene();
g_pD3DDev->Present( NULL, NULL, NULL, NULL );
}
}while(msg.message != WM_QUIT);
// スキンメッシュ情報削除
allocater.DestroyFrame( pRootFrame );
tex->Release();
g_pD3DDev->Release();
g_pD3D->Release();
return 0;
}
160行くらいに頑張って納めています(^-^;
setFrameIdという関数があります。この関数を通すと引数に渡しているID3DXSkinInfoインターフェイスからボーンIDの情報が引き出され、フレームに対応するボーンIDが刻印されます。フレームはツリー構造になっているので、どうしても再帰関数が必要になります。今回はsetFrameId関数の中にローカルstruct(create)を作り、そのstaticメソッドfにそのトラバースをしてもらっています。本当はC++にクロージャ(関数の中に関数を作れる仕組み)があればもっと素直に書けるのですが、C++にはそれが無いので、擬似的な関数内関数をこう実現しています。この関数ではついでにボーンオフセット行列(offsetMatrix)も格納しています。
OX::OXD3DXMESHCONTAINER構造体の中にはボーンコンビネーション配列(boneCombinationTable)を格納したのでした。この構造体にはサブメッシュ描画時に描画デバイスに渡す1メッシュに影響するボーン群のIDが格納されています。これが無いとスキンメッシュアニメーションは出来ないのです。例えばサブメッシュ17番に影響するボーンIDは5,13,15,16という感じです。これをすべてのサブメッシュ描画時に設定する必要があります。そのため、この構造体を取りだすgetMeshContainer関数を設けています。
これらの情報を得ておけば、後は描画ループに突入できます。描画ループではID3DXMesh::DrawSubset関数を呼び出してサブメッシュ毎に描画しています。1つのサブメッシュを描画するには、そのメッシュに影響する最大4本のボーンのワールド変換行列を描画デバイスに設定する必要があります。そのワールド変換行列は毎フレーム変化するので、計算し直さなければなりません。それを行っているのがupdateCombMatrix関数です。この関数の引数にはmap<DWORD, D3DXMATRIX>というマップを渡しています。この関数を通り抜けるとこのマップに各ボーンIDに対応したワールド変換行列が計算されてきます。ここを簡潔にするためにOXD3DXFRAME構造体にボーンオフセット行列を格納したんです(^-^)。
巷の本を見るとD3DXFRAMEにワールド変換行列をメンバとして追加している実装があったりします。それも悪く無いのですが、それをすると「インスタンス化」ではまります。スキンメッシュな同じモデルを複数描画する時、それぞれのモデルは独立したポーズを取ります。D3DXFRAMEは共有されてしまう物なので、そこに各モデル固有のワールド変換行列を入れてしまうと、フレームのコピーを都度作っていく事になってしまいます。共有されてしまう物の中には共有するメンバしか入れてはいかんのです。そのため、今回はID3DXAnimationControllerで姿勢を変更した後にupdateCombMatrix関数を通してボーンの姿勢を回収してしまう方法を取りました。
B コードの実行方法
上記のコードを実行するには次のようにします。
まず@の全コードをコピーしてoxallocatehierarchy.hとして保存します。次にAの全コードをmain.cppとしてこれも保存します。main.cppでは、今回「tiny.x」をテクスチャ付きで動かすように決め打ちしています。tiny.xのリソースはお使いのPCにインストールしたDirectX
SDKのフォルダ内にもありますが、面倒ならばこちらから全部まとめたアーカイブをDLしちゃって下さい(DXGSMP_No68_XFileSkinMeshAnimation.zip)。
VisualStudioで空のWin32アプリケーションを作成し、上記ファイルをプロジェクトファイル直下に置き、プロジェクトにコードを追加します。後はコンパイルして実行すれば動きます。リンカエラーが出た時にはDirectXのライブラリがリンクされていませんので、適宜パスを正しく設定してリンクが通るようにして下さい。
ビルドが通れば、下の絵のようなtinyが歩き出します!