DirectX 8.1:被写界深度 (Depth of Field)


~前回までの道は、ここへ繋がる?~




■はじめに

今回は、被写界深度です。
下の絵を見てください。中心のクルマははっきりと見えていますが、手前のクルマはボケています。また、背景の遠くのほうもボケています。
深度を見てピントを合わせるのが、被写界深度です。

今回のソースは、次のものです。

内容は次のとおりになっています。

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です。
tile.bmp (床デカール)
sky.bmp (空デカール)

あと、モデルとして、nsx.xと、実行ファイルの MyBase.exe 及び、 VC++ でコンパイルするためのプロジェクトファイル MyBase.dsw MyBase.dsp が入っています。

■方法

今回も、前回のフォーカスと同じようにボケた画像とはっきりした画像を合成します。
違いは深度を合成用のパラメータに用いることです。
深度を画像にすると、下の用になります(遠い場所が白、近くが黒です)。

深度の絵

前回は、はっきりした部分が白、ボケた部分が黒でした。
中央の車をはっきりさせたいので、中央が白、手前と奥が黒くなるように変換します。
図で記すと、次の変換をします。

深度の変換

以上の変換をすると深度は次のようになります。

変換した深度

このテクスチャーを合成パラメータに使った画像が次のようになります。

上の画像を使った被写界深度。

作ってみた感想ですが、ピントのあった部分が少なく、中央の車自体にもボケた部分が存在します。
そこで、中央の部分を大きくして、ピントの合う部分を増やします。

中央が広い深度の変換

その画像が下です。

中央の部分を広げた深度。

このテクスチャーを使用して合成した絵が最初の画像です。
ただし、このの方法では線形変換特有の急な変化が出がちなので、ガウズ型フィルタ(α∝exp(-c z^2))を用いると、さらによい結果が得られるかもしれません。

■合成する pixel shader プログラム

合成に用いるピクセルシェーダープログラムは次(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枚で合成しました。

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 の『ボクと魔王』を見た時に、やり方が分からず驚愕したのを覚えています。
テクスチャーへのレンダリングや、前回のフォーカスは今回の為の前準備といっても過言ではありません。
調整が必要なので、思うとおりに使うのは難しいかもしれませんが、レンダリングの効果の一つとして使ってみてはいかがでしょうか。




もどる

imagire@gmail.com