ID3DXMesh Internal

BBXの方ではなるべくネタばれなしで書いてみたけど,一応こっちで解説.DirectX 9.0 SDK Update (Summer 2003)での実装について書いているのでその辺はご注意を.
ID3DXMeshについて包括的に書かれた文章としてDirectXのヘルプに"Mesh Support in D3DX"という章があります.

Meshes

D3DX implements the mesh construct to load, manipulate, and render .x file contents. A mesh is basically a collection of vertices defining some geometry, and a set of indices defining the faces. There are several mesh types.

ID3DXBaseMesh provides the fundamentals. ID3DXMesh and ID3DXPMesh inherit from ID3DXBaseMesh, and add, respectively, mesh optimization using per-chip vertex cache and progressive meshes modeled after Hugues Hoppe's progressive mesh work. ID3DXSPMesh provides simplification meshes and ID3DXSkinInfo provides skinned mesh support.

ID3DXBaseMesh provides methods to manipulate and query ID3DXMesh and D3DXPMesh progressive-mesh objects, which inherit from the base mesh. This includes adjacency operations, geometry buffer retrieval, lock/unlock operations (vertex and index), and copying, rendering, face, and vertex information.

このうち単純なメッシュであるID3DXMeshについて書こうと思います.ID3DXMeshは基本的にプログラマが代替可能なラッパークラスです.ただID3DXMeshはそれなりに強力な最適化機能を持っていて,これの独自実装を持たない場合は十分に利用しがいのあるクラスといえるでしょう.

Mesh Object Data

A mesh contains a vertex buffer, an index buffer, and an attribute buffer.

  • The vertex buffer contains the vertex data, which are the mesh vertices.
  • The index buffer contains vertex indices for accessing the vertex buffer. This can reduce the vertex buffer size by reducing duplicate vertices. Only an indexed mesh uses the index buffer. If a mesh is made up of a triangle list, for example, it does not use the index buffer.
  • The attribute buffer contains attribute data. Attributes are properties of the mesh vertices, in no particular order. A D3DX mesh stores attributes in a group of DWORDS, for each face.

ID3DXMeshが内部に以下の3つのオブジェクトを保持するコンテナであるという視点は,ID3DXMeshを理解する上で重要です.

  • Vertex buffer
  • Index buffer
  • Attribute table

これらはそれぞれ専用メソッドで取得することができます.1ステップごとに内部データを注意深く観察することでID3DXMeshの内部動作のかなりの部分を推測することが出来るでしょう.
また,全てのメッシュがこれら3つのデータを持つわけではありません.頂点バッファは必ず存在します.インデックスバッファはインデックスドメッシュのみが保持します.

Attibute Tables

An attribute table is a concise representation of the contents of an attribute buffer. Attribute tables can be created by calling one of the Optimize methods with D3DXMESHOPT_ATTRSORT, by locking the attribute buffer and filling it with data, or by calling SetAttributeTable. A mesh contains an attribute table when the mesh is reordered into groups. This happens when Optimize is called, assuming an attribute sorting option (D3DXMESHOPT_ATTRSORT or higher) is supplied. D3DX Meshes use indexed triangle lists, and are therefore drawn with IDirect3DDevice9::DrawIndexedPrimitive.

Attribute tables are created as a result of calling Optimize. The faces don't have to be adjacent, because Optimize will reorder them to be adjacent. For example, the hands of a human mesh could use the same attribute. The attribute identifier (ID) helps sort the faces into groups. Meshes from .x files have automatically generated attributes for material and texture properties. You do need to call Optimize(ATTRSORT) or the more effective Optimize(VERTEXCACHE) to get good performance. The load functions try to present the data in the exact form it was saved out in. If you are using a vertex buffer/index buffer based mesh, the mesh application programming interface (API) provides optimization functions and skinning transformations with little overhead.

The optimization types are cumulative, starting from the least optimal (D3DXMESHOPT_COMPACT) to the most optimal (D3DXMESHOPT_IGNOREVERTS). D3DXMESHOPT_STRIPREORDER does a compaction and attribute sort. D3DXMESHOPT_VERTEXCACHE is always recommended, even on devices without a true vertex cache.

属性テーブルは D3DXMESHOPT_ATTRSORT オプションを指定して ID3DXMesh::Optimize を呼び出すか,ID3DXMesh::SetAttributeTable で直接設定することで存在するようになります.
Attribute IDというのは描画するポリゴンデータの集まりを区別するための0から始まる整数のことです.DirectXではステート変更回数を少なくしたいという要請からしばしば巨大なインデックスバッファと頂点バッファに小さなデータをパックします.このように論理的なデータサブセットを識別子で管理するマネージャクラスを構築された経験のある方も多いかもしれません.属性テーブルはこのAttribute IDについて事前計算した情報まとめたもので,具体的には以下に示すD3DXATTRIBUTERANGE構造体の配列という形をとります.

typedef struct _D3DXATTRIBUTERANGE {
    DWORD AttribId;
    DWORD FaceStart;
    DWORD FaceCount;
    DWORD VertexStart;
    DWORD VertexCount;
} D3DXATTRIBUTERANGE;

インデックスバッファ使用時,ID3DXMesh::DrawSubsetは内部的にIDirect3DDevice9::DrawIndexedPrimitiveを呼び出しています.このときの引数について調べてみましょう.まずIDirect3DDevice9::DrawIndexedPrimitiveの引数の意味についてはヘルプおよび拙作の以下のイラストを参考にしてください.

ID3DXMesh::DrawSubsetではTypeはD3DPT_TRIANGLELISTに固定となっています.一般的に最適化上最も有利とされるD3DPT_TRIANGLESTRIPを用いたストリップ化による最適化*1には対応できないことに注意してください.また,点や線分の描画にも対応出来ないことが分かります.
ID3DXMeshは常にBaseVertexIndexに0を設定します.このことからID3DXMeshの弱点に気づいた方はかなりのDirectXマニアでしょう.現状の3Dデバイスのほとんどは16bitインデックスに最適化されています.16bitインデックスでは各インデックスは頂点バッファの先頭から65535個以内の頂点までしかポイントすることができません.ただし歴史上何度もそうであったようにオフセット演算がサポートされていて,BaseVertexIndexに指定された分だけ頂点バッファの先頭からアドレス0をずらすことが可能です.これによって65536個以内の頂点列であれば巨大な頂点バッファにいくらでも*2詰め込むことが可能となっています.このBaseVertexIndexが0に固定と言うことはID3DXMeshで扱える頂点バッファのサイズがインデックスビット数で表現できる限界値に制限されてしまうということを意味しています.実際D3DXLoadMeshFromXで実験してみました.頂点数が65536個以上のXファイルを読み込ませた場合,ポリゴン数に依らずインデックスバッファは必ず32bitとなるようです.これはパフォーマンス上できれば避けたい事態です.興味深いことに一度32bitインデックスで作成されてしまうと,例えD3DXMESHOPT_COMPACTフラグを使用してOptimizeメソッドを呼び出し不要な頂点を削除し頂点数が65535以下となったとしても,やはりインデックスは32bitのままなのです.この仕様は最適化の余地を残していると言わざるを得ないでしょう.
MinIndexとNumVerticesに渡される値は属性テーブルが存在するかどうかで変化します.属性テーブル不在時には

  • MinIndex = 0
  • NumVertices = ID3DXMesh::GetNumVertices()

となります.属性テーブルが存在する場合は,

  • MinIndex = attribute.VertexStart
  • NumVertices = attribute.VertexCount

となります.一般的にVertexCountが少ないほどドライバ内での余計なデータ転送や頂点変換や同期待ちを回避できると考えられます.
StartIndexとPrimitiveCountは属性テーブル存在時には

  • StartIndex = attribute.FaceStart*3
  • PrimitiveCount = attribute.FaceCount

となります.属性テーブル不在時にはID3DXMeshの内部パラメータを直接使用しているようでした.
これらのデータはD3DSpyでDrawIndexedPrimitiveの引数を調べることで得たものです.
http://www.microsoft.com/japan/msdn/library/?url=/japan/msdn/library/ja/DirectX9_c/directx/graphics/programmingguide/tutorialsandsamplesandtoolsandtips/tools/d3dspy.asp?frame=true
以下に調査時のスクリーンショットを載せておきます.D3DSpyに興味を持った方は是非チャレンジしてみて下さい.

*1:追記:頂点キャッシュの効き具合によっては,D3DPT_TRIANGLELISTとD3DPT_TRIANGLESTRIPの間に速度差はほとんど無いこともあります.興味がある方はOptimizedMeshサンプルあたりで実験してみてください.インデックスバッファのサイズはD3DPT_TRIANGLELISTだと最適化しようがありませんが,D3DPT_TRIANGLESTRIPだとアルゴリズム次第で多くも少なくもなります.

*2:といっても限界はありますが