地平線マップ


~ Horizon map ~







下の画像をクリックすると、影が動くアニメーションが見られます(注意:4.92MB)。


■はじめに

今回は,前回のプログラムを変更して、地平線マップに取り組んでみます。
今回はセルフソフトシャドウのおまけ付きです。

今回のプログラムは、次のものです。
今回も、初期化に少し時間がかかります。

まぁ、いつものように適当にファイルが入っています。
APP WIZARD から出力されるフレームワークのファイルは紹介を省かせていただきます。

hlsl.fxシェーダの入ったエフェクトファイル
main.hアプリケーションのヘッダ
main.cppアプリケーションのソース

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

■何やってるの?

前回の遮蔽マップは、マップを作るときに見た角度からずれた角度に対するレンダリングのときに、 2つのマップの線型補間を使って計算していたために、2つの影を合成していたことがばればれでした。
それは明らかに変なので、影が滑らかに移動する方法について取り組んでみます。
その方法が地平線マップです。これは、DirectX 8.1 からサンプルとして収められていますし、 CEDEC2002の DirectX day でも紹介されているので、今となっては、特に目新しい技術ではありません。

地平線マップと前回の遮蔽マップの違いは、マップとして格納する変数の種類が違うということです。
遮蔽マップの時には、ライトが当てられたときに、その部分が影になるかどうかを格納しました。
今回は、視線の方向について、一番高い山の山頂までの角度をマップに保存します (今回のプログラムでは、実際には、一番高い部分までの角度のタンジェントを格納しますが、本質的には同じ事です)。

今回のマップを見ると、前回の影の形がくっきりと浮き出ていたマップとは違って、影の起きる方向に長い尾が確認できます。
これは、「視線方向に一番高い部分」ということで、1つ高い山があれば、しばらくはその山が高い部分になるからです。
今回は、影の落ちる角度とライトの角度を比較することによって、影になるか判定するので、ライトの角度、すなわち影の長さを自由に変えることができます。
高いライトなら影を短く、低いライトなら影を長くするなどの調整が簡単に行なえます。

今回は、視線も32方向に飛ばして、それぞれの方向に対する地平線マップを作成します。
1つのマップにRGBAの4成分を使って、4方向のマップを格納します。

この32方向の中間にある半端な角度の場合には、2つのマップを合成して影を求めるのですが、 今回の場合には、一番高い部分というものが補間されるので、途中の状態も滑らかなものとなります。

■地平線マップの作成

それでは、地平線マップを作成しましょう。
地平線マップは、前回の遮蔽マップとそれほど変わりません。
視線を飛ばして、視線の先のそれぞれのテクセルに関して、その高さと 視点の高さを比較して、それを視線のテクセルまでの距離で割って、tan(地面の高さの仰角)を計算します。

hlfx.fx
0236: // ------------------------------------------------------------
0237: // 地平線マップ作成ピクセルシェーダプログラム
0238: // ------------------------------------------------------------
0239: float  fRayDist;
0240: float4 PS_Cover(VS_OUTPUT In) : COLOR
0241: {
0242:     float4 Out;
0243:     
0244:     float4 center = tex2D( SrcSamp, In.Tex0 );
0245:     float4 offset = tex2D( SrcSamp, In.Tex1 );
0246: 
0247:     Out = (offset-center)/fRayDist;
0248:     
0249:     return Out;
0250: }

先ほどの計算では、一番高い場所の判定はしていません。
一番高い場所を検索するために、アルファ合成を使います。
レンダリングステートの D3DRS_BLENDOP に D3DBLENDOP_MAX というものがあります。
これは、ピクセルシェーダの出力と、すでにレンダリングされたフレームバッファの値を比較して、値の大きな方をフレームバッファに書き込むというものです。
まぁ、何のために使うんだこんなのと思っていたのですが、こんな使い道があるんですね。

また、テクセルをずらした量をシェーダに引き渡すのを忘れないようにしましょう。

main.cpp
0030: #define RS   m_pd3dDevice->SetRenderState
0382:             //-------------------------------------------------
0383:             // 遮蔽マップの作成
0384:             //-------------------------------------------------
0385:             typedef struct {FLOAT p[3]; FLOAT tu, tv;} TVERTEX3;
0386:             TVERTEX3 Vertex[4] = {
0387:                 // x   y   z   tu tv
0388:                 { -1, -1, 0.1f, 0, 1,},
0389:                 {  1, -1, 0.1f, 1, 1,},
0390:                 {  1,  1, 0.1f, 1, 0,},
0391:                 { -1,  1, 0.1f, 0, 0,},
0392:             };
0393:             m_pd3dDevice->SetFVF( D3DFVF_XYZ | D3DFVF_TEX1 );
0394:             m_pEffect->SetTexture(m_htSrcTex, m_pHeightTex);
0395:             RS(D3DRS_ALPHABLENDENABLE, TRUE);
0396:             RS(D3DRS_BLENDOP, D3DBLENDOP_MAX);
0397:             RS(D3DRS_SRCBLEND , D3DBLEND_ONE);
0398:             RS(D3DRS_DESTBLEND , D3DBLEND_ONE);
0399:             m_pEffect->Pass( 1 );
0400: 
0401:             for(i=0;i<4*COVER_MAPS;i++){
0402:                 if(0==(i%4)){
0403:                     m_pd3dDevice->SetRenderTarget(0, m_pCoverSurf[i/4]);
0404:                     m_pd3dDevice->Clear(0L, NULL
0405:                                     , D3DCLEAR_TARGET
0406:                                     , 0x00000000, 1.0f, 0L);
0407:                 }
0408: 
0409:                 RS(D3DRS_COLORWRITEENABLE,(1<<(i%4)));
0410:                 for(j=1; j<MAP_SIZE; j++){
0411:                     // ずらした量
0412:                     FLOAT d = 10.0f*(FLOAT)j/MAP_SIZE + 0.0001f;
0413:                     m_pEffect->SetFloat("fRayDist", d);
0414:                     // テクスチャをずらす位置
0415:                     D3DXVECTOR4 v = D3DXVECTOR4(
0416:                             ray_dir[i][0]*j/MAP_SIZE,
0417:                             ray_dir[i][1]*j/MAP_SIZE,0,0);
0418:                     m_pEffect->SetVector("vOffset", &v);
0419:                     // 描画
0420:                     m_pd3dDevice->DrawPrimitiveUP( D3DPT_TRIANGLEFAN
0421:                                 , 2, Vertex, sizeof( TVERTEX3 ) );
0422:                 }
0423:             }

■シーンの描画

さて、それでは地平線マップを使って影をつけましょう。
基本的には、前回のように、それぞれのマップを読み込んで合成し、 各ピクセルから光源方向にみた地面の高さを計算して、 それがライトの高さ(下のプログラムでは0.3)と比較して、地面が高ければ日が射さない影(遮蔽量が1)になって、地面が低ければ日が射します(遮蔽量が0)。
ライトの高さを変えれば、影の長さは変わります。
あとは、この遮蔽量を使って、照明計算をします。

float4 PS (VS_OUTPUT In) : COLOR
{
    float4 Out;
    
    // 遮蔽量
    float Cover = dot(vWeight[0], tex2D( CoverSamp0, In.Tex0 ));
          Cover+= dot(vWeight[1], tex2D( CoverSamp1, In.Tex0 ));
          Cover+= dot(vWeight[2], tex2D( CoverSamp2, In.Tex0 ));
          Cover+= dot(vWeight[3], tex2D( CoverSamp3, In.Tex0 ));
          Cover+= dot(vWeight[4], tex2D( CoverSamp4, In.Tex0 ));
          Cover+= dot(vWeight[5], tex2D( CoverSamp5, In.Tex0 ));
          Cover+= dot(vWeight[6], tex2D( CoverSamp6, In.Tex0 ));
          Cover+= dot(vWeight[7], tex2D( CoverSamp7, In.Tex0 ));

    Cover = (Cover<0.3)?1:0;

    // 色
    Out = tex2D( SrcSamp, In.Tex0 ) *(Cover+0.3f);

    return Out;
}

さて、これだけではくっきりした影になるので、ソフトにしましょう。
ぼんやりした影にするには、光の入射する高さと山の高さが近いピクセルについて、 影になったとしても、真っ暗にするのではなくて、少しだけ暗くするようにします。
具体的には、影に隠れたとき(光の高さ<光の方向の山の高さ)に、光が隠れた高さ(光の方向の山の高さ-光の高さ)に応じて、遮蔽量を減すことによって、影になった部分をだんだんと暗くしていきます。
また、遮蔽量が0より小さくなることはないので、0でクランプします。

hlfx.fx
0154: // ------------------------------------------------------------
0155: // 照明計算なしピクセルシェーダプログラム
0156: // ------------------------------------------------------------
0157: float4 vWeight[8];
0158: float4 PS (VS_OUTPUT In) : COLOR
0159: {
0160:     float4 Out;
0161:     
0162:     // 遮蔽量
0163:     float Cover = dot(vWeight[0], tex2D( CoverSamp0, In.Tex0 ));
0164:           Cover+= dot(vWeight[1], tex2D( CoverSamp1, In.Tex0 ));
0165:           Cover+= dot(vWeight[2], tex2D( CoverSamp2, In.Tex0 ));
0166:           Cover+= dot(vWeight[3], tex2D( CoverSamp3, In.Tex0 ));
0167:           Cover+= dot(vWeight[4], tex2D( CoverSamp4, In.Tex0 ));
0168:           Cover+= dot(vWeight[5], tex2D( CoverSamp5, In.Tex0 ));
0169:           Cover+= dot(vWeight[6], tex2D( CoverSamp6, In.Tex0 ));
0170:           Cover+= dot(vWeight[7], tex2D( CoverSamp7, In.Tex0 ));
0171: 
0172:     Cover = (Cover<0.3)?1:(1-5.0f*(Cover-0.3));
0173:     if(Cover<0) Cover=0;
0174:     
0175:     // 色
0176:     Out = tex2D( SrcSamp, In.Tex0 ) *(Cover+0.3f);
0177: 
0178:     return Out;
0179: }

■マップの枚数に関する考察

CEDECの時には、地平線マップは、8方向からのライティングを記述する2枚のテクスチャだけで十分だという説明がなされました。
しかし、今回は、8枚のテクスチャを追加って、32方向からのライティングを記録しています。
この枚数が少ないとどうなるのでしょうか?
ためしに、今回のプログラムでマップの枚数を1枚にしてみて、4方向からの光しか考えないと、次のアニメーションになります。

4方向しか遮蔽マップを作成しないときには、高い山の影はライトをぐるりと1週させたときに星型の軌跡を描きます。

従って、山の高さが高いと、マップの枚数が少ないときにはおかしな影ができることがわかります。。
山の高さがそんなに高くなくて、影がさほど伸びないなら、4方向や8方向でも十分な場合はあるでしょう。

■最後に

前回と今回は、テクスチャに遮蔽情報を埋め込んでライティングをしました。
今回は、遮蔽マップを作るときと、シーンのレンダリングに同じメッシュを使いましたが、 必ずしもそうでなければいけないわけではありません。
例えば、ポリゴン1枚使うだけでも、けっこうましな画面を作ることができます。

非常に遠くの景色で、詳細な情報が要らないなら、これで十分でしょう。
しかも、光源の方向によって、影を変化させることができるので、こけおどしもばっちりです。





もどる

imagire@gmail.com