Skip to content
Go back

Deferred Lighting in Uncharted 4

· Updated:

web

拙訳

ディファードライティング(DEFERRED LIGHTING)

目標と動機付け(Goals and Motivations)

  • マテリアルデータを読み込む・修正する必要があるスクリーンスペースエフェクトがたくさんある
    • パーティクル
    • デカール
    • より重要なのは: SSR、キューブマップ、間接シャドウ、など
  • マテリアルデータの節約から逃れられない

選択肢(Options)

  • フォワードジオメトリパスに伴ういくつかの出力(色、法線、支配的間接方向、など)を持った”安価”なGバッファパスで実験した
    • 現在のハードウェアにおいてジオメトリパスは安価ではない
    • 我々は…密なオブジェクトがある
  • フォワードシェーダでのライティングの複雑さは信じられないレジスタプレッシャーを追加する。これはジオメトリパスをさらに遅くする
  • 両方の世界の最善となる方法はあるのか?

解決法(Solution)

  • 完全にディファード化しよう!
  • Gバッファはゲーム中のすべてのマテリアルを残念なことにサポートしなければならない…
  • ありがたいことに、我々には大量のメモリと大量の帯域幅がある

Gバッファ(GBuffer)

  • ピクセルあたり16ビットの符号なしバッファ
  • 制作中には機能間でビットを絶えず移動させている。どれだけのビットが各機能に対して必要かを厳密に決めるため、大量のビジュアルテストを実施
  • GCNのパラメータパッキング命令を多用
  • より詳しいことは、水曜日の”The Technical Art of Uncharted 4”を確認して
Rrg
Gbspec
Bnormalxnormaly
AiblUseParentnormalExtraroughness

: Gバッファ0

RambientTranslucencysunShadowHighspecOcclusion
GhightmapShadowingsunShadowLowmetallic
BdominantDirectionXdominantDirectionY
AaoextraMaterialMasksheenthinWallTranslucency

: Gバッファ1

追加のGバッファ(Optional GBuffer)

  • 追加の第三Gバッファはより複雑なマテリアルで使われる。これはマテリアルのタイプに基づいてさまざまに解釈される
  • 追加のGバッファを用いるマテリアルの例として織物(fabric)、髪、肌、絹(silk)がある
  • Gバッファの解釈は相互排他的である(つまり、織物と肌を同じピクセルに持つことはできない)。この制約はマテリアルオーサリングパイプラインで強制される
  • 追加のGバッファはマテリアルが必要としない限り読み書きされない

問題(Problems)

  • ディファードシェーダはすぐさまに肥大化する
  • 肌、織物、植物、金属、髪などをサポートシなければならない…言うまでもなくすべてのライトタイプを

ディファードパイプライン(Deferred Pipeline)

  • マテリアル”ID”テクスチャを保存する
    • 実際ホントのマテリアルIDではなく、使用するシェーダ機能の単なるビットマスク
    • 12ビットが8ビットに圧縮される(機能の相互排他性を計算に入れることで)

分類(Classification)

  • 16x16タイルごとに、ルックアップテーブルを指すためにタイル全体に対するマテリアルマスクを用いる
  • ルックアップテーブルは事前計算される。これは、タイルにおける機能すべてをサポートするような、可能な限り最も単純なシェーダを保持する
uint materialMask = DecompressMaterialMask(materialMaskBuffer.Load(int3(screenCoord, 0)));

uint orReducedMaskBits;
ReduceMaterialMask(materialMask, groupIndex, orReducedMaskBits);

short shaderIndex = shderTable[orReducedMaskBits];

if (groupIndex == 0) {
    uint tileIndex = AtomicIncrement(shaderGdsOffsets(permutationIndex));

    tileBuffers[shaderIndex][tileIndex] = groupId.x | (groupId.y << 16);
}
  • シェーダがライティングする予定のタイルのリストへタイル座標をアトミックにプッシュする
  • アトミック整数はDispatchIndirectの引数バッファのディスパッチ数でもある

最適化(Optimization)

  • 織物シェーダを例に見てみよう
  • すべてのピクセルが織物である(すなわち、織物のマテリアルマスクビットが1に設定されている)タイルに対して、この分岐がやっていることはオーバーヘッドを追加しているだけ
  • 我々はそれが常にtrueに評価するべきであることを知っている
if (setup.materialMask.hasFabric) {
    ...
}
  • “分岐なし”順列テーブルというまた別の事前計算済みテーブルを作る。これはタイル内のすべてのピクセルが同じマテリアルマスクを持つときに使われる
  • 分類中の条件を確認して適切なテーブルを使う
  • 分岐を取り除くだけでなく、大局的なコンパイラ最適化の機会をもたらす
short shaderIndex = shaderTable[orReducedMaskBits];

: 以前

bool constantTileValue = IsTileConstantValue(...);
short shaderIndex = constantTileValue ?
        branchlessShaderTable[orReducedMaskBits] :
        shaderTable[orReducedMaskBits];

: 以後

結果(Results)

  • ワーストケースの高価なカットシーンでパフォーマンス改善
    • 最適化なし(“Uberシェーダ”)で4.0ms
    • 最適シェーダを選択して3.4ms (-15%)
    • 分岐なしシェーダを使用して2.7ms (-20%、全体で-30%)
  • 平均で、分岐なしシェーダは非常に小さなコストで追加の10-20%の改善をもたらす。最適なシェーダを選択しつつだと、平均で、20-30%の改善になる
  • 基礎パフォーマンスに影響を与えずにマテリアルの複雑さやバリエーションを持つことができる
    • ひとつのシェーダに複雑さ(例えば、絹シェーダ)を加えるだけで、ゲームの残りの部分には影響を与えない
  • インターフェイスが明確に透過的に実装される
    • 何度かのイテレーションの後に :)
  • おまけ: 分類のコンピュートシェーダは非同期コンピュートで動作する --- ランタイムにほとんど影響を与えない

議論(Discussion)

  • システムを更に向上させられるか
    • 同様にライトタイプに基づいて異なるコンピュートシェーダをディスパッチする。少数のライトタイプが複雑さやコストの大部分をもたらしている
  • イテレーションが辛い
    • 1ビットの価値が身に沁みる :)
    • 最終的には良いシステムになった
  • シンプルな方が常に優れている。わずかなパフォーマンス向上のためにある種の機能を犠牲にしたかもしれない

スペキュラオクルージョン(SPECULAR OCCLUSION)

  • キューブマップはローカルな遮蔽を計算に入れていない
  • この解決策として、時々、遮蔽物の中や周囲にキューブマップを追加する
    • ジオメトリの配置の仕方によって常にできるとは限らない
    • 言うまでもなくパフォーマンスおよびメモリのコストがかかる
  • そのサンプル位置においてAOの値のみを使うことができるかも。例えば、Frotbiteのスペキュラオクルージョン[Lagarde and de Rousiers 2014Lagarde, S. and de Rousiers, C. 2014. Moving Frostbite to Physically based rendering. Physically Based Shading in Theory and Practice course. ACM SIGGRAPH. https://seblagarde.wordpress.com/2015/07/14/siggraph-2014-moving-frostbite-to-physically-based-rendering/.]
  • うまく動作するが、指向性directionalityについてはどうだろう?

定式化(Formulation)

  • より正確なオクルージョンを得るため、サンプルポイントの遮蔽され方とその方向をエンコードした何かで、どうにかしてスペキュラローブを遮蔽したい

ベントコーン(Bent Cone)

(画像)

  • オフライン処理中に、 [Klehm et al. 2011Klehm, O., Ritschel, T., Eisemann, E. and Seidel, H.-P. 2011. Bent normals and cones in screen-space. Vision, modeling, and visualization (2011). 10.2312/PE/VMV/VMV11/177-182. https://diglib.eg.org/server/api/core/bitstreams/25a399e5-3d25-43f0-8651-16a536c49afa/content.]で概説される手法を用いてベントコーンを生成する
    • 最小遮蔽の方向は以下のように定義される
      • NSS(i):=jPid(Δij)1jPiΔijΔijd(Δij)N_{SS}(i) := \sum_{j \in P_i} d(\Delta_{ij})^{-1} \sum_{j \in P_i} \frac{\Delta_{ij}}{|\Delta_{ij}|} d(\Delta_{ij})
      • ここで、レイの長さがある距離より短い場合、d(v)d(\vec{v})11となる
    • そして、その角度は以下のように定義される
      • C(i):=(1max(0,2NSS(i)1))π2C(i) := (1 - \text{max}(0, 2|N_{SS}(i)| - 1)) \frac{\pi}{2}

リフレクションコーン(Reflection Cone)

(画像)

cosAngle(r):={0.1925log2(72.56r+42.03)r0.56560.0005otherwisecosAngle(r) := \begin{cases} 0.1925 \log_2(-72.56r + 42.03) & r \ge 0.5656 \\ 0.0005 & \text{otherwise} \end{cases}

交差(Intersection)

  • 両方のコーンを交差させる
  • そして、交差の立体角を得る
  • 以下の数式を用いて交差するコーンの立体角を計算することができる[Mazonka 2012Mazonka, O. 2012. Solid angle of conical surfaces, polyhedral cones, and intersecting spherical caps. arXiv:1205.1396 [math.MG]. 10.48550/arXiv.1205.1396. https://arxiv.org/abs/1205.1396.]
  • 2つのコーン角度θ1\theta_1θ2\theta_2とそのなす角α\alphaが与えられれば、交差の立体角は以下のようになる
Ω=Ω(θ1,γ1)+Ω(θ2,γ2)Ω(θ,γ)=2(βϕcosθ)cosϕ=tanγtanθcosβ=sinγsinθtanγ1=cosθ2cosαcosθ1sinαcosθ1\begin{array}{ll} \Omega &= \Omega(\theta_1, \gamma_1) + \Omega(\theta_2, \gamma_2) \\ \Omega(\theta, \gamma) &= 2(\beta - \phi \cos \theta) \\ \cos \phi &= \frac{\tan \gamma}{\tan \theta} \\ \cos \beta &= \frac{\sin \gamma}{\sin \theta} \\ \tan \gamma_1 &= \frac{\cos \theta_2 - \cos\alpha \cos\theta_1}{\sin\alpha \cos\theta_1} \end{array}

実装(Implementation)

  • ルックアップテーブルへの入力としてcosθ\cos\theta及びsinθ\sin\thetaを含めるために変数の若干の変更を行う
  • 遮蔽率を得るためにBRDFコーンの立体角1cosθ12π\frac{1 - \cos\theta_1}{2\pi}で交差の立体角を除算する
float IntersectionSolidAngle(float cosTheta1, floatcosTheta2, float cosAlpha) {
    float sinAlpha = sqrt(clamp(1.f - cosAlpha * cosAlpha, 0.0001f, 1.f));

    float tanGamma1 = (cosTheta2 - cosAlpha * cosTheta1) / (sinAlpha * cosTheta1);
    float tanGamma2 = (cosTheta1 - cosAlpha * cosTheta2) / (sinAlpha * cosTHeta2);

    float sinGamma1 = tanGamma1 / sqrt(1 + tanGamma1 * tanGamma1);
    float sinGamma2 = tanGamma2 / sqrt(1 + tanGamma2 * tanGamma2);

    // 1番目のコーンの立体角を計算する
    // (これは2*PIで除算されている。この係数はテクスチャ内で計算に入れられている。)
    float solidAngle1 = 1 - cosTheta1;

    return (SegmentSolidAngleLookup(cosTheta1, sinGamma1) + SegmentSolidAngleLookup(cosTheta2, sinGamma2)) / solidAngle1;
}

議論(Discussion)

  • サンプルがかなり遮蔽されているときはディレクショナルライトマップのスペキュラへフォールバックする
    • エネルギーを維持するが、反射ディテールは失われる
    • 大抵の場合は良好に見える
  • この手法は比較的高価
    • ひとつのパラメータが固定されると関数は単純化される。たぶんラフネスパラメータを固定して、ラフネスに基づいて最終結果を調整することになる可能性があった
    • パフォーマンス的な理由でいくつかのレベルでは無効化された
  • 動的オブジェクト(例えば、キャラクター)では動作しない。この場合、前述のより単純なオクルージョン手法にフォールバックする
  • 物理ベースではない
  • しかし、多くのオクルージョンの問題を解決する
  • 推し進めることができる
    • 他の表現を用いたより正確なオクルージョン
    • まったく異なる方向に進む