ボリューム影


~ステンシルバッファによる影~






■はじめに

今回はステンシルバッファを使ったボリューム影です。

まぁ、いつものように適当にファイルが入っています。

shadow.vsh影ボリュームの描画。
vs.vsh普通の描画。
draw.cppメインの描画部分。
draw.h描画の各関数の定義。
bg.cpp背景の描画。
load.cppメッシュのロード。
load.hロードのインターフェイス。
main.h基本的な定数など。
main.cpp描画に関係しないシステム的な部分。
sky.bmp (空)
tile.bmp (地面)

あと、モデルと実行ファイル及び、プロジェクトファイルが入っています。

■やってること

ステンシルバッファに対して演算して影になる部分を計算します。
影になる部分の計算は、最初に表面だけをレンダリングします。この時に、深度テストに合格したピクセルのステンシルの値を1増やします。
次に裏面だけをレンダリングします。ステンシルバッファの値は1減算します。
できたステンシルバッファに対して、ステンシルテストをしながら全画面に1枚のポリゴンを張り詰め、影を描画します。
凸多面体の場合は、空間にポリゴンを書けば、表も書けばその後ろに裏の面があるので、ステンシルバッファの値は0になっています。
裏面が地面の裏になっている部分は、減算する量が少ないので、ステンシルバッファの値が正の値になって、影が落ちる部分がわかります。

一番問題になるのは、物体の影になる部分を表現する影ボリュームの作成です。
一番すぐに思いつくのが、法線ベクトルがライト方向を向いているのか調べて、その方法に引き伸ばすことですが、 この方法は、特許公開2002-074391 とかに引っかかってしまうので、(交渉なんかしたくないですし)別の方法を考えましょう。
そこで考えたのが、裏面を向いたポリゴンを遠方平面に射影することでです。

裏を向いたポリゴンに関して、最遠方に描画することによって、影になる部分を表現します。
まぁ、他の特許がどうなっているのか知らないので、何に引っかかるかわからないですが、

この方法で作成した影ボリュームは、次のようになります。

白一色でわかりにくいですが、光の影になっている部分が放射方向に伸びています。

■ソースファイル

それではプログラムを見ていきましょう。
頂点シェーダですが、頂点ごとに伸ばす位置を計算して正負反転した後に引き伸ばします。
但し、表面も境界部分がちらついてしまうので、少し奥に押します(c16の定数分です)。
遠方平面の位置は、c15のw成分に入れます。

0001: ; Shadow.vsh
0002: ; c0-3   -- world + ビュー + 透視変換行列
0003: ; c12    -- {0.0, 0.5, 1.0, 2.0}
0004: ; c13    -- ライトのベクトル 
0005: ; c15    -- ライトの位置のベクトル 
0006: ;
0007: ; v0    頂点の座標値
0008: ; v3    法線ベクトル (w成分は1.0f)
0009: ; v7    テクスチャ座標
0010: 
0011: vs.1.0
0012: 
0013: ;座標変換
0014: sub r0, v0, c15       ; r0 = v-eye
0015: dp3 r1, r0, r0
0016: rsq r1.w, r1.w        ; r1.w = 1/|v-eye|
0017: mul r1.w, c15.w, r1.w ; r1.w = far/|v-eye|
0018: mad r2, r1.w, r0, -r0 ; r2 = (far-|v-eye|)*(v-eye)/|v-eye|
0019: 
0020: dp3 r1, v3, r0
0021: sge r1.w, r1.w, c12.x ; r1.w = (0<=N・L)?1:0
0022: add r1.w, r1, c16     ; オフセット
0023: 
0024: mad r0, -r2, r1.w, v0 ; r0 =  v - (0<=N・L)?(far-|v-eye|)*(v-eye)/|v-eye|:0
0025: mov r0.w, c12.z       ; r0.w = 1
0026: m4x4 oPos, r0, c0

描画の呼び出し部分は、次のようになります。
初期化時にステンシルバッファも初期化します。
その次に、普通に描画して深度バッファ及び影のない画面を作ります。

draw.c
0302: VOID Render(LPDIRECT3DDEVICE8 lpD3DDev)
0303: {
0304:     // 深度を初期化する
0305:     lpD3DDev->Clear(0, NULL, D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER|D3DCLEAR_STENCIL
0306:                     , D3DCOLOR_RGBA(0,0,0,0), 1.0f, 0L);
0307: 
0308:     // ------------------------------------------------------------------------
0309:     // 通常レンダリング
0310:     // ------------------------------------------------------------------------
0311:     lpD3DDev->SetTextureStageState(0,D3DTSS_COLOROP,    D3DTOP_SELECTARG1);
0312:     lpD3DDev->SetVertexShader(hVertexShader);
0313:     DrawModel(lpD3DDev, 1);

次に、ステンシルバッファに書き込みます。設定するレンダリングステートは、D3DRS_STENCIL****です。

D3DRS_STENCILENABLEステンシルバッファを有効にします
D3DRS_STENCILFUNCステンシルバッファと参照値を比較する方法を気めます
D3DRS_STENCILZFAIL深度落ちしたときのふるまいを設定します
D3DRS_STENCILFAILステンシルテスト落ちしたときのふるまいを設定します
D3DRS_STENCILPASSテストに合格した時のふるまいを設定します
D3DRS_STENCILREFステンシルバッファの値と比較する参照値です
D3DRS_STENCILMASK比較する時に特定のビットを判定するためのものです
D3DRS_STENCILWRITEMASK特定のビットに書き込むためのビット指定です

最初は、常にステンシルテストで合格する設定にして、合格した時にステンシルバッファの値を1増やします。

draw.c
0315:     // ------------------------------------------------------------------------
0316:     // 影の作成
0317:     // ------------------------------------------------------------------------
0318:     lpD3DDev->SetRenderState(D3DRS_ZWRITEENABLE, FALSE);// zバッファの更新無し
0319:     lpD3DDev->SetRenderState(D3DRS_STENCILENABLE, TRUE);// ステンシルバッファ使用
0320:     lpD3DDev->SetRenderState(D3DRS_COLORWRITEENABLE, 0);// 色は書かない
0321: 
0322:     lpD3DDev->SetRenderState(D3DRS_STENCILFUNC, D3DCMP_ALWAYS);// 常に合格
0323:     lpD3DDev->SetRenderState(D3DRS_STENCILZFAIL, D3DSTENCILOP_KEEP);// z落ちはそのまま
0324:     lpD3DDev->SetRenderState(D3DRS_STENCILPASS, D3DSTENCILOP_INCR);
0325:     lpD3DDev->SetRenderState(D3DRS_STENCILREF, 1);// ステンシル基準値は1
0326:     lpD3DDev->SetRenderState(D3DRS_STENCILMASK, 0xffffffff);
0327:     lpD3DDev->SetRenderState(D3DRS_STENCILWRITEMASK, 0xffffffff);
0328:     lpD3DDev->SetVertexShader(hShadowVertexShader);
0329:     lpD3DDev->SetRenderState(D3DRS_CULLMODE, D3DCULL_CCW);
0330:     DrawModel(lpD3DDev, 0);

次に裏面だけを描画して、合格した部分を減算します。

draw.c
0331:     
0332:     lpD3DDev->SetRenderState(D3DRS_STENCILPASS, D3DSTENCILOP_DECR);
0333:     lpD3DDev->SetRenderState(D3DRS_CULLMODE, D3DCULL_CW);
0334:     DrawModel(lpD3DDev, 0);
0335: 
0336:     lpD3DDev->SetRenderState(D3DRS_ZWRITEENABLE, TRUE);
0337:     lpD3DDev->SetRenderState(D3DRS_CULLMODE, D3DCULL_CCW);
0338:     lpD3DDev->SetRenderState(D3DRS_COLORWRITEENABLE, 0xf);

作ったステンシルバッファをチェックしながらポリゴンを全画面に一枚レンダリングします。

draw.c
0341:     // ------------------------------------------------------------------------
0342:     // 影の描画
0343:     // ------------------------------------------------------------------------
0344:     lpD3DDev->SetRenderState(D3DRS_ZENABLE, FALSE);
0345:     lpD3DDev->SetRenderState(D3DRS_STENCILFUNC, D3DCMP_LESSEQUAL);// ステンシルが0の時に描画
0346:     lpD3DDev->SetRenderState(D3DRS_STENCILFAIL, D3DSTENCILOP_KEEP);// ステンシル落ちは描画しない
0347:     lpD3DDev->SetRenderState(D3DRS_STENCILPASS, D3DSTENCILOP_KEEP);// 描画時は何もしない
0348:     lpD3DDev->SetRenderState(D3DRS_ALPHABLENDENABLE, TRUE);
0349:     lpD3DDev->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);
0350:     lpD3DDev->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);
0351:     lpD3DDev->SetTextureStageState( 0, D3DTSS_ALPHAOP, D3DTOP_MODULATE ); 
0352:     
0353:     {
0354:         typedef struct{
0355:             float   x, y, z, w;
0356:             DWORD   color;
0357:         } LVERTEX;
0358:         #define     FVF_LVERTEX         (D3DFVF_XYZRHW | D3DFVF_DIFFUSE)
0359:         lpD3DDev->SetTextureStageState(0,D3DTSS_COLORARG1,  D3DTA_DIFFUSE);
0360:         float size = 512.0f;
0361:         LVERTEX Vertex[4] = {
0362:             //  x   y  z rhw color
0363:             {   0,   0, 0, 1, D3DCOLOR_RGBA(0x0,0x0,0x0,0x80),},
0364:             {size,   0, 0, 1, D3DCOLOR_RGBA(0x0,0x0,0x0,0x80),},
0365:             {size,size, 0, 1, D3DCOLOR_RGBA(0x0,0x0,0x0,0x80),},
0366:             {   0,size, 0, 1, D3DCOLOR_RGBA(0x0,0x0,0x0,0x80),},
0367:         };
0368:         lpD3DDev->SetVertexShader( FVF_LVERTEX );
0369:         lpD3DDev->DrawPrimitiveUP( D3DPT_TRIANGLEFAN, 2, Vertex, sizeof( LVERTEX ) );
0370:     }

あとは、他のレンダリングの邪魔にならないように、レンダリングステートを適当に戻します。

draw.c
0372:     // ------------------------------------------------------------------------
0373:     // 元に戻す
0374:     // ------------------------------------------------------------------------
0375:     lpD3DDev->SetRenderState(D3DRS_ZENABLE, TRUE);
0376:     lpD3DDev->SetRenderState(D3DRS_STENCILENABLE, FALSE);
0377:     lpD3DDev->SetRenderState(D3DRS_ALPHABLENDENABLE, FALSE);
0378: }

■問題点

影ボリューム法に関しては、幾つかの制限があります。
その1つが凸多面体でなければ成らないことです。凸多面体でない時は、片方の面を描かない分、影に漏れが発生します。

さらに、カメラがポリゴンの中に入った時は、おかしな影が出来上がります。
これを防ぐために、前方平面よりも前にあるポリゴンを前方平面にせき止める方法が使われていたりします。

また、今回の方法では、ポリゴン数が少ない時に、物体の形と影の形が違ってきます。

より正しい影を描画する時は、稜線にポリゴンを仕込んでおき、仕込まれたポリゴンを光の方向に伸ばして描画する方法があります。 ただし、ポリゴンを仕込むのはめんどくさいので、ここでは省略します。

■最後に

やっていなかった影ボリュームです。
やってみると、綺麗に出ないです。モデルを変更しました。問題点が寿司詰めですね。
ステンシルバッファはピクセルシェーダで参照も書き込みもできないので不便なのですが、 あまっているリソースは使いましょう。





もどる

imagire@gmail.com