ホーム < ゲームつくろー! < プログラマブルシェーダ編 < HLSLの根っこ:最初の最初からやってみる

その6 HLSLの根っこ:最初の最初からやってみる


 HLSL(High-Level Shader Language:上位レベルシェーダ言語)は頂点シェーダ及びピクセルシェーダを記述できる言語です。前章までのアセンブリによるプログラムは仕組みとしては単純なのですが、やはりアルゴリズムの構築がパズルのようで、可読性の点からも厳しいものがあります。一方でHLSLはC言語風の表記方法で、はるかに読みやすいシェーダプログラムを書くことができます。これはちょうどアセンブラとC言語の関係と同じです。そう考えると世の中がHLSLに移行するのは目に見えています。

 HLSLについて書かれた本は沢山あります。ただ、私がアホなだけかもしれませんが、なぜだか本を見てもすっと理解できないんですよ(^-^;。それはきっと、根っこの根っこが分かっていないからなんだと思います。そこで、この言語についてホントに最初の最初から見てみることにしました。調べて書く「根っこシリーズ」。今回は、HLSLです。



@ HLSLとは?

 HLSLというのはプログラム言語です。つまり何らかのルールに従って書けば(入力)何らかの作用(出力)があるわけです。HLSLプログラムの入力は当然テキストです。ではHLSLから出力されるものは何か?これは「シェーダプログラム」に他なりません。HLSLを「コンパイル(=翻訳)」するとシェーダプログラム(アセンブリ記述)が吐き出されます。「上位レベル」というのはそういう意味です。まずは、この辺りをイメージするのが大切ですよね。

 HLSLについては、DirectXマニュアル(日本語版)のキーワードで「上位」と打てば出て来ます。これによると、HLSLはDirectX9から出て来た新しい言語で、頂点シェーダ、ピクセルシェーダそしてエフェクトの作成に使うシェーダプログラムの「追加機能」なのだそうです。しかし、今やすっかりこっちに取って代わられている感があります。HLSLには関数、式、文、標準データ型、ユーザ定義データ型、プリプロセッサディレクティブなどが定義されている、とあるのですが、これらをどう記述するか詳しい事は書かれておりません。う〜ん・・・例もあるんですが、今のところ何やらさっぱりです。どうやら今の知識でもってマニュアルから読み取れる内容は、これが限界のようです。



A 最小限のHLSLプログラムとは?

 どんな言語でも、一番最初は何もしないプログラムを書くのが基本です。何もしないというのは「何も書かない」ではなくて、「何の作用もしないけどコンパイラが動いて通る」という意味です。C言語であれば、

int main()
{
   return 0;
}

がたぶん最小です。これを理解して、初めて色々と付け足していけるというものです。ところがHLSLの場合、書籍を見ても最初からいくばくかのエフェクトを作り込んだ所から始められてしまいます。それだと何が必要で何がいらないかわからない。初学者にとって一番最初がすっきりとしていないんです。そこでまずはコンパイラに通る最小限のHLSLプログラムを見極めようと思います。ただ、取っ掛かりが無いので何をどうして良いか正直全然わかりません。

 書籍を再度見ますと、世の中にはHLSLプログラムができるツールが幾つかあるようです。ここではそれに頼ってみます。今回は非常に有名なエフェクトツールソフトである「nVIDIA FX Composer 1.8」を使う事にしました。nVIDIA ComposerはnVIDIAのウェブサイトからダウンロードできます。このソフトは作成したHLSLプログラムをすぐにコンパイルでき、しかもエフェクトのかかり方をその場で確認できるという恐ろしく強力なツールです。まず本体をダウンロードしてインストール後ツールを実行すると、何やらHLSLプログラムがいきなりごっちゃりと出てきます。当然良くわからないので、無視して閉じてしまいます。ツールを空っぽにした状態で、[File]→[New]→[Material...]を選択するとCreate Materialウィンドウが出て来ますので、その中から「Basic empty Material」を選択します。すると、極々短いHLSLプログラムが出て来ます。これだと何とかなりそうです。このプログラムを取っ掛かりにして、色々探っていきましょう。

 FX Composerより吐き出された全コードを以下に示します。

// An empty material, which simply transforms the vertex and sets the color to white.

//------------------------------------
float4x4 matWVP : WorldViewProjection;

//------------------------------------
struct vertexInput {
   float3 Position : POSITION;
};

struct vertexOutput {
   float4 HPosition : POSITION;
   float4 Diffuse : COLOR0;
};

//------------------------------------
vertexOutput VS_TransformDiffuse(vertexInput IN)
{
   vertexOutput OUT;
   OUT.HPosition = mul( float4(IN.Position.xyz , 1.0) , matWVP);
   OUT.Diffuse = float4(1.0f, 1.0f,1.0f,1.0f);
   return OUT;
}

//-----------------------------------
technique textured
{
   pass p0
   {
      VertexShader = compile vs_1_1 VS_TransformDiffuse();

      // Just use the color
      ColorArg1[0] = Diffuse;
      AlphaArg1[0] = Diffuse;
      ColorOp[0] = SelectArg1;
      AlphaOp[1] = SelectArg1;
   }
}

 まず、出て来たプログラムを全てコメントアウトします。まっさらにしちゃおうというわけです。コメントアウトにはC言語同様に「/* */」が使えます。この段階でプログラムをコンパイルしてみます。コンパイルは[Build]→[Compile]とするか「Ctr+F7」で実行されます。コンパイルしてみると次のようなエラーが出ました。

コンパイルエラー
new_material_3.fx : ID3DXEffectCompiler: There were no techniques
new_material_3.fx : ID3DXEffectCompiler: Compilation failed

なるほど、このソフトは裏でDirect3DXのID3DXEffectCompilerインターフェイスをうまく使っているようです。ID3DXEffectCompilerは、DirectXのマニュアルにもありますが、シェーダプログラムを実行時コンパイルするインターフェイスです。つまりソースがこのコンパイラを通れば、DirectXでも実行時コンパイルされる事が保証されます。
 まっさらな状態で出て来た一番最初のエラーは「techniquesがありません」と忠告しています。techniqueというのは、プログラムの最後の方にありますね。

technique textured
{
   pass p0
   {
      VertexShader = compile vs_1_1 VS_TransformDiffuse();

      // Just use the color
      ColorArg1[0] = Diffuse;
      AlphaArg1[0] = Diffuse;
      ColorOp[0] = SelectArg1;
      AlphaOp[1] = SelectArg1;
   }
}

どうやら、この「technique」というくくりが必要のようです。そこで、この部分のコメントアウトをはずして再度コンパイルしてみると、エフェクトファイル(.fx)をセーブするように言われます。セーブに値する状態になったというところでしょうか。適当なフォルダーにセーブしたところ、次のようなエラーが出ました。

コンパイルエラー
Test.fx : (31): error X3004: undeclared identifier 'VS_TransformDiffuse'

 これは「VS_TransformDiffuseというのが宣言されていない」というエラーのようです。techniqueの内部に確かにそういう関数があります。そこで、該当部分である「VertexShader...」という1行ををコメントアウトして、もう一度コンパイルしてみます。すると、おお!コンパイルに成功します。

コンパイルOK
Building...
Created HLSL EffectCompiler: C:\[ここはないしょ(^-^)]\シェーダープログラム\Test.fx
Creating Material: _32
Reloading Material due to change: new_material_5, Effect: C:\[ここはないしょ(^-^)]\シェーダープログラム\Test.fx
Build Complete - Updated: 2 materials associated with Effect: C:\[ここはないしょ(^-^)]\シェーダープログラム\Test.fx
Errors: 0, Warnings: 0
Created HLSL Effect from EffectCompiler: C:\[ここはないしょ(^-^)]\シェーダープログラム\Test.fx
Found valid technique: textured
Technique: textured Validated for this device


 コンパイラは指定のフォルダにTest.fxというHLSLエフェクトファイルを生成したようです(確かにありました)。この事から、少なくともコンパイラを通るためにはtechnique部分が必須である事がわかりました。C言語で言えばエントリ関数であるmain関数のようなものなのでしょう。
 さて、先ほどのtechnique部分には「//Just use the color」という部分が含まれています。これは必須なのでしょうか?それを確認するため、この部分をコメントアウトします。つまり以下のようなソースにするわけです。

technique textured
{
   pass p0
   {
   }
}

これでコンパイルしてみると、ちゃんと通ります。コンパイラを通すのに先ほどの部分はいらないということです。同様に色々と削ってみますと、最終的にここまできました。

technique { }

これ以上削るとコンパイルエラーが出てしまいます。どうやらこれが「HLSL言語の最小限プログラム」ということになりそうです。ようやく、欲しいものがお目見えしました。ここからプログラムが始まります。



B 最小限でレンダリングするとどうなるか?

 一番最小限のHLSLプログラムを実際にオブジェクトに適用してみるとどうレンダリングされるのか?FX Composerはそれをすぐに見ることができます。画面内に「Materials」というウィンドウがありまして、そこにエフェクトファイルの簡易レンダリング結果が示されています(左側にあるMaterialsタグをクリックすると出現します)。

右側の緑色の球は、FX Composerがデフォルトで生成するエフェクトファイルを適用した結果です。一方左側の何も表示されていないのが、最小限HLSLプログラムを適用した結果です。これを見て分かりますように、このプログラムを通すと何も表示されません。レンダリングされていないのか見えていないのかはわかりませんが、最小限HLSLは表示できないプログラムである事がわかりました。



C コメントアウトをはずしていこう

 さあ、ここからコメントアウトを少しずつはずしていきます。まずは最小限HLSLプログラムをこういう状態に戻します。

technique textured
{
   pass p0
   {
      VertexShader = compile vs_1_1 VS_TransformDiffuse();
   }
}

この状態でコンパイルすると「VS_TransformDiffuseという関数が宣言されていないよ」というエラーが出ます。そこで、このソースの上方にあるVS_TransformDiffuseという実装部分のコメントをはずして次の状態にします。

vertexOutput VS_TransformDiffuse(vertexInput IN)
{
   vertexOutput OUT;
   OUT.HPosition = mul( float4(IN.Position.xyz , 1.0) , matWVP);
   OUT.Diffuse = float4(1.0f, 1.0f,1.0f,1.0f);
   return OUT;
}

technique textured
{
   pass p0
   {
      VertexShader = compile vs_1_1 VS_TransformDiffuse();
   }
}

これでコンパイルしてみると、やはり「だめ!」と言われます。

コンパイルエラー
Test.fx : (18): error X3000: syntax error: unexpected token 'VS_TransformDiffuse'

これは文法間違い(syntax error)ですが、どうやら「vertexOutput」そして「vertexInput」という構造体が宣言されていないために生じたようです。これらの構造体はさらに上方に宣言されていますので、そこのコメントもはずして次のようにします。

struct vertexInput {
   float3 Position : POSITION;
};

struct vertexOutput {
   float4 HPosition : POSITION;
   float4 Diffuse : COLOR0;
};

vertexOutput VS_TransformDiffuse(vertexInput IN)
{
   vertexOutput OUT;
   OUT.HPosition = mul( float4(IN.Position.xyz , 1.0) , matWVP);
   OUT.Diffuse = float4(1.0f, 1.0f,1.0f,1.0f);
   return OUT;
}

technique textured
{
   pass p0
   {
      VertexShader = compile vs_1_1 VS_TransformDiffuse();
   }
}

C言語そっくりですよね。これで構造体の宣言は出来ているようなのですが、コンパイルすると先ほどと違うエラーが出ます。

コンパイルエラー
Test.fx : (20): error X3004: undeclared identifier 'matWVP'

「matWVPなんて変数知らないよ」と言っているわけですね。matWVPはVS_TransformDiffuse関数内で使われています。この変数はさらに上方で次のように宣言されています:

float4x4 matWVP : WorldViewProjection;

この部分をはずすと(コメントを全部はずした事になります)、ようやくコンパイラを通るようになります。結局ソースは余すことなく全部必要だったというわけです。

 さて、コンパイラを通った後、幾つか画面に変化がありました。まず、先ほどまで何も表示されていなかったMaterialsウィンドウに白い球が出現しました。

つまり、このシェーダプログラムを通すと真っ白くレンダリングできるということです。そして、右上にある「Shader Perf」ウィンドウにアセンブラが出力されました。


先ほどのHLSLプログラムの出力は正にここに凝縮されているわけで、何だか嬉しいもんです。作成したシェーダプログラムは「vs_1_1」。つまり頂点シェーダのバージョン1.1のようです。これは元のソースのtechnique内の、

VertexShader = compile vs_1_1 VS_TransformDiffuse();

という箇所が対応しているようです。なるほど、つまりVS_TransformDiffuse関数が頂点シェーダなのかピクセルシェーダなのか、そしてそのバージョンはいくつなのかを示すには、technique内で上のように宣言すれば良いということが読み取れます。アセンブラの他の箇所は、まぁ個々で何をしているかはわかりますが、読むのはちょっと面倒ですね。元のソースから判断してみる事にしましょう。



D 改めてFX ComposerのデフォルトHLSLソースを見てみます

 ようやくソースを見る余裕が出来たように思いますので、FX Composer 1.8が空のHLSLとして吐き出したプログラムを改めて眺めてみる事にします。

float4x4 matWVP : WorldViewProjection;

struct vertexInput {
   float3 Position : POSITION;
};

struct vertexOutput {
   float4 HPosition : POSITION;
   float4 Diffuse : COLOR0;
};

vertexOutput VS_TransformDiffuse(vertexInput IN)
{
   vertexOutput OUT;
   OUT.HPosition = mul( float4(IN.Position.xyz , 1.0) , matWVP);
   OUT.Diffuse = float4(1.0f, 1.0f,1.0f,1.0f);
   return OUT;
}

technique textured
{
   pass p0
   {
      VertexShader = compile vs_1_1 VS_TransformDiffuse();
   }
}

 頂点シェーダプログラムはVS_TransformDiffuse関数内に実装されています。これは頂点シェーダv1.1を使用します。まず、この関数はvertexOutput構造体を返すようです。この構造体を見ますと、float4型の変数が2つある事がわかります。float4というのはfloat型の4次元ベクトルのようです。vertexOutput構造体は2つの4次元ベクトルを保持しているようです。次に関数の引数に取られているvertexInputという構造体を見ますと、これは3次元ベクトル(Position)のメンバを1つだけ持っているようです。なるほどなるほど…

 ところで、techniqueの内部では関数は引数無しになっていますが、宣言では引数(vertexInput型)が存在しています。これでコンパイラを通るのですから、きっとこれはHLSLの仕様です。頂点シェーダプログラムなのですから、引数には1つの頂点情報が入って来るに違いありません(その3『頂点シェーダプログラムの基礎』をご覧下さい)。でも、1つの頂点には「座標」「頂点の色」「テクスチャ座標」など沢山の情報が含まれているはずです。にもかかわらず、vertexInput構造体は3次元ベクトル1つだけです。vertexInput構造体のメンバ変数はいったい頂点情報のどれなのでしょうか?気になるのは下の赤い部分です:

struct vertexInput {
   float3 Position : POSITION;
};

C言語ではこういう書き方は出来ません。これはHLSL独自の仕様だと思います。これについてHLSLのマニュアルを見ますと、はあ、なるほど、ありました。これは「頂点シェーダ入力セマンティクス」というものらしいです。セマンティクスというのは「記号」という意味ですから、これは頂点シェーダの入力を識別する記号という事になります。POSITIONセマンティクスは「頂点の座標」とマニュアルにあります。なるほど、つまりPositionというメンバ変数には「頂点の座標」が格納されてくるわけです!実にわかりやすですねぇ!頂点シェーダ入力セマンティクスには他にも色々あるようです。例えば、頂点の色(ディフューズ色)も入力値として使いたいとなれば、ここを、

struct vertexInput {
   float3 Position : POSITION;
   float4 DiffuseColor : DIFFUSE;
};

と追加すれば良いというわけです(結構ありますので詳しくはマニュアルをご覧下さい)。はぁ〜〜なるほど素晴らしいですね。

 vertexOutput構造体も同様になっています。これは「頂点シェーダ出力セマンティクス」と言い、現在HLSLのバージョンだと、

 ・ 位置座標(POSIITON)
 ・ ポイントサイズ(PSIZE)
 ・ 頂点フォグ(FOG)
 ・ 色(COLOR[n]:nはオプションの整数でCOLOR0などのように使う)
 ・ テクスチャ座標(TEXCOORD[n])

という5つが出力できるようです。確かにその3『頂点シェーダプログラムの基礎』で説明した頂点シェーダの出力に対応しています。わかりやすなぁ!

 構造体の定義部分の意味はこれでばっちりです。続いてVS_TransformDiffuse関数内を見てみましょう。最初にvertexOUT構造体であるOUT変数を宣言しています。続いてOUT.HPositionメンバ(POSITIONセマンティクス)に何か代入しています。式の右辺はこうなっています。

OUT.HPosition = mul( float4(IN.Position.xyz , 1.0) , matWVP);

mulというのは明らかに関数(組み込み関数)ですね。マニュアルを見ると、mul関数は2つの引数aとbを取り、双方を行列とみなして掛け算をする関数のようです。ただし、双方がベクトルの場合、第1引数は行ベクトル、第2引数は列別として処理されるようです。上のmul関数内の第1引数はfloat4型の変数を直接生成しているようです(こういう書き方ができるのですね)。IN.Positionというのは頂点座標の入力値で、これは3次ベクトルでした(POSITIONセマンティクス)。IN.Position.xyzとすることで、4次元ベクトルの最初の3成分にxyz座標を代入しているわけです。4つ目の成分は1.0です。第2引数はmatWVPという変数です。これは、プログラムの最初の方で宣言されていました:

float4x4 matWVP : WorldViewProjection;

変数の型はfloat4x4ですから、これは「行列」です。そして、これにもセマンティクスがついていますね。しかし、このセマンティクスはマニュアルにありません!色々調べますと、どうやらこれは「ユーザ定義セマンティクス」というもののようです。これはDirectXのプログラムとツールが吐き出すエフェクトファイル間で変数を識別をするために使うようです。ここで設定されているWorldViewProjectionセマンティクスというのは「ワールドビュー射影変換行列」を表すようです。つまり、このエフェクトを正しく使うためには、DirectXプログラムでmatWVPにワールドビュー射影変換行列を設定する必要があります。具体的にDirectXからエフェクトファイル独自のセマンティクスに従った値を設定する方法についてはそのうちですね。

 え〜mul関数の話をしていたのでした、第1引数が頂点座標(これはローカル座標です)、第2引数がワールドビュー射影変換です。その掛け算ですから、頂点シェーダの出力値であるOUT.HPositionにはスクリーン座標値が入る事になります。なるほどぉ、これはピクセルシェーダに渡す値として正しいですね。

 次の行のOUT.Diffuse(COLOR0セマンティクス)にはディフューズ色を格納しています。(1.0f, 1.0f, 1.0f, 1.0f)ですから真っ白です。

 さて以上から、この頂点シェーダは入力された頂点をスクリーン座標に変換し、頂点の色を真っ白にしてピクセルシェーダに渡すHLSLプログラムである事がわかりました!頂点シェーダは設定していませんので素通りするため、Materialsウィンドウに真っ白なオブジェクトが表示されたというカラクリのようです。いやはや、HLSLとは直感的で大変分かりやすい言語ですねぇ(^-^)



E よしHLSLを使おう!

 ここまで、あきれるほどしっかりと短いソースを見てきました。nVIDIA FX Composerが吐き出したHLSLプログラムは、基本を理解するのに十分な内容でした。少なくとも頂点シェーダについて、これに似せて書けば出来そうな気がします。おぼろげにコツをつかんだわけですが、これでもDirectXのHLSLマニュアルは最初よりはるかに読めるようになっているはずです。
 ソースを精査してわかったことは、HLSLは直感的でとてもわかりやすいものであるという事です。C言語風のプログラムの書き方で、シェーダプログラムを簡単に書く事が出来ます。さらにうれしいのは、FX Composerによってプログラムをコンパイルし、エフェクトの結果をすぐに目で確認できる点です。こうなりますと、もうアセンブラでシェーダプログラムをガリガリと書く気にはなれません(^-^)。これからは積極的にHLSLを使っていくべきですね。今後ゲームつくろ〜プログラマブルシェーダ編もHLSLで記述していく事にします。いきなり難しい事は出来ませんが、焦らず少しずつこなしていきます。