(WIP/作成中) Japanese memo of "Assassin's Creed 4: Black Flag" Road to next-gen graphics" @ GDC2014

概要



ディファード・正規化イラディアンスプローブ (Deferred Normalized Irradiance Probes)

  • Deferred Normalized Irradiance Probes というオフラインで事前計算したライトプローブの結果を利用したグローバル・イルミネーションの手法.
  • メインライトによる 1 バウンスのディフューズ間接光だけの近似手法.

  • 今までのアサシンクリードのシリーズでは環境光がフラットかつ一様になっていて, 自分たちはそれが弱点だと知っていた.
  • 次世代機向けのリアルタイム GI 系の手法を試したが, アーティファクトが多かったり, クオリティがあまり良くなかった.
  • なので,何かオフラインで事前計算した結果をランタイム側で利用することを考えた.
  • しかし, 動的な時間変化や天候変化のプリセットがあるこのゲームではどう対処したらいいだろうか ?

  • 晴れた日の場合, 大部分の間接光はメインライトから発生する.
  • 一方で, スカイライトによる間接光は AO ( ワールド AO, SSAO )でなんとかする.
  • メインライトの軌道は天候のプリセットには依存せず, ライトカラーと強さだけが依存する.
  • 従って, 「ライトカラーと強さ」を「ライトの転送関数」と分離することができる.
  • メインライトからのディフューズのバウンス光だけなら, ディフューズアルベド・法線・シャドウだけから何かの情報を事前計算して蓄えておくことができるのではないか ? と考えた.

  • 4 つの基底ベクトル(FarCry3のものと同じ)のそれぞれの方向に対して, RGB8 の正規化したイラディアンスを保存する.
  • 1 日の時間帯のうち, 8 種類の異なる時間帯分で保存してある.
  • つまり, 1 箇所のプローブのデータサイズは 4 方向 * 3 Byte(RGB8) * 8種類 = 96 (Byte)
  • 通常のライトプローブだと1 箇所・1種類で, 3 channel * 9 SH_basis * 2or4 Byte(half float/float) = 54or108 (Byte) なので,今回のプローブデータは通常よりもデータサイズが小さくなっている.
  • 配置間隔は 2m * 2m の一様グリッド.
  • 高さ方向は 1 層だけだけど, 2.5D 的なレイアウトになっている.
  • 具体的な例だと「中に入れない直方体の家」があり, その屋上には登って歩ける場合, プローブがその家の屋上に配置されることを意味する.

  • 今回使っている FarCry3 と同じ 4 方向のプローブの基底(下図の左側)の弱点についてです.
    • 基底のベクトルが斜め上を向いているので, 地面の低い位置からライトのバウンスが保存されない.
    • 地面からのライトのバウンスが横側まで侵食してくる. (ライトリーク)
    • 基底が正規化直交基底ではないので, エネルギーの増減が発生する.
  • 従って, できれば下図の右側のようなキューブマップの基底(直方体の各面の法線なので, 6 方向分)か, 0,1,2次のSH 基底(9 個分)を使うべき.

  • 将来的な TODO
    • シェーダへの適用時に, スペキュラ間接光も追加する. (GPU 処理が増加するので, 今回は断念)
    • 高さ方向に何層もプローブデータを持つ. (今回は 1 層分)
    • HDR データとして保存する.
    • スカイライトによるディフューズ間接光にも対応する.
    • ディフューズ間接光の複数回のバウンスに対応する.
    • 近場のプローブだけリアルタイムで, 何フレームか分をかけて更新する.

  • 事前計算の処理の流れ
    • ライトプローブを 2m 間隔で配置する. 配置する高さや, オブジェクトに埋まっているかどうかの判定は NavMesh を使って行う. またプローブを配置した高さは別途, 2D テクスチャに保存しておく.
    • GPU で環境キューブマップを撮影する.
    • 撮影結果に対してイラディアンスのフィルタを適用し, ライトプローブの 4 つの基底ベクトルのそれぞれについてのイラディアンスを求めて正規化する.
    • 8 個の異なる時間帯の分を計算するときには, 64m * 64m のセクタ用の大きなシャドウマップを再計算して使う.
    • ライトプローブを, 2D テクスチャに保存する.
  • ランタイムの処理の流れ
    • 近い 2 種類の時間帯のライトプローブの結果をブレンドし, そのときの時間帯と天候プリセットの値で非正規化することで, そのときのメインライトのディフューズバウンスによるイラディアンスを求める.
    • NavMesh と近い距離にある GI 情報しか有効でないと仮定しているので, NavMesh と「プローブが配置されている高さ」との距離に応じて滑らかにブレンドするようにしている.
    • 結果をスカイライトによる間接光と AO と組み合わせる.

  • 下図は一連の合成の流れ.
  • まずはスカイライトについて.
    • 中央の上図は法線バッファ.
    • 右上の水色のキューブマップはスカイライトの Ambient Cube.
    • その下の図はディファードパスで法線バッファの結果を元に, スカイライトの Ambient Cube を参照して, さらのワールド AO を適用したもの.
  • 次に太陽ライトについて.
    • 図の左側の 3 つの図は 3 枚のプローブのイラディアンステクスチャ(RGBA8)で, その右図はプローブを配した高さを示した 2D テクスチャ.
    • 左下の図はディファードパスで, 法線バッファを使ってイラディアンステクスチャを参照した結果. 画面中の高さとプローブを配置した高さが違う場合は, その距離の違いと高さの違いの両者によって結果をブレンドする.
  • 中央の下図は全てを合成した結果.

  • GPU 処理負荷
    • ステンシルバッファを利用したマスク処理による最適化(この場合, 空と海をはじく)を使わない場合, フルスクリーンでの PS3XBox360 での GPU の処理負荷は 1.2 msec ( 7.2% ).
    • 同じパスでのシャドウマスクのパスと合わせると, 合計 1.8 msec ( 10.8% ).
  • メモリコスト
    • プローブの配置間隔は 2m * 2m. 1 セクタあたり 64m * 64m.
    • 1 セクタあたりのプローブのテクスチャの解像度は 16ピクセル * 16ピクセル. ( ? 本当は 32ピクセル * 32ピクセルなのでは ? )
    • プローブのデータは 「4 方向分 * RGB8」 = 「RGBA8 * 3 枚」のテクスチャとして保持する.
    • プレイヤー周辺の 25 セクタ分のデータを読み込んでいる.
    • 25 セクタ * ( RGBA8(4Byte) * 3枚 ) 解像度16*16ピクセル * 8 種類の時間帯 = 600 (KByte)
    • 最適化して, ブレンドに使う 2 種類の時間帯のプローブデータだけを読み込んだり, BC3 圧縮も検討したが, 今回は必要なかったのでしていない.
  • CPU 処理負荷
    • 全更新で 0.6 msec (3.6%)
    • 画面中で適切なプローブのテクスチャを参照したり, その描画コール, またゲーム中のワールドのハイトデータを VRAM にコピーする必要もあった.
    • Havana の地形でプローブデータをブルートフォースで配置した場合, 110,000 個だった. (110,000 = 331 * 331 )
    • NavMesh でプレイヤーがアクセス可能なところにだけ, プローブを配置するようにした場合, 30,000 個.
  • プローブの事前計算にかかった時間.
    • NVidia GTX 680 で 8 分.( 1 種類の時間帯だと 1 分. 30000 個の場合, 1 秒あたり 500 個. )
    • エディタの裏でいくつかのプローブをベイクし直したり, ベイクし続ける機能もつけた.

  • 下図は, 従来のアサシンクリード3 の Ambient Cube だけによる環境光の場合.
  • 時間帯は午後7-8 時のもので, 太陽がほぼ沈んでいて街全体が影に入っている状態.
  • 環境光が一様なので, 法線マップがフラットになっていて, ライトによる表現力が弱い状態.

  • 下図は今回のライトプローブを使った太陽光の1 バウンスの間接光を考慮した状態.

  • 下図はそれに「空によるライティング」を考慮した状態.
  • ライトプローブの緑の線は, コリジョンによってプローブがないと判定されたので, 近くのプローブによって置き換えられたことを示す.

  • 下図はそれに AO (ワールド AO + SSAO) を追加した最終状態の絵.

作成中 : ボリュームフォグ























作成中 ; スクリーンスペースのローカルリフレクション

  • Abestergo の会社の内部と, 雨で濡れた表面でこのテクニックを使うことにした.
  • 利点
    • 追加のリフレクションパスが必要ない(ゲームエンジンに組み込みやすい)
      • 一方で, 平面反射は別に 1 つのジオメトリパスが必要
      • 別のパスが必要でないので, オブジェクト単位で CPU/GPU のコストが増えない.
    • 平面じゃなくても, それに対して映り込みが発生する
    • CPU コストがほとんど発生しない
    • 映り込む物体や, 反射を受ける物体がアニメーションしていても大丈夫.
    • グロッシーな反射や, 反射を近似的に表現できる
    • 環境キューブマップのスペキュラ用のオクルージョンにも使える


  • 下図はスクリーンスペースのローカルリフレクションがない場合.

  • 下図はスクリーンスペースのローカルリフレクションがある場合.
    • 丸で印を付けている箇所は表面のラフネスによって, リフレクションの像のぼけ具合が変化している.
    • ローカルリフレクションを使うとキャラクターやオブジェクトの接地感が向上し, シーンの構成をわかりやすくする.

  • (下図の説明はスライドにないので, 詳細が不明)

  • スクリーンスペースのレイマーチに使うデプスバッファなので, 下図の赤い領域のようにカメラから直接見えない物体背面に関してはデプスの情報がない.
  • 従って, コリジョン判定について何らかのヒューリスティックスを導入する必要がある.

  • 自分達が使ったローカルリフレクションは下の 5 つのステップから構成されている.
  • パスへの入力として, 1/4(1/2*1/2)の解像度のカラーバッファとデプスバッファを利用する.

  • ローカルリフレクションは画面内の情報だけを
  • 実際にリフレクションが発生する範囲は画面サイズの 5-60% ぐらいを占めるので, 全画面に対してローカルリフレクションの計算をすると無駄な計算が発生する.
  • 従って, リフレクションによる映り込みが起きる可能性がある領域のマスク画像を作る.
    • 逆に, 以下のタイプのピクセルは計算から除去するピクセル.
      • 画面内の情報だけからだと, リフレクションの計算ができないもの.
      • グロシネスが高くないもの(意味合い的にはスペキュラ反射率?)
      • フレネル項でリフレクションの映り込みが見えないもの.
  • 後のレイトレースの処理を高速化するために, リフレクションのマスク画像を作る.
    • 1/4 解像度のバッファの中で, 64x64 ピクセルのブロックごとに, 64 個の低精度のレイを飛ばす.
    • エイリアシングアーティファクトを防ぐために,「非一様でジッタリングの分布を持つ」事前計算で求めたジッタリングテクスチャを用意しておき, レイを飛ばす開始位置をジッタリングする.
    • それらのレイは 1 回あたりのステップ数を大きくし, またデプスの許容値も大きめ(両者とも, 後のパスの 4 倍)にしておくので, 処理は軽い
    • それらのレイはそのピクセルが画面内の情報を反射するかどうか? の判定だけに使う.
    • その情報を書き込む際には, コンピュートシェーダの UAV を使って書き込みを行っている.
    • こうして, 後の高精度のレイを飛ばすパスにおいては, 低精度のレイによる判定が有効な箇所だけ処理の対象とする.
  • それぞれのレイはそれが影響する範囲よりも少し大きめの範囲をカバーするようにしている. こうして, マスクによる一時的なノイズを減らしている.

  • 求めたマスク画像とコンピュートシェーダを使って, より解像度が高いデプスフィールドに対してレイマーチを行う.
  • ここで, マスク画像, カラー画像, デプスバッファのように利用可能な情報を全て使う.
  • マスクテクスチャを使って, レイトレーシングが不必要なスレッドグループの処理はすぐに抜けることができる.
  • しかし, 得られたリフレクション結果はノイジーでくっきりしているので, ブラーするパスが必要.

  • 次のステップで, ノイジーで穴があるリフレクションマスクを使ったので, リフレクションの結果に対して縦と横の分離ブラーを適用した.
  • これを行う目的は下の 2 つである.
    • (a) グロスに応じてブラーすることで, グロッシーなリフレクションを近似的に求めるため.
    • (b)ノイズや穴のフリッカリング, エイリアシングを減らすため.
  • ブラーの半径はグロスだけでなく, リフレクションの情報が必要かどうか ? にも比例している.
    • それに穴が空いているけど, リフレクションマスクによって何かリフレクションの情報を含むべき場合, ブラー/探索の半径が大きくなる.
  • 2 回のブラーのパスにおいて, 各サンプルのブラーのウェイトはリフレクションの情報が得られるか? とグロスの類似性(バイラテラルフィルタ)に比例している.
  • リフレクションの情報を持たないエリアのウェイトは低いので, 結果的に push-pull と同様の効果が得られるので, グロッシーなブラーによって穴を埋めることになる.

  • 下図の左側はレイトレーシング後のリフレクションバッファの結果である.
    • シャープで, エイリアスがあり, フリッカーが発生する. さらに, 半分の解像度で処理したので少し穴が発生している.
  • ブラーは 2 つの目的がある.
    • 1 つ目は(矢印として書いたように)シャープでエイリアスが発生しているリフレクションをブラーすること.
    • 2 つ目の目的は(赤い丸で示したように)穴を埋めること.
  • 下図の右側は最終結果を示したもの. 見た目がより良くなり, 柔らかい効果になり, 一時的あるいはカメラによる変化に対して安定するようになっている.
  • 最後に, フル解像度の情報を使ってリフレクションバッファのアップサンプリングを行っている.
  • アップサンプリングは保守的なものだが, デプスの差異に基づいたアップサンプリングではない.
  • その代わりに, 反射率の差異があるものをはじく, というアップサンプリングを使っていて, 自分達の用途ではこの手法の方が良かった.
    • カメラ平面に対して平行なキャラクターやオブジェクトのリフレクションをリジェクトできる(?)

  • 全画面にローカルリフレクションが起きるシーンでも, PS4 や XBoxOne だと GPU 処理負荷は 2msec 以下だった.
  • 平均的なシーン (Havana の地形のように画面中にいくつか水たまりがあるシーン)だと, 1msec 以下だった.

  • 最適化

  • コード例について


作成中 : コンソール向けの GPU の最適化


















作成中 : Parallax Occlusion Mapping



作成中 : プロシージャルな雨







参考文献