【C++】DirectX9で始めるシャドウマッピング入門:基礎から高品質影生成まで
DirectX9でのシャドウマッピングは、ライト視点で深度テクスチャを取得し、カメラ視点で深度比較を行って影を描く方法です。
深度バイアスとテクスチャ解像度が影品質を左右し、カスケード分割やPCFを併用するとセルフシャドウやジャギーを抑えつつ自然な陰影を得られます。
シャドウマッピングの基本フロー
シャドウマッピングは、3Dグラフィックスにおいてリアルな影を表現するための代表的な技術です。
DirectX9を使ってシャドウマッピングを実装する際には、影を生成するための一連のレンダリング処理の流れを理解することが重要です。
ここでは、シャドウマッピングの基本的なフローとDirectX9でのレンダリングパス構成について詳しく解説します。
影生成パスの全体像
シャドウマッピングの影生成は、大きく分けて2つのレンダリングパスから成り立っています。
これらのパスは、影の元となる深度情報を取得し、その情報をもとに影を描画するために使われます。
- ライト視点からの深度マップ生成パス
まず、ライト(光源)の視点からシーンをレンダリングします。
このとき、カメラ視点とは異なり、光が当たる方向から見たシーンの深度情報だけを取得します。
深度情報は、光源から各ピクセルまでの距離を表し、これをシャドウマップと呼ばれるテクスチャに格納します。
このパスでは、色の情報は不要で、深度値のみをレンダリングターゲットに書き込みます。
これにより、光源から見て遮蔽物となるオブジェクトの位置が記録されます。
- カメラ視点からのシーン描画パス
次に、実際に画面に表示されるカメラの視点からシーンをレンダリングします。
このとき、先ほど作成したシャドウマップを参照し、各ピクセルが光源から見て遮られているかどうかを判定します。
判定は、カメラ視点のピクセル位置をライト視点の座標系に変換し、シャドウマップの深度値と比較することで行います。
もしピクセルの深度がシャドウマップの深度より大きければ、そのピクセルは影の中にあると判断し、暗く描画します。
この2段階のレンダリングパスを経て、リアルな影がシーンに反映されます。
シャドウマップの解像度やバイアス調整、サンプリング方法などによって影の品質が大きく変わるため、これらのパラメータを適切に設定することが重要です。
DirectX9で採用されるレンダリングパス構成
DirectX9でシャドウマッピングを実装する際には、上記の基本フローを踏まえたレンダリングパスの構成を理解しておく必要があります。
DirectX9の固定機能パイプラインやシェーダープログラミングを活用しながら、以下のような手順で処理を進めます。
シャドウマップ用レンダーターゲットの作成
DirectX9では、深度情報を格納するために専用のテクスチャ(シャドウマップ)を作成します。
通常は深度フォーマット(例:D3DFMT_D16
やD3DFMT_D24S8
)のテクスチャを用意し、レンダーターゲットとして設定します。
このテクスチャにライト視点からの深度情報を書き込むことで、影の元となるデータを生成します。
ライト視点からのレンダリングパス
- レンダーターゲットの切り替え
シャドウマップ用の深度テクスチャをレンダーターゲットに設定し、カラーバッファは無効化またはクリアします。
- ビュー・プロジェクション行列の設定
ライトの位置と方向に基づくビュー行列と、ライトの投影方法(透視投影や正射影)に基づくプロジェクション行列を設定します。
- シーンの描画
シェーダーや固定機能パイプラインを使い、深度値のみを出力する描画を行います。
色の出力は不要で、Zバッファに深度を書き込みます。
カメラ視点からのレンダリングパス
- 通常のレンダーターゲットに切り替え
画面表示用のバックバッファに切り替え、カメラのビュー・プロジェクション行列を設定します。
- シャドウマップのバインド
ライト視点で作成したシャドウマップテクスチャをピクセルシェーダのサンプラーにセットします。
- 座標変換
カメラ視点の頂点座標をライト視点のクリップ空間に変換し、シャドウマップのテクスチャ座標として使用します。
- 影判定処理
ピクセルシェーダでシャドウマップの深度値と比較し、影の有無を判定します。
影がある場合はピクセルの色を暗くするなどの処理を行います。
ステート管理と最適化
DirectX9では、レンダリングパスごとにステートの切り替えが必要です。
例えば、深度バッファのクリア、レンダーターゲットの切り替え、サンプラーの設定などを適切に行います。
また、パフォーマンスを考慮して、不要な描画や状態変更を減らす工夫も重要です。
サンプルコード:ライト視点からの深度マップ生成
以下は、DirectX9でライト視点から深度マップを生成する際の簡単なコード例です。
ここでは、レンダーターゲットの切り替えとビュー・プロジェクション行列の設定を示します。
#include <d3d9.h>
#include <d3dx9.h>
// グローバル変数(省略可能)
LPDIRECT3DDEVICE9 g_pd3dDevice = nullptr;
LPDIRECT3DTEXTURE9 g_shadowMap = nullptr;
LPDIRECT3DSURFACE9 g_shadowSurface = nullptr;
// ライトのビュー・プロジェクション行列
D3DXMATRIX g_lightView;
D3DXMATRIX g_lightProj;
void SetupShadowMap()
{
// シャドウマップ用テクスチャ作成(例:1024x1024、16bit深度)
g_pd3dDevice->CreateTexture(1024, 1024, 1, D3DUSAGE_RENDERTARGET,
D3DFMT_R32F, D3DPOOL_DEFAULT, &g_shadowMap, NULL);
g_shadowMap->GetSurfaceLevel(0, &g_shadowSurface);
}
void RenderShadowMap()
{
// 現在のレンダーターゲットを保存
LPDIRECT3DSURFACE9 oldRenderTarget = nullptr;
g_pd3dDevice->GetRenderTarget(0, &oldRenderTarget);
// シャドウマップのサーフェスをレンダーターゲットに設定
g_pd3dDevice->SetRenderTarget(0, g_shadowSurface);
// バックバッファのクリア(深度もクリア)
g_pd3dDevice->Clear(0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER,
D3DCOLOR_XRGB(255, 255, 255), 1.0f, 0);
// ライトのビュー・プロジェクション行列をセット
g_pd3dDevice->SetTransform(D3DTS_VIEW, &g_lightView);
g_pd3dDevice->SetTransform(D3DTS_PROJECTION, &g_lightProj);
// シーンの描画(深度のみ)
// ここで深度のみを出力するシェーダや固定機能パイプラインを使う
// DrawSceneDepthOnly();
// レンダーターゲットを元に戻す
g_pd3dDevice->SetRenderTarget(0, oldRenderTarget);
if (oldRenderTarget) oldRenderTarget->Release();
}
このコードでは、シャドウマップ用のテクスチャを作成し、そのサーフェスをレンダーターゲットに設定してライト視点からシーンを描画しています。
深度のみを取得するため、色の出力は行わずZバッファに深度を書き込みます。
このように、シャドウマッピングの基本フローはライト視点での深度マップ生成とカメラ視点での影判定の2段階で構成されており、DirectX9ではレンダーターゲットの切り替えや行列の設定を適切に行うことが重要です。
これらの基礎を押さえることで、より高度な影表現や品質向上のテクニックに進む準備が整います。
必要な数学基盤
シャドウマッピングを正しく実装するためには、座標変換や行列計算の理解が欠かせません。
ここでは、ライト視点のビュー行列やプロジェクション行列の設定、さらにシャドウマップのテクスチャ座標に変換するためのバイアス行列について詳しく解説します。
また、深度値の線形化とその精度に関するポイントも説明します。
行列変換と座標系
3Dグラフィックスでは、オブジェクトの頂点座標を様々な座標系に変換しながら描画を行います。
シャドウマッピングでは特に、ライト視点の座標系への変換が重要です。
ライトビュー行列の計算
ライトビュー行列は、光源の位置と向きを基にシーンをライトの視点から見るための変換行列です。
これはカメラのビュー行列と同様の役割を果たします。
DirectX9では、D3DXMatrixLookAtLH
関数を使って簡単に計算できます。
引数にはライトの位置eye
、注視点at
、および上方向ベクトルup
を指定します。
#include <d3dx9.h>
// ライトの位置と方向を設定
D3DXVECTOR3 lightPos(10.0f, 20.0f, 10.0f);
D3DXVECTOR3 lightTarget(0.0f, 0.0f, 0.0f);
D3DXVECTOR3 lightUp(0.0f, 1.0f, 0.0f);
D3DXMATRIX lightView;
D3DXMatrixLookAtLH(&lightView, &lightPos, &lightTarget, &lightUp);
この行列は、ワールド空間の頂点をライトの視点空間に変換するために使います。
ライトの位置や向きを変えることで、影の方向や範囲を調整できます。
ライトプロジェクション行列の設定
ライトプロジェクション行列は、ライト視点から見たシーンの投影方法を決定します。
シャドウマップの範囲や形状に影響します。
主に2種類の投影方法があります。
- 透視投影(Perspective Projection)
スポットライトのように、光が一点から広がる場合に使います。
D3DXMatrixPerspectiveFovLH
関数で作成します。
- 正射影(Orthographic Projection)
平行光源(太陽光など)に適しています。
D3DXMatrixOrthoLH
関数で作成します。
例として、正射影行列の作成コードを示します。
float viewWidth = 20.0f;
float viewHeight = 20.0f;
float nearZ = 1.0f;
float farZ = 50.0f;
D3DXMATRIX lightProj;
D3DXMatrixOrthoLH(&lightProj, viewWidth, viewHeight, nearZ, farZ);
この行列は、ライト視点のクリップ空間に頂点を変換し、シャドウマップの深度値を正しく取得するために必要です。
viewWidth
やviewHeight
はシャドウマップに映る範囲を決めるパラメータで、シーンのサイズやライトの位置に応じて調整します。
バイアス行列による座標補正
シャドウマップの深度値をテクスチャとして利用する際、座標系の変換が必要です。
ライト視点のクリップ空間は
この変換を行うのがバイアス行列です。
バイアス行列は以下のように定義されます。
この行列をライトのビュー・プロジェクション行列に掛け合わせることで、頂点座標をシャドウマップのテクスチャ座標に変換できます。
DirectX9での実装例は以下の通りです。
D3DXMATRIX biasMatrix(
0.5f, 0.0f, 0.0f, 0.0f,
0.0f, -0.5f, 0.0f, 0.0f,
0.0f, 0.0f, 1.0f, 0.0f,
0.5f, 0.5f, 0.0f, 1.0f
);
// ライトのビュー・プロジェクション行列を計算済みとする
D3DXMATRIX lightViewProj = lightView * lightProj;
// バイアス行列を掛けてテクスチャ座標変換行列を作成
D3DXMATRIX shadowTransform = lightViewProj * biasMatrix;
このshadowTransform
行列を使って、カメラ視点の頂点をライト視点のシャドウマップテクスチャ座標に変換し、ピクセルシェーダで影判定に利用します。
深度値の線形化と精度
シャドウマッピングでは、ライト視点からの深度値をシャドウマップに格納しますが、この深度値は通常、非線形なクリップ空間のZ値です。
DirectX9の透視投影では、深度値はカメラからの距離に対して非線形に分布しており、遠くのオブジェクトほど深度値の差が小さくなります。
この非線形性は、シャドウマップの精度に影響を与えます。
特に遠距離の影がぼやけたり、アーティファクトが発生しやすくなります。
深度値の非線形性
透視投影の深度値
ここで、
このため、深度値は線形距離ではなく逆数的に分布します。
線形化の必要性
影の判定や比較を正確に行うために、深度値を線形化することがあります。
線形化することで、深度の差分が均等に扱われ、シャドウマップの精度が向上します。
線形化の計算例は以下の通りです。
ここで、near
とfar
は投影行列の近クリップ面と遠クリップ面の距離です。
DirectX9での実装例
ピクセルシェーダ内で深度値を線形化する場合、以下のようなコードを使います。
float LinearizeDepth(float depth, float near, float far)
{
return (near * far) / (far - depth * (far - near));
}
この線形化を行うことで、シャドウマップの比較精度が向上し、影のアーティファクトを減らすことができます。
深度精度の向上策
- 近クリップ面をできるだけ遠くに設定する
近クリップ面が近すぎると深度値の分布が偏り、精度が落ちます。
- シャドウマップの解像度を上げる
高解像度のシャドウマップを使うことで、より細かい深度差を表現できます。
- 正射影を使う
平行光源の場合は正射影を使うことで深度値の線形性が保たれ、精度が向上します。
これらの数学的基盤を理解し、適切に行列を設定し深度値を扱うことで、DirectX9でのシャドウマッピングの品質を大きく向上させることができます。
シャドウマップ用リソース準備
シャドウマッピングの実装において、シャドウマップを格納するためのリソース準備は非常に重要です。
特に、深度情報を正確に保存できるテクスチャのフォーマット選択や、DirectX9でのテクスチャ生成、レンダーターゲットの切り替え方法を理解しておく必要があります。
デプステクスチャのフォーマット選択
シャドウマップは深度情報を格納するため、通常のカラーテクスチャとは異なるフォーマットを使用します。
DirectX9では、深度バッファとして利用可能なフォーマットがいくつかありますが、代表的なものにD3DFMT_D16
とD3DFMT_D24S8
があります。
D3DFMT_D16 と D3DFMT_D24S8 の違い
フォーマット名 | 深度ビット数 | ステンシルビット数 | 特徴・用途 |
---|---|---|---|
D3DFMT_D16 | 16ビット | なし | 軽量でメモリ消費が少ない。精度は16ビットで、一般的なシャドウマップに十分。 |
D3DFMT_D24S8 | 24ビット | 8ビット | 深度精度が高く、ステンシルバッファも利用可能です。高品質な影表現や複雑なシーンに適します。 |
- D3DFMT_D16
16ビットの深度バッファはメモリ使用量が少なく、パフォーマンスに優れます。
ただし、深度の精度は24ビットに比べて低いため、遠距離の影や細かい影の表現でアーティファクトが出やすい場合があります。
- D3DFMT_D24S8
24ビットの深度バッファはより高精度で、影の品質が向上します。
さらに8ビットのステンシルバッファが付属しているため、複雑なマスク処理や影の合成に利用可能です。
ただし、メモリ消費が増え、パフォーマンスに影響を与える可能性があります。
用途やパフォーマンス要件に応じて適切なフォーマットを選択してください。
テクスチャ生成とバッファ確保
シャドウマップ用の深度テクスチャは、DirectX9のCreateTexture
関数で生成します。
深度フォーマットを指定し、レンダーターゲットとして使用可能なテクスチャを作成します。
以下は、16ビット深度フォーマットのシャドウマップテクスチャを生成する例です。
#include <d3d9.h>
#include <d3dx9.h>
LPDIRECT3DDEVICE9 g_pd3dDevice = nullptr;
LPDIRECT3DTEXTURE9 g_shadowMap = nullptr;
LPDIRECT3DSURFACE9 g_shadowSurface = nullptr;
void CreateShadowMapTexture(int width, int height)
{
// 深度フォーマットを指定してテクスチャを作成
HRESULT hr = g_pd3dDevice->CreateTexture(
width,
height,
1, // ミップマップレベル数
D3DUSAGE_RENDERTARGET, // レンダーターゲットとして使用
D3DFMT_R32F, // 32ビット浮動小数点フォーマット(深度用に使う例)
D3DPOOL_DEFAULT,
&g_shadowMap,
NULL
);
if (FAILED(hr)) {
// エラーハンドリング
OutputDebugStringA("シャドウマップテクスチャの作成に失敗しました。\n");
return;
}
// テクスチャのサーフェスレベル0を取得
g_shadowMap->GetSurfaceLevel(0, &g_shadowSurface);
}
ここで注意したいのは、DirectX9では深度専用のテクスチャフォーマットが直接サポートされていないため、深度値を格納するためにD3DFMT_R32F
(32ビット浮動小数点)などのレンダーターゲット可能なフォーマットを使うことが多い点です。
深度バッファとしては別途CreateDepthStencilSurface
で作成しますが、シャドウマップとして使う場合はこのように浮動小数点テクスチャを用いることが一般的です。
もしハードウェアが対応していれば、D3DFMT_D16
やD3DFMT_D24S8
の深度ステンシルサーフェスを直接レンダーターゲットとして使うことも可能です。
レンダーターゲット切り替え手順
シャドウマップの生成時には、レンダーターゲットを通常のバックバッファからシャドウマップ用のサーフェスに切り替える必要があります。
これにより、ライト視点からの深度情報をシャドウマップに書き込みます。
切り替えの基本的な手順は以下の通りです。
- 現在のレンダーターゲットと深度ステンシルサーフェスを保存
後で元に戻すために、現在のレンダーターゲットと深度ステンシルバッファを取得して保存します。
- シャドウマップのサーフェスをレンダーターゲットに設定
IDirect3DDevice9::SetRenderTarget
を使い、シャドウマップのサーフェスをレンダーターゲットに設定します。
- 深度ステンシルサーフェスの設定
シャドウマップ用の深度ステンシルサーフェスをSetDepthStencilSurface
で設定します。
- レンダーターゲットのクリア
深度バッファとカラーバッファ(必要に応じて)をクリアします。
- ライト視点からの描画処理を実行
- レンダーターゲットと深度ステンシルサーフェスを元に戻す
以下に具体的なコード例を示します。
void BeginShadowMapRender()
{
// 現在のレンダーターゲットと深度ステンシルサーフェスを保存
LPDIRECT3DSURFACE9 oldRenderTarget = nullptr;
LPDIRECT3DSURFACE9 oldDepthStencil = nullptr;
g_pd3dDevice->GetRenderTarget(0, &oldRenderTarget);
g_pd3dDevice->GetDepthStencilSurface(&oldDepthStencil);
// シャドウマップのサーフェスをレンダーターゲットに設定
g_pd3dDevice->SetRenderTarget(0, g_shadowSurface);
// 深度ステンシルサーフェスを設定(シャドウマップ用に作成済みとする)
// 例: g_shadowDepthStencilSurface
g_pd3dDevice->SetDepthStencilSurface(g_shadowDepthStencilSurface);
// クリア処理(深度バッファとカラーバッファ)
g_pd3dDevice->Clear(0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER,
D3DCOLOR_XRGB(255, 255, 255), 1.0f, 0);
// ここでライト視点からの描画処理を行う
// 描画終了後、レンダーターゲットと深度ステンシルサーフェスを元に戻す
g_pd3dDevice->SetRenderTarget(0, oldRenderTarget);
g_pd3dDevice->SetDepthStencilSurface(oldDepthStencil);
if (oldRenderTarget) oldRenderTarget->Release();
if (oldDepthStencil) oldDepthStencil->Release();
}
この手順を正しく行うことで、シャドウマップにライト視点の深度情報を正確に書き込むことができます。
レンダーターゲットの切り替えはDirectX9の基本操作ですが、シャドウマッピングでは特に重要な処理です。
これらのリソース準備を適切に行うことで、シャドウマップの生成と利用がスムーズになり、影の品質向上やパフォーマンス最適化に繋がります。
シェーダ実装の要点
DirectX9でシャドウマッピングを実装する際、シェーダの役割は非常に重要です。
特にライト視点での深度マップ生成用の頂点シェーダ、深度書き込みの最適化を行うピクセルシェーダ、そしてカメラ視点での影判定に使う比較サンプラの設定がポイントとなります。
ここではそれぞれの要点を具体的なコード例とともに解説します。
ライト視点レンダリング用頂点シェーダ
ライト視点からシーンをレンダリングする際の頂点シェーダは、主に頂点の変換と深度値の計算に集中します。
色やテクスチャ座標の計算は不要で、深度のみを出力する最小限の構成が望まれます。
深度のみ出力する最小構成
ライト視点の頂点シェーダは、ワールド座標の頂点をライトのビュー・プロジェクション行列で変換し、クリップ空間の座標を出力します。
ピクセルシェーダは深度値を自動的に書き込むため、特別な処理は不要です。
以下はHLSLで書いた最小構成の頂点シェーダ例です。
// ライト視点用頂点シェーダ
float4x4 g_mLightViewProj; // ライトのビュー・プロジェクション行列
struct VS_INPUT
{
float3 pos : POSITION;
};
struct VS_OUTPUT
{
float4 pos : POSITION;
};
VS_OUTPUT VS_ShadowMap(VS_INPUT input)
{
VS_OUTPUT output;
// ワールド座標をライトのクリップ空間に変換
float4 worldPos = float4(input.pos, 1.0f);
output.pos = mul(worldPos, g_mLightViewProj);
return output;
}
このシェーダは頂点の位置をライト視点のクリップ空間に変換するだけで、色やテクスチャ座標は扱いません。
ピクセルシェーダは不要で、固定機能パイプラインのままでも深度値はZバッファに書き込まれます。
ピクセルシェーダでの深度書き込み最適化
DirectX9の固定機能パイプラインでは、深度値は自動的にZバッファに書き込まれますが、シェーダを使う場合は深度値の書き込みを明示的に制御できます。
ピクセルシェーダで深度値を書き込むことで、より柔軟な深度処理や最適化が可能です。
深度書き込みの基本
ピクセルシェーダで深度値を書き込むには、SV_Depth
(DirectX10以降)やoDepth
(DirectX9の拡張)を使いますが、DirectX9のHLSLでは標準的に深度出力はサポートされていません。
そのため、深度値は通常Zバッファに自動的に書き込まれます。
しかし、深度値をピクセルシェーダで計算し、色として出力するテクニックもあります。
例えば、深度値を浮動小数点テクスチャに格納し、後で比較に使う場合です。
深度値を浮動小数点テクスチャに書き込む例
struct PS_INPUT
{
float4 pos : SV_POSITION;
};
float4 PS_WriteDepth(PS_INPUT input) : COLOR
{
// 深度値を0~1に正規化(Zバッファの値)
float depth = input.pos.z / input.pos.w;
return float4(depth, depth, depth, 1.0f);
}
このシェーダは、頂点シェーダから渡されたクリップ空間のZ値を正規化し、RGBAの各チャンネルに同じ深度値を出力しています。
これを32ビット浮動小数点テクスチャに書き込むことで、深度マップとして利用可能です。
最適化ポイント
- 不要な計算を省く
深度マップ生成時は色計算を行わず、深度値の計算だけに集中します。
- 浮動小数点テクスチャの利用
16ビット整数テクスチャよりも精度が高い32ビット浮動小数点テクスチャを使うと、深度の精度が向上します。
- ピクセルシェーダの簡素化
深度値の書き込み以外の処理を極力減らし、パフォーマンスを確保します。
カメラ視点レンダリング用比較サンプラ設定
カメラ視点でシーンを描画する際、シャドウマップを参照して影の有無を判定します。
このとき、シャドウマップはテクスチャとしてピクセルシェーダに渡され、深度比較を行うためのサンプラ設定が必要です。
比較サンプラとは
比較サンプラは、テクスチャのサンプリング時にテクスチャの深度値と指定した比較値を自動的に比較し、影の判定を効率的に行う機能です。
DirectX9ではD3DTEXF_NONE
やD3DTEXF_LINEAR
などのフィルタ設定に加え、D3DTEXF_COMPARISON
を使って比較サンプラを設定します。
サンプラステートの設定例
g_pd3dDevice->SetSamplerState(0, D3DSAMP_MINFILTER, D3DTEXF_LINEAR);
g_pd3dDevice->SetSamplerState(0, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR);
g_pd3dDevice->SetSamplerState(0, D3DSAMP_MIPFILTER, D3DTEXF_NONE);
// 比較サンプラの設定(ハードウェアが対応している場合)
g_pd3dDevice->SetSamplerState(0, D3DSAMP_COMPARISONFUNC, D3DCMP_LESS);
g_pd3dDevice->SetSamplerState(0, D3DSAMP_ADDRESSU, D3DTADDRESS_BORDER);
g_pd3dDevice->SetSamplerState(0, D3DSAMP_ADDRESSV, D3DTADDRESS_BORDER);
D3DSAMP_COMPARISONFUNC
にD3DCMP_LESS
を設定すると、サンプラはテクスチャの深度値が比較値より小さいかどうかを判定しますD3DTADDRESS_BORDER
はテクスチャ座標が範囲外の場合に境界色を使う設定で、影の境界のアーティファクトを減らすのに役立ちます
ピクセルシェーダでの比較サンプラ利用例
sampler2DShadow shadowMap : register(s0);
float4 PS_ShadowedRender(float4 pos : SV_POSITION, float4 shadowCoord : TEXCOORD0) : COLOR
{
// シャドウマップの比較サンプラを使って影判定
float shadow = tex2Dcmp(shadowMap, shadowCoord.xy, shadowCoord.z);
// 影がある場合は暗くする
float brightness = lerp(0.5f, 1.0f, shadow);
return float4(brightness, brightness, brightness, 1.0f);
}
この例では、tex2Dcmp
関数を使ってシャドウマップの深度値と比較し、影の強さを計算しています。
比較サンプラを使うことで、ピクセルシェーダ内での深度比較が効率的に行えます。
これらのシェーダ実装の要点を押さえることで、DirectX9環境でも効率的かつ高品質なシャドウマッピングが実現できます。
特にライト視点の頂点シェーダはシンプルに保ち、ピクセルシェーダでの深度書き込みや比較サンプラの設定を適切に行うことが重要です。
ライト視点でのレンダリング処理
シャドウマッピングにおけるライト視点でのレンダリング処理は、影の元となる深度情報を正確に取得するための重要なステップです。
ここでは、不透明オブジェクトのみを描画する理由、カラーバッファの無効化とZバッファのクリア方法、そしてシャドウマップ生成に適した描画ステートの設定について詳しく説明します。
不透明オブジェクトのみの描画
ライト視点でのレンダリングは、影の生成に必要な深度情報を取得することが目的です。
そのため、透明や半透明のオブジェクトは描画対象から除外するのが一般的です。
理由
- 深度情報の正確性確保
透明オブジェクトは複数のピクセルが重なり合うことが多く、深度値の管理が複雑になります。
影の判定に使う深度マップは単一の深度値を持つため、透明オブジェクトを含めると誤った影が生成される可能性があります。
- パフォーマンス向上
透明オブジェクトはライト視点での影生成に直接寄与しないため、描画を省略することで処理負荷を軽減できます。
実装例
描画ループで不透明オブジェクトのみを選別して描画します。
例えば、メッシュのマテリアルに透明度のフラグがある場合はそれをチェックします。
for (auto& mesh : sceneMeshes)
{
if (!mesh.isTransparent) // 不透明オブジェクトのみ描画
{
mesh.Draw(g_pd3dDevice);
}
}
このようにすることで、シャドウマップに正確な深度情報が記録されます。
カラーバッファ無効化とZクリア
ライト視点でのレンダリングは深度情報の取得が目的であり、色の情報は不要です。
したがって、カラーバッファへの書き込みを無効化し、Zバッファ(深度バッファ)のみをクリアして使用します。
カラーバッファ無効化
DirectX9では、SetRenderState
関数を使ってカラーバッファへの書き込みを制御できます。
具体的には、RGB成分の書き込みを無効にします。
g_pd3dDevice->SetRenderState(D3DRS_COLORWRITEENABLE, 0); // RGB書き込み無効
これにより、レンダーターゲットの色バッファは変更されず、深度バッファのみが更新されます。
Zバッファのクリア
レンダーターゲットを切り替えた直後は、深度バッファをクリアして初期化する必要があります。
これにより、前フレームの深度情報が残らず、正しい深度比較が可能になります。
g_pd3dDevice->Clear(0, NULL, D3DCLEAR_ZBUFFER, 0, 1.0f, 0);
ここで、1.0f
は深度バッファの最大値(最も遠い距離)を意味します。
カラーバッファのクリアは不要
カラーバッファへの書き込みを無効化しているため、カラーバッファのクリアは省略しても問題ありません。
クリアすると無駄な処理になるため、パフォーマンス向上に繋がります。
シャドウマップへの描画ステート設定
シャドウマップ生成時の描画ステートは、深度情報を正確に取得し、不要な描画処理を避けるために適切に設定する必要があります。
主な描画ステート設定
ステート名 | 設定値 | 説明 |
---|---|---|
D3DRS_ZENABLE | TRUE | 深度テストを有効にする |
D3DRS_ZWRITEENABLE | TRUE | 深度バッファへの書き込みを有効にする |
D3DRS_ALPHATESTENABLE | TRUE or FALSE | 透明度テストの有無(透明オブジェクト除外時はFALSE ) |
D3DRS_ALPHAREF | 0x08 | アルファテストの閾値 |
D3DRS_ALPHAFUNC | D3DCMP_GREATER | アルファテストの比較関数 |
D3DRS_COLORWRITEENABLE | 0 | カラーバッファへの書き込み無効化 |
D3DRS_CULLMODE | D3DCULL_CCW or D3DCULL_NONE | カリング設定(必要に応じて) |
例:描画ステート設定コード
// 深度テストと書き込みを有効化
g_pd3dDevice->SetRenderState(D3DRS_ZENABLE, TRUE);
g_pd3dDevice->SetRenderState(D3DRS_ZWRITEENABLE, TRUE);
// アルファテストを無効化(不透明オブジェクトのみ描画時)
g_pd3dDevice->SetRenderState(D3DRS_ALPHATESTENABLE, FALSE);
// カラーバッファ書き込みを無効化
g_pd3dDevice->SetRenderState(D3DRS_COLORWRITEENABLE, 0);
// カリング設定(例:反時計回りをカリング)
g_pd3dDevice->SetRenderState(D3DRS_CULLMODE, D3DCULL_CCW);
注意点
- 透明オブジェクトを描画する場合は、アルファテストを有効にして透明部分を除外することもありますが、シャドウマップ生成時は通常不透明オブジェクトのみ描画するため無効化します
- カリング設定はシーンのジオメトリに合わせて調整してください。カリングを無効にすると両面描画となり、パフォーマンスが低下します
これらの設定を適切に行うことで、ライト視点からの深度情報を効率的かつ正確にシャドウマップに記録できます。
影の品質向上とパフォーマンスの両立に繋がるため、レンダリング処理の基本として押さえておきましょう。
カメラ視点でのレンダリング処理
シャドウマッピングの最終段階であるカメラ視点からのレンダリング処理では、ライト視点で生成したシャドウマップを参照し、各ピクセルが影の中にあるかどうかを判定します。
ここでは、シャドウマップのサンプリング座標の計算方法、深度比較による影判定の仕組み、そして影の結果を画面に反映するためのブレンド方式について詳しく説明します。
シャドウマップサンプリング座標の計算
カメラ視点で描画する際、各ピクセルの位置をライト視点のシャドウマップテクスチャ座標に変換する必要があります。
これにより、ピクセルがライトから見てどの位置にあるかを特定し、シャドウマップの深度値と比較できます。
座標変換の流れ
- ワールド座標からライト視点のクリップ空間へ変換
頂点のワールド座標に対して、ライトのビュー行列とプロジェクション行列を掛け合わせた行列shadowTransform
を適用します。
- クリップ空間から正規化デバイス座標(NDC)へ変換
クリップ空間の座標は
- NDCからテクスチャ座標へ変換
シャドウマップのテクスチャ座標は
これはバイアス行列を用いて行います。
頂点シェーダでの計算例(HLSL)
float4x4 g_shadowTransform; // ライトのビュー・プロジェクション・バイアス行列
struct VS_INPUT
{
float3 pos : POSITION;
};
struct VS_OUTPUT
{
float4 pos : SV_POSITION;
float4 shadowCoord : TEXCOORD0;
};
VS_OUTPUT VS_Main(VS_INPUT input)
{
VS_OUTPUT output;
// カメラ視点の変換(省略、通常は別の行列で処理)
output.pos = mul(float4(input.pos, 1.0f), g_viewProj);
// シャドウマップ用の座標計算
output.shadowCoord = mul(float4(input.pos, 1.0f), g_shadowTransform);
return output;
}
g_shadowTransform
はライトのビュー・プロジェクション行列にバイアス行列を掛けたもので、これによりshadowCoord
はシャドウマップのテクスチャ座標として使えます。
深度比較による影判定
ピクセルシェーダでは、計算したシャドウマップのテクスチャ座標を使って、シャドウマップの深度値と現在のピクセルの深度を比較します。
この比較により、そのピクセルが影の中にあるかどうかを判定します。
判定の基本原理
- シャドウマップの深度値は、ライト視点から見た最も近い遮蔽物の距離を表します
- カメラ視点のピクセルの深度値(ライト視点での深度)とシャドウマップの深度値を比較し、ピクセルの深度がシャドウマップの値より大きければ、そのピクセルは遮蔽物の後ろにあり影の中と判断します
ピクセルシェーダでの比較例(HLSL)
sampler2DShadow shadowMap : register(s0);
float4 PS_Main(float4 shadowCoord : TEXCOORD0) : SV_Target
{
// シャドウマップの比較サンプラを使って影判定
float shadow = tex2Dcmp(shadowMap, shadowCoord.xy, shadowCoord.z);
// 影の強さに応じて明るさを調整
float brightness = lerp(0.5f, 1.0f, shadow);
return float4(brightness, brightness, brightness, 1.0f);
}
ここでtex2Dcmp
は、シャドウマップの深度値とshadowCoord.z
を比較し、影の有無を0~1の値で返します。
1は影なし、0は完全に影の中を意味します。
バイアスの適用
深度比較時には、シャドウマップの深度値にバイアスを加えて、セルフシャドウのアーティファクト(Peter Panning)を防止します。
バイアスは頂点シェーダやピクセルシェーダで調整可能です。
影結果のブレンド方式
影判定の結果を画面に反映する際、影の強さに応じてピクセルの色を調整します。
これには様々なブレンド方式があり、影の見た目や品質に大きく影響します。
基本的なブレンド方法
- 単純な乗算
影の強さ(0~1)をピクセルの色に乗算し、影の部分を暗くします。
float4 baseColor = tex2D(diffuseMap, texCoord);
float shadow = tex2Dcmp(shadowMap, shadowCoord.xy, shadowCoord.z);
float4 finalColor = baseColor * shadow;
- 線形補間(Lerp)
影の強さに応じて明るさを補間し、柔らかい影を表現します。
float brightness = lerp(0.5f, 1.0f, shadow);
float4 finalColor = baseColor * brightness;
ソフトシャドウ表現
影の境界を滑らかにするために、PCF(Percentage Closer Filtering)などのフィルタリングを用いて複数のサンプルを平均化し、影のエッジをぼかします。
これにより、より自然な影が得られます。
アルファブレンドとの組み合わせ
影の強さをアルファ値として扱い、既存の色とブレンドする方法もあります。
これにより、影の透明度や濃さを細かく調整可能です。
これらの処理を組み合わせることで、カメラ視点から見たリアルな影を表現できます。
シャドウマップの座標計算と深度比較を正確に行い、適切なブレンド方式を選択することが高品質なシャドウマッピングの鍵となります。
バイアス調整と精度問題
シャドウマッピングでは、深度比較の際にバイアスを適切に調整しないと、影のアーティファクトが発生しやすくなります。
特に「セルフシャドウの浮き上がり(Peter Panning)」や「影の漏れ(Shadow Acne)」といった問題が代表的です。
ここでは、影の品質を保つためのバイアス調整手法として、Constant Bias(定数バイアス)、Slope Scale Bias(傾斜スケールバイアス)、そしてバイアス値の自動調整アルゴリズムについて詳しく解説します。
Constant Bias の適用方法
Constant Biasは、シャドウマップの深度値に一定の定数値を加算または減算することで、深度比較の閾値を調整する手法です。
これにより、深度値のわずかな誤差や浮動小数点の丸め誤差による影のアーティファクトを軽減します。
実装例
ピクセルシェーダや頂点シェーダで、ライト視点の深度値に定数バイアスを加えます。
例えば、影判定時の比較値にバイアスを加える方法です。
float constantBias = 0.005f; // 調整可能な定数バイアス
// シャドウマップの深度値と比較する際にバイアスを加算
bool IsInShadow(float shadowMapDepth, float currentDepth)
{
return currentDepth - constantBias > shadowMapDepth;
}
このバイアスは固定値なので、シーンの状況に関わらず一定の値が適用されます。
値が小さすぎると影のアーティファクトが残り、大きすぎると影が不自然に浮いてしまうため、適切な値の調整が必要です。
DirectX9での設定例
DirectX9の固定機能パイプラインでは、D3DRS_DEPTHBIAS
ステートを使って定数バイアスを設定できます。
float bias = 0.0005f;
g_pd3dDevice->SetRenderState(D3DRS_DEPTHBIAS, *(DWORD*)&bias);
ただし、この値はハードウェアやシーンのスケールによって効果が異なるため、調整が必要です。
Slope Scale Bias の役割
Slope Scale Biasは、ポリゴンの傾斜角度に応じてバイアスを動的に調整する手法です。
ポリゴンの法線や深度の変化率に基づいてバイアスをスケーリングし、影のアーティファクトをより効果的に抑制します。
理論的背景
ポリゴンがカメラやライトに対して斜めに配置されている場合、深度値の誤差が大きくなりやすいです。
Slope Scale Biasはこの誤差を補正するため、深度の傾斜(スロープ)に比例したバイアスを加えます。
数式で表すと、
ここで、
実装例
Slope Scale Biasは頂点シェーダやピクセルシェーダで計算し、バイアス値を動的に変化させます。
DirectX9の固定機能パイプラインでは、D3DRS_SLOPESCALEDEPTHBIAS
ステートで設定可能です。
float slopeScaleBias = 2.0f; // 調整可能なスロープスケールバイアス
g_pd3dDevice->SetRenderState(D3DRS_SLOPESCALEDEPTHBIAS, *(DWORD*)&slopeScaleBias);
この値を大きくすると、斜めの面での影のアーティファクトが減りますが、影が不自然に浮くリスクもあるためバランスが重要です。
バイアス値自動調整アルゴリズム
Constant BiasとSlope Scale Biasの組み合わせは効果的ですが、シーンの状況やカメラ・ライトの位置によって最適なバイアス値は変化します。
そこで、バイアス値を自動的に調整するアルゴリズムを導入することで、影の品質を安定させることが可能です。
アルゴリズムの概要
- 深度勾配の計算
頂点シェーダやジオメトリシェーダで、画面空間における深度の勾配(
- バイアス値の算出
勾配の最大値にSlope Scale Biasを乗じ、Constant Biasを加算してバイアス値を決定します。
- バイアスの適用
ピクセルシェーダで深度比較時にこのバイアス値を用いて判定を行います。
実装例(擬似コード)
float constantBias = 0.001f;
float slopeScaleBias = 2.0f;
// 頂点シェーダで深度勾配を計算(例)
float dzdx = abs(ddx(shadowCoord.z));
float dzdy = abs(ddy(shadowCoord.z));
float maxSlope = max(dzdx, dzdy);
// バイアス値を動的に計算
float bias = constantBias + slopeScaleBias * maxSlope;
// 深度比較時にバイアスを適用
bool inShadow = (currentDepth - bias) > shadowMapDepth;
メリット
- シーンの形状や視点に応じてバイアスが変化するため、影のアーティファクトを最小限に抑えつつ、影の浮き上がりも防止できます
- 手動でバイアス値を調整する手間が減り、より安定した影表現が可能です
これらのバイアス調整手法を適切に組み合わせることで、DirectX9のシャドウマッピングにおける影の品質を大幅に向上させることができます。
特に自動調整アルゴリズムは、複雑なシーンや動的なライト環境で効果を発揮します。
アーティファクト対策
シャドウマッピングでは、影の表現に関するさまざまなアーティファクトが発生しやすく、これらを適切に対策しないとリアルな影を描画できません。
代表的な問題として、Peter-Panning、セルフシャドウの誤判定、そして影の痩せや膨らみがあります。
ここでは、それぞれの問題に対する具体的な防止策や調整方法を詳しく解説します。
Peter-Panning防止策
Peter-Panningは、影がオブジェクトの輪郭から不自然に離れて浮いてしまう現象です。
これは主にバイアスの設定が大きすぎることが原因で、影が本来の位置よりも手前にずれてしまいます。
原因
- バイアス値(Constant BiasやSlope Scale Bias)が過剰に大きいと、深度比較時に影の境界がずれてしまい、影がオブジェクトから離れて見えます
- 深度値の丸め誤差や解像度不足も影響します
防止策
- バイアス値の適切な調整
バイアスは小さくしすぎるとセルフシャドウの誤判定が増え、大きくしすぎるとPeter-Panningが発生します。
シーンに応じて最適な値を見つけることが重要です。
- Slope Scale Biasの利用
傾斜に応じてバイアスを動的に調整することで、平坦な面では小さなバイアス、斜面では大きなバイアスを適用し、Peter-Panningを抑制します。
- 深度値の精度向上
シャドウマップの解像度を上げたり、深度値の線形化を行うことで、誤差を減らし影の位置ズレを防ぎます。
- バイアスの自動調整アルゴリズム
深度勾配に基づいてバイアスを動的に変える方法を導入し、Peter-Panningの発生を最小限に抑えます。
セルフシャドウ軽減手法
セルフシャドウとは、オブジェクト自身の表面が誤って影として判定される現象で、影の境界にノイズやちらつきが生じることがあります。
原因
- 深度比較の際に、同じオブジェクトの表面の深度値が微妙に異なるため、誤判定が起こる
- バイアスが小さすぎると、深度のわずかな誤差でセルフシャドウが増加
軽減手法
- アルファテストの活用
透明部分を正しく除外することで、セルフシャドウの誤判定を減らせます。
- バイアスの適切設定
バイアスを適度に設定し、セルフシャドウの発生を抑制します。
- PCF(Percentage Closer Filtering)によるソフトシャドウ化
複数の深度サンプルを平均化し、影の境界をぼかすことでセルフシャドウのノイズを軽減します。
- 法線情報の利用
法線ベクトルを使ってセルフシャドウの判定を補正し、誤判定を減らす高度な手法もあります。
痩せ影と膨らみ影のバランス調整
シャドウマッピングでは、影が実際よりも細く(痩せ影)なったり、逆に広がって(膨らみ影)しまうことがあります。
これらはバイアスやサンプリング方法の設定に起因します。
痩せ影の原因と対策
- 原因
バイアスが大きすぎる、またはシャドウマップの解像度が低い場合に発生しやすいです。
影の境界が本来より内側に入り込み、影が細くなります。
- 対策
バイアスを適切に下げる、シャドウマップの解像度を上げる、PCFなどのフィルタリングで影の境界を滑らかにします。
膨らみ影の原因と対策
- 原因
バイアスが小さすぎると、深度比較で影の範囲が広がり、影が膨らんで見えます。
特にPCFのサンプル数が多い場合に顕著です。
- 対策
バイアスを適度に上げる、PCFのサンプル数や重み付けを調整する、シャドウマップの解像度を適切に設定します。
バランス調整のポイント
- バイアス値は影の痩せと膨らみのトレードオフであるため、シーンやライトの条件に応じて微調整が必要です
- PCFのカーネルサイズや重み付けを工夫し、影のエッジを自然に見せることが重要です
- シャドウマップの解像度を高くすると、痩せ影や膨らみ影の問題は軽減されますが、パフォーマンスとのバランスを考慮してください
これらのアーティファクト対策を適切に実施することで、DirectX9のシャドウマッピングにおける影の品質を大幅に向上させ、リアルで自然な影表現を実現できます。
影品質向上技法
シャドウマッピングの基本的な実装では、影のエッジがギザギザになったり、硬い影ができてしまうことがあります。
影の品質を向上させるためには、ソフトシャドウ化や高度な深度表現技術、さらにシーンのスケールに応じたテクスチャ解像度の調整が重要です。
ここでは、PCFによるソフトシャドウ化、VSM・ESMの概要と導入ポイント、そしてテクスチャ解像度戦略について詳しく解説します。
PCFによるソフトシャドウ化
PCF(Percentage Closer Filtering)は、シャドウマップの深度比較を複数のサンプルで行い、その結果を平均化することで影の境界をぼかし、自然なソフトシャドウを実現する技術です。
DirectX9でも比較的簡単に実装でき、影の品質向上に効果的です。
3×3カーネルサンプル
PCFの基本的な実装は、シャドウマップの周囲の9つのサンプル(3×3カーネル)を取得し、それぞれの深度比較結果を平均化します。
これにより、影の境界が滑らかになり、ギザギザやジャギーを軽減できます。
ピクセルシェーダでの3×3 PCF例(HLSL)
sampler2DShadow shadowMap : register(s0);
float2 texelSize; // シャドウマップの1ピクセルのサイズ(1/解像度)
float4 PS_PCF3x3(float4 shadowCoord : TEXCOORD0) : COLOR
{
float shadow = 0.0f;
// 3x3のオフセット
for (int y = -1; y <= 1; y++)
{
for (int x = -1; x <= 1; x++)
{
float2 offset = float2(x, y) * texelSize;
shadow += tex2Dcmp(shadowMap, shadowCoord.xy + offset, shadowCoord.z);
}
}
shadow /= 9.0f; // 平均化
return float4(shadow, shadow, shadow, 1.0f);
}
このコードでは、シャドウマップの周囲9点の深度比較結果を合計し、平均を取っています。
texelSize
はシャドウマップの解像度に応じて計算し、正確なサンプリング位置を指定します。
カスタムフィルタと重み付け
3×3カーネルの単純平均ではなく、重み付けを行うことでより自然な影のぼかしが可能です。
例えば、ガウシアンフィルタを用いて中心に近いサンプルに高い重みを与え、遠いサンプルの影響を減らします。
ガウシアン重み付けの例
float kernel[9] = {
1.0f, 2.0f, 1.0f,
2.0f, 4.0f, 2.0f,
1.0f, 2.0f, 1.0f
};
float kernelSum = 16.0f;
float4 PS_PCF3x3Weighted(float4 shadowCoord : TEXCOORD0) : COLOR
{
float shadow = 0.0f;
int index = 0;
for (int y = -1; y <= 1; y++)
{
for (int x = -1; x <= 1; x++)
{
float2 offset = float2(x, y) * texelSize;
shadow += kernel[index] * tex2Dcmp(shadowMap, shadowCoord.xy + offset, shadowCoord.z);
index++;
}
}
shadow /= kernelSum;
return float4(shadow, shadow, shadow, 1.0f);
}
このように重み付けを行うことで、影のエッジがより滑らかになり、自然なソフトシャドウが得られます。
VSMとESMの概要と導入ポイント
PCF以外にも、影品質を向上させるための高度なシャドウマッピング技術として、VSM(Variance Shadow Maps)とESM(Exponential Shadow Maps)があります。
これらは深度の分布を統計的に扱い、ソフトシャドウを効率的に実現します。
VSM(Variance Shadow Maps)
- 概要
深度値の1次および2次モーメント(平均と分散)をシャドウマップに格納し、ピクセルシェーダで分散を利用して影の確率を計算します。
これにより、PCFよりも高速にソフトシャドウを生成可能です。
- 特徴
- シャドウマップは通常の深度テクスチャではなく、2チャンネル(平均・分散)を持つテクスチャを使用
- フィルタリングが容易で、ぼかしやすい
- 影の境界が滑らかで自然
- ただし、光源の近くで「ライトリーク」と呼ばれる影漏れが発生しやすい
- 導入ポイント
- シャドウマップのフォーマットを
D3DFMT_G16R16F
などの浮動小数点2チャンネルに設定 - 頂点シェーダ・ピクセルシェーダでモーメントを計算・格納
- ピクセルシェーダでChebyshevの不等式を用いて影の確率を計算
- シャドウマップのフォーマットを
ESM(Exponential Shadow Maps)
- 概要
深度値に指数関数を適用してシャドウマップに格納し、比較時に指数関数の逆変換を用いて影の強さを計算します。
これにより、影の境界が滑らかになります。
- 特徴
- 実装が比較的簡単
- ソフトシャドウが得られます
- バイアス調整が不要に近い
- ただし、影の濃淡が不自然になる場合があります
- 導入ポイント
- 深度値を指数関数で変換してシャドウマップに書き込みます
- ピクセルシェーダで指数関数の逆変換を用いて影の強さを計算
- パラメータ調整で影の濃さをコントロール
シーンスケールに応じたテクスチャ解像度戦略
シャドウマップの解像度は影の品質に直結しますが、高解像度にするとメモリ消費や描画負荷が増大します。
シーンのスケールやカメラの距離に応じて適切に解像度を調整することが重要です。
解像度選択のポイント
- 近距離の影は高解像度で描画
カメラに近いオブジェクトの影は目立つため、高解像度のシャドウマップで詳細に描画します。
- 遠距離の影は低解像度で十分
遠くの影はぼやけて見えるため、低解像度でも違和感が少ないです。
- カスケードシャドウマップ(CSM)との併用
複数のシャドウマップを距離ごとに分割し、近距離は高解像度、遠距離は低解像度で描画する手法が効果的です。
動的解像度調整の例
// カメラからの距離に応じてシャドウマップ解像度を切り替え
int GetShadowMapResolution(float distance)
{
if (distance < 20.0f) return 2048;
else if (distance < 50.0f) return 1024;
else return 512;
}
メモリとパフォーマンスのバランス
- 高解像度シャドウマップはメモリ消費と描画負荷が増えるため、必要な範囲だけに適用することが望ましいです
- シャドウマップの解像度を動的に変更する場合は、テクスチャの再生成や切り替えコストにも注意が必要です
これらの影品質向上技法を適切に組み合わせることで、DirectX9環境でもリアルで自然な影を効率的に描画できます。
PCFによるソフトシャドウ化は手軽に導入でき、VSMやESMはより高度な表現を可能にします。
さらに、シーンスケールに応じた解像度戦略を採用することで、パフォーマンスと品質のバランスを最適化できます。
カスケードシャドウマップ(CSM)
カスケードシャドウマップ(CSM)は、広範囲のシーンに対して高品質な影を効率的に描画するための技術です。
カメラ視点の距離に応じてシャドウマップを複数の「カスケード(分割)」に分割し、近距離は高解像度、遠距離は低解像度で影を生成します。
ここでは、CSMの分割数と分割距離の決定方法、境界フェードとシームレス化の手法、そして各カスケードの解像度設定について詳しく解説します。
分割数と分割距離の決定
CSMでは、カメラの視錐台(ビューの範囲)を複数の距離範囲に分割し、それぞれに対応するシャドウマップを生成します。
分割数と分割距離の設定は影の品質とパフォーマンスに大きく影響します。
分割数の選択
- 一般的に3~4分割が多く使われます
- 分割数が多いほど近距離の影の品質は向上しますが、描画コストが増加します
- ハードウェア性能やシーンの規模に応じて適切な分割数を選択します
分割距離の決定方法
- 均等分割
カメラの近クリップ面から遠クリップ面までを均等に距離で分割します。
実装が簡単ですが、遠距離のカスケードに多くの領域が割り当てられ、近距離の解像度が不足しやすいです。
- 指数分割(Logarithmic Split)
近距離に多くの分割を割り当て、遠距離は広くカバーします。
これにより、近距離の影の品質が向上します。
分割距離
ここで、
- 混合分割(Practical Split)
均等分割と指数分割を線形補間してバランスを取る方法もあります。
実装例(擬似コード)
float nearClip = 1.0f;
float farClip = 100.0f;
int cascadeCount = 4;
float lambda = 0.5f; // 0:均等分割, 1:指数分割
float cascadeSplits[4];
for (int i = 1; i <= cascadeCount; i++)
{
float uniformSplit = nearClip + (farClip - nearClip) * (i / (float)cascadeCount);
float logSplit = nearClip * pow(farClip / nearClip, i / (float)cascadeCount);
cascadeSplits[i - 1] = lambda * logSplit + (1 - lambda) * uniformSplit;
}
境界フェードとシームレス化
複数のカスケード間で影の境界が目立つと、不自然な線やちらつきが発生します。
これを防ぐために、境界フェードやシームレス化の工夫が必要です。
境界フェードの手法
- フェード領域の設定
各カスケードの境界付近にフェード領域を設け、影の強さを線形またはスムーズに補間します。
- ピクセルシェーダでの補間
カスケードごとの影判定結果を取得し、境界付近では両方のカスケードの影の強さをブレンドします。
シームレス化のポイント
- カスケードのオーバーラップ
境界付近でカスケードの範囲を少し重ねることで、急激な切り替えを緩和します。
- 座標の正確な計算
各カスケードのシャドウマップ座標を正確に計算し、境界でのズレを最小化します。
- フィルタリングの活用
PCFなどのソフトシャドウ技術を用いることで、境界のギザギザやノイズを軽減します。
各カスケードの解像度設定
カスケードごとにシャドウマップの解像度を変えることで、パフォーマンスと品質のバランスを最適化します。
解像度の割り当て方
- 近距離カスケードは高解像度
カメラに近いカスケードは詳細な影が必要なため、高解像度のシャドウマップを割り当てます。
- 遠距離カスケードは低解像度
遠距離は影のディテールが見えにくいため、低解像度で十分です。
例:4カスケードの解像度設定
カスケード | 解像度 (例) |
---|---|
1 (最も近い) | 2048×2048 |
2 | 1024×1024 |
3 | 512×512 |
4 (最も遠い) | 256×256 |
動的解像度調整
- シーンの状況やパフォーマンスに応じて、解像度を動的に変更することも可能です
- 解像度の切り替え時はテクスチャの再生成やリソース管理に注意が必要です
これらのCSMの設定を適切に行うことで、広範囲かつ高品質な影を効率的に描画でき、リアルなライティング表現が可能になります。
分割数や距離の決定、境界の滑らかな切り替え、解像度の最適化はCSMの成功に欠かせない要素です。
パフォーマンス最適化
シャドウマッピングは影の品質向上に効果的ですが、計算負荷やメモリ消費が大きくなりがちです。
特にDirectX9の環境ではリソース制約もあるため、パフォーマンスを最適化する工夫が重要です。
ここでは、動的解像度コントロール、描画順序とオクルージョンカリング、VRAM消費量とメモリ転送制御の3つの観点から具体的な最適化手法を解説します。
動的解像度コントロール
シャドウマップの解像度を固定にすると、シーンの状況やカメラの距離に関わらず一定の負荷がかかります。
動的解像度コントロールは、状況に応じてシャドウマップの解像度を調整し、パフォーマンスと品質のバランスを取る手法です。
実装例
- カメラ距離に応じた解像度変更
カメラが近い場合は高解像度、遠い場合は低解像度に切り替えます。
これにより、無駄な高解像度処理を避けられます。
int GetShadowMapResolution(float cameraDistance)
{
if (cameraDistance < 20.0f) return 2048;
else if (cameraDistance < 50.0f) return 1024;
else return 512;
}
- フレームレートに応じた調整
フレームレートが低下した場合に解像度を下げ、パフォーマンスを維持します。
- カスケードシャドウマップとの連携
各カスケードの解像度を動的に調整し、近距離カスケードは高解像度、遠距離は低解像度に設定します。
注意点
- 解像度変更時はテクスチャの再生成やリソースの再割り当てが必要なため、頻繁な切り替えは避けます
- 解像度の切り替えはフレーム単位ではなく、数フレーム単位で行うと安定します
描画順序とオクルージョンカリング
描画順序の最適化とオクルージョンカリングは、不要な描画を減らしGPU負荷を軽減する重要な手法です。
描画順序の最適化
- 不透明オブジェクトの前方から後方への描画
深度テストを活用し、後方のオブジェクトの描画を省略できるようにします。
- シャドウマップ生成時の描画順序
ライト視点から見て近いオブジェクトを先に描画し、深度バッファに早期に書き込むことで、後続のオブジェクトの描画を省略可能にします。
オクルージョンカリング
- CPUベースのカリング
シーン内のオブジェクトがライト視点やカメラ視点から見えない場合は描画しないようにします。
バウンディングボックスや球体で視錐台カリングを行います。
- ハードウェアオクルージョンカリング
DirectX9ではハードウェアによるオクルージョンクエリを利用し、描画前にオブジェクトの可視性を判定します。
非表示の場合は描画をスキップします。
// オクルージョンクエリの例(擬似コード)
IDirect3DQuery9* occlusionQuery;
g_pd3dDevice->CreateQuery(D3DQUERYTYPE_OCCLUSION, &occlusionQuery);
occlusionQuery->Issue(D3DISSUE_BEGIN);
DrawBoundingBox(object);
occlusionQuery->Issue(D3DISSUE_END);
DWORD samplesPassed = 0;
while (S_OK != occlusionQuery->GetData(&samplesPassed, sizeof(DWORD), D3DGETDATA_FLUSH));
if (samplesPassed > 0)
{
DrawObject(object);
}
効果
- 不要なオブジェクトの描画を減らし、GPU負荷を軽減
- シャドウマップ生成時の描画負荷も削減可能です
VRAM消費量とメモリ転送制御
シャドウマップは高解像度のテクスチャを複数使用するため、VRAMの消費が大きくなりがちです。
メモリ転送の最適化も含めて管理することが重要です。
VRAM消費量の削減策
- 解像度の適切な設定
必要以上に高解像度のシャドウマップを使わない。
動的解像度コントロールと組み合わせます。
- カスケード数の最適化
カスケード数を増やしすぎるとテクスチャ数が増え、VRAM消費が増大するためバランスを取ります。
- テクスチャ圧縮の活用
浮動小数点テクスチャの圧縮は難しいが、可能な場合は圧縮フォーマットを利用します。
メモリ転送の最適化
- テクスチャの更新頻度を減らす
シャドウマップは毎フレーム更新が基本ですが、動的なライトやオブジェクトが少ない場合は更新頻度を下げます。
- レンダーターゲットの再利用
シャドウマップ用のテクスチャやサーフェスを使い回し、不要なリソースの生成・破棄を避けます。
- バッファのロック回数を減らす
テクスチャやバッファのロック・アンロックはコストが高いため、必要最低限に抑えます。
VRAMとメモリ転送の管理は、シャドウマッピングのパフォーマンスに直結します。
適切な解像度設定、リソース管理、更新頻度の調整を行い、効率的な描画を目指しましょう。
これらのパフォーマンス最適化手法を組み合わせることで、DirectX9環境でも高品質なシャドウマッピングを快適に実行できます。
動的解像度コントロールで負荷を調整し、描画順序とオクルージョンカリングで無駄な描画を削減、さらにVRAMとメモリ転送を効率化することが重要です。
デバッグと可視化
シャドウマッピングの実装では、影の品質や動作を正確に把握するためにデバッグと可視化が欠かせません。
特にDirectX9の環境では、シャドウマップの内容を直接確認できる機会が少ないため、オンスクリーン表示や深度レンジの可視化、境界線のハイライトなどの手法を活用して問題点を特定しやすくします。
ここでは、代表的なデバッグ手法を詳しく解説します。
シャドウマップのオンスクリーン表示
シャドウマップは通常、深度情報を格納したテクスチャとしてGPU内部で管理されており、直接画面に表示されることはありません。
デバッグ時には、このシャドウマップを画面の一部に表示して内容を確認することが有効です。
実装方法
- シャドウマップテクスチャの取得
シャドウマップとして使用しているテクスチャをピクセルシェーダでサンプリングします。
- 画面の小領域に描画
画面の隅などに小さな四角形(スクリーン四角形)を描画し、そのテクスチャにシャドウマップを割り当てます。
- 深度値の正規化
深度値は0~1の範囲ですが、場合によっては線形化やスケーリングを行い、見やすく調整します。
サンプルコード(擬似コード)
// シャドウマップテクスチャをセット
g_pd3dDevice->SetTexture(0, g_shadowMap);
// スクリーン四角形の頂点バッファを用意し描画
DrawScreenQuad(x, y, width, height);
ピクセルシェーダ例(HLSL)
sampler2D shadowMap : register(s0);
float4 PS_DisplayShadowMap(float2 texCoord : TEXCOORD) : COLOR
{
float depth = tex2D(shadowMap, texCoord).r;
return float4(depth, depth, depth, 1.0f); // グレースケール表示
}
この方法で、シャドウマップの深度分布を視覚的に確認でき、影の範囲や解像度の問題を把握しやすくなります。
深度レンジのヒートマップ表示
シャドウマップの深度値は単なるグレースケール表示ではわかりにくい場合があります。
深度レンジを色で表現するヒートマップ表示を行うことで、深度の分布や偏りを直感的に把握できます。
実装方法
- 深度値を0~1の範囲で取得し、色相や輝度にマッピングします
- 例えば、深度が浅い部分を赤、深い部分を青にするなど、カラーマップを適用します
ピクセルシェーダ例(HLSL)
float4 DepthToHeatmap(float depth)
{
float3 color;
color.r = saturate(1.0f - depth * 2.0f); // 赤は近いほど強い
color.g = saturate(1.0f - abs(depth - 0.5f) * 4.0f); // 緑は中間距離で強い
color.b = saturate(depth * 2.0f); // 青は遠いほど強い
return float4(color, 1.0f);
}
float4 PS_Heatmap(float2 texCoord : TEXCOORD) : COLOR
{
float depth = tex2D(shadowMap, texCoord).r;
return DepthToHeatmap(depth);
}
このヒートマップ表示により、深度の偏りや不自然な分布が一目でわかり、シャドウマップの設定調整に役立ちます。
境界線ハイライトによる誤差確認
シャドウマップの境界部分は影の品質に大きく影響し、誤差やアーティファクトが発生しやすい領域です。
境界線をハイライト表示することで、影の切れ目や不連続な部分を視覚的に検出できます。
実装方法
- シャドウマップの深度値の勾配(隣接ピクセル間の差分)を計算し、急激な変化がある部分を境界として検出します
- 境界部分を赤や黄色などの目立つ色で表示します
ピクセルシェーダ例(HLSL)
float2 texelSize; // シャドウマップの1ピクセルサイズ
float4 PS_BoundaryHighlight(float2 texCoord : TEXCOORD) : COLOR
{
float depthCenter = tex2D(shadowMap, texCoord).r;
float depthRight = tex2D(shadowMap, texCoord + float2(texelSize.x, 0)).r;
float depthUp = tex2D(shadowMap, texCoord + float2(0, texelSize.y)).r;
float diffX = abs(depthCenter - depthRight);
float diffY = abs(depthCenter - depthUp);
float edgeThreshold = 0.01f; // 調整可能な閾値
if (diffX > edgeThreshold || diffY > edgeThreshold)
{
return float4(1, 0, 0, 1); // 赤色で境界をハイライト
}
else
{
return float4(depthCenter, depthCenter, depthCenter, 1); // 通常表示
}
}
効果
- 影の境界の不連続や誤差を視覚的に把握できるため、バイアス調整やフィルタリングの効果検証に役立ちます
- 境界のギザギザやノイズの原因を特定しやすくなります
これらのデバッグと可視化手法を活用することで、DirectX9でのシャドウマッピング実装における問題点を効率的に発見し、影の品質向上に繋げることができます。
オンスクリーン表示やヒートマップ、境界線ハイライトは、開発中の必須ツールとして積極的に導入しましょう。
トラブルシューティング
シャドウマッピングの実装では、さまざまな問題が発生しやすく、影が正しく表示されない、影が飛ぶ、遠景で影が途切れるなどのトラブルがよく起こります。
ここでは、代表的な問題に対するチェックリストや原因の分析、具体的な対処法を詳しく解説します。
影が表示されない場合のチェックリスト
影がまったく表示されない場合、以下のポイントを順に確認してください。
- シャドウマップの生成が正しく行われているか
- ライト視点からのレンダリングパスで深度情報が正しく書き込まれているか
- シャドウマップ用のレンダーターゲットや深度ステンシルサーフェスが正しく設定されているか
- 深度バッファのクリアが行われているか
- シャドウマップのテクスチャが正しくバインドされているか
- カメラ視点のレンダリング時にシャドウマップテクスチャがピクセルシェーダのサンプラーにセットされているか
- サンプラーステート(フィルタリング、アドレッシング)が適切に設定されているか
- 座標変換行列の誤り
- カメラ視点の頂点座標からライト視点のシャドウマップ座標への変換行列(ビュー・プロジェクション・バイアス行列)が正しく計算・適用されているか
- バイアス行列の掛け忘れや順序の誤りがないか
- 深度比較のロジックの誤り
- ピクセルシェーダでの深度比較が正しく行われているか
- バイアス値が大きすぎて常に影判定が偽になっていないか
- レンダーステートの設定ミス
- 深度テストや深度書き込みが有効になっているか
- カラーバッファの書き込みが無効化されている場合、影の描画に影響がないか
- ハードウェアやドライバの制限
- 使用しているGPUが必要なフォーマットや機能をサポートしているか
- ドライバのバグや制限による問題がないか
ライト位置ズレによる影飛び
影飛びとは、影が不自然に動いたり、ちらついたりする現象で、特にライトの位置や行列の計算ミスが原因で起こります。
主な原因
- ライトのビュー行列やプロジェクション行列の計算誤差
ライトの位置や方向が正しく設定されていないと、シャドウマップの座標系がずれ、影が飛びます。
- バイアス値の不適切な設定
バイアスが小さすぎるとセルフシャドウの誤判定が増え、影がちらつくことがあります。
- シャドウマップの解像度不足
解像度が低いと、影の位置が粗くなり、動きに対して影が飛んで見えます。
対処法
- ライトの行列を正確に計算する
- ライトの位置、注視点、上方向ベクトルを正しく設定し、
D3DXMatrixLookAtLH
などでビュー行列を作成 - プロジェクション行列もシーンに合ったパラメータで設定
- バイアスの調整
- Constant BiasとSlope Scale Biasを適切に設定し、影のちらつきを抑制
- 自動調整アルゴリズムの導入も効果的
- シャドウマップ解像度の向上
- 必要に応じて解像度を上げます
- カスケードシャドウマップを利用し、近距離は高解像度に
- 行列の更新タイミングを確認
- ライトの行列が毎フレーム正しく更新されているか
- 行列の更新漏れや古い行列の使用を防止
遠景で影が途切れる問題への対応
遠景で影が途切れたり消えたりする問題は、シャドウマップの範囲設定や深度精度の問題が主な原因です。
主な原因
- ライトの投影範囲が狭い
シャドウマップのビュー・プロジェクション行列で設定した範囲が遠景をカバーしていない。
- 深度バッファの精度不足
遠距離になるほど深度値の分布が圧縮され、影の判定が不安定になります。
- シャドウマップの解像度不足
遠距離の領域に十分なピクセル数が割り当てられていない。
対処法
- ライトの投影範囲を適切に設定
- 遠クリップ面を十分に遠くに設定し、遠景をカバー
- 正射影の場合は投影ボックスのサイズを調整
- カスケードシャドウマップの導入
- 遠距離用のカスケードを設け、解像度を落としても影を途切れさせない
- 深度値の線形化とバイアス調整
- 深度の非線形性を考慮し、線形化処理を行います
- バイアスを調整して遠距離の影判定を安定化
- シャドウマップ解像度の最適化
- 遠距離カスケードの解像度を適切に設定し、必要に応じて動的に調整
これらのトラブルシューティング手法を活用し、DirectX9でのシャドウマッピング実装における問題を迅速に解決しましょう。
影が表示されない、影が飛ぶ、遠景で途切れるといった典型的な問題は、原因を特定し適切な対策を講じることで改善可能です。
さらなる発展例
シャドウマッピングの基本実装をマスターした後は、より複雑でリアルなシーンに対応するための発展的な技術に挑戦すると良いでしょう。
ここでは、複数ライトへの対応、アニメーションメッシュとスキニングの対応、そしてポストエフェクトとの連携による雰囲気向上について詳しく解説します。
複数ライトへの拡張
複数の光源が存在するシーンでは、それぞれのライトに対してシャドウマップを生成し、影を合成する必要があります。
これにより、よりリアルで複雑なライティング表現が可能になります。
実装のポイント
- ライトごとのシャドウマップ生成
各ライトの視点からシャドウマップを作成します。
ライトの数が増えるとレンダリングパスも増加するため、パフォーマンスに注意が必要です。
- シャドウマップの管理
複数のシャドウマップテクスチャを用意し、ライトごとに適切にバインド・切り替えを行います。
- 影の合成
カメラ視点のピクセルシェーダで、各ライトのシャドウマップを参照し、影の有無を判定。
複数の影を合成して最終的な影の強さを決定します。
合成方法の例
- 加算合成
各ライトの影の強さを加算し、影が濃くなる表現。
- 乗算合成
影の存在確率を乗算し、複数の影が重なる部分をより暗くする表現。
注意点
- ライト数が多い場合はパフォーマンスが大幅に低下するため、影を落とすライトを限定するか、影の品質を調整する必要があります
- シャドウマップの解像度や更新頻度をライトごとに最適化すると効率的です
アニメーションメッシュとスキニング対応
動的に変形するアニメーションメッシュに対してもシャドウマッピングを適用するには、スキニング処理に対応した実装が必要です。
実装のポイント
- スキニング行列の適用
頂点シェーダでボーン行列を用いて頂点を変形し、その後ライト視点のビュー・プロジェクション行列に変換します。
- ライト視点でのスキニングレンダリング
シャドウマップ生成時にもアニメーションメッシュを正しく変形させて描画する必要があります。
- パフォーマンス考慮
スキニング処理は計算コストが高いため、シャドウマップ生成用のシェーダも最適化が求められます。
サンプルコード(頂点シェーダの一部)
float4x4 g_boneMatrices[MAX_BONES];
float4x4 g_lightViewProj;
struct VS_INPUT
{
float3 pos : POSITION;
float4 boneWeights : BLENDWEIGHT;
uint4 boneIndices : BLENDINDICES;
};
struct VS_OUTPUT
{
float4 pos : POSITION;
};
VS_OUTPUT VS_SkinnedShadow(VS_INPUT input)
{
float4 skinnedPos = float4(0,0,0,0);
for (int i = 0; i < 4; i++)
{
skinnedPos += mul(float4(input.pos, 1.0f), g_boneMatrices[input.boneIndices[i]]) * input.boneWeights[i];
}
VS_OUTPUT output;
output.pos = mul(skinnedPos, g_lightViewProj);
return output;
}
ポストエフェクト連携での雰囲気向上
シャドウマッピングの影をポストエフェクトと組み合わせることで、シーン全体の雰囲気やリアリティをさらに高めることができます。
代表的なポストエフェクト例
- ブラー(ぼかし)
シャドウマップの影の境界をポスト処理でぼかし、より自然なソフトシャドウを実現。
- アンビエントオクルージョン(AO)との組み合わせ
シャドウマップの影とAOを合成し、陰影の深みを増します。
- カラーグレーディングやトーンマッピング
影の色調や明暗を調整し、シーンの雰囲気を演出。
実装のポイント
- シャドウマップ生成後、影の情報をテクスチャとして取得し、ポストエフェクト用のシェーダに渡します
- ポストエフェクトの処理順序を考慮し、影の描画後に適用します
- パフォーマンスに配慮し、必要に応じて解像度を落としたバッファで処理
サンプル:影のぼかし(ガウシアンブラー)
- シャドウマップを低解像度のテクスチャにレンダリング。
- 横方向と縦方向に分けてガウシアンブラーを適用。
- 元のシーン描画時にぼかした影テクスチャを合成。
これらの発展的な技術を取り入れることで、DirectX9環境でもよりリアルで表現力豊かなシャドウマッピングが可能になります。
複数ライト対応やアニメーションメッシュのスキニング対応は実装の難易度が上がりますが、シーンのリアリティ向上に大きく寄与します。
また、ポストエフェクトとの連携は視覚的な質感を高める強力な手段です。
まとめ
本記事では、DirectX9を用いたシャドウマッピングの基礎から応用までを詳しく解説しました。
ライト視点での深度マップ生成やカメラ視点での影判定の基本フロー、行列変換やバイアス調整による精度向上、さらにPCFやVSMなどの影品質向上技法、カスケードシャドウマップによる広範囲対応、パフォーマンス最適化手法、デバッグ方法、トラブルシューティング、そして複数ライトやアニメーション対応などの発展例まで幅広く理解できます。
これにより、DirectX9環境で高品質かつ効率的なシャドウマッピング実装が可能になります。