フォグの実装

フォグ

ゼルダの伝説を遊んでいたら,フォグを作りたくなりました。
レガシーな定数フォグ,高さフォグから実装して,ゆくゆくは大気散乱,ライトシャフトが出来たら良いなと思っています。 本来は,レンダリング時にフォグの計算も同時に行うべきだとは思いますが,今回はポストエフェクト的に1パス追加して計算します。

http://www.iquilezles.org/www/articles/fog/fog.htm

定数フォグ

光の強度を{I}として,単位距離だけ進んだら,{a}倍だけ減衰するモデルを考えます。

{ \displaystyle
\begin{eqnarray}
\frac{dI}{dx} = -a I
\end{eqnarray}
}

この微分方程式を解けば,

{ \displaystyle
\begin{eqnarray}
I(x) = I_0 \exp{(-a x)}
\end{eqnarray}
}

つまり,背景色{I_0}に対して,指数的に減衰させれば距離に応じてフォグがかかっていきます。

const float2 uv = In.UV.xy;
float fogWeight = 0.0f;

const float depth = SampleLod0(g_DepthTexture, g_LinearSampler, uv).r; // 非線形Depth
float3 viewPosition = DepthToViewPosition(depth, TexCoordToScreenSpace(uv), DEPTH_FACTOR_1, DEPTH_FACTOR_2, DEPTH_FACTOR_3, DEPTH_FACTOR_4); // View空間座標
fogWeight += CONSTANT_FOG_SCALE * max(0.0f, 1.0f - exp(-CONSTANT_FOG_ATTENUATION_RATE * viewPosition.z));

const float3 bgColor = SampleLod0(g_ColorTexture, g_LinearSampler, uv).rgb;
const float3 fogColor = 0.8f;
float4 outputColor = lerp(bgColor, fogColor, fogWeight);

実装した結果です。

f:id:hikita12312:20171230140850p:plain:w600
f:id:hikita12312:20171230140832p:plain:w300
Constant Fog OFF
f:id:hikita12312:20171230140850p:plain:w300
Constant Fog ON

シーンの山並みは,パーリンノイズと,フラクタルブラウン運動で作成しました。 http://mrl.nyu.edu/~perlin/noise/
https://thebookofshaders.com/13/

高さフォグ

先程の定数フォグの減衰率{a}が,空間の高さに依存して変わるようなモデルを考えます。

{ \displaystyle
\begin{eqnarray}
\frac{da}{dy} &=& -b a \\
a(y) &=& a_0 \exp{(-b y)}
\end{eqnarray}
}

ここで,2次元平面で考えて,観測者が{(0,y_0)}の位置{P}から,{(d,y_0+h)}の位置{Q}の光源を見たとします。 この経路は,{0 \leq x \leq d}の範囲で次のように表せるはずです。

{ \displaystyle
\begin{eqnarray}
k &=& \frac{h}{d} \\
y &=& y_0 + k x
\end{eqnarray}
}

定数フォグと同様の積分を考えると{P}から{Q}までの直線状の経路積分となります。

{ \displaystyle
\begin{eqnarray}
\frac{dI}{dx} &=& -a(y(x)) I \\
\int \frac{dI}{I} &=& \int_P^Q -a(y(x)) dx \\
&=& \int_0^d  -a_0 \exp{(-b (y_0 + k x))} dx \\
&=& \frac{a_0 \exp{(-b y_0)} }{b k} \Big( \exp{(-b h)} - 1\Big) \\
I &=& I_0 \exp{\Big\{ \frac{a_0 \exp{(-b y_0)} }{b k} \Big( \exp{(-b h)} - 1\Big) \Big\} }
\end{eqnarray}
}

解析的に積分ができるので,フェッチする地点のワールド空間での座標とカメラの座標があれば,高さフォグを計算できます。

const float2 uv = In.UV.xy;
float fogWeight = 0.0f;

float3 viewPosition = DepthToViewPosition(depth, TexCoordToScreenSpace(uv), DEPTH_FACTOR_1, DEPTH_FACTOR_2, DEPTH_FACTOR_3, DEPTH_FACTOR_4); // View空間座標
const float3 heightFogCameraPosition = CAMERA_POSITION - float3(0.0f, HEIGHT_FOG_OFFSET, 0.0f); // オフセットされている
const float3 worldPosition = Transform(INV_VIEW, viewPosition.xyz);
const float3 deltaVec = worldPosition - heightFogCameraPosition;
const float H = deltaVec.y; // 背景までの高さ
const float D = length(deltaVec.xz); // 背景までの水平距離
const float K = H / D; // 傾き
const float a0 = HEIGHT_FOG_ATTENUATION_RATE;
const float b = HEIGHT_FOG_HEIGHT_WEGHT_RATE;
const float Y0 = heightFogCameraPosition.y;
const float bgLightWeight = exp(a0 * exp(-b * Y0) * (exp(-b*H) - 1.0f) / (b * K) ); // 減衰した背景光の重みの計算
fogWeight += HEIGHT_FOG_SCALE * max(0.0f, 1.0f - bgLightWeight);

const float3 bgColor = SampleLod0(g_ColorTexture, g_LinearSampler, uv).rgb;
const float3 fogColor = 0.8f;
float4 outputColor = lerp(bgColor, fogColor, fogWeight);

実装した結果です。下の方ほど霧が濃くなっているのがわかります。

f:id:hikita12312:20171230141008p:plain:w600
f:id:hikita12312:20171230140954p:plain:w300
Height Fog
f:id:hikita12312:20171230141008p:plain:w300
Height Fog+Constant Fog

大気散乱

今までの実装だと,フォグの色を決め打ちで与えていて単純に線形補間をしていただけでした。 ここに空の青色や,太陽の効果を乗せるのが大気散乱らしいです。 以下の資料を見ながら,大気散乱を実装してみます。

https://developer.nvidia.com/gpugems/GPUGems2/gpugems2_chapter16.html
https://blogs.unity3d.com/jp/2015/05/28/atmospheric-scattering-in-the-blacksmith/
http://nishitalab.org/user/nis/abs_sig.html#sig93
https://software.intel.com/en-us/blogs/2013/06/26/outdoor-light-scattering-sample
http://amd-dev.wpengine.netdna-cdn.com/wordpress/media/2012/10/ATI-LightScattering.pdf

大気散乱は,光の波長よりも小さい粒子によるレイリー散乱と,光の波長よりも大きい粒子によるミー散乱によるものが大きいようです。 Rendering Outdoor Light Scattering in Real Timeの資料を見ると,これらの散乱の係数は次のような位相関数となっているそうです。

{ \displaystyle
\begin{eqnarray}
\beta_{\mathrm {Reyligh}}(\theta) &=& \frac{3}{16 \pi}(1+\cos^2{\theta}) \\
\beta_{\mathrm {Mie}}(\theta) &=& \frac{1}{4 \pi}\frac{(1+g)^2}{(1+g^2-2g\cos{\theta})^{3/2}}
\end{eqnarray}
}

{\theta}は背景地点から太陽方向のベクトルと,カメラ方向のベクトルのなす角で,{g}はMie散乱の因子です。 {g}はGemsの記事を見ると, -0.75 から -0.999程度が良いと書いてありました。 なんでこういう式になるかは,まだちゃんと追っていないです。

レイリー散乱による色と重み,ミー散乱による色と重みを用意して,位相関数の結果を使って混合することで,フォグの色としてみます。

float RayleighPhaseFunction(float fCos2)
{
    return 3.0f / (16.0f * PI) * (1.0f + fCos2);
}
float MiePhaseFunction(float g, float fCos)
{
    return (1 - g)*(1 - g) / (4.0f*PI*pow(1 + g*g - 2.0f*g*fCos, 1.5f));
}

const float betaPhaseR = RayleighPhaseFunction(sunCos*sunCos);
const float betaPhaseM = MiePhaseFunction(mieCoeffG, sunCos);
float3 fogColor = (betaPhaseR*rayleighCoeff*rayleighColor + betaPhaseM*mieCoeff*mieColor) / (rayleighCoeff + mieCoeff);

結果です。パラメータが雑ですが,適当にレイリー散乱の成分に空の青色,ミー散乱の成分に太陽光の白色や夕焼けの色を入れました。

f:id:hikita12312:20171230141212p:plain:w600
f:id:hikita12312:20171230141212p:plain:w300 f:id:hikita12312:20171230141226p:plain:w300

フォグに大気の色が乗っています。単に色を付けただけなので,遮蔽とかは全く考えられていません。 そのうちライトシャフトも合わせて作ったり,天球を作ったりしてみたいですね。