今回は、被写界深度です。
下の絵を見てください。中心のクルマははっきりと見えていますが、手前のクルマはボケています。また、背景の遠くのほうもボケています。
深度を見てピントを合わせるのが、被写界深度です。
今回のソースは、次のものです。
内容は次のとおりになっています。
dof.psh | ピクセルシェーダープログラム。ボケてる画面と普通の画面を合成する。 |
draw.h | 描画の各関数の定義。特に意味無いので出番無し。 |
draw.cpp | メインの描画部分。 |
blur.psh | ピクセルシェーダープログラム。ボケた画面を作る。 |
blur.vsh | 頂点シェーダープログラム。ボケた画面を作る。 |
bg.cpp | 背景表示。半球ライティングの時のものと同じ |
vs.vsh | 頂点シェーダープログラム。α成分書き込み付き平行光源ライト。 |
ps.psh | 頂点シェーダープログラム。平行光源ライト。 |
load.cpp | シェーダーやテクスチャーのロード開放 |
load.h | シェーダーやテクスチャーのロード開放 |
main.cpp | 描画に関係しないシステム的な部分。変更が無いので、出番無し。 |
main.h | 基本的な定数など。今回も画面サイズは512x512です。 |
あと、モデルとして、nsx.xと、実行ファイルの MyBase.exe 及び、 VC++ でコンパイルするためのプロジェクトファイル MyBase.dsw MyBase.dsp が入っています。
今回も、前回のフォーカスと同じようにボケた画像とはっきりした画像を合成します。
違いは深度を合成用のパラメータに用いることです。
深度を画像にすると、下の用になります(遠い場所が白、近くが黒です)。
前回は、はっきりした部分が白、ボケた部分が黒でした。
中央の車をはっきりさせたいので、中央が白、手前と奥が黒くなるように変換します。
図で記すと、次の変換をします。
以上の変換をすると深度は次のようになります。
このテクスチャーを合成パラメータに使った画像が次のようになります。
作ってみた感想ですが、ピントのあった部分が少なく、中央の車自体にもボケた部分が存在します。
そこで、中央の部分を大きくして、ピントの合う部分を増やします。
その画像が下です。
このテクスチャーを使用して合成した絵が最初の画像です。
ただし、このの方法では線形変換特有の急な変化が出がちなので、ガウズ型フィルタ(α∝exp(-c z^2))を用いると、さらによい結果が得られるかもしれません。
合成に用いるピクセルシェーダープログラムは次(dof.psh)です。
0001: ; dof.psh 0002: ; 0003: 0004: ps.1.1 0005: 0006: def c0, 0.0f, 0.0f, 0.0f, 0.5f 0007: 0008: ; テクスチャーの色を引っ張ってくる 0009: tex t0 ; 元テクスチャー 0010: tex t1 ; ボケテクスチャー 0011: tex t2 ; 大ボケテクスチャー 0012: 0013: ; いい感じの深度を引っ張る 0014: lrp r0.a, c0, t0, t2 ; 深度ははっきりしたものと、大ボケのものの平均を取る 0015: cnd_x4_sat r0.a, r0.a, 1-r0.a, r0.a; 0.5 の時にはっきりなるようにする 0016: 0017: ; 0.0→0.5 の成分を作る 0018: mov_x2_sat r1.a, r0.a ; t0.a : 0.0→0.5→1.0 0019: ; r1.a : 0.0→1.0→1.0 (r0.a が 0.0 から 0.5 の下半分が、0.0 から 1.0 になる) 0020: lrp r1.rgb, r1.a, t1, t2 ; t0.a : 0.0→0.5→1.0 0021: ; r1.rgb: t2 →t1 →t1 (r0.a が 0.0 から 0.5 の下半分が、 t2 から t1 になる) 0022: 0023: ; 0.5→1.0 の成分を作る 0024: mov_sat r1.a, r0_bx2.a ; t0.a : 0.0→0.5→1.0 0025: ; r1.a : 0.0→0.0→1.0 (r0.a が 0.5 から 1.0 の上半分が、0.0 から 1.0 になる) 0026: lrp r0.rgb, r1.a, t0, t1 ; t0.a : 0.0→0.5→1.0 0027: ; r0.rgb: t1 →t1 →t0 (r0.a が 0.5 から 1.0 の上半分が、 t1 から t0 になる) 0028: 0029: ; 二つを上手く混ぜる 0030: cnd r0.rgb, r0.a, r0, r1 ; r0.rgb = (0.5 < r0.a) ? r0 : r1 0031: ; t0.a : 0.0→0.5→1.0 0032: ; r0.rgb: t2 →t1 →t0 ( 0.0 から 0.5 は r1、0.5 から 1.0 は r0 を取る)
先ず、tex コマンドで、テクスチャーを読み込みます。
t0 に元の画像、t1、t2 にだんだん大きくボケた画像を入力します。
それぞれの画像にはα成分に深度が入っています。
さて、次の黄色の部分は、今までに出てこなかった事柄です。
やっていることは、合成に用いる深度を、はっきりしたテクスチャーと一番ボケたテクスチャーの平均に設定します。
t0.a + t2.a r0.a = ------------ 2
実際の画像として表現すると、次のようになります。
違いは、元の画像でくっきりしていた端の部分が、適度にボケていることです。
これがないと、深度が急激に変化している部分で、ボケるべき部分にくっきりした画像の色が侵食してくるので、
シャープな画像が得られません。
平均をとっても、侵食は避けられないのですが、多少はごまかすことができます。
次の cnd_x4_sat 命令が、中央が広い深度の変換をします。
cnd 命令は、r0.a が 0.5 よりも大きい時は3項目、小さい時は4項目を読み取ります。
従って、次の命令を使えば、0.5 で折り返す山なりの形が得られます。
cnd r0.a, r0.a, 1-r0, r0 r0.a |0.0 → 0.5 → 1.0 ----------------------------- 出力 |0.0 → 0.5 → 0.0
あとの、x4 は、4倍して 0.25 の時に 1.0 になるように調整します。
さらに、sat 命令を使って、0.0 から 1.0 に値を制限することによって、台形な形の変換をすることができます。
残りはテクスチャーの合成です。
ドジ研の忘年会の時にMasaさんから、
『テクスチャーは3枚で合成するといいよ』と、いわれたので、今回は3枚で合成しました。
方法としては、lrp 命令を使って、r0.a が 0 から 0.5 の時に、t2 から t1 を合成した r1.rgb と、
0.5 から 1.0 の時に t1 から t0 を合成した r0.rgb を用意して、
cnd 命令を使って最終的に合成します。
では、それ以外のソースで、フォーカスの時とは違う部分を説明します。
先ずは、初期化です。
今回は、ブラー用のテクスチャーを3枚用意します(nBlurTex=3)。
その初期化部分は次のようになります。
0166: //----------------------------------------------------------------------------- 0167: // Name: InitBlurTexture() 0168: // Desc: ボケたテクスチャー用の下準備 0169: //----------------------------------------------------------------------------- 0170: HRESULT InitBlurTexture(LPDIRECT3DDEVICE8 lpD3DDev) 0171: { 0172: HRESULT hr; 0173: DWORD i; 0174: 0175: // 頂点バッファの作成 0176: D3D_BLUR_VERTEX *pBlurDest; 0177: WORD *pIndex; 0178: lpD3DDev->CreateVertexBuffer( 4 * sizeof(D3D_BLUR_VERTEX), 0179: D3DUSAGE_WRITEONLY, D3DFVF_BLUR_VERTEX, D3DPOOL_MANAGED, 0180: &pBlurVB ); 0181: // 頂点をセットアップ 0182: pBlurVB->Lock ( 0, 0, (BYTE**)&pBlurDest, 0 ); 0183: for (i = 0; i < 4; i++) { 0184: pBlurDest->x = (i == 0 || i == 1)?-1:(float)1; 0185: pBlurDest->y = (i == 0 || i == 2)?-1:(float)1; 0186: pBlurDest->z = 0.0f; 0187: pBlurDest->tu = (i == 2 || i == 3)?1:(float)0; 0188: pBlurDest->tv = (i == 0 || i == 2)?1:(float)0; 0189: pBlurDest++; 0190: } 0191: pBlurVB->Unlock (); 0192: // インデックスをセットアップ 0193: lpD3DDev->CreateIndexBuffer( 6 * sizeof(WORD), 0194: 0, 0195: D3DFMT_INDEX16, D3DPOOL_MANAGED, 0196: &pBlurIB ); 0197: pBlurIB->Lock ( 0, 0, (BYTE**)&pIndex, 0 ); 0198: pIndex[0] = 0; pIndex[1] = 1; pIndex[2] = 2; 0199: pIndex[3] = 1; pIndex[4] = 3; pIndex[5] = 2; 0200: pBlurIB->Unlock (); 0201: 0202: // 描画用テクスチャーを用意する 0203: D3DSURFACE_DESC Desc; 0204: LPDIRECT3DSURFACE8 lpZbuffer = NULL; 0205: if( FAILED(hr = lpD3DDev->GetRenderTarget(&pBackbuffer))) return hr; 0206: if( FAILED(hr = pBackbuffer->GetDesc( &Desc ))) return hr; 0207: 0208: // 深度バッファのサーフェスを確保する 0209: if( FAILED(hr = lpD3DDev->GetDepthStencilSurface( &lpZbuffer ))) return hr; 0210: 0211: for(i = 0; i < nBlurTex; i ++){ 0212: // テクスチャーの生成 0213: if( FAILED(hr = lpD3DDev->CreateTexture(Desc.Width, Desc.Height, 1 0214: , D3DUSAGE_RENDERTARGET, D3DFMT_A8R8G8B8, D3DPOOL_DEFAULT, &pTexture[i]))) return hr; 0215: // テクスチャーとサーフェスを関連づける 0216: if( FAILED(hr = pTexture[i]->GetSurfaceLevel(0,&pTextureSurface[i]))) return hr; 0217: // テクスチャー用の描画と深度バッファを関連付ける(一枚目だけ深度を持つ) 0218: if( FAILED(hr = lpD3DDev->SetRenderTarget(pTextureSurface[i], (i==0)?lpZbuffer:NULL ))) return hr; 0219: } 0220: // ぼけテクスチャー作成用のリソースを確保する 0221: for (i = 0; i < nTempTex; ++i) { 0222: // テクスチャーの生成 0223: if( FAILED(hr = lpD3DDev->CreateTexture(Desc.Width, Desc.Height, 1 0224: , D3DUSAGE_RENDERTARGET, D3DFMT_A8R8G8B8, D3DPOOL_DEFAULT, &pTmpTexture[i]))) return hr; 0225: // テクスチャーとサーフェスを関連づける 0226: if( FAILED(hr = pTmpTexture[i]->GetSurfaceLevel(0,&pTmpSurface[i]))) return hr; 0227: // テクスチャー用の描画と深度バッファを関連付ける 0228: if( FAILED(hr = lpD3DDev->SetRenderTarget(pTmpSurface[i], NULL ))) return hr; 0229: } 0230: 0231: // 描画を元の画面に戻す 0232: lpD3DDev->SetRenderTarget(pBackbuffer, lpZbuffer ); 0233: 0234: // シェ-ダーのロード 0235: if ( FAILED(CVertexShaderMgr::Load(lpD3DDev, "blur.vsh", &hBlurVertexShader, dwBlurDecl)) ) return hr; 0236: if ( FAILED( CPixelShaderMgr::Load(lpD3DDev, "blur.psh", &hBlurPixelShader)) ) return hr; 0237: if ( FAILED( CPixelShaderMgr::Load(lpD3DDev, "dof.psh", &hFocusPixelShader)) ) return hr; 0238: 0239: // 定数レジスタの設定 0240: float const s = 4.0f/3.0f; 0241: float const inv_w = s / (float)WIDTH; 0242: float const inv_h = s / (float)HEIGHT; 0243: lpD3DDev->SetVertexShaderConstant(20, &D3DXVECTOR4 ( 0.0f, 0.0f, 0.0f, 0.0f), 1); 0244: lpD3DDev->SetVertexShaderConstant(21, &D3DXVECTOR4 ( 0.0f, inv_h, 0.0f, 0.0f), 1); 0245: lpD3DDev->SetVertexShaderConstant(22, &D3DXVECTOR4 (inv_w, inv_h, 0.0f, 0.0f), 1); 0246: lpD3DDev->SetVertexShaderConstant(23, &D3DXVECTOR4 (inv_w, 0.0f, 0.0f, 0.0f), 1); 0247: 0248: return S_OK; 0249: }
実は前回との違いは一箇所です。テクスチャーを生成する CreateTexture の引数のフォーマット部分が D3DFMT_A8R8G8B8 になっています。
このフォーマットを指定することにより、α成分に書き込みを行うことができます。
次に描画部分です。
違いは定数レジスタの設定が一つ増えている事と、テクスチャーの設定を3つ行っていることです。
後は、ボケた画像を作る方法は前回と同じです。
0282: //----------------------------------------------------------------------------- 0283: // Name: Render() 0284: // Desc: Draws the scene 0285: //----------------------------------------------------------------------------- 0286: VOID Render(LPDIRECT3DDEVICE8 lpD3DDev) 0287: { 0288: DWORD i, j, k; 0289: D3DXMATRIX mWorld, mView, mProj, m; 0290: 0291: D3DXVECTOR4 lightDir(1.0f, 1.0f, 0.5f, 0.0f), vl; 0292: 0293: LPDIRECT3DSURFACE8 lpZbuffer = NULL; 0294: lpD3DDev->GetDepthStencilSurface( &lpZbuffer ); 0295: lpD3DDev->SetRenderTarget(pTextureSurface[0], lpZbuffer); 0296: lpD3DDev->Clear(0,NULL,D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, D3DCOLOR_XRGB(0,0,0),1.0f,0); 0297: 0298: // ビュー行列 0299: D3DXVECTOR3 eye = D3DXVECTOR3(0.0f,1.4f*MeshRadius,2.5f*MeshRadius); 0300: D3DXVECTOR3 lookAt = D3DXVECTOR3(0.0f, 0.0f, 0.0f); 0301: D3DXVECTOR3 up = D3DXVECTOR3(0.0f, 1.0f, 0.0f); 0302: // 通常表示 0303: D3DXMatrixLookAtLH(&mView, &eye, &lookAt, &up); 0304: 0305: const float min = 0.01f; 0306: const float max = 100.0f; 0307: D3DXMatrixPerspectiveFovLH(&mProj 0308: ,60.0f*PI/180.0f // 視野角 0309: ,(float)WIDTH/(float)HEIGHT // アスペクト比 0310: ,min,max // 最近接距離,最遠方距離 0311: ); 0312: // z値を0.0fから1.0fに補正する定数 0313: lpD3DDev->SetVertexShaderConstant(15, &D3DXVECTOR4(3.0f/(max-min), -0.45f*max/(max-min), 0.0f, 0.0f), 1); 0314: 0315: lpD3DDev->SetRenderState( D3DRS_ALPHABLENDENABLE, FALSE); 0316: lpD3DDev->SetVertexShader(hVertexShader); 0317: lpD3DDev->SetPixelShader(hPixelShader); 0318: 0319: // 0320: // 背景描画 0321: // 同じなので省略 0433: // 0434: // 完成した一枚絵を描画する 0435: // 0436: lpD3DDev->SetRenderState( D3DRS_ALPHABLENDENABLE, FALSE); 0437: lpD3DDev->SetRenderTarget(pBackbuffer, lpZbuffer ); // 描画をバックバッファに戻す 0438: lpD3DDev->SetPixelShader(hFocusPixelShader); 0439: lpD3DDev->SetTexture( 0, pTexture[0] ); // 元テクスチャー 0440: lpD3DDev->SetTexture( 1, pTexture[1] ); // ボケテクスチャー 0441: lpD3DDev->SetTexture( 2, pTexture[2] ); // 大ボケテクスチャー 0442: lpD3DDev->DrawIndexedPrimitive( D3DPT_TRIANGLELIST, 0, 4, 0, 2 ); 0443: // 0444: // 環境を元に戻す 0445: // 0446: lpD3DDev->SetPixelShader(NULL); 0447: lpD3DDev->SetRenderState(D3DRS_ZENABLE, TRUE); 0448: }
頂点シェーダーの c15 レジスタの設定は、モデルを描画する時に反映されます。
今回、描画に関して地面や空のモデルも、車と同じレンダリングを行っています。
レンダリングのプログラムは、次になります。
0001: ; c0-3 -- world + ビュー + 透視変換行列 0002: ; c12 -- {0.0, 0.5, 1.0, 2.0} N.B. 今回出番無し 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 0025: 0026: 0027: ; テクスチャーを張る 0028: mov oT0, v7
黄色い部分が今回反映されたα成分への深度の書き込みです。
dp4 命令で、z値を求め(oPos.zと同じ値)、線形合成して適当に変換します。
数式では、次のように表されます。
oD0.w = c15.x * r0.w + c15.y 3.0 z - 0.45 max = ------------------- max - min
max - min を用いることによって、Z値を 0.0~1.0 の範囲にスケーリングします。
それ以外に3倍したり、0.45 max を引いたりしていますが、これは完全に目あわせす。
出力される画像がいい感じになるように調整しました。
また、Pixel Shader プログラムは次のようになります。
テクスチャーが無い場合(車)や、平行光源に影響されない場合(空)でもきちんと動くように、
色はテクスチャーと頂点色を加算合成し、α成分は上の頂点シェーダープログラムで得られた頂点色を用いています。
0001: ; ps.psh 0002: ps.1.0 0003: 0004: ; テクスチャーの色を引っ張ってくる 0005: tex t0 0006: 0007: add r0, v0, t0 0008: mov r0.a, v0
以上が前回からの違いです。
被写界深度を実装しました。
被写界深度は、PS2 の『ボクと魔王』を見た時に、やり方が分からず驚愕したのを覚えています。
テクスチャーへのレンダリングや、前回のフォーカスは今回の為の前準備といっても過言ではありません。
調整が必要なので、思うとおりに使うのは難しいかもしれませんが、レンダリングの効果の一つとして使ってみてはいかがでしょうか。