2022年7月30日土曜日

Depth Of Field (被写界深度)

影ができたらポストプロセスエフェクトをやってみようと思っていた。
その中でも気になってたのが被写界深度というもの。

例のごとく某書籍でやり方をみると、その前の章で作ってきたものを利用して作るのですぐには取りかかれなさそう。

まず前回やったSSAOの結果に掛けていたブラーの処理。
参考にしたサイトの処理では単純にループで指定した回数×回数の範囲の平均を取るだけだった。
書籍の方ではガウシアンブラーという、中心から重みを付けてぼかしていく方法でなんとなくこっちの方が良さそう。
すでに用意したブラー処理は回数を指定すると範囲を広げることができるんだけど重たくなるので3~5の範囲でしか使わないと思う。
書籍の方の処理はfor文の2重ループではなく、展開した形で実装されていた。
シェーダの最適化とか、[unroll]指定で同じようにはなるんだろうけど5×5の固定で真似してみた。

次に、元画像の横幅半分のテクスチャを用意して、ミップマップみたいに1/4の画像を階層的に作る処理。
書き込む際に上記で作ったブラー処理を掛けながら出力する。

縮小テクスチャ

書籍では8段階で縮小画像を作っていたが、こんなに必要ない。
例えば元が1024だった場合、8回分割すると最後は4×4。
最低でも64×64だろうと思い、とりあえずlog2( width )-2をループ回数に設定した。

最後に縮小テクスチャを使って、最終的な画像を出力するんだけどぼかす対象とぼかさない対象を決める方法を某書籍では深度バッファを使って、中心のZとの差にしていた。
その差は微々たるものなので、powで拡張して最後に8を掛ける。
その結果の整数部分を縮小画像のインデックスになる。

書籍の通りに実装して動かしてみるも実行時エラーが発生する。
落ちているのは最終的に出来たテクスチャをレンダリングしている箇所なんだけど、今作った部分を外して代わりに縮小テクスチャを表示する分には問題は起きない。

処理をよく見ると気持ち悪いfor文があり、このfor文の最後のif文に問題があった。

	for (int i = 1; i <= 8; ++i) {
		if (i - no < 0) continue;
		retColor[i-no]= Get5x5GaussianBlur(texShrink, smp, input.uv*uvSize + uvOfst, dx, dy, float4(uvOfst, uvOfst + uvSize));
		uvOfst.y += uvSize.y;
		uvSize *= 0.5f;
		if (i - no > 1) break;
	}

某書籍のソースはこんな感じになっておりfor直後のif文で、iがnoと同じになるまで空ループする。中に入るようになったら、i-noが0,1,2で周ることになる。ただし、retColorの配列は2で定義されているので落ちているっぽい。
ここでやりたかったことはおそらく、noに当たる段階の画像を決めて、その画像とその次の画像を使ってぼかすということなんだけど、この処理だと常に1番目の画像と2番目の画像しか使ってない。最初のif文で空ループしてしまってるので、uvを参照するオフセットが動かないから。8個も画像準備してるのに意味ない・・・。
やりたいことが理解できたので、ループをやめて直接計算するようにした。

被写界深度

一応それっぽく表示された。
周りのボケ感がもっとほしいのでZとの差を計算している部分を調整してみた。

被写界深度 パラメータ調整版

一番離れているところが4番目、5番目ぐらいの画像を使うようにしてみたら、ボケはすごくなったけど、下の影にアーティファクトが発生。

中心もボケてる

更に中心のオブジェクトもボケてしまっている。

小さすぎる画像は使い物にならないので画像の分割数はもっと少なくても良さそう。
1024で4、4096で6になるようにこうした。

	this->nLoop = fMax( 4, fMin( 6, tI4( ::log2( oSize.width ))-6 )) ;	// 1024で4段階 4096で6段階

差の計算も8掛けているのはやめて差が大きいほど指数的に増えるように調整した。

被写界深度 再調整版

アーティファクトが出るレベルで差が激しいパラメータでも、中心のオブジェクトはそんなにボケなくなった。

でもZ面が同じようなオブジェクトの配置であればこれで大丈夫そうだけど、奥行きのある物体でこれをやるとぶれてしまうと思う。フォーカスを当てるオブジェクトだけくっきりして、周りがボケてる感じにしたい。
そこで考えたのが、対象オブジェクトだけの深度バッファを用意すればそこだけマスクできるのではないかと。


被写界深度 マスク版

パラメータ調整前に中心のオブジェクトもぶれてしまう状態のシェーダでマスクを適用させてみたけど、うまくいってる。


被写界深度 マスク反転

ちなみに反転させると対象のみブレさせてモザイク掛けてるみたいにもできる。

	tStr( LR"---(
	#define MASK %d
	#define LOOP_COUNT %d
		float2 texel = float2( 1.0f / %ff, 1.0f / %ff ) ;
		float d = abs( TexDepth.Sample( Sampler, float2( 0.5, 0.5 )).r - TexDepth.Sample( Sampler, In.UV ).r ) ;
		d = pow( 1.0f + d, %ff ) - 1.0f ;
	#if MASK
		d = TexMask.Sample( Sampler, In.UV ).r < 1.0f ? 0.0f : d ;
	#endif
		int no ;
		d = modf( d, no ) ;
		no = min( no, LOOP_COUNT-1 ) ;
		float2 size = float2( 1, 0.5 ) ;
		float2 osf = float2( 0, 0 ) ;
		float4 col[ 2 ] ;
		if( no == 0 ) {
			col[ 0 ] = Tex.Sample( Sampler, In.UV ) ;
			col[ 1 ] = GetColorBlur( TexShrink, Sampler, In.UV * size + osf, texel.x, texel.y, float4( osf, osf + size )) ;
		} else {
			size *= pow( 0.5f, no-1 ) ;
			osf.y = ( pow( 0.5f, no )-1 ) / ( 0.5f - 1.0f ) - 1.0f ;
			col[ 0 ] = GetColorBlur( TexShrink, Sampler, In.UV * size + osf, texel.x, texel.y, float4( osf, osf + size )) ;
			osf.y += size.y ;
			size *= 0.5f ;
			col[ 1 ] = GetColorBlur( TexShrink, Sampler, In.UV * size + osf, texel.x, texel.y, float4( osf, osf + size )) ;
		}
		Out.Col = lerp( col[0], col[1], d ) ;
	)---"_fs, bMask ? 1 : 0, this->nLoop, oSize.width, oSize.height, nPow )

最終的なシェーダはこれ。
マスクはオプションで、使う場合は深度バッファを別途用意する。

3人バージョン

比較のためにZ座標を変えたおっさんを2体追加してみたけど、なんか思ってたのと違う。
素材が悪いんだろうけど、きれいなレベルで抑えるとボケが足りない気もするし、まだ改善の余地がたくさんある。書籍にも「簡易的な被写界深度の実装」と書かれているので、これから改良を加えていこう。

0 件のコメント:

コメントを投稿