DirectX 9の描画命令 (3)

前回はDirectX Graphicsの描画命令がDraw*Primitiveに集約されていることを説明した.ゲーム紹介記事に並ぶ一昔のハイエンドCGのようなスクリーンショットにしても,基本的にはたった1種類の命令の呼び出しで実現できるのである.これは考えてみればにわかには信じがたい事実である.3Dレンダリングの多様性をどのようにして1種類の命令に集約させているのだろうか? まずはDraw*Primitiveの引数に着目してみよう.

HRESULT DrawPrimitive(
    D3DPRIMITIVETYPE PrimitiveType,
    UINT StartVertex,
    UINT PrimitiveCount
);
HRESULT DrawIndexedPrimitive(
    D3DPRIMITIVETYPE Type,
    INT BaseVertexIndex,
    UINT MinIndex,
    UINT NumVertices,
    UINT StartIndex,
    UINT PrimitiveCount
);
HRESULT DrawPrimitiveUP( 
    D3DPRIMITIVETYPE PrimitiveType,
    UINT PrimitiveCount,
    const void *pVertexStreamZeroData,
    UINT VertexStreamZeroStride
);
HRESULT DrawIndexedPrimitiveUP(
    D3DPRIMITIVETYPE PrimitiveType,
    UINT MinVertexIndex,
    UINT NumVertexIndices,
    UINT PrimitiveCount,
    const void *pIndexData,
    D3DFORMAT IndexDataFormat,
    CONST void* pVertexStreamZeroData,
    UINT VertexStreamZeroStride
);

以上,4つの描画命令についての定義である.どれも決して引数が多すぎると言うほどのものではない.DrawPrimitiveにいたっては引数は3つである.たったこれだけの引数で描画に必要なパラメータ全てが伝えられるはずがない*1.つまり隠れたパラメータが存在する.
DirectX Graphicsでは,デバイス*2のプロパティとして様々な描画パラメータを保持している.描画に使用するテクスチャ,Zバッファの扱い,ワイヤーフレームで描画するか,など様々なパラメータが存在する.Draw*Primitiveは呼ばれる直前のステートを描画に利用し,これによって引数の数はあの程度で済んでいるのである.なお,Draw*Primitiveを呼び出した後に設定を変更しても,以前のDraw*Primitiveには影響は与えない.これらのデバイスプロパティは,描画ステートと呼ばれている.参考までに以前BBXに行った書き込みから引用しよう.

Direct3D 以降,描画命令は基本的にプリミティブ描画に一本化されました. しかし,様々な描画方法を実現するためのパラメータを 『DrawほにゃららPrimitive』の引数に全て突っ込むわけにはいきません. これは拡張性の問題もありますし,パフォーマンス絡みの理由など様々な要因があるかと思います. とにかく結果として Direct3D 及びその直系子孫である DirectX Graphics は巨大なステートを持ったシステムになりました. 言い換えれば多数のグローバル変数を使用し『DrawほにゃららPrimitive』を呼んだ時点でのグローバル変数の値に応じて動作を変えるという,非常にハードウェアよりの設計思想をしています.
http://bbx.hp.infoseek.co.jp/cgi-bin/bbx.cgi?log=38&vew=73

正確にはグローバル変数と言い切るのはやや語弊があるかもしれない.現実的には1つのアプリケーションは1つのデバイスしか使わないため,シングルトンオブジェクトが非常に多数のプロパティを持っているという表現の方が正確だろうか.これがいかに多いかは,エフェクトファイル(これはまだ説明していないが)のステートに関するヘルプをご覧になって頂くと実感しやすいかと思う.
パフォーマンスという観点では,ステート変更数は描画コマンド発行数と同じオーダーで影響を与える恐れがある.これはどういう事かというと,DirectXのドライバは『ステート変更』及び『Draw*Primitive』の呼び出しをコマンドとしてキューに格納していき,CPUとは独立(並列)に処理を行うという仕組みをとっていることに原因がある.ポリゴンスループットやテクスチャ転送速度はGPUの性能で決まるが,コマンド発行数の限界を決めるのは基本的にCPUである.ここのところずっとGPUの方が進化速度が速いため,CPU律速は珍しい現象ではなくなっている*3.みんな我慢して速くて熱いCPUを買わなきゃダメっ,ということである.非同期描画に関しては奥が深い.暇つぶしに読むのに面白そうなスレッドを紹介しておこう.
http://bbx.hp.infoseek.co.jp/cgi-bin/bbx.cgi?log=38&vew=73
また多数のステートをいかに管理するかという問題について,ステートブロックとエフェクト(ID3DXEffect)という仕組みがある.前者はCPUがレジスタの内容を一発で退避するための仕組みに似ていて,ドライバレベルでサポートされた機能である.後者はD3DX由来のソフトウェアライブラリで,テキストファイルにステートセットを記述し,ステートセットをオブジェクトのように扱うという仕組みである.
プログラムとデータの分離というテーマで見たとき,エフェクトはなかなか興味深いステップである.先ほどのBBXの投稿より再度引用.

昔のゲームプログラミング(N88 BASIC)などでは画像データもプログラム内部に直接記述したものですが,今や画像データは専用フォーマットを使って外部に置くことに何らためらいがありません. 外部ファイル化という観点で見れば,エフェクトファイルをこのようなデータとプログラムの分離と見ることも可能です. そもそも今でも頂点データはXファイルなどで外部に持つことが多いので,レンダーステートを外部に切り出すというのもそれほど突拍子もない発想ではありません. で,このエフェクトファイルですが,頂点シェーダやピクセルシェーダを定義したり,セットしたりすることも可能です. DirectX9 ではエフェクトファイル内でのシェーダ記述用に HLSL という専用言語まで導入され,ますます有用になっています. またエフェクトファイルの解釈は実行時に行うことが可能です. うまくやれば描画設定やシェーダコードを変更するのにリコンパイルが不要になりますし,実行時に描画設定を試行錯誤する,といったことも可能になるでしょう.
http://bbx.hp.infoseek.co.jp/cgi-bin/bbx.cgi?log=38&vew=73

最後にエフェクトファイルを扱うことが出来るツールとして,ビデオチップ二大巨頭であるATIとnVidiaそれぞれのツールを紹介しておこう.

また,エフェクトファイルを利用したプログラミングは,川西さんがMSDNに書かれているC# HLSL Viewerシリーズが入門用にちょうど良いかと思われる.
http://www.microsoft.com/japan/msdn/directx/japan/dx9/hlsl3.asp

*1:まあ恐ろしく巨大なパラメータ構造体のアドレスを渡しているとかそういう可能性もあるが,結果的にそうはなっていない.

*2:IDirect3DDevice9を経由して操作する

*3:ATIとnVidia合作資料の"What Is a Batch?"以降を参照