地形のテッセレーション

ワンダと巨像を遊んでいたら地形を作りたくなりました。

テッセレーション

https://msdn.microsoft.com/ja-jp/library/ee417841(v=vs.85).aspx
http://www.nvidia.co.jp/object/tessellation_jp.html

テッセレーションはシェーダーパイプラインのステージの一つで,パッチと呼ばれる1つの面を複数の面に分割します。 頂点シェーダーの後,ピクセルシェーダーの前に実行されます。
例えば四角形のパッチを分割する場合,ハルシェーダーで4つエッジと,四角形の内部のXYについての分割数を指定して, テッセレーターが分割した結果をドメインシェーダーで加工してからピクセルシェーダーに渡します。

f:id:hikita12312:20180224194636p:plain:w400

テッセレーションを地形に使うことで,地形の詳細度を動的に計算して,必要な部分だけ三角形をたくさん生成することで, レンダリングの負荷を下げつつ,滑らかな地形を表現しようということです。

Height Map/Normal Map

まず,地形の元データーである,高さマップと法線マップを作成します。

f:id:hikita12312:20180224110047p:plain:w300
Height Map
f:id:hikita12312:20180224110109p:plain:w300
Normal Map

高さマップはパーリンノイズとフラクタルブラウン運動で作成しました。 http://mrl.nyu.edu/~perlin/noise/
https://thebookofshaders.com/13/

法線マップは高さマップから,隣接する高さの差分を利用して変分すれば作れます

{ \displaystyle
\begin{eqnarray}
\Delta_x y(x,z) &=& \frac{h(x+1, z) - h(x-1, z)}{2 \Delta x} \\
\Delta_z y(x,z) &=& \frac{h(x, z+1) - h(x, z-1)}{2 \Delta z} \\
{\bf u} &=& (1, \Delta_x y(x,z), 0) \\
{\bf v} &=& (0, \Delta_z y(x,z), 1) \\
{\bf n} &=& \frac{{\bf u}}{|{\bf u}|} \times \frac{{\bf v}}{|{\bf v}|}
\end{eqnarray}
}

分割度マップ

先程のHeightMapで表される地形は,32x32個のパッチで表現される地形であるとします。 それぞれのパッチの地形分割度に対応した値を格納した32x32サイズのテクスチャを地形分割度マップとして作成します。

まずは距離適応型のテッセレーションとして,単純にカメラから近い地形ほど分割度が増えるようにしてみます。

実際に出来上がった分割度マップです。

f:id:hikita12312:20180224112208p:plain:w300

赤い色ほど,パッチの分割数が多い事を示しています。 カメラに近い場所ほど赤色になり,カメラからは遠い場所は黒色になります。

地形のテッセレーション

なぜ,わざわざ分割度マップを別パスで作成したかというと,テッセレーションのパッチ間のエッジの切れ目を滑らかに接続するためです。 単純に分割数を各パッチに割り当てるだけだと,隣接するパッチの分割数が異なるときに,その境界が正しく接続しません。
そこで,1パス目で分割度を計算してから,注目するパッチと隣接するパッチの分割度の平均値を,注目するパッチのエッジの分割度として指定します。 こうすることで,隣接し合うパッチの境界の分割度は互いに一致するので,エッジの切れ目は作られないことになります。

f:id:hikita12312:20180224200410p:plain:w500

実際に分割してみます。テッセレーション部分のコードの概略は以下です。 注目するパッチとその上下左右のパッチの分割度を取得するために,Gather命令を利用しているのが工夫ポイントです。

分割して,レンダリングをした結果が以下のものです。

f:id:hikita12312:20180224114028p:plain:w600
f:id:hikita12312:20180224114028p:plain:w300 f:id:hikita12312:20180224114106p:plain:w300

輪郭検出

単純に距離だけで分割度を決定すると,遠くの山の輪郭がカクカクとしたものになってしまいます。 そこで,輪郭となりうる度合いも分割度マップの計算に取り入れてみます。

サンプリング点の法線と,サンプリング点からカメラに向かうベクトルの内積を見て輪郭になりうるかの度合いを組み込んでいます。

f:id:hikita12312:20180224112208p:plain:w300
輪郭検出なし
f:id:hikita12312:20180224114928p:plain:w300
輪郭検出あり
f:id:hikita12312:20180224114959p:plain:w300
輪郭検出なし
f:id:hikita12312:20180224115026p:plain:w300
輪郭検出あり

輪郭検出をいれると,全体的に分割度が上昇するバイアスはかかりますが,カメラ方向に法線が向いているパッチの分割はそのままなのがわかります。

結果

輪郭検出も入れた結果です。

f:id:hikita12312:20180224115302p:plain:w600
f:id:hikita12312:20180224115302p:plain:w300 f:id:hikita12312:20180224115343p:plain:w300
f:id:hikita12312:20180224121533g:plain

輪郭検出の有りと無しの比較です

f:id:hikita12312:20180224114028p:plain:w300
輪郭検出なし
f:id:hikita12312:20180224115302p:plain:w300
輪郭検出あり
f:id:hikita12312:20180224114106p:plain:w300
輪郭検出なし
f:id:hikita12312:20180224115343p:plain:w300
輪郭検出あり

まあそれっぽい気がします。

視錐台カリング

画面に映っていないパッチに対してテッセレーションを行うことは無駄になります。
その為,透視投影をした時に幾何的に画面に表示されないパッチの分割処理をスキップするような処理を入れると多少高速化されます。 ...が実装はしたのですが,パッチのAABBが上手に計算できていないためか視錐台のnear平面あたりでイマイチ不安定なので現在は利用していません。 そのうち修正します。

小ネタ : スクリーンスペースの矩形描画

小ネタです。 以下,DirectX系を考えています。

1つの三角形で矩形のUVを描画する

普通四角形のテクスチャを画面全体に表示しようとすると,こんな感じでスクリーンスペースで2つの三角形を描くかと思います。

f:id:hikita12312:20180125224401p:plain:w300

これと同じことを,大きな三角形一つで実現できます。

f:id:hikita12312:20180125224617p:plain:w400

はみ出たところはクリッピングされるので問題ありません。 画面全体にレンダリングしない場合でも,適当にスケーリングして.ID3D11DeviceContext::RSSetScissorRects()などシザー矩形でクリップすれば大丈夫です。

頂点バッファを利用しないで三角形を描画する

結論から言うと,DirectX11ならば,頂点シェーダーの入力にSV_VertexIDを使うと良いです。 SV_VertexIDは何個目の頂点を処理しているかが入ってくるシステム値です。

実際に先程の大きな三角形で画面全体にUVを張る頂点シェーダーは以下のようになります。

struct OUTPUT 
{
    float4 Position : SV_POSITION;
    float2 UV : TEXCOORD0;
};

OUTPUT VS_TEX2D(uint index : SV_VertexID)
{
    //     0----2
    //     |    /
    //     |  /
    //     1
    const float2 vId = float2(index / 2u, index % 2u); // 三角形を定義する 0 ~ 1 空間

    OUTPUT  Out = (OUTPUT  )0;
    // Positionの計算
    // {(-1,1), (-1,-3), (3, 1)}
    screenPosition = float2(4.0f, -4.0f) * vId.xy+ float2(-1.0f, 1.0f);
    Out.Position = float4(screenPosition.xy, 0.1f, 1.0f);

    // UV座標の計算
    // {(0,0), (0,2), (2,0)}
    Out.aUV[index] = float2(2.0f, 2.0f) * vId.xy;

    return Out;
}

この頂点シェーダーを利用すると,頂点バッファもインデックスバッファも利用せずに,シェーダーだけセットして,

m_pDeviceContext->Draw(3, 0);

みたいなドローコールを直接呼び出すだけで画面全体にUVを張れます。

周辺減光の実装

周辺減光

カメラで撮った絵の画面中心から離れるほど暗く見えるような効果を周辺減光と言います。
最近のゲームでもよく見かける効果です。
アーティスティックな効果の他にも,グレア効果など スクリーンスペースのエフェクトにおいて,画面端の境界によるアーティファクトを軽減する役割があります。

周辺減光を及ぼす原因としては,大きく分けて,「口径食」と「コサイン四乗則」があるようです。

https://en.wikipedia.org/wiki/Vignetting
https://www.ccs-inc.co.jp/guide/column/light_color/vol08.html

口径食

レンズの経や絞りによって,光が遮られることによって周辺部が暗くなる効果を指します。
さらにMechanical VignettingとOptical Vignettingに分類できるそうですが,イマイチ使い分け方がよくわかりません。
結局のところレンズ系や物理的な制約によって周辺が暗くなる効果ですので,適当に画面中心から遠ざかるほど暗くなるようなシェーダーを書いてみます。

const float2 d = abs(uv - float2(0.5f, 0.5f)) * float2(2.0f*ASPECT_RATIO, 2.0f);
const float r2 = dot(d, d);
float vignettingFactor = 0.0f;
// 口径食の効果
vignettingFactor += pow(min(1.0f, r2 / VIGNETTING_MECHANICAL_RADIUS2), VIGNETTING_MECHANICAL_INV_SMOOTHNESS) * VIGNETTING_MECHANICAL_SCALE;
// 合成
outputColor.rgb *= lerp(float3(1.0f, 1.0f, 1.0f), VIGNETTING_COLOR, saturate(vignettingFactor));

UnityのPostProcessingStackの周辺減光のコードを若干参考にしました。 これで再現した口径食はこんな感じです。

f:id:hikita12312:20180120165716p:plain:w300
周辺減光OFF
f:id:hikita12312:20180120165448p:plain:w300
口径食ON
f:id:hikita12312:20180120165448p:plain:w300
ImageCircle 小さめ
f:id:hikita12312:20180120165559p:plain:w300
ImageCircle 大きめ

コサイン四乗則

Natural Vignettingと呼ばれるものがこちらのコサイン四乗則になります。
光軸とレンズへの入射光のなす角{\theta}のときに以下のような性質を満たすと考えます

f:id:hikita12312:20180120162544p:plain:w240
  • 斜めに入射する光は単位面積あたりの強度が{\cos{\theta}}倍で小さくなる
  • 斜めに入射する光は,直進する光と比べて光源からの距離が{\cos{\theta}}倍長い。光源から逆二乗で単位面積あたりの強度が下がるので{\cos^2{\theta}}
  • 世の中の光源は斜めから見ると{\cos{\theta}}倍で減衰するものが多い

これらの要素を乗算すると,結局{\cos^4{\theta}}で画面端の色が減衰すると考えることが出来ます。

{\cos{\theta}}の計算を詳しく考えてみます。 0~1空間でのUV空間において,アスペクト比を考慮した画面中心からの距離を{r}とします。ただし縦方向画面端で1.0となるように正規化しています。 (口径食のシェーダーで書いたdot(d,d)が{r^2}です)
このときに,イメージセンサーまでの距離(=焦点距離)を{f},さらにイメージセンサーの縦サイズを{h}とすると,{\cos{\theta}}

{ \displaystyle
\begin{eqnarray}
\cos{\theta} &=& \frac{f}{\sqrt{r^2h^2 + f^2}} \\
&=& \frac{1}{\sqrt{ (h/f)^2 r^2 + 1}}
\end{eqnarray}
}

となります。{(h/f)^2}がコサイン四乗則による減光度合いを決めるパラメータとなります。 これを先程のシェーダーに組み込んだ結果です。

const float2 d = abs(uv - float2(0.5f, 0.5f)) * float2(2.0f*ASPECT_RATIO, 2.0f);
const float r2 = dot(d, d);
float vignettingFactor = 0.0f;
// 口径食の効果
vignettingFactor += pow(min(1.0f, r2 / VIGNETTING_MECHANICAL_RADIUS2), VIGNETTING_MECHANICAL_INV_SMOOTHNESS) * VIGNETTING_MECHANICAL_SCALE;
// コサイン4乗則
const float cosTheta = 1.0f / sqrt(r2*VIGNETTING_NATURAL_COS_FACTOR + 1.0f);
vignettingFactor += (1.0f-pow(cosTheta , VIGNETTING_NATURAL_COS_POWER)) * VIGNETTING_NATURAL_SCALE;
// 合成
outputColor.rgb *= lerp(float3(1.0f, 1.0f, 1.0f), VIGNETTING_COLOR, saturate(vignettingFactor));
f:id:hikita12312:20180120165716p:plain:w300
周辺減光OFF
f:id:hikita12312:20180120170133p:plain:w300
コサイン四乗則ON

{(h/f)^2}は4.0程度にしています。口径食はOFFです。それっぽい気がします。

結果

口径食もコサイン四乗則も入れた結果です。

f:id:hikita12312:20180120170240p:plain

ライトシャフトの実装

ライトシャフト

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

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倍程度高速なはずです。

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

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

フォグの実装

フォグ

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

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

残像の実装

残像の実装

グレア効果の一つとして,残像効果を実装します。 実装自体は簡単で,ブルーム効果で利用した高輝度成分をソースとして, 残像用のバッファに一定割合で加算合成を繰り返していくだけで実現できます。

f:id:hikita12312:20171224161009p:plain:w600

1秒あたりに,残像の効果はどれだけ減少するかの因子を{A}と定義すると,以下の式で残像バッファを更新することで残像効果になります。

{ \displaystyle
\begin{eqnarray}
\alpha &=& A^{\Delta t} \\
I_{\mathrm{buffer}}(t+\Delta t) &=& \alpha I_{\mathrm{buffer}}(t) + (1-\alpha)I_{\mathrm{src}}(t)
\end{eqnarray}
}

さらに,残像のソースとなる{I_{\mathrm{src}}}に対して,ColorMatrixによるカラーコレクションを適用します。 色相を180度反転させることで,高輝度カラーの補色の残像を作成することができて, 人間の目で明るいところを見た後に生じる残像のような色合いとなります。

結果

f:id:hikita12312:20171224162423p:plain:w600
f:id:hikita12312:20171224162453g:plain

1フレーム前の結果を補間なしに利用するので,画面が急激に動くと残像のコマ飛び感が激しいです。 ゆっくりカメラが動いている場合には自然な気もするのですが,残像効果の重みは小さいほうが良いかもしれません。 計算負荷は測っていないのですが,残像バッファは1/2サイズの縮小バッファで十分なことと, 単純な2テクスチャの合成処理なので,高速に適用できると思います。

モーションブラーの実装

モーションブラー

ゲームのフレームとフレームの間の動きに応じて,ブラーを与える効果がモーションブラーです。 レンダリングされた一枚のシーンがカメラによって撮影されたものと考えて,露出時間からブラーを計算します。

https://docs.unity3d.com/ja/current/Manual/PostProcessing-MotionBlur.html

モーションブラーを適用することで,レースゲームのスピード感が出たり, フレームレートが低い場合でも,滑らかに動いているような表現ができます。

今回は,モーションブラーの方法として,「A Reconstruction Filter for Plausible Motion Blur」という論文を内容を実装してみます。

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

「A Reconstruction Filter for Plausible Motion Blur」の方法はUnityでも実装されている方法のようです。

速度マップ

モーションブラーを作成するために,オブジェクトがどのような速度を持っているかの情報を持った速度マップを作成します。
一般に3Dモデルを画面に表示するときには,Projection,View,Modelといった,3つの行列を利用して,ワールド空間からスクリーン空間へと変換することで実現します。 このとき1フレーム前の行列を保存しておいて,現在のフレームとの差分を考えることで,フレーム間のオブジェクトの速度が計算されます。

float4x4 g_CurrentModelMatrix, g_CurrentViewMatrix, g_CurrentProjectionMatrix;
float4x4 g_PrevModelMatrix, g_PrevViewMatrix, g_PrevProjectionMatrix;

float4 currentPosition = position * CurrentModelMatrix * CurrentViewMatrix * CurrentProjectionMatrix;
float4 prevPosition = position * PrevModelMatrix * PrevViewMatrix * PrevProjectionMatrix;

float2 velocity = currentPosition.xy/currentPosition.w - prevPosition.xy/prevPosition.w;
velocity *= 0.5f; // 画面サイズを1.0としたUV座標系での速度に変換

画像は右に移動をするオブジェクトに対して,速度マップを作成した結果です。 見やすくするために,実際の速度の20倍の値を表示しています。

f:id:hikita12312:20171221103726p:plain:w500

A Reconstruction Filter for Plausible Motion Blur

先程の速度マップの速度情報に従ってボカす方向とボカす量を決めればモーションブラーが出来上がります。 しかし,速度マップは,そのフレームでオブジェクトがあった位置にしか,速度情報は書き込まれません。

f:id:hikita12312:20171221104516p:plain:w200

上の図のような状態では,濃い青色のオブジェクトの部分には「右に移動している」という情報が格納されていますが, 本来通過してきたはずのオブジェクトの左側の境界の外の付近の速度マップの値は0です。 単純に現在の速度マップの値を信用してブラーをかけると,境界外部に対してブラーをかけることが出来ません。

A Reconstruction Filter for Plausible Motion Blurの方法では, 注目するピクセルの周辺を含めた支配的な速度を仮定してブラーを行って,境界外部へのブラーを解決しています。 紹介されている方法では3つのパスを利用してモーションブラーを適用します。 まず,{w\times h}サイズの速度マップがあったとして,{w/k \times h/k}サイズのTileMaxと呼ばれるバッファを作成します。

f:id:hikita12312:20171221105201p:plain:w300

TileMaxは,元の速度マップを{k\times k}のタイルに分割して,そのタイル内部で最大の速度を格納します。 図の赤い矢印が中心のタイルが保持する最大の速度です。
さらに,TileMaxから周辺のタイルを含めて最大の速度を持ったTileを自身に格納する,{w/k \times h/k}サイズのNeighborMaxと呼ばれるバッファも作成します。

f:id:hikita12312:20171221105848p:plain:w300

図の赤い矢印が,周辺を含めた最大の速度として,NeighborMaxになります。
この,NeighborMaxがブラーを行うときに,支配的な周辺の速度であるとして,利用されます。

最後に,シーンのカラーテクスチャ,深度テクスチャ,元の速度マップ,そして今作成されたNeighborMaxを利用して,最終的なブラー結果を作成します。 シェーダーは論文のコードをそのまま実装しただけですが,以下のようなものになります。

シェーダーでは,NeighborMaxの速度ベクトル方向に対してサンプリングを行っていき,実際のサンプリングされた地点の速度と深度値からブラー量を決定しています。 この時,注目しているテクセルよりも,サンプルされたテクセルが前にあるか後にあるかでどの重みを採用するか計算を行っているようです。 また,jitterとして,ランダムなオフセットを与えることで,サンプル数が少ない場合でもそれなりに見えるようになっています。

論文ではOpenGL座標系だからか,カメラ奥方向がZ-となっていたので修正しています。 また,ブラー量を無理やりスケールする係数BLUR_SCALEも追加しています。

結果

結果です。BLUR_SCALE=6,K=16,SAMPLE_NUM=16で適用しました。 GPU実行時間は,GTX970の環境で1280x720サイズのテクスチャで.0.25ms程度でした。

f:id:hikita12312:20171221203813p:plain:w300
Motion Blur OFF
f:id:hikita12312:20171221203826p:plain:w300
Motion Blur ON
f:id:hikita12312:20171221212424g:plain
Motion Blur OFF
f:id:hikita12312:20171221212443g:plain
Motion Blur ON

まだまだキレイにする工夫の余地はありそうですが,とりあえずこんな感じです。