ソフトシャドウ


~やわらかい影は好きですか?~




■はじめに

掲示板でタコスケさんから「ソフトシャドウできんの?」って挑戦状を叩き付けられたので考えてみました。

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

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

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

■方法

今回も4パス使ってます。
それぞれのパスでは次の手順を踏んでます。

パス1深度をレンダリング
パス2深度のエッジを作る
パス3エッジをぼかす
パス4深度と1パス目のテクスチャーの深度を比較し影の部分を算出し、ぼかしたエッジを掛けて影をぼかす

■パス1:深度をレンダリング

最初に深度をテクスチャーにレンダリングします。
今回も、透視影マップのゆがんだ画面です。

幾何学的には位置ベクトルに行列を掛けてライト方向からの透視変換した後の座標を計算します。
色の成分は、深度

    Zfar(Z   -Znear)      Zfar       1  Zfar Znear
z = ― ――――― = ――――― - ― ――――
    Z   (Zfar-Znear)  (Zfar-Znear)    Z (Zfar-Znear)

を使います。

vs.vsh
0002: ; c4-7   -- world + ビュー + 透視変換行列
0003: ; c15    -- 深度調整
0005: ; v0    頂点の座標値
0006: ; v7    テクスチャ座標
0008: vs.1.0
0009: 
0013: ;座標変換
0014: m4x4 oPos,  v0,     c4
0015: 
0016: ; 深度を色に入れる
0017: dp4 r0,     v0,     c7
0018: rcp r0,     r0
0019: mad oD0,    r0,   c15.x, c15.y

ピクセルシェーダーとしては、その色を与えれば十分です。

ps.psh
0004: ps.1.0
0008: mov r0, v0

c言語の部分は、テクスチャーにレンダリングする設定をしてから、画面をクリアしてからモデルを描画します。

draw.cpp
0512:     // -----------------------------------------------------
0513:     // 深度と色の作成
0514:     // -----------------------------------------------------
0515:     // テクスチャーに描画
0516:     lpD3DDev->SetRenderTarget(pTextureSurface, lpZbuffer);
0517:     lpD3DDev->Clear(0,NULL, D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER, D3DCOLOR_XRGB(0,0,0),1.0f,0);
0518:     lpD3DDev->SetTextureStageState(0,D3DTSS_COLOROP,    D3DTOP_MODULATE);
0519:     lpD3DDev->SetTextureStageState(0,D3DTSS_COLORARG1,  D3DTA_TEXTURE);
0520:     lpD3DDev->SetTextureStageState(0,D3DTSS_COLORARG2,  D3DTA_DIFFUSE);
0521:     lpD3DDev->SetTextureStageState(1,D3DTSS_COLOROP,    D3DTOP_DISABLE);
0522:     lpD3DDev->SetVertexShader(hVertexShader);
0523:     lpD3DDev->SetPixelShader(hPixelShader);
0524:     bShadowMap = TRUE;
0525:     bBg = FALSE;
0526:     DrawModel(lpD3DDev, bShadowMap, bBg);

■パス2:深度のエッジを作る

次は、深度に関するエッジを取ります。
具体的には、ピクセルの対角的な成分の差をとって、和をとります。

頂点シェーダーは隣り合うピクセルの中心座標を取るようにテクスチャー座標をずらします。

edge.vsh
0002: ; c20-23   -- テクスチャーをずらすオフセット
0003: ;             (0,0), (1,0), (0,1), (1,1)
0004: ; v0    頂点の座標値
0005: ; v7    テクスチャ座標
0006: 
0007: vs.1.0
0008: 
0009: ; 座標を代入
0010: mov oPos, v0
0011: 
0012: ; それぞれテクスチャーの位置をずらして出力
0013: add oT0, v7, c20
0014: add oT1, v7, c21
0015: add oT2, v7, c22
0016: add oT3, v7, c23

ピクセルシェーダーでは、差の値を大きくしながらピクセル差をとります。

edge.psh
0004: ps.1.0
0005: 
0006: tex t0      ; 0:1 0  1:0 1  2:0 0  3:0 0
0007: tex t1      ;   0 0    0 0    1 0    0 1
0008: tex t2
0009: tex t3
0010: 
0011: mov         r0, t3.a
0012: mov         r1, t2.a
0013: add_x4      r0, r0, -t0.a   ; r0 =  4(t3.a-t0.a)
0014: add_x4      r1, r1, -t1.a   ; r1 =  4(t2.a-t1.a)
0015: mul_x4      r0, r0, r0      ; r0 = 64(t3.a-t0.a)^2
0016: mul_x4      r1, r1, r1      ; r1 = 64(t2.a-t1.a)^2
0017: add_x4_sat  r0, 1-r0.a,-r1.a; r0 = 4(1-64((t3.a-t0.a)^2+(t2.a-t1.a)^2))

レンダリング対象は、先ほどとは違うテクスチャーにレンダリングします。
使うテクスチャーは深度を描き込んだテクスチャーです。

draw.cpp
0527:     // -----------------------------------------------------
0528:     // 深度エッジの作成
0529:     // -----------------------------------------------------
0530:     lpD3DDev->SetRenderTarget(pTextureSurfaceEdge, NULL );
0531:     for (i = 0; i < 4; i++) {
0532:         lpD3DDev->SetTextureStageState(i, D3DTSS_ADDRESSU,  D3DTADDRESS_CLAMP);
0533:         lpD3DDev->SetTextureStageState(i, D3DTSS_ADDRESSV,  D3DTADDRESS_CLAMP);
0534:         lpD3DDev->SetTextureStageState(i, D3DTSS_COLOROP,   D3DTOP_SELECTARG1);
0535:         lpD3DDev->SetTextureStageState(i, D3DTSS_COLORARG1, D3DTA_TEXTURE);
0536:         lpD3DDev->SetTexture( i, pTexture );                    // テクスチャー
0537:     }
0538:     lpD3DDev->SetVertexShader(hVertexShaderEdge);
0539:     lpD3DDev->SetPixelShader(hPixelShaderEdge);
0540:     lpD3DDev->SetStreamSource( 0, pFinalVB, sizeof(D3D_FINAL_VERTEX) );
0541:     lpD3DDev->SetIndices(pFinalIB,0);
0542:     lpD3DDev->DrawIndexedPrimitive( D3DPT_TRIANGLELIST, 0, 4, 0, 2 );

■パス3:エッジをぼかす

次に、作成した深度エッジをぼかします。

ぼかす方法は16サンプルボックスフィルターです。
エッジを取ったときよりも一回り大きいテクセル座標でテクスチャー座標をずらします。

bokashi.vsh
0002: ; c30-33   -- テクスチャーをずらすオフセット
0003: ;             (0,0), (2,0), (0,2), (2,2)
0004: ; v0    頂点の座標値
0005: ; v7    テクスチャ座標
0006: 
0007: vs.1.0
0008: 
0009: ; 座標を代入
0010: mov oPos, v0
0011: 
0012: ; それぞれテクスチャーの位置をずらして出力
0013: add oT0, v7, c30
0014: add oT1, v7, c31
0015: add oT2, v7, c32
0016: add oT3, v7, c33

ピクセルシェーダーでは、それぞれのテクセルの平均を取ります。

bokashi.psh
0004: ps.1.0
0005: 
0006: def c0, 0.5f, 0.5f, 0.5f, 0.5f
0007: 
0008: tex t0      ; 0:1 0  1:0 1  2:0 0  3:0 0
0009: tex t1      ;   0 0    0 0    1 0    0 1
0010: tex t2
0011: tex t3
0012: 
0013: mul r0, t0, c0
0014: mul r1, t1, c0
0015: mad r0, t2, c0, r0
0016: mad r1, t3, c0, r1
0017: lrp r0, c0, r0_bx2, r1_bx2

今度もまた、別のテクスチャーにレンダリングします。
ビデオメモリを節約する方法として、深度のテクスチャーのRGBもしくはα成分に埋め込む手がありますがそれはお好みでどうぞ。

draw.cpp
0543:     // -----------------------------------------------------
0544:     // 深度エッジをぼかす
0545:     // -----------------------------------------------------
0546:     lpD3DDev->SetRenderTarget(pTextureSurfaceBokashi, NULL );
0547:     for (i = 0; i < 4; i++) {
0548:         lpD3DDev->SetTexture( i, pTextureEdge );                    // テクスチャー
0549:     }
0550:     lpD3DDev->SetVertexShader(hVertexShaderBokashi);
0551:     lpD3DDev->SetPixelShader(hPixelShaderBokashi);
0552:     lpD3DDev->SetStreamSource( 0, pFinalVB, sizeof(D3D_FINAL_VERTEX) );
0553:     lpD3DDev->SetIndices(pFinalIB,0);
0554:     lpD3DDev->DrawIndexedPrimitive( D3DPT_TRIANGLELIST, 0, 4, 0, 2 );

■パス4:影を作る

最後が影を作る部分です。
色の成分にやはり深度を入れます。
また、oT1, oT2 の2つのテクスチャー座標にライト方向からのスクリーン座標を入れます。

final.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: mov oT0,    v7
0016: 
0017: ;座標変換
0018: m4x4 oPos, v0, c0
0019: 
0020: ; ((l,n) + l.w)*c14 (平行光源のライティング)
0021: dp4 r0.w,   v3,   c13
0022: mul oD0.xyz,r0.w, c14
0023: 
0024: ; 深度を色のw成分に入れる
0025: dp4 r0.w,   v0,     c7
0026: rcp r0.w,   r0.w
0027: mad oD0.w,  r0.w,   c15.x, c15.y
0028: 
0029: ; シャドウバッファ
0030: dp4 r0.x,   v0,   c4
0031: dp4 r0.y,  -v0,   c5
0032: mul r0.xy, r0, r0.w
0033: mad oT1.xy, r0, c12.y, c12.y
0034: mad oT2.xy, r0, c12.y, c12.y

ピクセルシェーダーでは、ぼかしたテクスチャ―を定数に掛けて、影の部分の色を調整します。
ちょうどエッジになっている部分がぼかしの中心で、黒くなっているので、 その部分は減算されず明るいままで、中に行くに従って減算する量は大きくなります。

final.psh
0004: ps.1.0
0005: 
0006: def c0, 0.0f, 0.0f, 0.0f, 0.000000000001f   ; z-オフセット
0007: def c1, 0.3f, 0.3f, 0.3f, 0.0f      ; 影の濃さ
0008: 
0009: ; テクスチャーの色を引っ張ってくる
0010: tex t0                              ; デカールテクスチャー
0011: tex t1                              ; 深度テクスチャー
0012: tex t2                              ; ぼかした深度エッジ
0013: 
0014: add r1, t0, v0                      ; 色=頂点色+デカール
0015: mul r0, c1, t2                      ; r0 = c1*t2 ; 影として減算する量
0016: 
0017: ; r0 = (t1.a < v0.a-0.1f) ? r1 : r1-t2*c1
0018: add r0.a, v0,  -c0
0019: add r0.a, r0,  -t1_bias
0020: cnd r0, r0.a, r0, c0
0021: add r0, r1, -r0

今度は、レンダリングする場所をバックバッファに移して、レンダリングです。
ぼかしのテクスチャーと深度比較のために深度のテクスチャーをセットします。

draw.cpp
0555:     // -----------------------------------------------------
0556:     // 影の作成
0557:     // -----------------------------------------------------
0558:     // 描画をバックバッファに戻す
0559:     lpD3DDev->SetRenderTarget(pBackbuffer, lpZbuffer );
0560:     lpD3DDev->Clear(0,NULL,D3DCLEAR_ZBUFFER, 0,1.0f,0);
0561:     lpD3DDev->SetVertexShader(hFinalVertexShader);
0562:     lpD3DDev->SetPixelShader(hFinalPixelShader);
0563:     lpD3DDev->SetTexture( 1, pTexture );                    // 元テクスチャー   
0564:     lpD3DDev->SetTexture( 2, pTextureBokashi );             // ぼかしたエッジテクスチャー
0565:     lpD3DDev->SetTexture( 3, NULL);
0566:     bShadowMap = FALSE;
0567:     bBg = TRUE;
0568:     DrawModel(lpD3DDev, bShadowMap, bBg);

■最後に

地面はぼけたんですが、車体に落ちる影がぼけていないのでそこが残念です。
エッジが取れていない奥の部分もぼけていませんが、これは調整で何とかできそうです。
あと、ぼけかたが一様なので、エッジを取るときやぼかすときに調整したほうがよさそうですね。
この画像では目だちませんが、ぼかした結果、一部の影が消える現象が出ます。 例えば、奥の車のバックミラー周辺の影が消えすぎていたりしています。 少し改良が必要ですね。

今回は、影の境界をぼかす方法をとりましたが、ライトの位置をずらして何回かレンダリングする方法も考えられますね。 まぁ、それも加工しないと綺麗にならないと思うので、調整が大変ですが…

ソフトシャドウを実現する方法は調べていないので、どのような方法があるか知らないのですが、 今回のも一つの方法としていいのではないのでしょうか。




もどる

imagire@gmail.com