ジオメトリシェーダでステンシルシャドウ&最適化

投稿日:

ジオメトリシェーダでステンシルシャドウを実装してみます。以前(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 – 790 回のダウンロード – 65 KB

隣接付三角形リストの生成

//zg_model.h/cpp
bool CreateShadowVol(const Object& obj, Mesh& out_mesh);

PMD/VMDファイル、シェーダファイル

sample_data.hで読み込むファイルを指定します。

リンクなど

スクリーンショットのモデルとモーション

モデルデータ

ままま式GUMIβ版

モーションデータ

【第9回MMD杯Ex】星間飛行GUMI/ニコニコ動画

コメントを残す

メールアドレスが公開されることはありません。

認証:数字を入力してください(必須) * Time limit is exhausted. Please reload CAPTCHA.