~カゲスターっていったら歳がばれますか?~




■はじめに

ITX2002Summerに遊びに行って、 その後で(ITXのほうが前座かな?)、黒田さん主催で飲んだのですが、 その2次会で、黒田さんやBeeさんはなげさんと、 Beeさんの卒研話やもろもろの話題で盛り上がりました。 そのときにシャドウバッファの話題が出たので、もうすこし出し惜しみしとく予定でしたが、ここで出しちゃいます。

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

shadow.vsh頂点シェーダー。
shadow.pshピクセルシェーダー。
vs.vsh頂点シェーダー。深度テクスチャ作成用。
ps.pshピクセルシェーダー。深度テクスチャ作成用。
draw.cppメインの描画部分。
draw.h描画の各関数の定義。ひそかにカメラのソースが入ってる。
bg.cpp背景表示用。
main.h基本的な定数など。今回も出番無し。
main.cpp描画に関係しないシステム的な部分。変更が無いので、出番無し。
load.cppロード。頂点ブレンドのブレンドファクターの計算もする。
load.hロードのインターフェイス。
tile.bmp (床デカール)
sky.bmp (空デカール)

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

■基本方針

シャドーバッファは、まず被写界深度のときのように(光源方向から見た)深度をテクスチャーとして保存します。

2パス目で、作成した深度テクスチャー(バッファ)の、深度(色)と、描画する深度を比較して、 描画したい深度がテクスチャーの深度よりも奥なら、その部分は影になっているはずなので、 暗く描画します。
深度を比較するときは、深度テクスチャーを作ったときのスクリーン座標にテクスチャー座標をあわせます。
スクリーン座標をテクスチャー座標にして、そのままテクスチャーを張った場合、

が、得られます。この値と現在の頂点のライト方向からの深度

の差分をとれば、影となる部分が判明します。

影となる部分を、元になる絵

から、減算すれば、影が求まります。

ここで、見える絵はマッハバンドでまくりですが、これはPNG8で画像を圧縮したためで、実際はもう少し解像度高いです。

■全体の流れ

さて、描画部分を説明しましょう。
最初は、レンダリングするターゲットをテクスチャーにして描画します。
次に、レンダリングターゲットを通常に戻して、シェーダーとビュー行列を変えて描画します。 また、テクスチャーとして、レンダリングしたテクスチャーも指定します。
ただし、2度目の描画のときは、一度目のライト方向からのビュー座標をシェーダーのパラメータとして、引き渡します。

draw.cpp
0375: VOID Render(LPDIRECT3DDEVICE8 lpD3DDev)
0376: {
0377:     DWORD i, j, k;
0378:     D3DXMATRIX mWorld, mView, mLightView, m;
0391: 
0392:     // z値を0.0fから1.0fに補正する定数
0393:     lpD3DDev->SetVertexShaderConstant(15, &D3DXVECTOR4(3.0f/(z_max-z_min), -0.45f*z_max/(z_max-z_min), 0.0f, 0.0f), 1);
0379:     
0380:     LPDIRECT3DSURFACE8 lpZbuffer = NULL;
0381:     lpD3DDev->GetDepthStencilSurface( &lpZbuffer );
0382:     lpD3DDev->SetRenderTarget(pTextureSurface, lpZbuffer);
0383:     lpD3DDev->Clear(0,NULL,D3DCLEAR_TARGET, D3DCOLOR_XRGB(0,0,0),1.0f,0);
0384: 
0396:     lpD3DDev->SetVertexShader(hVertexShader);
0397:     lpD3DDev->SetPixelShader(hPixelShader);
0398: 
0389:     // 影テクスチャー生成
0385:     // ライト方向からのビュー行列
0386:     D3DXVECTOR3 l_eye    = 30.0f*lightDir;
0387:     D3DXVECTOR3 l_lookAt = D3DXVECTOR3(0.0f,  0.0f,  0.0f);
0388:     D3DXVECTOR3 l_up     = D3DXVECTOR3(0.0f,  1.0f,  0.0f);
0390:     D3DXMatrixLookAtLH(&mLightView, &l_eye, &l_lookAt, &l_up);
0394:     
0399:     DrawModel(lpD3DDev, mLightView, mLightView);
0400: 
0401:     // 
0402:     // 元に戻して描画
0403:     // 
0404:     lpD3DDev->SetRenderTarget(pBackbuffer, lpZbuffer );                 // 描画をバックバッファに戻す
0405:     lpD3DDev->Clear(0,NULL,D3DCLEAR_ZBUFFER, 0,1.0f,0);
0408: 
0406:     lpD3DDev->SetVertexShader(hShadowVertexShader); 
0407:     lpD3DDev->SetPixelShader(hShadowPixelShader);   
0409:     lpD3DDev->SetTexture( 1, pTexture );                    // 元テクスチャー
0410:     
0415:     // 通常表示
0411:     // ビュー行列
0412:     D3DXVECTOR3 eye    = D3DXVECTOR3(0.0f,1.4f*MeshRadius,2.5f*MeshRadius);
0413:     D3DXVECTOR3 lookAt = D3DXVECTOR3(0.0f,  0.0f,  0.0f);
0414:     D3DXVECTOR3 up     = D3DXVECTOR3(0.0f,  1.0f,  0.0f);
0416:     D3DXMatrixLookAtLH(&mView, &eye, &lookAt, &up);
0417:     DrawModel(lpD3DDev, mView, mLightView);
0427: }

■一度目のレンダリング

深度テクスチャーの作成部分ですが、頂点シェーダ―は被写界深度のときと同じように、頂点色のアルファ部分に深度を入れます。

vs.vsh
0001: ; c0-3   -- world + ビュー + 透視変換行列
0003: ; c13    -- ライトのベクトル (w成分は環境光の強さ)
0004: ; c14    -- ライトの色(メッシュの色)
0005: ; c15    -- 深度変換パラメータ
0006: ;
0007: ; v0    頂点の座標値
0008: ; v3    法線ベクトル (w成分は1.0f)
0009: ; v7    テクスチャ座標
0010: 
0011: vs.1.0
0012: 
0013: ;座標変換
0014: dp4 oPos.x, v0,   c0
0015: dp4 oPos.y, v0,   c1
0016: dp4 oPos.z, v0,   c2
0017: dp4 oPos.w, v0,   c3
0018: 
0019: ; ((l,n) + l.w)*c14 (平行光源のライティング)
0020: dp4 r0.w,   v3,   c13
0021: mul oD0,    r0.w, c14
0022: 
0023: dp4 r0,     v0,   c2
0024: mad oD0.w,  r0,   c15.x, c15.y
0026: 
0027: ; テクスチャーを張る
0028: mov oT0,    v7

ピクセルシェーダーは色の部分にテクスチャーを合成していますが、実際にはアルファ部分だけ頂点色を出力すればOKです。

ps.psh
0001: ; ps.psh
0002: ps.1.0
0003: 
0004: ; テクスチャーの色を引っ張ってくる
0005: tex t0
0006: 
0007: add r0,   v0, t0
0008: mov r0.a, v0

■2度目のレンダリング

肝心の2回目のレンダリングですが、特徴的なのは、2段階目のテクスチャー座標 oT1 に光源方向からの座法変換した結果を代入していることです。
そのときに同次座標のw=1平面への射影をするのを忘れないようにしましょう。
また、テクスチャーの中心座標が(0.5, 0.5)になるように調整をする必要もあります。
あと、比較用に、1パス目と同じように、深度値をアルファ成分に入れています。

0001: ; Shadow.vsh
0002: ; c0-3   -- world + ビュー + 透視変換行列
0003: ; c4-7   -- world + ライトビュー + 透視変換行列
0004: ; c13    -- ライトのベクトル (w成分は環境光の強さ)
0005: ; c14    -- ライトの色(メッシュの色)
0006: ; c15    -- 深度変換パラメータ
0007: ;
0008: ; v0    頂点の座標値
0009: ; v3    法線ベクトル (w成分は1.0f)
0010: ; v7    テクスチャ座標
0011: 
0012: vs.1.0
0013: 
0014: ;座標変換
0015: dp4 oPos.x, v0,   c0
0016: dp4 oPos.y, v0,   c1
0017: dp4 oPos.z, v0,   c2
0018: dp4 oPos.w, v0,   c3
0019: 
0020: ; ((l,n) + l.w)*c14 (平行光源のライティング)
0021: dp4 r0.w,   v3,   c13
0022: mul oD0,    r0.w, c14
0023: 
0024: ; デカールテクスチャー
0025: mov oT0,    v7
0026: 
0027: ; シャドウバッファ
0028: mov oT1, c12.z
0029: dp4 r0.x,   v0,   c4
0030: dp4 r0.y,  -v0,   c5
0031: dp4 r0.z,   v0,   c6
0032: dp4 r0.w,   v0,   c7
0033: rcp r0.w, r0.w
0034: mul r0.xy, r0, r0.w
0035: mad oT1.xy, r0, c12.y, c12.y
0036: 
0037: ; 深度を色のw成分に入れる
0038: mad oD0.w,  r0.z,   c15.x, c15.y

ピクセルシェーダーは、cnd 命令を使って、元の色から減算する量を決定します。

0001: ; shadow.psh
0002: ;
0003: 
0004: ps.1.0
0005: 
0006: def c0, 0.0f, 0.0f, 0.0f, 0.1f  ; z-オフセット
0007: def c1, 0.3f, 0.3f, 0.3f, 0.0f  ; 影の濃さ
0008: 
0009: ; テクスチャーの色を引っ張ってくる
0010: tex t0                              ; デカールテクスチャー
0011: tex t1                              ; 深度テクスチャー
0012: 
0013: add r1, t0, v0                      ; 色=頂点色+デカール
0014: 
0015: ; r0 = (t1.a < v0.a-0.1f) ? c0 : c0 
0016: add r0, v0,  -c0
0017: add r0, r0,  -t1_bias
0018: cnd r0, r0.a, c1, c0
0019: add r0, r1, -r0
0020: 
0021: ;mov r0, r1                         ; 影の計算をしない場合
0022: ;mov r0, t1.a                       ; ライト方向から見た影テクスチャー表示
0023: ;mov r0, v0.a                       ; 描画したいポリゴンの影方向からの深度表示

大小比較するときに、0.1(=c0.w)だけ引きました。 この値を引かないと、バッファの値と深度が同じ時に、座標が完全には一致しないための深度判定のミスが起こり、縞模様が発生してしまいます。

この現象は、シャドウマップで影を重ねたり、ライトマップで光を重ねる時に顕著に現れるので、 ゲームなどを良く見ると、影が少し浮く現象が見られます(シャドウマップの時は、1パスを使ったり、完全に座標を一致させて描画するなどの解決する方法がありますが、今回の方法に関しては、それは使えません)。

■問題点

この方法に関して、よく知られた欠点があります。
それは、シャドーバッファの精度が低いということです。
また、テクスチャーの面積が広くないと汚くなることが知られています。 その影響はオフセットの大きさに現れますし(原理的には精度が無限なら深度が非常に近い部分での縞模様は出ない)。
影の縁の部分が点々として汚くなることでわかります。
さらに深刻な現象も現れていて、画面の右側(光の方向の反対側)の一部の影が消えてしまいます。

これは、奥の部分で色が飽和してしまい、遠近の判断が正常に処理されていないためです。
現実に使うためには、「手前」と、「奥」の範囲を慎重に決めなければなりません。

また、今回の方法では、自分に落ちる影(自己影:セルフシャドウ)も起きているのですが、 オフセットをかましている分、途中から影が発生するのと同時に、境界部分が汚くなっています。

これも、シャドウバッファの精度を高くするとともに、テクスチャーの面積を広くする必要が出てきます。
D3DFMT_A32ってありませんかねぇ。もちろん比較演算も使えるようにして。

あと、原理的な問題として、ふちの形がくっきりして、半影が表現できないです。

■最後に

ほんとは、シャドーボリュームをやってから、シャドーバッファをしようと考えていたのですけど、 話にでたので、こちらをやりました。結構綺麗に出ていますね。

実は、シャドーボリュームがうまくいかなかったので、ほってあるんですよね。
個人的には「ステンシルバッファはなくなる。」って、勝手に思ってるんですけど、 アルファでやろうとしたら、塗りつぶしがうまくいかなかったんだなこりゃ。
あぁ、でもシャドーボリュームもやらなきゃ。

ここでの「ステンシルバッファはなくなる。」というのは、勝手な妄想で根拠のあることではありません。 ただ、「ステンシルバッファ」、「Zバッファ」、「wバッファ」など、似たようなもので種類の違うものが多いので、 最終的には汎用的な「バッファ」に統一されて、プログラマブルに制御するようになるのではないかと想像しています。




もどる

imagire@gmail.com