Skip to content
Go back

The Visibility Buffer: A Cache-Friendly Approach to Deferred Shading

· Updated:

paper web

拙訳

Introduction

  • deferred shadingが人気
    • 見えないフラグメントのライティングを計算しなくて良くなるため
    • 表面の属性をバッファに格納することで可視性を先に解決する
  • deferred shadingには根本的な問題が存在する
    • g-bufferの読み書きで盛大に帯域を消費してしまう
      • 4メガピクセルのスクリーン、24バイトのGバッファが4枚、60Hz = 46GB/s
    • visibilityとシェーディングの分離が不完全
      • g-bufferを生成するのに結局シェーディングを行わないといけない
        • 隠れてしまう表面に対する処理は無駄では?
      • ジオメトリがマテリアルでバッチされなければならない
        • visibilityの計算にはもっとやりようがあるのでは?
      • g-bufferの中だけでやりくりしなければならない
        • すべてのマテリアルに合うようにg-bufferを保守していくのは手間では?
    • 部分的に半透明なジオメトリを正しくレンダリングするのが難しい
      • g-bufferではサンプル1つに表面は1つだけ
      • 実用上、半透明ジオメトリは後でforwardパスでレンダリングする
  • 本手法では
    • g-bufferをvisibility bufferに置き換える
      • サンプルごとにトライアングル番号とインスタンスIDだけを格納する
        • 4バイト、多くても8バイト
    • deferred shadingと比べて消費する帯域がめっちゃ減る
    • シェーディングのすべてをdeferredパスで行えるようになる
  • z-prepass
    • ピクセルシェーダなしでシーンをレンダリングして深度バッファを先に作っておく
    • その後、Zテストをequalにして普通にシーンをレンダリングする
  • Forward+ [Harada 2012Harada, T. 2012. A 2.5D Culling for Forward+. Technical Briefs. ACM SIGGRAPH Asia. 10.1145/2407746.2407764. https://www.slideshare.net/slideshow/a-25d-culling-for-forward-siggraph-asia-2012/34909590.]
    • z-onlyパスの深度バッファを使ってタイル粒度でライトカリングを行う
    • 単純なシーンでは良いけど、ジオメトリが複雑になってくると上手くスケールしない
      • 映りそうなジオメトリをすべてサブミットし直す必要があるため
  • Light pre-passまたはdeferred lighting [Engel 2008Engel, W. 2008. Designing a Renderer for Multiple Lights: The Light Pre-Pass Renderer. ShaderX7. http://www.shaderx7.com/.]
    • シェーディングとirradiance計算を分けるようにz-prepassを修正した手法
      • z-onlyパスで、法線とスペキュラローブを追加で格納する
      • full-screen-quadパスで、ディフューズとスペキュラのirradianceを計算する
      • ジオメトリをサブミットし直して、最終的なirradianceを計算する
    • ライティングとシェーディングを良い感じに分離できる
  • compressed geometry buffer [Liktor and Dachsbacher 2012Liktor, G. and Dachsbacher, C. 2012. Decoupled deferred shading for hardware rasterization. Proceedings of the ACM SIGGRAPH symposium on interactive 3D graphics and games 143–150. 10.1145/2159616.2159640. https://cg.ivd.kit.edu/english/ShadingReuse.php.]
    • シェーディングサンプルで表面属性を格納する
    • visibilityサンプルはシェーディングサンプルへの参照とする
    • シェーディングサンプルの方が粗いところではストレージコストが減る
    • visibilityサンプルとシェーディングサンプルの多対1の関係を作れるようになる
  • フレームバッファのタイル化
    • フレームバッファをon-chipキャッシュに乗るくらい小さくタイルに分ける
    • Larrabeeのグラフィクスドライバで使われた [Seiler et al. 2008Seiler, L., Carmean, D., Sprangle, E., Forsyth, T., Abrash, M., Dubey, P., Junkins, S., Lake, A., Sugerman, J., Cavin, R., Espasa, R., Grochowski, E., Juan, T. and Hanrahan, P. 2008. Larrabee: a many-core x86 architecture for visual computing. ACM Trans. Graph. 27, 3, 1–15. 10.1145/1360612.1360617.]
      • アプリケーション層でこれをやるとドローコールが倍々で増えてしまうのでダメダメ
    • PowerVRのような低電力GPUではハードウェア層で似たようなことをやっていたりする [Imagination Technologies Ltd. 20112011. POWERVR Series5 Graphics - SGX Architecture Guide for Developers.]
      • ピクセルシェーダ前にvisibilityを解決する
      • トライアングルへの参照はタイルサイズのtag bufferに格納される
      • 本手法はこの戦略をアプリケーション層で実装したものとみなすことができる

The Visibility Buffer

  • 本手法
    • すべてのジオメトリをvisibility bufferにレンダリングする
      • visibilityサンプルごとにトライアングルIDとインスタンスIDをエンコードした4バイト整数を格納する
      • テッセレーションの効いたジオメトリがあったりシーンがバカでかかったりするときは8バイトでも良い
    • シェーディングを必要とするタイルのワークリストを表面のマテリアルごとに構築する
      • タイルは16 x 8ピクセル
    • 可視ジオメトリのサンプルが1つ以上あるタイルのリストを範囲として、マテリアルごとにシェーディングカーネルをディスパッチする
      • カーネルは頂点の補間からライティングまでシェーディングの全行程を処理してピクセルごとに1つのカラーを出力する
  • 本手法の主な動機はメモリフットプリントを削減すること
    • 1080pの8xサンプリングでもバッファは64MB程度
    • 可視トライアングル用のデータを読み出す手間はあるけど、multisampled g-bufferよか大分マシ
    • 大事なことは、メモリアクセスがスクリーンスペースでコヒーレントであるということ
      • キャッシュがワーキングセットの総量より著しく小さくてもキャッシュヒット率が高まる
  • 再計算を行うことでストレージを節約している
    • 重心座標の生成、頂点属性の補間、テクスチャ座標の差分計算を再計算する
    • ピクセル単位で頂点シェーダが走るので、その分の再計算も行われる
    • 昨今の傾向として計算より帯域の方がボトルネックになりやすいので、トレードオフとしては懸命な方
  • 表面マテリアルごとのワークリスト生成というオーバーヘッドを導入している
    • マテリアルごとにシェーダをディスパッチする必要があるから、必要な部分だけで処理を行いたい
    • ってことで、シェーダごとにタイルのリストを作っておいた方が良いよね

Implementation

  • visibility bufferパイプラインとともにリファレンスのg-buffer式deferredパイプラインを実装した
    • DirectX 11のピクセルシェーダとコンピュートシェーダを使う
  • ライトカリングは実装しない
    • よく知られているし、ここで述べたいことにはそれほど関係がない
    • カリング済みのタイル毎ライトリストを使ってシェーディングを行う
  • 半透明も扱わない
    • とはいえ、g-bufferパイプラインですでに使われる方法がvisibility bufferパイプラインでもちゃんと使える

Reference G-Buffer Pipeline

  • 24バイトのg-bufferフォーマットを使う
    • half4 position; half2 normal; unorm84 diffuse; unorm84 specular; uint triangleId;
    • 法線はスフィアマップにエンコードして圧縮する
    • アンチエイリアシングのため、4バイトのトライアングルおよびインスタンスのIDを格納する
    • ライティングはタイル毎ライトリストを持つ16x8ピクセルのタイルを使うコンピュートシェーダが1つ
      • タイルサイズはグループ共有メモリの大きさで決まっている
      • ワークグループが小さいとGPUを使い切りにくくなるので、64ピクセル以下だとパフォーマンスが低下する

The Visibility Buffer Pipeline

  • 3つのフェーズから成る
    • 第1フェーズでは、可視性を解決する
      • 深度とプリミティブIDをマルチサンプルバッファに記録する
    • 第2フェーズでは、ワークリストを作る
      • まず、タイルとシェーダのペアから成る8バイト整数のリストを作る
      • 次に、このリストをシェーダでソートして、タイルIDのみをリストに記録し直す
        • 合わせて、シェーダごとにタイルリストの先頭を記録しておく
    • 第3フェーズでは、表面シェーダごとにコンピュートカーネルをディスパッチする
      • 頂点を集めて、頂点をシェーディングして、属性を補間して、表面をシェーディングして、照明計算して、マルチサンプルをブレンドして、カラーを書き出す
      • 補間された頂点の計算以降はg-bufferパイプラインとまったく同じ
  • タイルサイズはこちらも16x8

Anti-Aliasing

  • deferred shadingスキームでは、GPUで効率的にアンチエイリアシングをやるのは中々に大変
  • visibilityサンプリングレートを効率よく高められるのが本手法の良いところなので、そこを活かしたい
    • とはいえ、visibilityサンプルすべてでシェーディングを行うのはフレーム時間に対してシェーディングコストの比率が大きくなりすぎてパフォーマンス結果が歪んでしまいそう
  • 本手法では:
    • ピクセル内のサンプルを反復して、ユニークなvisibilityサンプル1つに対してshading sample recordを1つを生成する
      • これらのサンプルはピクセル中に現れる回数に比例して重み付けされる
    • 各shading recordをスレッドグループのスレッド1つに割り当てて、シェーディングして、カラーを記録する
    • カラーをブレンドして、ピクセルごとに1つのカラーを書き出す
  • このアプローチはそこまで画期的ではない
    • けど、両方で同様に実装できるという、設計上の選択としては合理的なもの
    • よりシンプルなアプローチで実験したりもしたけど、今回のやつがそれなりに良かった

Result

(省略)

Discussion

  • レンダリングとはつまりサンプリングであるということ
  • 本手法はg-bufferパイプラインよりさらに”deferred”であるということ
    • 表面のマテリアルでソートしたりするのはvisibilityにとって最適とはいえないのでは?
    • visibilityパスでは表面固有の計算が存在しないので、新しいことができるんじゃなかろうか
      • 例えば、より上手にカリングを行う方法とか
  • 電力効率を第一に考えていかなければならない時代が来るということ
    • データのメモリ移動って電力を食う処理のひとつなんです
    • 帯域を食わすより追加の計算をさせる方がその点で言えば良い
      • 帯域がきついマシンではそっちのほうが早くなるかも
    • 将来的には帯域は増えるだろうけど、計算資源の方がそれより早く増えるだろうし
      • そういう意味では、メモリ帯域の相対的なコストは増え続けると思われる

Tessellation

  • 本手法は現時点ではテッセレーションをサポートしてないけど、検討段階のものはある
    • deferredパスでドメインシェーダの再計算を行う
      • パッチのUV座標、パッチID、インスタンスIDを8バイトでvisibility bufferに格納しておく
      • バッファサイズは倍になるけど、それでもg-bufferよりかは断然小さい

Conclusion

(省略)