目的
大量のParticle等の半透明オブジェクトをレンダリングする際に、1/4×1/4サイズの、超低解像度のRTにAlphaBlending描画を行い、それをオリジナルの解像度にCompsiteする。
参照
クリックしてTatarchuk-Destiny-SIGGRAPH2013.pdfにアクセス
P125-137
上記参照のUber-Low-Res Particles概要
まず、Quater-Resの不透明Depthを用意。Depth値は、4×4ピクセルの中で、カメラから一番遠い値を採用。このDepthBufferは、AlphaBlendedのレンダリングで、深度テストの値として使用。加えて、AlphaBlend描画用のQuater-ResのRGBAバッファと、RG16Fのdepth transitionバッファを用意。depth transitionバッファには、深度の中央値と、分散値を格納。
値の具体的な計算方法については、説明されていないが、depthでソートする代わりにvarianceでParticleをソートすると記されている箇所から、Particleのvarianceは事前計算されるものと推定される。分散が大きくなるケースで、分散値のClampを行うなどの工夫を施している。
P134-P137のスクリーンショットでは、不透明と半透明が交差していないので、実際の効果のほどは不明。
上記参照を踏まえて、Uber-Low-Res Renderingについて考察してみたいと思います。
アーティファクトの原因について
まず、Uber-Low-Res Renderingを行ううえで、問題となるケースについて考えたいと思います。
4×4の計16Pixelの不透明オブジェクトの深度値のうち、最も離れた位置を不透明の深度として使用するわけですが、このときに、16Pixel分の不透明の深度値の分布が広がっている場合に、その分布の範囲内に、半透明のレンダリングが行われると、問題になります。本来なら、各Pixelの深度値に応じて、描画/棄却されますが、1Pixelにまとめられ描画されてしまいます。これは避けられません。
しかし、もし、AlphaBlend描画の深度値を保存しておくことが出来れば、高解像度バッファへの合成の際に、深度値を比較することで、各Pixelの深度値に応じて描画/棄却する動作を再現することが可能になります。
もちろん、全てのAlphaBlend描画の深度値を保存しておくことは不可能ではないですが、それでは本末転倒です。
何らかの方法で、半透明ピクセルの深度値を近似する方法を考える必要があります。
深度の値とAlphaの値について
まず、AlphaBlendのプリミティブの深度を推定する際に、Alphaによる重み付けを考慮するべきだと思います。つまり、透明に近いプリミティブのピクセルが、深度の計算に大きな影響を与えるべきではないと思います。加えて、AlphaBlendingの演算結果は描画順序に依存しますが、算出される深度値もこの順序の影響を受けるべきだと思います。これ等を考慮して、推定深度値はAlphaBlendで描画します。
ColorバッファのAlphaには、合成されたAlphaの値が格納されているので、後ほどこの値を用いて、推定深度値を正規化することが出来ます。
AlphaBlendでピクセルが複数回描画された場合は、ピクセルのカラーは、後から描画されたピクセルに大きく影響を受けることになりますが、推定深度値も同様に後から描画したピクセルの影響を大きくうけるので、合理的だと思います。この数値を、AlphaBlendピクセルの、深度値の中央値に相当するものにします。
考察
上記で、なんとなくですが、AlphaBlendピクセルの、深度値が算出されました。
妥当性の問題などを考える前に、この状態で発生するアーティファクトを想像します。容易に想像されるのが、不透明オブジェクトと半透明オブジェクトが交差するようなケースをレンダリングした際に、低解像度の4×4のピクセルブロックが視認されることです。ここにグラデーションのようなものをつくり出せれば良いのですが、不透明バッファの深度分布より、何かを算出すれば、ブロック間の不連続性で、それ自体がアーティファクトの原因になるでしょう。
したがって、半透明バッファの描画より、一様に計算されるものを使う必要があります。
やはり、半透明のバッファを作成する際に、深度値の2次モーメント的なものを計算するほうがよさそうです。そうすると、考えられるのが、深度値の二乗を、AlphaBlendで描画することです。こうすると、実際の描画でAlphaBlend描画が多数行われ、深度分布が広がっていたとしても、最後に行われた数回の描画が、値に大きな影響を及ぼすはずです。中央値と同様に、最後に行われた数回の描画が大きく影響を与えることとなり、算出される分散値は、それほど大きなものにならないのではないかと予想されます。
試算
実際にAlphaBlendで描画した際に、どのような値の推移になるかを試算してみました。
想定するレンダリングは、平均と分散を用いるこの手法で一番問題になると思われる、同一ピクセルの深度分布が大きいケースです。初めに、カメラに遠い領域で描画を3回行い、次に、カメラに近い場所で3回描画したケースを試算します。
はじめの試算はDepth*Alpha、つまり深度値のAlphaによる加重平均と、分散を用いたものです。
D:Depth | A:Alpha | Accume (A) |
Accume (D*A) |
Mean | Accume (D^2*A) |
S^2 | SQRT(S^2) | Mean-0.3SD (Begin) |
Mean+0.7SD (End) |
1.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | |||
0.75 | 0.50 | 0.50 | 0.38 | 0.75 | 0.28 | 0.56 | 0.00 | 0.75 | 0.75 |
0.74 | 0.50 | 1.00 | 0.75 | 0.75 | 0.56 | 0.56 | 0.01 | 0.74 | 0.75 |
0.73 | 0.50 | 1.50 | 1.11 | 0.74 | 0.82 | 0.55 | 0.01 | 0.74 | 0.75 |
0.30 | 0.50 | 2.00 | 1.26 | 0.63 | 0.87 | 0.43 | 0.19 | 0.57 | 0.76 |
0.30 | 0.50 | 2.50 | 1.41 | 0.56 | 0.91 | 0.36 | 0.22 | 0.50 | 0.71 |
0.30 | 0.50 | 3.00 | 1.56 | 0.52 | 0.96 | 0.32 | 0.22 | 0.45 | 0.67 |
この描画では、後半3回で、深度0.3でAlpha0.5の描画を行っています。AlphaBlendでこれを描画した場合、深度0.3の場所で行った3回の描画が、全体の色の87%を占めることになります。しかし、深度平均値は、最終的に0.52となっています。偏差の値も、0.2付近に上昇したのち、後半3度の描画でも収束することはありませんでした。この数値をもとに、このピクセルの合成開始深度と、合成終了深度を計算すると、その深度範囲は広いままで収束しません。
次の試算は、DepthのAlphaBlendを平均値とし、Depthの2乗のAlphaBlendを分散として用いた場合です。
D:Depth | A:Alpha | Blended (A) |
Blended (D) |
Mean | Blended (D^2) |
S^2 | SQRT(S^2) | Mean-0.3SD (Begin) |
Mean+0.7SD (End) |
1.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | |||
0.75 | 0.50 | 0.50 | 0.38 | 0.75 | 0.28 | 0.56 | 0.00 | 0.75 | 0.75 |
0.74 | 0.50 | 0.75 | 0.56 | 0.74 | 0.41 | 0.55 | 0.00 | 0.74 | 0.75 |
0.73 | 0.50 | 0.88 | 0.64 | 0.74 | 0.47 | 0.54 | 0.01 | 0.73 | 0.74 |
0.30 | 0.50 | 0.94 | 0.47 | 0.50 | 0.28 | 0.30 | 0.22 | 0.44 | 0.66 |
0.30 | 0.50 | 0.97 | 0.39 | 0.40 | 0.19 | 0.19 | 0.18 | 0.34 | 0.53 |
0.30 | 0.50 | 0.98 | 0.34 | 0.35 | 0.14 | 0.14 | 0.14 | 0.31 | 0.44 |
この描画でも、上記と同じ条件で行っています。AlphaBlendを用いて深度平均値を算出しているので、後半3度の描画で急速に0.3に近づきます。偏差の値も、後半3度の描画で0.14まで下がります。この数値をもとに、このピクセルの合成開始深度と、合成終了深度を計算すると、0.3付近で行った後半の描画を中心とした分布となります。
懸念事項として残るのは、4度目の描画(深度0.3における最初の描画)では、偏差の値が0.22まで上昇するので、この状態でのピクセルの合成深度の分布は広範囲なものとなります。しかし、この時点のカラーバッファの内容は、深度0.3のピクセルの成分がちょうど50%なので、この時点で偏差が大きくなるのは、ある意味正しいとも言えます。
ピクセルの深度分布を決める、Mean-0.3SDと、Mean+0.7SDの根拠は特にありません。強いて言えば、深度ソートをしている状態で、AlphaBlendを用いて深度平均を算出すると、算出される平均値は、真の平均より浅い位置になります。ですので、平均値より浅い方向に範囲を取りすぎると、実際の描画範囲より、前方に大きく逸脱した範囲となってしまう可能性があるため、それを避ける目的で、浅いほうに0.3、深いほうに0.7としています。範囲の大きさは偏差の値としています。
Composite
高解像度バッファへの合成は、算出した合成開始深度と合成完了深度を用い、その間を適当な指数関数などで補完すれば良いのではないでしょうか。
その際の低解像度バッファからの深度平均値の参照は、Bilinearフィルタリングが妥当に使用できますが、分散値は、Bilinearフィルタリングを使用すると、未描画ピクセルと描画ピクセルをフィルタリングする際に、値が低下することになり、アーティファクトになるかもしれません。
従って、Uber-Low-Resバッファへの描画が完了したら、深度平均値と分散値より、合成開始深度と合成終了深度を算出し、別バッファに格納後、高解像度バッファへの合成を行う必要があるかもしれません。
まとめ
上記試算を行ってわかったことは、やはりこのレンダリングには向いているものと、向いていないものがあるということです。向いているものは、煙や爆発、大量の飛沫等の、ある程度の位置で集中し、しかし、ぼんやりと存在し、個々のAlpha値が比較的低いものです。
反対に向いていないものは、魔法のエフェクトなどの1枚のポリゴンで表現される半透明オブジェクトです。これ等のオブジェクトは、不透明オブジェクトと交差するケースが多く、また、その存在深度も明確でないとなりません。このようなものには向いていません。
レンダリングする対象物を考慮して、この技法の導入を検討する必要があります。
余談
(ここからは完全に余談です。)
参照のDestinyのスライドで、半透明バッファの初期値が(0,0,0,1)と記述されていました。何でかなと調べた結果が以下の通りです。初期値が(0,0,0,0)で行ったレンダリング場合と、BlendOpは違いますが、それ以外に大きな違いは見出せませんでしたが、参考までに。
従来のAlphaBlending
blend(source, dest) = (source.rgb * source.a) + (dest.rgb * (1 – source.a))
Pre-MultipliedのAlphaBlendingを使う場合
blend(source, dest) = source.rgb + (dest.rgb * (1 – source.a))
Pre-MulのCompositeについて
RT(0,0,0,0)で開始する場合
Render tex1:
RT1.rgb = (tex1.rgb) + ((1-tex1.a) * (0,0,0))
RT1.a = (tex1.a) + ((1-tex1.a) * 0)
Render tex2:
RT2.rgb = (tex2.rgb) + ((1-tex2.a) * RT1.rgb)
= (tex2.rgb) + ((1-tex2.a) * tex1.rgb)
RT2.a = (tex2.a) + ((1-tex2.a) * (RT1.a))
= (tex2.a) + ((1-tex2.a) * (tex1.a))
FBにComposite:
FB.rgb = (RT2.rgb) + ((1 – RT2.a) * FB.rgb)
= (tex2.rgb) + ((1-tex2.a) * tex1.rgb) + ((1-((tex2.a) + ((1-tex2.a) * (tex1.a)))) * FB.rgb)
= (tex2.rgb) + ((1-tex2.a) * tex1.rgb) + (((1-tex2.a) * (1-tex1.a)) * FB.rgb)
RT(0,0,0,1)で開始する場合
Render tex1:
RT1.rgb = (tex1.rgb) + ((1-tex1.a) * (0,0,0))
RT1.a = 1 * (1-tex1.a)
Render tex2:
RT2.rgb = (tex2.rgb) + ((1-tex2.a) * RT1.rgb)
= (tex2.rgb) + ((1-tex2.a) * tex1.rgb)
RT2.a = RT1.a * (1-tex2.a)
FBにComposite:
FB.rgb = (RT2.rgb) + (RT2.a * FB.rgb)
= (tex2.rgb) + ((1-tex2.a) * tex1.rgb) + (((1-tex2.a) * (1-tex1.a)) * FB.rgb)
こちらでは初めまして。
>半透明バッファの初期値が(0,0,0,1)
これなんですが、自分もたまたま同じでした。
理由としては、風景の見え方に対して煙のトランスミッタンス的な考え方で、
乗算で見えなくなっていくのをやりたかったためです。
もしかしたら Destiny 開発チームの理由も一緒かもしれません。
で、そのうえで従来のアルファブレンドの式で同じ結果になる、というのが
とても勉強になりました。ありがとうございます。
早速プログラム、書き換えました。笑
コメントありがとうございます!
なるほどそうした捉え方で考えると合点がいきますね。