ブルームの実装

ブルーム

前回,HDRテクスチャのトーンマッピングをしたので, よりHDRテクスチャを使っている感を出すためにブルームを実装します。

ブルームとは光が周辺のピクセルに滲み出るようなエフェクトの事を呼びます。詳しくはUnityの説明にでも。
https://docs.unity3d.com/jp/540/Manual/script-Bloom.html
現実のカメラ的には,ブルームはレンズ内部の光の乱反射とかが原因で起こるものだそうです。

ブルームの実装方法の概略は以下の通りです。

  1. ソースとなるHDRテクスチャからブルーム効果の発生する高輝度成分ピクセルを抜き出したHDRテクスチャを作る
  2. 高輝度テクスチャをブルームの種として,ガウスフィルターをかける
  3. ガウスフィルターによってボケた高輝度テクスチャを元のHDRテクスチャに書き戻す

簡単そうですね。

高輝度成分の抜き出し

こんな感じのシェーダーを書きました。定数バッファとかの定義は割愛。

float4 PS_HighLuminanceFilter(PPFX_OUT In) : SV_TARGET0
{
    float3 src = g_SrcTexture.SampleLevel(g_LinearSampler, In.UV.xy, 0.0f).rgb;
    float luminance = GetLuminance(src.rgb);
    return float4(src.rgb * max(0.0f, (luminance - HIGH_LUM_THRESHOLD)) * EXPOSURE, 0.0f);
}

注目するべきは,トーンマッピング時の露光(Exposure)をマスク後に乗算しているところ。
露光によってブルームエフェクトの滲み出しのサイズは変わるべきなので,ブラーを掛ける前に露光乗算する必要があります。

高輝度成分抜き出し時に輝度とソースカラーを乗算するような形になっていますが,輝度にブルームの量を比例させたかっただけです。
単純に高輝度以外は0でマスクするとか,いろいろ方法はあると思います。

で,抜き出された高輝度テクスチャが以下のものです。 f:id:hikita12312:20170820164458p:plain

高輝度抜き出しの閾値(HIGH_LUM_THRESHOLD)は0にしています。
左がソースとなるHDRテクスチャ,右がブラーの種となる高輝度成分テクスチャです。

f:id:hikita12312:20170813031754p:plain:w300 f:id:hikita12312:20170820164458p:plain:w300

現実世界のレンズ内部の光の乱反射には,ステップ関数的な閾値は無い気がしたので,閾値は結局0にしました。

ガウスブラー

いわゆるガウス関数を使って,注目するピクセルの周囲のピクセルを畳み込みます。

{ \displaystyle
f(x,y) = \frac{1}{Z}\exp{\Bigl(-\frac{x^2+y^2}{2\sigma^2}\Bigr)} 
}

{Z}は正規化因子です。畳み込むカーネルサイズの重みを全部足して1になるように規格化します。
ちなみにガウス関数x,yで分離して別々に積分できるので,X軸方向への1次元ガウスブラーをかけた後に, Y軸方向への1次元ガウスブラーをかければ,テクセルフェッチの数を減らしつつ2次元のガウスブラーと同じ結果を得られます。
昔に学校で証明をやった気がしますが忘れました。

f:id:hikita12312:20170820174025p:plain:w300

図はX軸方向に5x5のガウスブラーのカーネルをかけるときの重みの模式図です。
各テクセルでフェッチしてこの重みを掛けて足して,同じことをY軸方向で行えば5x5のガウスブラーが出来ますが, よくよく図を見ると,一番端の0.054の重みのテクセル部分が少し勿体無い気がします。 ガウス関数の端のほうはブラーを行う上での寄与が少ないのに1テクセルフェッチするのが少し無駄になっている気もします。

ガウス関数の変曲点までは普通に各テクセルを1テクセルずつフェッチして, 変曲点より先は,ガウス関数の端のほうとみなして,の微分値から1~2テクセルをリニアサンプラを利用してフェッチすることで, カーネルサイズを見かけ上稼ぐという方法を使ってみます。

f:id:hikita12312:20170820174802p:plain:w300

5x5のガウスカーネルが,7x7のガウスカーネルになりました。 ガウス関数の端のほうは重みが小さいので,適当に線形補間しても,あまり問題はないでしょう。 当然精度は落ちますが,そもそもの目的はガウスブラーを正確にかけることではなく,ブルームエフェクトの実装なので,良しとします。

縮小バッファによるブラーの拡大

ガウスブラーを利用する限り,大きなブラーをかけるときには,ガウスカーネルのサイズがどうしても大きくなってしまうという問題があります。 これを解決するために,昔からよく利用されている方法が縮小バッファ法です。

f:id:hikita12312:20170820180200p:plain

ガウスブラーのカーネルサイズは,そこそこのサイズ(今回は11x11)としてブラーをかけますが, 1/2倍に縮小したテクスチャを次々に作って,さらに同じサイズのカーネルでブラーをかけます。
そして,最終的に出来上がった1/2,¼,…サイズのブラーテクスチャ列を,適当に線形補間しつつ合成すれば大きなブラーのかかったブルームが出来るという方法です。

テクスチャを1/2に縮小することで,ガウス関数標準偏差が実質2倍になったような効果を与えることが出来て,さらに再帰的にブラーをかけるので, どんどん大きなブラーをかけることが出来ます。縮小をすればするほど,テクスチャサイズが小さくなっていくので,GPUへの負荷も大きくはないというのも利点です。
再帰的にブラーをかけることで,1/2サイズの縮小を繰り返すことによるブロック状のアーティファクトが発生を抑えるという効果もあります。

以下が,実際に縮小バッファ法によって作成された,各サイズのブラー結果です。

f:id:hikita12312:20170820181048p:plain

そもそもブラーという情報量を落とす処理なので,ブルームの種となる高輝度テクスチャも1/2サイズで作成して負荷を下げています。 この5枚のガウスブラー結果を単純に算術平均した結果が以下のブルームテクスチャです。

f:id:hikita12312:20170820181553p:plain

このブルームテクスチャを,元のHDRテクスチャに加算して,トーンマップ,露光調整,ガンマ補正等を行った最終結果が以下のものです。

f:id:hikita12312:20170820181720p:plain
f:id:hikita12312:20170813031817p:plain f:id:hikita12312:20170820181720p:plain

左はブルームOFF,右はブルームONです。ふわっと光が漏れ出して眩しい感じの表現ができているかと思います。
ちなみにブルーム関連処理のGPUでの実行時間は,1280x780の解像度の場合はGTX970のマシンで0.21ms程度でした。