理由あってDirectX9とかAssimpとか弄ってるのでメモ.
Assimp
- Assimp
- Open Asset Import Library.COLLADAとかいろいろな3Dデータのファイル形式を読み込むためのライブラリ.
- 現在最新は3.0系.
- 2.0系には,assimp-netというP/Invokeベースのラッパーもある.
- Visual Studio用のソリューションファイルが入ってるのでビルドは簡単.
- デフォルトだとHAS_ITERATOR_DEBUGGING=0というマクロがプロパティシートで定義されているので注意(デバッグ時は少なくとも必要ないマクロ).
Assimpはシーングラフ(aiScene)を返すので,これを環境に応じたデータ構造にマップする必要がある.
DirectX9ならID3DXMeshにマップするのがまぁフツーの方法なんだろうけど,D3DXのメッシュって.Xを読み込むために使うものみたいで,あんまり使いよくない気がする.
んで,ID3DXMesh::GenerateAdjacencyというメソッドにハマったのでメモ.
D3DXメッシュの属性バッファと属性テーブル
ID3DXMeshは面ごとにDWORDを持たせることができてID3DXMesh::DrawSubsetメソッドで属性別に描画することができる.
属性はバーテックスバッファ,インデックスバッファとは別の属性バッファというDWORDの配列に書き込むことで設定できる.
これは面のグループ化に使うものだけど,属性は要するに面のマテリアルに対応している.
ややこしいのは属性バッファの他に属性テーブルというのがある.
面を描画する際は,デバイスにその面(の属性)に対応したマテリアルとかシェーダの設定をしてから,面を描画する操作をする. 同じマテリアルの面があるならまとめて描画したほうが早いわけだけど,インデックスバッファに記述されている面の順序が属性でソートされているとは限らない.
この面のソートをやってくれるのがID3DXMesh::Optimizeメソッド(※)で,属性テーブルは要するに面を属性でソートした結果,同じ属性を共有する範囲がどこからどこまでか,というのを記録しておくためのモノのようだ.
※ おそらくD3DXMESHOPT_ATTRSORTを使った場合のみ
属性テーブルはID3DXMesh::Optimizeで作るか,手動で設定することもできる.ただ属性テーブルは,無くてもID3DXMesh::DrawSubsetで描画できるので,おそらく属性テーブルが設定されているか否かで挙動を変えているのだと思う.
手順としては以下のような感じになる.
// 頂点データ struct VertexData; // 頂点宣言(のもと) D3DVERTEXELEMENT9 vertdecl[] = { /*...*/ }; // バーテックスバッファにコピーする内容 VertexData vertices[NUM_VERTICES] = { /*...*/ }; // インデックスバッファにコピーする内容 short indices[NUM_INDICES] = { /*...*/ }; // 属性バッファにコピーする内容 DWORD attributes[NUM_INDICES/3] = { /*...*/ }; // デバイス LPDIRECT3DDEVICE9 device; // メッシュ LPD3DXMESH mesh; void Load() { // メッシュの生成(エラー処理は省略) // 面の数 頂点数 フラグ 頂点宣言 デバイス メッシュ D3DXCreateMesh(NUM_INDICES/3, NUM_VERTS, D3DXMESH_MANAGED, vertdecl, device, &mesh); // バーテックスバッファに値を設定 VertexData* vb; mesh->LockVertexBuffer(0, (LPVOID)&vb); memcpy(vb, vertices, sizeof(vertices)); mesh->UnlockVertexBuffer(); // インデックスバッファに値を設定 short *ib; mesh->LockIndexBuffer(0, (LPVOID)&ib); memcpy(ib, indices, sizeof(indices)); mesh->UnlockIndexBuffer(); // 属性バッファに値を設定 DWORD *ab; mesh->LockAttributeBuffer(0, &ab); memcpy(ab, attributes, sizeof(attributes)); mesh->UnlockAttributeBuffer(); // Optimizeファミリを呼び出す準備.隣接リストの生成. vector<DWORD> adjList; adjList.resize(3 * mesh->GetNumFaces()); mesh->GenerateAdjacency(EPSION, &adjList[0]); // EPSIONは適当な値(1.0f/512とか) // Optimizeの一種 mesh->OptimizeInPlace(D3DXMESHOPT_ATTRSORT, &adjList[0], NULL, NULL, NULL); }// void Load() void Render() { device->Clear(/*...*/); device->BeginScene(); DWORD num_subset; device->GetAttributeTable(NULL, &num_subset); for(DWORD i = 0; i < num_subset; ++i) { // 属性 i に対応するレンダリングステート,マテリアル,エフェクトの設定をする // ... mesh->DrawSubset(i); }// for device->EndScene(); device->Present(NULL, NULL, NULL, NULL); }// void Render()
ややこしいことになったのはID3DXMesh::GenerateAdjacencyの部分で,頂点宣言でD3DDECLUSAGE_POSITIONに指定したものはD3DDECLTYPE_FLOAT3にしておかないとこの関数は失敗する.デバッグランタイムでログ出して初めて分かりました.
位置ベクトルなら別にfloat4でもいい気がするんだけどなぁ.
これでようやくAssimpで読み込んだデータを表示できそう.
Assimpで読み込んだモデルはaiNodeってデータ構造で,こいつが複数のaiMeshっていうのを持っているんだけど,これはただ一つのマテリアルを参照する.要するにaiMeshがD3DXメッシュのサブセットに対応するというわけだ.
全aiMeshの頂点と面を,D3DXメッシュのバーテックスバッファとインデックスバッファに統合して,aiMeshごとに属性を振ってあげればいいだろう.マテリアルをちゃんとマップするのはめんどくさそうだけどまぁ何とかなるだろ.
参考