両面影マップ


~ 2枚の深度バッファを使ってみた ~






■はじめに

2chで「if氏のシャドウのサンプルはセルフシャドウがちらついて変なのが残念。」といわれてしまったので、 影がもう少し綺麗に出るように改良してみました。
今回もHW影マップを使っているので RADEON では動きません (FF の Windows 版が動かないのって、ここらへんが原因じゃないの?)。

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

final.vsh最終合成描画の頂点シェーダー。
final.psh最終合成描画のピクセルシェーダー。
vs.vsh深度描画の頂点シェーダー。
ps.psh深度描画のピクセルシェーダー。
shadow.vsh影作成の頂点シェーダー。
shadow.psh影作成のピクセルシェーダー。
draw.cppメインの描画部分。
draw.h描画の各関数の定義。
bg.cpp背景の描画。
main.h基本的な定数など。
main.cpp描画に関係しないシステム的な部分。
load.cppロード。
load.hロードのインターフェイス。
tile.bmp (床デカール)
sky.bmp (空デカール)

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

■やっていること

影が汚くなる原因を考えましょう。
その一つは、影を判定する深度と、深度バッファの値が非常に近い時に、 ピクセル単位で深度値の判定を間違える場合があることです。
これを回避するために、バイアスをかませたり、裏面の深度マップを使う方法が考えられています。
ただし、これらの方法には欠点があり、バイアスをかけた場合には、影の形がずれます。 また、裏面の深度マップを使う方法では、やはり裏面の部分の判定時にちらつきが出てしまいます。
これらの問題を回避する方法として、1番手前にあるポリゴンの深度と、 2番目に置かれたポリゴンの深度の平均を取るアイデアが提唱されています。
この方法は気に入ったのですが、「深度の平均を取る」という操作が、DirectX8世代のハードウェアでは (誤差も大きくなるので)大変なので、さらに一ひねりした方法を考えました。
それは、「1番手前のポリゴンの深度と、2番目に置かれたポリゴンの深度に関して、 レンダリングしようとするピクセルから遠いほうの深度を深度判定に用いる」という方法です。
この方法によって、現在手広く出回っているグラフィックチップでも、よりよい影の判定が可能になります。
ちなみに、手前から2番目にあるポリゴンを描画するのは、実は簡単です。
全てがポリゴンで覆われた「閉じている」モデルの場合、裏面に関する深度をレンダリングすれば、 2番目に置かれたポリゴンの深度が分かります。
例えば、下の図で、赤い部分は青色の2番目の深度を判定に使用し、 緑の部分は、赤い部分の深度値を判定に用います。

ただし、この方法にも欠点があります。
それは、車のウイングのような薄い部分を描画するときには、やはりちらつきが出てしまうことです。 これに関しては、よい解決方法が思い浮かばなかったので、放置しています。

もう一つの影が汚くなる原因は、テクスチャーの解像度です。
影バッファはフレームバッファと同じサイズだけ取っています、 ライトの方向とカメラの方向は違うので、影バッファが影を生成するための情報は十分ではありません。
そこで、影を作成するときに、テクスチャ座標を少しずらした深度マップを複数枚使って平均を取ることによって、 影マップ特有の量子化ノイズを除去します。
この結果、半影ができます。
半影を作るもっとまっとうな方法である、カメラの角度を変えながら複数回レンダリングする方法もありますが、 レンダリング回数が増えてしまうので、今回は簡易的なテクスチャをずらす方法でレンダリングします。
この方法でも、ノイズを取りきれない部分もありますので、さらに改良を加えていく楽しみが残っています。

結果的に、レンダリング工程は次のようになります。

1パス目に、表面の深度をレンダリングします。
同時に、色のα成分にも深度を格納します。この色は、1番手前のポリゴンと2番目のポリゴンとどちらが近いのかを判定するために用います。

2パス目に、裏面の深度をレンダリングします。
このときも、色成分に深度を格納します。但し、色成分に深度を保存します。
深度バッファは、1パス目と2パス目で違うバッファを使いますが、フレームバッファは同じバッファを使います。

3パス目から6パス目は、上下左右に少しずつずらしながら、影になる部分を加算合成で合成します。
このとき、3パス目は深度を書き込みますが、以降は3パス目の深度情報を使って(深度バッファを書き込まないで)レンダリングします。 4パス目以降に深度を書き込まないのは、深度の書き込みを減らして、フィルレートを稼ぐためと、 spinでちらりと見た、遅延レンダリングのアイデアを拝借して、深度情報を収めた幾何バッファ(Gバッファ) をレンダリングに使用することによって、奥のポリゴンはオクルージョンかリングで落として、 前面だけの判定で済むようにしたからです。ただし、本家の遅延レンダリングは、幾何バッファを作った後は、 一枚のフルスクリーンポリゴンで処理するので、今回行なう処理よりもはるかに効率が良くなっています。

最後の7パス目は、求まった影の部分をシーンから減算して影になる部分を求めます。
ただし、今回は頂点シェーダの鏡面反射光の成分に環境色の成分を与え、影の色と陰の色を同じにし、 なじませるようにしました。

■深度作成のパス

最初の工程は、深度バッファの作成になります。
この工程では、表示する面を表と裏を切り替えて深度バッファを更新します。
同時に、D3DRS_COLORWRITEENABLEを使って、アルファ成分と赤成分だけに書き込みをします。

draw.cpp
0509:     // ------------------------------------------------------------------------
0510:     // 影マップの作成
0511:     // ------------------------------------------------------------------------
0512:     // ビューポートの変更       x y         w                h      min_z max_z
0513:     D3DVIEWPORT8 newViewport = {0,0,SHADOWMAP_WIDTH, SHADOWMAP_HEIGHT, 0, 1};
0514:     D3DVIEWPORT8 oldViewport;
0515:     lpD3DDev->GetViewport(&oldViewport);
0516:     lpD3DDev->SetViewport(&newViewport);
0517: 
0518:     // シェーダーの設定
0519:     lpD3DDev->SetVertexShader(hVertexShader);
0520:     lpD3DDev->SetPixelShader(hPixelShader);
0521: 
0522:     // ------------------------------------------------------------------------
0523:     // 表面描画
0524:     lpD3DDev->SetRenderTarget(pTextureSurfaceCol, pTextureSurfaceZ[0]);
0525:     lpD3DDev->Clear(0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, 0x000000, 1.0f, 0L);
0526:     lpD3DDev->SetRenderState(D3DRS_COLORWRITEENABLE, D3DCOLORWRITEENABLE_ALPHA);
0527:     DrawModel(lpD3DDev, TYPE_DEPTH_CCW);
0528: 
0529:     // ------------------------------------------------------------------------
0530:     // 裏面描画
0531:     lpD3DDev->SetRenderTarget(pTextureSurfaceCol, pTextureSurfaceZ[1]);
0532:     lpD3DDev->Clear(0, NULL, D3DCLEAR_ZBUFFER, 0x000000, 1.0f, 0L);
0533:     lpD3DDev->SetRenderState(D3DRS_COLORWRITEENABLE, D3DCOLORWRITEENABLE_RED);
0534:     DrawModel(lpD3DDev,TYPE_DEPTH_CW);

TYPE_DEPTH_CW というのは、独自に設定したタイプで、DrawModelの中では、次のような場合分けが行なわれています。

draw.cpp
0396:     //
0397:     // レンダリング状態の設定
0398:     // 
0399:     if(TYPE_DEPTH_CW == type){
0400:         lpD3DDev->SetRenderState(D3DRS_CULLMODE, D3DCULL_CW);
0401:     }else{
0402:         lpD3DDev->SetRenderState(D3DRS_CULLMODE, D3DCULL_CCW);
0403:     }

モデルを表示するシェーダは次のとおりです。
座標変換は、普通に座標変換して、深度をいろ情報として出力します。

vs.vsh
0001: ; c4-7   -- world + ライトビュー + ライト透視変換行列
0002: ; c12    -- {0.0, 0.5, 1.0, 2.0}
0003: ; c16    -- 深度情報を
0004: ;
0005: ; v0    頂点の座標値
0006: ; v3    法線ベクトル (w成分は1.0f)
0007: ; v7    テクスチャ座標
0008: 
0009: vs.1.0
0010: 
0011: ;座標変換
0012: m4x4 oPos,  v0,   c4
0013: 
0014: ; 深度を色にする
0015: dp4 r0, v0, c6
0016: mad oD0, r0, c16.x, c16.y

ピクセルシェーダでは、単純に入力された色をフレームバッファに出力します。

0001: ; ps.psh
0002: 
0003: ps.1.0
0004: 
0005: mov r0, v0

■影作成のパス

次に、深度バッファの値と、描画しようとするポリゴンの深度を比較して、どの部分が影になるか算出します。
方法としては、レンダリング用の1つのターゲットを用意して、そのテクスチャにテスト結果を加算合成します。
それぞれのテストでは、深度バッファの値を少しずらします。
また、メモリ帯域を稼ぐために、最初の工程で深度を作成し、 後の工程では、その深度テストに合格したピクセルだけをピクセルシェーダで処理します (ここが遅延シェーディング的な考え方をしている部分です)。
ただ、同じ頂点データを使っても、何故か深度テストに失格するときがあったので、 D3DRS_ZBIAS を使って、同じ深度でも仮想的に手前に来るように処理しました。

draw.cpp
0536:     // ------------------------------------------------------------------------
0537:     // 影のマップを作成
0538:     // ------------------------------------------------------------------------
0539:     lpD3DDev->SetRenderTarget(pTextureSurfaceShadow, pTextureSurfaceShadowZ);
0540:     lpD3DDev->Clear(0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, 0x000000, 1.0f, 0L);
0541:     lpD3DDev->SetViewport(&oldViewport);
0542:     lpD3DDev->SetRenderState(D3DRS_COLORWRITEENABLE, D3DCOLORWRITEENABLE_RED
0543:                                                     |D3DCOLORWRITEENABLE_GREEN
0544:                                                     |D3DCOLORWRITEENABLE_BLUE);
0545:     lpD3DDev->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_ONE);
0546:     lpD3DDev->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_ONE);
0547:     lpD3DDev->SetTextureStageState( 0, D3DTSS_ALPHAOP, D3DTOP_MODULATE);
0548:     lpD3DDev->SetVertexShader(hShadowVertexShader); 
0549:     lpD3DDev->SetPixelShader(hShadowPixelShader);   
0550:     lpD3DDev->SetTexture( 0, pTextureCol );                 // 深度テクスチャー
0551:     lpD3DDev->SetTexture( 1, pTextureZ[0] );                // 影マップ
0552:     lpD3DDev->SetTexture( 2, pTextureZ[1] );                // 影マップ
0553: 
0554:     // ------------------------------------------------------------------------
0555:     // 影描画
0556:     DrawModel(lpD3DDev, TYPE_SHADOW_0);
0557: 
0558:     // 以降は、深度バッファを見ながら加算レンダリング(書き込まない)
0559:     lpD3DDev->SetRenderState( D3DRS_ZWRITEENABLE, FALSE);
0560:     lpD3DDev->SetRenderState( D3DRS_ALPHABLENDENABLE, TRUE);
0561:     lpD3DDev->SetRenderState( D3DRS_ZBIAS, 16);
0562: 
0563:     DrawModel(lpD3DDev, TYPE_SHADOW_1);
0564:     DrawModel(lpD3DDev, TYPE_SHADOW_2);
0565:     DrawModel(lpD3DDev, TYPE_SHADOW_3);
0566: 
0567:     lpD3DDev->SetRenderState( D3DRS_ZBIAS, 0);
0568:     lpD3DDev->SetRenderState( D3DRS_ALPHABLENDENABLE, FALSE );
0569:     lpD3DDev->SetRenderState( D3DRS_ZWRITEENABLE, TRUE );

実際に深度バッファの座標をずらすのは、射影空間からテクスチャ座標系への変換で行ないます。
それぞれの工程に応じて、16箱サンプリングと同様のずらし方で斜めにずらします。

draw.cpp
0362:     // 射影空間から、テクスチャーの空間に変換する
0363:     float fOffsetX, fOffsetY;
0364:     switch(type){
0365:     case TYPE_SHADOW_0:
0366:         fOffsetX = 0.5f - (1.0f / (float)SHADOWMAP_WIDTH);
0367:         fOffsetY = 0.5f - (1.0f / (float)SHADOWMAP_HEIGHT);
0368:         break;
0369:     case TYPE_SHADOW_1:
0370:         fOffsetX = 0.5f - (1.0f / (float)SHADOWMAP_WIDTH);
0371:         fOffsetY = 0.5f + (1.0f / (float)SHADOWMAP_HEIGHT);
0372:         break;
0373:     case TYPE_SHADOW_2:
0374:         fOffsetX = 0.5f + (1.0f / (float)SHADOWMAP_WIDTH);
0375:         fOffsetY = 0.5f - (1.0f / (float)SHADOWMAP_HEIGHT);
0376:         break;
0377:     case TYPE_SHADOW_3:
0378:         fOffsetX = 0.5f + (1.0f / (float)SHADOWMAP_WIDTH);
0379:         fOffsetY = 0.5f + (1.0f / (float)SHADOWMAP_HEIGHT);
0380:         break;
0381:     default:
0382:         fOffsetX = 0.5f + (0.5f / (float)SHADOWMAP_WIDTH);
0383:         fOffsetY = 0.5f + (0.5f / (float)SHADOWMAP_HEIGHT);
0384:         break;
0385:     }
0386:     unsigned int range = 0xFFFFFFFF >> (32 - 24);
0387:     float fBias    = +0.000001f * (float)range;// カリングを反対にするので、通常とは逆のオフセット
0388:     D3DXMATRIX mScaleBias( 0.5f,     0.0f,     0.0f,         0.0f,
0389:                            0.0f,    -0.5f,     0.0f,         0.0f,
0390:                            0.0f,     0.0f,     (float)range, 0.0f,
0391:                            fOffsetX, fOffsetY, fBias,        1.0f );
0392: 
0393:     mTex   = mVP  * mScaleBias;
0394:     mLight = mVPL * mScaleBias;

頂点シェーダでは、最初の工程と同様に座標や色を作成し、テクスチャ座標を追加の情報として座標変換します。

0001: ; Shadow.vsh
0002: ; c0-3   -- world + ビュー + 射影変換行列
0003: ; c4-7   -- world + ライトビュー + 射影変換行列
0004: ; c8-11  -- world + ライトビュー + 射影 + テクスチャ座標変換行列
0005: ; c15    -- ライトの色(メッシュの色)
0006: ;
0007: ; v0    頂点の座標値
0008: ; v3    法線ベクトル (w成分は1.0f)
0009: ; v7    テクスチャ座標
0010: 
0011: vs.1.1
0012: 
0013: ;座標変換
0014: m4x4 oPos,  v0,   c0
0015: 
0016: ; 深度を色にする
0017: dp4 r0, v0, c6
0018: mad oD0, r0, c16.x, c16.y
0019: 
0020: m4x4 oT0,   v0,   c8    ; 深度テクスチャー
0021: m4x4 oT1,   v0,   c8    ; 影マップ
0022: m4x4 oT2,   v0,   c8    ; 影マップ

ピクセルシェーダでは、現在のポリゴンの深度値v0と、色情報に関する深度マップの値を比較し、 表と裏で遠いほうがどちらになるかを判定し、判定した深度バッファによる影の判定の結果を採用します。

0001: ; shadow.psh
0002: ;
0003: ps.1.1
0004: 
0005: def c0, 1.0f, 0.0f, 0.0f, 0.0f
0006: def c1, 0.25f, 0.25f, 0.25f, 0.0f
0007: 
0008: tex t0   ; 深度
0009: tex t1   ; 影マップ
0010: tex t2   ; 影マップ
0011: 
0012: add t0, t0, -v0
0013: mul t0, t0,  t0
0014: add r0, t0, -t0_bias.a
0015: dp3 r0, r0, c0
0016: cnd r0, r0.a, t2, t1
0017: mul r0, r0, c1

さらに詳しく言えば、最初の

0012: add t0, t0, -v0

の命令で、深度マップの色による深度と、色レジスタに入力された現在の深度の差をとります。
次に、

0013: mul t0, t0,  t0

命令で、各係数に関する差の2乗を作ります。
これで、符号によらない判定ができるようになります。 次の命令

0014: add r0, t0, -t0_bias.a

で、赤色成分に、(表の深度ー現在の深度)^2-(裏の深度-現在の深度)^2+0.5 が入ります。
0.5は、後のcnd命令で比較のために使用する値です。
cnd命令で使用するのは、α成分ですから、その後の命令で、α成分に赤色成分を代入します。

0015: dp3 r0, r0, c0

見て分かるように、内積計算を使って、α成分に代入しました。この内積は、c0.x=1で、残りは0にしているので 赤色の成分だけが抽出できます。
その後に、いよいよ比較命令を使って深度バッファを選びます。

0016: cnd r0, r0.a, t2, t1

r0.a が0.5よりも大きいときは、表までの距離のほうが裏までの距離よりも遠くなるので、 裏面の深度バッファによる判定を採用します。小さければ、その逆です。
最後に、得られた影の情報をスケーリングして、影の濃さが飽和しないように調整します。

0017: mul r0, r0, c1

■最終的な合成のパス

最後の工程では、得られた影の部分を乗算して、最終的なレンダリング結果を作ります。 呼び出しの部分では、先ほど作成したレンダリング結果をテクスチャにして普通にレンダリングします。

draw.cpp
0571:     // ------------------------------------------------------------------------
0572:     // シーンの作成
0573:     // ------------------------------------------------------------------------
0574:     // 描画を戻す
0575:     lpD3DDev->SetRenderTarget(pBackbuffer, lpZbuffer );
0576:     lpD3DDev->Clear(0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, 0x000000, 1.0f, 0L);
0577: 
0578:     // シェーダーの設定
0579:     lpD3DDev->SetVertexShader(hFinalVertexShader);  
0580:     lpD3DDev->SetPixelShader(hFinalPixelShader);    
0581: 
0582:     lpD3DDev->SetTexture( 1, pTextureShadow );              // 影マップ
0583:     DrawModel(lpD3DDev, TYPE_FINAL);
0584:     
0585:     // 他でレンダリングしても問題が出ない設定にする
0586:     lpD3DDev->SetTexture( 0, NULL );
0587:     lpD3DDev->SetTexture( 1, NULL );
0588:     lpD3DDev->SetTexture( 2, NULL );
0589:     lpD3DDev->SetTexture( 3, NULL );
0590:     lpD3DDev->SetPixelShader(0);

頂点シェーダでは、普通にレンダリングします。
色の成分0に平行光源の拡散光を入れ、色の成分1に環境色の強さを入れます。
テクスチャ座標に関しては、普通のテクスチャと、影の部分を記したマップの2つを設定します。

0001: ; final.vsh
0002: ; c0-3   -- world + ビュー + 射影変換行列
0004: ; c13    -- ライトのベクトル
0005: ; c14    -- ライトの色(拡散光)
0006: ; c15    -- ライトの色(環境光)
****: ; c20-23 -- world + ビュー + 射影 + テクスチャ変換行列
0007: ;
0008: ; v0    頂点の座標値
0009: ; v3    法線ベクトル
0010: ; v7    テクスチャ座標
0011: 
0012: vs.1.1
0013: 
0014: ;座標変換
0015: m4x4 oPos,  v0,   c0
0016: 
0017: ; (L・N)*c14 (拡散光)
0018: dp3 r0.w,   v3,   c13
0019: mul oD0,    r0.w, c14
0020: mov oD1,    c15         ; 環境光
0021: 
0022: mov oT0,    v7          ; デカールテクスチャー
0023: m4x4 oT1,   v0,   c20   ; 影マップ

ピクセルシェーダでは、影になっている部分に関して、環境色の強さを引いて、値を正の値に修正した後に、 もう一度環境色の強さを足すことによって、一番暗い部分の濃さを環境色の濃さに抑えます。
後は、モデルの色を求めて影の濃さを乗算すると、最終的な絵が出来上がります。

0001: ; final.psh
0002: ;
0003: ps.1.1
0004: 
0005: def c0, 0.9f, 0.9f, 0.9f, 0.0f  ; 影の濃さ
0006: 
0007: tex t0   ; デカール
0008: tex t1   ; 影テクスチャ
0009: 
0010: mad_sat r0, t1, c0, -v1
0011: add r0, r0, v1          ; 陰 = max(c0*影の濃さ, 環境色)   v1:環境色
0012: 
0013: add r1, t0,  v0     ; モデルの色は頂点+デカール
0014: mul r0, r0, r1      ; 陰をつける

■最後に

まだ、車の後ろの部分が陰になるときに縞が見えてしまいますね。
これ以上は、オブジェクトごとに影マップを作らないと厳しいなぁ。

今回の方法は、7パス使うので、現在のハードウェアでは現実的でない方法です。
将来的なハードウェアになって、マルチレンダリングターゲットの方法が使えるようになれば、 深度を作成する部分や影を抽出する部分が1パスになるので、もう少し現実的になるでしょうか。
遠い面を判定に使うのは我ながら面白いアイデアだと思うので、他の応用例を考えてみたいと思います。





もどる

imagire@gmail.com