【C++】DirectX9 ピクセルシェーダー入門―HLSLで学ぶ高速ライティングとポストエフェクト
DirectX 9のピクセルシェーダーは、GPUで各ピクセルの色や深度を計算し、画面効果を自在に制御できる仕組みです。
HLSLで記述し、テクスチャ参照やライティング、ポストエフェクトを高速に実装できます。
固定機能より表現力が高く、リアルタイム描画に適しています。
DirectX9レンダリングパイプライン概要
DirectX9のレンダリングパイプラインは、3Dグラフィックスを画面に描画するための一連の処理ステージで構成されています。
各ステージは特定の役割を持ち、頂点データの処理から最終的なピクセルの色決定までを担当します。
DirectX9では、固定機能パイプラインに加えて、プログラマブルステージが導入されており、これにより開発者はシェーダープログラムを用いて柔軟かつ高度なグラフィックス効果を実現できます。
プログラマブルステージとは
プログラマブルステージとは、レンダリングパイプラインの中でプログラム可能な部分を指します。
DirectX9では主に「頂点シェーダー」と「ピクセルシェーダー」の2つのプログラマブルステージが存在します。
- 頂点シェーダー(Vertex Shader)
頂点シェーダーは、3Dモデルの頂点ごとに実行されるプログラムです。
頂点の位置変換や法線の計算、テクスチャ座標の補間などを行います。
これにより、頂点データを自由に加工でき、アニメーションや変形、ライティングの準備などが可能です。
- ピクセルシェーダー(Pixel Shader)
ピクセルシェーダーは、ラスタライズされたピクセルごとに実行されるプログラムです。
ここで最終的なピクセルの色や透明度、深度値などを計算します。
テクスチャのサンプリングやライティング計算、ポストエフェクトなどを実装でき、画面に表示される見た目を細かく制御できます。
プログラマブルステージの導入により、従来の固定機能パイプラインでは難しかった複雑なエフェクトやリアルタイムの物理ベースレンダリングが可能になりました。
HLSL(High-Level Shader Language)という高水準言語を使ってシェーダーを記述し、GPU上で高速に実行されます。
ピクセルシェーダーの役割
ピクセルシェーダーは、レンダリングパイプラインの最終段階に位置し、各ピクセルの色を決定する重要な役割を担います。
具体的には、以下のような処理を行います。
- テクスチャのサンプリング
モデルに貼り付けられたテクスチャ画像から、ピクセルに対応する色を取得します。
テクスチャ座標をもとに色を補間し、リアルな表面表現を実現します。
- ライティング計算
ピクセル単位での光の当たり具合を計算します。
法線ベクトルやライト方向、視点方向を用いて拡散反射や鏡面反射を計算し、物体の質感や陰影を表現します。
- 色の合成と調整
テクスチャカラーとライティング結果を組み合わせて最終色を決定します。
環境光や影、特殊効果のための色調整もここで行います。
- ポストエフェクトの適用
ブラーや色調補正、グロー効果などの画面全体にかかるエフェクトもピクセルシェーダーで実装可能です。
ピクセルシェーダーは、GPUの並列処理能力を活かして大量のピクセルを高速に処理します。
DirectX9ではShader Model 2.0や3.0がサポートされており、これらのバージョンによって使用可能な命令数や機能が異なります。
Shader Model 3.0ではループや分岐が使えるため、より複雑な処理が可能です。
以下にピクセルシェーダーの処理の流れを簡単にまとめます。
- 頂点シェーダーから補間されたテクスチャ座標や法線、ライト方向などの入力を受け取る
- テクスチャから色をサンプリングする
- 法線とライト方向の内積を計算し、拡散反射の強さを求める
- 環境光や鏡面反射を加味して最終色を計算する
- 出力カラーとしてレンダリングパイプラインに渡す
このように、ピクセルシェーダーは画面に表示される色の決定に直接関わるため、グラフィックスの見た目を大きく左右します。
HLSLを使って自由にプログラムできるため、リアルなライティングや多彩なエフェクトを実装する際の中心的な役割を果たします。
ピクセルシェーダーバージョンと制限
Shader Model 2.0
Shader Model 2.0はDirectX9世代で広く使われたピクセルシェーダーの仕様で、基本的なライティングやテクスチャマッピングを実装するのに適しています。
Shader Model 2.0の特徴は以下の通りです。
- 命令数制限
最大で64命令まで使用可能です。
これにより複雑な計算は制限されますが、基本的な拡散反射や環境光の計算は十分に行えます。
- レジスタ数制限
ピクセルシェーダーで使用できる定数レジスタは最大32個(float4単位)です。
これらはライトの色や位置、マテリアルパラメータなどの定数を格納します。
- テクスチャサンプル数
最大で4つのテクスチャを同時にサンプリング可能です。
複数のテクスチャを組み合わせたマルチテクスチャリングが可能です。
- 分岐命令の制限
条件分岐やループはサポートされていません。
すべての命令は直線的に実行されるため、複雑な制御構造は実装できません。
Shader Model 2.0は当時のハードウェアに最適化されており、軽量で高速なシェーダーを作成できますが、表現力には限界があります。
例えば、動的なループ処理や複雑な条件分岐を使ったエフェクトは実装できません。
Shader Model 3.0
Shader Model 3.0はShader Model 2.0の制限を大幅に緩和し、より高度なグラフィックス表現を可能にした仕様です。
DirectX9の後期に登場し、対応GPUも増えました。
- 命令数の増加
最大512命令まで使用可能で、複雑な計算や多段階の処理が可能です。
- レジスタ数の増加
定数レジスタは最大32個のままですが、動的にレジスタを割り当てることができ、より柔軟なプログラムが書けます。
- 動的分岐とループのサポート
if文やforループなどの制御構造が使えます。
これにより、条件によって処理を切り替えたり、繰り返し処理を行うことが可能です。
- テクスチャサンプル数の増加
最大で16個のテクスチャを同時にサンプリング可能です。
複雑なマルチテクスチャリングやシェーダー内でのテクスチャ合成が実現できます。
- その他の機能強化
頂点シェーダーとの連携が強化され、より高度なライティングモデルやポストエフェクトの実装が容易になりました。
Shader Model 3.0は表現力が高い反面、対応GPUが限定される場合があるため、ターゲット環境に応じて選択が必要です。
レジスタと命令数の比較
Shader Model 2.0と3.0の主な違いを表にまとめます。
項目 | Shader Model 2.0 | Shader Model 3.0 |
---|---|---|
最大命令数 | 64 | 512 |
定数レジスタ数 | 32 | 32 |
テクスチャサンプル数 | 4 | 16 |
動的分岐・ループ | 非対応 | 対応 |
対応GPU | 広範囲 | 一部限定 |
この表からわかるように、Shader Model 3.0は命令数やテクスチャサンプル数が大幅に増え、動的分岐も可能になっているため、より複雑でリアルなシェーダーを作成できます。
シェーダモデル選定のポイント
シェーダモデルを選ぶ際は、以下のポイントを考慮してください。
- ターゲットハードウェアの対応状況
Shader Model 3.0は新しいGPUでサポートされていますが、古い環境では動作しない場合があります。
対応GPUの普及率を確認しましょう。
- 必要な表現力
複雑なライティングやポストエフェクト、動的な処理が必要な場合はShader Model 3.0が適しています。
基本的なテクスチャマッピングや単純なライティングならShader Model 2.0で十分です。
- パフォーマンス要件
Shader Model 3.0は高機能ですが、命令数が多くなるとGPU負荷が増加します。
パフォーマンスを重視する場合はShader Model 2.0で軽量なシェーダーを作成することも検討してください。
- 開発の柔軟性
動的分岐やループを使いたい場合はShader Model 3.0が必須です。
これによりコードの可読性や保守性も向上します。
以上を踏まえ、開発環境や目的に応じて適切なシェーダモデルを選択することが重要です。
HLSL基本構文
データ型と精度指定
HLSL(High-Level Shader Language)では、シェーダー内で使用するデータ型が豊富に用意されています。
主にベクトルや行列を扱うため、グラフィックス処理に適した型が中心です。
代表的なデータ型は以下の通りです。
- float, float2, float3, float4
浮動小数点数型。
float
は単一のスカラー値、float2
は2成分のベクトル、float3
は3成分、float4
は4成分のベクトルを表します。
色や座標、法線などの表現に使います。
- int, int2, int3, int4
整数型。
主にループカウンタや条件判定に使います。
- bool
真偽値型。
条件分岐に使用します。
- matrix
行列型。
float4x4
のようにサイズを指定し、変換行列などに使います。
精度指定はDirectX9のHLSLでは明示的に指定することは少ないですが、モバイルや組み込み向けのShader Modelではmin16float
やhalf
などの低精度型もあります。
DirectX9のPC向けではfloat
が標準的に使われます。
変数の定義と初期化
HLSLでは変数の定義はC言語に似ています。
型名の後に変数名を書き、必要に応じて初期化も可能です。
float4 color = float4(1.0, 0.5, 0.0, 1.0); // オレンジ色のRGBA
float3 normal; // 法線ベクトル(初期化なし)
int index = 0; // 整数の初期化
構造体も定義でき、頂点シェーダーやピクセルシェーダーの入力・出力に使います。
struct PS_INPUT
{
float2 TexCoords : TEXCOORD0;
float3 Normal : NORMAL;
};
初期化は定数や一時変数に対して行い、シェーダーの実行時に動的に値を変更する場合は別途定数バッファや定数レジスタを使います。
シェーダ関数とエントリーポイント
HLSLのシェーダーは関数として記述し、エントリーポイントとなる関数を指定します。
エントリーポイントは頂点シェーダーならvs_main
、ピクセルシェーダーならps_main
など任意の名前を付けられますが、コンパイル時に指定します。
関数の基本構文は以下の通りです。
float4 ps_main(PS_INPUT input) : COLOR0
{
// 処理内容
return float4(1.0, 0.0, 0.0, 1.0); // 赤色を返す
}
- 戻り値の型は出力の型を表し、ピクセルシェーダーでは通常
float4
(RGBAカラー)です - 引数は入力構造体で、頂点シェーダーから補間されたデータが渡されます
- セマンティクス(後述)で出力の意味を指定します
セマンティクスの基本
セマンティクスはHLSLで変数や構造体のメンバに付ける注釈で、GPUがそのデータの意味を理解するために使います。
例えば、頂点の位置、法線、テクスチャ座標、カラーなどの役割を示します。
代表的なセマンティクスは以下の通りです。
セマンティクス | 用途 |
---|---|
POSITION | 頂点の位置 |
NORMAL | 法線ベクトル |
TEXCOORD0〜 | テクスチャ座標 |
COLOR0〜 | 頂点カラーやピクセルカラー |
SV_POSITION | 画面空間の位置(頂点シェーダー出力) |
セマンティクスは頂点シェーダーの入力・出力やピクセルシェーダーの入力・出力で使われ、パイプライン間でデータを正しく受け渡す役割を果たします。
COLORとTEXCOORDの使い分け
COLOR
とTEXCOORD
はどちらもピクセルシェーダーの入力として使われますが、用途が異なります。
- COLOR
頂点カラーや補間された色データを渡すために使います。
例えば、頂点ごとに異なる色を持たせたい場合に利用します。
float4
型でRGBAカラーを表現することが多いです。
- TEXCOORD
テクスチャ座標を渡すために使います。
float2
型が一般的で、テクスチャのどの位置をサンプリングするかを示します。
複数のテクスチャを使う場合はTEXCOORD0
, TEXCOORD1
のように番号を付けて区別します。
使い分けのポイントは、色データならCOLOR
、テクスチャ座標やその他の補間データならTEXCOORD
を使うことです。
GPU内部での最適化や意味の明確化に役立ちます。
以上の基本構文を理解することで、HLSLでのシェーダープログラミングの土台が築けます。
次のステップでは、これらの構文を使って実際にピクセルシェーダーを記述し、ライティングやテクスチャサンプリングを実装していきます。
C++側からのシェーダ管理
IDirect3DDevice9とシェーダの関係
DirectX9のレンダリングにおいて、IDirect3DDevice9
は描画処理の中心となるインターフェースです。
シェーダーの管理や実行もこのデバイスを通じて行います。
具体的には、ピクセルシェーダーや頂点シェーダーの設定、定数の送信、テクスチャのバインドなどをIDirect3DDevice9
のメソッドで操作します。
ピクセルシェーダーはIDirect3DDevice9::SetPixelShader
で設定し、描画時にGPUがそのシェーダーを使ってピクセル処理を行います。
シェーダーのコンパイル結果はIDirect3DPixelShader9
インターフェースのオブジェクトとして管理され、これをデバイスにセットする形です。
また、シェーダーに渡す定数(ライトの色や行列など)はSetPixelShaderConstantF
などの関数で送信します。
テクスチャはSetTexture
でバインドし、シェーダー内のサンプラーと紐づけます。
シェーダの読み込みとコンパイル
HLSLで記述したシェーダーコードは、C++側でコンパイルしてGPUが理解できるバイナリに変換する必要があります。
DirectX9ではD3DXCompileShaderFromFile
やD3DXCompileShader
関数を使ってコンパイルします。
以下はファイルからピクセルシェーダーをコンパイルし、IDirect3DPixelShader9
オブジェクトを生成する例です。
#include <d3d9.h>
#include <d3dx9.h>
#include <iostream>
IDirect3DPixelShader9* LoadPixelShader(IDirect3DDevice9* device, const char* filename, const char* entryPoint, const char* shaderModel)
{
ID3DXBuffer* shaderBuffer = nullptr;
ID3DXBuffer* errorBuffer = nullptr;
HRESULT hr = D3DXCompileShaderFromFileA(
filename,
nullptr,
nullptr,
entryPoint,
shaderModel,
0,
&shaderBuffer,
&errorBuffer,
nullptr);
if (FAILED(hr))
{
if (errorBuffer)
{
std::cerr << "Shader compile error: " << (char*)errorBuffer->GetBufferPointer() << std::endl;
errorBuffer->Release();
}
return nullptr;
}
IDirect3DPixelShader9* pixelShader = nullptr;
hr = device->CreatePixelShader(
(DWORD*)shaderBuffer->GetBufferPointer(),
&pixelShader);
shaderBuffer->Release();
if (FAILED(hr))
{
std::cerr << "Failed to create pixel shader." << std::endl;
return nullptr;
}
return pixelShader;
}
この関数は指定したHLSLファイルのピクセルシェーダーをコンパイルし、成功すればIDirect3DPixelShader9
のポインタを返します。
失敗時はエラーメッセージを標準エラー出力に表示します。
定数バッファの送信
シェーダー内で使う定数(ライトの色や行列など)は、C++側からGPUに送信する必要があります。
DirectX9ではピクセルシェーダー用の定数はSetPixelShaderConstantF
で送ります。
これはfloat4の配列として渡すため、構造体のメンバをfloat4単位に揃えて送るのが一般的です。
ライトパラメータの設定
例えば、ライトの色や環境光の色をピクセルシェーダーに渡す場合、以下のように定数を設定します。
struct LightParams
{
float lightColor[4]; // ライトの色RGBA
float ambientColor[4]; // 環境光の色RGBA
};
void SetLightConstants(IDirect3DDevice9* device, const LightParams& params)
{
// レジスタ0にライトの色をセット
device->SetPixelShaderConstantF(0, params.lightColor, 1);
// レジスタ1に環境光の色をセット
device->SetPixelShaderConstantF(1, params.ambientColor, 1);
}
この例では、ピクセルシェーダーの定数バッファのb0
に対応するレジスタ0にライト色、b1
に環境光色を送っています。
HLSL側の定数バッファとレジスタ番号を合わせることが重要です。
テクスチャのバインド
テクスチャはIDirect3DDevice9::SetTexture
でバインドします。
ピクセルシェーダーのサンプラーregister(s0)
に対応するスロット0にテクスチャをセットします。
void BindTexture(IDirect3DDevice9* device, IDirect3DTexture9* texture)
{
device->SetTexture(0, texture);
}
複数のテクスチャを使う場合は、SetTexture(1, texture2)
のようにスロット番号を変えてバインドします。
シェーダー内のサンプラーのregister(sN)
と対応させる必要があります。
これらの操作を組み合わせて、C++側からシェーダーの読み込み、コンパイル、定数送信、テクスチャバインドを行い、GPUでのピクセルシェーダー処理を制御します。
適切に管理することで、リアルタイムに変化するライティングやエフェクトを実現できます。
テクスチャサンプリングの基礎
DirectX9のピクセルシェーダーでリアルな表面表現を行うためには、テクスチャサンプリングの理解が不可欠です。
テクスチャサンプリングとは、テクスチャ画像からピクセルに対応する色を取得する処理であり、その品質や見た目はサンプラーステートの設定やフィルタリング、アドレッシングモードによって大きく変わります。
サンプラーステートの設定
サンプラーステートは、テクスチャのサンプリング方法を制御する設定群です。
DirectX9ではIDirect3DDevice9::SetSamplerState
関数を使って設定します。
主に以下のパラメータを指定します。
- D3DSAMP_MINFILTER
テクスチャの縮小時(ピクセルよりテクスチャが小さく表示される場合)のフィルタリング方法を指定します。
- D3DSAMP_MAGFILTER
テクスチャの拡大時(ピクセルよりテクスチャが大きく表示される場合)のフィルタリング方法を指定します。
- D3DSAMP_MIPFILTER
MipMapレベル間の補間方法を指定します。
- D3DSAMP_ADDRESSU / D3DSAMP_ADDRESSV
テクスチャ座標のU(横)方向、V(縦)方向のアドレッシングモードを指定します。
サンプラーステートの設定例を示します。
void SetSamplerStates(IDirect3DDevice9* device, DWORD samplerIndex)
{
// 縮小時はリニアフィルタリング
device->SetSamplerState(samplerIndex, D3DSAMP_MINFILTER, D3DTEXF_LINEAR);
// 拡大時もリニアフィルタリング
device->SetSamplerState(samplerIndex, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR);
// MipMap間もリニア補間
device->SetSamplerState(samplerIndex, D3DSAMP_MIPFILTER, D3DTEXF_LINEAR);
// テクスチャ座標の繰り返し設定(ラップ)
device->SetSamplerState(samplerIndex, D3DSAMP_ADDRESSU, D3DTADDRESS_WRAP);
device->SetSamplerState(samplerIndex, D3DSAMP_ADDRESSV, D3DTADDRESS_WRAP);
}
この設定により、テクスチャの拡大縮小時に滑らかな補間が行われ、テクスチャ座標が0〜1の範囲を超えた場合は繰り返し表示されます。
フィルタリングとアドレッシング
フィルタリング
フィルタリングは、テクスチャのサンプリング時に複数のテクセル(テクスチャのピクセル)から色を補間する方法です。
主なフィルタリングモードは以下の通りです。
フィルタリングモード | 説明 |
---|---|
D3DTEXF_POINT | 最近傍補間。高速だがジャギーが目立ちます。 |
D3DTEXF_LINEAR | 線形補間。滑らかな見た目になります。 |
D3DTEXF_ANISOTROPIC | 異方性フィルタリング。斜め方向のぼやけを抑制。高品質。 |
異方性フィルタリングは特に斜めから見たテクスチャの鮮明さを保つために有効ですが、パフォーマンスコストが高くなります。
アドレッシング
アドレッシングは、テクスチャ座標が0〜1の範囲を超えた場合の挙動を制御します。
主なモードは以下の通りです。
アドレッシングモード | 説明 |
---|---|
D3DTADDRESS_WRAP | 座標を0〜1の範囲で繰り返す(ラップ)。 |
D3DTADDRESS_CLAMP | 端の色を引き伸ばす(クランプ)。 |
D3DTADDRESS_MIRROR | 座標を反転しながら繰り返す(ミラー)。 |
D3DTADDRESS_BORDER | 範囲外はボーダーカラーを使用。 |
用途に応じて使い分けます。
例えば、繰り返し模様のテクスチャはWRAP
、境界で色を伸ばしたい場合はCLAMP
を使います。
MipMapの活用
MipMapは、テクスチャの縮小表示時に発生するモアレやちらつきを抑えるために、複数解像度のテクスチャを用意する技術です。
元のテクスチャ(レベル0)から段階的に解像度を半分にしたテクスチャを複数生成し、描画時の距離やサイズに応じて適切なレベルを選択します。
MipMapを使うことで、以下の効果が得られます。
- 描画品質の向上
遠くのオブジェクトのテクスチャが滑らかに見え、ちらつきやジャギーが減少します。
- パフォーマンスの向上
遠距離のテクスチャは低解像度のMipMapを使うため、メモリ帯域やキャッシュ効率が改善されます。
MipMapの生成はテクスチャ作成時に行い、DirectX9のD3DXCreateTextureFromFileEx
などの関数で自動生成できます。
IDirect3DTexture9* texture = nullptr;
HRESULT hr = D3DXCreateTextureFromFileEx(
device,
"texture.png",
D3DX_DEFAULT, D3DX_DEFAULT,
D3DX_DEFAULT, // MipLevels 自動生成
0,
D3DFMT_UNKNOWN,
D3DPOOL_MANAGED,
D3DX_DEFAULT,
D3DX_DEFAULT,
0,
nullptr,
nullptr,
&texture);
MipMapを有効にした場合、サンプラーステートのD3DSAMP_MIPFILTER
をD3DTEXF_LINEAR
やD3DTEXF_POINT
に設定して、MipMap間の補間方法を指定します。
これらの設定を適切に組み合わせることで、テクスチャの見た目を大きく改善し、リアルで滑らかな描画が可能になります。
特にゲームやリアルタイム3Dアプリケーションでは、パフォーマンスと品質のバランスを考慮しながらサンプラーステートを調整することが重要です。
代表的なライティング実装
リアルな3D表現に欠かせないのがライティング計算です。
DirectX9のピクセルシェーダーでよく使われる代表的なライティングモデルとして、ランバート拡散光とブリン・フォン鏡面反射があります。
ここではそれぞれの実装方法とポイントを解説し、複数ライト対応のテクニックも紹介します。
ランバート拡散光
ランバート拡散光は、表面が均一に光を拡散反射する理想的なモデルです。
光の強さは光源方向と法線ベクトルの内積で決まり、陰になる部分は0にクリップされます。
ピクセルシェーダー内での計算は以下のようになります。
float3 normal = normalize(input.Normal);
float3 lightDir = normalize(input.LightDir);
float diff = max(dot(normal, lightDir), 0.0);
float4 diffuse = diff * LightColor;
ここでinput.Normal
は補間された法線ベクトル、input.LightDir
はライトからピクセルへの方向ベクトルです。
dot
関数で内積を計算し、max
で陰の部分を0にします。
法線計算のポイント
- 正規化
法線ベクトルは補間される過程で長さが変わるため、必ずシェーダー内でnormalize
関数を使って正規化します。
正規化しないとライティングが不自然になります。
- 座標空間の統一
法線とライト方向は同じ座標空間で計算する必要があります。
通常はワールド空間かビュー空間に変換してから渡します。
- 法線マップの活用
より詳細な凹凸表現には法線マップを使い、ピクセル単位で法線を変化させることもあります。
ブリン・フォン鏡面反射
ブリン・フォンモデルは鏡面反射の計算に使われ、光沢のある表面のハイライトを表現します。
視線方向とライト方向の中間ベクトル(ハーフベクトル)と法線の角度を使って反射強度を計算します。
計算式は以下の通りです。
float3 viewDir = normalize(input.ViewDir);
float3 lightDir = normalize(input.LightDir);
float3 halfVec = normalize(viewDir + lightDir);
float spec = pow(max(dot(normal, halfVec), 0.0), SpecularPower);
float4 specular = spec * SpecularColor;
SpecularPower
は鏡面反射の鋭さを制御するパラメータで、大きいほどハイライトが小さく鋭くなりますSpecularColor
は鏡面反射の色です
ハーフベクトルの計算
- 正規化が必須
viewDir + lightDir
の結果は正規化しないと正しい角度計算ができません。
- 視線方向の取得
視線方向はカメラ位置からピクセル位置へのベクトルで、頂点シェーダーやC++側で計算して渡します。
- パフォーマンス考慮
ハーフベクトル計算は比較的軽量ですが、複数ライトの場合は計算コストが増えるため注意が必要です。
複数ライト対応
複数のライトを扱う場合、各ライトの拡散光と鏡面反射を合算して最終色を計算します。
ピクセルシェーダー内でループを使う方法と、ループアンローリングで展開する方法があります。
float4 finalColor = float4(0,0,0,0);
for (int i = 0; i < NUM_LIGHTS; ++i)
{
float3 lightDir = normalize(LightDirs[i]);
float diff = max(dot(normal, lightDir), 0.0);
float3 halfVec = normalize(viewDir + lightDir);
float spec = pow(max(dot(normal, halfVec), 0.0), SpecularPower);
finalColor += diff * LightColors[i] + spec * SpecularColors[i];
}
ループアンローリングのコツ
- Shader Modelの制限
Shader Model 2.0では動的ループが使えないため、ループを手動で展開(アンローリング)します。
- パフォーマンス向上
ループアンローリングによりGPUのパイプラインが効率的に動作し、分岐による遅延を減らせます。
- コード例
float4 finalColor = float4(0,0,0,0);
// ライト0
float3 lightDir0 = normalize(LightDirs[0]);
float diff0 = max(dot(normal, lightDir0), 0.0);
float3 halfVec0 = normalize(viewDir + lightDir0);
float spec0 = pow(max(dot(normal, halfVec0), 0.0), SpecularPower);
finalColor += diff0 * LightColors[0] + spec0 * SpecularColors[0];
// ライト1
float3 lightDir1 = normalize(LightDirs[1]);
float diff1 = max(dot(normal, lightDir1), 0.0);
float3 halfVec1 = normalize(viewDir + lightDir1);
float spec1 = pow(max(dot(normal, halfVec1), 0.0), SpecularPower);
finalColor += diff1 * LightColors[1] + spec1 * SpecularColors[1];
// 必要に応じてライト数分繰り返す
- 定数バッファの管理
複数ライトのパラメータは配列として定数バッファにまとめ、C++側から送信します。
- ライト数の制限
シェーダーの命令数や定数レジスタ数に注意し、ライト数を制限することが多いです。
これらのライティングモデルを組み合わせることで、リアルで多彩な光の表現が可能になります。
法線の正規化や座標空間の統一、パフォーマンスを考慮したループ展開などのポイントを押さえて実装してください。
カラーテクニック
ピクセルシェーダーでの色表現は、単にテクスチャの色を表示するだけでなく、様々なカラーテクニックを駆使することでより豊かな表現が可能になります。
ここではグラデーション合成、トーンマッピング、ガンマ補正の3つの基本的なカラーテクニックについて詳しく解説します。
グラデーション合成
グラデーション合成は、複数の色やテクスチャを滑らかにブレンドして色の変化を表現する技術です。
例えば、地形の高さに応じて色を変えたり、光の当たり具合で色調を変化させる際に使います。
HLSLでの基本的なグラデーション合成の例を示します。
float4 color1 = float4(1.0, 0.0, 0.0, 1.0); // 赤
float4 color2 = float4(0.0, 0.0, 1.0, 1.0); // 青
float blendFactor = saturate(input.TexCoords.y); // 0〜1の範囲にクランプ
float4 outputColor = lerp(color1, color2, blendFactor);
return outputColor;
lerp
関数は線形補間を行い、blendFactor
が0のときcolor1
、1のときcolor2
を返しますsaturate
は値を0〜1に制限します
この方法を応用して、テクスチャの色とグラデーションを組み合わせたり、複数のグラデーションを重ねることも可能です。
トーンマッピング
トーンマッピングは、HDR(高ダイナミックレンジ)で計算された明るい色を、ディスプレイの表示可能な範囲に収めるための処理です。
これにより、明暗差の大きいシーンでも自然な見た目を実現します。
代表的なトーンマッピングの手法に「Reinhardトーンマッピング」があります。
HLSLでの簡単な実装例は以下の通りです。
float3 hdrColor = input.Color.rgb; // HDRカラー
// Reinhardトーンマッピング
float3 mapped = hdrColor / (hdrColor + 1.0);
// ガンマ補正用に0〜1に正規化
mapped = saturate(mapped);
return float4(mapped, input.Color.a);
- 明るい部分は自然に抑えられ、暗い部分はほぼそのまま保持されます
- トーンマッピング後にガンマ補正を行うことが多いです
トーンマッピングはシーン全体の明るさやコントラストを調整するため、ポストエフェクトとしてピクセルシェーダーの最後に適用することが一般的です。
ガンマ補正
ガンマ補正は、ディスプレイの非線形な輝度特性に合わせて色を調整する処理です。
多くのディスプレイはガンマ2.2程度の特性を持つため、シェーダー内でガンマ補正を行わないと色が暗く見えたり不自然になります。
ガンマ補正の基本的な式は以下の通りです。
\[C_{\text{corrected}} = C_{\text{input}}^{\frac{1}{\gamma}}\]
ここで\(\gamma\)は一般的に2.2です。
HLSLでの実装例は以下のようになります。
float gamma = 2.2;
float3 color = input.Color.rgb;
// ガンマ補正(ガンマ逆数を使う)
float3 corrected = pow(color, 1.0 / gamma);
return float4(corrected, input.Color.a);
pow
関数で各色成分に対してべき乗を計算します- 入力色は0〜1の範囲であることが前提です
逆に、テクスチャやレンダリング結果をガンマ空間に変換する場合は、ガンマ値をかける処理を行います。
多くのレンダリングパイプラインでは、最終出力前にガンマ補正を適用して正しい色再現を実現しています。
これらのカラーテクニックを組み合わせることで、より自然で美しい映像表現が可能になります。
グラデーション合成で色の変化を滑らかにし、トーンマッピングで明暗差を調整し、ガンマ補正でディスプレイに適した色を出力する流れが基本となります。
ポストエフェクト基礎
ポストエフェクトは、レンダリングされた画像に対して画面全体の色調整や特殊効果を加える技術です。
DirectX9のピクセルシェーダーを活用して、リアルタイムに多彩な視覚効果を実現できます。
ここでは代表的なスクリーンスペーステクニック、ブラー効果、カラーフィルタについて詳しく解説します。
スクリーンスペーステクニック
スクリーンスペーステクニックとは、画面に描画された最終画像(フレームバッファ)をテクスチャとして扱い、そのテクスチャをピクセルシェーダーで加工する方法です。
これにより、シーン全体に対してエフェクトをかけることが可能になります。
実装の流れは以下の通りです。
- シーンを通常通りレンダリングし、レンダーターゲット(テクスチャ)に描画します。
- そのレンダーターゲットをピクセルシェーダーの入力テクスチャとしてバインドします。
- フルスクリーンクアッド(画面全体を覆う四角形)を描画し、ピクセルシェーダーでテクスチャを加工します。
- 加工結果を画面に出力します。
この方法は、ブラーや色調補正、グロー効果など多くのポストエフェクトの基盤となります。
ブラーとぼかし効果
ブラー(ぼかし)は、画像のエッジを滑らかにし、柔らかい印象を与える効果です。
ポストエフェクトとしてよく使われ、動きの表現やグロー効果の下地としても活用されます。
ブラーの基本的な実装は、周囲のピクセルの色を平均化する「畳み込みフィルタ」を用います。
単純なボックスブラーから高品質なガウシアンブラーまで様々な手法があります。
ガウシアンブラー
ガウシアンブラーは、ガウス関数に基づく重み付けを行うぼかしで、自然で滑らかなぼかし効果が得られます。
重みは中心ピクセルに近いほど大きく、遠いほど小さくなります。
HLSLでの簡単な1次元ガウシアンブラーの例を示します。
ここでは横方向のブラーを行います。
sampler2D inputTexture : register(s0);
float2 texelSize; // 1ピクセルのテクスチャ座標の大きさ
float4 ps_main(float2 uv : TEXCOORD) : COLOR
{
float4 color = float4(0,0,0,0);
// ガウス重み(例: 5サンプル)
float weights[5] = {0.204164, 0.304005, 0.093913, 0.010381, 0.000229};
// 中心ピクセル
color += tex2D(inputTexture, uv) * weights[0];
// 左右のピクセル
for (int i = 1; i < 5; ++i)
{
color += tex2D(inputTexture, uv + float2(texelSize.x * i, 0)) * weights[i];
color += tex2D(inputTexture, uv - float2(texelSize.x * i, 0)) * weights[i];
}
return color;
}
texelSize
はC++側で1.0f / テクスチャ幅
と1.0f / テクスチャ高さ
を計算して渡します- 実際には縦方向のブラーと組み合わせて2パスで処理することが多いです
ガウシアンブラーは計算コストが高いため、パフォーマンスと品質のバランスを考慮してサンプル数や重みを調整します。
カラーフィルタと色調整
カラーフィルタは、画像の色味を変えたり、特定の色成分を強調・抑制する処理です。
色調整はポストエフェクトの基本で、シーンの雰囲気やムードを演出するのに役立ちます。
代表的なカラーフィルタの例をHLSLで示します。
sampler2D inputTexture : register(s0);
float4 ps_main(float2 uv : TEXCOORD) : COLOR
{
float4 color = tex2D(inputTexture, uv);
// セピア調フィルタ
float3 sepia;
sepia.r = dot(color.rgb, float3(0.393, 0.769, 0.189));
sepia.g = dot(color.rgb, float3(0.349, 0.686, 0.168));
sepia.b = dot(color.rgb, float3(0.272, 0.534, 0.131));
return float4(sepia, color.a);
}
- セピア調は古い写真のような暖かみのある色調を作り出します
- 他にもグレースケール変換や色相・彩度の調整などがよく使われます
色調整は単純な乗算や加算だけでなく、色空間変換やカーブ補正を組み合わせることで多彩な表現が可能です。
これらのポストエフェクト基礎技術を組み合わせることで、ゲームやアプリケーションの映像表現を大きく向上させられます。
スクリーンスペーステクニックを理解し、ブラーやカラーフィルタを適切に実装することが重要です。
デバッグとプロファイリング
DirectX9のピクセルシェーダー開発において、正確な動作確認とパフォーマンスの最適化は非常に重要です。
ここでは、代表的なツールや手法を用いたデバッグとプロファイリングの方法について詳しく説明します。
PIX for Windowsの活用
PIX for Windowsは、Microsoftが提供するDirectXアプリケーション向けの強力なデバッグ・プロファイリングツールです。
DirectX9のレンダリングパイプラインを詳細に解析でき、シェーダーの動作確認やパフォーマンスボトルネックの特定に役立ちます。
主な機能は以下の通りです。
- フレームキャプチャ
実行中のアプリケーションから特定のフレームをキャプチャし、そのフレームのレンダリングコマンドやリソース状態を詳細に調査できます。
- シェーダーデバッグ
ピクセルシェーダーや頂点シェーダーのソースコードをステップ実行し、変数の値や命令の実行結果を確認できます。
これにより、シェーダーのロジックエラーを効率的に発見できます。
- パフォーマンス解析
GPUの負荷や描画時間を計測し、どのシェーダーや描画コールがボトルネックになっているかを特定できます。
使い方のポイントとしては、まずアプリケーションをPIXで起動し、問題のあるフレームをキャプチャします。
次に、シェーダーデバッグモードで該当シェーダーの命令をステップ実行し、期待通りの動作かを確認します。
パフォーマンス解析では、GPU時間の多いシェーダーを重点的に最適化します。
シェーダアセンブリの確認
HLSLで記述したシェーダーはコンパイル時にGPUが理解できるアセンブリコードに変換されます。
シェーダアセンブリを確認することで、コンパイル結果の最適化状況や命令の詳細を把握できます。
DirectX9のD3DXCompileShader
関数などでコンパイル時にアセンブリコードを取得可能です。
以下はコンパイル時にアセンブリを取得する例です。
ID3DXBuffer* shaderBuffer = nullptr;
ID3DXBuffer* errorBuffer = nullptr;
ID3DXBuffer* asmBuffer = nullptr;
HRESULT hr = D3DXCompileShaderFromFileA(
"shader.hlsl",
nullptr,
nullptr,
"ps_main",
"ps_3_0",
D3DXSHADER_ENABLE_BACKWARDS_COMPATIBILITY,
&shaderBuffer,
&errorBuffer,
&asmBuffer);
if (asmBuffer)
{
const char* asmCode = (const char*)asmBuffer->GetBufferPointer();
printf("Shader Assembly:\n%s\n", asmCode);
asmBuffer->Release();
}
アセンブリコードを読むことで、無駄な命令や不要な定数の使用を発見し、シェーダーの効率化に役立てられます。
また、特定のGPUでの動作を理解する際にも有効です。
フレームタイム解析
フレームタイム解析は、1フレームの描画にかかる時間を計測し、パフォーマンスのボトルネックを特定する手法です。
DirectX9では、CPU側の計測だけでなく、GPUの描画時間も計測可能です。
代表的な方法は以下の通りです。
- CPUタイマーによる計測
QueryPerformanceCounter
などの高精度タイマーを使い、BeginScene
からEndScene
までの時間を計測します。
ただし、GPUの処理は非同期のため正確なGPU時間は得られません。
- GPUタイマークエリ
DirectX9のIDirect3DQuery9
インターフェースを使い、GPUの描画時間を計測します。
D3DQUERYTYPE_TIMESTAMP
やD3DQUERYTYPE_TIMESTAMPDISJOINT
を利用してGPUの処理時間を取得可能です。
- PIXのパフォーマンス解析機能
PIXを使うとGPUの各描画コールの時間を詳細に解析でき、どのシェーダーや描画処理が重いかを視覚的に把握できます。
フレームタイム解析の結果をもとに、シェーダーの命令数削減や定数更新の最適化、不要な描画の削減などを行い、パフォーマンス向上を図ります。
これらのデバッグ・プロファイリング手法を活用することで、DirectX9のピクセルシェーダー開発における問題発見と効率的な最適化が可能になります。
特にPIXは強力なツールなので積極的に利用しましょう。
パフォーマンス最適化
DirectX9のピクセルシェーダーはGPU上で大量のピクセルを並列処理するため、パフォーマンスの最適化が重要です。
ここでは、シェーダーの実行効率を高めるための具体的なテクニックを解説します。
定数の更新頻度を抑える
ピクセルシェーダーに渡す定数(ライトの色や行列など)は、CPUからGPUへ転送されるため、更新頻度が高いとバス帯域やパイプラインの負荷が増大します。
定数の更新はできるだけ必要なタイミングに限定し、無駄な更新を避けることが重要です。
- フレーム単位での更新に留める
毎フレーム同じ値を送るのは問題ありませんが、ピクセル単位やドローコール単位で頻繁に更新するとパフォーマンスが低下します。
- 定数バッファのまとめ
複数の定数を1つのバッファにまとめて一括更新すると、APIコール回数を減らせます。
- 変更があった場合のみ更新
値が変わらない場合は更新をスキップし、無駄な転送を防ぎます。
C++側のコード例:
static LightParams prevParams = {};
if (memcmp(&prevParams, ¤tParams, sizeof(LightParams)) != 0)
{
device->SetPixelShaderConstantF(0, currentParams.lightColor, 1);
device->SetPixelShaderConstantF(1, currentParams.ambientColor, 1);
prevParams = currentParams;
}
このように前回の値と比較して変更があれば更新する方法が有効です。
動的分岐の最小化
Shader Model 3.0以降で動的分岐(if文やループ)が使えますが、GPUのアーキテクチャによっては分岐がパフォーマンス低下の原因になることがあります。
特に分岐の条件がピクセルごとに異なる場合、全ての分岐パスが実行される「分岐の爆発」が起こることがあります。
- 分岐の使用は必要最低限に
条件分岐は複雑な処理を避けるために使いますが、単純な場合は分岐を使わずに数学的な式で代替することが望ましいです。
- 分岐の条件をできるだけ均一に
ピクセルごとに条件が大きく異なるとパフォーマンスが悪化するため、条件が画面全体で似た値になるよう工夫します。
- ループの展開(アンローリング)
ループ回数が固定なら手動で展開し、分岐を減らすことで効率化できます。
例:条件分岐を使わずにmax
関数でクリップする
float diff = max(dot(normal, lightDir), 0.0);
RT内メモリ帯域削減
レンダーターゲット(Render Target、RT)への書き込みはメモリ帯域を大きく消費します。
ピクセルシェーダーのパフォーマンスを向上させるためには、RTへの書き込みを最小限に抑えることが重要です。
- 不要なピクセルの書き込みを避ける
アルファテストや深度テストで描画不要なピクセルを早期に除外し、無駄な書き込みを減らします。
- 出力カラーの簡素化
複雑な計算結果をそのまま書き込むのではなく、必要最低限の情報に絞ることで帯域を節約します。
- 複数レンダーターゲット(MRT)の活用
複数のRTに同時に書き込む場合は、必要なRTだけに書き込み、不要なRTは無効化します。
MRT使用時の注意
複数レンダーターゲット(MRT)を使うと、一度の描画で複数のRTに同時に出力できますが、以下の点に注意が必要です。
- 帯域幅の増加
MRTは同時に複数のRTに書き込むため、メモリ帯域の消費が単純に倍増します。
パフォーマンスに大きな影響を与えるため、必要なRT数を最小限に抑えます。
- フォーマットの整合性
MRTに設定するRTは同じフォーマットやサイズである必要があります。
異なる場合は動作が不安定になることがあります。
- シェーダーの出力数制限
Shader Model 2.0では最大4つ、3.0では最大8つのRTに対応していますが、GPUによっては制限が異なるため事前に確認が必要です。
- パイプラインの設定
MRTを使う場合はIDirect3DDevice9::SetRenderTarget
を複数回呼び出してRTを設定し、シェーダー側で対応する出力を用意します。
これらの最適化ポイントを意識してシェーダーやレンダリングコードを設計することで、DirectX9環境でも高いパフォーマンスを維持しつつ美しいグラフィックスを実現できます。
よくある落とし穴
DirectX9のピクセルシェーダー開発では、パフォーマンスや表現力を追求する中でさまざまな問題に直面します。
ここでは特に多くの開発者が遭遇しやすい「精度不足によるバンディング」「異なるGPU間の互換性」「D3DERR_INVALIDCALLの対処」について詳しく解説します。
精度不足によるバンディング
バンディングとは、グラデーションや滑らかな色変化が階段状に見えてしまう現象です。
ピクセルシェーダーでの計算精度が不足すると発生しやすく、特に暗い部分や微妙な色の変化で目立ちます。
- 原因
DirectX9のShader Model 2.0や3.0では、ピクセルシェーダーの計算は基本的に16ビット浮動小数点(half精度)で行われることが多く、これが精度不足の一因です。
特に色の補間やライティング計算で小さな差分が失われるとバンディングが発生します。
- 対策
- 高精度の使用
HLSLでfloat
型を使い、可能な限り32ビット浮動小数点で計算します。
ただし、ハードウェアによっては内部的に16ビットに変換される場合もあります。
- ノイズの追加
微細なノイズ(ディザリング)を加えることでバンディングを目立たなくする手法があります。
- ガンマ補正の適切な適用
ガンマ空間での計算や補正を正しく行うことで、色の階調を滑らかに見せられます。
- 色の範囲を広げる
計算途中で色の範囲を狭めず、できるだけ広い範囲で処理することも効果的です。
- 注意点
精度を上げるとパフォーマンスに影響が出る場合があるため、バランスを考慮してください。
異なるGPU間の互換性
DirectX9は多くのGPUで動作しますが、GPUメーカーやモデルによってシェーダーの動作や性能に差異があります。
これが原因で、あるGPUでは正常に動作するシェーダーが別のGPUで問題を起こすことがあります。
- 主な問題例
- 命令数やリソース制限の違い
Shader Modelのバージョンは同じでも、実際に使える命令数や定数レジスタ数が異なる場合があります。
- 動的分岐のサポート差
一部のGPUは動的分岐を正しくサポートしないことがあります。
- 浮動小数点の精度や丸め誤差
計算結果が微妙に異なり、見た目に差が出ることがあります。
- テクスチャフォーマットの対応状況
特定の圧縮テクスチャやフォーマットが使えない場合があります。
- 対策
- ターゲットGPUの明確化
対応させたいGPUの仕様を事前に調査し、制限内でシェーダーを設計します。
- Shader Modelの適切な選択
互換性を重視するならShader Model 2.0を選ぶことも検討します。
- テスト環境の多様化
複数のGPUで動作確認を行い、問題を早期に発見します。
- フォールバックの用意
高機能シェーダーが動作しない場合に備え、簡易版シェーダーを用意します。
D3DERR_INVALIDCALLの対処
D3DERR_INVALIDCALL
はDirectX9のAPI呼び出しでよく発生するエラーで、無効な呼び出しやパラメータの不整合が原因です。
ピクセルシェーダー関連では以下のようなケースが多いです。
- 原因例
- シェーダーのコンパイルに失敗しているのに、そのままセットしようとした
- シェーダーのバイナリが不正、または破損しています
- 定数バッファのサイズやレジスタ番号がシェーダーの期待と合っていない
- テクスチャやサンプラーのバインドが正しくない
- デバイスの状態が不適切(例:デバイスがリセット中など)
- 対策
- コンパイルエラーのチェック
シェーダーコンパイル時にエラーメッセージを必ず確認し、問題があれば修正します。
- API呼び出しの戻り値確認
HRESULT
をチェックし、失敗時は詳細な原因をログに出します。
- 定数バッファの整合性確認
シェーダー側の定数バッファ定義とC++側の送信内容が一致しているか確認します。
- テクスチャとサンプラーの対応
シェーダーのサンプラー番号とC++側のSetTexture
のスロットが合っているかを確認。
- デバイス状態の管理
デバイスがリセットされていないか、適切に初期化されているかを確認します。
- デバッグ方法
PIXなどのツールでAPI呼び出しのトレースを行い、どの呼び出しでエラーが発生しているか特定します。
これらの落とし穴は開発初期から注意深く対処することで、トラブルを未然に防ぎ、安定したシェーダー動作を実現できます。
特に精度問題とGPU間の互換性は見た目や動作に大きく影響するため、十分なテストと調整が必要です。
サンプルコード拡張例
DirectX9のピクセルシェーダーを使った基本的なライティングやテクスチャサンプリングに慣れてきたら、より高度な表現を目指してサンプルコードを拡張してみましょう。
ここでは「法線マップによるディテール追加」「セルシェーディング」「夜景Bloom効果」の3つの代表的な拡張例を紹介します。
法線マップによるディテール追加
法線マップは、テクスチャの色データに法線ベクトルの微細な変化を格納し、ピクセル単位での凹凸表現を可能にする技術です。
これにより、ポリゴン数を増やさずにリアルな表面ディテールを表現できます。
実装例(HLSLピクセルシェーダー)
sampler2D DiffuseMap : register(s0);
sampler2D NormalMap : register(s1);
float3 LightDir; // ライト方向(ワールド空間)
float3 ViewDir; // 視線方向(ワールド空間)
struct PS_INPUT
{
float2 TexCoords : TEXCOORD0;
float3 Tangent : TANGENT;
float3 Binormal : BINORMAL;
float3 Normal : NORMAL;
};
float4 ps_main(PS_INPUT input) : COLOR0
{
// 法線マップから法線を取得(0〜1の範囲を-1〜1に変換)
float3 normalTex = tex2D(NormalMap, input.TexCoords).rgb * 2.0 - 1.0;
// タンジェント空間の基底を作成
float3x3 TBN = float3x3(normalize(input.Tangent), normalize(input.Binormal), normalize(input.Normal));
// 法線マップの法線をワールド空間に変換
float3 normal = normalize(mul(normalTex, TBN));
// ライト方向と視線方向を正規化
float3 lightDir = normalize(LightDir);
float3 viewDir = normalize(ViewDir);
// 拡散光計算
float diff = max(dot(normal, lightDir), 0.0);
// テクスチャカラー取得
float4 baseColor = tex2D(DiffuseMap, input.TexCoords);
// 簡単な鏡面反射(ブリン・フォン)
float3 halfVec = normalize(lightDir + viewDir);
float spec = pow(max(dot(normal, halfVec), 0.0), 16.0);
float4 finalColor = baseColor * diff + float4(spec, spec, spec, 1.0);
return saturate(finalColor);
}
- 法線マップはRGBで法線のXYZ成分を格納し、
0〜1
の値を-1〜1
に変換します - タンジェント空間(TBN行列)を使って法線マップの法線をワールド空間に変換します
- 拡散光と鏡面反射を計算し、テクスチャカラーに反映します
この手法により、凹凸の細かい表現が可能になり、リアルな質感を演出できます。
セルシェーディング
セルシェーディングは、アニメ調の輪郭強調や色の階調を意図的に減らした表現方法です。
ピクセルシェーダーでライティングの結果を段階的に区切ることで実現します。
実装例(HLSLピクセルシェーダー)
float4 ps_main(PS_INPUT input) : COLOR0
{
float3 normal = normalize(input.Normal);
float3 lightDir = normalize(LightDir);
// 拡散光の計算
float diff = dot(normal, lightDir);
// 階調を4段階に区切る
if (diff > 0.75)
diff = 1.0;
else if (diff > 0.5)
diff = 0.7;
else if (diff > 0.25)
diff = 0.4;
else
diff = 0.1;
float4 baseColor = tex2D(DiffuseMap, input.TexCoords);
float4 finalColor = baseColor * diff;
return finalColor;
}
diff
の値を閾値で区切り、滑らかなグラデーションを階段状に変換します- 輪郭線は別途エッジ検出シェーダーやポストエフェクトで強調することが多いです
この方法で、アニメや漫画のような独特の表現が可能になります。
夜景Bloom効果
Bloom効果は、明るい部分がにじんで輝いて見える効果で、夜景や光源の表現に使われます。
基本的には明るい部分を抽出し、ぼかし(ブラー)をかけて元画像に加算します。
実装の流れ
- 明るい部分の抽出
ピクセルシェーダーで輝度が一定以上の部分だけを抽出し、他は黒にします。
- ブラー処理
抽出したテクスチャにガウシアンブラーなどのぼかしをかけます。
- 加算合成
元の画像にぼかした輝度部分を加算して合成します。
明るい部分抽出の例(HLSL)
float4 ps_brightExtract(float2 uv : TEXCOORD) : COLOR0
{
float4 color = tex2D(SceneTexture, uv);
// 輝度計算(加重平均)
float luminance = dot(color.rgb, float3(0.299, 0.587, 0.114));
// 閾値以上ならそのまま、未満は黒に
if (luminance > 0.8)
return color;
else
return float4(0,0,0,0);
}
SceneTexture
はレンダリング済みのシーンテクスチャです- 輝度が0.8以上の部分だけを抽出しています
この後、抽出結果にブラーをかけて元画像に加算することで、夜景の光がにじむような美しいBloom効果が得られます。
これらの拡張例は、基本的なピクセルシェーダーの知識を活かしつつ、より高度で魅力的なグラフィックス表現を実現するためのステップです。
実際の開発では、これらを組み合わせたりパラメータを調整して独自の表現を追求してください。
さらなる発展
DirectX9のピクセルシェーダーを使った基本的なグラフィックス表現をマスターした後は、より高度な技術や最新の手法に挑戦することで、表現力や効率を大幅に向上させられます。
ここではShader Model 3.0の特徴を活かしたループと分岐の活用、動的ライトシャフトの実装、そしてディファードシェーディングへの応用について解説します。
Shader Model 3.0のループと分岐
Shader Model 3.0では、Shader Model 2.0までの制限だった動的なループや条件分岐がサポートされ、シェーダーの表現力が飛躍的に向上しました。
これにより、複雑な計算や柔軟な処理が可能になります。
- 動的ループ
ループ回数を変数で制御できるため、ライト数やテクスチャ数に応じて処理を柔軟に変えられます。
例えば、複数ライトのループ処理を動的に行うことが可能です。
- 条件分岐
if
文やswitch
文が使え、ピクセルごとに異なる処理を実装できます。
これにより、複雑なエフェクトや最適化が容易になります。
例:動的ループで複数ライト処理
#define MAX_LIGHTS 8
uniform int numLights;
uniform float3 LightDirs[MAX_LIGHTS];
uniform float4 LightColors[MAX_LIGHTS];
float4 ps_main(PS_INPUT input) : COLOR0
{
float3 normal = normalize(input.Normal);
float4 color = float4(0,0,0,0);
for (int i = 0; i < numLights; ++i)
{
float diff = max(dot(normal, normalize(LightDirs[i])), 0.0);
color += diff * LightColors[i];
}
return saturate(color);
}
numLights
を動的に変えられるため、ライト数の変更に柔軟に対応可能です- Shader Model 2.0ではループアンローリングが必要でしたが、3.0では簡潔に書けます
ただし、動的分岐はGPUによってはパフォーマンスに影響する場合があるため、使用時は注意が必要です。
動的ライトシャフト
ライトシャフト(ゴッドレイ)は、光が物体の間を通り抜ける際に発生する光の筋を表現するエフェクトです。
動的ライトシャフトは、シーンの光源やカメラの位置に応じてリアルタイムに変化するため、臨場感のある表現が可能です。
実装のポイント
- スクリーンスペースでの処理
ライトシャフトはポストエフェクトとしてスクリーンスペースで実装されることが多いです。
レンダリング済みの深度バッファや光源位置を使い、光の散乱を計算します。
- サンプリングの積み重ね
光源方向に沿って複数回テクスチャをサンプリングし、明るさを加算して光の筋を作ります。
- 減衰とノイズの調整
光の強さは距離や遮蔽物によって減衰させ、自然な見た目にします。
ノイズや揺らぎを加えることでリアルさを増します。
簡単なサンプルコード(HLSL)
sampler2D SceneTexture : register(s0);
float2 LightScreenPos; // 光源のスクリーン座標
int NumSamples = 30;
float Decay = 0.95;
float Exposure = 0.3;
float4 ps_lightShaft(float2 uv : TEXCOORD) : COLOR0
{
float2 deltaTexCoord = (LightScreenPos - uv) / NumSamples;
float2 coord = uv;
float illuminationDecay = 1.0;
float4 color = float4(0,0,0,0);
for (int i = 0; i < NumSamples; ++i)
{
coord += deltaTexCoord;
float4 sample = tex2D(SceneTexture, coord);
sample *= illuminationDecay * Exposure;
color += sample;
illuminationDecay *= Decay;
}
return color;
}
- 光源からピクセルまでの方向に沿ってテクスチャをサンプリングし、光の筋を作成します
Decay
やExposure
で光の減衰や明るさを調整します
ディファードシェーディングへの橋渡し
ディファードシェーディングは、ジオメトリ情報(位置、法線、色など)を複数のレンダーターゲット(Gバッファ)に一度に書き出し、後段のシェーダーでライティングを行う手法です。
これにより、多数のライトを効率的に処理できるメリットがあります。
DirectX9での実装ポイント
- 複数レンダーターゲット(MRT)の活用
IDirect3DDevice9::SetRenderTarget
で複数のRTを設定し、頂点シェーダーやピクセルシェーダーでGバッファに情報を書き込みます。
- Gバッファの内容
典型的には、ワールド座標やビュー座標の位置、法線、拡散色、スペキュラ色などを格納します。
- ライティングパスの分離
ライティングは別のパスでGバッファを参照し、ピクセルごとにライト計算を行います。
これによりライト数に依存しない描画が可能です。
シンプルなGバッファ出力例(HLSL)
struct PS_OUTPUT
{
float4 Position : COLOR0;
float4 Normal : COLOR1;
float4 Diffuse : COLOR2;
};
PS_OUTPUT ps_gbuffer(PS_INPUT input)
{
PS_OUTPUT output;
output.Position = float4(input.WorldPos, 1.0);
output.Normal = float4(normalize(input.Normal), 0.0);
output.Diffuse = tex2D(DiffuseMap, input.TexCoords);
return output;
}
- MRTで3つのRTにそれぞれ位置、法線、拡散色を出力しています
- 後段のライティングシェーダーでこれらを参照し、複数ライトの計算を効率化します
メリットと課題
- メリット
- 多数のライトを効率的に処理可能
- ライティング計算を後段にまとめられるため柔軟性が高い
- 課題
- メモリ帯域の消費が増える
- MRTの制限やハードウェア依存性に注意が必要
Shader Model 3.0の機能を活かしたループ・分岐の活用や動的ライトシャフトの実装、そしてディファードシェーディングの基礎を理解することで、DirectX9環境でも高度で効率的なリアルタイムレンダリングが可能になります。
これらの技術は最新のグラフィックス開発においても重要な基盤となるため、ぜひ習得を目指してください。
まとめ
本記事では、DirectX9のピクセルシェーダーをC++とHLSLで扱う基本から応用までを解説しました。
レンダリングパイプラインの理解やShader Modelの違い、HLSLの基本構文、シェーダーの管理方法を押さえたうえで、テクスチャサンプリングや代表的なライティング技術、ポストエフェクトの基礎も紹介しています。
さらに、パフォーマンス最適化やデバッグ手法、実践的な拡張例、さらにはShader Model 3.0の高度な機能やディファードシェーディングへの応用まで幅広くカバーしました。
これにより、DirectX9環境で効率的かつ美しいリアルタイムグラフィックスを実装するための知識が身につきます。