光源計算の処理と最適化を行います。点光源などの影響範囲が限定されている場合、計算が必要なピクセルを検出することで処理時間を短縮します。

左下:最終結果 右下:Diffuse
光源計算
ジオメトリバッファを使い、ピクセル毎の光源計算を行います。画面全体のピクセルに対して処理してしまうと時間がかかりるぎるため、光源の影響範囲のピクセルのみ処理します。点光源であれば球、スポットライトは円柱などを描画。
複数の光源計算は1つのシェーダで一括して行わずに、光源毎に球や円柱を加算合成で描画します。これで光源の数や種類に制限がなくなり、時間の許す限り何個でも追加可能(ディファード系レンダリングの特徴)。
シェーダコード 点光源
//デプスバッファ
Texture2D<float> txDepth : register( t0 );
SamplerState samDepth : register( s0 );
//法線(-1~+1 ⇒ 0~1)
Texture2D<float3> txNormal : register( t1 );
SamplerState samNormalr : register( s1 );
//スペキュラ強度
Texture2D<float4> txSpecPow : register( t2 );
SamplerState samSpecPow : register( s2 );
struct PS_INPUT
{
float4 Pos : SV_POSITION;
};
struct LLP_PTLIGHT
{
float4 Pos;
float4 Col;
float4 Prm;//減衰式 = Prm.x*r^Prm.w + Prm.y, Prm.z = 影響半径
};
//-------------------------
//ピクセルシェーダ
PS_OUTPUT psMain( PS_INPUT input )
{
PS_OUTPUT o = (PS_OUTPUT)0;
// 定数バッファより
float2 viewport = (レンダーターゲットの幅,高さ);
matrix proj_mtx = プロジェクション行列;
matrix view_mtx = ビュー行列;
LLP_PTLIGHT PtLight = 点光源情報(定数バッファ);
//ジオメトリバッファのテクスチャ座標(法線、デプス値取得用)
float2 tex = input.Pos.xy/viewport .xy;
// スクリーン座標 左上(-1,+1) 右下(+1,-1)
float2 spos = float2(2,-2)*tex + float2(-1,1);
// 位置(view座標系)
float3 vpos;
float dep = txDepth.Sample( samDepth, tex );
vpos.z = proj_mtx._m32/(dep - proj_mtx._m22);
vpos.xy = spos.xy*vpos.z/float2(proj_mtx._m00,proj_mtx._m11);
// 点光源の位置(view座標系)
float4 ptpos = mul(float4(PtLight.Pos.xyz,1), view_mtx);
// 位置(view座標系) = -視線方向
// 法線(view座標系) 0~1を-1~+1に変換
float3 nor = 2.0*txNormal.Sample( samNormalr, tex) - 1;
//点光源までの距離
float3 dir = ptpos.xyz - vpos.xyz;
float r = length(dir);
// 光源の方向(view座標系)
dir = dir/r;
// 減衰 A*r^n + B
float atte = saturate( PtLight.Prm.x*pow(r,PtLight.Prm.w) + PtLight.Prm.y );
// 拡散
float df = saturate( dot(nor,dir) );
// 鏡面
float3 h = normalize( dir - normalize(vpos) );//ハーフベクトル
float spec_pow = 255*txSpecPow.Sample( samSpecPow, tex ).x;
float3 spec = pow( saturate( dot(nor,h) ), spec_pow );
o.diff = float4( atte*df*PtLight.Col.xyz, 1);
o.spec = float4( atte*spec*PtLight.Col.xyz, 1);
return o;
}
減衰モデル
距離の2乗に反比例する物理モデルだと減衰が急激過ぎて扱いにくいので、物理法則を無視した独自の方法を考えました。
減衰率 = A*r^n + B (^n:n乗) r : 光源からの距離 n : 任意の値(≠0) 減衰曲線を制御 rmin : 減衰開始距離 減衰率=1 rmax : 影響範囲 減衰率=0 A = -1/(rmax^n-rmin^n) B = rmax^n/(rmax^n-rmin^n)

光源計算の最適化
光源計算を行う際、処理ピクセル数を減らすために影響範囲の形状で描画を行いますが、それだけでは不十分です。奥と手前部分の影響範囲外のピクセルも処理してしまうため、計算が必要な部分をさらに限定して最適化を行います。

緑:影響範囲 赤:範囲外(奥) 青:範囲外(手前)
ステンシルテストを使った最適化
ステンシルテストに失敗した場合、ピクセルシェーダが実行されないことを利用します。赤青の部分と緑の部分に異なるステンシル値を書き込み、ステンシルテストで緑の部分だけが合格するようにステート設定し描画します。
3回のレンダリング処理が必要、赤青検出のステンシル書き込み2回と光源計算。
ステンシル書き込み時間 < 最適化による時間短縮
でないと意味がない。
デプステストでの最適化
デプステストはピクセルシェーダ後に実行されるため意味がないように見えますが、GPUの早期Zカリングという機能が利用できればピクセルシェーダ前にキャンセル可能です。ただし明示的に利用できず、DirectXやドライバー任せなので機能が有効にならない場合があります。
早期Zカリングは事前に大まかな単位でデプステストを行い、ピクセルシェーダ実行前に不要なピクセルを検出、カリングする機能。
2回レンダリング、赤青のどちらかを光源処理時のデプステスト(早期Zカリング)で除去。
//注 Directx9のFXファイルを参考にした独自フォーマットです
//2パス ステンシルテストとデプステスト(早期Zカリング)を使用した最適化
technique PointLightTwoPass
{
// 裏面&近い(GREATER)場合、ステンシル1それ以外0
// ステンシル=1が光源計算候補のピクセル
pass P0 {
CullMode = FRONT;
DepthFunc = GREATER;
DepthWriteMask = ZERO;
StencilEnable = TRUE;
StencilReadMask = 0xff;
StencilWriteMask = 0xff;
StencilRef = 1;
FrontFaceStencilFail = KEEP;
FrontFaceStencilDepthFail = KEEP;
FrontFaceStencilPass = KEEP;
FrontFaceStencilFunc = ALWAYS;
BackFaceStencilFail = ZERO;
BackFaceStencilDepthFail = ZERO;
BackFaceStencilPass = REPLACE;//StencilRef = 1
BackFaceStencilFunc = ALWAYS;
VertexShader = compile vs_4_0 vsMain();
PixelShader = compile ps_5_0 psNone();
}
// ステンシル=1 & 表面 & 遠い(LESS)場合のみ光源計算実行
pass P1 {
CullMode = BACK;
DepthEnable = TRUE;
DepthFunc = LESS;
DepthWriteMask = ZERO;
//加算合成
BlendEnable[0] = TRUE;
SrcBlend[0] = ONE;
DestBlend[0] = ONE;
BlendOp[0] = ADD;
SrcBlendAlpha[0] = ONE;
DestBlendAlpha[0] = ZERO;
BlendOpAlpha[0] = ADD;
BlendEnable[1] = TRUE;
SrcBlend[1] = ONE;
DestBlend[1] = ONE;
BlendOp[1] = ADD;
SrcBlendAlpha[1] = ONE;
DestBlendAlpha[1] = ZERO;
BlendOpAlpha[1] = ADD;
StencilEnable = TRUE;
StencilReadMask = 0xff;
StencilWriteMask = 0xff;
StencilRef = 1;
FrontFaceStencilFail = KEEP;
FrontFaceStencilDepthFail = KEEP;
FrontFaceStencilPass = KEEP;
FrontFaceStencilFunc = EQUAL;
BackFaceStencilFail = KEEP;
BackFaceStencilDepthFail = KEEP;
BackFaceStencilPass = KEEP;
BackFaceStencilFunc = EQUAL;
VertexShader = compile vs_4_0 vsMain();
PixelShader = compile ps_5_0 psMain();
}
}
サンプルプログラム
“ライトプリパスレンダリングPart2 光源計算&最適化” をダウンロード dx11LightPrePass2.zip – 943 回のダウンロード – 90 KB