Screen Space Ambient Occlusion : Alchemy AO

Ambient Occlusion

アンビエントオクルージョンをやってみます。

http://frederikaalund.com/a-comparative-study-of-screen-space-ambient-occlusion-methods/
http://ambientocclusion.hatenablog.com/entry/2013/11/07/152755

アンビエントオクルージョンは下の図のように,環境光と呼ばれる全ての方向から一様に降り注ぐ光が 奥まった場所や凹んだ部分では物体に遮蔽されて,暗く見えるような効果のことを呼びます。
(詳しい説明は参考URLのページに任せます)

f:id:hikita12312:20171118230106p:plain:w400

左側の凹んだ場所にある点は,右側の点とくらべて,周りの物体に光が遮蔽されて,多少暗く見えるはずです。 暗くなる度合いは,点の接平面の法線方向の半球面上で,開けている領域がどの程度存在するかの積分計算で求めることが出来ます。

この計算を3D空間で真面目にやらないで,カメラに射影された2Dの空間上でDepthテクスチャや法線テクスチャをうまく使って高速に計算する方法が Screen Space Ambient Occlusion (以下,SSAO)です。

Alchemy AO

真面目に半球面上の積分計算を行うと,とてもリアルタイムでは出来ないので,積分計算を高速に行う近似的な方法がいくつかあるのですが, 今回はAlchemy AOという手法を実装してみます。

http://casual-effects.com/research/McGuire2011AlchemyAO/index.html

Alchemy AOは注目する画素周辺にスクリーンスペースでランダムな点を打って,積分計算を近似する手法です。 ちなみに,Unity標準のポストエフェクトのアセットのAmbient Occlusionは,ソースコードを見る限りAlchemyAOっぽいです。

https://github.com/Unity-Technologies/PostProcessing

具体的な表式を見てみます。

{ \displaystyle
\begin{eqnarray}
A = \mathrm{max}\Big(0, 1-\frac{2\sigma}{N}\sum^N_{i=1}\frac{\mathrm{max}(0, {\bf v}\cdot{\bf n}+z\beta )}{ {\bf v}\cdot{\bf v} + \epsilon } \Big)
\end{eqnarray}
}


f:id:hikita12312:20171119020346p:plain:w500

注目するテクセルの法線{\bf n}と,その周辺でランダムにサンプルされた方向ベクトル{\bf v}との内積の総和を取ることが肝のようです。 max演算によって,半球の裏側はサンプルされないということになっています。 {\epsilon}は正規化時に発散しないためのパラメータでしょうか。{\beta}はよくわかりません。

{\bf v}をランダムにサンプルするためのサンプリング半径は透視変換によって決まります。 例えばDirextX系で,View空間{(x_v,y_v,z_v)}座標をクリッピング空間{(x_c,y_c,z_c)}座標に移すとして,

{ \displaystyle
\begin{eqnarray}
x_c &=& \frac{r_{\rm aspect}}{\tan{(\theta_{\rm fovY}/2)}} \frac{x_v} {z_v} \\\
y_c &=& \frac{1}{\tan{(\theta_{\rm fovY}/2)}} \frac{y_v} {z_v}
\end{eqnarray}
}

これより,{x_v,y_v}をView空間でのサンプリング半径{r}と置き換えれば,カメラから遠い場所ほどサンプリング半径が小さくなるよう決められます。

実際にAlchemy AOを実装してみます。

f:id:hikita12312:20171119230458p:plain:w300
元画像
f:id:hikita12312:20171119230512p:plain:w300
元画像法線

この元画像に従って,{\sigma=0.3,r=0.2,N=16,\beta=0.001,\epsilon=0.001}のパラメータでAO値を計算した結果です。 ちなみに1280x720サイズの元画像に対して,1/2サイズの640x360サイズでAOを作っています。

f:id:hikita12312:20171119230829p:plain:w400

溝っぽい場所が暗くなっています。 GPU実行時間は,GTX970の環境で0.51ms程度でした。

Bilateral Blur

上の画像のAO結果にはノイズが残っているので,そのまま元の画像に乗算合成してしまうと汚い画面になってしまいます。 単純にキレイにするならば,サンプリング数を増やせば良いのですが16サンプル程度でも0.5msかかっている以上,サンプリング数を増やすのは難しそうです。 そこで,ノイズ除去の為にブラーを適用します。

単にガウスブラーを適用すると,ノイズは除去されるのですが,下の画像のように画面全体がボケてしまします。

f:id:hikita12312:20171119231456p:plain:w400

三次元の構造から、エッジを保ったままノイズを除去するために,Bilateral Blurを利用します。

Bilateral Blurの基本的な表式は,1次元の場合は以下のようなものです

{ \displaystyle
\begin{eqnarray}
C'(x_i) = \frac{1}{Z} \sum_j C(x_i) w(x_i, x_j) e^{-\frac{(x_i-x_j)^2}{2\sigma^2}} 
\end{eqnarray}
}

位置{x}の色{C(x)}に対して,標準偏差{\sigma}を利用して畳み込みを行うことでブラーをかけた色{C'(x)}を得ます。 {Z}は正規化のための分配関数です。 重みの項{W}が存在しなければ,ガウスブラーと一緒ですが,この{W}を工夫することで,エッジを保ったままブラーを適用します。

今回利用したBilateral Blurの重み{W}も加えた計算式は以下のようなものです。

{ \displaystyle
\begin{eqnarray}
C'(x_i) = \frac{1}{Z} \sum_j C(x_i) \Big(\frac{{\bf n}_i \cdot {\bf n}_j + 1}{2}\Big)^a \Big(\frac{1}{|d_i-d_j|+\epsilon_d}\Big)^b e^{-\frac{(x_i-x_j)^2}{2\sigma^2}} 
\end{eqnarray}
}

法線{\bf n}とDepth値{d}を利用することで,急激に法線や深度が変化する部分をサンプルしないようにしています。 {(\frac{{\bf n}_i \cdot {\bf n}_j + 1}{2})^a}の項は法線が似た方向を持っていなければ0になるような値で, {(\frac{1}{|d_i-d_j|+\epsilon_d})^b}の項はDepthが近い値ほど大きな値になる重みです。Depth値{d}は線形化したものではなく,非線形なDepthをそのまま利用しています。

この重みの付け方は以下の資料のBirateral Upsamplingの重みを参考にしました

http://developer.amd.com/wordpress/media/2012/10/ShopfMixedResolutionRendering.pdf

{a=32,b=0.25}を利用して,実際にBilateral Blurをかけた結果です。

f:id:hikita12312:20171119233718p:plain:w400
f:id:hikita12312:20171119231456p:plain:w300
Gauss Blur
f:id:hikita12312:20171119233718p:plain:w300
Bilateral Blur

エッジを残しつつ,ノイズを除去することができました。
GPU実行時間は,GTX970の環境で0.2ms程度でした。

ブラーをかけた結果を元の画像に乗算合成した結果が以下のものです。

f:id:hikita12312:20171119234212p:plain:w600
f:id:hikita12312:20171119230458p:plain:w300
元画像
f:id:hikita12312:20171119234212p:plain:w300
SSAO後

ウサギとウサギの間のくぼんでいるところがちゃんと暗くなっています。 画面左側の金色のウサギの隣のウサギとの間の暗がりがわかりやすいです。 全部合わせてコミコミで1280x720サイズのテクスチャの1/2バッファで1.0ms以下の負荷でAOがかけられました。

本当はBilateral Blurの重みの参考にした資料で説明されている, Bilateral Upsamplingと呼ばれるアップサンプリングで 1/2の縮小バッファで作成されたSSAO結果を合成するべきなのですが, ただのBilinearフィルタでのアップサンプルでも, そんなにアーティファクトが目立たないような気がしたので未実装です。

Compute Shader での実装

今までの実装はPixel Shaderを利用したものでしたが,試しにCompute Shaderでも実装してみました。

f:id:hikita12312:20171119235340p:plain:w600
f:id:hikita12312:20171119235402p:plain:w600

AlchemyAOを実装する際に,サンプリング時に毎回Depthテクスチャのフェッチが必要になるのですが, ComputeShader版では,32x32のテクセルにスレッドを割当て,32x32サイズのブロックの周囲に64x64のブロックを4つ作って,Depth値を先にキャッシュしています。 実質4テクセルフェッチで半径48ピクセル範囲の計算を行えるようにすることで,高速化を図っています。

64サンプリングでPixelShader版とComputeShader版を比べてみると,1280x720サイズの1/2縮小バッファで,

  • PS版 : 2.03ms
  • CS版 : 0.47ms

という結果でした。Compute Shader版のほうが4倍近く速いです。 しかし,キャッシュを取る関係上,キャッシュのサイズ以上のサンプリング半径の部分は不正確になり,ブロックノイズが生じている気がします。 もう少し調整の余地がありそうです。

もろもろのソースコードは以下。