スキャッターベースのボケ

ギャザーベースとスキャッターベース

前回までの被写界深度処理は,ギャザーベースと呼ばれているカテゴリの方法です。 周囲のサンプリングして集めて一つのピクセルの色を決定するという意味でギャザーベースと言われているのだと思います。
ただし,ギャザーベースでは"注目するピクセル"に対する処理なので, 前ボケのブラーを作るときにも問題になったように, 前ボケが焦点の合っている場所の上に重なるように生じる現象はできません。

https://gpuopen.com/gdc2017-cinematic-depth-of-field/

また,大きなサイズのボケを作ると,大きなカーネルが必要になったり,ボケの細かい形状の制御が出来なかったりする問題があります。

そこで,今回はスキャッターベースと呼ばれる,あるピクセルの情報を周囲のピクセルに書き込む方法でボケの処理を実装してみようと思います。

スキャッターベースの概略

以下のサイトを参考にしました。

https://www.slideshare.net/siliconstudio/cedec-2008imagire-day

https://www.slideshare.net/siliconstudio/cedec-2012-34760454

http://hexadrive.jp/lab/demo/726/

今回やってみた方法の基本的な流れはこんな感じです。

f:id:hikita12312:20171010102539p:plain:w600

画面全体のピクセルに対応する位置に縮退した矩形を作れるように頂点として入力します。 その4頂点をVertexShaderでボケサイズに応じて膨らませて,PixelShaderでボケの形状を決定します。 周囲のピクセルからサンプリングするわけではなく,ボケ自体が膨らむのがポイントです。
膨らませる処理にGeometryShaderを利用する方法もあると思いますが,今回は試していません。

マスク処理

画面の解像度が1280x720である場合,そのままピクセルとボケの矩形を対応させると, 一つの矩形あたり2個の三角形を書くとして約164万ポリゴンのレンダリングとなります。 こうなってしまうとリアルタイムで行うことが難しいので,例えば縮小バッファを利用して,1/2サイズのバッファを利用します。 バッファのサイズを半分にすれば4倍高速化できますが,それでも40万ポリゴン近くです。

そこで,今回は目立つボケ以外はスキャッター処理を行わないというマスクを作成しました。

f:id:hikita12312:20171010103823p:plain:w300
元画像
f:id:hikita12312:20171010103844p:plain:w300
マスク

マスクの条件は,

  • 輝度のエッジとなっている
  • 輝度が高い
  • ボケの直径(CoC)がある程度の大きさ以上

となっていて,輝度が高く周囲よりも明るいボケが目立つ領域であると仮定したマスクとなっています。 エッジの計算はSobelフィルターを利用しています。 マスクされる領域のスキャッターボケは,VertexShaderで頂点を膨らませないで縮退させておくと,ラスタライズの時点で無視されるので高速化出来ます。

ボケの形状の作成

VertexShaderで膨らんだスキャッターボケの矩形領域に対して,実際に五角形や六角形のボケを書いていきます。 今回書いたボケ形状用のPixelShaderのコード片は以下です。

float4 PS_Create(SCATTER_BOKSH_VS_OUT In) : SV_TARGET0
{
    const float coc = In.Color.a;
    // 表示範囲が前ボケ,後ボケ境界からはみ出すならば描画終了(後述)
    if ((coc > 0.0f) && (In.UV.z >= 0.0f)) discard;
    if ((coc < 0.0f) && (In.UV.z <= 0.0f)) discard;

    float3 color = In.Color.rgb;
    float2 rPos = In.UV.xy;
    if (coc < 0.0f) rPos *= 1.0f; // 前ボケのときの形状反転

    // ボケ形状の計算
    const float deltaTheta = 2 * PI / APERTURE_BLADE;
    // 描画点の光軸中心からの角度 (0~2pi)
    float theta = atan2(rPos.y, rPos.x) + PI + APERTURE_ROTATE;
    // APERTURE_BLADE角形のi番目頂点の角度を基準にした相対theta (0~2pi/APERTURE_BLADE)
    const float modTheta = frac(theta / deltaTheta)*deltaTheta;
    // bokehRadius=1.0としたときの,APERTURE_BLADE角形の境界となる境界
    const float effBound = 1.0f - (1.0f - APERTURE_CIRCULARITY)*(1.0f - cos(deltaTheta / 2.0f))*sin(APERTURE_BLADE / 2.0f*modTheta); 
    const float bokehRadius2 = rPos.x*rPos.x + rPos.y*rPos.y; // (0~1)
    const float effBokehRadius = sqrt(bokehRadius2) / effBound;
    // ボケ形状範囲外ならば描画しない
    if (effBokehRadius > 1.0f) discard;

    // ボケの形状による減衰(明度)を計算(0.0~1.0)
    float attenuation = saturate(1.0f - pow(effBokehRadius, 10.0f)); // 後ボケは中心ほど明るい
    // 前ボケならば明度を反転,
    if (coc < 0.0f) attenuation = 1.0f - attenuation;
    // CoCの大きさによる減衰
    // 本来は面積に反比例するが,マスクによって,積分されるボケ減っているので,面積反比例だと,あまりに薄い
    // 厳密な計算をするべきだが,coc^-1の重みでなんとなく良さそうだったのでこれで
    const float cocAttenuationWeight = min(1.0f, 1.0f / coc);
    attenuation *= cocAttenuationWeight;
    // 明度の計算
    color.rgb *= attenuation*APERTURE_BRIGHTNESS_SCALE;

    return float4(color, coc);
}

ボケの内部の色味だとか収差だとかは今回は適当に作っています。 ボケ形状を作成する部分は,APERTURE_BLADEで{n}角形を指定し,APERTURE_CIRCULARITYでボケの円形度合いを決めています。

f:id:hikita12312:20171011104928p:plain:w300

円弧を多角形の辺を軸と見たときのsinカーブに近似できるとして計算しています。 実際にPixelShaderから出力されたボケ形状は以下の画像のようになりました。適当な近似でもそれっぽくなっていると思います。

f:id:hikita12312:20171011105944p:plain

本来ボケの面積に従って明度が反比例していくべきですが,今回の場合ですとマスクによってボケの総量が減っているので,単純に面積に反比例だと色が薄くなりすぎます。 アドホックに直径に反比例という形で明度を減衰しています。

スキャッターボケテクスチャの作成

PixelShaderで作成したボケテクスチャを加算合成してテクスチャに書き出します。 このとき,入力テクスチャと同じサイズのテクスチャではなく,入力サイズの高さを2倍にしたアスペクト比で,異なる倍率の縮小バッファを3枚用意しました。

f:id:hikita12312:20171012102919p:plain:w400

VertexShaderで頂点を膨らませるときに細工をして,前ボケならばテクスチャの上半分,後ボケならばテクスチャの下半分に書き出すようにします。 カメラのボケは前ボケと後ボケで反転したり収差が変わったりして異なる形状になりますが,それを分離して表現することが可能となります。

f:id:hikita12312:20171012104625p:plain:w600

さらに,縮小バッファとして用意した異なるサイズ毎に,VerterxShaderでの膨らましとPixelShaderでの書き込みの一連のパスをそれぞれ行います。 大きなサイズのボケを書き出すときに,膨らんだスキャッター矩形の内部のピクセル数が多ければ多いほど負荷が高くなりますが, 大きいサイズのボケほど小さい縮小バッファを使うようにすれば負荷を抑えることが出来ます。 大きいボケには,それほどディティールが必要ないので縮小バッファで十分です。 ドローコールは3倍に増えますが,大きいボケの負荷を下げることが出来るので全体としての負荷は下がります。 さらに,ボケのサイズ毎に3つのテクスチャに分類して書き出すことで,ボケの前後関係を分けて書き出すことができることもメリットです。

結果

出来上がったスキャッターボケテクスチャをギャザーボケの結果に加算合成することで,最終出力が得られます。 スキャッターボケテクスチャのアルファ値にそのボケの生じた距離を格納することで,合成を行うときに後ボケをマスクをすることもできます。
(2017/10/12現在,合成周りがまだ甘いです...)

以下が結果です。 f:id:hikita12312:20171012105802p:plain:w600 こちら五角形の後ボケ,ある程度大きいボケでもキレイに表示されています。

f:id:hikita12312:20170926225548p:plain:w300
ギャザーボケ六角形
f:id:hikita12312:20171012105954p:plain:w300
スキャッターボケ六角形

フラットなブラーの結果と比較しました。 ギャザーベースのボケよりハッキリボケの輪郭が出ていて,大きいボケが表現できています。

f:id:hikita12312:20171012110415p:plain:w600

今度は円形ボケにして接写的に焦点を手前に合わせてみました。 だいたい良いんですが,一部の後ボケがマスクされていなかったり, ギャザーボケとスキャッターボケの合成が甘くて,ギャザーボケの結果が薄っすら見えてしまったりと改善の余地はありそうです。