2022年7月19日火曜日

Deferred Shadowmap

前回シャドウマップができたけど、これって1シーンに複数のメッシュがある場合はまず深度バッファに全部分書き込んで、各メッシュを描画する度に影の処理をする必要がある。

某書籍でディファードレンダリングという言葉を目にした。
色々調べていくと、どうやら通常のレンダリングのことをフォワードレンダリングというのに対して、ディファードレンダリングというその名の通り遅延して後からレンダリングする方法で、ライトを大量に扱える仕組みらしい。
ライトはまだ良くわからないけど、なんとなく影と同じじゃないかと思った。
あるメッシュが作る影が別のメッシュに影響を与えるから、メッシュをレンダリングする度に影の考慮も必要になる。
もしかするとディファードレンダリングを使えば、各メッシュをレンダリングするときは影は考慮せずレンダリングして、最後に1回だけ影描画すれば行けるんじゃないかと。

某書籍を順を追って実装していた時、マルチレンダーターゲットでピクセルシェーダから色と法線に分けて出力して、そのあとそれぞれをテクスチャとして受け取ってディフューズの計算をしてレンダリング出力することはやった。
シャドウマップで必要なのは、色、法線に加えて、頂点の位置を影の行列で変形した位置情報が必要なのでそれを出力してみた。

ピクセルシェーダ
	Out.Col = In.Col ;			// 色
	Out.Normal = In.Normal ;	// 法線
	Out.SPos = In.SPos ;		// 影空間の位置


ディファードレンダリングのシャドウマップ

なんかそれっぽく描かれたけどおかしなところがたくさんある。
まず気づいたのが法線。
立体物の面が黒くなってディフューズの計算がうまく行ってない。
前にやったときはうまく行ってたはずなので本を読み返してみるとちょっと違っていた。


法線テクスチャ出力時
	Out.Normal = float4( In.Normal.xyz * 0.5 + 0.5, 1.0 ) ;
最初何をしているかわからなかったんだけど、法線の値は-1~1の範囲なので0.5を掛けて-0.5~0.5の範囲にして、0.5を足すことにより、0~1の範囲におさめているっぽい。
この変換をしてなかったときはマイナスのデータが消えてしまっている。

法線テクスチャ参照時
	float3 Normal = TexNormal.Sample( Sampler, In.UV ).xyz * 2.0 - 1.0 ;
参照時は0~1に変換されたデータを、2を掛けて0~2にして、1を引くことで-1~1の範囲に復元している。

ディファードレンダリング 法線修正版

ディフューズの計算はうまく行ったみたいだけど、影が足りない。
何がどうなってるかわからないので、ネットで調べているとUVのグリッドを描画することで確認をする方法を見つけた。

フォワードレンダリング グリッド表示

ディファードレンダリング グリッド表示

フォワードレンダリングではUVのグリッドが全体をカバーしているが、ディファードレンダリングのUVは右上だけで左下の範囲がない感じだ。
このUVの計算は、SPosのテクスチャを利用している。
もしかして、法線みたいにマイナスの値がだめなのかもしれない。
いろいろ調べていくと、位置のVectorはwで割ることにより-1~1の範囲に収められるらしいことがわかった。

位置テクスチャ出力時
	Out.SPos = float4(( In.SPos.xyz / In.SPos.w ) * float3( 0.5, -0.5, 0.5 ) + 0.5, 1.0 ) ;
出力時、xyz / wで-1~1の範囲に変換して、xyは送り込んだ先でuvとしてそのまま使うため、0.5を掛けて0.5を足すことで0~1の範囲に変換する。
y座標に関しては上が大きくて下が小さい。
v座標は上が0で、下が1なので逆にするために-0.5を掛けている。
zはxと同じなのでここでは0~1の範囲に収まっている。

位置テクスチャ参照時
	float3 Shadow = TexSPos.Sample( Sampler, In.UV ).xyz ;
    float z = Shadow.z * 2.0 - 1.0 ;
    float2 ShadowUV = Shadow.xy ;
参照時は、xyは変換無しでそのままをUVとして利用、zは2を掛けて1を引くことで、-1~1の範囲に変換している。

ディファードレンダリング 位置修正版

位置情報を補正した結果、UVの範囲が全体になって影が表示されるようになった。
精度があらすぎてものすごい分厚いシャドウアクネ出てるので、それも補正しておく

ディファードレンダリング バイアス補正版

フォワードシェーディングでのzのバイアスは0.001だったのに対して、ディファードレンダリングのバイアスは0.01で10倍。
影の精度もめちゃくちゃ悪く、フォワードレンダリングよりも粗すぎてこの状態では使い物にならない。精度の粗さで本来1本のグリッドが2つに分かれて広がっているんだろう。
ネットで情報を集めつつ試行錯誤を1週間続けたが解決できなかった。
ディファードレンダリングは諦めて、フォワードレンダリングで頑張っていこうと決意した矢先、気になるページを見つけた。

ここには、zの座標から元の位置を算出する方法が書かれていた。
元の座標がわかれば影描画につかった行列を掛けて直接SPosを作り出せるはず。
そしたらもっと精度が良くなるかも?という淡い期待を胸に実装してみるが結果はだめ。
UVのGridも全く描画されなかったり斜めに1本だけ書かれたりで全くうまくいかない。

そもそも法線や、位置のデータはなんで直接そのまま渡せないんだろう?
よく考えればわかることだったが、なぜかそこまで考えが至らなかった。
きっとVSからPSへは思った通りにデータが受け渡されているから余計に勘違いしやすかった。

ピクセルシェーダのアウトプットはfloat4となっているので、単純にfloat4つ分のデータが出力されていると勘違いしていたが、用意しているテクスチャのフォーマットがDXGI_FORMAT_R8G8B8A8_UNORMなので、実際はunsigned intの4バイトだろう。各rgbaは0~255までの範囲のデータでしかない。だから0~1の範囲に収めてやるとテクスチャ出力時には0~255に変換され、そのテクスチャを読み込む際は、自分で-1~1に復元しないといけなかった。
(DXGI_FORMAT_R16G16B16A16_UNORMにしたり、DXGI_FORMAT_R32G32B32A32_FLOATにすれば精度は上がるんだろうけど、その分サイズが4倍、16倍になるので現実的ではなくなる気がする)
	float4 DepthToPos(
		in		float		z,		// Depth
		in		float2		uv,		// UV
		in		float4x4	ipv		// Inverse Proj * View
	) {
		float4 p = float4( uv.x * 2.0f - 1.0f, ( 1.0f - uv.y ) * 2.0f - 1.0f, z, 1.0f ) ;
		p = mul( ipv, p ) ;
		p.xyz = p.xyz / p.w ;
		p.w = 1.0f ;
		return p ;
	}
この関数のzにG-Buffer経由の位置情報のZを渡していたが、精度が圧倒的に足りてないと気づいたので、32bitをZに全振りしてる深度バッファの値を渡してみた結果・・・

ディファードレンダリング 深度バッファ版

ついに、フォワードシェーディングと同じくらいの精度でディファードレンダリングでもシャドウマップがレンダリングできた。

ディファードレンダリング 複数メッシュ

最初に言ったディファードレンダリングなら、影の深度バッファを作ってしまえばそれぞれのメッシュを描画する際は影のことは一切考えなくても大丈夫というのは本当だった。
マップとおっさんを同時に描画して、影は最後に付けているけど、マップの影はおっさんにかかっているし、おっさんの影も自分自身とマップにも反映されている。

Diferred Shadowのキーワードで探しまくったが出てくるのはOpenGLやUnityの情報ばかりでDirectXの情報はかなり少なかった。途中ディファードレンダリングでは影は扱えないのか?と不安になりつつも情報を集め続けた結果、意味の分からなかった計算もなんとなくわかったし、良かった・・・。

0 件のコメント:

コメントを投稿