ホーム < ゲームつくろー! < プログラマブルシェーダ編

その11 極薄情報のID3DXFragmentLinkerを紐解け!


※ ID3DXFragmentLinkerはDirectX9から撤廃されてしまいましたー!残念、無念!以下の記事内容は現在の(2011.8現在の)DirectX9ではすでに無効です。


 ID3DXFragmentLinkerインターフェイス、ご存知でしょうか?「さぁ・・・」と思う方がきっと殆どだと思います。ごたぶにもれず私もそうでした。それもそのはずでして、私が持っている日本語のシェーダ本には全く載っていません。DirectXの日本語マニュアルには存在はあるもののその使い方の説明はゼロ。英語マニュアルにかろうじて使い方が載っている程度です。Googleさんで検索すると世界にわずか433件(2007.12現在)しか情報がありません。特に日本人にとって、実に実にマイナーなインターフェイスになっている状態です。

 しかし!このインターフェイスはシェーダを扱う方に絶対に知ってもらいたい!シェーダプログラムに一つの革命が起こるインターフェイスなんです!端的に言えば「シェーダの実行時変更」が可能になります!!

 この章では素晴らしい魅力があるにもかかわらず情報が極薄であるID3DXFragmentLinkerインターフェイスを使った動的なシェーダの実行について日本一詳しく説明致します!



@ ID3DXFragmentLinkerは動的シェーダ生成機

 このインターフェイスが何をするのか?このインターフェイスはプログラマが作成したシェーダコードのかけらである「フラグメント」を繋いで(=リンクして)1つのシェーダコードを「実行時に」作ってくれます。「?」と今一ピンと来ない方は、次のような状況を考えてみて下さい。

 2つのモデルがあります。1つはボーンが入っているモデルです。もう1つは家などの頂点が動かない固定頂点モデルです。この2つのモデルを描画するようにシェーダ(エフェクト)を対応させるには、「ボーン操作をする頂点シェーダ」と「通常の頂点シェーダ」の両方を書く必要があります。必要なシェーダの数は2つという事です。さらに、ボーンが入っているモデルにもテクスチャが貼られたものとそうでないものもあるかもしれません。この場合、テクスチャを扱うピクセルシェーダを2種類用意しなければなりません。固定頂点モデルも同様なので、この時点で4つのシェーダが必要になります。さらにライトを使うものと使わないもの、影を落とすものとそうじゃないもの、深度テクスチャに書き込むべきものとそうでないもの、頂点変換をするものとしないものなど条件が増える度に必要なシェーダの数は倍々に増えていくわけです。条件が10個あったら、すべてに対応するのに必要なシェーダの数は1024個です!もちろんこの状態ですでにシェーダプログラムとしては破綻しておりますが、さらに「法線マップ処理も追加したいな・・・」となったら「うがーー!!」と死んでしまいます(^-^;。

 この例えは決して大げさではありません。シェーダで様々なモデルを描画させようとすると、こういう状態は間違いなくやってきます。これを打破する唯一にして最強の武器がID3DXFragmentLinkerなんです!

 例えば先の例に出てきたボーン処理と固定頂点変換は、途中の処理はかなり異なるものの、最終的には1つの頂点の位置を決定する「頂点変換作業」です。この場合、

 @ ボーン処理を担当するコード(=フラグメント)
 A 固定頂点変換をするコード

を用意し、頂点シェーダの頂点変換作業の部分に対してボーンを持っているモデルの場合は@を、そうでないモデルにはAを差し込みます。驚くべき事に、ID3DXFragmenLinkerは実行時に頂点シェーダの一部分を差し替える事が可能なんです!これはピクセルシェーダも同様でして、例えばテクスチャの有無に対して貼り付け部分を担当するフラグメントを用意して挿入できてしまいます。これがあれば、条件に対するフラグメントを書くだけで良く、先ほどの組み合わせ爆発は完全に解消されます!!これを知った時はさすがに私も大踊りしてしまいました。



A ID3DXFragmentLinkerの情報源

 この超魅力的なインターフェイスについて調べたいのですが、残念ながら情報が大変に希少です。日本語版マニュアル上で検索をかけると15件引っ掛かってきますが、その内12件はこのインターフェイスのメンバメソッドの説明、2件は情報無し、そして1件は「D3DXCreateFragmentLinker関数」というこのインターフェイスを作成する関数の記述でした。最後関数は後で使うのですが、結局日本語マニュアルにはこのインターフェイスについての直接的な使い方の説明は一切ありません。

 続いて英語版マニュアル(Aug2007)で検索をかけると、日本語版には無い項目がヒットしました。「Using Shaders in Direct3D 9」と「Writing HLSL Shaders (Direct3D 9)」にわりとしっかりした記述があります。そして、「FragmentLinker Sample」というサンプルもあります。

 より詳しい完全な資料をお求めの方は上記を参考にして下さい。以下からはこのインターフェイスについて試行錯誤し理解した部分について説明致します。



B フラグメント

 先の文章に何度か出てきた「フラグメント」。これは「破片、かけら」という意味があります。その名の通り、フラグメントはシェーダコードのかけらです。このフラグメントを組み合わせて繋げていくことで1つのシェーダ機能が実現されます。フラグメントについての直接的な答えは英語版マニュアルの「Writing Shader Fragments」にあります。以下に和訳付きで抜粋いたしましたのでご参照下さい:

フラグメントとは?

A fragment is a stand-alone shader function. A fragment can be compiled from a vertex shader or a pixel shader. By building encapsulated shader functions, they can be linked together to increase functionality.

You can share parameters from one fragment with another fragment using a semantic. Fragments can be compiled and linked at run time, or compiled offline and linked at run time. One way to use fragments is to generate overloaded shader functions so that each fragment can be compiled for a different compile target. This is a convenient way to use fragments to cover a range of shader models within an effect.

HLSL and assembly fragments can be linked with a fragment linker. The linker (ID3DXFragmentLinker) generates a symbol table that contains the constant table (ID3DXConstantTable). The linker can also optimize register use and remove dead code. Since the linker is part of D3DX, it can be updated independently of the runtime.

(和訳)
フラグメントは単独で動作するシェーダ関数です。フラグメントは頂点シェーダもしくはピクセルシェーダからコンパイルできます。カプセル化されたシェーダ関数をビルドすると、それらを一緒にリンクして(シェーダの)機能を増加できます。

セマンティクスを使うことでフラグメント同士でパラメータを共有できます。フラグメントは実行時にコンパイルおよびリンクができます、またオフラインでコンパイルして実行時にリンクする事もできます。フラグメントを使う一つの方法はオーバーロードされたシェーダ関数を生成する事です。これはフラグメントが異なるコンパイルターゲットでコンパイルされるためです。これは1つのエフェクトで広範囲のシェーダモデルをカバーする目的でフラグメントを使う便利な方法です。

HLSLとアセンブリフラグメントはフラグメントリンカでリンクする事ができます。リンカ(ID3DXFragmentLinker)は定数テーブル(ID3DXConstantTable)を含むシンボルテーブルを生成します。リンカはまたレジスタの使用を最適化しデッドコードを削除します。リンカはD3DXの一部分であるため、実行時に独立して更新できます。

 要点をまとめます。

・ フラグメントはシェーダ関数(シェーダ内で定義される関数)である。
・ セマンティクスを使う事でフラグメント間で共通の変数を使用できる。
・ リンクするとシェーダの機能を追加・変更できる。
・ リンク時に使っていないコードや変数は削除されて最適化される。
・ フラグメントはコンパイルする必要がある。

次にフラグメントの具体例を見てみましょう。



C フラグメントコードのルール

 フラグメントはテキストで書くシェーダコードですが、通常のHLSLとはちょっと異なる独特の書き方をします。詳しい情報は英語版マニュアルの「Writing Shader Fragments」にあります。

 例えば、頂点変換をするフラグメントコードを見てみましょう:

頂点変換フラグメントコード
float4x4 g_WorldViewProj;   // ワールドビュー射影変換行列

void VertexTransform(
  in float4 LocalPos : POSITION,     // 入力ローカル位置
   out float4 ScreenPos : POSITION    // 出力スクリーン位置
   out float4 vWVPPos : r_WVPPOS )   // フラグメント共通変数(位置の保持)
{
   ScreenPos = mul( LocalPos, g_WorldViewProj );
   vWVPPos = ScreenPos;
}

// フラグメント関数宣言
vertexfragment VertexTranslateFragment = compile vs_2_0 VertexTranslate();

 このソースでフラグメントのルールを説明します。まず、グローバル変数はHLSLと同じように宣言できます。上ではワールドビュー射影変換行列を設定していますね。続くVertexTransformというのはフラグメント関数で、もちろん任意の名前を付ける事ができます。

 大切なのはその引数、特にセマンティクスです。第1引数をご覧下さい。LocalPos変数には「in」という接頭子が付き、さらにPOSITIONセマンティクスがついています。このように宣言すると、この変数には頂点シェーダの入力値であるローカル位置が自動的に飛び込んできます。続いて第2引数を見ると、ここには「out」というマークとPOSITIONセマンティクスが付いています。もうお分かりだと思うのですが、こうするとこの変数に入れた変換後頂点位置が頂点シェーダのPOSITIONセマンティクスの出力となります。

 第3引数には見慣れない「r_WVPPOS」というセマンティクスが付いています。これは私が適当につけたセマンティクスです。実は頭に「r_」が付いたセマンティクスは「フラグメント間の共有変数」として認識されます。別のフラグメントでも同じ名前のセマンティクスを引数に使えば、そこに前のフラグメントで代入された値が入ってくるわけです。上の例で頂点変換した値をvWVPPosに代入しているのは、他のフラグメントで変換後位置を使う場合を考慮しているためです。このようなフラグメント共通変数は必須ではありませんが、これを使わないとフラグメントにする意味が半減してしまうでしょう。

 もう1つ、フラグメントシェーダの特徴であり必須なのが最下段にある「vertexfragment」というフラグメント関数の宣言です。これは通常の変数宣言と同じで、頂点シェーダフラグメントの入れ物となる変数を定義しています。右辺にはテクニックで見かける記述をそのまま書き代入しています。ID3DXFragmentLinkerはこうして宣言されたフラグメントだけを認識できます。逆に言えば、この記述がないと関数はフラグメントとしては認識されずに無視されます。一つルールとして、この宣言は必ずフラグメント関数の宣言より下に定義しなければなりません。C言語のようにプロトタイプ宣言は出来ません。

 ピクセルシェーダの例も挙げておきます。モデルにテクスチャを貼り付けるフラグメントを書いてみます:

テクスチャ貼り付けフラグメントコード
sampler2D BaseSampler;

void TexturePaste(
   in float4 InTexCoord0 : TEXCOORD0,
   out float4 color0 : COLOR0 )
{
   color0 = Tex2D( BaseSampler, InTexCoord0 );
}

pixelfragment TexturePasteFragment = compile ps_2_0 TexturePaste();

今度は引数にピクセルシェーダの入力値であるTEXCOORD0、出力値にはCOLOR0が渡されています。フラグメントの内部ではInTexCoord0を頼りにテクスチャから色をピックアップしてcolorに渡しています。ピクセルシェーダフラグメントもpixelfragmentというフラグメント変数を設けて外部から認識できるようにします。

 フラグメントを記述する上での注意点ですがテクニックは必須ではありません。テクニックがある場合はID3XEffectインターフェイスが使えますが、動的にシェーダを作るので結局ID3DXEffect::SetVertexShaderメソッドなどでシェーダを登録する事になります。テクニックが無い場合は純粋なシェーダファイルとして扱われる事になるのでIDirect3DDevice9::SetVertexShaderメソッドなどでそれを登録する事になります。



D ID3DXFragmentLinkerにフラグメントを登録する

 フラグメントの書き方は大体Cの通りです。ID3DXFragmentLinkerは沢山ある細切れ状態のフラグメントを並べて1つの頂点シェーダ・ピクセルシェーダを作成してくれます。それにはやはり色々とお作法があります。

 フラグメントは人の手で書いたテキストファイルです。プログラム上でそれを扱うにはコンパイルする必要があります。これは事前にコンパイル済みフラグメントを作る方法と、実行時にコンパイルする方法の2通りがあります。事前コンパイルはプログラマブルシェーダ基本編第10章「プリコンパイル済みhlslファイルを作ろう」で説明したfxc.exeを使います。詳しくはそちらをご覧下さい。エフェクトファイルではなくてシェーダファイルなので、コンパイル時にプロファイルにfx_2_0の代わりにvs_2_0やps_2_0などを指定する事に注意して下さい。

 コンパイルしたフラグメントファイルは、次にプログラムで扱える状態に整えられます。その作業を担ってくれるのがD3DXGatherFragmentFromFile関数です:

D3DXGatherFragmentsFromFile関数
HRESULT D3DXGatherFragmentsFromFile(
   LPCTSTR pSrcFile,
   CONST D3DXMACRO* pDefines,
   LPD3DXINCLUDE pInclude,
   DWORD Flags,
   LPD3DXBUFFER* ppShader,
   LPD3DXBUFFER * ppErrorMsgs
);

pSrcFileはフラグメントが収められているファイルを指定します。
pDefinesにはフラグメントファイル内で使われているマクロ(#define)の挙動を指定するのですが、めんどくさい事をしていなければNULLでかまいません。
pIncludeには#ncludeの処理を記述するID3DXIncludeインターフェイスを渡します。ここも#includeを使っていなければNULLが指定できます。
Flagsにはコンパイルフラグを指定します。0を指定すると普通にコンパイルしますが、pSrcFileにコンパイル済みフラグメントファイルを指定している場合はD3DXSHADER_SKIPVALIDATIONフラグを指定してコンパイル作業を飛ばします。これにより高速化されるわけです。詳しいフラグはマニュアルを参照してください。
ppShaderにはファイル内に定義されたフラグメントをコンパイルしたバッファが返ります。
ppErrorMasgsはコンパイルエラーの情報が文字列として返ります。うまく行かない時はここに返るエラー文字列を見る事でその原因を探れます。

 この関数で大切なのはpSrcFileとFlags、そしてppShaderだけです。典型的な設定例は、

D3DXGatherFragmentsFromFile関数の設定例
ID3DXBuffer *pCompiledShader, *pError;
if( FAILED( D3DXGatherFragmentsFromFile(
   "Test.fx", 0, 0, 0, &pCompiledShader, &pError )
{
   char *pErrorStr = (char*)pError->GetBufferPointer();
}

という感じでしょうか。関数が成功するとpCompiledShaderにファイルに含まれているフラグメントをコンパイルしたバッファが束になって返ります。ただ、その中身をプログラマが見る事は通常ありません。「何かコンパイルされたコードが配列のような状態であるんだな」と思って頂ければ良いかと思います。

 コンパイル済みバッファを得たら、次にID3DXFragmentLinkerインターフェイスを作ってそこにフラグメントをごそっと登録します:

ID3DXFragmentLinkerの生成とフラグメントの登録
ID3DXFragmentLinker* g_pFragmentLinker;
D3DXCreateFragmentLinker( pd3dDevice, 0, &g_pFragmentLinker );  // リンカ生成

g_pFragmentLinker->AddFragments( (DWORD*)g_pCompiledFragments->GetBufferPointer() );

 これはもうお作法でして不変的なコードです。D3DXCreateFragmentLinker関数でID3DXFragmentLinkerオブジェクトを作り、AddFragmentメソッドで先ほど得たフラグメントバッファをリンカに登録します。引数でDWORD*型に変換していますが、これは仕様です。四の五の言わずにこうして下さい。多分バッファの先頭には各コンパイル済みフラグメントへのオフセットか何かが並んでいるのだろうなと想像しますが、隠蔽されている部分なので気にする必要はありません。

 さて、これでID3DXFragmentLinker内にフラグメントが登録できました!



E ID3DXFragmentLinkerでシェーダを作成する

 登録したコンパイル済みフラグメントを扱うには各フラグメントに対応する「フラグメントハンドル」を使用しますフラグメントハンドルを取得するにはID3DXFragmentLinker::GetFragmentHandleByNameメソッドを使います:

フラグメントハンドルを取得
D3DXHANDLE fragmentHandle[2];
fragmentHandle[0] = (D3DXHANDLE)g_pFragmentLinker->GetFragmentHandleByName("VertexTranslateFragment");
fragmentHandle[1] = (D3DXHANDLE)g_pFragmentLinker->GetFragmentHandleByName("TexturePasteFragment");

 GetFragmentHandleByNameメソッドにはフラグメント名を指定します。この名前はフラグメントファイル内でvertexfragment及びpixelfragmentとして指定したものです。ここでフラグメントファイルとプログラムががっちり結びつくわけですね。後はそれをD3DXHANDLEにキャストして格納するだけです。

 上で配列として組み込んでいるのにはちゃんと意味があります。フラグメントはその順番が大切です。当然ですが、ピクセルシェーダフラグメントを実行してから頂点シェーダフラグメントを実行する事はできません。また頂点シェーダ内でもソースの実行順序があるわけです。そういう順番を確定するためにハンドルをその実効順に配列に格納する必要があります。

 フラグメントをリンカに登録して、必要なハンドルを取得すれば、それを組み合わせて1つのシェーダを作る事ができます。シェーダを作るにはID3DXFragmentLinker::LinkShaderメソッドを使います:

ID3DXFragmentLinker::LinkShaderメソッド
HRESULT LinkShader(
   LPCSTR pProfile,
   DWORD Flags,
   CONST D3DXHANDLE * rgFragmentHandles,
   UINT cFragments,
   LPD3DXBUFFER* ppBuffer,
   LPD3DXBUFFER * ppErrorMsgs
);

pProfileには「vs_2_0」「ps_3_0」などのシェーダバージョンを文字列で指定します。
Flagsにはリンクフラグを指定します。0で構いません。詳しくはマニュアルをご参照下さい。
rgFragmentHandlesが大切で、ここに先ほどのフラグメントハンドル配列のポインタを渡すわけです。
その要素数はcFragmentsに渡します。
この関数が成功すればppBufferに頂点シェーダやピクセルシェーダが返されます。これは後々でIDirect3DVertexShader9やIDirect3DPixelShader9にキャストして使用する事になります。
関数が失敗した場合はppErrorMsgsにエラー文字列が返ります。

 このメソッドがこのインターフェイス最大の特徴であり、極めて重要な部分です。配列に格納するフラグメントハンドルの順序や種類を変えると、ppBufferに返される頂点シェーダの意味合いが変わります。これはシェーダを動的に作り出している事になります。さらに良いことに、この繋ぎ変えの作業は高速です(英語版マニュアルに"This is a very lightweight operation."とあります)!

 こうして出来たシェーダインターフェイスを描画デバイスに登録すれば、続く描画処理でシェーダを通るようになります。



F グローバル変数を扱うID3DXConstantTableインターフェイス

 さてこれでフラグメントを駆使して大変柔軟なシェーダを実行時に作成できるのですが、シェーダを完全に機能させるためには「グローバル変数」を忘れてはいけません。ワールドビュー射影変換行列やテクスチャインターフェイスをシェーダに与える窓口です。これらを設定するためのID3DXConstantTableインターフェイスがちゃんと用意されています。このインターフェイスを取得するにはD3DXGetShaderConstantTable関数を使います:

変数テーブル取得
LPD3DXCONSTANTTABLE pConstantTable;
D3DXGetShaderConstantTable( (DWORD*)pVertexShader->GetBufferPointer(), &pConstantTable );

pVertexShaderは先のID3DXFragmentLinker::LinkShaderメソッドで作成したシェーダです。そのバッファを第1引数に指定すると、第2引数にグローバル変数のテーブルが返されます。

 ID3DXConstantTableインターフェイスが持つメソッドを見ると

・ SetFloat
・ SetInt
・ SetMatrix
etc...

と、どこかで見たようなメソッドが並んでいます。そうです、このメソッドはID3DXEffectインターフェイスが持っているそれとまったく一緒なんですね。これらの設定メソッドを通してシェーダ(正確にはレジスタ)に値を登録します。このインターフェイスはフラグメントの内部をチェックしてデッド変数(使っていない変数)を認識しないようにしてくれます。これは限りあるレジスタを温存する最適化です。



G シェーダを設定して実行だ!

 ここまででシェーダインターフェイス(IDirect3DVertexShader9もしくはIDirect3DPixelShader9)とグローバル変数テーブル(ID3DXConstantTable)が揃っています。後は描画デバイス(IDirect3DDevice9)にシェーダを設定して描画するだけです。これはお決まりでして、

シェーダ設定
pd3dDevice->SetVertexShader( pVertexShader );
pd3dDevice->SetPixelShader( pPixelShader );

とシェーダを描画デバイスに設定します。続いて先ほどのID3DXConstantTableを通してシェーダ変数の値を設定します。



H ○×オリジナルサンプル

 これで一通りの説明が終りですが、やっぱりサンプルが欲しい所です。DirectXには「FragmentLinker Sample」というサンプルが用意されていまして、それを見ればまぁわかるのですが、ご存知のようにDirectXのサンプルは木の葉を森で隠す書き方をしているために見通しが良くありません。そこで、○×オリジナルサンプルを作成しました。フラグメントから頂点シェーダを作成するシンプルなサンプルです。



 ID3DXFragmentLinkerをを知った時、私は目から火花が出るほど衝撃を受けました。それまでシェーダというのは「頂点シェーダとピクセルシェーダとテクニックを書くものだ!」と思っていたわけですが、それは静的なシェーダに過ぎなかったわけです。実は動的なシェーダを組む方法があった!もうバチバチです(笑)。
 それにしても、今回は本当に情報が少なかった(T_T)。日本語でまともな情報は無く、英語のマニュアルにある情報を読み、サンプルを見てやっと今回の章にたどり着きました。この希少情報が皆さんのシェーダ製作の助けになれば幸いです。