【C++】DirectX9シェーダープログラミング入門:HLSLと最適化テクニックで描くリアルタイム3D表現
DirectX9のシェーダープログラミングは、GPUに頂点処理とピクセル処理を任せることで、高速かつ柔軟な描画を実現できます。
HLSLで頂点シェーダとピクセルシェーダを記述し、コンパイル後にDirect3Dへセットするだけで、独自の変換やライティング、ポストエフェクトを自在に適用でき、固定機能時代に比べ表現力が飛躍的に向上します。
最適化には計算の整理とテクスチャ帯域の管理が要となります。
DirectX9とシェーダーモデルの関係
DirectX9は、Microsoftが提供する3DグラフィックスAPIの一つで、ゲームやリアルタイム3Dアプリケーションの開発に広く使われています。
DirectX9の特徴の一つに、シェーダープログラミングのサポートがあります。
シェーダーはGPU上で動作する小さなプログラムで、頂点やピクセルの処理をカスタマイズし、よりリアルで多彩な表現を可能にします。
DirectX9では、シェーダーモデル(Shader Model)という規格に基づいてシェーダーの機能が定義されています。
シェーダーモデルは、GPUがサポートするシェーダーの機能セットや命令数、レジスタ数などの制限を示すもので、DirectX9では主にシェーダーモデル2.0が標準的に使われています。
シェーダーモデル2.0の主な機能
シェーダーモデル2.0(SM2.0)は、DirectX9世代のGPUで広くサポートされているシェーダー規格です。
SM2.0は、頂点シェーダーとピクセルシェーダーの両方に対して一定の機能と制限を定めています。
以下に主な特徴をまとめます。
- 命令数の上限
頂点シェーダーは最大64命令、ピクセルシェーダーは最大64命令(ただし、ピクセルシェーダーは実際には32命令程度が一般的)まで記述可能です。
これにより、複雑すぎるシェーダーは動作しませんが、基本的なライティングやテクスチャマッピングは十分に実装できます。
- レジスタ数の制限
頂点シェーダーは最大16個の定数レジスタ(float4単位)、ピクセルシェーダーは最大32個の定数レジスタを使用可能です。
これらのレジスタは行列やベクトル、スカラー値の格納に使います。
- テクスチャサンプラー数
ピクセルシェーダーは最大4つのテクスチャサンプラーを扱えます。
これにより、複数のテクスチャを組み合わせたマルチテクスチャリングが可能です。
- 条件分岐の制限
SM2.0では条件分岐が限定的にサポートされており、複雑なif文やループは使いにくいです。
代わりに、三項演算子やステップ関数を使った条件処理が多用されます。
- 浮動小数点精度
32ビット浮動小数点演算が基本ですが、GPUによっては精度に差があるため、計算結果の誤差に注意が必要です。
- 頂点シェーダーの機能
頂点の座標変換、法線変換、簡単なライティング計算、テクスチャ座標の生成や変換などが可能です。
スキニング(ボーンアニメーション)も行えますが、命令数の制限から複雑なスキニングは工夫が必要です。
- ピクセルシェーダーの機能
テクスチャのサンプリング、色の補間、ライティングの最終計算、アルファブレンディングの制御などが可能です。
複数のテクスチャを組み合わせて複雑なマテリアル表現ができます。
これらの機能により、SM2.0は当時のゲームや3Dアプリケーションでリアルタイムに多彩な表現を実現する基盤となりました。
vs_2_0 と ps_2_0 の制限事項
DirectX9のHLSLでは、頂点シェーダーとピクセルシェーダーに対してそれぞれバージョン指定を行います。
代表的なものがvs_2_0
(頂点シェーダーモデル2.0)とps_2_0
(ピクセルシェーダーモデル2.0)です。
これらにはいくつかの制限事項があり、シェーダー設計時に注意が必要です。
vs_2_0(頂点シェーダーモデル2.0)の制限
- 命令数制限
最大64命令まで。
これを超えるとコンパイルエラーになります。
複雑な行列計算やスキニングは命令数を節約する工夫が必要です。
- 定数レジスタ数
最大16個のfloat4定数レジスタが使用可能です。
ワールド・ビュー・プロジェクション行列やライトパラメータなどを格納します。
- 入力頂点属性数
最大16個の入力レジスタ(POSITION、NORMAL、TEXCOORDなど)を受け取れますが、ハードウェアによっては制限が厳しい場合があります。
- 出力レジスタ数
最大10個の出力レジスタを持てます。
これにより、頂点シェーダーからピクセルシェーダーへ渡すデータ量が制限されます。
- ループと分岐の制限
ループは静的な回数であれば使えますが、動的なループや複雑な条件分岐は制限されます。
GPUの性能に依存するため、ループ展開が推奨されることもあります。
ps_2_0(ピクセルシェーダーモデル2.0)の制限
- 命令数制限
最大64命令ですが、実際には32命令程度が多くのGPUでの上限です。
複雑なピクセルシェーダーは命令数オーバーに注意が必要です。
- 定数レジスタ数
最大32個のfloat4定数レジスタが使えます。
ライティングパラメータや色補正用の定数を格納します。
- テクスチャサンプラー数
最大4つのテクスチャサンプラーを使用可能です。
複数のテクスチャを組み合わせるマルチテクスチャリングに対応します。
- 出力形式
ピクセルシェーダーは1つのカラー出力(RGBA)を返します。
複数レンダーターゲット(MRT)はSM3.0以降の機能です。
- 条件分岐の制限
if文やループは制限されており、複雑な条件分岐は使いにくいです。
代わりに三項演算子や条件演算子を使うことが多いです。
- テクスチャの種類
2Dテクスチャ、キューブマップ、ボリュームテクスチャがサポートされていますが、使用できるテクスチャタイプはハードウェア依存です。
これらの制限を理解し、シェーダーの設計や最適化を行うことがDirectX9でのリアルタイム3D表現を成功させるポイントです。
特に命令数やレジスタ数の制限は、パフォーマンスと表現力のバランスを取る上で重要な要素となります。
以上のように、DirectX9のシェーダーモデル2.0は当時のGPU性能に合わせた機能セットと制限を持ち、HLSLでのシェーダープログラミングを可能にしています。
これらの基礎を押さえることで、より効果的にリアルタイム3Dグラフィックスを描画できます。
HLSL文法基礎
データ型とレジスタの対応
HLSL(High-Level Shading Language)では、GPUのレジスタに対応した基本的なデータ型が用意されています。
これらのデータ型は、頂点やピクセルシェーダーで扱うベクトルや行列の表現に最適化されています。
主なデータ型は以下の通りです。
データ型 | 説明 | サイズ(float単位) | レジスタ占有数(float4単位) |
---|---|---|---|
float | 単一の浮動小数点数 | 1 | 1/4(1レジスタの1成分) |
float2 | 2成分のベクトル(x, y) | 2 | 1/2 |
float3 | 3成分のベクトル(x, y, z) | 3 | 1 |
float4 | 4成分のベクトル(x, y, z, w) | 4 | 1 |
float3x3 | 3×3の行列 | 9 | 3 |
float4x4 | 4×4の行列 | 16 | 4 |
HLSLのレジスタはfloat4
単位で管理されているため、float3
やfloat4
は1つのレジスタを占有します。
float2
は1レジスタの半分程度ですが、実際には1レジスタの一部として扱われることが多いです。
例えば、以下のように定数バッファに行列やベクトルを格納します。
float4x4 WorldViewProjection; // 4つのfloat4レジスタを占有
float3 LightDirection; // 1つのfloat4レジスタを占有(w成分は未使用)
float4 DiffuseColor; // 1つのfloat4レジスタを占有
このように、HLSLのデータ型はGPUのレジスタ構造に密接に結びついているため、効率的なレジスタ使用を意識した設計が重要です。
セマンティクスの役割
HLSLでは、シェーダーの入力や出力変数に「セマンティクス(semantics)」を付与します。
セマンティクスは、変数がどのような役割を持つかをGPUやDirect3Dランタイムに伝えるためのタグのようなものです。
これにより、頂点バッファのどのデータがどの変数に対応するか、またシェーダー間でどのデータが受け渡されるかが明確になります。
代表的なセマンティクスには以下があります。
セマンティクス名 | 用途 |
---|---|
POSITION | 頂点の座標 |
NORMAL | 法線ベクトル |
TEXCOORD | テクスチャ座標 |
COLOR | 頂点カラー |
SV_POSITION | 変換後のスクリーンスペース座標(DirectX10以降) |
セマンティクスは、頂点シェーダーの入力や出力、ピクセルシェーダーの入力に付けられ、パイプライン内でのデータの流れを制御します。
POSITION・NORMAL・TEXCOORD の使い分け
POSITION
頂点の空間座標を表します。
通常はワールド空間やローカル空間の3D座標が格納されます。
頂点シェーダーの入力として使われ、変換後はスクリーンスペース座標として出力されます。
float4
型で、w
成分は透視投影のために使われます。
NORMAL
頂点の法線ベクトルを表します。
ライティング計算に必須で、通常は正規化された3成分のベクトルfloat3
です。
頂点シェーダーでワールド空間やビュー空間に変換され、ピクセルシェーダーに渡されます。
TEXCOORD
テクスチャマッピング用のUV座標を表します。
float2
やfloat3
で表現され、複数のテクスチャ座標セットを扱う場合はTEXCOORD0
、TEXCOORD1
のように番号を付けて区別します。
頂点シェーダーからピクセルシェーダーへ受け渡されます。
これらのセマンティクスを正しく使い分けることで、シェーダー間のデータの受け渡しがスムーズになり、意図した描画結果が得られます。
組み込み関数の活用
HLSLには、シェーダー開発を効率化するための多くの組み込み関数が用意されています。
これらは数学的な計算やテクスチャサンプリング、補間などに使われ、GPU上で高速に実行されます。
主な組み込み関数をいくつか紹介します。
- 数学関数
dot(floatN a, floatN b)
ベクトルの内積を計算します。
ライティングの拡散反射計算などで頻繁に使います。
cross(float3 a, float3 b)
3成分ベクトルの外積を計算し、法線ベクトルの計算に使います。
normalize(floatN v)
ベクトルを正規化します。
法線ベクトルの単位化に必須です。
lerp(floatN a, floatN b, float t)
線形補間を行います。
アニメーションや色の補間に使います。
saturate(float x)
値を0から1の範囲にクランプします。
ライティングの明るさ制御などに便利です。
- テクスチャ関係
tex2D(sampler2D s, float2 uv)
2DテクスチャからUV座標で色をサンプリングします。
ピクセルシェーダーでのテクスチャマッピングに使います。
texCUBE(samplerCUBE s, float3 direction)
キューブマップテクスチャから方向ベクトルで色を取得します。
環境マッピングに利用されます。
- 条件演算子
step(edge, x)
条件分岐の代わりに使うことが多いです。
smoothstep(edge0, edge1, x)
フェード効果に使います。
サンプルコード:ベクトルの正規化と内積計算
float3 LightDir = normalize(float3(0.0f, 1.0f, 0.0f)); // 上方向のライト
float3 Normal = normalize(input.Normal); // 入力法線の正規化
// ランバート照明の拡散反射成分を計算
float Diffuse = saturate(dot(Normal, LightDir));
output.Color = Diffuse * input.Color;
このコードは、ライト方向と法線の内積を計算し、拡散反射の強さを求めています。
saturate
で0未満や1超過の値を制限し、最終的に頂点カラーに乗算しています。
組み込み関数を活用することで、複雑な計算も簡潔に記述でき、GPUの性能を最大限に引き出せます。
頂点シェーダ実装ステップ
行列変換の流れ
頂点シェーダーの基本的な役割の一つは、3D空間の頂点座標をスクリーンに表示できる2D座標に変換することです。
この変換は複数の行列を組み合わせて行います。
主に「ワールド行列」「ビュー行列」「プロジェクション行列」の3つを使い、これらを合成して頂点に適用します。
ワールド・ビュー・プロジェクション合成
- ワールド行列(World Matrix)
モデルのローカル座標をワールド座標に変換します。
モデルの位置、回転、スケールを反映します。
- ビュー行列(View Matrix)
ワールド座標をカメラ視点の座標系に変換します。
カメラの位置や向きを反映します。
- プロジェクション行列(Projection Matrix)
3D空間の座標を2Dスクリーン座標に射影します。
透視投影や正射影の設定を行います。
これらの行列を合成したものを「ワールド・ビュー・プロジェクション行列(WVP行列)」と呼びます。
頂点シェーダーでは、入力頂点の座標にこのWVP行列を掛けて変換します。
float4x4 WorldViewProjection; // 定数バッファにセットされるWVP行列
struct VertexInput
{
float4 position : POSITION;
};
struct VertexOutput
{
float4 position : POSITION;
};
VertexOutput main(VertexInput input)
{
VertexOutput output;
// WVP行列で頂点座標を変換
output.position = mul(input.position, WorldViewProjection);
return output;
}
このmul
関数は行列とベクトルの乗算を行い、頂点の座標をスクリーン空間に変換します。
output.position
はクリップ空間の座標となり、これが後のラスタライズ処理に渡されます。
ライティング計算のパターン
頂点シェーダーでのライティング計算は、主に法線ベクトルを使って光の当たり具合を計算し、頂点カラーや明るさを決定します。
代表的な計算方法に「ランバート照明」と「Phongシェーディング」があります。
ランバート照明
ランバート照明は拡散反射(ディフューズ)を計算するシンプルなモデルです。
光源方向と法線ベクトルの内積を使い、光が当たる角度に応じて明るさを決めます。
計算式は以下の通りです。
:法線ベクトル(正規化済み) :光源方向ベクトル(正規化済み) :拡散反射の強さ(0以上)
HLSLでの実装例:
float3 normal = normalize(input.Normal);
float3 lightDir = normalize(LightDirection);
float diffuseIntensity = saturate(dot(normal, lightDir)); // 0~1にクランプ
output.Color = diffuseIntensity * input.Color;
saturate
関数で負の値を0にし、1を超えないように制限しています。
これにより、光が当たらない面は暗くなり、当たる面は明るくなります。
Phongシェーディング
Phongシェーディングは拡散反射に加え、鏡面反射(スペキュラ)を計算することでよりリアルな光沢表現を実現します。
計算は以下の3成分から成ります。
- 環境光(Ambient):全体的な一定の明るさ
- 拡散反射(Diffuse):ランバート照明と同様
- 鏡面反射(Specular):光源からの反射光のハイライト
鏡面反射の計算式は、
:鏡面反射係数 :反射ベクトル(光源方向の反射) :視線ベクトル(カメラ方向) :光沢の鋭さを表す指数(スペキュラハードネス)
HLSLでの実装例:
float3 normal = normalize(input.Normal);
float3 lightDir = normalize(LightDirection);
float3 viewDir = normalize(CameraPosition - input.Position.xyz);
// 拡散反射
float diffuse = saturate(dot(normal, lightDir));
// 反射ベクトル
float3 reflectDir = reflect(-lightDir, normal);
// 鏡面反射
float specular = pow(saturate(dot(reflectDir, viewDir)), SpecularPower);
// 環境光
float ambient = AmbientIntensity;
output.Color = input.Color * (ambient + diffuse) + SpecularColor * specular;
この計算により、物体表面に光沢のあるハイライトが表現され、よりリアルな質感を演出できます。
スキニングとアニメーション
キャラクターアニメーションなどで使われるスキニングは、複数のボーン(骨格)行列を使って頂点を変形させる技術です。
頂点シェーダーで行列スキニングを実装することで、GPU上で高速にアニメーションを処理できます。
行列パレットスキニング
行列パレットスキニングは、複数のボーン行列を配列(パレット)として用意し、各頂点に対して影響を与えるボーンの行列を重み付きで合成します。
頂点の最終位置は以下のように計算されます。
:元の頂点位置(ホモジニアス座標) :ボーン行列 :ボーンの影響度(ウェイト) :影響を与えるボーンの数(通常は最大4)
HLSLでの実装例(最大4ボーン影響):
float4x4 BoneMatrices[MaxBones]; // ボーン行列配列
struct VertexInput
{
float4 position : POSITION;
float4 boneWeights : BLENDWEIGHT;
uint4 boneIndices : BLENDINDICES;
};
VertexOutput main(VertexInput input)
{
float4 skinnedPos = float4(0,0,0,0);
// 4つのボーン行列を重み付きで合成
for (int i = 0; i < 4; i++)
{
uint index = input.boneIndices[i];
float weight = input.boneWeights[i];
skinnedPos += mul(input.position, BoneMatrices[index]) * weight;
}
output.position = mul(skinnedPos, WorldViewProjection);
return output;
}
この方法で、複雑なキャラクターアニメーションをリアルタイムに処理できます。
ウェイトの正規化と精度
ボーンウェイトは頂点に対して影響度を示す値で、通常は0から1の範囲で合計が1になるよう正規化されています。
正規化されていないと、頂点の位置が不正確になり、アニメーションが破綻することがあります。
ウェイトの正規化はCPU側で行うことが多いですが、シェーダー内で補正することも可能です。
float totalWeight = input.boneWeights.x + input.boneWeights.y + input.boneWeights.z + input.boneWeights.w;
float4 normalizedWeights = input.boneWeights / totalWeight;
また、ウェイトの精度はfloat4
で表現されるため、細かい影響度の差異も表現可能ですが、GPUの性能や命令数制限を考慮して、影響ボーン数は4つ程度に抑えるのが一般的です。
これらのステップを踏むことで、頂点シェーダーでの基本的な座標変換からリアルなライティング、そして複雑なアニメーションまで実装できます。
効率的な行列計算と適切なライティングモデルの選択が、リアルタイム3D表現の鍵となります。
ピクセルシェーダ実装ステップ
テクスチャフェッチとサンプラーステート
ピクセルシェーダーでは、テクスチャから色や情報を取得する「テクスチャフェッチ」が基本的な処理の一つです。
HLSLではtex2D
関数を使って2Dテクスチャの色をUV座標でサンプリングします。
float4 color = tex2D(sampler0, input.TexCoord);
ここでsmapler0
はテクスチャサンプラーで、テクスチャのフィルタリングやラップモードなどの設定を持っています。
これらの設定はDirect3DのAPI側でIDirect3DDevice9::SetSamplerState
関数を使って行います。
主なサンプラーステートの種類と役割は以下の通りです。
ステート名 | 説明 |
---|---|
D3DSAMP_MINFILTER | テクスチャの縮小時のフィルタリング方法(例:線形補間) |
D3DSAMP_MAGFILTER | テクスチャの拡大時のフィルタリング方法 |
D3DSAMP_MIPFILTER | ミップマップの選択方法 |
D3DSAMP_ADDRESSU | U方向のテクスチャ座標のラップモード(例:ラップ、クランプ) |
D3DSAMP_ADDRESSV | V方向のテクスチャ座標のラップモード |
適切なサンプラーステートを設定することで、テクスチャの見た目やパフォーマンスを調整できます。
例えば、遠くのテクスチャを滑らかに表示したい場合はミップマップと線形フィルタリングを使います。
マルチテクスチャ合成
複数のテクスチャを組み合わせて1つのピクセル色を作る技術をマルチテクスチャ合成と呼びます。
DirectX9のピクセルシェーダーでは最大4つのテクスチャサンプラーを使えるため、複雑なマテリアル表現が可能です。
デカールとライトマップのブレンド
デカールテクスチャは、基本のテクスチャに追加で貼り付ける汚れや傷などのディテールを表現します。
ライトマップは、事前に計算された照明情報をテクスチャとして持ち、リアルな陰影を表現します。
これらをブレンドする例を示します。
float4 baseColor = tex2D(samplerBase, input.TexCoord);
float4 decalColor = tex2D(samplerDecal, input.TexCoord);
float4 lightMap = tex2D(samplerLightMap, input.TexCoord);
// デカールはアルファでブレンド
float4 blendedColor = lerp(baseColor, decalColor, decalColor.a);
// ライトマップを乗算して陰影を加える
blendedColor.rgb *= lightMap.rgb;
return blendedColor;
このコードでは、lerp
関数でデカールのアルファ値に応じて基本色とデカール色を線形補間し、最後にライトマップの色を乗算して陰影を加えています。
これにより、リアルな質感と照明効果を同時に表現できます。
正確な法線計算
リアルなライティングを実現するためには、正確な法線ベクトルの計算が不可欠です。
特に法線マップを使う場合、頂点法線だけでなく接線空間(Tangent Space)での法線計算が必要になります。
Tangent Space生成
Tangent Spaceは、頂点ごとに「接線(Tangent)」「副接線(Bitangent)」「法線(Normal)」の3つの直交ベクトルで構成される座標系です。
法線マップはこの空間で定義されているため、ピクセルシェーダーでのライティング計算はこの空間で行います。
接線空間の生成は通常、CPU側で行い、頂点データとしてシェーダーに渡します。
生成方法は以下のような手順です。
- 頂点の法線ベクトルを取得
- 頂点のUV座標の差分から接線ベクトルを計算
- 接線ベクトルを正規化し、法線と直交するように調整
- 副接線は法線と接線の外積で求める
HLSLの頂点シェーダーで接線空間を受け取り、ピクセルシェーダーに渡す例:
struct VertexInput
{
float4 position : POSITION;
float3 normal : NORMAL;
float3 tangent : TANGENT;
float2 texcoord : TEXCOORD0;
};
struct VertexOutput
{
float4 position : POSITION;
float3 normal : TEXCOORD0;
float3 tangent : TEXCOORD1;
float2 texcoord : TEXCOORD2;
};
VertexOutput main(VertexInput input)
{
VertexOutput output;
output.position = mul(input.position, WorldViewProjection);
output.normal = normalize(mul(input.normal, (float3x3)World));
output.tangent = normalize(mul(input.tangent, (float3x3)World));
output.texcoord = input.texcoord;
return output;
}
ピクセルシェーダーでは、接線空間の基底を使って法線マップの法線をワールド空間に変換し、ライティング計算に利用します。
法線マップフォーマット
法線マップはRGBテクスチャとして格納され、各ピクセルの色が接線空間の法線ベクトルを表します。
一般的なフォーマットは以下の通りです。
チャンネル | 内容 | 範囲 |
---|---|---|
R | 接線空間のX成分(Tangent) | -1.0 ~ 1.0 |
G | 接線空間のY成分(Bitangent) | -1.0 ~ 1.0 |
B | 接線空間のZ成分(Normal) | 0.0 ~ 1.0 |
多くの法線マップはRGB値を[0,1]の範囲で格納しているため、シェーダー内で[-1,1]に変換する必要があります。
float3 normalMapSample = tex2D(normalSampler, input.TexCoord).rgb;
float3 normalTangentSpace = normalize(normalMapSample * 2.0f - 1.0f);
この変換により、テクスチャの色から正規化された法線ベクトルを得られます。
これを接線空間の基底(接線、副接線、法線)に変換してワールド空間の法線として使います。
これらの技術を組み合わせることで、ピクセルシェーダーでのリアルな質感表現や複雑なライティング効果を実現できます。
テクスチャフェッチの最適化や正確な法線計算は、見た目のクオリティとパフォーマンスの両立に欠かせません。
エフェクトフレームワーク活用
.fxファイルの構造
DirectX9のエフェクトフレームワークは、シェーダーコードやレンダリング状態の設定を一つのファイルにまとめて管理できる仕組みです。
このファイルは拡張子.fx
で保存され、HLSLコードと共にテクニックやパス、定数の宣言を含みます。
.fx
ファイルの基本的な構造は以下の要素で構成されます。
- グローバル変数宣言
シェーダーで使う定数やテクスチャ、サンプラーなどを宣言します。
これらはC++側から値をセットできます。
- シェーダ関数
頂点シェーダーやピクセルシェーダーのエントリーポイント関数を定義します。
vs_main
やps_main
のように命名されることが多いです。
- テクニック(technique)
複数のレンダリングパスをまとめた単位で、描画の設定を管理します。
テクニックはレンダリング時に選択して使います。
- パス(pass)
テクニック内に複数のパスを定義でき、それぞれに頂点シェーダーやピクセルシェーダー、レンダリングステートの設定を記述します。
以下は簡単な.fx
ファイルの例です。
float4x4 WorldViewProjection;
texture DiffuseTexture;
sampler2D DiffuseSampler = sampler_state
{
Texture = <DiffuseTexture>;
MinFilter = Linear;
MagFilter = Linear;
};
struct VS_INPUT
{
float4 Position : POSITION;
float2 TexCoord : TEXCOORD0;
};
struct VS_OUTPUT
{
float4 Position : POSITION;
float2 TexCoord : TEXCOORD0;
};
VS_OUTPUT vs_main(VS_INPUT input)
{
VS_OUTPUT output;
output.Position = mul(input.Position, WorldViewProjection);
output.TexCoord = input.TexCoord;
return output;
}
float4 ps_main(VS_OUTPUT input) : COLOR
{
return tex2D(DiffuseSampler, input.TexCoord);
}
technique BasicTechnique
{
pass P0
{
VertexShader = compile vs_2_0 vs_main();
PixelShader = compile ps_2_0 ps_main();
}
}
この例では、BasicTechnique
というテクニックに1つのパスP0
があり、頂点シェーダーとピクセルシェーダーを指定しています。
WorldViewProjection
行列やテクスチャは外部からセット可能です。
テクニックとパスの設計
テクニックは、描画の異なるシナリオや品質設定を切り替えるために使います。
例えば、影の有無やライティングモデルの違い、ポストエフェクトの有無などをテクニック単位で管理できます。
パスは、1つのテクニック内で複数の描画ステップを定義するための単位です。
マルチパスレンダリングや複雑なエフェクトを実装する際に使います。
テクニック設計のポイント
- 用途ごとにテクニックを分ける
例えば、BasicLighting
、ShadowPass
、PostProcess
など、用途に応じてテクニックを分けると管理しやすくなります。
- 品質設定の切り替え
低品質・高品質のシェーダーをテクニックで切り替え、パフォーマンス調整が可能です。
パス設計のポイント
- マルチパスレンダリング
1つのテクニック内で複数のパスを使い、例えば1パス目でジオメトリを描画し、2パス目でエフェクトを重ねる処理ができます。
- レンダリングステートの設定
各パスでブレンドモードや深度テストの設定を変えられます。
これにより、透明度のあるオブジェクトや特殊効果を実装しやすくなります。
以下は複数パスを持つテクニックの例です。
technique MultiPassTechnique
{
pass BasePass
{
VertexShader = compile vs_2_0 vs_main();
PixelShader = compile ps_2_0 ps_main();
// 深度テスト有効、ブレンド無効
ZEnable = true;
AlphaBlendEnable = false;
}
pass AdditivePass
{
VertexShader = compile vs_2_0 vs_main();
PixelShader = compile ps_2_0 ps_additive();
// 深度テスト無効、加算ブレンド有効
ZEnable = false;
SrcBlend = SrcAlpha;
DestBlend = One;
AlphaBlendEnable = true;
}
}
この例では、BasePass
で通常描画し、AdditivePass
で加算ブレンドのエフェクトを重ねています。
アノテーションでのツール連携
.fx
ファイルでは、変数やテクニック、パスに対して「アノテーション(annotation)」を付けられます。
アノテーションはメタデータのようなもので、ツールやエディタが変数の意味や用途を理解しやすくするために使います。
例えば、変数に対してUIでスライダーを表示したり、色を選択できるようにしたりするための情報を付加できます。
float SpecularPower < string UIName = "Specular Power"; float UIWidget = Slider; float UIMin = 1.0; float UIMax = 128.0 > = 32.0;
この例では、SpecularPower
変数に対して「UIName」「UIWidget」「UIMin」「UIMax」というアノテーションが付いています。
これにより、エフェクト編集ツールでスライダーとして表示され、1から128の範囲で調整可能になります。
アノテーションは以下のような用途で使われます。
- UI表示のカスタマイズ
スライダー、チェックボックス、カラーピッカーなどのウィジェット指定。
- 変数の説明やカテゴリ分け
変数の意味を説明したり、グループ化したりするためのテキスト。
- ツール連携
エフェクト編集ツールやデバッガーが変数を認識しやすくします。
アノテーションを活用することで、開発者やデザイナーが.fx
ファイルを効率的に編集・管理でき、ワークフローの向上につながります。
シェーダーパラメータ管理
定数バッファ送信の最適化
DirectX9のシェーダープログラミングでは、CPUからGPUへシェーダー定数(定数バッファ)を送信する際の効率化が重要です。
定数の更新はレンダリングパフォーマンスに大きく影響するため、無駄な更新を避ける工夫が求められます。
不要な定数更新の回避
毎フレーム、すべての定数をGPUに送るのではなく、変更があった場合のみ更新する方法が効果的です。
例えば、ワールド行列が変わらない場合は再送信をスキップします。
// 変更フラグを管理
bool worldMatrixChanged = true;
if (worldMatrixChanged)
{
device->SetVertexShaderConstantF(0, (float*)&worldMatrix, 4);
worldMatrixChanged = false;
}
このようにフラグを使って更新の有無を管理すると、無駄なデータ転送を減らせます。
バッチングによるまとめ送信
複数の定数を個別に送るのではなく、まとめて一度に送ることでAPIコールのオーバーヘッドを減らせます。
例えば、ワールド・ビュー・プロジェクション行列を一つの配列にまとめて送信します。
float constants[48]; // 4x4行列×3つ分
memcpy(constants, &worldMatrix, sizeof(float) * 16);
memcpy(constants + 16, &viewMatrix, sizeof(float) * 16);
memcpy(constants + 32, &projectionMatrix, sizeof(float) * 16);
device->SetVertexShaderConstantF(0, constants, 12);
この方法はAPI呼び出し回数を減らし、パフォーマンス向上に寄与します。
定数のアライメントとパディング
定数はfloat4
単位(4つのfloat)で管理されるため、データ構造のアライメントに注意が必要です。
例えば、float3
を使う場合でもfloat4
分の領域を確保し、パディングを入れることでGPU側の読み取りが正しく行われます。
float4 someVector; // float3でもfloat4で宣言し、wは未使用にする
これにより、定数バッファの破損や誤動作を防げます。
行列キャッシュ戦略
行列はシェーダーで頻繁に使われるため、CPU側での計算とGPUへの送信を効率化することが重要です。
事前計算とキャッシュ
ワールド、ビュー、プロジェクション行列は毎フレーム計算されますが、変化がない場合は再計算を避けるべきです。
例えば、カメラが動いていなければビュー・プロジェクション行列は再計算不要です。
bool viewProjChanged = true;
if (viewProjChanged)
{
viewProjectionMatrix = viewMatrix * projectionMatrix;
viewProjChanged = false;
}
このようにフラグ管理で無駄な計算を減らします。
合成行列の送信
シェーダー側で複数の行列を掛け合わせるよりも、CPU側で合成した行列を送るほうがパフォーマンスが良い場合が多いです。
例えば、WorldViewProjection
行列を事前に計算して送信します。
worldViewProjectionMatrix = worldMatrix * viewProjectionMatrix;
device->SetVertexShaderConstantF(0, (float*)&worldViewProjectionMatrix, 4);
これにより、頂点シェーダーの命令数を削減できます。
行列の転置
DirectXのHLSLは行ベクトル形式を使うため、CPU側で計算した行列は転置して送る必要があります。
転置を忘れると座標変換が正しく行われません。
D3DXMatrixTranspose(&transposedMatrix, &originalMatrix);
device->SetVertexShaderConstantF(0, (float*)&transposedMatrix, 4);
転置処理は送信前に必ず行いましょう。
テクスチャスロット整理術
ピクセルシェーダーで複数のテクスチャを使う場合、テクスチャスロットの管理が重要です。
DirectX9では最大4つのテクスチャサンプラーが使えますが、スロットの割り当てを整理しないと混乱やパフォーマンス低下の原因になります。
スロットの役割を固定化
テクスチャスロットに役割を決めて固定化すると、コードの可読性と保守性が向上します。
例えば、
スロット番号 | 用途 |
---|---|
0 | ベーステクスチャ |
1 | 法線マップ |
2 | ライトマップ |
3 | デカール |
このように決めておくと、シェーダーコードやC++側のテクスチャセット処理が一貫します。
テクスチャバインドの一元管理
テクスチャのセット処理は関数やクラスで一元管理し、スロット番号の指定ミスを防ぎます。
void SetMaterialTextures(IDirect3DDevice9* device, Material& mat)
{
device->SetTexture(0, mat.BaseTexture);
device->SetTexture(1, mat.NormalMap);
device->SetTexture(2, mat.LightMap);
device->SetTexture(3, mat.DecalTexture);
}
未使用スロットのクリア
使わないスロットに古いテクスチャが残ると、意図しない描画結果になることがあります。
描画前に未使用スロットはnullptr
でクリアしましょう。
device->SetTexture(3, nullptr);
サンプラーステートの統一
同じ種類のテクスチャは同じサンプラーステート設定を使うことで、状態変更のオーバーヘッドを減らせます。
例えば、すべての2Dテクスチャに対して線形フィルタリングを統一します。
これらのパラメータ管理の工夫により、DirectX9のシェーダープログラミングでパフォーマンスと安定性を両立できます。
特に定数バッファの送信最適化とテクスチャスロットの整理は、リアルタイム描画の効率化に直結します。
ポストプロセスエフェクト
レンダーターゲットの切り替え手法
ポストプロセスエフェクトを実装する際、まず重要なのがレンダーターゲットの切り替えです。
通常のレンダリングはバックバッファに描画されますが、ポストプロセスでは一旦シーンをテクスチャとしてレンダーターゲットに描画し、そのテクスチャを使って画面全体にエフェクトをかけます。
DirectX9ではIDirect3DDevice9::SetRenderTarget
関数を使ってレンダーターゲットを切り替えます。
以下の手順で行います。
- レンダーターゲット用のテクスチャ作成
IDirect3DDevice9::CreateTexture
でD3DUSAGE_RENDERTARGET
フラグを指定し、レンダーターゲット用のテクスチャを作成します。
- レンダーターゲットの取得
作成したテクスチャのサーフェイスをGetSurfaceLevel(0, &pSurface)
で取得します。
- レンダーターゲットの設定
SetRenderTarget(0, pSurface)
で描画先を切り替えます。
- シーンの描画
通常通りシーンを描画しますが、描画先はレンダーターゲットテクスチャです。
- 元のバックバッファに戻す
描画後はSetRenderTarget(0, pBackBufferSurface)
で元のバックバッファに戻します。
- ポストプロセスシェーダーでテクスチャを使用
レンダーターゲットテクスチャをピクセルシェーダーの入力として使い、画面全体にエフェクトをかけます。
// レンダーターゲット用テクスチャ作成例
IDirect3DTexture9* pRenderTargetTex = nullptr;
device->CreateTexture(width, height, 1, D3DUSAGE_RENDERTARGET,
D3DFMT_A8R8G8B8, D3DPOOL_DEFAULT, &pRenderTargetTex, nullptr);
// サーフェイス取得
IDirect3DSurface9* pRenderTargetSurface = nullptr;
pRenderTargetTex->GetSurfaceLevel(0, &pRenderTargetSurface);
// 元のバックバッファ取得
IDirect3DSurface9* pBackBuffer = nullptr;
device->GetRenderTarget(0, &pBackBuffer);
// レンダーターゲット切り替え
device->SetRenderTarget(0, pRenderTargetSurface);
// シーン描画処理...
// バックバッファに戻す
device->SetRenderTarget(0, pBackBuffer);
// 後でpRenderTargetTexを使ってポストプロセス描画
このようにレンダーターゲットを切り替えることで、シーン全体をテクスチャとして扱い、様々なポストプロセスエフェクトを実現できます。
ブルームの実装
ブルームは、明るい部分がにじんで輝いて見える効果で、リアルな光の表現に欠かせません。
DirectX9でのブルーム実装は一般的に以下のステップで行います。
- 明るい部分の抽出(閾値処理)
シーンのレンダーターゲットから明るい部分だけを抽出します。
ピクセルシェーダーで輝度が閾値以上の部分を残し、それ以外は黒にします。
- ぼかし処理(ガウスブラー)
抽出した明るい部分を水平・垂直方向に分けて複数回ぼかします。
これにより光のにじみが表現されます。
- 合成
元のシーン画像にぼかした明るい部分を加算合成し、ブルーム効果を加えます。
明るい部分抽出のピクセルシェーダー例
float4 ps_brightExtract(float2 uv : TEXCOORD0) : COLOR
{
float4 color = tex2D(sceneSampler, uv);
float luminance = dot(color.rgb, float3(0.299, 0.587, 0.114)); // 輝度計算
float threshold = 0.8;
float bright = saturate((luminance - threshold) / (1.0 - threshold));
return color * bright;
}
ガウスブラーの簡易例(水平ぼかし)
float4 ps_gaussBlurH(float2 uv : TEXCOORD0) : COLOR
{
float4 sum = float4(0,0,0,0);
float weights[5] = {0.227027, 0.1945946, 0.1216216, 0.054054, 0.016216};
float2 texelSize = float2(1.0 / textureWidth, 1.0 / textureHeight);
sum += tex2D(sceneSampler, uv) * weights[0];
for (int i = 1; i < 5; i++)
{
sum += tex2D(sceneSampler, uv + float2(texelSize.x * i, 0)) * weights[i];
sum += tex2D(sceneSampler, uv - float2(texelSize.x * i, 0)) * weights[i];
}
return sum;
}
垂直ぼかしも同様にfloat2(0, texelSize.y * i)
方向にサンプリングします。
合成例
float4 ps_bloomComposite(float2 uv : TEXCOORD0) : COLOR
{
float4 sceneColor = tex2D(sceneSampler, uv);
float4 bloomColor = tex2D(bloomSampler, uv);
return sceneColor + bloomColor; // 加算合成
}
このように複数のレンダーターゲットとパスを使い、ブルームを実装します。
カラーグレーディング
カラーグレーディングは、映像やゲームの色調を調整し、特定の雰囲気やスタイルを演出する技術です。
DirectX9では、3D LUT(Look-Up Table)を使ったカラーグレーディングが効果的です。
3D LUTの適用
3D LUTは、RGBの入力色を3次元のテーブルで変換し、色調補正を行います。
3D LUTは通常、
DirectX9では3Dテクスチャがサポートされていないため、3D LUTは2Dテクスチャに展開して扱います。
例えば、16×16×16の3D LUTは256×16の2Dテクスチャに変換されます。
3D LUT適用のピクセルシェーダー例
// 3D LUTのサイズ
static const float lutSize = 16.0;
// 3D LUTを2Dテクスチャとして扱う
texture lutTexture;
sampler2D lutSampler = sampler_state
{
Texture = <lutTexture>;
MinFilter = Linear;
MagFilter = Linear;
MipFilter = Linear;
};
float4 Apply3DLUT(float3 color)
{
// 入力色を0~1に正規化済みとする
float sliceSize = 1.0 / lutSize;
float slicePixelSize = sliceSize / lutSize;
float sliceInnerSize = slicePixelSize * (lutSize - 1);
// RGB成分を3D LUTの座標に変換
float blueIndex = color.b * (lutSize - 1);
float sliceIndex = floor(blueIndex);
float sliceFrac = blueIndex - sliceIndex;
// 2DテクスチャのUV計算
float2 uv1 = float2(
color.r * sliceInnerSize + slicePixelSize * 0.5 + sliceIndex * sliceSize,
color.g * sliceInnerSize + slicePixelSize * 0.5
);
float2 uv2 = uv1 + float2(sliceSize, 0);
// 2つのスライスを線形補間
float4 sample1 = tex2D(lutSampler, uv1);
float4 sample2 = tex2D(lutSampler, uv2);
return lerp(sample1, sample2, sliceFrac);
}
float4 ps_colorGrading(float2 uv : TEXCOORD0) : COLOR
{
float4 color = tex2D(sceneSampler, uv);
color.rgb = Apply3DLUT(color.rgb).rgb;
return color;
}
このシェーダーは、入力色の青成分で2つのスライスを選び、線形補間して色を変換します。
これにより、複雑な色調補正をリアルタイムに実現できます。
これらのポストプロセス技術を組み合わせることで、DirectX9環境でも高品質な映像表現が可能になります。
レンダーターゲットの適切な管理とシェーダーの工夫が、リアルタイムでの美しいエフェクト実装の鍵です。
マルチパスレンダリング
G-Buffer構築
マルチパスレンダリングの代表的な手法の一つに、Deferred Rendering(遅延レンダリング)があります。
その基盤となるのがG-Buffer(Geometry Buffer)です。
G-Bufferはシーンのジオメトリ情報を複数のレンダーターゲットに分割して格納するためのバッファ群で、後段のライティング計算に必要な情報を保持します。
G-Bufferに格納する主な情報は以下の通りです。
バッファ名 | 格納内容 | フォーマット例 |
---|---|---|
アルベド(Diffuse) | 頂点やテクスチャから得られる基本色 | D3DFMT_A8R8G8B8 |
法線ベクトル | ライティング計算用の法線 | D3DFMT_A2B10G10R10 など |
深度(Depth) | カメラからの距離情報 | D3DFMT_D24S8 |
スペキュラ強度 | 鏡面反射の強さ | D3DFMT_A8R8G8B8 など |
DirectX9では複数のレンダーターゲット(MRT: Multiple Render Targets)を使い、1回のジオメトリパスでこれらの情報を同時に出力します。
これにより、ライティング計算を後のパスに分離でき、複雑な光源処理が効率的に行えます。
G-Buffer構築の頂点シェーダー例
struct VS_INPUT
{
float4 Position : POSITION;
float3 Normal : NORMAL;
float2 TexCoord : TEXCOORD0;
};
struct VS_OUTPUT
{
float4 Position : POSITION;
float3 WorldNormal : TEXCOORD0;
float2 TexCoord : TEXCOORD1;
};
float4x4 WorldViewProjection;
float4x4 World;
VS_OUTPUT vs_main(VS_INPUT input)
{
VS_OUTPUT output;
output.Position = mul(input.Position, WorldViewProjection);
float3 worldPos = mul(input.Position, World).xyz;
output.WorldNormal = normalize(mul(input.Normal, (float3x3)World));
output.TexCoord = input.TexCoord;
return output;
}
G-Buffer構築のピクセルシェーダー例
texture DiffuseTexture;
sampler2D DiffuseSampler = sampler_state { Texture = <DiffuseTexture>; };
struct PS_INPUT
{
float3 WorldNormal : TEXCOORD0;
float2 TexCoord : TEXCOORD1;
};
struct PS_OUTPUT
{
float4 Albedo : COLOR0;
float4 Normal : COLOR1;
float4 Specular : COLOR2;
};
PS_OUTPUT ps_main(PS_INPUT input)
{
PS_OUTPUT output;
output.Albedo = tex2D(DiffuseSampler, input.TexCoord);
// 法線は[-1,1]範囲を[0,1]に変換して格納
output.Normal = float4(normalize(input.WorldNormal) * 0.5f + 0.5f, 1.0f);
output.Specular = float4(1.0f, 1.0f, 1.0f, 32.0f); // スペキュラカラーとパワー
return output;
}
このように、G-Bufferは複数のレンダーターゲットに分けて情報を格納し、後続のライティングパスで利用します。
Deferred Lightingの流れ
Deferred Lighting(遅延ライティング)は、G-Bufferに格納されたジオメトリ情報を使って、光源ごとにライティング計算を行う手法です。
これにより、多数の光源が存在しても効率的に処理できます。
Deferred Lightingの基本的な流れは以下の通りです。
- ジオメトリパス(G-Buffer構築)
シーンのジオメトリを描画し、アルベド、法線、深度、スペキュラ情報などをG-Bufferに書き込みます。
- ライトパス
各光源ごとにスクリーンスペースのフルスクリーンクワッドを描画し、ピクセルシェーダーでG-Bufferの情報を参照してライティング計算を行います。
- G-Bufferから法線や深度を取得し、光源との関係を計算
- 拡散反射、鏡面反射、影などのライティングを計算
- ライトの影響を加算合成
- 合成パス
ライトパスの結果を合成し、最終的な画面出力を行います。
ライトパスのピクセルシェーダー例
texture AlbedoTex : register(t0);
texture NormalTex : register(t1);
texture SpecularTex : register(t2);
sampler2D AlbedoSampler = sampler_state { Texture = <AlbedoTex>; };
sampler2D NormalSampler = sampler_state { Texture = <NormalTex>; };
sampler2D SpecularSampler = sampler_state { Texture = <SpecularTex>; };
float3 LightPosition;
float3 LightColor;
float3 CameraPosition;
float4 ps_lightPass(float2 uv : TEXCOORD0) : COLOR
{
float4 albedo = tex2D(AlbedoSampler, uv);
float4 normalEncoded = tex2D(NormalSampler, uv);
float3 normal = normalize(normalEncoded.rgb * 2.0f - 1.0f);
float4 specularData = tex2D(SpecularSampler, uv);
// 深度からワールド座標復元(省略)
float3 lightDir = normalize(LightPosition - worldPos);
float diff = saturate(dot(normal, lightDir));
float3 viewDir = normalize(CameraPosition - worldPos);
float3 reflectDir = reflect(-lightDir, normal);
float spec = pow(saturate(dot(viewDir, reflectDir)), specularData.a);
float3 lighting = LightColor * (albedo.rgb * diff + spec * specularData.rgb);
return float4(lighting, 1.0f);
}
このシェーダーはG-Bufferの情報を使い、光源の影響を計算して色を出力します。
複数の光源がある場合は、ライトパスを複数回実行し、結果を加算合成します。
このように、マルチパスレンダリングのG-Buffer構築とDeferred Lightingの流れを理解し実装することで、多数の光源を効率的に扱い、リアルタイム3D表現のクオリティを大幅に向上させられます。
最適化の視点
ループアンローリングと命令数削減
DirectX9のシェーダーは命令数に制限があるため、ループの使い方に注意が必要です。
ループアンローリングとは、ループの繰り返し処理を展開して命令数を減らすテクニックです。
特にシェーダーモデル2.0では動的ループが制限されているため、静的ループの展開が推奨されます。
例えば、以下のようなループを使ったコードは、
float4 sum = float4(0,0,0,0);
for (int i = 0; i < 4; i++)
{
sum += tex2D(sampler0, uv + offsets[i]);
}
コンパイラが自動的にアンローリングしない場合は、手動で展開します。
float4 sum = tex2D(sampler0, uv + offsets[0]) +
tex2D(sampler0, uv + offsets[1]) +
tex2D(sampler0, uv + offsets[2]) +
tex2D(sampler0, uv + offsets[3]);
これにより、ループの制御命令がなくなり、GPUのパイプライン効率が向上します。
ただし、展開しすぎると命令数が増えすぎるため、バランスが重要です。
分岐の回避と条件式の工夫
シェーダーモデル2.0では条件分岐(if文やループの条件判定)が制限されており、分岐が多いとパフォーマンスが大幅に低下します。
分岐を避けるために、条件式を工夫して三項演算子や数学関数で代替することが効果的です。
例えば、以下のif文は、
if (value > 0.5)
result = a;
else
result = b;
三項演算子で書き換えます。
result = (value > 0.5) ? a : b;
さらに、step
関数を使うと条件判定を数学的に表現できます。
float mask = step(0.5, value); // value >= 0.5なら1.0、未満なら0.0
result = lerp(b, a, mask);
この方法はGPUの分岐処理を避け、パイプラインの効率を保ちます。
テクスチャ帯域の節約
テクスチャフェッチはGPUの帯域幅を大きく消費するため、テクスチャの使用を最適化することが重要です。
以下のポイントを意識します。
- テクスチャサイズの適正化
必要以上に大きなテクスチャは帯域を圧迫します。
解像度を適切に設定し、ミップマップを活用して遠距離では低解像度を使います。
- テクスチャサンプル数の削減
ピクセルシェーダーでのテクスチャフェッチ回数を減らすため、複数の情報を1つのテクスチャにまとめる(例:アルベドとスペキュラを同じテクスチャの異なるチャンネルに格納)ことが有効です。
- テクスチャ圧縮の活用
DXT圧縮などのGPU対応圧縮フォーマットを使い、メモリ帯域と容量を節約します。
- テクスチャキャッシュの活用
UV座標の連続性を保ち、キャッシュヒット率を高める工夫をします。
例えば、テクスチャ座標の補間を工夫し、無駄なフェッチを減らします。
GPUプロファイリング
GPUプロファイリングは、シェーダーのパフォーマンスボトルネックを特定し、最適化の指針を得るために不可欠です。
DirectX9環境では、Microsoftの「PIX for Windows」が代表的なツールです。
PIX for Windowsのチェックポイント
PIXはGPUのレンダリングパイプラインを詳細に解析できるツールで、シェーダーの命令数、テクスチャフェッチ数、パイプラインステージの利用状況などを可視化します。
- チェックポイントの設定
コード内でPIXBeginEvent
とPIXEndEvent
を使い、特定の処理区間をマークします。
これにより、どの処理が重いかを詳細に分析できます。
PIXBeginEvent(device, 0, "Deferred Lighting Pass");
// ライトパス描画処理
PIXEndEvent(device);
- シェーダー命令数の確認
PIXは各シェーダーの命令数を表示し、命令数制限に近いかどうかを判断できます。
命令数が多すぎる場合は、ループアンローリングや命令削減を検討します。
- テクスチャフェッチ数の分析
テクスチャフェッチが多いと帯域幅が圧迫されるため、PIXでフェッチ数を確認し、必要に応じてテクスチャ統合やフェッチ削減を行います。
- パイプラインステージのボトルネック特定
頂点シェーダー、ピクセルシェーダー、ラスタライザなど、どのステージが負荷の原因かを特定し、最適化の優先順位を決められます。
これらの情報を活用し、シェーダーのパフォーマンスを継続的に改善していくことが重要です。
デバッグとトラブルシュート
ステージごとの可視化
DirectX9のシェーダープログラミングでは、頂点シェーダーやピクセルシェーダーなど各ステージの処理結果を可視化することがトラブルシュートに非常に役立ちます。
これにより、どの段階で問題が発生しているかを特定しやすくなります。
頂点シェーダーステージの可視化
頂点シェーダーの出力を可視化するには、頂点の座標や法線、テクスチャ座標などを色や位置に変換して画面に表示します。
例えば、法線ベクトルをRGBカラーにマッピングして表示する方法があります。
float4 vs_debug(VS_INPUT input) : POSITION
{
float3 normal = normalize(input.Normal);
// 法線を[0,1]に変換して色として出力
float4 color = float4(normal * 0.5f + 0.5f, 1.0f);
return color;
}
このように法線の向きを色で確認でき、法線の向きが正しく計算されているかをチェックできます。
ピクセルシェーダーステージの可視化
ピクセルシェーダーでは、特定の計算結果を色として出力し、問題のある部分を特定します。
例えば、テクスチャ座標の範囲外アクセスやライティングの異常を検出するために、UV座標やライティング強度を色で表現します。
float4 ps_debug(PS_INPUT input) : COLOR
{
// UV座標を赤と緑にマッピング
return float4(input.TexCoord.xy, 0.0f, 1.0f);
}
このように表示することで、UV座標の範囲や補間が正しいかを視覚的に確認できます。
深度バッファやレンダーターゲットの可視化
深度バッファの値は通常見えませんが、ピクセルシェーダーで深度値を色に変換して表示することも可能です。
これにより、深度の異常やクリッピングの問題を発見できます。
float depth = input.Position.z / input.Position.w;
return float4(depth, depth, depth, 1.0f);
シェーダーテストパターンの作成
シェーダーの動作確認やデバッグを効率化するために、テストパターンを作成することが有効です。
テストパターンは、特定の入力に対して期待される出力を簡単に確認できるように設計します。
基本的な色の出力
まずは単純に固定色を出力するシェーダーを作り、描画パイプラインが正常に動作しているかを確認します。
float4 ps_solidColor(float2 uv : TEXCOORD0) : COLOR
{
return float4(1.0f, 0.0f, 0.0f, 1.0f); // 赤色
}
グラデーションパターン
UV座標を使ってグラデーションを表示し、テクスチャ座標の補間や範囲をチェックします。
float4 ps_gradient(float2 uv : TEXCOORD0) : COLOR
{
return float4(uv.x, uv.y, 0.0f, 1.0f);
}
ライティングの簡易テスト
法線やライト方向を固定して、ライティング計算の結果を確認するテストパターンも有効です。
float3 normal = float3(0, 0, 1);
float3 lightDir = normalize(float3(0, 0, -1));
float diffuse = saturate(dot(normal, lightDir));
return float4(diffuse, diffuse, diffuse, 1.0f);
これにより、ライティングの基本的な動作を検証できます。
コンパイルエラーの読み解き方
HLSLシェーダーのコンパイル時に発生するエラーは、文法ミスや型の不一致、セマンティクスの誤りなど多岐にわたります。
エラーメッセージを正しく読み解くことがトラブルシュートの第一歩です。
エラーメッセージの構造
典型的なエラーメッセージは以下のような形式です。
error X3000: syntax error: unexpected token 'float4'
file.hlsl(23,15)
error X3000
:エラーコードsyntax error
:エラーの種類unexpected token 'float4'
:具体的な問題点file.hlsl(23,15)
:ファイル名と行・列番号
よくあるエラー例と対処法
- 文法エラー(syntax error)
セミコロンの付け忘れや括弧の不一致が多いです。
エラーメッセージの行番号を確認し、該当箇所を修正します。
- 型の不一致(type mismatch)
例えば、float3
とfloat4
を混同している場合に発生します。
型変換や変数宣言を見直します。
- セマンティクスの誤り
入力や出力のセマンティクスが正しくないとエラーになります。
例えば、頂点シェーダーの出力にPOSITION
がない場合などです。
- 未定義の変数や関数
変数名のスペルミスや関数の宣言漏れが原因です。
宣言を確認し、正しい名前を使います。
デバッグのコツ
- エラーメッセージの行番号を必ず確認し、該当箇所を重点的にチェックします
- 複数のエラーが出る場合は、最初のエラーを修正すると後続のエラーが解消されることが多い
- 小さなコード単位でコンパイルを繰り返し、問題箇所を絞り込みます
- HLSLの仕様やセマンティクスのドキュメントを参照し、正しい使い方を確認します
これらのデバッグ手法を活用することで、DirectX9のシェーダープログラミングにおける問題を効率的に発見・解決できます。
ステージごとの可視化やテストパターンは特に効果的で、開発の初期段階から積極的に取り入れることをおすすめします。
よくある落とし穴と回避策
精度不足によるアーティファクト
DirectX9のシェーダープログラミングでは、計算精度の不足が原因で様々なアーティファクト(描画の不具合)が発生しやすいです。
特にシェーダーモデル2.0では、浮動小数点演算の精度やレジスタの制限が影響します。
主な精度不足の原因と症状
- 深度バッファの精度不足
遠距離のオブジェクトでZファイティング(ちらつき)が発生しやすくなります。
これは深度値の分布が非線形で、遠くの深度差が小さくなるためです。
- 法線の正規化不足
法線ベクトルが正規化されていないと、ライティング計算で不自然な明暗やハイライトの乱れが起こります。
- 小数点の丸め誤差
頂点変換やライティング計算での丸め誤差が蓄積し、微妙な色ムラやジッターが生じることがあります。
回避策
- 深度バッファのフォーマット選択
可能な限り高精度な深度フォーマット(例:D24S8
)を使い、カメラの近クリップ距離を適切に設定して深度分布を最適化します。
- 法線の正規化を徹底
頂点シェーダーやピクセルシェーダー内で必ずnormalize
関数を使い、法線ベクトルを単位ベクトルにします。
float3 normal = normalize(input.Normal);
- 計算の順序と型の統一
演算の順序を工夫し、同じ型(float4など)で計算を行うことで丸め誤差を減らします。
特に行列乗算は転置や型変換に注意します。
- シェーダーモデルのアップグレード検討
可能であればSM3.0以上を使い、より高精度な演算や命令を活用するのも有効です。
セマンティクス不一致
HLSLでは、入力・出力変数にセマンティクスを正しく指定しないと、シェーダー間のデータ受け渡しがうまくいかず、描画結果が崩れたりコンパイルエラーが発生したりします。
よくあるセマンティクス不一致の例
- 頂点シェーダーの出力とピクセルシェーダーの入力の不一致
例えば、頂点シェーダーでTEXCOORD0
を出力しているのに、ピクセルシェーダーでTEXCOORD1
を受け取っている場合、正しいテクスチャ座標が渡りません。
- 必須セマンティクスの欠落
頂点シェーダーの出力にPOSITION
がないと、ラスタライズが正しく行われません。
- 型とセマンティクスの不整合
例えば、float3
にPOSITION
を付けているが、float4
が期待されている場合など。
回避策
- セマンティクスの命名規則を統一
頂点シェーダーの出力とピクセルシェーダーの入力で同じセマンティクス名と型を使います。
struct VS_OUTPUT
{
float4 Position : POSITION;
float2 TexCoord : TEXCOORD0;
};
struct PS_INPUT
{
float4 Position : POSITION;
float2 TexCoord : TEXCOORD0;
};
- 必須セマンティクスを必ず含める
特にPOSITION
は頂点シェーダーの出力に必須です。
- コンパイル時の警告・エラーを無視しない
セマンティクス関連のエラーは必ず修正し、ドキュメントで正しい使い方を確認します。
ライティング結果のちらつき
ライティング計算でちらつき(フリッカー)が発生することがあります。
これは主に以下の原因によります。
原因
- 法線の不連続
モデルの頂点法線が隣接面で大きく異なると、ライティングが急激に変化し、ちらつきが生じます。
- スキニングのウェイト誤差
ボーンウェイトの合計が1.0になっていなかったり、正規化されていないと頂点位置が不安定になり、ライティングが揺れます。
- 深度バッファの精度不足
Zファイティングにより、ポリゴンの重なり部分でちらつきが発生します。
- ピクセルシェーダーの条件分岐による不安定な結果
分岐の切り替わり部分で色が急変し、ちらつきに見えることがあります。
回避策
- 法線のスムージング
モデルの法線を頂点単位で平均化し、隣接面間の法線差を減らします。
モデリングツールやエクスポート時に設定します。
- ウェイトの正規化
スキニング用のウェイトは必ず合計1.0に正規化し、シェーダー内でも必要に応じて補正します。
float totalWeight = input.boneWeights.x + input.boneWeights.y + input.boneWeights.z + input.boneWeights.w;
float4 normalizedWeights = input.boneWeights / totalWeight;
- 深度バッファの設定見直し
クリップ距離の調整や高精度フォーマットの使用でZファイティングを軽減します。
- 条件分岐の工夫
分岐を避け、lerp
やstep
関数で滑らかな切り替えを行い、色の急変を防ぎます。
これらの落とし穴はDirectX9のシェーダープログラミングで頻繁に遭遇しますが、原因を理解し適切に対処することで、安定した高品質な描画を実現できます。
拡張テクニック
スクリーンスペースアンビエントオクルージョン
スクリーンスペースアンビエントオクルージョン(SSAO)は、シーン内の物体同士が近接している部分の陰影をリアルタイムに計算し、環境光の遮蔽効果を表現する技術です。
DirectX9環境でも比較的軽量に実装可能で、シーンの奥行き情報と法線情報を利用して画面上で陰影を生成します。
SSAOの基本的な流れ
- 深度バッファと法線バッファの取得
シーンの深度情報と法線情報をG-Bufferなどで取得します。
- サンプリングカーネルの生成
ピクセル周辺の複数のサンプル点をランダムに生成し、遮蔽判定に使います。
- 遮蔽判定
各サンプル点の深度と比較し、遮蔽されているかを判定。
遮蔽されているほど陰影が濃くなります。
- 結果の合成
遮蔽率を計算し、環境光の強さに乗算して最終的な色に反映します。
SSAOのピクセルシェーダー例(簡易版)
float4 ps_ssao(float2 uv : TEXCOORD0) : COLOR
{
float occlusion = 0.0;
float3 normal = tex2D(normalSampler, uv).rgb * 2.0 - 1.0;
float depth = tex2D(depthSampler, uv).r;
for (int i = 0; i < sampleCount; i++)
{
float2 sampleUV = uv + sampleKernel[i].xy * radius;
float sampleDepth = tex2D(depthSampler, sampleUV).r;
float rangeCheck = smoothstep(0.0, 1.0, radius / abs(depth - sampleDepth));
occlusion += (sampleDepth >= depth + bias ? 1.0 : 0.0) * rangeCheck;
}
occlusion = 1.0 - (occlusion / sampleCount);
return float4(occlusion, occlusion, occlusion, 1.0);
}
このように、SSAOは画面空間の情報だけでリアルな陰影を追加できるため、パフォーマンスと見た目のバランスが良い拡張技術です。
シャドウマッピング
シャドウマッピングは、光源から見たシーンの深度情報をテクスチャにレンダリングし、その情報を使って影の有無を判定する技術です。
DirectX9でも広く使われており、リアルタイムシャドウ表現の基本となります。
シャドウマッピングの基本手順
- シャドウマップの生成
光源視点でシーンをレンダリングし、深度情報をシャドウマップテクスチャに保存します。
- シーンのレンダリング
カメラ視点でシーンを描画し、各ピクセルのワールド座標を光源空間に変換します。
- シャドウ判定
ピクセルの光源空間深度とシャドウマップの深度を比較し、影の中かどうかを判定します。
シャドウマッピングのピクセルシェーダー例(基本)
float4 ps_shadow(float4 posLightSpace : TEXCOORD0) : COLOR
{
float2 shadowUV = posLightSpace.xy / posLightSpace.w * 0.5 + 0.5;
float depth = posLightSpace.z / posLightSpace.w;
float shadowMapDepth = tex2D(shadowMapSampler, shadowUV).r;
float shadow = depth - bias > shadowMapDepth ? 0.3 : 1.0; // 影なら暗くする
return float4(shadow, shadow, shadow, 1.0);
}
PCFでのソフトシャドウ
PCF(Percentage Closer Filtering)は、シャドウマップのサンプリングを複数回行い、その平均を取ることで影のエッジをぼかし、ソフトシャドウを実現する技術です。
PCFの実装例
float4 ps_pcfShadow(float4 posLightSpace : TEXCOORD0) : COLOR
{
float2 shadowUV = posLightSpace.xy / posLightSpace.w * 0.5 + 0.5;
float depth = posLightSpace.z / posLightSpace.w;
float shadow = 0.0;
int samples = 4;
float2 offset[4] = {
float2(-texelSize.x, -texelSize.y),
float2(texelSize.x, -texelSize.y),
float2(-texelSize.x, texelSize.y),
float2(texelSize.x, texelSize.y)
};
for (int i = 0; i < samples; i++)
{
float sampleDepth = tex2D(shadowMapSampler, shadowUV + offset[i]).r;
shadow += depth - bias > sampleDepth ? 0.0 : 1.0;
}
shadow /= samples;
return float4(shadow, shadow, shadow, 1.0);
}
この方法で影の境界が滑らかになり、より自然な見た目になります。
GPUベースパーティクルシステム
GPUベースパーティクルシステムは、パーティクルの位置や速度などの物理計算をGPU上で行い、高速かつ大量のパーティクルをリアルタイムに描画する技術です。
DirectX9では頂点シェーダーやピクセルシェーダーを活用し、CPU負荷を大幅に軽減できます。
基本的な構成
- 頂点バッファにパーティクルデータを格納
位置、速度、寿命などの情報を頂点属性として持たせます。
- 頂点シェーダーで物理計算
頂点シェーダー内でパーティクルの移動や加速度、寿命の減少を計算し、次フレームの位置を決定します。
- ピクセルシェーダーで描画
パーティクルの色や透明度、テクスチャを制御し、見た目を調整します。
頂点シェーダーでのパーティクル更新例
struct VS_INPUT
{
float3 position : POSITION;
float3 velocity : TEXCOORD0;
float life : TEXCOORD1;
};
struct VS_OUTPUT
{
float4 position : POSITION;
float life : TEXCOORD0;
};
float deltaTime;
VS_OUTPUT vs_particle(VS_INPUT input)
{
VS_OUTPUT output;
// 位置更新
float3 newPos = input.position + input.velocity * deltaTime;
// 寿命減少
float newLife = input.life - deltaTime;
output.position = float4(newPos, 1.0);
output.life = newLife;
return output;
}
このようにGPUでパーティクルの状態を更新し続けることで、CPUの負荷を抑えつつ大量のパーティクルをリアルタイムに処理できます。
これらの拡張テクニックは、DirectX9のシェーダープログラミングでより高度でリアルな表現を実現するための重要な技術です。
適切に組み合わせて活用することで、ゲームや3Dアプリケーションの表現力を大幅に向上させられます。
今後の発展的アイデア
ハイブリッドレンダリングへの応用
ハイブリッドレンダリングは、リアルタイムレンダリングとオフラインレンダリングの長所を組み合わせる手法で、リアルタイム性と高品質な表現の両立を目指します。
DirectX9のシェーダープログラミング技術を活かしつつ、より高度なレンダリング技術と組み合わせることで、表現力の向上が期待できます。
ハイブリッドレンダリングの概要
- リアルタイムレンダリング
GPUで高速に処理し、インタラクティブな描画を実現。
DirectX9のDeferred Renderingやポストプロセスエフェクトが代表例。
- オフラインレンダリング
レイトレーシングやグローバルイルミネーションなど、物理的に正確な光の挙動を計算。
高品質だが処理時間が長い。
DirectX9シェーダー技術の活用例
- プリコンピューテッドライトマップの利用
オフラインで計算したライトマップをDirectX9のテクスチャとして読み込み、リアルタイムシェーダーで合成。
これにより静的な間接光を高品質に表現。
- スクリーンスペース技術との組み合わせ
SSAOやスクリーンスペースリフレクション(SSR)など、リアルタイムで近似的に間接光を表現する技術と組み合わせます。
- レイトレーシング結果のテクスチャ化
オフラインで生成したレイトレーシング画像や影情報をテクスチャとして利用し、リアルタイムシーンに反映。
今後の展望
DirectX9のシェーダー技術を基盤に、ハイブリッドレンダリングの要素を取り入れることで、限られたリソースでも高品質な表現が可能になります。
特にライトマップや環境マップの活用、スクリーンスペース技術の応用は現実的なアプローチです。
最新APIへのコンバージョン準備
DirectX9は歴史あるAPIですが、最新のグラフィックスAPI(DirectX12、Vulkan、Metalなど)への移行が進んでいます。
今後の開発を見据え、DirectX9のシェーダープログラムやレンダリング技術を最新APIにスムーズに移行するための準備が重要です。
コンバージョンのポイント
- HLSLコードの互換性確認
DirectX9のHLSLはシェーダーモデル2.0/3.0が中心ですが、最新APIではより高機能なシェーダーモデル(5.0以降)が使えます。
コードの書き換えや最適化が必要です。
- シェーダーリフレクションの活用
最新APIではシェーダーの入力・出力情報を動的に取得できるリフレクション機能が充実しています。
これに対応したコード設計を心がけると移行が楽になります。
- レンダリングパイプラインの再設計
DirectX9の固定機能パイプラインやエフェクトフレームワークは最新APIでは廃止されているため、パイプラインステートオブジェクト(PSO)やルートシグネチャを使った設計に切り替えが必要です。
- マルチスレッドレンダリング対応
最新APIはマルチスレッドでのコマンド生成を前提としているため、DirectX9のシングルスレッド設計からの見直しが求められます。
移行のための具体的な準備
- シェーダーコードのモジュール化
共通処理や関数を分割し、再利用しやすい構造にします。
- 抽象化レイヤーの導入
DirectX9固有のAPI呼び出しを抽象化し、将来的にAPI切り替えが容易になるように設計。
- 最新APIの学習と検証
DirectX12やVulkanの基本的なレンダリングフローやシェーダー管理方法を理解し、小規模なプロトタイプを作成。
- ツールチェーンの整備
最新のHLSLコンパイラやデバッグツール、プロファイラを導入し、開発環境を整えます。
これらの準備を進めることで、DirectX9の資産を活かしつつ、将来的な技術進化に対応した開発が可能になります。
最新APIの恩恵を受けるためにも、早期からの計画的な移行検討が望まれます。
まとめ
本記事では、DirectX9のシェーダープログラミングにおける基礎から応用、最適化、デバッグ、拡張テクニックまで幅広く解説しました。
HLSLの文法やシェーダーモデルの制限を理解し、頂点・ピクセルシェーダーの実装やパラメータ管理を効率化する方法を紹介。
さらに、ポストプロセスやマルチパスレンダリング、GPUプロファイリングなど実践的な技術も網羅しています。
これにより、リアルタイム3D表現の品質向上とパフォーマンス最適化が可能となり、将来的な最新APIへの移行準備にも役立つ知識が得られます。