2022年7月28日木曜日

Screen Space Ambient Occlusion(SSAO)

いままでの影つながりでスクリーンスペースアンビエントオクルージョンをやってみることにした。

某書籍のチャプター15でやり方が載っているが、いまいちこの本の信頼性が薄れているのでネットで探すことにした。
DeferredRenderingがうまくいくきっかけとなったサイトにSSAOの記事とソースも公開されていたのでこれを参考にやっていこうと思う。

シェーダ内でランダムなデータが必要になるようで、CPU側でこれの準備をする。
シェーダにわたす際、サンプルでは2つのデータをそれぞれ違う方法で渡していた。
1つはStructuredBuffer<float4>という定義でテクスチャとして渡す方法。
もう1つはTexture2D<float4>でテクスチャとして渡す方法。

1つ目の渡し方は自分のライブラリでは未実装だったのでどういったものか調べてみると、定数バッファと同じデータの書き込み方で、シェーダ内では配列として扱えるものだった。
現状メッシュのボーンは255決め打ちで配列を用意して必要分だけ書き込んでいるが、StructuredBufferの方法なら要素数は別途定数バッファで渡す必要はあるが可変個扱えるのは良さそう。
今回の実装は定数バッファで行って、そのうち使えるようにしようと思う。

もう1つは通常テクスチャと同じだけど、サイズが16×16と小さいデータ。
これは自分のライブラリでテクスチャの実装をした際出来ないと結論付けたもので、どうしても64×64以下のデータは作れなかった。MMDの実装をした時、Toonレンダリング用のテクスチャとか、やたらと小さいテクスチャばかりだったので、読み込んだ画像が64×64以下だった場合は、一旦GDI+で拡大してからテクスチャに渡すようにしていた。
もしこのソースでやり方がわかるならラッキーと思い、ソースを調べていったけどどう見てもCPU側で作ったノイズデータを使ってない。
通常GPUに渡すために2つのリソースを用意する。
1つはD3D12_HEAP_TYPE_UPLOADで、もう一つはD3D12_HEAP_TYPE_DEFAULT。一旦UPLOADの方にMapして書き込んで、UPLOADからDEFAULTにCopyTextureRegionでコピーするというのが手順だけど、サンプルではCopyTextureRegionを呼んでいない。
64x64以下のデータで関数を呼び出すとエラーが出てしまいコピー出来ないんだけどどういうことなんだろう?

実際に動かしてみてPIXでデバッグしてみるとやっぱりノイズテクスチャは真っ黒(全部0)で、データが入っていない。シェーダでテクスチャを参照している箇所を固定値に変えても結果は同じだった。この作者は気づいてないのだろうか?

この部分は残念だったけど、ノイズデータは不要とわかったので1つの定数バッファだけで実装してみた。
結果は予想していたものとは違って変な状態。
ソースを見比べても同じようにしたつもりだけどうまくいかない。
PIXでデバッグしてみると、色んなところで値がinfやnanになっている。
サンプルのプログラムも確認してみると、結構infになっている。でもちゃんと表示されている。

サンプルのデバッグ


あと、PIXで実行時間が表示されるが、全体が19.39msかかっていて、SSAOが13.01msとなってた。

SSAOのパフォーマンス

レンダリングのサイズが1920×1080なので、いまテストで動かしている1024×1024よりは大きいけど、ちょっとかかりすぎのような気がする。
SSAOの記事を探している時、SSAOにはいろいろな問題があってそれぞれの問題をこうやって対処したみたいな感じに書かれていたので、期待していたんだけど一気に採用見合わせ。

仕方ないので、某書籍の実装でやってみることにした。
こっちの方はシェーダ内でランダム計算をしていたがその部分は実装済みのデータを利用するようにして修正。

	const int3 puv = int3( In.Pos.xy, 0 ) ;
	const float dp = TexDepth.Load( puv ).r ;
	float4 pos = DepthToPos( dp, In.UV, gCam.InversePV ) ;
	pos.xyz /= pos.w ;

	float3 normal ;
	DeferredDecode( TexNormal.Load( puv ), normal ) ;

	float div = 0.0f ;
	float ao = 0.0f ;
	if( dp < 1.0f ) {
		for( uint i = 0 ; i < gSsao.SampleCount ; i++ ) {
			float3 omega = gSsao.SampleKernel[ i ].xyz ;
			float dt = dot( normal, omega ) ;
			float s = dt < 0.0f ? -1 : 1 ;
			omega *= s ;
			float4 rpos = mul( gCam.TransPV, float4( pos.xyz + omega * gSsao.Radius, 1 )) ;
			rpos.xyz /= rpos.w ;

			const int3 ruv = int3(( rpos.x + 1.0f ) * gSsao.Width * 0.5f, ( 1.0f - rpos.y ) * gSsao.Height * 0.5f, 0 ) ;
			const bool IsOutside = ( ruv.x < 0.0f ) || ( ruv.x > gSsao.Width ) || ( ruv.y < 0.0f ) || ( ruv.y > gSsao.Height ) ;
			if( !IsOutside ) {
				dt *= s ;
				div += dt ;
				float z = TexDepth.Load( ruv ).r ;
				ao += step( z, rpos.z ) * dt ;
			}
		}
		ao /= div ;
	}
	Out.AO = saturate( pow( 1.0f - ao, gSsao.SsaoPower )) ;
	return Out ;

サンプルと某書籍のコードをミックスしてみた。
某書籍にない処理は2箇所。
IsOutSideは画面外の影も画面端に反映されてしまうのを防ぐためのもの(だけど、半径が大きいと消しきれない)
最後のpowで影の強さを調整。
参考にした2つのシェーダソースとも、Zから位置を取得するのにプロジェクションの逆行列を使っている。だけど、DeferredRenderingではプロジェクションとビューの逆行列を使った。両方とも試したけど、若干の違いがあるものの同じような結果になった。別途用意しないと行けないので、カメラに持っているPVをそのまま利用することにする。誰かプロジェクションの逆行列使わないとここがまずいぞって教えてくれないかな?

実行してみるとそれっぽいのが表示された。
SSAO 半径10 試行回数32

半径10は元のサンプルのをそのまま使ってたから。
大きすぎるので調整したのがこれ。

SSAO 半径0.25 試行回数16

一般の記事に出てくる感じの結果にはなった。
この半径って、配置する物体のスケールに合わせる必要がありそうなので、作るもののサイズが決まってから調整していく感じかな。

SSAO 半径5 試行回数128 

試行回数を増やせば半径が大きくてもそれっぽくなるみたい。
半径が小さくて、試行回数が少ないとオブジェクト自体に影がかかっておかしな結果になる。

次に、試行回数を少なくするためにブラーを掛ける処理を実装。
某書籍では試行回数を256回にしていてブラーは掛けていなかったけど、処理速度的に問題だろう。調べた感じだと、試行回数を減らして代わりにブラーを掛けるのがいいらしい。

ブラーの処理は簡単でSSAOの結果を受け取って、周辺のドットを足して平均を取るだけ。

#define BLUR_SIZE 3
	float w ;
	float h ;
	Tex.GetDimensions( w, h ) ;
	const float2 texel = 1.0f / float2( w, h ) ;
	float result = 0.0f ;
	const float b = float( BLUR_SIZE ) * -0.5f + 0.5f ;
	const float2 bo = float2( b, b ) ;
	for( int i = 0 ; i < BLUR_SIZE ; i++ ) {
		for( int j = 0 ; j < BLUR_SIZE ; j++ ) {
			const float2 offset = ( bo + float2( i, j )) * texel ;
			result += Tex.Sample( Sampler, In.UV + offset ).r ;
		}
	}
	Out.Blur = result / float( BLUR_SIZE * BLUR_SIZE ) ;

定数バッファもなし。
実行時に変更可能でないものはマクロで指定している。
シェーダソースを実行時にコンパイルしているので、上記の場合は「BLUR_SIZE %d」として、展開している。
w,hに関して、SSAOの方では他に渡すものもあって定数バッファで渡しているけど、こっちではテクスチャのGetDimensionsで取得している。
試しにGetDimensionsと固定値の処理時間を測ってみたら、GetDimensionsは452.45us、固定値は398.39usだった。
単位がマイクロ秒なので大したことない気もするけど、1024×1024のピクセルシェーダで約50us余計にかかるみたいなので、固定値で展開するようにしようか。

SSAO+Blur

このSSAOだとあまり違いがわからないけど、3×3のブラーを掛けた結果。

ここまでこの結果を普通のテクスチャに出力していたけど、影と同じでデータは1つで良いのでテクスチャをフォーマットをDXGI_FORMAT_R8_UNORMにした。これでサイズは1/4になる。

で、合成したのがこれ。

SSAO合成結果

あんまり感動がない。
これをやると「質感が上がってスゲー」を期待していたんだけど、処理速度気にしてしょぼいパラメータにしているからか?
処理速度は全体が3.4msで、SSAOが1.98msで全体の60%ぐらい使ってこれだとなくてもいいんじゃないかと思うレベル。

SSAO 半径2 試行回数128

試行回数を128回にして半径も広げた結果、不自然感はあまり変わらない。もっといい感じの場面じゃないとだめなのかもしれない。
ちなみに処理時間はギリギリ60FPSを保って15.5msだったけど、SSAOの処理は14.24ms掛かっていた。


0 件のコメント:

コメントを投稿