Volumetric Fogの実装概要を知る
Volumetric Fogの実装概要を知る
はじめに
レイトレーシングを用いたVolumetric Fog表現手法を知るために,実際に実装して動作確認をしてみようと思います.本ページでは実装手順に沿って説明をおこない,場合によってはVolumetric Fogの表現自体の話を挟んでいきます.
実装概要
はじめに,処理の概要を図示してみます.
レイトレーシングを用いたVolumetric Fogは,各ピクセルでRayをとばし,各ステップでの光量を累加していくことで,ピクセルの最終的な光量を決定します.
各ステップで光に遮蔽されていることが少ない場所は光量がより多く累加され,結果光の筋のように見える,という原理です.
早速具体的な実装内容を見ていきます.Unity 2021.3系で開発していますが,どのバージョンでも応用は可能かと思います.
1. レイトレーシング
まずはRayをとばす疑似コードを簡潔に表します.画像と照らし合わせて見てもらえると分かりやすいかと思います.
void RayTrace()
{
// ステップ数
int stepCount = N;
// Rayの長さ
float3 rayDirection = cameraFarWorldPos - cameraWorldPos;
// ステップごとのRayの長さ
float3 stepDirection = rayDirection / (float)stepCount;
// Rayはカメラのワールド座標からスタートする
float3 currentWorldPos = cameraWorldPos;
[loop]
for (int i = 0; i < stepCount; ++i)
{
// 現在のステップでの光量を取得する
// 次のステップに進む(ステップごとのRayの長さを加算する)
currentWorldPos += stepDirection;
}
}
ピクセルシェーダで,ピクセルごとにRayベクトルを定義し,StepCount分だけ分割してステップを進めていきます.
各ステップで光量を収集していくことが次の内容になります.
RayDirectionですが,Camera.CalculateFrustumCorners
で視錐台ベクトルを取得できます.これにより視錐台のNearClipからFarClipに向かう4方向のベクトルを求めることができます.
視錐台ベクトル
各ピクセルシェーダのRayDirectionですが,頂点シェーダで視錐台RayDirectionを出力し,ラスタライザで重心座標系の線形補間をおこなうことによって,フラグメントシェーダで各ピクセルのRayDirectionを取得することができます.
詳しくはこちらを見ると理解が深まると思います:
注意点として,上記記事ではTriangleを利用してフルスクリーン描画をおこなっているため,三角形を2つ合わせた四角形でのフルスクリーン描画をおこなう場合,uvの扱いに注意が必要です.
2. ステップごとに光量を累加する
ステップごとの探索ができるようになったので,各ステップで光量を計算していきます.
簡単に言うと遮蔽されている場所では光量が累加されず,遮蔽されていない場所では,ライトからの距離によって光量を計算します.
レイトレーシングのloop内で光量を加算していきます.
[loop]
for (int i = 0; i < stepCount; ++i)
{
// 現在のステップでの光量を取得する
// 次のステップに進む(ステップごとのRayの長さを加算する)
currentWorldPos += stepDirection;
}
光量の計算は,厳密な計算をするほどより正確な値が出力されますが,その分計算負荷が増加してしまうため,見た目と負荷のトレードオフを考える必要があります.Volumetric Fogは大気中の粒子が光を散乱,屈折することで光の強度が変化します.今回はパフォーマンスを考慮し,光の散乱と減衰のみ超簡易的に表現しようと思います.
実装
光量を累加する疑似コードは以下になります.
float extinction = 0.f;
float lightWeight = 0.f;
[loop]
for (int i = 0; i < stepCount; ++i)
{
// 現在のステップでの光量を取得する
// ShadowAttenuationを取得する ※1
float attenuation = GetShadowAttenuation(currentPosition);
// 光の減衰を計算する
//減衰率を計算する ※2
extinction += EXTINCTION;
// 例: extinction += 1.0 / (c1 + c2 * STEP_SIZE + c3 * STEP_SIZE * STEP_SIZE);
// 参考: http://learnwebgl.brown37.net/09_lights/lights_attenuation.html
// 光源からの距離を取得し,光の減衰を表現する
// 遮蔽されている場合, attenuation=0となるため光量が加算されない
float shadowExtinction = attenuation * extinction;
// 光の散乱による疑似的な光量の強さを乗算し, 光量を累加する ※3
lightWeight += shadowExtinction * INTENSITY;
// 次のステップに進む(ステップごとのRayの長さを加算する)
currentWorldPos += stepDirection;
}
ライトのカラーを決定する
half4 fogColor.rgb = lightWeight * COLOR.rgb;
fogColor.a = lightWeight;
ShadowAttenuationを取得する (※1)
GetShadowAttenuationでは,光源からの距離や遮蔽判定のためにシャドウマップを取得します(画像はシャドウカスケードを考慮しています).
光の減衰 (※2)
各座標でのカメラから見える光の強度は,
- 光源からの距離
- カメラからの距離
が遠くなるほど弱くなります.減衰率は,光源と物体間の距離をdとすると
に比例するようですが,ライトの種類や表現によっては上記によらない方が綺麗に見えることもありますので,ウェイトはステップごとに近似的に計算します.
光の散乱 (※3)
現象としては,光の波が大気中の粒子に干渉したとき,波が周囲に派生することを指します.光の波が散乱することで光の強度が変化したり,光量にむらが発生します.
散乱の度合いは波長と粒子サイズによって変化します.
粒子サイズ >>>>> 波長 の場合,光が遮られるので粒子の影が発生します(遮光).
粒子サイズ >> 波長 の場合,光の波が粒子の後ろ側にまわり込みます(回折).
粒子サイズ > 波長 の場合,更に散乱が周囲に広がります(ミー散乱)
粒子サイズ < 波長 の場合,等方向に散乱し,粒子サイズの6乗に比例します(レイリー散乱).
このように,厳密な計算をおこなう場合,波長と各座標での粒子サイズを定義する必要があります.また,複雑な計算処理が発生します.
今回は超簡易的な実装を目指しているため,散乱度合いは遮光する程度の大きな粒子(=GameObject)以外は依存せず,一様であるとします.即ち,Intensityという定数で表現するに留めます.
最後に,今まで求めたウェイトにカラーを乗算し,最終カラーを決定します.これでVolumetric Fogが表現できました.
次へ
表現ができたものの,このままだと
Screen.x × Screen.y × StepCount
分だけ処理が走ってしまい,とても思い処理となっています.そこで次回,処理を軽量化するためのアプローチをおこなっていきます.
次回おこなうアプローチは以下の通りです.
- オブジェクトとの衝突判定による探索の早期リターン
- ノイズによる探索ステップオフセットずらし
- 縮小サイズ探索,縮小バッファ書き込み
- Volumetric Fogバッファのブラー
参考
Discussion