ジオメトリシェーダでステンシルシャドウを実装してみます。以前(DX9など)はシャドウボリュームの生成が面倒でしたが、ジオメトリシェーダを使うことによって簡単にできそうです。それと、単純な実装ではピクセルシェーダの負荷が大きすぎるため、できるだけシャドウボリュームの描画を減らす最適化も同時に行います。

ステンシルシャドウ
詳しい原理などは省略。ポリゴンの辺を光源方向に引き伸ばしたシャドウボリュームを描画することで影を生成します。表の場合ステンシル値を+1、裏の場合に-1とすると影の部分のステンシル値が0以外になるという仕組みを使い、すべてのポリゴンに対してこの処理を行うことで、ポリゴンモデルの影を落とすことができます。シャドウボリュームの生成にジオメトリシェーダを利用します。
デプスステンシルステートの設定
//シャドウボリューム用シェーダエフェクト定義
#ifdef ZGFX
technique Basic
{
pass P0
{
RenderTargetWriteMask[0] = 0;//更新しない
CullMode = NONE;
DepthEnable = TRUE;
DepthFunc = LESS;
DepthWriteMask = ZERO;//デプス値更新しない
StencilEnable = TRUE;
StencilReadMask = 0xff;
StencilWriteMask = 0xff;
FrontFaceStencilFail = KEEP;
FrontFaceStencilDepthFail = KEEP;
FrontFaceStencilPass = INCR;//表だったら+1
FrontFaceStencilFunc = ALWAYS;
BackFaceStencilFail = KEEP;
BackFaceStencilDepthFail = KEEP;
BackFaceStencilPass = DECR;//裏だったら-1
BackFaceStencilFunc = ALWAYS;
VertexShader = compile vs_4_0 vsBasicBlend();
GeometryShader = compile gs_4_0 gsBasic();
PixelShader = compile ps_4_0 psBasic();
}
}
#endif//ZGFX
※ステートの設定は独自FXファイルで行っています
シャドウボリューム生成と最適化
すべてのポリゴンに対してシャドウボリュームを生成した場合、とてつもない量のピクセル処理が発生してしまいます。それを軽減するために事前に描画不要なシャドウボリュームを検出し、最適化を行います。隣接ポリゴンが共有している辺から延びるシャドウボリューム面は、必ず片方が+1、もう片方が-1となるため描画をキャンセルできます(ただし、光源方向から見た方向が同じ場合のみキャンセル可能)。これをジオメトリシェーダで検出し、必要なシャドウボリュームのみを描画します。隣接ポリゴンの情報が必要となるため、プリミティブは隣接付三角形リスト(triangleadj)を使用。隣接するポリゴンがない場合、正反対の隣接ポリゴンを登録、ジオメトリシェーダで判定、そこには必ずシャドウボリュームを描画。
bool vec_equal(float3 a, float3 b)
{
return a.x==b.x && a.y==b.y && a.z==b.z;
}
// ジオメトリシェーダ
[maxvertexcount(12)]
void gsBasic( triangleadj GS_INPUT input[6],
uint primID : SV_PrimitiveID,
inout TriangleStream<PS_INPUT> Stream )
{
// input[].Posはワールド座標系
float3 sdw_dir = -normalize(Light.vDirLight[0].xyz);
float4 v0 = input[0].Pos;
float4 va01 = input[1].Pos;
float4 v1 = input[2].Pos;
float4 va12 = input[3].Pos;
float4 v2 = input[4].Pos;
float4 va20 = input[5].Pos;
float4 vs0 = v0 + float4(sdw_dir*100.0,0);
float4 vs1 = v1 + float4(sdw_dir*100.0,0);
float4 vs2 = v2 + float4(sdw_dir*100.0,0);
// 誤差 ポリゴンの面積などで調整が必要
const float d_eps = 0.0000001;
// 光源方向から見た向き
float dl0 = dot( cross(v1.xyz - v0.xyz ,v2.xyz - v0.xyz).xyz, sdw_dir);
if( dl0 > d_eps)return;
// 隣接ポリゴンの向き
float dl1 = dot( cross(va01.xyz - v0.xyz ,v1.xyz - v0.xyz).xyz, sdw_dir);
float dl2 = dot( cross(va12.xyz - v1.xyz ,v2.xyz - v1.xyz).xyz, sdw_dir);
float dl3 = dot( cross(va20.xyz - v2.xyz ,v0.xyz - v2.xyz).xyz, sdw_dir);
float4x4 vpmtx = mul(Scene.mtxView, Scene.mtxProj);
float4 pv0 = mul(v0, vpmtx);
float4 pv1 = mul(v1, vpmtx);
float4 pv2 = mul(v2, vpmtx);
float4 pvs0 = mul(vs0, vpmtx);
float4 pvs1 = mul(vs1, vpmtx);
float4 pvs2 = mul(vs2, vpmtx);
PS_INPUT vo0,vo1;
//隣接ポリゴンの描画で相殺される場合は、描画しない
//隣接ポリゴンが正反対の場合、隣接ポリゴンなし 必ず描画
if( dl1 > d_eps || vec_equal(v2.xyz,va01.xyz)){
vo0.Pos = pv0;
vo1.Pos = pvs0;
Stream.Append(vo0);
Stream.Append(vo1);
vo0.Pos = pv1;
vo1.Pos = pvs1;
Stream.Append(vo0);
Stream.Append(vo1);
Stream.RestartStrip();
}
if( dl2 > d_eps || vec_equal(v0.xyz,va12.xyz)){
vo0.Pos = pv1;
vo1.Pos = pvs1;
Stream.Append(vo0);
Stream.Append(vo1);
vo0.Pos = pv2;
vo1.Pos = pvs2;
Stream.Append(vo0);
Stream.Append(vo1);
Stream.RestartStrip();
}
if( dl3 > d_eps || vec_equal(v1.xyz,va20.xyz)){
vo0.Pos = pv2;
vo1.Pos = pvs2;
Stream.Append(vo0);
Stream.Append(vo1);
vo0.Pos = pv0;
vo1.Pos = pvs0;
Stream.Append(vo0);
Stream.Append(vo1);
Stream.RestartStrip();
}
}
※本当に最適化されているか未確認、FPS計測で確認予定
追記2013/4/30 最適化の効果を測定
CPU:Intel Core i3 3.30GHz
GPU:GeForce GTX 660
最適化 | FPS | ピクセル書込 |
---|---|---|
前 | 255 | 18917158 |
後 | 1200 | 3722369 |
効果絶大。
影の描画
シャドウボリュームによって影の部分のステンシル値が0以外になっているので、その部分を半透明描画で暗くします。
画面全体に以下のシェーダ定義で黒いポリゴンを描画。
//影描画用シェーダエフェクト定義
#ifdef ZGFX
technique Basic
{
pass P0
{
CullMode = NONE;
DepthEnable = FALSE;
DepthWriteMask = ZERO;
DepthFunc = GREATER;
StencilEnable = TRUE;
StencilReadMask = 0xff;
StencilWriteMask = 0x00;
FrontFaceStencilFail = KEEP;
FrontFaceStencilDepthFail = KEEP;
FrontFaceStencilPass = KEEP;
FrontFaceStencilFunc = NOT_EQUAL;
BackFaceStencilFail = KEEP;
BackFaceStencilDepthFail = KEEP;
BackFaceStencilPass = KEEP;
BackFaceStencilFunc = NOT_EQUAL;
BlendEnable[0] = TRUE;
SrcBlend[0] = SRC_ALPHA;
DestBlend[0] = INV_SRC_ALPHA;
BlendOp[0] = ADD;
SrcBlendAlpha[0] = ONE;
DestBlendAlpha[0] = ZERO;
BlendOpAlpha[0] = ADD;
VertexShader = compile vs_4_0 vsMain();
PixelShader = compile ps_4_0 psMain();
}
}
#endif//ZGFX
※ステートの設定は独自FXファイルで行っています
サンプルプログラム
“ステンシルシャドウ” をダウンロード dx11StencilShadow.zip – 877 回のダウンロード – 65 KB
隣接付三角形リストの生成
//zg_model.h/cpp
bool CreateShadowVol(const Object& obj, Mesh& out_mesh);
PMD/VMDファイル、シェーダファイル
sample_data.hで読み込むファイルを指定します。