今回は、被写界深度です。
下の絵を見てください。中心のクルマははっきりと見えていますが、手前のクルマはボケています。また、背景の遠くのほうもボケています。
深度を見てピントを合わせるのが、被写界深度です。
今回のソースは、次のものです。
内容は次のとおりになっています。
| 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 の『ボクと魔王』を見た時に、やり方が分からず驚愕したのを覚えています。
テクスチャーへのレンダリングや、前回のフォーカスは今回の為の前準備といっても過言ではありません。
調整が必要なので、思うとおりに使うのは難しいかもしれませんが、レンダリングの効果の一つとして使ってみてはいかがでしょうか。