DirectX 9 High Level Shading Language 入門

Craig Peeper
Microsoft Corporation

Jason L. Mitchell
ATI Research

July 2003
日本語版最終更新日 2003 年 8 月 25 日

適用対象:
   DirectX(R) 9 High Level Shading Language

要約: Craig Peeper と Jason Mitchell が、近刊の著書 ShaderX2 - Introduction and Tutorials with DirectX 9 をもとに、多数のサンプル シェーダと最適化戦略の例を含めて、Microsoft DirectX High Level Shading Language を詳しく解説します。

目次:

はじめに
単純な例
アセンブリ言語とコンパイル ターゲット
言語の基本事項
組み込み関数
D3DX エフェクトを使うエンジンへの統合
D3DX エフェクトを使わないエンジンへの統合
SDK アップデート
結論
謝辞

はじめに

High Level Shading Language (HLSL) は DirectX(R) 9 の最も強力な新しいコンポーネントの 1 つです。この標準の高級言語を使用することで、シェーダの開発者は、レジスタ割り当て、レジスタ読み込みポートの制限、命令の同時発行といった細かいハードウェア上の問題を心配することなく、アルゴリズム レベルで考えながらシェーダを実装できます。開発者をハードウェアの細部から解放するだけでなく、HLSL には、コードの再利用の容易さ、可読性の向上、最適化コンパイラの存在など、通常の高級言語のすべての利点を備えています。本書と ShaderX2 - Shader Tips & Tricks の多くの章では、HLSL で書かれたシェーダを利用します。そのため、この入門の章を読んでおけば、本文で取り上げているシェーダの内容を理解し、実際に使用するのがはるかに簡単になるでしょう。

この章では、言語そのものの基本構造と、HLSL シェーダをアプリケーションに統合するときの戦略の概要を説明します。

単純な例

HLSL の詳しい解説を行う前に、まず単純な木目を手続き的にレンダリングするアプリケーションから、1 つの HLSL 頂点シェーダと 1 つの HLSL ピクセル シェーダの例を取り上げてみましょう。次に示す最初の HLSL シェーダは、単純な頂点シェーダです。

float4x4 view_proj_matrix;
float4x4 texture_matrix0;

struct VS_OUTPUT
{
   float4 Pos     : POSITION;
   float3 Pshade  : TEXCOORD0;
};


VS_OUTPUT main (float4 vPosition : POSITION)
{
   VS_OUTPUT Out = (VS_OUTPUT) 0; 

   // 位置をクリップ空間に変換
   Out.Pos = mul (view_proj_matrix, vPosition);

   // Pshade の変換
   Out.Pshade = mul (texture_matrix0, vPosition);

   return Out;
}

このシェーダの最初の 2 行は、view_proj_matrixtexture_matrix0v という名前の 2 つの 4 x 4 行列を宣言しています。これらのグローバル スコープの行列の次には、構造体の宣言があります。この VS_OUTPUT 構造体は、Pos という名前の float4 と、Pshade という名前の float3 の 2 つのメンバを持っています。

このシェーダの main 関数は、float4 の入力引数を 1 つ取り、VS_OUTPUT 構造体を返します。float4 の入力引数である vPosition は、このシェーダに対する唯一の入力です。返される VS_OUTPUT 構造体は、この頂点シェーダの出力を定義します。ここでは、これらの引数と構造体メンバの後に付いている POSITION および TEXCOORD0 キーワードのことは気にしないでください。これらは セマンティックと呼ばれるもので、その意味についてはこの章で後で解説します。

main 関数の実際のコード本体を見ると、mul という名前の組み込み関数が、入力のvPosition ベクトルと view_proj_matrix 行列の乗算を行うために使われていることがわかります。この組み込み関数は、頂点シェーダ内で、頂点行列の乗算によく使われます。この例では、vPositionmul の第 2 引数なので、列ベクトルとして扱われています。vPosition ベクトルが mul の 第 1 引数だった場合には、行ベクトルとして扱われます。mul 組み込み関数とその他の組み込み関数については、この章で後に詳しく解説します。入力位置 vPosition をクリップ空間に変換した後に、vPosition とさらに別の行列 texture_matrix0 との乗算が行い、3D のテクスチャ座標を生成します。この 2 つの変換の結果が、戻り値である VS_OUTPUT 構造体のメンバに書き込まれます。頂点シェーダは、必ず、最低限でもクリップ空間位置を出力しなくてはなりません。頂点シェーダから出力されるその他の値は、ポリゴン上で補間・ラスタ化され、ピクセル シェーダへの入力として提供されます。この例では、3D Pshade はインタポレータを通して頂点シェーダからピクセル シェーダに渡されます。

次に、単純な HLSL による木目の手続き的なピクセル シェーダを示します。このピクセル シェーダは、上記の頂点シェーダと協調して動作するように作られており、ps_2_0 ターゲット用にコンパイルされます。

float4 lightWood; // xyz == 木目の明るい色
float4 darkWood;  // xyz == 木目の暗い色
float  ringFreq;  // リングの頻度

sampler PulseTrainSampler;

float4 hlsl_rings (float4 Pshade : TEXCOORD0) : COLOR
{
    float scaledDistFromZAxis = sqrt(dot(Pshade.xy, Pshade.xy)) * ringFreq;

    float blendFactor = tex1D (PulseTrainSampler, scaledDistFromZAxis);
 
    return lerp (darkWood, lightWood, blendFactor);
}

このシェーダの最初の数行は、グローバル スコープの 4 浮動小数点 float4 のペアと、1 つのスカラー float です。これらの変数の後に、PulseTrainSampler という名前のサンプラが宣言されます。サンプラについてはこの章で後に詳しく解説しますが、ここではフィルタリングやテクスチャ座標アドレシング モードなどを定義する状態に関連付けられた、ビデオ メモリ内のウィンドウのようなものと考えてください。変数とサンプラの宣言の後に、シェーダ コードの本体があります。入力引数は、ポリゴン上で補間される Pshade です。これは、上記の頂点シェーダによって個々の頂点について計算された値です。ピクセル シェーダでは、シェーダ空間の z 軸からの直交距離を計算・スケーリングして、PulseTrainSampler にバインドされたテクスチャにアクセスするための 1D テクスチャ座標として使います。tex1D() サンプリング関数が返すスカラー色は、シェーダのグローバル スコープで宣言された 2 つの定数色 (lightWooddarkWood) をブレンドするときのブレンド係数として使います。このブレンドの 4D ベクトル結果が、ピクセル シェーダの最終的な出力となります。すべてのピクセル シェーダは、少なくとも 4D RGBA カラーを返さなくてはなりません。ピクセル シェーダのその他のオプションの出力については、この章で後に解説します。

アセンブリ言語とコンパイル ターゲット

さて、HLSL シェーダの例を 2 つ見たので、次にこの言語が Direct3D、D3DX、アセンブリ シェーダ モデル、そして開発者が作成するアプリケーションとどのような関係にあるのかを簡単に説明します。シェーダが初めて Direct3D に追加されたのは DirectX 8 のときでした。このときには、いくつかの仮想シェーダ マシンが定義されました。それぞれが、代表的な 3D グラフィックス ハードウェア ベンダが製造している個々のグラフィックス プロセッサにほぼ対応していました。そして個々の仮想シェーダ マシンごとに、アセンブリ言語が設計されました。DirectX 8.0 と DirectX 8.1 では、これらのシェーダ モデル (vs_1_1とps_1_1~ps_1_4) を使って書かれたプログラムは比較的短く、通常、開発者は適切なアセンブリ言語を使って直接にプログラムを書いていました。図 1 の左側に示しているように、アプリケーションはこの人間が読める形のアセンブリ言語コードを、D3DXAssembleShader() を通して D3DX ライブラリに渡し、シェーダのバイナリ表現を受け取って、これを CreatePixelShader() または CreateVertexShader() を通して Direct3D に渡していました。過去のアセンブリ シェーダ モデルの詳細については、Shader X と DirectX SDK を含むオンラインとオフラインの各種のリソースを参照してください。

図 1. DirectX 8 と DirectX 9 におけるアセンブリとコンパイルでの D3DX の使用

図 1 の右側に示しているように、DirectX 9 での状況も、アプリケーションが HLSL シェーダを D3DXCompileShader() API を通して D3DX に渡し、コンパイルされたバイナリ表現を受け取って、CreatePixelShader() または CreateVertexShader() を通して Direct3D に渡すというよく似たものです。生成されるバイナリの asm コードは、選択されたコンパイル ターゲットのみに左右され、ユーザーまたは開発者のシステムにあるグラフィックス デバイスの種類には依存しません。つまり、生成されるバイナリの asm はベンダに依存せず、どこでコンパイルまたは実行を行っても同じです。実際、Direct3D ランタイム自身は HLSL については何も知らず、バイナリ アセンブリ シェーダ モデルについての知識しか持っていません。この仕組みには、HLSL コンパイラを Direct3D ランタイムとは独立にアップデートできるという利点があります。実際、本書の印刷時から 2003 年の夏の終わりの発売日までに、Microsoft はアップデートされた HLSL コンパイラを含んだ DirectX SDK アップデートをリリースする予定です。

D3DX の HLSL コンパイラの開発に加え、DirectX 9.0 では最新世代の 3D グラフィックス ハードウェアの機能を公開する、新たなアセンブリ レベル シェーダ モデルが追加されています。アプリケーション開発者は、これらの新しいモデル (vs_2_0、vs_3_0、ps_2_0、ps_3_0) 用のアセンブリ言語を直接扱うこともできますが、ほとんどの開発者はシェーダ開発を完全に HLSL に移行すると予想されています。

ハードウェアの現実

もちろん、特定のシェーディング アルゴリズムを表現する HLSL プログラムを書けるからといって、それがどのハードウェア上でも動作するわけではありません。前に述べたように、アプリケーションは D3DX を呼び出して、D3DXCompileShader() APIを通して HLSL シェーダをバイナリ asm にコンパイルします。この API エントリポイントの引数の 1 つに、HLSL コンパイラが最終的なシェーダ コードを表現するのにどのアセンブリ言語モデル (またはコンパイル ターゲット) を使うべきかを定義する引数があります。HLSL シェーダ コンパイルを (オフラインではなく) 実行時に行う場合、アプリケーションは Direct3D デバイスの能力を確認して、それに対応するコンパイル ターゲットを選択できます。HLSL シェーダで表現されたアルゴリズムが、選択されたコンパイル ターゲットで実行するには複雑すぎると、コンパイルは失敗します。つまり、HLSL はシェーダ開発にとっては大きな利点となりますが、これによって開発者が、さまざまな能力のグラフィックス デバイスを持つターゲット オーディエンスに合わせたゲームを出荷しなくてはならないという現実から解放されるわけではありません。ゲーム開発者は、依然としてビジュアルの面では多層的なアプローチをとり、高度なグラフィックス カード用には高度なシェーダを、古いカード用には基本的なバージョンを書かなくてはなりません。しかし、HLSL を適切に利用すれば、その負担は大幅に軽減されます。

コンパイルの失敗

上で述べたように、HLSL シェーダを特定のコンパイル ターゲット用にコンパイルするのに失敗した場合、そのシェーダはそのコンパイル ターゲットには複雑すぎます。これには、シェーダが必要とするリソースが多すぎる、選択されたコンパイル ターゲットでサポートされていない、動的分岐のような何らかの能力が必要であるなどの理由が考えられます。たとえば、特定のテクスチャ マップに 6 回アクセスする HLSL シェーダを作成したとします。このシェーダを ps_1_1 コンパイル ターゲット用にコンパイルすると、ps_1_1 モデルは 4 つのテクスチャしかサポートしていないため、コンパイルは失敗します。コンパイルの失敗のもう1つの一般的な原因は、選択されたコンパイル ターゲットの最大命令数を超えたというものです。つまり、HLSL で表現されたアルゴリズムを特定のコンパイル ターゲットで実行した場合、命令の数が多くなりすぎるということです。

選択されるコンパイル ターゲットによって、シェーダ作成者が使える HLSL 構文への制約はないという点に注意してください。たとえば、シェーダ作成者は 'for' ループ、サブルーチン、'if-else' 文などを使用し、ループ、分岐、または 'if-else' 文をネイティブにサポートしていないターゲット用にコンパイルできます。このような場合、コンパイラはループを展開し、関数呼び出しをインライン展開し、'if-else' 文の両方の分岐を実行して、'if-else' 文で使われている値に基づいて正しい結果を選択します。もちろん、結果として得られたシェーダが長くなりすぎたり、コンパイル ターゲットのリソースの上限を超えたりした場合には、コンパイルは失敗します。

コマンドライン コンパイラ: FXC

多くの開発者は、アプリケーションのロード時または最初の使用時に、HLSL シェーダをカスタマのマシン上で D3DX を使ってコンパイルする代わりに、シェーダを出荷する前に HLSL からバイナリ asm にコンパイルします。これにより、HLSL のソースを他人に見られずに済みますし、アプリケーションが実行するすべてのシェーダを社内の品質保証プロセスでチェックできます。開発者がシェーダをオフラインでコンパイルするために利用できる便利なユーティリティが、DirectX 9.0 SDK に含まれている fxc コマンドライン コンパイラです。このユーティリティは、シェーダをコマンドラインでコンパイルできるだけでなく、指定されたコンパイル ターゲットの逆アセンブル コードを生成できる多数の便利なオプションを持っています。逆アセンブルされた出力は、シェーダを最適化したい場合や、仮想シェーダ マシンの能力を詳しく調べたいときなど、開発時に非常に役立ちます。これらのコマンドライン オプションを表 1 に示します。

表 1. FXC のコマンドライン オプション

-T target コンパイル ターゲット (既定値: vs_2_0)
-E name エントリ ポイント (既定値: main)
-Od 最適化を無効にする
-Vd 検証を無効にする
-Zi デバッグ情報を有効にする
-Zpr 行列を行優先でパックする
-Zpc 行列を列優先でパックする
-Fo file オブジェクト ファイルを出力する
-Fc file 生成されたコードのリストを出力する
-Fh file 生成されたコードを含んだヘッダーを出力する
-D id=text マクロを定義する
-nologo 著作権メッセージを表示しない

これで、シェーダ開発において HLSL コンパイラがどのように使われるかがわかったので、次は言語の実際の仕組みについて解説します。コンパイル ターゲットの概念と、下位のアセンブリ シェーダ モデルのさまざまな能力を念頭に置きながら読み進めるようにしてください。

言語の基本事項

HLSL の頂点シェーダとピクセル シェーダの大まかな形と、それらが下位にあるアセンブリ シェーダとどのような関係にあるかがわかったので、次は言語そのものについてに詳しく説明します。

キーワード

キーワードは HLSL 言語用に予約されている定義済みの識別子であり、プログラム内で識別子として使用することはできません。'*' の付いたキーワードは、大文字小文字を区別しません。

表 2. HLSL 言語用に予約されているキーワード

asm* bool compile const
decl* do double else
extern false float for
half if in inline
inout int matrix* out
pass* pixelshader* return sampler
shared static string* struct
technique* texture* true typedef
uniform vector* vertexshader* void
volatile while    

以下のキーワードは、現在は使われていませんが、将来のために予約されています。

表 3. 現在使われていないが、予約済みのキーワード

auto break compile const
char class case catch
default delete const_cast continue
explicit friend dynamic_cast enum
mutable namespace goto long
private protected new operator
reinterpret_cast short public register
static_cast switch signed sizeof
throw try template this
typename unsigned using union
virtual      

データ型

HLSL は、単純なスカラーから、ベクトルや行列などの複雑な型まで、さまざまなデータ型をサポートしています。

スカラー型

言語は以下のスカラー データ型をサポートしています。

表 4. スカラー データ型

bool true または false
int 32ビット符号付き整数
half 16ビット浮動小数点値
float 32ビット浮動小数点値
double 64ビット浮動小数点

アセンブリ レベルのプログラミング モデルについての知識がある人には周知のことですが、グラフィックス プロセッサは、現時点でこれらのすべてのデータ型をネイティブにはサポートしていません。そのため、整数は浮動小数点ハードウェアを使ってエミュレートしなくてはならない場合があります。つまり、これらのプラットフォーム上で浮動小数点として表現できる整数範囲を超える整数演算は、期待どおりに動作することは保証されません。さらに、すべてのターゲット プラットフォームが half または double の値をネイティブにはサポートしているわけではありません。ターゲット プラットフォームがネイティブにサポートしていない場合、これらのデータ型は float を使ってエミュレートされます。

ベクトル型

HLSL シェーダではベクトル変数の宣言をよく行うことになります。これらのベクトルを宣言する方法には、以下のものを含めてさまざまなものがあります。

表 5. ベクトル型

vector 4 次元のベクトル。個々の要素は float 型です。
vector < type, size > size 次元のベクトル。個々の要素は type スカラー型です。

しかし、シェーダ作成者がベクトルを宣言するときには、型の名前の後に、2 ~ 4 の整数を付けるという方法が最もよく使われます。たとえば、4 成分の float を宣言するときには、次に示す任意のベクトル宣言が使えます。

float4 fVector0;
float  fVector1[4];
vector fVector2;
vector <float, 4> fVector3;

3 成分の bool を宣言するときには、次に示す任意の宣言が使えます。

bool3 bVector0;
bool  bVector1[3];
vector <bool, 3> bVector2;

ベクトルを定義したら、配列アクセス構文または入れ替えを使って個々の成分にアクセスできます。入れ替えのケースでは、成分は {x. y, z,w} または {r, g, b, a} 名前空間に属していなくてはなりません(両方を同時に使うことはできません)。次に例を示します。

float4 pos = {3.0f, 5.0f, 2.0f, 1.0f};
float  value0 = pos[0]; // value0 は 3.0f
float  value1 = pos.x;  // value1 は 3.0f
float  value2 = pos.g;  // value2 は 5.0f
float2 vec0   = pos.xy; // vec0 は {3.0f, 5.0f}
float2 vec1   = pos.ry; // 入れ替えが不正であるため無効

ps_2_0 とそれ以前のピクセル シェーダ モデルは、恣意的な入れ替えをネイティブにはサポートしていないので注意してください。このため、入れ替えを使っている短い HLSL のコードは、これらのターゲット用にコンパイルすると、かなり汚いバイナリ asm になります。これらのアセンブリ モデルで利用可能なネイティブの入れ替えについて研究するようにしてください。

行列型

HLSL シェーダでよく使うことになるもう 1 つの一般的な変数型は、データの 2 次元配列である行列です。色やベクトルと同様に、行列は bool、int、hal、float、double の任意の基本データ型から構成できます。行列は任意のサイズを持つことができますが、通常、シェーダ作成者は 4 行 x 4 列までの行列をよく使います。この章の冒頭に示したサンプルの頂点シェーダでは、2 つの 4 x 4 浮動小数点行列をグローバル スコープで宣言していました。

float4x4 view_proj_matrix;
float4x4 texture_matrix0;

もちろん、行列ではこれ以外の次元も使用できます。たとえば、3 行 x 4 列の浮動小数点行列は、次のように宣言できます。

float3x4            mat0;
matrix<float, 3, 4> mat1;

ベクトルと同様に、行列の個々の成分は、配列や構造/入れ替えの構文を使ってアクセスできます。たとえば、次の配列インデックス構文を使用すると、行列 view_proj_matrix の左上の成分にアクセスできます。

float fValue = view_proj_matrix[0][0];

また、行列成分のアクセスと入れ替えのための構造体構文も定義されています。ゼロを基点とした行 - 列の位置は、次のように指定します。

_m00, _m01, _m02, _m03
_m10, _m11, _m12, _m13
_m20, _m21, _m22, _m23
_m30, _m31, _m32, _m33

1 を基点とした行 - 列の位置は、次のように指定します。

_11, _12, _13, _14
_21, _22, _23, _24
_31, _32, _33, _34
_41, _42, _43, _44

行列へのアクセスには、配列表記も使えます。次に例を示します。

float2x2 fMat = {3.0f, 5.0f,  // row 1
                 2.0f, 1.0f}; // row 2

float  value0 = fMat[0];      // value0 is 3.0f
float  value1 = fMat._m00;    // value1 is 3.0f
float  value2 = fMat._12      // value2 is 5.0f
float  value3 = fMat[1][1]    // value3 is 1.0f
float2 vec0   = fMat._21_22;  // vec0 is {2.0f, 1.0f}
float2 vec1   = fMat[1];      // vec1 is {2.0f, 1.0f}

型修飾子

HLSL には、シェーダ内で使えるオプションの型修飾子が 2 つあります。おなじみの const 型修飾子は、シェーダ コードによって値を変更できない変数を指定するために使います。このような変数を代入文の左辺で(つまり左辺値として)使うと、コンパイル エラーが発生します。

row_major および col_major 型修飾子は、ハードウェア定数ストアの中での行列のレイアウトを指定するために使います。col_major型修飾子は、行列の個々の行が単一の定数レジスタに格納されることを示します。同様に、col_major を使用すると、行列の個々の列が単一の定数レジスタに格納されます。デフォルトは col_major です。

記憶クラス修飾子

記憶クラス修飾子は、コンパイラに対して、特定の変数のスコープと寿命に関する情報を伝えます。これらの修飾子はオプションであり、変数型の前でない限り、任意の順序で指定できます。

C と同様に、変数は static または extern として宣言できます (この 2 つの修飾子は同時には指定できません)。グローバル スコープでは、static 記憶クラス修飾子は、その変数がシェーダによってのみアクセスされ、アプリケーションが API を介してアクセスすることはないことを指定します。グローバル スコープで宣言されてない変数は、アプリケーションが API を介して変更できます。C と同様に、ローカル スコープでの static 修飾子は、変数に含まれているデータが、宣言元の関数の呼び出しの前後で保存されることを示します。

extern 修飾子をグローバル変数に対して使うと、その変数はシェーダの外から API を介して変更できるようになります。ただし、これはグローバル スコープで宣言された変数のデフォルトの動作なので、冗長です。

shared 修飾子は、特定のグローバル変数がエフェクト間で共有されることを指定するために使います。

uniform の変数は、HLSL シェーダの外部で (Set*ShaderConstant*() API を通して) 設定されていると仮定します。グローバル変数は uniform として宣言されているとして扱われます。これらの変数は、シェーダ内で値が変更される可能性があるので、const と仮定することはできません。

例として、グローバル スコープで以下の変数を宣言したとします。

extern float translucencyCoeff;
const  float gloss_bias;
static float gloss_scale;
float diffuse;

変数 diffusetranslucencyCoeffSet*ShaderConstant*() API から設定でき、シェーダ自身によっても変更できます。const 変数 gloss_biasSet*ShaderConstant*() API から設定できますが、シェーダ コード内では変更できません。最後に、static 変数 gloss_scaleSet*ShaderConstant*() API から設定できず、シェーダ内でのみ変更できます。

初期化子

これまでのいくつかの例で見たように、C と同じように、宣言時に変数を初期化できます。次に例を示します。

float2x2 fMat = {3.0f, 5.0f,  // 行 1
                 2.0f, 1.0f}; // 行 2
float4   vPos = {3.0f, 5.0f, 2.0f, 1.0f};
float fFactor = 0.2f;

ベクトルの使用

HLSL では、ベクトルの計算を行うときに、いくつかの落とし穴に注意する必要があります。幸いなことに、そのほとんどは、3D グラフィックス用のシェーダを作成しているという点から明らかです。たとえば、標準の 2 項演算子は、成分ごとに作用するものとして定義されています。

float4 vTone = vBrightness * vExposure;

vBrightnessvExposure がどちらも float4 型だった場合、これは以下のもの等価です。

float4 vTone;
vTone.x = vBrightness.x * vExposure.x;
vTone.y = vBrightness.y * vExposure.y;
vTone.z = vBrightness.z * vExposure.z;
vTone.w = vBrightness.w * vExposure.w;

これは、4D ベクトルの vBrightnessvExposure の内積ではないことに注意してください。また、この方法で行列変数の乗算を行っても、行列の乗算が行われるわけではありません。内積や行列の乗算は、後に説明する組み込み関数 mul(), によって行います。

コンストラクタ

HLSL シェーダでよく使うもう 1 つの言語機能がコンストラクタです。これは C++ のコンストラクタに似ていますが、複合的なデータ型を扱うための拡張が施されています。次にコンストラクタの例を示します。

float3   vPos     = float3(4.0f, 1.0f, 2.0f);
float    fDiffuse = dot(vNormal, float3(1.0f, 0.0f, 0.0f));
float4   vPack    = float4(vPos, fDiffuse);

通常、コンストラクタは、シェーダ作成者がリテラル値を持つ数量を一時的に定義したい場合 (上の dot(vNormal, float3(1.0f, 0.0f, 0.0f))) や、小さなデータ型を明示的にまとめてパックしたい場合 (上の float4(vPos, fDiffuse)) に使います。この例では、float4 コンストラクタは float3float を引数として取り、これらのデータをまとめてパックした float4 を返します。

型キャスト

シェーダ作成と生成されるコードの効率を高めるために HLSL の型キャストの動作を理解しておくのはいいことです。型のキャストは、ある変数を、その代入先の変数に合わせるためにプロモートまたはデモートするためによく使います。たとえば次の例では、リテラルの float 0.0f が、vResult を初期化するために float4 {0.0f , 0.0f , 0.0f , 0.0f } にキャストされています。

float4   vResult = 0.0f;

ベクトルや行列などの次元の大きいデータ型を、より次元の小さいデータ型に代入するときにも、これと似たキャストが行われます。この場合には、余ったデータは実質的に省略されます。例として、次のコードを考えてみましょう。

float3   vLight;
float    fFinal, fColor;
fFinal = vLight * fColor;

この例では、vLight は、スカラーの float である fColor との乗算における最初の成分のみを使って float にキャストされます。この例では、fFinalvLight.x * fColor と等しくなります。

表 6 に示した HLSL の型キャストの規則をよく理解しておくことをお勧めします。

表 6. HLSL の型キャストの規則

スカラーからスカラーへ つねに有効。 bool 型から整数または浮動小数点型にキャストするとき、false は 0、true は 1 とみなされます。整数または浮動小数点型から bool にキャストするとき、0 の値は false、0 以外の値は true とみなされます。浮動小数点型から整数型にキャストするとき、値は 0 に向けて丸められます。この切り捨ての動作は C と同じです。
スカラーからベクトルへ つねに有効。 キャストは、スカラーを複製してベクトルの全成分に書き込むことによって行われます。
スカラーから行列へ つねに有効。 キャストは、スカラーを複製して行列の全成分に書き込むことによって行われます。
スカラーから構造体へ このキャストは、スカラーを複製して構造体の全成分に書き込むことによって行われます。
ベクトルからスカラーへ つねに有効。 ベクトルの最初の成分を選択します。
ベクトルからベクトルへ キャスト先のベクトルは、キャスト元のベクトルよりも大きくてはなりません。キャストは左側の値を残し、残りの値を切り捨てることによって行われます。このキャストにおいては、列行列、行行列、および数値構造体はベクトルとして扱われます。
ベクトルから行列へ ベクトルのサイズは、行列のサイズと等しくなくてはなりません。
ベクトルから構造体へ 構造体がベクトルよりも大きくなく、構造体のすべての成分が数値である場合に有効。
行列からスカラーへ つねに有効。 行列の左上の成分が選択されます。
行列からベクトルへ 行列のサイズは、ベクトルのサイズと等しくなくてはなりません。
行列から行列へ キャスト先の行列は、どの次元においてもキャスト元の行列よりも大きくてはなりません。キャストは左側の値を残し、残りの値を切り捨てることによって行われます。
行列から構造体へ 構造体のサイズは行列のサイズと等しくなくてはならず、構造体のすべての成分は数値でなくてはなりません。
構造体からスカラーへ 構造体は少なくとも 1 つのメンバを含んでいなくてはなりません。
構造体からベクトルへ 構造体はベクトルのサイズ以上でなくてはなりません。最初の成分からベクトルのサイズまでの要素は数値でなくてはなりません。
構造体から行列へ 構造体は行列のサイズ以上でなくてはなりません。最初の成分から行列のサイズまでの成分は数値でなくてはなりません。
構造体からオブジェクトへ 構造体は少なくとも 1 つのメンバを含んでいなくてはなりません。このメンバの型は、オブジェクトの型と同じでなくてはなりません。
構造体から構造体へ キャスト先の構造体は、キャスト元の構造体よりも大きくてはなりません。キャスト元とキャスト先のすべての成分の間に有効なキャストが存在している必要があります。

構造体

上の最初のシェーダの例からわかるように、HLSL シェーダでは構造体を定義すると便利なことがあります。たとえば、多くのシェーダ作成者は、頂点シェーダのコードで出力構造体を定義し、この構造体を頂点シェーダの main 関数からの戻り型として使用します (ピクセル シェーダでは、ほとんどのピクセル シェーダは 1 つの float4 出力を持つので、この方法はあまり使われません)。次に示すのは、後で説明する NPR Metallic シェーダで使われている構造体です。

struct VS_OUTPUT
{
   float4 Pos   : POSITION;
   float3 View  : TEXCOORD0;
   float3 Normal: TEXCOORD1;
   float3 Light1: TEXCOORD2;
   float3 Light2: TEXCOORD3;
   float3 Light3: TEXCOORD4;
};

HLSL シェーダの中で、一般的な用途の構造体を宣言することもできます。構造体は上記の型キャストの規則に従います。

サンプラ

ピクセル シェーダの中でサンプリングを行おうとしている個々のテクスチャ マップについて、サンプラを宣言する必要があります。前に示した hlsl_rings() シェーダを思い出してください。

float4 lightWood; // xyz == 木目の明るい色
float4 darkWood;  // xyz == 木目の明るい色
float  ringFreq;  // リングの頻度

sampler PulseTrainSampler;

float4 hlsl_rings (float4 Pshade : TEXCOORD0) : COLOR
{
    float scaledDistFromZAxis = sqrt(dot(Pshade.xy, Pshade.xy)) * ringFreq;

    float blendFactor = tex1D (PulseTrainSampler, scaledDistFromZAxis);
 
    return lerp (darkWood, lightWood, blendFactor);
}

このシェーダでは、グローバル スコープで PulseTrainSampler という名前のサンプラを宣言し、これを tex1D() 組み込み関数の第 1 引数として渡しています (組み込み関数については次のセクションで説明します)。HLSL サンプラは、サンプラの API のコンセプトと直接の対応関係を持っており、ひいては 3D グラフィックス プロセッサの中のテクスチャのアドレシングとフィルタリングを行う実際のシリコンとの対応関係を持っています。サンプラは、特定のシェーダの中でアクセスする予定があるすべてのテクスチャ マップについて定義する必要がありますが、同じサンプラを 1 つのシェーダの中で複数回使えます。これは、ShaderX2 - Shader Tips & Tricks の章 "Advanced Image Processing with DirectX 9 Pixel Shaders" で説明しているように、画像処理アプリケーションでは頻繁に行われます。一般に入力画像は、シェーダ コードで表現されているフィルタ カーネルにデータを提供するために、異なるテクスチャ座標で何度もサンプリングされるからです。たとえば、次のシェーダはラスタライザを使って、Sobel フィルタのペアを適用して高さマップを法線マップに変換しています。

sampler InputImage;

float4 main( float2 topLeft    : TEXCOORD0, float2 left        : TEXCOORD1,
             float2 bottomLeft : TEXCOORD2, float2 top         : TEXCOORD3,
             float2 bottom     : TEXCOORD4, float2 topRight    : TEXCOORD5,
             float2 right      : TEXCOORD6, float2 bottomRight : TEXCOORD7): COLOR
{
   // 8 つのタップをすべて取得
   float4 tl = tex2D (InputImage, topLeft);
   float4  l = tex2D (InputImage, left);
   float4 bl = tex2D (InputImage, bottomLeft);
   float4  t = tex2D (InputImage, top);
   float4  b = tex2D (InputImage, bottom);
   float4 tr = tex2D (InputImage, topRight);
   float4  r = tex2D (InputImage, right);
   float4 br = tex2D (InputImage, bottomRight);

   // Sobel 演算子を使って dx を計算
   //
   //           -1 0 1 
   //           -2 0 2
   //           -1 0 1
   float dX = -tl.a - 2.0f*l.a - bl.a + tr.a + 2.0f*r.a + br.a;

   // Sobel 演算子を使って dy を計算
   //
   //           -1 -2 -1 
   //            0  0  0
   //            1  2  1
   float dY = -tl.a - 2.0f*t.a - tr.a + bl.a + 2.0f*b.a + br.a;

   // 外積を計算し、再正規化を行う
   float4 N = float4(normalize(float3(-dX, -dY, 1)), tl.a);

   // 符号付きの値を -1..1 の範囲から 0..1 の範囲に変換し、その値を返す
   return N * 0.5f + 0.5f;
}

このシェーダは InputImage という 1 つのサンプラしか使っていませんが、tex2D() 組み込み関数を使って 8 回サンプリングを行っています。

組み込み関数

これまでのセクションで述べたように、DirectX 9 High Level Shading Language には多数の便利な組み込み関数が用意されています。数値演算関数を初めとする多くの組み込み関数は、開発者の便宜のために用意されているものですが、前述の tex1D()tex2D() のように、サンプラを介してテクスチャにアクセスするために必要なものもあります。

数値演算組み込み関数

次の表 7 に示している数値演算組み込み関数は、HLSL コンパイラによってマイクロ演算に変換されます。abs()dot() などの組み込み関数は 1 つのアセンブリ レベル演算に直接マップされますが、refract()step() などのケースでは、複数のアセンブリ命令にマップされます。ddx()ddy()fwidth() のように、一部のコンパイル ターゲットではサポートされていないケースもあります。次に数値演算組み込み関数を示します。

表 7. 数値演算組み込み関数

abs(x) 絶対値 (成分ごと)。
acos(x) x の個々の成分の逆余弦を返します。個々の要素は [-1, 1] の範囲になくてはなりません。
all(x) x のすべての成分が 0 以外の値であるかどうかをテストします。
any(x) x のいずれかの成分が 0 以外の値であるかどうかをテストします。
asin(x) x の個々の要素の逆正弦を返します。個々の成分は [-pi/2, pi/2] の範囲になくてはなりません。
atan(x) x の逆正接を返します。戻り値は [-pi/2, pi/2] の範囲です。
atan2(y, x) y/x の逆正接を返します。yx の符号は、戻り値の [-pi, pi] の範囲での象限を決定するために使います。atan2 は、x が 0 で y が 0 でない場合でも、原点以外のすべての点において定義されています。
ceil(x) x よりも大きい、または x と等しい最も小さい整数を返します。
clamp(x, min, max) x を [min, max] の範囲にクランプします。
clip(x) x のいずれかの要素が 0 よりも小さい値であれば、現在のピクセルを破棄します。これは、x の個々の要素が平面からの距離を表している場合に、クリップ平面をシミュレートするために使えます。この組み込み関数は、texkill を生成したいときに使います。
cos(x) x の余弦を返します。
cosh(x) x の双曲線余弦を返します。
cross(a, b) 2 つの 3D ベクトル、ab の外積を返します。
D3DCOLORtoUBYTE4(x) UBYTE4 ストリーム コンポーネントをサポートしていない一部のハードウェアに対応するために、4D ベクトル x 成分を入れ替えし、スケーリングします。
ddx(x) スクリーン空間の x 座標に対する、x の偏微分係数を返します。
ddy(x) スクリーン空間の y 座標に対する、x の偏微分係数を返します。
degrees(x) x をラジアン単位から度単位に変換します。
determinant(m) 正方行列 m の行列式を返します。
distance(a, b) 2 つの点、ab の間の距離を返します。
dot(a, b) 2 つのベクトル、ab の外積を返します。
exp(x) e を底とする指数 ex を返します。
exp2(a) 2 を底とする指数 (成分ごと)。
faceforward(n, i, ng) -n * sign(dot(i, ng)) を返します。
floor(x) x よりも小さい、または x と等しい最も大きい整数を返します。
fmod(a, b) a / b の浮動小数点の剰余 f を返します。i を整数として、a = i * b + f となります。fx と同じ符号を持ち、a の絶対値は b の絶対値よりも小さくなります。
frac(x) x の小数部 f を返します。fは 0 以上、1 未満の値です。
frexp(x, out exp) x の仮数部と指数部を返します。frexp は仮数部を返し、指数部は出力パラメータ exp に格納されます。x が 0 の場合、この関数は仮数部と指数部の両方について 0 を返します。
fwidth(x) abs(ddx(x))+abs(ddy(x)) を返します。
isfinite(x) xが有限の場合は true を、そうでない場合は false を返します。
isinf(x) x が +INF または -INF の場合は true を、そうでない場合は false を返します。
isnan(x) x が NAN または QNAN の場合は true を、そうでない場合は false を返します。
ldexp(x, exp) x * 2exp を返します。
len(v) ベクトル長。
length(v) ベクトル v の長さを返します。
lerp(a, b, s) a + s(b - a) を返します。ab の間で線形補間を行います。つまり戻り値は、s が 0 の場合には as が 1 の場合には b となります。
log(x) x の e を底とする対数を返します。x が負の場合、関数は不定値を返します。x が 0 の場合、関数は +INF を返します。
log10(x) x の 10 を底とする対数を返します。x が負の場合、関数は不定値を返します。x が 0 の場合、関数は +INF を返します。
log2(x) x の 2 を底とする対数を返します。x が負の場合、関数は不定値を返します。x が 0 の場合、関数は +INF を返します。
max(a, b) ab のうちの大きい方の値を選択します。
min(a, b) ab のうちの小さい方の値を選択します。
modf(x, out ip) x を、それぞれ x と同じ符号を持つ小数部と整数部に分割します。x の符号付きの小数部が返されます。整数部は出力引数 ip に格納されます。
mul(a, b) ab の間で行列乗算を実行します。a がベクトルだった場合には、行ベクトルとして扱われます。b がベクトルだった場合には、列ベクトルとして扱われます。内側の次元 acolumnsbrows は等しくなくてはなりません。結果の次元は arows x bcolumns となります。
normalize(v) 正規化されたベクトル v / length(v) を返します。v の長さが 0 だった場合、結果は不定です。
pow(x, y) xy を返します。
radians(x) x を度単位からラジアン単位に変換します。
reflect(i, n) 入射光の向きを i、表面の法線を n としたときの反射ベク v を返します。v = i - 2 * dot(i, n) * n となります。
refract(i, n, eta) 入射光の向きを i、表面の法線を v、屈折の相対インデックスを eta としたときの反射ベクトル v を返します。in の間の角度が指定された eta にとって大きすぎる場合、refract は (0,0,0) を返します。
round(x) x を最も近い整数に丸めます。
rsqrt(x) 1 / sqrt(x)を返します。
saturate(x) x を範囲 [0, 1] にクランプします。
sign(x) x の符号を計算します。x が 0 未満の場合は -1、0 に等しい場合には 0、0 よりも大きい場合は 1 を返します。
sin(x) x の正弦を返します。
sincos(x, out s, out c) x の正弦と余弦を返します。sin(x) は出力パラメータ s に格納されます。cos(x) は出力パラメータ c に格納されます。
sinh(x) x の双曲線正弦を返します。
smoothstep(min, max, x) x < min の場合は 0 を返します。x > max の場合は 1 を返します。x が [min, max] の範囲ならば、0 と 1 の間のスムーズ エルミート補間を返します。
sqrt(x) 平方根 (成分ごと)。
step(a, x) (x = a) ? 1 : 0 を返します。
tan(x) x の正接を返します。
tanh(x) x の双曲線正接を返します。
transpose(m) 行列 m の転置行列を返します。ソースの次元が mrows × mcolumns である場合、結果の次元は mcolumns × mrows となります。

テクスチャ サンプリング組み込み関数

テクスチャ データのシェーダへのサンプリングには、16 のテクスチャ サンプリング組み込み関数が使えます。4 種類のテクスチャ (1D、2D、3D、キューブ マップ) と 4 種類のロード (通常、微分係数、射影、バイアス) があり、この 16 の組み合わせそれぞれに 1 つの組み込み関数が用意されています。

表 8. テクスチャ サンプリング組み込み関数

tex1D(s, t) 1D テクスチャ参照。s はサンプラです。t はスカラーです。
tex1D(s, t, ddx, ddy) 微分係数付きの 1D テクスチャ参照。s はサンプラです。t、ddx、ddy はスカラーです。
tex1Dproj(s, t) 1D 射影テクスチャ参照。s はサンプラです。t は 4D ベクトルです。t は、参照が行われる前に、その最後の成分による除算が行われます。
tex1Dbias(s, t) 1D バイアス テクスチャ参照。 s はサンプラです。t は 4Dベクトルです。ルックアップが行われる前に、mip レベルが t.w だけ補正されます。
tex2D(s, t) 2D テクスチャ参照。 s はサンプラです。 t は 2D テクスチャ座標です。
tex2D(s, t, ddx, ddy) 2D テクスチャ参照。 s はサンプラです。t は 2D テクスチャ座標です。
tex2Dproj(s, t) 微分係数付きの 2D テクスチャ参照。 s はサンプラです。t、ddx、ddy は 2D ベクトルです。
tex2Dbias(s, t) 2D 射影テクスチャ参照。 2D 射影テクスチャ参照。
tex3D(s, t) 3D ボリューム テクスチャ参照。 s はサンプラです。t は 3D テクスチャ座標です。
tex3D(s, t, ddx, ddy) 微分係数付きの 3D ボリューム テクスチャ参照。 s はサンプラです。t、ddx、ddy は 3D ベクトルです。
tex3Dproj(s, t) 3D 射影ボリューム テクスチャ参照。 s はサンプラです。t は 4D ベクトルです。t は、参照が行われる前に、その最後の成分による除算が行われます。
tex3Dbias(s, t) 3D バイアス テクスチャ参照。 s はサンプラです。t は 4D ベクトルです。参照が行われる前に、mip レベルが t.w だけ補正されます。
texCUBE(s, t) キューブマップ参照。 キューブマップ参照。s はサンプラです。t は 3D テクスチャ座標です。
texCUBE(s, t, ddx, ddy) 微分係数付きのキューブマップ参照。 s はサンプラです。t、ddx、ddy は 3D ベクトルです。
texCUBEproj(s, t) 射影キューブマップ参照。 s はサンプラです。t は 4D ベクトルです。t は、参照が行われる前に、その最後の成分による除算が行われます。
texCUBEbias(s, t) バイアス キューブマップ参照。 s はサンプラです。t は 4D ベクトルです。参照が行われる前に、mip レベルが t.w だけ補正されます。

tex1D()tex2D()tex3D()texCUBE() 組み込み関数は、テクスチャのサンプリングに最もよく使われる組み込み関数です。ddx および ddy 引数を取るテクスチャ ロード組み込み関数は、これらの明示的な微分係数を使ってテクスチャ LOD を計算します。通常、これらの微分形式は、その前に ddx() および ddy() 数値演算組み込み関数を使って計算します。これらは手続き的なピクセル シェーダを作成するときに特に重要ですが、ps_2_0 とそれ以前のコンパイル ターゲットではサポートされていません。

tex*proj() 組み込み関数は、射影テクスチャの読み込みを行うために使います。テクスチャのサンプリングに使うテクスチャ座標は、テクスチャにアクセスする前に、最後の成分による除算が行われます。これらのうち、tex2Dproj() は、射影シャドウ マップやこれに似たエフェクトに必要なため、最も多く使われます。

tex*bias() 組み込み関数は、バイアスされたテクスチャ サンプリングに使います。バイアスはピクセル レベルで計算できます。これは通常は、特殊なエフェクトのためのテクスチャに「ぼかし」の効果を追加するために使います。たとえば、ShaderX2 - Shader Tips & Tricks の章 "Motion Blur Using Geometry and Shading Distortion" で説明しているように、RADEON 9700 Animusic Pipe Dream デモの、ボールの動きに「ぼかし」を追加しているピクセル シェーダは、texCUBEbias() 組み込み関数を使って、ローカル シーンのキューブ環境マップにアクセスしています。

   // 反射を外延の量だけぼかす
   
   float3 vCubeLookup = vReflection + i.Pos/fEnvMapRadius;
   float4 cReflection = texCUBEbias(tCubeEnv, 
float4(vCubeLookup, fBlur * fTextureBlur)) * vReflectionColor;

このコード例では、fBlur * fTextureBlur 呼び出しで使うテクスチャ座標の第 4 成分に格納されており、キューブ マップにアクセスするときに使うバイアスを決定します。

これで、言語のいくつかの仕組みについての説明は終わりです。次は、DirectX 9 の HLSL シェーダで、データの入力と出力がどのように行われるかを説明します。

シェーダ入力

頂点シェーダとピクセル シェーダは、可変(Varying)入力均一(Uniform)入力の 2 種類の入力データを取ります。可変入力は、シェーダの個々の実行に固有のデータです。頂点シェーダの場合、可変データ(位置、法線など)は頂点ストリームから入力されます。均一データ(マテリアル色、ワールド変換行列など)は、シェーダの複数の実行で同じです。アセンブリ モデルを知っている人は、均一データは定数レジスタで、可変データは頂点およびピクセル シェーダの 'v'/'t' レジスタで指定されると言えばわかるでしょう。

均一入力

均一データは、HLSL では 2 つの方法で指定できます。最も一般的な方法は、グローバル変数を宣言し、それらを頂点シェーダまたはピクセル シェーダの中で使用するという方法です。シェーダ内でグローバル変数を使うとき、その変数は、シェーダが必要とする均一変数のリストに追加されます。第 2 の方法は、トップレベル シェーダ関数の入力引数を均一変数としてマークするという方法です。これには、指定された変数を、シェーダが使用する均一変数のリストに追加するように指定する効果があります。次のコード例では、この 2 つの方法を示しています。

// グローバルな均一変数を宣言する 
// 定数テーブルには 'UniformGlobal' という名前で追加される
float4 UniformGlobal;

// 均一入力パラメータを宣言する
// 定数テーブルには '$UniformParam' という名前で追加される 
float4 main( uniform float4 UniformParam ) : POSITION
{
   return UniformGlobal * UniformParam;
}

シェーダが使う均一変数は、定数を通してアプリケーションに通知されます。定数テーブルは、シェーダが使う均一変数が、グローバル変数の前にどのように定数レジスタにロードされなくてはならないかを定義するシンボル テーブルです(注: 均一入力関数引数は、グローバル変数とは異なり、'$' を前に付けて定数テーブルに追加されます。'$' が必要なのは、「ローカル」な均一入力と、同じ名前のグローバル変数の間での名前の衝突を避けるためです)。

定数テーブルは、シェーダが使うすべての均一変数の定数レジスタ位置を含んでいます。また、このテーブルには、個々の定数テーブル エントリについて、型情報とデフォルト値も含まれています(指定されていた場合)。次に、定数テーブルを印刷したときの例を示します。コンパイラが生成する定数テーブルはコンパクトな 2 進形式で格納されています。このテーブルを実行時に解釈する API については、D3DX エフェクトを使わない HLSL 統合のセクションで説明します。

サンプル シェーダに対して fxc.exe が生成する定数テーブルのテキスト出力:

//
// Generated by Microsoft (R) D3DX9 Shader Compiler
//
//  Source: hemisphere.fx
//  Flags: /E:VS /T:vs_1_1
//
// Registers:
//
//     Name         Reg   Size
//     ------------ ----- ----
//     Projection   c0       4
//     WorldView    c4       3
//     DirFromLight c7       1
//     DirFromSky   c8       1
//     $bHemi       c18      1
//     $bDiff       c19      1
//     $bSpec       c20      1
//
//
// Default values:
//
//     DirFromLight
//       c7   = { 0.577, -0.577, 0.577, 0 };
//
//     DirFromSky
//       c8   = { 0, -1, 0, 0 };

可変入力

可変入力は、トップレベル シェーダ関数の入力引数に入力セマンティックを付けることによって指定します。すべてのトップレベル シェーダ入力は、セマンティックを介して可変入力としてマークするか、その値がシェーダの実行中は一定であることを示す 'uniform' のマークを付けなくてはなりません。トップレベル シェーダ入力がセマンティックまたは 'uniform' キーワードでマークされていなかった場合、シェーダのコンパイルは失敗します。

入力セマンティックは、特定のシェーダ入力を、グラフィックス パイプラインの前のステージの出力にリンクするために使う名前です。たとえば、入力セマンティック POSITION0 は、頂点シェーダが、頂点バッファからの位置データをどこにリンクするかを指定するために使います。

ピクセル シェーダと頂点シェーダは、個々のシェーダ ユニットに入力を提供するグラフィックス パイプライン内の場所が異なるため、異なる入力セマンティックのセットを持っています。頂点シェーダの入力セマンティックは、頂点バッファから、頂点シェーダが利用できる形式にロードされる頂点ごとの情報を記述します (位置、法線、テクスチャ座標、色、接線、従法線など)。これらの入力セマンティックは、頂点バッファ内で頂点データ要素を記述するために使う D3DDECLUSAGE 列挙型と UsageIndex の組み合わせに直接マップされます。

ピクセル シェーダの入力セマンティックは、ラスタ化ユニットがピクセルごとに提供する情報を記述します。このデータは、現在のプリミティブの頂点ごとの頂点シェーダの出力の間で補間を行うことによって生成します。基本的なピクセル シェーダの入力セマンティックは、入力色とテクスチャ座標の情報を入力パラメータにリンクします。

入力セマンティックをシェーダ入力に割り当てるには、2 つの方法があります。第 1 の方法では、コロン ':' と入力セマンティック名を入力パラメータ宣言に追加します。第 2 の方法では、個々の要素に入力セマンティックが代入された入力構造体を定義します。この章と、ShaderX の関連書籍では、両方のスタイルを使用しています。

次に、入力セマンティックの例を示します。

// セマンティック バインディングのための入力構造体を宣言する
struct InStruct
{
float4 Pos1 : POSITION1
};

// 位置データが格納される Pos 変数を宣言する
float4 main( float4 Pos : POSITION0, InStruct In ) : POSITION
{
return Pos * In.Pos1;
}

// 補間された COLOR0 値が格納される Col 変数を宣言する
float4 mainPS( float4 Col : COLOR0 ) : COLOR
{
return Col;
}  

表 9. 頂点シェーダの入力セマンティック

POSITIONn 位置
BLENDWEIGHTn ブレンドの重み
BLENDINDICESn ブレンド インデックス
NORMALn 法線ベクトル
PSIZEn ポイント サイズ
COLORn
TEXCOORDn テクスチャ座標
TANGENTn 接線
BINORMALn 従法線
TESSFACTORn テセレーション係数

表 10. ピクセル シェーダの入力セマンティック

COLORn
TEXCOORDn テクスチャ座標

n はオプションの整数です。例: PSIZE0, DIFFUSE1

シェーダ出力

頂点シェーダとピクセル シェーダは、その後のグラフィックス パイプライン ステージに対して出力データを提供します。出力セマンティックは、シェーダが生成するデータが次のステージの入力にどのようにリンクされるべきかを指定します。たとえば、頂点シェーダの出力セマンティックは、出力をラスタライザにおけるインターポレータとリンクして、ピクセル シェーダのための入力データを生成するために使います。ピクセル シェーダ出力は、個々のレンダー ターゲットについてアルファ ブレンディング ユニットに提供される値か、デプス バッファに書き込まれる深度値です。

頂点シェーダの出力セマンティックは、シェーダをピクセル シェーダとラスタライザ ステージの両方にリンクするために使います。POSITION 出力は、ラスタライザが利用し、ピクセル シェーダには公開されない、個々の頂点シェーダからの必要な出力です。TEXCOORDnCOLORn は、ピクセル シェーダが補間後の処理に利用する出力を示します。

ピクセル シェーダの出力セマンティックは、ピクセル シェーダの出力色を正しいレンダ ターゲットにバインドします。ピクセル シェーダからの色の出力は、出力 レンダー ターゲットがどのように変更されるかを決定するアルファ ブレンド ステージにリンクされます。DEPTH 出力セマンティックは、現在のラスタ位置における出力深度値の変更に使えます。注: DEPTH とマルチ レンダー ターゲット ("MRT" とも呼ばれます) は、一部のシェーダ モデルでのみサポートされています。

出力セマンティックの構文は、入力セマンティックを指定するための構文と同一です。セマンティックは、'out' パラメータとして宣言されたパラメータで直接に指定するか、out パラメータとして返される構造体または関数の戻り値を定義する際に代入できます。

表 11. 頂点シェーダの出力セマンティック

POSITION 位置
PSIZE ポイント サイズ
FOG 頂点フォグ
COLORn 色 (例: COLOR0)
TEXCOORDn テクスチャ座標 (例: TEXCOORD0)

表 12. ピクセル シェーダの出力セマンティック

COLORn レンダー ターゲット n の色
DEPTH 深度値

n はオプションの整数です。例: TEXCOORD3, COLOR0

次のコード例は、HLSL シェーダからデータを出力するさまざまな方法を示しています。

// セマンティック バインディングを含んだ出力構造体を宣言する
struct OutStruct
{
float2 Tex2 : TEXCOORD2
};

// Tex0 out パラメータを、TEXCOORD0 データを含むパラメータとして宣言する
float4 main(out float2 Tex0 : TEXCOORD0, out OutStruct Out ) : POSITION
{
   Tex0 = float2(1.0, 0.0);
        Out.Tex2 = float2(0.1, 0.2);
return float4(0.5, 0.5, 0.5, 1);
}

// Col 変数を、補間された COLOR0 値を含む変数として宣言する
float4 mainPS( out float4 Col1 : COLOR1) : COLOR
{
   // out パラメータを使ってレンダ ターゲット 1 に出力する
   Col1 = float4(0.0, 0.0, 0.0, 0.0);

   // 宣言された戻り出力を使ってレンダ ターゲット 0 に出力する
return float4(1.0, 0.9722, 0.3333334, 0);
}


struct PS_OUT
{
   float4 Color: COLOR;
   float  Depth: DEPTH;
};

//
// ピクセル シェーダから出力するための 3 種類の方法
//

PS_OUT PSFunc1() { ... }

void PSFunc2(out float4 Color : COLOR,
       out float Depth : DEPTH)
{
  ...
}

void PSFunc3(out PS_OUT Out)
{
  ...
}

サンプル シェーダ

言語そのものと、この言語がグラフィックス パイプラインの他の部分と入力および出力を介してどのように接続されるかについての説明が終わったので、次は NPR Metallic という名前のサンプル シェーダを取り上げます。この名前は、図 2 に示すように、セル アニメーション スタイルでレンダリングした世界に存在する金属表面のように見えることを目的として設計されていることから来ています。このエフェクトは "Shader Programming with RenderMonkey" の章で解説している RenderMonkey シェーダ開発環境に付属しているもので、ATI Developer Relations Web サイト(www.ati.com/developer) から入手できます。

図 2. NPR Metallic

まず、HLSL で書かれた NPR Metallic 頂点シェーダを示します。

float4x4 view_proj_matrix;

float4 view_position;
float4 light0;
float4 light1;
float4 light2;

struct VS_OUTPUT
{
   float4 Pos   : POSITION;
   float3 View  : TEXCOORD0;
   float3 Normal: TEXCOORD1;
   float3 Light1: TEXCOORD2;
   float3 Light2: TEXCOORD3;
   float3 Light3: TEXCOORD4;
};

VS_OUTPUT main( float4 inPos    : POSITION,
                float3 inNorm   : NORMAL )
{
   VS_OUTPUT Out = (VS_OUTPUT) 0;

   // 変換した頂点位置を出力する 
   Out.Pos = mul( view_proj_matrix, inPos );

   Out.Normal = inNorm;

   // ビュー ベクトルを計算する 
   Out.View = normalize( view_position - inPos );

   // 現在の頂点位置から 3 つのライトへのベクトルを計算する 
   Out.Light1 = normalize (light0 - inPos);   // Light 1
   Out.Light2 = normalize (light1 - inPos);   // Light 2
   Out.Light3 = normalize (light2 - inPos);   // Light 3

   return Out;
}

この頂点シェーダの冒頭には、グローバル スコープでの行列といくつかの float の宣言があります。view_proj_matrixview_positionlight0light1light2 です。これらはいずれも、API によって外部から設定でき、シェーダそのものの中で変更可能な暗黙の均一変数です。

これらのグローバル変数の後には、main 関数の戻り型である VS_OUTPUT という名前の構造体の定義があります。つまり、この頂点シェーダは、必要とされる 4D 位置に加えて、5 つの 3D テクスチャ座標を出力します。

main 関数を見ると、この頂点シェーダは入力位置として 4D ベクトル、入力法線として 3D ベクトル、テクスチャ座標として 2D ベクトルを取ることがわかります。入力位置 inPosmul() 組み込み関数view_proj_matrix を使って変換し、法線の inNorm はそのまま出力に渡します。

最後に、オブジェクト空間の頂点位置から 3 つのライトとビュー位置への 3D ベクトルをすべて計算します。これらの 3D ベクトルは、、単位長になるように normalize() 組み込み関数に渡します。これらの正規化した 3D ベクトルは、いずれもポリゴン上で補間される 3D テクスチャ座標として頂点シェーダから出力されます。

コンパイル ターゲットとアセンブリ モデルに関する以前の議論を再確認するために、このシェーダをコンパイルし、アセンブリ出力を見てみることにしましょう。まず、上記のコードを NPRMetallic.vhl という名前のファイルに入力します。これを、fxc を使ってコマンドラインでコンパイルします。

fxc -nologo -T vs_1_1 -Fc -Vd NPRMetallic.vhl

この頂点シェーダはフロー制御を必要としないので、vs_1_1 コンパイル ターゲットを選択します。また、コード ファイルを生成し、検証を無効にするためのフラグを設定しています。次に、生成されたコード ファイルの一部を示します。

// Parameters:
//     float4 light0;
//     float4 light1;
//     float4 light2;
//     float4 view_position;
//     float4x4 view_proj_matrix;
//
// Registers:
//     Name             Reg   Size
//     ---------------- ----- ----
//     view_proj_matrix c0       4
//     view_position    c4       1
//     light1           c5       1
//     light2           c6       1
//     light0           c7       1

    vs_1_1
    dcl_position v0
    dcl_normal v1
    mul r0, v0.x, c0
    mad r2, v0.y, c1, r0
    mad r4, v0.z, c2, r2
    mad oPos, v0.w, c3, r4
    add r1, -v0, c4
    dp4 r1.w, r1, r1
    rsq r1.w, r1.w
    mul oT0.xyz, r1, r1.w
    add r8, -v0, c7
    dp4 r8.w, r8, r8
    rsq r8.w, r8.w
    mul oT2.xyz, r8, r8.w
    add r3, -v0, c5
    add r10, -v0, c6
    dp4 r3.w, r3, r3
    rsq r3.w, r3.w
    mul oT3.xyz, r3, r3.w
    dp4 r10.w, r10, r10
    rsq r10.w, r10.w
    mul oT4.xyz, r10, r10.w
    mov oT1.xyz, v1

コード ファイルの冒頭には、この頂点シェーダへのパラメータがあります。つまり、このシェーダを特定のアプリケーションの中で正しく動作させるために API を通して設定する必要のあるグローバル スコープの変数があります。次のセクションには、アセンブリ シェーダを正しく動作させるためにアプリケーションがこれらのパラメータをロードしなくてはならないハードウェア レジスタが示されています。次に、21 個のアセンブリ命令にコンパイルされたシェーダ コードそのものがあります。コード全体を説明することはしませんが、シェーダの main 関数への入力の POSITION および NORMAL セマンティックの直接の結果である dcl_position および dcl_normal 文に注目してください。さらに、最終的な結果が oPosoT0oT1oT2oT3oT4 レジスタに格納されている点にも注意してください。これは、関数の戻り型が、対応するセマンティックでタグづけられたメンバを含んでいることから引き起こされています。どうしても必要というわけではありませんが、fxc を使って HLSL からアセンブリ コードを生成する方法と、その読み方を理解しておくと、開発のいくつかの段階、特に最適な HLSL を書こうと試みるときに役に立ちます。

これで、頂点シェーダを使ってジオメトリをクリップ空間に変換し、ポリゴン上で補間される値を定義したので、次はこれらの補間されたすべての数値を使用するピクセル シェーダについて解説します。

次に NPR Metallic ピクセル シェーダを示します。

float4 Material;

sampler Outline;

float4 main( float3 View:   TEXCOORD0,
             float3 Normal: TEXCOORD1,
             float3 Light1: TEXCOORD2,
             float3 Light2: TEXCOORD3,
             float3 Light3: TEXCOORD4 ) : COLOR
{
   // 入力法線ベクトルを正規化する
   float3 norm = normalize (Normal);
 
   float4 outline = tex1D(Outline, 1 - dot (norm, normalize(View)));

   float lighting = (dot (normalize (Light1), norm) * 0.5 + 0.5) +
                    (dot (normalize (Light2), norm) * 0.5 + 0.5) +
                    (dot (normalize (Light3), norm) * 0.5 + 0.5);

   return outline * Material * lighting;
}

前に述べたように、このシェーダはいくつかの変数をグローバル スコープで宣言しています。この例では、レンダリングするオブジェクトのマテリアル値を定義する 4D ベクトル Material と、オブジェクトの輪郭線に使う特殊なテクスチャへのアクセスに使うサンプラ Outline があります。頂点シェーダで計算した 5 つの 3D テクスチャ座標が、このピクセル シェーダの main への入力であり、ビュー ベクトル、法線ベクトル、3 つのライト ベクトルを定義しています。

テクスチャ座標はポリゴン上で線形補間が行われるので、特定のピクセルが正規化されていない値を含んでいる可能性があります。このため、シェーダはまず補間された法線ベクトルを normalize() 組み込み関数を使って再正規化します。その後、輪郭線テクスチャを、正規化した法線とビュー ベクトルとの内積を使ってサンプリングします。次に、法線ベクトルと正規化されたライト ベクトルの、一連のスケーリングとバイアスをした内積の和を計算することで、ライティングを計算します。

このピクセル シェーダの最後の行では、変数 outlineMateriallighting の積を返しています。最初の 2 つは 4D ベクトルで、最後の値はスカラーです。前に型キャストに関連して説明したように、スカラーとベクトルの乗算を行うと、スカラーは、すべての要素が元のスカラーと等しいベクトルに一時的にプロモートされます。つまり、次の 2 つの式は等価です。

   return outline * Material * lighting;


   return outline * Material * float4(lighting, lighting, lighting, lighting);

最終的には、すべての成分とスカラー lighting の乗算が行われ、図 2 の最終結果が得られます。

NPRMetallic 頂点シェーダのときと同様に、fxc を使って、ピクセル シェーダ用のコード ファイルを生成してみましょう。

fxc -nologo -T ps_2_0 -Fc -Vd NPRMetallic.phl

このコンパイルでは以前と同じフラグを使用していますが、ps_2_0 ターゲット用にコンパイルを行っています。次に、結果として得られる 29 個の命令から成るシェーダを示します。

// Parameters:
//     float4 Material;
//     sampler Outline;
//
// Registers:
//     Name         Reg   Size
//     ------------ ----- ----
//     Material     c0       1
//     Outline      s0       1

    ps_2_0
    def c1, 1, 0, 0, 0.5
    dcl t0.xyz
    dcl t1.xyz
    dcl t2.xyz
    dcl t3.xyz
    dcl t4.xyz
    dcl_2d s0
    dp3 r0.w, t1, t1
    rsq r2.w, r0.w
    mul r9.xyz, r2.w, t1
    dp3 r9.w, t0, t0
    rsq r9.w, r9.w
    mul r4.xyz, r9.w, t0
    dp3 r9.w, r9, r4
    add r11.xy, -r9.w, c1.x
    texld r6, r11, s0
    dp3 r9.w, t2, t2
    rsq r9.w, r9.w
    mul r1.xyz, r9.w, t2
    dp3 r9.w, r1, r9
    mad r9.w, r9.w, c1.w, c1.w
    dp3 r8.w, t3, t3
    rsq r10.w, r8.w
    mul r5.xyz, r10.w, t3
    dp3 r0.w, r5, r9
    mad r9.w, r0.w, c1.w, r9.w
    add r9.w, r9.w, c1.w
    dp3 r2.w, t4, t4
    rsq r11.w, r2.w
    mul r1.xyz, r11.w, t4
    dp3 r8.w, r1, r9
    mad r10.w, r8.w, c1.w, r9.w
    add r5.w, r10.w, c1.w
    mul r6, r6, r5.w
    mul r0, r6, c0
    mov oC0, r0

前と同じように、変数(この例では定数 Material とサンプラ Outline)がファイルの冒頭に置かれています。これらは、シェーダを正常に動作させるためには、アプリケーションから API を介して適切に設定しなくてはなりません。

ps_2_0 命令の後には、いくつかのマジック定数の def 命令があります。この def 命令は、それ以降の ALU 演算で使用される定数を定義する実際のアセンブリ命令ストリームに現れるスロット ゼロの命令です。この種の定数定義は、通常は HLSL シェーダにリテラル値の結果が含まれている結果として生成されます。たとえば、NPRMetallic ピクセル シェーダの以下の文です。

1 - dot (norm, normalize(View)

dot (normalize (Light1), norm) * 0.5 + 0.5

この定数定義の後には、dcl tn.xyz の形式の 5 つの 3D テクスチャ座標宣言があります。頂点シェーダの場合と同様に、これらはこの HLSL シェーダの main 関数への入力引数のセマンティックの結果です。テクスチャ座標宣言の後には、サンプラ宣言 dcl_2d s0 があります。これは、サンプラ 0 に 2D テクスチャがバインドされなくてはならないことを指定しています。HLSL シェーダでは tex1D() 組み込み関数が使われていたのですから、これは奇妙に思えるかもしれません。このずれが生じているのは、Direct3D API やシェーダ アセンブリ言語に 1D テクスチャというようなものが存在しないからです。tex1D() 組み込み関数は、実際には HLSL シェーダの作成者がコンパイラに対して、テクスチャ座標の 1 つの成分のみを使えばよいことを指示するための手段に過ぎません。これにより、場合によってはアセンブラ命令が節約できます。

HLSL とアセンブリ コードの対応関係についてある程度説明したので、次はできるだけ適切な HLSL を書くための最適化戦略について解説します。

最適化

DirectX 9 HLSL コンパイラには優れたオプティマイザが組み込まれていますが、HLSL コードの作成者は、さまざまなところで数サイクルの節約が可能です。これは将来は学問的な練習問題にしかならないかもしれませんが、HLSL を使うかどうかにかかわらず、レガシーの 1.x シェーダ モデルをターゲットとして使えるかどうかという点での違いが生じる場合があります。

高性能のシェーダを作成するために覚えておかなくてはならない最も重要なことは、コンパイラはコードの作成者が指定したことを実行しなくてはならないということです。つまり、シェーダに対して一定数の数値演算や、出力要素に特定の値を格納するよう要求すると、シェーダはそれらの演算を実行しなくてはなりません。コンパイラはデッド コードを削除するだけの知恵は備えていますが、そのシェーダの外部条件のために最終的には不要になる値については何の情報も持ちません。たとえば、ピクセル シェーダが2番目テクスチャ座標を使わない場合、頂点シェーダはおそらくそのテクスチャ座標を計算するべきでないでしょう。もちろん、HLSL コンパイラは、頂点シェーダをコンパイルするときに、これを知ることはできません。さらに、特定のサンプラでつねに nA?A-1 関数参照テクスチャを使うために、サンプリング組み込み関数で使う 2 番目テクスチャ座標を計算する必要はないということがわかっているとします。しかし、tex2D() 組み込み関数を使うと、HLSL コンパイラは最終的には不要であっても、2 番目テクスチャ座標を計算せざるをえません。コンパイラは、ビジュアル クオリティとパフォーマンスのトレードオフを行わず、コード作成者が指定したとおりのことを実行するアセンブリ プログラムをビルドするよう設計されています。

高性能なシェーダのためのもう 1 つのきわめて重要な目標は、計算が必要な頻度でのみ行われるようにすることです。計算をピクセルごとではなく頂点ごとに行っても大丈夫なのであれば、そのようにします。通常、この種の演算の節約は、最も大きな性能向上に結び付きます。一様である値に対する演算についても、同じような最適化を施すことができます(シェーダの実行を通して変化しない演算)。たとえば、ワールド アンビエントの色値とオブジェクトのマテリアル アンビエント値の乗算を、頂点またはピクセルごとに繰り返して行うのではなく、事前に行っておいて、その積をシェーダに渡すようにします。

以下のセクションでは、言語機能がアセンブリ コンストラクトとどのように対応しているかをある程度詳しく説明します。頂点またはピクセル シェーダのアセンブリ コードを書く方法を理解する必要はありませんが、アセンブリ モデルの基本的な制限と効率性を理解しておくことはとても役立ちます。主なアセンブリ機能を理解しておくことは、コンパクトで効率の高いシェーダを生成するためには不可欠です。

行列データ型の使用

HLSL と C 標準の明らかな違いの 1 つは、ベクトル型と行列データ型が追加されていることです。これらのデータ型は、コードの作成を容易にし、組み込み関数が正しく動作できるようにすることを目的に追加されたものですが、データ型を正しく使えば、より優れたコードの生成が可能です。ベクトル型を使うと、コンパイラはベクトル命令のすべての能力を、より簡単に利用できるようになります。コンパイラは可能であればスカラー演算を自動的にベクトル化しますが、一般には、HLSL コードをベクトル演算に合わせて書けば、最高の性能を引き出すことができます。

シェーダは行列の代わりにベクトルの配列を使って実装してもかまいませんが、行列の格納には行列データ型の使用を推奨します。行列データ型を使うと、コンパイラは内部行列を、その行列の使われ方によって列優先または行優先で格納できます。この最適化は、行列がピクセル シェーダまたは頂点シェーダ内で生成される場合にきわめて有効です。前に述べたように、入力行列では、コンパイラはつねにコンパイラ フラグに基づいて列優先と行優先のどちらかの格納形式を使用します。デフォルトの設定は列優先です。

整数データ型の使用

HLSL では、int データ型について理解し、これを正しく使うことが重要です。int データ型を本来使うべきでない場所で使うと、簡単に余分な命令が生成されてしまいます。intデータ型が HLSL に追加されたのは、相対アドレシングをわかりやすく、また効率的にすることが目的です。アドレシングの目的に、切り捨ての規則なしに float データ型を使うと、配列への間違ったアクセスが行われる可能性があります。たとえば、2.5 というインデックス変数を使って float4x4 行列にアクセスすると、行列にアクセスする前に 2.5 の切り捨てが行われるのではなく、行列 2 の半分と行列 3 の半分がアクセスされてしまいます。これを修正するためには、配列へのアクセスに使用されるすべての float を、各要素のサイズでの乗算を行う前に丸めなくてはなりません。C の正しい丸め規則は、利用可能な命令を使って簡単に実現することはできないため、この演算はコストが高くなる可能性があります。

不要な丸めや切り捨てが行われるのを防ぐために、値を整数値としてマークする int データ型が追加されました。入力データが間違って float データとして扱われるのを避けるために、int として使うすべての入力は int として定義するべきです。たとえば、頂点ストリーム要素から読み込まれる行列パレット インデックスは int としてマークします。入力の int としての宣言は、切り捨てが行われず、その値が整数値と仮定されるようにする「スロット ゼロ」の演算です。入力が int として宣言されていなければ、シェーダは期待どおりの動作をしません。シェーダ内で float を int にキャストしたり、アドレシングの目的に float を使用したりすると、切り捨てが行われます。int でない中間値を int にキャストすると、切り捨てのオーバーヘッドが発生します。

次に、float のインデックスと整数のインデックスを使って生成されたコードを示します。

OutPos = mul(Pos, WorldArray[Index]);
float として宣言されたインデックス
frc r0.w, r1.w
add r2.w, -r0.w, r1.w
mul r9.w, r2.w, c61.x
mova a0.x, r9.w
m4x4 oPos, v0, c0[a0.x]
int として宣言されたインデックス
mul r0.w, c60.x, r1.w
mova a0.x, r0.w
m4x4 oPos, v0, c0[a0.x]

int として宣言されたインデックス

ほとんどの現行の頂点シェーダ / ピクセル シェーダ ハードウェアはフロー制御をサポートしていません。ハードウェアはシェーダを逐次実行し、個々の命令を 1 回だけ実行するように設計されています。新しいハードウェアは、限定された形のフロー制御、すなわち静的分岐命令の予測静的ループ動的分岐動的ループをサポートしています。HLSLは、さまざまなレベルのフロー制御をサポートしている任意の、またはすべてのモデルに合わせてコンパイルすることができるため、制限の厳しいモデル上で実行されることが予定されるシェーダを作成するときには、このことを念頭に置く必要があります。前に述べたように、HLSL の構文には、コンパイル ターゲットに基づく制限はいっさいありませんが、シェーダをそのコンパイル ターゲット上で実装するのが不可能な場合にはコンパイル時エラーが発生します。

ループは、シェーダ内で頻繁に発生する一種のフロー制御です。一部のハードウェアでは静的ループと動的ループの両方がサポートされていますが、ほとんどのハードウェアでは逐次実行が行われます。ループをサポートしていないモデルでは、すべてのループを展開しなくてはなりません。これはコストの高い操作になるかもしれませんが、最小限の労力で優れたコードを生成するために使うことができます。典型的な例として、DirectX 9 SDK の DepthOfField サンプルでは、ps_1_1 シェーダでも展開されたループを使用しています。高性能のシェーダを作成するためには、コンパイラにループの展開を行わせるか、シェーダの能力を超えて実行性能が落ちたり、命令数の上限を超えたりするケースを認識しておく必要があります。

'if' 文の使用は、ほとんどのアセンブリ レベルのシェーダ モデルで分岐がサポートされていないために、パフォーマンスに大きな影響を与える可能性があります。どのような形式の分岐もサポートしていないモデルでは、'if' の両方の側を実行し、'if' のどちらの側が選ばれたかに基づいて、出力を選択する必要があります。この種の実行方法は、CPU プログラミングの世界から来た大部分の HLSL シェーダ作成者が期待するものとは若干異なるものでしょう。CPU で高コストな演算を避けるために使われる一般的な最適化手法は、分岐をサポートしていないシェーダ モデルでは、高コストのパスと低コストのパスの両方が実行されるために、期待どおりに動作しません。一部のシェーダ モデルは、いくつかのレベルの分岐をサポートしています。すなわち、命令の予測静的な if ブロック動的な if ブロックです。

vs_1_1 における if の使用例:

if (Value > 0)
    Position = Value1;
else
    Position = Value2;

生成されるアセンブリ:

// Value > 0 に基づいて lerp 値を計算
mov r1.w, c2.x
slt r0.w, c3.x, r1.w
// Value1 と Value2 の間の lerp
mov r7, -c1
add r2, r7, c0
mad oPos, r0.w, r2, c1

現行のハードウェア シェーディング モデルで最も広くサポートされている分岐は、静的分岐です。静的分岐は、Boolean のシェーダ定数に基づいて、コード ブロックのオン / オフを切り替えることができる、シェーダ モデルの能力です。これは、現在レンダリングされているオブジェクトの種類に応じて、コストが高くなる可能性のあるコード パスを有効 / 無効にするための非常に便利な手段となります。Draw の呼び出しの間で、現在のシェーダでサポートしたいさまざまなフィーチャーを決定し、その動作を得るために必要な Boolean フラグを設定します。この方法の有利な点は、Boolean 定数によって「無効化」されたすべての命令は、実行時に完全にスキップされるということです。短所は、if ブロックの有効 / 無効の切り替えを、低い頻度 (draw の呼び出しの間) でしか行えないことです。一方、両方の側を実行するアプローチでは、ピクセルまたは頂点レベルで、2 つのパスの出力の間で動的に切り替えを行うことができます。

最もよく知られている分岐サポートは、動的分岐です。一部のシェーダ モデルが提供している動的分岐サポートは、標準的な CPU が提供しているサポートとよく似ています。、分岐のコストに加えて、分岐の選択された側にある命令のコストがかかります。この実行コストは、ほとんどの人が CPU 上のコードの最適化に関連して知っているレベルに相当します。この形式の分岐の問題は、ほとんどのハードウェアでは利用できず、また現時点では頂点シェーダ用のものしか存在しないということです。これらのモデルのシェーダの最適化は、CPU 上で実行されるコードの最適化とよく似ています。

入力型の宣言の重要性

シェーダへの入力のは、予想とは異なる形で使われます。頂点バッファから頂点シェーダ レジスタへ、または頂点シェーダ出力からピクセル シェーダ入力レジスタへのデータのロード方法は、Direct3D の仕様に細かく定義されています。つまり、シェーダ入力値は、つねに 4 つの float から成るベクトルに展開されます。これは、データ型の宣言は、データのシェーダへのロード方法の指定というよりは、単なるヒントに過ぎないということを意味しています。これを利用すれば、いくつかの最適化を行うことができます。

シェーダ アセンブリの作成者がよく使う最適化は、データがレジスタにロードされるときのデータの展開方法を利用するものです。たとえば頂点シェーダでは、.w 成分は、頂点バッファに存在しなかった場合には 1.0 に設定され、.y/.z 成分は、頂点バッファに存在しなかった場合には 0.0 に設定されます。このことが役に立つのは、頂点シェーダ内の位置です。ワールド行列で乗算を行うときには、.w 成分を 1.0 に設定する必要がよく生じますが、頂点バッファは一般に .xyz 成分しか含んでいません。位置入力パラメータが float4 として宣言されていると、.w 成分は、入力レジスタをロードするハードウェアによって 1.0f に設定されます。この最適化では、頂点バッファにどのようなデータが格納されているかがわかっている必要があるので、コンパイラはこの種の最適化を自動的に実行することはできません。

もう 1 つの最適化は、すべての入力パラメータを、シェーダ内で使われるときの適切な型で宣言しておくというものです。たとえば、入力されるデータが整数で、そのデータがアドレシングの目的に使われる場合には、切り捨てを防ぐためにパラメータを int として宣言することが重要となります。入力を int として宣言することの微妙な問題は、入力内の値が本当に整数値でなくてはならないということです。さもないと、コンパイラは入力データが真に整数データであると仮定して最適化を実行するため、生成されたコードは正しく動作しない可能性があります。

精度の問題 (logp, expp, lit)

正しい結果を出し、妥当な性能を達成するシェーダを作成するためには、精度について正しく理解しておく必要があります。ほとんどのシェーダ プロファイルでは、内部精度は固定されており、正しい結果を得るためにはこの精度を考慮に入れなくてはなりません。たとえば、ps_1_x モデルは比較的精度の低い固定小数点レジスタを持っています。スペキュラ ハイライトで小さい数の累乗を計算するだけで、すぐにバンディングが発生します。

vs_1_1 や vs_2_0 などの一部の他のモデルでは、いくつかの命令について低精度のバージョンが用意されています。具体的には、logp、expp、および lit を、それぞれ log、exp、および pow の低精度のバージョンとして使うことができます。一部のハードウェアでは、低精度と高精度のバージョンのパフォーマンス上の違いは大きくありません。log と exp はそれぞれ 10 命令スロットを使用し、logp と expp は 1 命令しか使用しないので、生成された asm コードのサイズが増大し、特に vs_1_1 コンパイル ターゲットでは命令数の上限を超えてしまう可能性があります。これらの低精度の命令へのアクセスは、出力を 'half' という名前の低精度のデータ型にキャストするか、それに格納するように宣言することによって行います。演算からの低精度の出力は、コンパイラに対し、その演算を可能な限り低い精度で実行するよう指示します。一部のピクセル シェーダ ハードウェアは、これ以外の演算も、より低い精度で実行できます。

次に log と logp の例を示します。

float LogValue = log(Value);
// vs_1_1 で 10 個の命令に相当 
//                 on vs_1_1
log r0, c0
float LogValue = (half)log(Value);
// vs_1_1 で 1 個の命令に相当 
    //                vs_1_1
logp r0, c0

ps_1_x コンパイル ターゲットの使用

オリジナルのピクセル シェーダ モデル (ps_1_1、ps_1_2、ps_1_3、および ps_1_4) は、高度なプログラミング可能性を提供していますが、実行可能な操作に関していくつかの制約があります。HLSL を使うと ps_1_x プロファイルを効率的にターゲットにできますが、シェーダ作成者はその下位の制限のセットを理解することが必要です。これは高性能のシェーダを作成するためだけでなく、シェーダのコンパイルを成功させるためにも重要です。ほとんどの人が最初にぶつかる制限は命令数でしょう。しかし、通常これは ps_1_x コンパイル ターゲットのその他の制限を無視したことが原因です。

ps_1_x コンパイル ターゲットについて最初に覚えておかなくてはならないのは、ターゲット ハードウェアが任意の入れ替えを持てないということです。この制限のため、コンパイラは入れ替えが必要となるたびに、余分な命令を使わなくてはなりません。生成される余分な命令のせいで、プログラムはこれらのコンパイル ターゲットで許される合計の命令数を簡単に超えてしまいます。ps_1_1~ps_1_3 のターゲットは、任意の書き込みマスクや複製入れ替え (.r.g.b.a) をサポートしておらず、同じ状況を引き起こす可能性があります。ps_1_4 プロファイルは複製入れ替えと任意の書き込みマスクをサポートしています。もちろん、これらの制限があっても、興味深く複雑なシェーダは簡単に書くことができます。これは、ps_1_x コンパイル ターゲットをターゲットとする HLSL コードを書くときに念頭に置いておくべき 1 つの事柄に過ぎません。

ps_1_x ターゲットには、当然ながら、より新しいピクセル シェーダ モデルと比べると不利な面がありますが、1 つの利点は、「スロット ゼロ」の入力修飾子と出力修飾子が存在するということです(値を 0 ~ 1 の範囲でクランプする、入力の補数を得る、入力の符号を反転する、入力をバイアスするなど)。これらの修飾子は、少数の命令で多数の作業を行うシェーダを生成したいときに非常に便利です。コンパイラはすべての修飾子を可能な限り自動的にマッチさせますが、HLSL シェーダの作成者は、これらの修飾子を実行したい操作という観点から考えて使えば、より効率を高めることができます。実際、一部の組み込み関数は、この種のシェーダを簡単に書けるようにする目的で HLSL に追加されています。たとえば、ピクセル シェーダでスロット ゼロの _sat 修飾子の生成を試みるときには、saturate() 組み込み関数の使用を推奨します。以下に、ps_1_x ターゲットにコンパイルしたときにスロット ゼロの入力修飾子を生成する HLSL コード シークエンスをいくつか紹介します。

_bx2 修飾子

HLSL コンパイラに _bx2 修飾子を生成させたいときには、数種類の HLSL コード シーケンスが使えます。次に示すどの main 関数でも、コンパイラは _bx2 修飾子を生成します。

float4 main( float3 Col : COLOR0, float3 Tex : TEXCOORD0 ) : COLOR0
{
    return dot(Col, Tex*2 - 1);
}

float4 main( float3 Col : COLOR0, float3 Tex : TEXCOORD0) : COLOR0
{
    float3 val = Tex*2;
    val = val -1;
    return dot(Col,val);
}

float4 main( float3 Col : COLOR0, float3 Tex : TEXCOORD0 ) : COLOR0
{
    return dot(Col, (Tex -.5f)*2 );
}

すべての main 関数が、同じ asm シェーダを生成します。

ps_1_1
texcoord t0
dp3 r0, v0, t0_bx2

Tex*2 -1 バージョンは ps_2_0 以降のターゲットでより最適なコードを生成するので、このバージョンの使用が推奨されます。

_bias 修飾子

次のコードを使用すると、HLSL コンパイラは _bias 修飾子を生成します。

float4 main( float3 Col : COLOR0, float3 Tex : TEXCOORD0 ) : COLOR0
{
    return dot(Col, (Tex - .5f));
}

この main 関数は、次のアセンブリ シェーダを生成します。

ps_1_1
texcoord t0
dp3 r0, v0, t0_bias

ps_1_1、ps_1_2、または ps_1_3 では、ソースが 0 ~ 1 の範囲にあることがわかっていない限り、_bias は使えないことに注意してください。つまり、事前に飽和クランプしておく必要があります。

_x2 修飾子 (ps_1_4 のみ)

次のコードを使用すると、HLSL コンパイラは _x2 修飾子を生成します。

float4 main( float3 Col : COLOR0, float3 Tex : TEXCOORD0 ) : COLOR0
{
    return dot(Col, Tex*2);
}

この HLSL コードは、次の asm シェーダ コードを生成します。

ps_1_4
texcrd r0.xyz, t0
dp3 r0, v0, r0_x2

_x2_x4_x8_d2_d4_d8 出力書き込み修飾子

ps_1_x モデルには出力書き込み修飾子のセットが存在しており、コンパイラに結果として得られる asm にそれらの修飾子を出力させる HLSL コードを書くことが可能です。ps_1_1~ps_1_3 モデルは命令の結果を 2 倍 (_x2)、4 倍 (_x4)、および 1/2 倍 (_d2) にする修飾子をサポートしており、ps_1_4 モデルは _x2_x4_x8_d2_d4、および _d8 の 6 つの修飾子すべてをサポートしています。次のコードは、N = 2、4、8、0.5、0.25、または 0.125 の対応する修飾子を生成します。

static const float N = 2;

float4 main( float4 Col[2] : COLOR0) : COLOR0
{
    return (Col[0] + Col[1] )*N;
}

上の HLSL コードは、次の asm 出力を生成します。

ps_1_1
add_x2 r0, v0, v1

補数修飾子

ps_1_x ターゲットをコンパイルするときに、コンパイラに補数修飾子を生成させる HLSL コードを書くことも可能です。これは、補数をとる値が 0 ~ 1 の範囲にある場合にのみ動作することに注意してください (つまり、その値は事前に飽和しておかなくてはなりません)。次の HLSL コードを使用すると、コンパイラはスロット ゼロの補数修飾子を生成します。

float4 main( float4 Col[2] : COLOR0) : COLOR0
{
    return (1-Col[0]) * (Col[1]);
}

この HLSL コードは、次の asm シェーダを生成します。

ps_1_1
mul r0, 1-v0, v1

飽和修飾子

次の 2 つのシェーダは、_sat 修飾子を生成します。この修飾子は、すべてのピクセル シェーダ コンパイル ターゲットで利用できます。

float4 main( float4 Col[2] : COLOR0) : COLOR0
{
    return saturate(Col[0]);
}

float4 main( float4 Col[2] : COLOR0) : COLOR0
{
    return clamp(Col[0],0,1);
}

どちらの HLSL シェーダも、次の asm シェーダを生成します。

ps_1_1
mov_sat r0, v0

符号反転修飾子

次のシェーダは、やはりすべてのシェーダ ターゲットで利用できる符号反転修飾子を生成します。(ps_1_x では、定数レジスタの否定を直接にとることはできないので、定数をいったん一時的な記憶域に移動する必要があり、単一のスロット ゼロの符号反転にはならないことに注意してください)。

float4 main( float4 Col[2] : COLOR0) : COLOR0
{
    return -Col[0];
}

この HLSL コードは、次の asm シェーダを生成します。

ps_1_1
mov r0, -v0

ps_1_x をターゲットとするときの戦略

ps_1_x プロファイルに合わせて最適化を行うときの最も優れた戦略は、まずシェーダを ps_2_0 上で書くことです。これにより、ps_2_0 対応のハードウェアで素早く簡単にプロトタイピングを行うことができます。シェーダが期待どおりの動作をするようになったら、これを目的の ps_1_x モデルに対してクロスコンパイルします。検証無効オプション (fxc.exeの-Vd) を使用すると、選択された ps_1_x モデルで命令数の上限がなかった場合に、シェーダがいくつの命令にコンパイルされるかを調べることができます。シェーダが上限に収まらなかった場合には、シェーダの不要な部分を削除し、ps_1_x 用の効率的なフォールバックを作成します。

HLSL シェーダについての詳しい解説を終了し、次は HLSL シェーダ サポートのアプリケーションへの統合に関連する問題について説明します。HLSL は、D3DX エフェクトを使用するエンジンにも、使用しないエンジンにも統合できるので、この両方のアプローチを解説します。また、RenderMonkey のようなシェーダ開発環境を使用すれば、アプリケーション コードを書かずに HLSL での実験を始めることができます。RenderMonkey の詳細については、"Shader Programming with RenderMonkey" の章を参照してください。

D3DX エフェクトを使うエンジンへの統合

D3DX エフェクト フレームワークは、プロフェッショナルな開発者の間で普及しつつある、D3DX ライブラリの非常に有用なコンポーネントです。当然ながら、DirectX 9 では D3DX エフェクトがアップデートされ、HLSL のサポートが追加されました。D3DX エフェクトについての知識がない人は、3D アプリケーションでの特殊なエフェクトのレンダリングを簡単にカプセル化できるように設計された抽象化と考えてください。エフェクトはレンダリング ステートと、asm または HLSL で書かれたシェーダをカプセル化できます。これには任意のレガシー ハードウェアをターゲットとしたフォールバック バージョンも含まれます。1 つのエフェクトは、一般に 1 つの .fx または .fxl ファイルに格納され、ファイルそのものはテクニックと呼ばれる複数のバージョンのエフェクトを含むことができます。たとえば、あるエフェクトの、レガシー シェーダ サポートやシェーダをまったく持たない古いハードウェア上で動作する基本的なバージョンを作成できます。この種のテクニックの典型的な使用例は、DirectX SDK の Water サンプルです。このサンプルは、異なる世代のハードウェアをターゲットとした数種類のテクニックを使用しています。もちろん、より少数のテクスチャしか使用せず、高度な処理を行っていない基本的なテクニックはあまり見た目はよくありませんが、そのことがまさに要点なのです。D3DX エフェクトを使用すると、この品質とスピードのトレードオフをきわめて自然に管理できます。

エフェクト ファイル

ここではエフェクトの細かい点には触れませんが、HLSL での使い方を理解するためには、エフェクト ファイルの基本的な構造を理解しておく必要があります。典型的なエフェクト ファイルは、次のような内容を持っています。

// ライティング定数
VECTOR g_Leye;
float4 GlobalAmbient = 0.5;
float Ka = 1;
float Kd = 0.8;
float Ks = 0.9;
float roughness = 0.1;
float noiseFrequency;

MATRIX matWorldViewProj;
MATRIX matWorldView;
MATRIX matITWorldView;
MATRIX matWorld;
MATRIX matTex0;

TEXTURE tVolumeNoise;
TEXTURE tMarbleSpline;

sampler NoiseSampler = sampler_state
{
   Texture = (tVolumeNoise);

   MinFilter = Linear;
   MagFilter = Linear;
   MipFilter = Linear;
   AddressU  = Wrap;
   AddressV  = Wrap;
   AddressW  = Wrap;
   MaxAnisotropy = 16;
};

sampler MarbleSplineSampler = sampler_state
{
   Texture = (tMarbleSpline);

   MinFilter = Linear;
   MagFilter = Linear;
   MipFilter = Linear;
   AddressU  = Clamp;
   AddressV  = Clamp;
   MaxAnisotropy = 16;
};

float3 snoise (float3 x)
{
    return 2.0f * tex3D (NoiseSampler, x) - 1.0f;
}

float4 ambient(void)
{
   return GlobalAmbient;
}

float4 soft_diffuse(float3 Neye, float3 Peye)
{
   // ビュー空間における頂点からライトへの正規化したベクトルを計算する (Leye)
   float3 Leye = (g_Leye - Peye) / length(g_Leye - Peye);

   float NdotL = dot(Neye, Leye) * 0.5f + 0.5f;

   // N.L
   return float4(NdotL, NdotL, NdotL, NdotL);
}

float4 specular(float3 NNeye, float3 Peye, float k)
{
    // ビュー空間における頂点からライトへの正規化したベクトルを計算する (Leye)
    float3 Leye = (g_Leye - Peye) / length(g_Leye - Peye);

    // Veye を計算する
    float3 Veye = -(Peye / length(Peye));

    // 等分角を計算する
    float3 Heye = (Leye + Veye) / length(Leye + Veye);

    // N.H を計算する
    float NdotH = clamp(dot(NNeye, Heye), 0.0f, 1.0f);

    float NdotH_2  = NdotH    * NdotH;
    float NdotH_4  = NdotH_2  * NdotH_2;
    float NdotH_8  = NdotH_4  * NdotH_4;
    float NdotH_16 = NdotH_8  * NdotH_8;
    float NdotH_32 = NdotH_16 * NdotH_16;

    return NdotH_32 * NdotH_32;
}

float4 hlsl_bluemarble (float3 P    : TEXCOORD0,
                        float3 Peye : TEXCOORD1,
                        float3 Neye : TEXCOORD2) : COLOR
{
   float4 Ct;
   float4 Ci;
   float3 NNeye;
   float marble;
   float f;

   // 適切な周波数に分割する
   P = P/16;

   marble = -2.0f * snoise(P * noiseFrequency) + 0.75f;

   NNeye = normalize(Neye);

   //  f のカラー スプラインに沿ったキューブ補間 (alpha でのグロス)
   Ct = tex1D (MarbleSplineSampler, marble);

   // イルミネーションからのカラー
   Ci = Ct * (Ka * ambient() + Kd * soft_diffuse(NNeye, Peye)) +
        Ct.w * Ks * specular(NNeye, Peye, roughness);

   return Ci;
}

VERTEXSHADER asm_marble_vs =
decl {}
asm
{
   vs.1.1

   dcl_position v0
   dcl_normal   v3

   m4x4 oPos, v0, c[0]           // 位置をクリップ空間に変換する

   m4x4 r0, v0, c[17]            // 変換された Pshade (テクスチャ行列 0 を使用)
   mov oT0, r0

   m4x4 oT1, v0, c[4]            // 位置をビュー空間に変換する
   m3x3 oT2.xyz, v3, c[8]        // 法線をビュー空間に変換する
};


technique technique_hlsl_bluemarble
{
   pass P0
   {       
      // asm シェーダでは、次のように変数名をハードウェア レジスタに
      // マップするだけでよい
      VertexShaderConstant[0]  = <matWorldViewProj>;
      VertexShaderConstant[4]  = <matWorldView>;
      VertexShaderConstant[8]  = <matITWorldView>;
      VertexShaderconstant[12] = <matWorld>;
      VertexShaderConstant[17] = <matTex0>;    

      VertexShader = <asm_marble_vs>;
      PixelShader  = compile ps_2_0 hlsl_bluemarble();

      CullMode = CCW;
   }
}

このサンプルのエフェクト ファイルを、下の方から説明していきます。このエフェクト ファイルの最後のコード ブロックは、1 つのレンダリング パスのみを持つ technique_hlsl_bluemarble という名前のテクニックを定義しています。このシングル パスは、アセンブリ言語で書かれた頂点シェーダと、HLSL で書かれたピクセル シェーダを使用します。このパスの最初の数行は、このパスが呼び出されたときに上位のエフェクト変数から特定のハードウェア定数レジスタにロードされる 5 種類の行列を宣言しています。この明示的なマッピングは、エフェクト ファイルでは asm シェーダに対してのみ行われます。ピクセル シェーダについては、HLSL で書かれているため、このような明示的なマッピングは行われません。次の行は、このパスで使う頂点シェーダ、asm_marble_vs という名前のアセンブリ シェーダを宣言しています。

 VertexShader = <asm_marble_vs>;

次の行はピクセル シェーダを定義しています。これは、hlsl_bluemarble() 関数をメインのエントリポイントとして使用して、ps_2_0 ターゲット用にコンパイルされます。

      PixelShader  = compile ps_2_0 hlsl_bluemarble();

テクニック定義の前にあるコード ブロックは、アセンブリ言語で手作業で書いた頂点シェーダです。その上には、HLSL ピクセル シェーダのメインのエントリ ポイントである hlsl_bluemarble があります。このコードを見ると、この関数が tex1D() 組み込み関数に加えて、 ambient()soft_diffuse() などのいくつかのユーティリティ関数を呼び出していることがわかります。これらのユーティリティ関数は以前にこのエフェクト内で定義されており、ps_2_0 ターゲット用にコンパイルすると、対応するアセンブリにインライン展開されます。

ユーティリティ関数の上には、NoiseSamplerMarbleSplineSampler という 2 つのサンプラの宣言があります。これらの宣言は以前と同じですが、エフェクト ファイル内では、使用するアドレシングおよびフィルタリング サンプラ ステートを定義するコードを括弧で囲んで指定できます。また、エフェクト ファイルでは、テクスチャもサンプラ宣言の上で定義できます。エフェクトの先頭には、アプリケーション レベルから設定可能な一連のグローバル変数の宣言があります。

エフェクト API

エフェクトを作成して、ファイルに格納したら、アプリケーション コードから使えます。もちろん、まず最初に D3DXCreateEffectFromFile() APIを使ってエフェクトを作成する必要があります。これに成功したら、エフェクト API を使って、エフェクトに必要となる適切な変数を設定できます。たとえば、行列の設定は SetMatrix() エントリポイントを使って行えます。

m_pEffect->SetMatrix ("matWorldViewProj", &m_matWorldViewProj);
m_pEffect->SetMatrix ("matWorldView", &m_matWorldView);
m_pEffect->SetMatrix ("matITWorldView", &m_matITWorldView);
m_pEffect->SetMatrix ("matWorld", &m_matWorld);
m_pEffect->SetMatrix ("matTex0", &m_ObjectParameters.m_matTex0);

また、同じような方法で任意の float やベクトルを設定できます。

m_pEffect->SetFloat ("noiseFrequency ", &m_fNoiseFreq);
m_pEffect->SetVector("g_Leye", &g_Leye);

テクスチャも同様です。

m_pEffect->SetTexture("tVolumeNoise",  m_pVolumeNoiseTexture);
m_pEffect->SetTexture("tMarbleSpline", m_pMarbleColorSplineTexture);

適切な定数をすべて設定したら、目的のテクニックを設定し、そのすべてのパスをレンダリングできます (この例では 1 つのみ)。

m_pEffect->SetTechnique(m_pEffect->GetTechniqueByName("technique_hlsl_bluemarble"));

m_pEffect->Begin(&cPasses, 0);
for (iPass = 0; iPass < cPasses; iPass++)
{
   m_pEffect->Pass(iPass);

   // ジオメトリのレンダリング
}
m_pEffect->End();

この例からわかるように、これはアプリケーションをいくつかの不要な負担から解放する単純なプロセスです。たとえば、アプリケーションは g_Leye をどのハードウェア定数レジスタにロードするべきなのか、あるいはノイズ テクスチャをどのサンプラにバインドするべきなのかといったことを知る必要がまったくありません。これらの細部は、すべて D3DX エフェクト フレームワークによって管理されます。

D3DX エフェクトを使わないエンジンへの統合

一部の ISV は、クロスプラットフォーム開発やオーバーヘッドの問題から、コードを D3DX にあまり密接に結びつけないようにしています。そのため、HLSL シェーダ管理では D3DX エフェクトを使うと非常に便利ですが、必須ではないようになっています。もちろん、D3DX エフェクトの手軽さを諦めるということは、アプリケーションが特定のシェーダでのレンダリングを行う前に、適切な定数とサンプラを管理し、セットアップしなくてはならないことを意味します。ここでは、その方法について説明します。

HLSL コードのコンパイルをトリガーする D3DX エフェクトを作成しないので、アプリケーション内で HLSL コンパイラを明示的に呼び出す必要があります。実際、これはアセンブリ シェーダ用に作成するアプリケーション コードとよく似ていますが、D3DXAssembleShader*() ルーチンの 1 つの代わりに D3DXCompileShader*() ルーチンの 1 つを呼び出す点が異なります。その後、結果として得られた asm コードを、コンパイルではなくアセンブルされたアセンブリ シェーダのときと同じように、適切なCreatePixelShader() または CreateVertexShader() エントリポイントに渡します。次のコードに、この方法での使用例を示します。

if (FAILED (hr = D3DXCompileShaderFromFile (g_strVHLFile, NULL, NULL,
"main", "vs_1_1", NULL, &pCode, NULL, &m_VS_ConstantTable)))
{
   return hr;
}

if (FAILED (hr = m_pd3dDevice->CreateVertexShader
((DWORD*)pCode->GetBufferPointer(), &m_HLSLVertexShader)))
{
   return hr;
}

上のコードから、D3DXCompileShader*() ルーチンは D3DXAssembleShader*() ルーチンにはないいくつかの追加の引数を持っていることがわかります。具体的には、シェーダのメインのエントリ ポイントの名前とコンパイル ターゲットを指定する必要があります (上記の "main" と "vs_1_1")。またオプションとして、#define、インクルード ファイル、そしてデバッグ情報、最適化、検証、行列データの配置順序の生成を制御するフラグを指定できます。これらすべての入力は、最初の 6 つの引数を通して D3DXCompileShader*()ルーチンに渡されます。最後の 3 つの引数は、コンパイラが書き込むバッファへのポインタです。これらはバイナリ アセンブリ コード、人間が読める形式のエラー メッセージ (オプション)、および定数テーブルです。バイナリ アセンブリ コードは CreatePixelShader() または CreateVertexShader() に渡されますが、定数テーブルは、アプリケーションが特定の HLSL シェーダを実行する前に、正しい定数データをどのようにロードすべきかを知るために使われます。以下では、HLSL シェーダをエフェクトを使わずにアプリケーションに統合するときに最も重要となる、D3DXCompileShader*() ルーチンが返す最後の引数について説明します。その他の引数については、ドキュメントを参照してください。

定数テーブル

D3DXCompileShader*() ルーチンが返す定数テーブルは、上位の定数とサンプラを、特定のハードウェア定数およびサンプラにマップするために使います。グローバル スコープで宣言された非静的変数は、コンパイル済みのシェーダへの入力引数とみなされ、シェーダが正しく動作するために適切に初期化する必要があります。定数テーブルはこのマッピングを提供します。一般に、アプリケーションにとっては ID3DXConstantTable インターフェイスを直接に使うのが一番簡単です。この方法だと、アプリケーションは定数テーブルの実際のデータ構造を解析する必要がありません。ID3DXConstantTable インターフェイスは、既知の HLSL 変数のハンドルを、それらの ASCII 名に基づいて参照するための便利なメソッドを多数備えています。つまり、これらの HLSL 変数の適切な値は、次のコードを使って設定できます。

D3DXHANDLE handle;

if (handle = m_PS_ConstantTable->GetConstantByName(NULL, "ringFreq"))
{
   m_PS_ConstantTable->SetFloat(m_pd3dDevice, handle, m_fRingFrequency);
}

if (handle = m_PS_ConstantTable->GetConstantByName(NULL, "lightWood"))
{
   m_PS_ConstantTable->SetVector(m_pd3dDevice, handle, &lightWood);
}

同じように、テクスチャとサンプラの状態も、次のコード例に示すように正しくセットアップする必要があります。

if (handle = m_PS_ConstantTable->GetConstantByName(NULL, "NoiseSampler"))
{
   m_PS_ConstantTable->GetConstantDesc(handle, &constDesc, &count);

   if (constDesc.RegisterSet == D3DXRS_SAMPLER)
   {
      m_pd3dDevice->SetTexture (constDesc.RegisterIndex, m_pVolumeNoiseTexture);

      // ノイズ サンプラのための適切なサンプラ状態を設定する
      m_pd3dDevice->SetSamplerState (constDesc.RegisterIndex,
 ..., ...);
   }
}

これはレンダー ステート、テクスチャ ステージ ステート、サンプラ ステートはアプリケーションが維持しなくてはならず、D3DX エフェクトを使用したときのように HLSL シェーダ コードにカプセル化されることはないということを意味しています。

もちろん、特にシェーダ作成ツールでは、アプリケーションが変数やサンプラの名前について事前にわかっているとは限りません。この場合には、ID3DXConstantTable::GetDesc() メソッドを使って定数テーブル内の定数の数を調べなくてはなりません。その後、アプリケーションは、上記のコード例で使われている ID3DXConstantTable::GetConstantByName() メソッドの代わりに、ID3DXConstantTable::GetConstantElement() メソッドを使います。一般論として、HLSL シェーダのサポートを D3DX エフェクトを使用せずにアプリケーションに統合したい場合には、ID3DXConstantTable インターフェイスについて学んでおくことをお勧めします。

SDK アップデート

DirectX 9.0 とその後の DirectX 9.0b パッチのリリース以降、Microsoft は開発者向けの定期的な SDK アップデートをリリースしようとしています。これらの SDK アップデートは、Direct3D ランタイムの変更は含んでいませんが、HLSL コンパイラを含む重要な D3DX ツールのアップグレードを含んでいます。最新のコンパイラ リビジョンを使用し、HLSL ソースに対応する最高の asm を生成できるように、つねに最新の DirectX SDK アップデートを適用することを強くお勧めします。

結論

ここでは、DirectX 9 の新しい機能である Direct3D High Level Shading Language (HLSL) について詳しく説明しました。言語そのものの仕組みを紹介し、サンプル シェーダの主な概念を改めて解説しました。また、コンパイル プロセスと、最適なパフォーマンスを実現するシェーダを作成する方法についての考察をいくつか示しました。この入門の章で、これ以降の章で示している HLSL シェーダを理解し、HLSL シェーダを自分のプロジェクトに統合するための基盤となる知識を入手できたものと思います。

謝辞

サンプルの HLSL シェーダを提供してくれた ATI の 3D Application Research Group に感謝します。Microsoft の Dan Baker と Loren McQuade は、特に最適化のセクションについてフィードバックを提供してくれました。また、Mark Wang と Wolfgang Engel は、文章をわかりやすくするための貴重なコメントを寄せてくれました。