連載
.NET&Windows Vistaへ広がるDirectXの世界

第7回 プログラマブル・シェーダによる積極的なGPUの活用

NyaRuRu
Microsoft MVP Windows - DirectX(Jan 2004 - Dec 2007)
2007/05/08

頂点アウトプット→ラスタライザ→ピクセル・インプット

 いま三角ポリゴンの描画を考えると、3つの頂点アウトプットは、それぞれの頂点がスクリーン上に投影された位置を含んでいる。このとき、もともとの三角形を塗りつぶすことは、スクリーン上に投影された3点の内側のピクセルを塗りつぶすことに対応する。当然のことながら、低解像度のディスプレイよりも、高解像度のディスプレイの方が三角形に含まれるピクセル数は多くなり、より詳細に三角形を描画することができる。

ラスタライズとディスプレイ解像度
同じスクリーン座標で表される三角ポリゴンでも、高解像度のディスプレイの方がより多くのピクセルに分割される。

 このように、スクリーン座標で表されたポリゴンをピクセルの集まりに分割する処理のことをラスタライズと呼び、ラスタライズ処理を行うパイプライン・ユニットのことをDirect3Dではラスタライザと呼んでいる。

 なお、ラスタライズという処理自体は、3Dプログラミングに特有というわけではなく、むしろGDIやGDI+のようないわゆる「2D描画ライブラリ」、あるいはIllustratorなどのベクタ・グラフィックス・ツールでおなじみのものである。これはそもそも、われわれの使用しているディスプレイがピクセルの集合としてしか画像を表示できず、メモリ上ではベクトル形式でも、表示時には必ずピクセル形式に変換しなければならないためである。

 さて、Direct3Dのラスタライザにはピクセル化以外にも重要な機能がある。その機能とは、各頂点の持つ付加的なパラメータをポリゴン内で線形補間し、それをピクセル・インプットに引き渡すというものだ。先ほどの頂点アウトプット構造体が、TEXCOORD0 Usageの付与された2次元ベクトルを含んでいたことを思い出してほしい。

 ラスタライザは、頂点アウトプット構造体でTEXCOORD0 Usageを持つフィールドの値を、ピクセル・インプット構造体で同じくTEXCOORD0 Usageを持つフィールドに転送するという役目も持っているのだ。次のようなピクセル・インプット構造体を考えてみよう。

struct PixelIn
{
  float2 UV : TEXCOORD0;
};
ピクセル・インプット構造体の例(HLSLによる記述)
フィールドにUsageを指定すると、頂点アウトプット構造体で同じUsageとして定義されているフィールドの値を受け取ることができる。
線分や三角ポリゴンを描画している場合は、頂点が複数存在するため、各頂点からの相対的な位置によって補間された値が使用される。
正確には、このときの補間はスクリーン上の単純な線形補間ではなく、パースペクティブ・コレクトな線形補間(奥行きを考慮した線形補間)である。

 このピクセル・インプット構造体を利用するラスタライズ処理の流れを図示すると以下のようになる。

サンプル・コード実行時のラスタライズ処理の流れ
以下の処理が、すべてのポリゴンについて繰り返し実行される。
  頂点シェーダから出力された頂点出力構造体(VertexOut)3つによって、1つの三角ポリゴンが構成される。
  POSITION Usageを持つフィールドからスクリーン上の頂点位置が決まるので、塗りつぶすべきピクセルがラスタライザによって求められる。
  ラスタライザによって、それぞれのピクセルごとにピクセル・インプット(PixelIn)が計算される。このとき、ピクセル・インプット構造体のフィールドは、同じUsageを持つ頂点出力構造体の値を補間したものがあらかじめセットされる。
  ピクセル・インプット構造体をパラメータとして、ピクセル・シェーダが実行される。なお、実際のシェーダ・コードにはピクセル・インプット構造体といったメモリ構造は存在せず、すべてレジスタ渡しのコードに変換されている。

 このようにラスタライザは汎用的な線形補間器としても用いることができる。例えば単純なカラー・グラデーションならラスタライザだけでも作成可能だ。

 また、TEXCOORD0のUsageを指定したからといって、その値の用途がテクスチャのUV座標計算に限定されるということはない。UsageにはTEXCOORDやCOLORといったものがあるが、これは固定機能処理時代の慣習を引きずってそう呼ばれているだけで、レジスタが汎用化された現代のプログラミング・シェーダでは、単にレジスタ間の対応を指すラベルとして用いられているにすぎない。

 一般的には、HLSLを他人に見せるときのことも考慮し、用途に合ったUsageを指定しておくのがよいだろう。Usageの一覧は、HLSLのヘルプに見ることができる。一方で、特殊なエフェクトでは、TEXCOORD0に「電位」や「温度」、「屈折率」などが入っているということも考えられる。

ピクセル・インプット→ピクセル・シェーダ→ピクセル・アウトプット

 ラスタライザによって生成されたピクセル・インプット構造体は、ピクセル・シェーダによってピクセル・アウトプット構造体に変換される。このピクセル・アウトプット構造体が最終的な出力色を含んでいる。

 頂点アウトプット構造体にスクリーン座標を含める必要があったように、ピクセル・アウトプット構造体にはピクセルの塗りつぶし色が必須となっている。次のピクセル・アウトプット構造体は、ピクセルの塗りつぶし色のみを含む最も単純なものである。

struct PixelOut
{
  float4 Color : COLOR0;
};
最も単純なピクセル・アウトプット構造体(HLSLによる記述)
ここでも、COLOR0 Usageによってフィールドの用途が識別されている。

 例として、UV座標の値をそのまま赤(R)と緑(G)に出力するピクセル・シェーダを作ってみよう。

PixelOut MyPixelShader(PixelIn input)
{
  PixelOut output;
  output.Color = float4( input.UV, 0.0f, 1.0f );
  return output;
}
シンプルなピクセル・シェーダ
UV座標をそのまま赤(R)と緑(G)に変換している。
いわゆるテクスチャ・マッピングも、通常はこの段階で行われる。一般的なテクスチャ・マッピングは、補間されたUV座標を基にテクスチャ画像の1点の色を取得し、それを出力色に反映させるというものだ。
なお、頂点シェーダとは異なり、ピクセル・シェーダは実行をキャンセルすることができる。ピクセル・シェーダ中でclip命令を使用すると、このピクセルの描画を“なかったこと”にできる。

 ピクセル・シェーダもほかのピクセルに影響を及ぼせないという点では頂点シェーダと同じで、複数のピクセル・シェーダが同時に実行されても問題が起きないようになっている。これはGPUが相手にしている世界で求められるスループットを考えれば当然の仕様で、例えば1024×1024のスクリーンの塗りつぶしは100万回ものピクセル・シェーダの実行を必要としている。ハードウェア的に複数のピクセル・シェーダを同時に実行できるプログラミング・モデルは、GPUの性能向上を図るうえでも当然の要求というわけだ。

Sample1の実行結果
4隅の頂点に指定されたUV座標がピクセルごとに補間され、ピクセル・シェーダによって赤と緑の出力色に変換されている。

描画の実行

 レンダリング・パイプラインの仕組みとプログラミングを駆け足で見てきたが、このレンダリング・パイプラインの駆動を開始するトリガーは、GraphicsDeviceクラスのDrawIndexedPrimitives/DrawPrimitivesメソッドである。レンダリング・パイプラインの設定をデバイスにセットし、描画APIを呼び出すところを見てみよう。

 頂点シェーダやピクセル・シェーダも描画ステートの一種なので、エフェクト・ファイル(第2回参照)に設定を記述できる。

 エフェクト・ファイルは、テクニック(Technique)とパス(Pass)という階層構造で描画ステートのセットを管理している。1つのパスは1回のDrawIndexedPrimitives/DrawPrimitivesメソッドの呼び出しに対応していて、複数のパスをまとめたものがテクニックである。すなわち、テクニックは重ね塗り(マルチパス・レンダリング)の1回に対応する。今回使用するのは次のようなパスだ。

technique Render
{
  pass Pass0
  {
    VertexShader = compile vs_3_0 MyVertexShader();
    PixelShader = compile ps_3_0 MyPixelShader();
  }
}
Simple.fxで使用しているステート定義
パスを1つだけ含むテクニックで、MyVertexShaderとMyPixelShaderをデバイスにセットしている。

 このように、描画に関するさまざまな記述を行ったエフェクト・ファイルをXNAのプロジェクトに加えると、コンテント・パイプラインによってビルド時にコンパイルされ、専用のバイナリ・ファイルにシリアライズされる。このバイナリ・ファイルを読み込んで動くのがXNA FrameworkのEffectクラスで、C#コードとエフェクト・ファイルの連携の要である。XNA Frameworkでは、エフェクト・ファイルに指定したアセット名を基に、対応するEffectオブジェクトを得ることができる。

 さて、以前にも述べたようにDirect3Dは巨大なステート・マシンなので、描画API(DrawPrimitivesメソッド)が呼び出された瞬間にデバイスにセットされていた描画ステートが描画に使用される。描画後は、必要に応じて元のステートの復元を行う。これをパターン化すると、DrawPrimitives命令の直前に描画ステートを設定し、描画直後にステートを元に戻すという定型処理が現れる。

 このパターンに基づいて、描画ステートの記述を分離したのがエフェクト・システムである。例えば、取得したEffectオブジェクトを描画結果に反映させるには、ちょうど描画命令の前後に割り込むようにEffectオブジェクトのメソッドを呼び出すことになる。以下のコードをご覧いただきたい。

// エフェクト描画の開始
effect.Begin();

// 頂点ストリーム、頂点宣言をセット
// (これらはエフェクト・ファイルで設定できない)
graphics.GraphicsDevice.Vertices[0].SetSource(vb, 0, 16);
graphics.GraphicsDevice.VertexDeclaration = vertexDecl;

foreach(EffectPass pass in effect.CurrentTechnique.Passes)
{
  pass.Begin();
  graphics.GraphicsDevice.DrawPrimitives(
    PrimitiveType.TriangleFan, 0, 2);
  pass.End();
}

//エフェクト描画の終了
effect.End();
Effectクラスを利用した定型的な描画処理
2つずつあるBeginメソッドとEndメソッドで、必要に応じてデバイスの描画ステートが変更されている。一方、頂点ストリームと頂点宣言の設定はエフェクト・ファイルに記述されていないので、別途明示的に設定を行っている。
なお、描画の開始時に「effect.Begin(SaveStateMode.SaveState);」としておくと、Effectオブジェクトによって変更されたステートが「effect.End();」で復元されるようになる。

 エフェクト・システムで興味深いのは、Effectオブジェクトの挙動をエフェクト・ファイルに記述できるところにある。すなわち、Effectオブジェクトは抽象的なインターフェイスにすぎず、実際の実装クラスはエフェクト・ファイルの特殊な記法や、HLSLというシェーダ言語で記述されていると見なせるわけだ。

 それではいよいよ、ピクセル・シェーダによるマンデルブロ集合の描画に移る。


 INDEX
  .NET&Windows Vistaへ広がるDirectXの世界
  第7回 プログラマブル・シェーダによる積極的なGPUの活用
    1.トピックの由来とサンプル・コード
    2.描画の流れ(1)
    3.描画の流れ(2)
  4.描画の流れ(3)
    5.描画の実行
 
インデックス・ページヘ  「.NET&Windows Vistaへ広がるDirectXの世界」


Insider.NET フォーラム 新着記事
  • 第2回 簡潔なコーディングのために (2017/7/26)
     ラムダ式で記述できるメンバの増加、throw式、out変数、タプルなど、C# 7には以前よりもコードを簡潔に記述できるような機能が導入されている
  • 第1回 Visual Studio Codeデバッグの基礎知識 (2017/7/21)
     Node.jsプログラムをデバッグしながら、Visual Studio Codeに統合されているデバッグ機能の基本の「キ」をマスターしよう
  • 第1回 明瞭なコーディングのために (2017/7/19)
     C# 7で追加された新機能の中から、「数値リテラル構文の改善」と「ローカル関数」を紹介する。これらは分かりやすいコードを記述するのに使える
  • Presentation Translator (2017/7/18)
     Presentation TranslatorはPowerPoint用のアドイン。プレゼンテーション時の字幕の付加や、多言語での質疑応答、スライドの翻訳を行える
@ITメールマガジン 新着情報やスタッフのコラムがメールで届きます(無料)

注目のテーマ

Insider.NET 記事ランキング

本日 月間