DirectXの話 第142回
シャドウマップを利用したVolumetric Light Shaft 14/12/19 up
前回の更新が1年前ということでかなり間が空きました。
正直なところ、DX12が出るまでは放置かな、と思っていたのですが、割とそれなりに上手く言ったので折角だから更新しました。
今回の更新はシャドウマップを利用したライトシャフトです。
書籍『GPU Pro 5』にPS4の『Killzone : Shadow Fall』で使用されているライトシャフトの技術が掲載されていますが、あれです。
ただ、あそこまでしっかりやってませんし、調整もまともにしてないのですが、基本的な部分は抑えているかな、と思います。
ライトシャフトを実現する手法としてはスクリーン空間の太陽の位置から放射状にブルームを作成する方法があります。
この方法は太陽が画面内、もしくはスクリーンより前方にある場合にはそれなりに効果がありますが、この条件が満たされないと(例えば太陽が真上にある場合)にはうまくいきません。
また、点光源やスポットライトにも対応ができません。
今回実装した手法はシャドウマップを生成しているライトであれば基本的にどんなライトにも対応ができます。
今回の実装自体は平行光源に対してですが、シャドウマップを生成しさえすれば他のライトでも同じように実装できます。
では、今回の実装の描画パスについて解説します。
普通にシーンを描画し、その後のポストプロセスとしてライトシャフトを描画します。
ライトシャフトを描画する際のパスは以下のとおりです。
1.深度を半解像度にダウンサンプリングする
2.半解像度の深度からライトシャフトボリュームを描画する
3.ライトシャフトボリュームにバイラテラルブラーをかける
4.前回のバッファを利用してTemporal Reprojection
この結果を次のTemporal Reprojectionに利用する
5.バイラテラルアップサンプリングでアップサンプリングしつつシーンに加算合成
描画パスについては1,3,4,5は説明の必要はないんじゃないかと思います。
これらの技術はスクリーン空間AOやスクリーン空間リフレクションでも用いられるため、グラフィクスプログラマにはお馴染みでしょう。
というわけで、今回は2番のライトシャフトボリューム生成部分に絞って解説を行います。
このようなボリュームを伴ったライティングは、現実世界では埃っぽい部屋、霧の立ち込める森の中などで見られます。
この現象が起こるにはそれなりに強いライトとそのライトを散乱させる空気中の何らかの物体が必要になります。
この物体は主に埃、霧などの水蒸気、煙などです。
つまり、光はこれらの微小物体に衝突し、その結果散乱、最終的にカメラにその散乱光が到達するという仕組みになっています。
しかし、ポリゴンによる描画ではポリゴンが描画されない部分にはライティングの処理が行えません。
ソリッドな物体(岩とか木とか)はポリゴンモデルで描画されますが、空気中の埃や水蒸気はポリゴンモデルでは描画されませんので、普通にやってはライトシャフトを実現できません。
もちろん、ライトシャフトの形状のオブジェクトを描画する方法もありますが、ライトの方向が変わったりした場合に対応できません。
では、どうやってライトシャフトのボリュームを描画するかというと、最近よく聞くレイマーチングを利用します。
レイトレーシングのようにカメラから各ピクセルにレイを飛ばします。
そのレイ上にいくつかのサンプリングポイントを配置し、それらのサンプリングポイントを空気中の微粒子とみなしてライティングを行い、その結果を加算します。
もちろん、微粒子に到達するライトは他のオブジェクトによって遮蔽されることがあります。
この遮蔽情報はもちろんシャドウマップから参照します。
つまり、シャドウマップを利用したライティング処理を微粒子に対して行う、その微粒子はサンプリングポイントに存在するものとする、という体で処理を行います。
ライトシャフトボリュームの描画でやっている部分は基本的にこれだけなのですが、より良い結果を生むにはいくつかの工夫が必要になります。
まず、サンプリングするレイの長さを考えます。
以下の図はSIGGRAPH 2014で発表されたスライドから抜粋したものです。
これは点光源に対してレイマーチングを行う場合の図ですが、点光源が有効な範囲内のみでレイマーチングを行うようにしています。
点光源の範囲球と視点から深度バッファに衝突する点までのレイとの接触判定を取り、球に対して入射した点から出射した点まででレイマーチングを行います。
スポットライトの場合は衝突判定が面倒かもしれませんが、ある程度で構わないでしょう。
なお、平行光源の場合はシャドウマップの描画範囲内でレイを分割するのが良いようですが、今回の実装では行っていません。
また、カスケードシャドウマップを利用している場合は各レベルごとにレイを生成、サンプリングするほうが良いようです。(これはGPU Pro 5に掲載されてた)
2つ目の工夫はサンプリングポイントにランダム性を持たせることです。
今回の実装では2Dのノイズテクスチャを利用してサンプリングポイントの開始位置をずらしています。
サンプリングポイントのステップ距離は基本的に変えていませんので、開始位置がずれると自動的にサンプリングポイントがずれるようになります。
サンプリングポイントの数をかなり増やせればそちらのほうがいいのですが、さすがに128個もサンプリングポイントがあると速度面で問題がありすぎます。
フレームによってノイズが変化することで時間方向でサンプリング数を増やせる、という魂胆ですね。
まあ、この手の手法はスクリーン空間リフレクションでもよく使われますね。
3つ目は散乱にそれなりの計算式を用いることです。
ライティング結果を適当な係数掛けて、というのもありではあるのですが、リアリティを求める場合はそれなりの理論がほしいところです。
GPU Pro 5では、ミー散乱を実現するため、Henyey-Greenstein関数を用いるとあります。
この関数は以下の形で知られています。
cosθは視線とライトベクトルの内積から求め、gは粒子の種類に応じて決定します。
ミー散乱ではライトベクトルの方向に散乱が強く出るらしいので、cosθが1.0の場合は最大になり、-1.0の場合は最小になります。
画面前方からライトが当たった場合に散乱が強くなる計算です。
また、gを0.0に設定すれば定数になりますので、どうしても後ろや横からのライトでライトシャフトを作成したい場合にも対応ができます。
最後は微粒子の濃度を空間によって変化させることでより現実的な映像を作成する手法です。
空気中の微粒子は濃度が一様ではありません。そのため、現実のライトシャフトでは散乱にムラができます。
これを実現する方法として、カメラ空間中での3Dテクスチャの使用方法がGPU Pro 5に書かれています。
KZ:SFではこの3Dテクスチャをパーティクルで埋めることで濃度の差を実現していたようです。
残念ながら、今回の実装ではこの部分はサボりました。すみませんすみません。
ただ、アニメ調のレンダリングを行う場合はムラがない方がいいかもしれませんね。
というわけでサンプルは以下からダウンロードして下さい。
平行光源1つ分しか処理していないため、割と高速に動作する…と思いますがどうでしょう?
操作はいつもどおりです。
次回は果てしなく未定。
DX12が来たら…かなぁ。