ホーム < ゲームつくろー! < プログラマブルシェーダ編 < シェーダプログラムの流れを知ろう


その2 シェーダプログラムの流れを知ろう


 その1で頂点シェーダとピクセルシェーダがストリームの過程のどこで使われるかを見てきました。レンダリングストリームはIDirect3DDevice9::DrawPrimitive関数系の描画関数を呼び出した瞬間にスタートします。そして、関数から戻った時にはもう描画は終わっています。ということは、描画の前に自作のシェーダプログラムをGPUに教える必要があるわけです。では、具体的にどうやってシェーダプログラムをGPUに教え使ってもらうのか?この流れはシェーダプログラムを理解する上でとても大切なのです。ここでは、シェーダプログラムの流れについて見ていくことにしましょう。



@ 先にシェーダプログラムを知りたいのですが…

 巷の本を眺めますと、先にシェーダプログラム自体をある程度紹介してからその使い方に進んでいるものが多いのですが、私がシェーダプログラムを始めた時、この順番にやられてえらい苦労しました。アセンブラで書かれたもの自体なじみが無く、良くわからないレジスタなるものを駆使されてもさっぱりだったんです。ということで、人によるかもしれないのですが、○×ではまずなじみのあるDirect3Dのプログラム部分の大きな流れを先に知ってから、シェーダプログラムの細部を次に見る方式で行きます。シェーダプログラムの詳細は次章で紹介します。



A 頂点シェーダの使い方(大局編)

 シェーダプログラムの使い方は頂点シェーダもピクセルシェーダも大して変わりません。そこで、ここでは頂点シェーダを例にその流れを紹介します。頂点を宣言してから頂点シェーダを使ってレンダリングするまでの作業をざっと並べると次のようになります。

 しぇーだしぇーだと並んでいますが、大した事はしませんのでご安心を。まず柔軟な頂点フォーマット(FVF)を宣言し、実際に頂点を作成します。これは、いままでの固定機能パイプラインの時と何ら変わりません。
 次に、どんな頂点情報を扱うのかをシェーダに教えるため、頂点データを宣言します。これにはD3DVERTEXELEMENT9という構造体を利用します。固定機能パイプラインで言えば、D3DFVF_XYZなどで頂点情報を宣言するのと全く一緒ですが、頂点シェーダ風のやり方があります。ここまでが頂点に関する手順です。
 次からはシェーダプログラムに関する部分になります。まず頂点シェーダプログラムを作成し、GPUがわかる形にコンパイルし、IDirect3DBufferにコンパイル後のアセンブル命令を保持します。これも固定的なお決まりのやり方がありますので簡単です。
 次に生成したシェーダプログラムバッファを用いて頂点シェーダインターフェイス(シェーダハンドラ)を作成します。
 最後に生成した頂点シェーダインターフェイスをストリームに設定し、固定機能パイプラインをプログラマブルパイプラインに切り替えて、レンダリングを行います。

 では、次から各論に移ります。



B 頂点フォーマットの定義

 頂点フォーマット定義は通常通り行います。例えば頂点情報と頂点カラーを使うのであれば、次のような宣言になるでしょうか。

頂点フォーマットの定義
struct CUSTOMVTX
{
   float x, y, z;    // 頂点位置
   DWORD color;     // 頂点カラー
};

#define CUSTOMFVF  D3DFVF_XYZ | D3DFVF_DIFFUSE

 頂点フォーマットを扱う場合「座標変換済み頂点」を宣言してはいけません。具体的にはRHW(D3DFVF_XYZRHW)を定義してはいけないと言うことです。座標変換済み頂点を定義してしまうと、頂点シェーダ自体が自動的にスキップされてしまいます。



C 頂点シェーダで扱う頂点情報の宣言

 次に自分が定義したFVFの内容を教えてあげます。これについて、DirectX8の頃にはやけに面倒な方法を用いる必要がありましたが、DirectX9になってD3DVERTEXELEMENT9構造体の配列を用いるように変更され、すっきりと整理されました。

頂点データ宣言
D3DVERTEXELEMENT9 VtxElem[3];

// 頂点座標情報の宣言
VtxElem[0].Stream = 0;
VtxElem[0].Offset = 0;
VtxElem[0].Type = D3DDECLTYPE_FLOAT3;
VtxElem[0].Method = D3DDECLMETHOD_DEFAULT;
VtxElem[0].Usage = D3DDECLUSAGE_POSITION;
VtxElem[0].UsageIndex = 0;

// 頂点カラー情報の宣言
VtxElem[1].Stream = 0;
VtxElem[1].Offset = 12;
VtxElem[1].Type = D3DDECLTYPE_D3DCOLOR;
VtxElem[1].Method = D3DDECLMETHOD_DEFAULT;
VtxElem[1].Usage = D3DDECLUSAGE_COLOR;
VtxElem[1].UsageIndex = 0;

// 終端宣言
VtxElem[2].Stream = 0xFF;
VtxElem[2].Offset = 0;
VtxElem[2].Type = D3DDECLTYPE_UNUSED;
VtxElem[2].Method = 0;
VtxElem[2].Usage = 0;
VtxElem[2].UsageIndex = 0;

 すっきりと整理されたとは言え、この構造体を理解できるかが頂点シェーダを理解することにも繋がります。そのために、あえてここではメンバ変数に代入する形式を記述しました(通常は宣言時初期化を用います)。
 構造体は要素3の配列になっています。これは、今回「頂点座標」と「頂点カラー」の2つを用いるためです(3番目は終端宣言)。テクスチャ座標など、他のフォーマットも用いるのであれば、その分配列を用意します。今回は要素0番に頂点座標の情報を、要素1番に頂点カラーの情報を記述することにしましょう。

 まず、Streamは頂点シェーダを設定する「ストリーム番号」を示します。その1で示したレンダリングまでの流れのことをストリームと言います。Direct3Dは複数のストリームを定義することができ、それぞれに頂点シェーダを個別定義することが可能です。上のようにすると、「ストリーム0番の頂点シェーダに頂点データを教えますよ」という意味になるわけです。
 Offsetは作成したFVFに定義した変数へのオフセット値を定義します。今回独自に定義したCUSTOMVTX構造体において、頂点情報は先頭に宣言されていますから、VtxElem[0].Offset=0となっています。同様に頂点カラーへのオフセット値は12(バイト)ですからVtxElem[1].Offset=12となっているわけです。
 Typeは作成したFVFに定義した変数の型を示します。これはD3DDECLTYPE列挙型で定義されています。頂点情報(D3DFVF_XYZ)はfloat[3]型ですから、それを表すDeDDECLTYPE_FLOAT3を指定します。頂点カラー(D3DFVF_DIFFUSE)の場合はD3DDECLTYPE_D3DCOLORというマクロを用います。どのFVF情報がどのマクロにあたるかは固定的でして、マニュアルの「頂点宣言」にすべて記載されていますのでご参照下さい。
 Methodというのは、テッセレーション(ポリゴン分割)の方法を指定します。普通テッセレーションはあまり使いませんので、デフォルト設定(D3DDECLMETHOD_DEFAULT)で十分でしょう。
 UsageはFVF情報自体を表すマクロを指定します。頂点座標であればD3DDECLUSAGE_POSITION、頂点カラーならD3DDECLUSAGE_COLORと、何を使うかは決まっています。これもマニュアルの「頂点宣言」にすべてありますので、それを参照すれば何も悩むことはありません。
 UsageIndexというのはUsageが重複しているものについて、固有番号を振って識別するものです。例えば頂点カラーと頂点スペキュラはUsageが重複していまして、インデックスでどちらであるかを判断します。ちなみに頂点カラーは0番、スペキュラは1番と指定されています。これもマニュアルの頂点宣言に全部書いてあります。

 終端宣言はその名の通り頂点宣言が終わったことを教えるターミネータで必ず必要になります。これが無いと次に説明する頂点宣言インターフェイスを生成できません。

 上のような配列の設定方法はわかりやすいのですが、やや面倒でもあります。配列の宣言時初期化を用いれば、もっとすっきり書けます。

// 頂点座標情報の宣言
D3DVERTEXELEMENT9 VtxElem[]={
   {0, 0, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_POSITION, 0},
   {0, 12, D3DDECLTYPE_D3DCOLOR, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_COLOR, 0},
   D3DDECL_END()
};

D3DDECL_END()というのは終端宣言のマクロで、宣言時初期化の時にしか使えませんが、便利です。

 宣言を終了したら、頂点宣言を格納するIDirect3DVertexDeclaration9インターフェイスを生成します。これはIDirect3DDevice9->CrateVertexDeclaration関数を使います。Declarationというのは「宣言・公表」という意味です。

頂点シェーダ命令を定義
IDirect3DVertexDecralation9 *pDec;
pD3DDev9->CreateVertexDeclaration( VtxElem, &pDec );

 このインターフェイスは描画時に用いられます。



D 頂点シェーダプログラムの生成

 次に頂点シェーダの命令を並べたプログラムを生成します。頂点シェーダプログラムの生成方法には大きく2つあります。1つは外部のテキストファイルを読み込む方法、もう1つはプログラム上に「頂点シェーダ命令の文字列」として直接書き込む方法です。ここでは後者の方法を例とします。前者のファイル読み込みによる方法も後ほどご紹介します。
 「頂点シェーダ命令の文字列」というのは、その名が示す通り単なるプログラムの文章です。それは例えば以下のように定義します。

頂点シェーダ命令を定義
const char VShader[] =
   "vs_1_1                        \n"
   "dcl_position v0              \n"
   "dcl_color    v1              \n"
   "m4x4        oPos, v0, c0 \n"
   "mov          oD0 , v1      \n";

可変長の長い文字列で頂点シェーダ命令を記述します。この命令群の詳細は今は説明しませんが、これこそがその1でも示したプログラマブルパイプライン(頂点シェーダ)そのものです。この謎な短いプログラムだけで頂点の変換を実現できていることには驚きますよね。



E 頂点シェーダの生成

 シェーダプログラムソースから、実際に頂点シェーダが扱う「頂点シェーダハンドラ」を生成するのが次の作業です。それにはまず、上のシェーダ命令が正しいかどうかをチェックし、GPUが使用できる形にコンパイル(アセンブル)します。これはD3DXAssembleShader関数を用います。

頂点シェーダ命令をコンパイル
ID3DXBuffer *pSheder;   // シェーダ命令格納バッファ
ID3DXBuffer *pError;     // コンパイルエラー情報格納バッファ

HRESULT hr;
hr =D3DXAssembleShader(
   VShader,        // 命令配列へのポインタ
   sizeof(VShader) - 1,    // 命令文字数
   0,                // プリプロセッサ定義
   NULL,          // インクルード命令指定(今回は無し)
   0,                // コンパイルオプション
   &pSheder,     // シェーダバッファ
   &pError         // コンパイルエラーバッファ
);

if( FAILED(hr) )
   return 0;

 第1引数で頂点シェーダ命令コードを与え、第2引数でその文字列の長さを指定します。マイナス1しているのは、最後のナル文字を除くためです。第3〜5引数まではコンパイルオプションです。上の例では何もオプションを使用していません。このコンパイルがうまく行けば、第6引数pShederにコンパイルされた実行コードを格納したバッファへのポインタが返り、エラーがあれば第7引数にエラー情報が返ります。正にコンパイルをソース内で実行時に行っているわけです。

 返されたpShederは単なる命令を列挙したバッファです。ここからさらにGPUとやり取りをするインターフェイスである頂点シェーダハンドラ(IDirect3DVertexShader9)を生成します。これにはIDirect3DDevice9::CreateVertexShader関数を用います。

頂点シェーダの生成
// シェーダハンドラ
IDirect3DVertexShader9 *pShaderHandler;

pD3DDev9->CreateVertexShader(
   (DWORD*)pSheder->GetBufferPointer(),
   &pShaderHandler
);



E ピクセルのレンダリング

 ここまででレンダリングするための準備が大体整います。実際のレンダリング部分は、固定機能パイプラインと殆ど同じなのですが、幾つか違う点もあります。注目は太文字で示した部分です。

ピクセルのレンダリング
// 変換行列の設定
D3DXMATRIX mat, matView, matProj;
D3DXMatrixLookAtLH(&matView, &D3DXVECTOR3(0,0,1), &D3DXVECTOR3(0,0,0), &D3DXVECTOR3(0,1,0));
D3DXMatrixPerspectiveFovLH( &matProj, 0.785398163f, 480.0f/640.0f, 0.1f, 10000.0f);
D3DXMatrixMultiply( &mat, &matView, &matProj );

// 行列を転置してシェーダに伝える
D3DXMatrixTranspose( &mat, &mat );
pD3DDev9->SetVertexShaderConstantF( 0, (float*)&mat, 4 );

// 描画
pD3DDev9->SetStreamSource( 0, m_pVertex, sizeof(CUSTOMVTX) );
g_pD3DDev->SetVertexDeclaration(pDec);
pD3DDev9->SetVertexShader( pShaderHandler );
pD3DDev9->DrawPrimitive( D3DPT_TRIANGLEFAN, 0, 2 );

 まず頂点の変換行列をいつものように生成した後、その行列を転置します(D3DXMatrixTranspose関数)。「なぜ?」と言われると「そうだから」としか答えようのない部分です。これは、GPUの行列計算方法とDirectXのそれが異なるためです。ですから、正しい頂点変換座標を得るために必ず転置してください
 その転置行列をGPUに教えるには、IDirect3DDevice9::SetVertexShaderConstantF関数を利用します。この関数はGPU内で扱う定数を設定する関数で、本当に良く利用される関数の1つです。行列だけでなく、ユーザが独自に定義する様々な「定数」をGPUに教えることができます。Direct3Dの行列は16個のfloat型の定数の塊ですが、この関数ではfloat[4]という単位で定数を定義します。第3引数にある「4」というのが塊の個数です。つまりfloat[4]型*4個=float型16個ということですね。第1引数の0というのはGPU内の定数レジスタ(定数配列のようなものです)のインデックス番号でして、この設定だと0番〜3番までの4つの定数レジスタを上書きします。
 ストリームに頂点を設定した後、IDirect3DDevice9::SetVertexDeclaration関数で頂点情報をデバイスに教えます。これはIDirect3DDevice9::SetFVF関数と全く同じ作用をします。よって、SetFVF関数を呼ぶ必要はありません。
 頂点シェーダに切り替える作業はIDirect3DDevice9::SetVertexShader関数に頂点シェーダハンドラを渡すことで行います。これで固定機能パイプラインがプログラマブルパイプライン切り替わってしまいます。ですから、固定機能パイプラインに変換行列を設定するSetMatrix関数は上で扱われていません(設定しても当然無視されます)。


 以上で頂点シェーダが組み込まれ、レンダリングが行われます。



F 外部ファイルから頂点シェーダプログラムのロード

 DおよびEで頂点シェーダプログラムを設定してコンパイルしました。Dではソース上に文字列として直接命令文を書き込んだのですが、外部ファイルを参照することもできます。それには、D3DXAssembleShaderFromFile関数を用います。

シェーダプログラムを外部からロード
D3DXAssembleShaderFromfile(
  "VertexShaderfile.fx",        // 頂点シェーダプログラムファイル名
   0,                // プリプロセッサ定義
   NULL,          // インクルード命令指定(今回は無し)
   0,                // コンパイルオプション
   &pSheder,     // シェーダバッファ
   &pError         // コンパイルエラーバッファ
)

 D3DXAssembleShader関数と殆ど同じです。ただ第1引数に頂点シェーダプログラムが格納されたファイル名を指定するのと、文字列の長さを指定しなくて良いと言う点が異なります。後の処理は全て共通します。



 さて、これで「自分が宣言した頂点が頂点シェーダプログラムによって『何らかの処理』をされて2D画面に表示できる座標に変換される」というプログラマブルパイプラインの流れができました。この流れは殆ど固定的です。後は頂点シェーダプログラムを差し替えるだけで、独自の効果を演出することが可能になります。頂点シェーダもピクセルシェーダも、これまでの固定機能パイプラインによるレンダリング方法に差し込む形式ですから、うまくクラス化すればシェーダプログラムを書くだけで良くなります。

 では、次の章で頂点シェーダプログラムの詳細を見ていくことにしましょう。