ライトシャフトの実装

ライトシャフト

前回のフォグで, レイリー散乱やミー散乱のような,視線方向と光源方向から光の散乱の度合いが変わる現象を実装しました。
しかしながら,前回までの方法では,遮蔽物のことを何も考えていなかったので,光源が遮蔽されている場合でもレイリー散乱やミー散乱の色が見えていました。

f:id:hikita12312:20180108140600p:plain:w300
Fog無し
f:id:hikita12312:20180108140624p:plain:w300
Fog有り

上の図の例では,光源が山の後ろにあるはずなのに,白っぽい太陽光がフォグで散乱されています。

今回は,フォグの散乱において,構造物の遮蔽を考えて,光が筋状に伸びるライトシャフトを実装します。 参考にしたサイトは次のものです。

https://developer.nvidia.com/gpugems/GPUGems3/gpugems3_ch13.html
https://developer.nvidia.com/VolumetricLighting
https://docs.unrealengine.com/latest/JPN/Engine/Rendering/LightingAndShadows/LightShafts/index.html

遮蔽されているかどうかの判定に光源方向からのシャドウマップを使ったり,ボリュームのメッシュを作ったりと様々な方法がありますが, 今回は簡単にUEでも実装されているブルーム手法を元にして作ってみます。

マスクへの放射ブラー

ビュー空間における太陽のUV座標値と,Z値から,太陽よりも手前にある物体を0とするマスクを作成します。

f:id:hikita12312:20180108141320p:plain:w400

このマスクに対して,スクリーンスペースにおける太陽の位置を中心とした,放射ブラーを適用します。
放射ブラーを適用するときには,グレアの光芒を実装したときに光を延ばすときに利用した, ピンポンブラーを利用してテクスチャフェッチ数を削減します。 再帰的に4点({i=0,1,2,3})を太陽の方向に向かってサンプリングしますが,この時のサンプリングする位置{r_p(i)}と重み{w_p(i)}を以下のように決めます。

{ \displaystyle
\begin{eqnarray}
p &=& 0,1,2,\cdots \\\
i &=& 0,1,2,4 \\\
\\\
r_p(i) &=& 4^p i\\\
w_p(i) &=& \frac{a^{r_p(i)}}{Z} = \frac{a^{4^p i}}{Z} \\\
\end{eqnarray}
}

{Z}は正規化因子で,再帰的に畳み込まれる{w_p(i)}を全範囲で加算して1となるように決定します。 ただし,光芒のときとは違ってサンプル位置{r_p(i)}が太陽のスクリーンスペースを飛び越えてしまわないようクランプをする必要があります。

ピンポンブラーを行うテクスチャは,R成分に0~1範囲のマスク値,G成分に正規化因子用のバッファを格納していて, 再帰的に行う最後のパスでのみ,NORMALIZEマクロが有効なシェーダーを利用して,G成分でR成分を割って正規化を行っています。

実際にマスクテクスチャに対して,放射ブラーを適用した結果です。

f:id:hikita12312:20180108142516p:plain:w400

このマスクテクスチャを利用して,レイリー散乱,ミー散乱の成分のフォグの色をスケールすることで, 擬似的に光源の遮蔽と,光の回折効果によるライトシャフトの効果を作り出します。

適用結果

実際に適用した結果です。

f:id:hikita12312:20180108143004p:plain:w600
f:id:hikita12312:20180108140624p:plain:w300
LightShaft 無効
f:id:hikita12312:20180108143004p:plain:w300
LightShaft 有効

山によって,太陽の光の散乱が遮蔽されて,隙間から光が差し込んでいる様子が表現されています。

また,先程作成したマスクテクスチャにたいして,太陽を中心として放射状にノイズを付与することで,キメの細かいライトシャフトを擬似的に作り出せます。 https://game.watch.impress.co.jp/docs/series/3dcg/512295.html

// ライトシャフト用のマスクの作成パス (放射ブラー前)
float4 PS_CreateMask(PPFX_OUT In) : SV_TARGET0
{
    const float2 uv = In.UV.xy;
    float4 outputColor = 0.0f;
    const float2 sunVec = SUN_UV - uv;
    const float noiseWeight = ValueNoise( atan2(sunVec.y, sunVec.x*ASPECT_RATIO) * NOISE_FREQUENCY); // 0~1範囲でノイズを付与
    outputColor.x = ((GetLinearDepth(uv) >= SUN_Z) ? (1.0f) : (0.0f)) * (1.0f - noiseWeight*NOISE_SCALE);
    outputColor.y = 1.0f;
    return outputColor;
}
f:id:hikita12312:20180108143736p:plain:w400
f:id:hikita12312:20180108143756p:plain:w600
f:id:hikita12312:20180108143004p:plain:w300
ノイズなし
f:id:hikita12312:20180108143756p:plain:w300
ノイズあり

ちゃんと計測してはいませんが, 前回のフォグの計算に加えて,マスクを作成するパスが増えただけですので,それほど重い計算ではありません。 ピンポンブラー1回分ですので,少なくとも4本の光芒を出すよりかは4倍程度高速なはずです。

今回の方法は手軽にライトシャフトを使えて良い方法ですが,あくまでスクリーンスペース上での計算ですので,太陽がカメラ方向に存在しないと正常に動作しません。 カメラの裏側に太陽がある場合では,適度にマスクをフェードアウトさせるようなことをしないとダメかと思います。 さらに,光源が中途半端に近い位置にあると,マスクの境界が不自然に見えやすいです。 本当にどこに太陽があっても破綻の無いライトシャフトを作るには,ボリュームレンダリングのようなことをしないとダメなのでしょう。

ただし,太陽のように光源が無限遠方にあるかつ,太陽が常にカメラ方向に存在するとか,そもそもレイリー散乱やミー散乱がそれほど強くないようなシーンならば, ブルーム手法によるライトシャフトも有効かと思います。