DirectX9での
上位レベルシェーダー言語(HLSL)

2002.12.22 Yutaka Yoshisaka.


2002.12.21にDirectX9 SDKが公開されました。 それの目玉機能の「HLSL」を触ってみます。
「HLSL」(High Level Shader Language)とは、DirectX8.1以前のアセンブラライクの 頂点シェーダー・ピクセルシェーダーを、C言語のような高級言語の形式で 記述できる「上位レベルシェーダー言語」です。
これにより、シェーダーの敷居(可読性)の悪さから一部開放される、かもしれません。

HLSLの特徴

今まで、頂点シェーダー・ピクセルシェーダーをそれぞれのファイルの分けて管理しなければ ならなかったのを、1つのファイルにまとめてしまうことができます。 また、複数のシェーダーを1つのファイルに納めることもできます。
もちろん、記述がC言語ライクですので可読性も高いです。
まだ、命令数(組み込み関数など)・制御文(for/whileなど)が未実装なものが多いのですが、 じきにハードウェアの進化に伴って完成型に近づいていくものと思います。
現状は、複数ライトの計算などをまるまるシェーダーに乗っけてしまう、というのは (ループなどがハードウェア対応してないため)難しいのですが、頂点シェーダー3.0・ピクセルシェーダー3.0に なってくると、ピクセルごとのフォンライティングを行いつつ、とか夢ではないのかもしれません。

DX8.1→DX9の移行

単純に、「LPDIRECT3D8」「LPDIRECT3DDEVICE8」を「LPDIRECT3D9」「LPDIRECT3DDEVICE9」に変えて、 リンクのライブラリを「d3d8.lib」から「d3d9.lib」とかに変えればOK・・・と思ってたのですが、 そうも行かなかったです。エラーの嵐でした(^_^;;
さすがはメジャーバージョンアップ。
一応、移行ポイントをピックアップしてみます。

もちろん、他にも変更点はあるはずです。
一応、こちらで作ってるやつで引っかかったものだけを抽出しました。


HLSLへの移行

それでは、「頂点シェーダー」「ピクセルシェーダー」をHLSLに移行してみます。

HLSLでは、「エフェクト」(拡張子fxのファイル)としてシェーダープログラムを記述します。
全体の流れは、
初期化処理として、

描画処理として、

最後に破棄処理として、

という感じでしょうか。

それぞれのやり方を見ていきます。



● エフェクトファイルの読込

「Shader.fx」のような、頂点・ピクセルシェーダー情報が記述されたファイルを読み込み、 「ID3DXEffect」型のオブジェクトとして保持します。

LPD3DXEFFECT g_pEffect;

HRESULT hr;
hr = D3DXCreateEffectFromFile(g_pd3dDevice , "Shader.fx", 
  NULL , NULL , 0 , NULL , &g_pEffect , NULL );

シェーダー内部で構文エラーなどがあると、hrはD3D_OK以外を返します。
このあたりのデバッグは、よくシェーダーとにらめっこしてください。
#私はかなり悩みました(^_^;;。それについては後述です。

● 頂点フォーマットの指定

オブジェクトの各頂点ごとにどんな情報を持つのか、というフォーマットを「CreateVertexDeclaration」にて 指定・作成します。
「IDirect3DVertexDeclaration9」型のオブジェクトとして保持されます。


LPDIRECT3DVERTEXDECLARATION9 g_pVertexDeclaration;

HRESULT hr;

D3DVERTEXELEMENT9 decl[] ={
  //Position
  { 0, 0,  D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_POSITION, 0 },

  //Normal
  { 0, 12,  D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_NORMAL, 0 },

  //Base tex coords
  { 0, 24,  D3DDECLTYPE_FLOAT2, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_TEXCOORD, 0 },

  //Diffuse color
  { 0, 32,  D3DDECLTYPE_D3DCOLOR, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_COLOR, 0 },

  //v4 = Specular color
  { 0, 36,  D3DDECLTYPE_D3DCOLOR, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_COLOR, 1 },

  D3DDECL_END()
};

hr = g_pd3dDevice->CreateVertexDeclaration(decl , &g_pVertexDeclaration);


ここで、「decl」は何をしているのかというと、頂点情報として 順に「頂点座標 / 頂点法線 / テクスチャUV / デフューズ色 / スペキュラ色」の情報を持つ、 と教えてあげています。

ちなみに、「CreateVertexBuffer」で作成する頂点の1つの要素は以下のようになっているとします。


typedef struct {
  D3DXVECTOR3 position;  //位置
  D3DXVECTOR3 normal;    //法線
  float tu,tv;           //テクスチャのUV
  D3DCOLOR diffuse;      //デフューズ光
  D3DCOLOR specular;     //スペキュラー光
} D3DCTRL_CUSTOM_VERTEX_INFO;


「decl」の指定と1対1で対応しているのがおわかりでしょうか。
これにて、メインプログラムであるC言語側と、シェーダーのHLSLとの頂点情報の一致を計っています。

「CreateVertexDeclaration」にて、頂点フォーマットを保持するオブジェクトを作成します。
シェーダーが関わる描画を行う場合は、このオブジェクトを渡して、 頂点フォーマットはコレ、っていうのをシェーダー側に明示してあげる必要があります。

● 定数をシェーダーに渡す

シェーダー内では、 グローバル変数としてfloat4型のベクトルや行列、テクスチャ(のポインタ)などを 定義することができます。
描画の際は、シェーダーに定数を与える必要があるとき、このグローバル変数に対して値を設定します。

例えば、ワールド・ビュー・透視の行列を与える場合は、以下のようにします。


D3DXMATRIX matMatrixSet, matWorld, matView, matProj;

g_pd3dDevice->GetTransform(D3DTS_WORLD, &matWorld);
g_pd3dDevice->GetTransform(D3DTS_VIEW, &matView);
g_pd3dDevice->GetTransform(D3DTS_PROJECTION, &matProj);
D3DXMatrixMultiply(&matMatrixSet, &matWorld, &matView);
D3DXMatrixMultiply(&matMatrixSet, &matMatrixSet, &matProj);

//行列を指定
g_pEffect->SetMatrix("gWVPMatrix", &matMatrixSet);


この場合は、シェーダー内のグローバル変数「float4x4 gWVPMatrix」に対して、行列の値を入れます。
頂点シェーダーに「SetPixelShaderConstant」にて行列を渡すときのように「D3DXMatrixTranspose」で転置する必要はありません。


そのほか、float4のベクトルを渡すには、

D3DXVECTOR4 amb = D3DXVECTOR4(0.2f , 0.2f , 0.2f , 1.0f);
g_pEffect->SetValue("gAmbient", (void*)(float *)&amb, sizeof(D3DXVECTOR4));


のようにします。
この場合は、シェーダーのグローバル変数「float4 gAmbient」に情報を渡しています。

テクスチャを渡す場合は、「SetTexture」を使用します。
これは、D3DDeviceのSetTextureとは別なので注意してください。


g_pEffect->SetTexture("gTexture0",pTexture);


この場合は、シェーダーのグローバル変数「texture gTexture0」に情報を渡しています。


● 頂点フォーマットを教える

前に「CreateVertexDeclaration」で作成した頂点フォーマットの情報を、 「SetVertexDeclaration」にて渡します。
これで、シェーダーとの頂点情報の整合性が取れます。


g_pd3dDevice->SetVertexDeclaration( g_pVertexDeclaration );



● 特定のシェーダーを割り当て

シェーダーファイル内の「テクニック」(頂点シェーダー・ピクセルシェーダーをラップしてる感じのもの) を指定します。


D3DXHANDLE hTechnique = g_pEffect->GetTechniqueByName( "BaseColor" );
g_pEffect->SetTechnique( hTechnique );


この場合は、「BaseColor」というテクニックを現在のカレントとして割り当てています。
もちろん、複数のテクニックを1つのシェーダーファイルに記述しておいて 描画に応じて切り替える、ということも可能です。

● シェーダーを介しての描画処理

シェーダーを介して描画処理を行います。
シェーダー内でマルチパス指定が可能なので、それも考慮して描画する場合は 以下のようにします。


UINT nPasses,iPass;

//マルチパスを考慮した回数
g_pEffect->Begin( &nPasses, 0 );

//パス回数分繰り返して描画
for(iPass=0 ; iPass<nPasses ; iPass++){
  g_pEffect->Pass( iPass );

  g_pd3dDevice->SetStreamSource(0, pVertexBuffer , 0,sizeof(D3DCTRL_CUSTOM_VERTEX_INFO));
  g_pd3dDevice->SetIndices(pIndexBuffer);
  g_pd3dDevice->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, 0,
      0, VerCou, 0, PolyCou);
}
g_pEffect->End();


描画部分は、「Begin」〜「End」で囲みます。
パス分のループにて、「Pass」でカレントのパスを指定して、 DrawIndexedPrimitiveなどの描画処理を行います。
これにて、頂点処理はシェーダー内の指定の頂点シェーダーで、 ピクセル処理はシェーダー内の指定のピクセルシェーダーで行われることになります。

● 破棄処理

アプリケーションが終了する場合などに、エフェクトと頂点情報のオブジェクトを破棄します。
これは、他でもおなじみの「Release」を呼び出します。


if(g_pEffect){
  g_pEffect->Release();
  g_pEffect = NULL;
}

if(g_pVertexDeclaration){
  g_pVertexDeclaration->Release();
  g_pVertexDeclaration = NULL;
}


以上が、全体的な流れになります。
次は、シェーダーファイル内の記述です。

シェーダーファイル内の記述

シェーダーファイルとして「Shader.fx」を作成するとします。


//ワールド*ビュー*投影変換の行列
float4x4 gWVPMatrix;

//アンビエント値
float4 gAmbient = {0.2f,0.2f,0.2f,1.0f};

//基本色
float4 gBaseColor = {1.0f, 0.1f, 0.2f, 1.0f};

//----------------------//
//  頂点の入力情報
//----------------------//
struct VS_INPUT
{
  float4 vPosition    : POSITION;    //頂点座標
  float4 vNormal      : NORMAL;      //法線ベクトル
  float2 vTexCoords   : TEXCOORD0;   //テクスチャUV
  float4 vDiffuse     : COLOR0;      //デフューズ色
  float4 vSpecular    : COLOR1;      //スペキュラ色
};

//----------------------//
//  頂点の出力情報
//----------------------//
struct VS_OUTPUT
{
  float4 vPosition    : POSITION;    //頂点座標
  float4 vDiffuse     : COLOR0;      //デフューズ色
  float4 vSpecular    : COLOR1;      //スペキュラ色
  float2 vTexCoords   : TEXCOORD0;   //テクスチャUV
};

//----------------------//
//  ピクセルの入力情報
//----------------------//
struct PS_INPUT
{
  float4 vDiffuse     : COLOR0;     //デフューズ色
  float4 vSpecular    : COLOR1;     //スペキュラ色
  float2 vTexCoords   : TEXCOORD0;  //テクスチャUV
};

//----------------------//
//  ピクセルの出力情報
//----------------------//
struct PS_OUTPUT
{
  float4 vColor       : COLOR0;     //最終的な出力色
};

//------------------------//
// 頂点シェーダー処理
//------------------------//
VS_OUTPUT VSBaseColor(VS_INPUT v)
{
  VS_OUTPUT o = (VS_OUTPUT)0;

  //座標変換
  o.vPosition = mul(v.vPosition , gWVPMatrix);

  //デフューズ色をそのまま格納
  o.vDiffuse = v.vDiffuse;

  //スペキュラ色をそのまま格納
  o.vSpecular = v.vSpecular;

  return o;
}

//------------------------//
// ピクセルシェーダー処理
//------------------------//
PS_OUTPUT PSBaseColor(PS_INPUT p)
{
  PS_OUTPUT o = (PS_OUTPUT)0;

  o.vColor = (p.vDiffuse + gAmbient) * gBaseColor + p.vSpecular;

  return o;
}

//------------------------//
// テクニックの記述
//------------------------//
technique BaseColor
{
  pass P0
  {
    VertexShader = compile vs_1_1 VSBaseColor();
    PixelShader  = compile ps_1_1 PSBaseColor();
  }
}


まず、関数の外にある「gWVPMatrix」「gAmbient」「gBaseColor」が グローバル変数となります。C言語側から値を代入する(または逆に取得する) ことが可能です。
シェーダー内で光源計算をさせる場合は、光線ベクトルや位置情報などを ここに持たせるといいかもしれません。

その後にある「VS_INPUT」「VS_OUTPUT」が頂点シェーダーの処理が実行される場合の 入力と出力の構成定義です。型を指定しています。
設定できる要素は、シェーダーのバージョンによって差がありますので 詳しくはDirectX9のヘルプを参照してください。
「PS_INPUT」「PS_OUTPUT」は、ピクセルシェーダーの処理が実行される場合の 入力と出力の構成です。これも、シェーダーのバージョンによって差があります。
最終的には、「その点での色」を返すことになります。

それぞれの定義の「:」の後の「POSITION」「COLOR0」などですが、 これは、各パラメータの種類を指定しています。
この種類や並びは、「CreateVertexDeclaration」で指定した頂点フォーマットと1対1で対応しているもの、 と思っていいかと思います。
逆に言うと、これらが不一致だと描画が思わぬ結果になってしまうことになります。

「VSBaseColor」は、頂点シェーダー部の関数です。
この例では、頂点座標だけをグローバル変数の変換行列で変換後、出力情報に送ってます。
その他の色情報は、そのまま出力しています。

「PSBaseColor」は、ピクセルシェーダー部の関数です。
見ていただければ一目瞭然ですが「(デフューズ色 + アンビエント) * 基本色 + スペキュラ色」を計算して 結果を出力しています。
この場合は、0.0〜1.0内に収まるように自動でクリップされるようです。

「technique BaseColor」にて、「テクニック」として頂点シェーダー・ピクセルシェーダーの 関数を指定しています。
このときに、頂点シェーダーとピクセルシェーダーのバージョンも指定してます。

・・・と簡単に書いてますが、実はここにたどり着くまでにエラーの連続だったんです(^_^;;
まず、入力・出力で使用できるパラメータは、十分DirectX9のヘルプを見て 何が使えるのか確認するようにしてください。
あと、「mul」などの組み込み関数ですが、バージョンにより使えなかったりするのもあります。
(Ver1.xなら、ほとんど使えないかと)
ピクセルシェーダーで必死で組み込み関数を使って書いてたのですが、これもほとんど使えないですね。
使えない関数を使っていた場合は、「D3DXCreateEffectFromFile」にてエラーが返されます。
ピクセルシェーダーは、頂点シェーダー部よりも制約は依然強いです(ピクセルシェーダー1.xの場合、maxも使えなかったです)。

最後に、サンプルのソースをアップしておきます。
学習にお役立てください。



サンプルソース:dx90_test01.zip (476KB)

1つの白色平行光源と赤色点光源を配置してます。
光源計算はC言語側で行い、頂点ごとに光を考慮したデフューズ・スペキュラを渡してます。
頂点の「位置・法線・デフューズ色・スペキュラ色・UV」をHLSLに渡して、 最終的な結果を描画してます。
また、テクスチャ描画自身もHLSLで行ってます。
シェーダーファイルは「Shader.fx」です。