下の画像をクリックすると、影が動くアニメーションが見られます(注意:3.21MB)。
ここ1年、テクスチャに影の情報を格納してライティングする手法が流行し始めています。
DirectXのサンプルで言えば、BumpSelfShadowのサンプルがそれにあたります。
最近では、球面調和関数(Spherical Harmonics)を使った手法がメジャーになりつつあるのですが、急にそんな難しいことをやってもやけがわからなくなるのがオチなので、
簡単な影のプログラムに挑戦してみましょう。
今回のプログラムは、次のものです。
今回は、初期化に少し時間がかかります。
まぁ、いつものように適当にファイルが入っています。
APP WIZARD から出力されるフレームワークのファイルは紹介を省かせていただきます。
hlsl.fx | シェーダの入ったエフェクトファイル |
main.h | アプリケーションのヘッダ |
main.cpp | アプリケーションのソース |
あと、実行ファイル、モデル及び、プロジェクトファイルが入っています。
今回の方法は、特定の方向から光をあてた時にあらかじめ影になる部分をテクスチャ(遮蔽マップ)に格納しておき、
実際のレンダリングの時には、そのテクスチャをかぶせることにより影の陰影を作り出します。
この方法は、セルフシャドウを作成するのに適していて、いろいろな方向から光があたった場合の影を作成しておき、
いい感じで補間すれば、セルフシャドウの影をリアルタイムに動かすことができます。
遮蔽マップの作成方法はちょっと面倒です。
その部分に光があたるかどうかを計算しなくてはなりません。
今回は、デカールテクスチャをxz平面にべたっと張っているモデルで計算しました。
このモデルでは、モデルを真上から見た状態とデカールテクスチャが対応していて、
デカールテクスチャ上でまっすぐな直線は、モデルのローカル座標系のxz平面でもまっすぐに伸びます。
実際に影があたっているかどうかを調べる方法ですが、光があたっているかではなく、
視点から光の方向を見たときに、遮蔽物があるかどうかで光があたるかどうかを判定します。
それぞれのテクセルを視点として、適当な方向に光線を飛ばします。
その光線が途中でより高いテクセルにさえぎられれば、光は届かないので影になります。
今回は、視線を32方向に飛ばして、それぞれの方向に対する影を求めておき、
中間の角度はぞれらの影を補完する方法で影を計算します。
32方向の影を格納するために、RGBAのそれぞれの成分を1枚の方向として、
1つのテクスチャに4つの方向を格納して、計8枚のテクスチャを作成します。
このテクスチャは1度求めれば、枚フレーム作成しなくても良いので、
初期化の段階でテクスチャを作成します。もちろん、実際の製品で使うときには、前もってテクスチャを計算しておき、
実際の表示のときは、そのテクスチャを読み込むだけにするのが良いでしょう。
遮蔽マップを作成するのですが、その前にそれぞれの位置での高さを格納した高さマップを作成します。
遮蔽マップは、それぞれの位置から光線を飛ばしたときにその光がさえぎられるかぶつかるかを高さマップから検索します。
高さマップは、模様としてはるデカールマップと同じテクスチャ座標を使ってはれるように調整します、
位置座標にテクスチャ座標の値を符号化スケーリングした値を出力します。ただし、y座標は上下がひっくり返るので、符号が変わります。
後は、色成分にローカル座標での高さを出力します。
必要ならば、0から1の範囲に高さが収まるようにスケーリングをしましょう。
hlsl.fx 0177: // ------------------------------------------------------------ 0178: // 高さマップ作成頂点シェーダプログラム 0179: // ------------------------------------------------------------ 0180: VS_OUTPUT VS_Height ( 0181: float4 Pos : POSITION // モデルの頂点 0182: , float4 Normal : NORMAL // 法線ベクトル 0183: , float4 Tex0 : TEXCOORD0 // テクスチャ座標 0184: ){ 0185: VS_OUTPUT Out = (VS_OUTPUT)0; // 出力データ 0186: 0189: // 位置座標 0190: Out.Pos = 2.0 * Tex0 - 1.0; 0191: Out.Pos.y *= -1; 0192: Out.Pos.z = 0.5; 0193: Out.Pos.w = 1; 0194: 0195: // 色 0196: Out.Color = Pos.y; 0197: 0198: return Out; 0199: }
ピクセルシェーダでは、色をそのまま出力します。
hlsl.fx 0201: // ------------------------------------------------------------ 0202: // 高さマップ作成ピクセルシェーダプログラム 0203: // ------------------------------------------------------------ 0204: float4 PS_Height (VS_OUTPUT In) : COLOR 0205: { 0206: float4 Out; 0207: 0208: // レンダリングターゲット 0209: Out = In.Color; 0210: 0211: return Out; 0212: }
アプリケーション側では、レンダリングターゲットを高さマップのサーフェスに切り替えて とりあえずzテストやカリングはしないようにしてレンダリングします。
amin.cpp 0348: //------------------------------------------------- 0349: // レンダリングターゲットの変更 0350: //------------------------------------------------- 0351: m_pd3dDevice->SetRenderTarget(0, m_pHeightSurf); 0352: m_pd3dDevice->SetDepthStencilSurface(NULL); 0353: // ビューポートの変更 0354: D3DVIEWPORT9 viewport_height = {0,0 // 左上の座標 0355: , MAP_SIZE // 幅 0356: , MAP_SIZE // 高さ 0357: , 0.0f,1.0f}; // 前面、後面 0358: m_pd3dDevice->SetViewport(&viewport_height); 0359: 0365: //------------------------------------------------- 0366: // 高さマップの作成 0367: //------------------------------------------------- 0368: RS( D3DRS_ZENABLE, FALSE ); 0369: RS( D3DRS_CULLMODE, D3DCULL_NONE ); 0370: m_pEffect->Pass( 0 ); 0371: 0372: // 背景の描画 0373: pMtrl = m_pMeshBg->m_pMaterials; 0374: for( i=0; i<m_pMeshBg->m_dwNumMaterials; i++ ) { 0375: m_pEffect->SetTexture(m_htSrcTex, m_pMeshBg->m_pTextures[i] ); 0376: m_pMeshBg->m_pLocalMesh->DrawSubset( i ); // 描画 0377: pMtrl++; 0378: }
高さマップを作成したら、次は高さマップから遮蔽マップを作成します。
遮蔽マップの作成は、高さマップからの1対1の対応として変換できます。
遮蔽マップを作成するのは、各テクセルに対して、視線方向にずらしたテクセルと比較して、
ずらしたテクセルの高さが自分から伸びた視線よりも高いかどうかを比較する作業になります。
最初にClear命令を使って、画面を白く塗りつぶしておきます。
次に、半透明合成をするように設定します。半透明合成の方法は出力した色での乗算合成です。
この合成をすると、影になる部分は黒く出力して、明るい部分は白く出力することにより、
最終的に光が射す部分は白く、どこかでさえぎられる部分は黒くなります。
次に、がんがん描画していきます。
RGBAの4枚のレイヤーに色を出力するので、描画回数はマップの枚数x4になります。
次に、それぞれの成分だけに書き込みするように、書き込みマスクD3DRS_COLORWRITEENABLEを設定します。
あとは、少しづつテクスチャをずらしながら描画します。
テクスチャをずらす量をシェーダの変数 vOffset に入れます。
すらす量は、レイヤーごとに設定されたずらす方向(視線)のベクトルray_dir[i]にその描画回数に比例する量でづらしていきます。
また、比較のために、テクセルから光線を飛ばしたときの光線が飛ぶ高さをfRayHeightに格納します。
main.cpp 0380: //------------------------------------------------- 0381: // 遮蔽マップの作成 0382: //------------------------------------------------- 0383: typedef struct {FLOAT p[3]; FLOAT tu, tv;} TVERTEX3; 0384: TVERTEX3 Vertex[4] = { 0385: // x y z tu tv 0386: { -1, -1, 0.1f, 0, 1,}, 0387: { 1, -1, 0.1f, 1, 1,}, 0388: { 1, 1, 0.1f, 1, 0,}, 0389: { -1, 1, 0.1f, 0, 0,}, 0390: }; 0391: m_pd3dDevice->SetFVF( D3DFVF_XYZ | D3DFVF_TEX1 ); 0392: m_pEffect->SetTexture(m_htSrcTex, m_pHeightTex); 0393: RS(D3DRS_ALPHABLENDENABLE, TRUE); 0394: RS(D3DRS_SRCBLEND , D3DBLEND_ZERO); 0395: RS(D3DRS_DESTBLEND , D3DBLEND_SRCCOLOR); 0396: m_pEffect->Pass( 1 ); 0397: 0398: for(i=0;i<4*COVER_MAPS;i++){ 0399: if(0==(i%4)){ 0400: m_pd3dDevice->SetRenderTarget(0, m_pCoverSurf[i/4]); 0401: m_pd3dDevice->Clear(0L, NULL 0402: , D3DCLEAR_TARGET 0403: , 0xffffffff, 1.0f, 0L); 0404: } 0405: 0406: RS(D3DRS_COLORWRITEENABLE,(1<<(i%4))); 0407: for(j=1; j<MAP_SIZE; j++){ 0408: // レイの高さ 0409: FLOAT d = 2.0f*(FLOAT)j/MAP_SIZE + 0.0001f; 0410: m_pEffect->SetFloat("fRayHeight", d); 0411: // テクスチャをずらす位置 0412: D3DXVECTOR4 v = D3DXVECTOR4( 0413: ray_dir[i][0]*j/MAP_SIZE, 0414: ray_dir[i][1]*j/MAP_SIZE,0,0); 0415: m_pEffect->SetVector("vOffset", &v); 0416: // 描画 0417: m_pd3dDevice->DrawPrimitiveUP( D3DPT_TRIANGLEFAN 0418: , 2, Vertex, sizeof( TVERTEX3 ) ); 0419: } 0420: }
遮蔽マップを計算する視線の方向は、32方向のそれぞれの方向の単位ベクトルになります。
まあ、sinとcosの基本的な使い方で全周を32等分するだけです。
main.cpp 0054: //------------------------------------------------------------- 0055: // ローカル変数 0056: //------------------------------------------------------------- 0057: // レイの方向 0058: static FLOAT ray_dir[4*COVER_MAPS][2] = { 0059: { cosf(D3DX_PI* 0/(2*COVER_MAPS)), sinf(D3DX_PI* 0/(2*COVER_MAPS))}, 0060: { cosf(D3DX_PI* 1/(2*COVER_MAPS)), sinf(D3DX_PI* 1/(2*COVER_MAPS))}, 0061: { cosf(D3DX_PI* 2/(2*COVER_MAPS)), sinf(D3DX_PI* 2/(2*COVER_MAPS))}, 0062: { cosf(D3DX_PI* 3/(2*COVER_MAPS)), sinf(D3DX_PI* 3/(2*COVER_MAPS))}, 0063: { cosf(D3DX_PI* 4/(2*COVER_MAPS)), sinf(D3DX_PI* 4/(2*COVER_MAPS))}, 0064: { cosf(D3DX_PI* 5/(2*COVER_MAPS)), sinf(D3DX_PI* 5/(2*COVER_MAPS))}, 0065: { cosf(D3DX_PI* 6/(2*COVER_MAPS)), sinf(D3DX_PI* 6/(2*COVER_MAPS))}, 0066: { cosf(D3DX_PI* 7/(2*COVER_MAPS)), sinf(D3DX_PI* 7/(2*COVER_MAPS))}, 0067: { cosf(D3DX_PI* 8/(2*COVER_MAPS)), sinf(D3DX_PI* 8/(2*COVER_MAPS))}, 0068: { cosf(D3DX_PI* 9/(2*COVER_MAPS)), sinf(D3DX_PI* 9/(2*COVER_MAPS))}, 0069: { cosf(D3DX_PI*10/(2*COVER_MAPS)), sinf(D3DX_PI*10/(2*COVER_MAPS))}, 0070: { cosf(D3DX_PI*11/(2*COVER_MAPS)), sinf(D3DX_PI*11/(2*COVER_MAPS))}, 0071: { cosf(D3DX_PI*12/(2*COVER_MAPS)), sinf(D3DX_PI*12/(2*COVER_MAPS))}, 0072: { cosf(D3DX_PI*13/(2*COVER_MAPS)), sinf(D3DX_PI*13/(2*COVER_MAPS))}, 0073: { cosf(D3DX_PI*14/(2*COVER_MAPS)), sinf(D3DX_PI*14/(2*COVER_MAPS))}, 0074: { cosf(D3DX_PI*15/(2*COVER_MAPS)), sinf(D3DX_PI*15/(2*COVER_MAPS))}, 0075: { cosf(D3DX_PI*16/(2*COVER_MAPS)), sinf(D3DX_PI*16/(2*COVER_MAPS))}, 0076: { cosf(D3DX_PI*17/(2*COVER_MAPS)), sinf(D3DX_PI*17/(2*COVER_MAPS))}, 0077: { cosf(D3DX_PI*18/(2*COVER_MAPS)), sinf(D3DX_PI*18/(2*COVER_MAPS))}, 0078: { cosf(D3DX_PI*19/(2*COVER_MAPS)), sinf(D3DX_PI*19/(2*COVER_MAPS))}, 0079: { cosf(D3DX_PI*20/(2*COVER_MAPS)), sinf(D3DX_PI*20/(2*COVER_MAPS))}, 0080: { cosf(D3DX_PI*21/(2*COVER_MAPS)), sinf(D3DX_PI*21/(2*COVER_MAPS))}, 0081: { cosf(D3DX_PI*22/(2*COVER_MAPS)), sinf(D3DX_PI*22/(2*COVER_MAPS))}, 0082: { cosf(D3DX_PI*23/(2*COVER_MAPS)), sinf(D3DX_PI*23/(2*COVER_MAPS))}, 0083: { cosf(D3DX_PI*24/(2*COVER_MAPS)), sinf(D3DX_PI*24/(2*COVER_MAPS))}, 0084: { cosf(D3DX_PI*25/(2*COVER_MAPS)), sinf(D3DX_PI*25/(2*COVER_MAPS))}, 0085: { cosf(D3DX_PI*26/(2*COVER_MAPS)), sinf(D3DX_PI*26/(2*COVER_MAPS))}, 0086: { cosf(D3DX_PI*27/(2*COVER_MAPS)), sinf(D3DX_PI*27/(2*COVER_MAPS))}, 0087: { cosf(D3DX_PI*28/(2*COVER_MAPS)), sinf(D3DX_PI*28/(2*COVER_MAPS))}, 0088: { cosf(D3DX_PI*29/(2*COVER_MAPS)), sinf(D3DX_PI*29/(2*COVER_MAPS))}, 0089: { cosf(D3DX_PI*30/(2*COVER_MAPS)), sinf(D3DX_PI*30/(2*COVER_MAPS))}, 0090: { cosf(D3DX_PI*31/(2*COVER_MAPS)), sinf(D3DX_PI*31/(2*COVER_MAPS))}, 0091: };
あとは、シェーダでのお話になります。
シェーダでは、ずらした位置のテクスチャの座標と比較の元になる現在のテクセルのテクスチャ座標を出力します。
hlsl.fx 0213: // ------------------------------------------------------------ 0214: // 遮蔽マップ作成頂点シェーダプログラム 0215: // ------------------------------------------------------------ 0216: float4 vOffset; 0217: VS_OUTPUT VS_Cover ( 0218: float4 Pos : POSITION // モデルの頂点 0219: , float2 Tex0 : TEXCOORD0 // テクスチャ座標 0220: ){ 0221: VS_OUTPUT Out = (VS_OUTPUT)0; // 出力データ 0222: 0223: // 頂点座標 0224: Out.Pos = Pos; 0225: 0226: // 色 0227: Out.Tex0 = Tex0; 0228: Out.Tex1 = Tex0+vOffset.xy; 0229: 0230: return Out; 0231: }
ピクセルシェーダが大事な比較のルーチンです。
元の位置と、光線の飛んでいく位置の高さを高さマップから読み込んで、
それが、光線の本来の位置よりも高ければ(光線をさえぎれば)黒を出力し、
光線がその場所にあたらなければ白を出力します。
hlsl.fx 0233: // ------------------------------------------------------------ 0234: // 遮蔽マップ作成ピクセルシェーダプログラム 0235: // ------------------------------------------------------------ 0236: float fRayHeight; 0237: float4 PS_Cover(VS_OUTPUT In) : COLOR 0238: { 0239: float4 Out; 0240: 0241: float4 center = tex2D( SrcSamp, In.Tex0 ); 0242: float4 offset = tex2D( SrcSamp, In.Tex1 ); 0243: 0244: Out = (center+fRayHeight < offset) ? 0 : 1; 0245: 0246: return Out; 0247: }
さて、遮蔽マップができたら、マップを使って地面を描画しましょう。
基本的に、頂点シェーダは難しくありません。普通にテクスチャを張ります。
ここで、デカールのテクスチャと、遮蔽マップに同じテクスチャ座標を使います。
ピクセルシェーダ2.0では、同じテクスチャ座標で何枚もテクスチャを張れるので、
今回は1つしかテクスチャ座標を出力しませんが、頂点シェーダ1.1等を使うときは、
それぞれの座標を別々に出力する必要があります。
hlsl.fx 0135: // ------------------------------------------------------------ 0136: // 頂点シェーダプログラム 0137: // ------------------------------------------------------------ 0138: VS_OUTPUT VS ( 0139: float4 Pos : POSITION // モデルの頂点 0140: ,float4 Normal : NORMAL // 法線ベクトル 0141: ,float4 Tex0 : TEXCOORD0 // テクスチャ座標 0142: ){ 0143: VS_OUTPUT Out = (VS_OUTPUT)0; // 出力データ 0144: 0145: float4 pos = mul( Pos, mWVP ); 0146: 0147: // 位置座標 0148: Out.Pos = pos; 0149: 0150: Out.Tex0 = Tex0; 0151: 0152: return Out; 0153: }
ピクセルシェーダでは、遮蔽マップから明るさを求めて、その色を拡散光に用います。
hlsl.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: // 色 0173: Out = tex2D( SrcSamp, In.Tex0 ) *(Cover+0.3f); 0174: 0175: return Out; 0176: }
明るさの求め方は、それぞれの遮蔽マップの強さを重みで線型合成します。
重みは、光のあたる方向に依存するそれぞれのマップの影響度です。
重みはアプリケーション側で求めるので、ここでは単純に遮蔽マップによる影の情報が
重みで足しあわされるだけの計算になります。
重み 遮蔽マップの各方向の明るさ Cover = vWeight[0].x * tex2D( CoverSamp1, In.Tex0 ).r + vWeight[0].y * tex2D( CoverSamp1, In.Tex0 ).g + vWeight[0].z * tex2D( CoverSamp1, In.Tex0 ).b + vWeight[0].w * tex2D( CoverSamp1, In.Tex0 ).a + vWeight[1].x * tex2D( CoverSamp2, In.Tex0 ).r +
重みの計算は、ライトの方向から、計算します。
下のプログラムで、dirがモデルのローカル座標系でのXZ平面でのライトの方向(正しくは、光源が存在する方向)になります。
今回の遮蔽マップでは、光の入射する高さは情報として入らないので、2次元のベクトルになります。
次に、内積を使って、射影マップを作成したときの視線の方向ray_dirとライトがどのくらい向いているのかを調べます。
内積を使っているので、ライトのベクトルと視線ベクトルが同じ向きを向いているときは、1になります。
そして、それていけば内積の値は段々と小さくなります。
何所まで影響させるかというのが問題になるのですが、ここでは、隣の遮蔽マップの視線の方向を向いたときには、
影響が0になるようにします。
つまり、今回は、32方向に視線を飛ばすので、内積の値がcos(2π/32)よりも小さいときに影響が0になるようにします。
そこで、内積の値からcos(2π/32)を引いて、その値が正の部分にだけ遮蔽マップの影響が出るように値を調整します。
最後に、影響の総和を1になるように、重みの値を合計して、全ての重みをその合計で割るという規格化をします。
main.cpp 0541: //------------------------------------------------- 0542: // シーンの描画 0543: //------------------------------------------------- 0544: // 遮蔽マップを合成する重み 0545: D3DXVECTOR4 array[COVER_MAPS]; 0546: float acc=0; 0547: float theta = fmod(0.5*this->m_fTime, 2.0*D3DX_PI); 0548: float dir[2] = {sinf(theta), cosf(theta)}; 0549: int no=0; 0550: for(i=0;i<COVER_MAPS;i++){ 0551: array[i].x = max(0, dir[0]*ray_dir[no][0]+dir[1]*ray_dir[no][1]-cosf(D3DX_PI/(2*COVER_MAPS)));no++; 0552: array[i].y = max(0, dir[0]*ray_dir[no][0]+dir[1]*ray_dir[no][1]-cosf(D3DX_PI/(2*COVER_MAPS)));no++; 0553: array[i].z = max(0, dir[0]*ray_dir[no][0]+dir[1]*ray_dir[no][1]-cosf(D3DX_PI/(2*COVER_MAPS)));no++; 0554: array[i].w = max(0, dir[0]*ray_dir[no][0]+dir[1]*ray_dir[no][1]-cosf(D3DX_PI/(2*COVER_MAPS)));no++; 0555: } 0556: for(i=0;i<COVER_MAPS;i++) 0557: acc += array[i].x + array[i].y + array[i].z + array[i].w; 0558: for(i=0;i<COVER_MAPS;i++){ 0559: array[i].x/=acc; 0560: array[i].y/=acc; 0561: array[i].z/=acc; 0562: array[i].w/=acc; 0563: } 0564: m_pEffect->SetVectorArray("vWeight", array, COVER_MAPS);
後は、射影マップを設定して、シェーダを使って描画するだけです。
main.cpp 0567: // 変換行列 0568: m = m_mWorld * m_mView * m_mProj; 0569: m_pEffect->SetMatrix( m_hmWVP, &m ); 0570: 0571: m_pEffect->SetTexture("CoverTex0", m_pCoverTex[0]); 0572: m_pEffect->SetTexture("CoverTex1", m_pCoverTex[1]); 0573: m_pEffect->SetTexture("CoverTex2", m_pCoverTex[2]); 0574: m_pEffect->SetTexture("CoverTex3", m_pCoverTex[3]); 0575: m_pEffect->SetTexture("CoverTex4", m_pCoverTex[4]); 0576: m_pEffect->SetTexture("CoverTex5", m_pCoverTex[5]); 0577: m_pEffect->SetTexture("CoverTex6", m_pCoverTex[6]); 0578: m_pEffect->SetTexture("CoverTex7", m_pCoverTex[7]); 0579: 0580: // 背景の描画 0581: pMtrl = m_pMeshBg->m_pMaterials; 0582: for( i=0; i<m_pMeshBg->m_dwNumMaterials; i++ ) { 0583: m_pEffect->SetTexture(m_htSrcTex, m_pMeshBg->m_pTextures[i] ); 0584: m_pMeshBg->m_pLocalMesh->DrawSubset( i ); // 描画 0585: pMtrl++; 0586: }
影がぼけないと方向による切り替えが不自然にくっきりしますね。
色々な方向に光線を飛ばせば綺麗になると思いますが、
最初から複雑なプログラムにしてもしょうが無いので、これぐらいにとどめましょう。
だんだんとバージョンアップしていきましょう。
まあ、次は水平線マップですかね。