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

その9 HLSL内の#includeをID3DXIncludeで展開する



 シェーダプログラムではお決まりのコードが沢山あります。例えば座標変換やライトからの反射量計算、点のグレー化など、細かく沢山あります。さらに今後DirectX10以降でジオメトリシェーダなどでも新しい技術がどんどんお目見えしてくるはずです。それらを毎回書いてももちろん構いませんが、「過去の資産を有効に再活用する」という目線に立つなら、やはりシェーダプログラムもサブルーチン化するべきだと思います。

 別のファイルに分離したサブルーチンをその場所にプログラム展開するのが「#includeディレクティブ」です。C言語で誰もが書く一文ですね。C言語ライクなHLSLも#includeディレクティブをサポートしています。ただ、実はちょっと癖がありまして、リソース化されたHLSLでは「展開ができない」という避けられない問題が発生します。

 そこでこの章ではプログラマブルシェーダ内で宣言された#includeディレクティブを展開する方法について試行錯誤してみます。



@ ファイルベースなら全く問題なし

 #includeディレクティブはコード内に記述する事はできません。大抵はシェーダプログラムの最初に記述します。働きは単にそこに指定ファイル内のソースを展開するだけです。

 HLSLコードをファイルに分割している時、D3DXCreateEffectFromFile関数でファイルからエフェクトを作成する際には#include展開が自動的に行われます。実はファイルベースでエフェクトを提供する分には#includeディレクティブの取り扱いに難しい問題は何も発生しないんです。ですからファイルベースHLSLでゲームを作成されている方は、ここでお話はおしまいです(^-^;。



A リソースベースは大問題!

 一方、前章「HLSLシェーダプログラムのリソース化」でお話したシェーダプログラムをリソースにおいて隠蔽する形式でゲームを作成している方は、#includeディレクティブの問題に直面してしまいます。

 実は、D3DXCreateEffectFromResource関数は#include展開をしてくれません。それは当然でして、プログラマが勝手に名前(ID)を付けたリソースをDirectXが自動的に判断する術が無いためです。ただ、プログラマがそれを判断して展開ソースを知らせる手順は提供されています。その仕事を担うID3DXIncludeインターフェイスについて、以下で詳しく説明します。



B ID3DXIncludeインターフェイス

 ID3DXIncludeインターフェイスは#includeディレクティブが読み込まれた時の動作を規定するインターフェイスです。持っているメソッドは2つ(Openメソッド、Closeメソッド)なんですが、DirectX側で実装されていません(当然です)。よって、プログラマがこれらメソッドを独自に実装する必要があります。

 インターフェイスとメソッドの役目だけ先に説明しておきます。この実装済みインターフェイスはD3DXCreateEffectFromResource関数の第5引数(pInclude)に渡されます。もしリソースコード内に#includeがあった時には、渡したインターフェイスのOpenメソッドが呼び出されます(コールバックされます)。またD3DXCreateEffectFromResource関数を抜け出す時にはCloseメソッドが呼ばれます。Openメソッドの役目は#includeディレクティブに指定されているファイル名からそのソースコードを教える事、Closeメソッドの役目はインターフェイスの後処理です。

 ではインターフェイスの宣言を見てみましょう:

ID3DXInclude宣言部
DECLARE_INTERFACE(ID3DXInclude)
{
   STDMETHOD(Open)(THIS_
      D3DXINCLUDE_TYPE IncludeType,
      LPCSTR pFileName,
      LPCVOID pParentData,
      LPCVOID *ppData,
      UINT *pBytes) PURE;

   STDMETHOD(Close)
      (THIS_ LPCVOID pData) PURE;
};

Openメソッドの引数は5つです。

IncludeType[in]にはD3DXINCLUDE_TYPE列挙型の値が入力されてきます。これは#includeファイルの所在を表しますが、変な事をしない限りは99%D3DXINC_LOCAL(#includeファイルがローカルパスにある)が入ってきます。ファイルを展開する場合のパスの検索に使うのかなと思います。
pFileName[in]には#includeが指定するファイルの名前がそのまま入ってきます。
pParentData[in]には#includeファイルを含むコンテナへのポインタが入ってくる・・・とマニュアルにありますが、多分D3DXCreateEffectFromResource関数を使う分には考慮しなくても良いのかなと想像しています。ここはわかり次第更新します。
ppData[out]には#includeファイルの展開コード文字列へのポインタを返します。これが極めて大切です。
pByte[out]は文字列のサイズを知らせます。

Closeメソッドの引数は1つです。

pData[in]にはOpenメソッドの第4引数に渡したポインタが入ってきます。Closeメソッドが呼ばれる時にはそのポインタ先のコードをもう展開してコンパイルもしましたよという意味でもあるので、文字列を削除するなどの処理をここで行います。


 以上からID3DXIncludeインターフェイスを実装する際には「pFileNameに入ってきたファイル名に該当するHLSLコードをリソースから引っ張り出し、そのサイズを知るにはどうするか?」という点に注目する必要がありそうです。



C Openメソッドで見るリソースの扱い方

 では、実際にOpenメソッドを実装してみましょう。このメソッド内でやる事は、引数で指定されたインクルードファイルに該当するHLSLコードをリソース内から引っ張り出して、その文字列ポインタとサイズを返す事です。簡単そうで微妙にメンドクサイ部分です。

 リソースはIDとして管理されています。しかし、Openメソッドの引数に入ってくるのは「ファイル名」です。リソースにアクセスするにはIDでなければならないことから、ファイル名→IDという変換が必要になります。ここではそれをグローバル関数で行う事にしました。

 作成するハッシュ関数はConvertEffectFileNameToID関数です。この関数の実装部分をまずはどうぞ:

ConvertEffectFileNameToID関数
ファイル名リソースID構造体
struct FILENAMERESOURCEID
{
   LPCSTR FileName;
   DWORD ID;
};

// ハッシュ検索用グローバル配列設定
FILENAMERESOURCEID g_pEffectNameIDHash[] = {
   { FXID_ZVALUEPLOT_FILENAME     , FXID_ZVALUEPLOT     },  // Z値プロットエフェクト
   { FXID_DEPTHBUFSHADOW_FILENAME , FXID_DEPTHBUFSHADOW },  // 深度バッファシャドウエフェクト
   { FXID_VERTEXBLEND_FILENAME    , FXID_VERTEXBLEND    },  // 頂点ブレンドエフェクト
};

/////////////////////////////////////////////////
// エフェクトファイル名からリソースIDを取得
/////////////////////////////////////////
bool IKD::ConvertEffectFileNameToID( LPCSTR pFileName, DWORD *pID )
{
   // ファイル名数を算出
   size_t sz = sizeof(g_pEffectNameIDHash) / sizeof( FILENAMERESOURCEID );

   // 引数のファイル名と比較
   size_t i;
   for(i=0; i<sz; i++)
   {
      if(strcmp(g_pEffectNameIDHash[i].FileName, pFileName)==0){
         // IDを返す
         *pID = g_pEffectNameIDHash[i].ID;
         return true;
      }
   }

   return false;
}

FILENAMERESOURCEID構造体はその名の通りエフェクトファイル名とリソースIDをついにして保持します。g_pEffectNameIDHashハッシュテーブルはその構造体を静的な配列形式で保持します。上の実装は全てマクロ名で隠蔽していますが、もちろん直接ファイル名とIDを書き込んでも構いません。このグローバル配列は新しいエフェクトを追加する度に変更する事になります。ConvertEffectFileNameToID関数は入力されたファイル名(pFileName)とハッシュテーブルを比較して、該当するリソースIDがあればそれをpIDの先に返します。

 この関数を作っておけば、Openメソッドの実装は比較的簡単になります。関数からリソースIDが取得できた後の流れは次のようになります。

@ IDからリソースハンドルを取得
A リソースハンドルを使ってリソースのサイズを取得
B リソースハンドルからリソースをロードし、グローバルハンドルを取得
C グローバルハンドルを使ってリソースメモリをロック

これはソースを見た方が早いと思いますので、Openメソッドの実装部を公開します:

CEffectResourceInclude::Openメソッド
HRESULT CEffectResourceInclude::Open(
   D3DXINCLUDE_TYPE IncludeType, LPCSTR pFileName, LPCVOID pParentData, LPCVOID *ppData, UINT *pBytes)
{
   // 引数のファイル名からリソースIDを検索する
   DWORD ID;
   if(!ConvertEffectFileNameToID( pFileName, &ID ))
      return D3DXERR_INVALIDDATA; // 無効

   // IDに該当するHLSLコードリソースを検索
   HRSRC hRsrc = FindResource(NULL, MAKEINTRESOURCE(ID), RT_RCDATA);
   if( hRsrc == NULL )
      return D3DXERR_INVALIDDATA; // 無効

   // リソースをロード
   HGLOBAL hGlobalRsrc = LoadResource( NULL, hRsrc );
      if( hGlobalRsrc == NULL )
         return D3DXERR_INVALIDDATA; // 無効
   DWORD sz = SizeofResource( NULL, hRsrc ); // リソースサイズを取得

   // リソースをロックして文字列を抽出
   *ppData = LockResource( hGlobalRsrc );
   *pBytes = (UINT)sz;

   return S_OK;
}


上から行きましょう。FindResource APIは第2引数のIDと第3引数のリソース型の情報を元にしてリソースを検索し、存在すればそのリソースハンドルを返してくれます。無い場合はNULLが返ります。第1引数はモジュールハンドルを渡すのですが、別のモジュールでない事がきっと殆どでしょうから、ここはNULLで構いません。

 リソースハンドルは次のリソースロードで使用します。LoadResource APIはりソースハンドルを元にリソースを操作できるグローバルハンドルを返してくれます。このハンドルを通せばソースコードを文字列として得る事ができます。SizeofResourceはリソースのサイズを返してくれます。

 グローバルハンドルを通してソースコードの文字列へのポインタを得るにはLockResource APIを用います。この関数が返すポインタの先をOpenメソッドに渡されたppDataの指す先に渡せば、ソースコードの引渡しは完了です。同時にpBytesにそのサイズを渡します。後は忘れずにS_OKを返せば、#includeの展開が行われます!

 リソースハンドルやロックしたリソースは昔は閉じなければならなかったのですが、今は閉じる必要が無くなったようです(マニュアルにも「もう古い」と書かれています)。よって、Closeメソッドはどうやら空実装で良さそうです。



D D3DXCreateEffectFromResource関数の新しい書き方

 上のような独自のID3DXIncludeインターフェイスを実装してしまえば、後はD3DXCreateEffectFromResource関数にそのインターフェイスを渡せば良いだけになります。実際の実装例を見てみましょう:

ID3DXInclude派生クラスを用いたD3DXCreateEffectFromResourceの実装例
CEffectResourceInclude Inc;   // ID3DXIncludeの実装派生クラス

if(FAILED( D3DXCreateEffectFromResource(
   cpDev.GetPtr(),
   NULL,
   MAKEINTRESOURCE(FXID_DEPTHBUFSHADOW),
   NULL,
   &Inc,
   0,
   NULL,
   m_cpEffect.ToCreator(),
   NULL ))
   )
   return false;


この書き方は、今の段階で鉄板です。



 一度この実装をしてしまえば、後はハッシュテーブルの書き換えや入れ替えだけでシェーダプログラム内の#includeディレクティブの動作を正しく行えるようになります。もちろんクラスを書き換える必要はありません(コンパイルは必要です)。

 この記事を作成するに当たりID3DXIncludeインターフェイスについて色々と調べたのですが、マニュアルには全く情報が無いんですよね。またそのためなのか世の中にもビックリするほど情報が落ちていませんでした。しかし何とか上の形で#includeを処理できるようになってめでたしめでたしです。この記事がこのインターフェイスを必要とする皆様の参考になりますと幸いです。