Skip to content
Go back

Decima Engine: Advances in Lighting and AA

· Updated:

slides web

拙訳

デシマエンジン(Decima Engine)

これが何なのか知らない人たち向けに:

Decima EngineはKillzoneシリーズのためにGuerrilla Gamesによって作られたゲームエンジンである。

しかし、より大きくより美しい世界をサポートするため、それ以来、拡張と最適化を行いってきた。そこには、Horizon Zero Dawnのそれも含まれる。

そして近年、コジマプロダクションとのDecima Engineの共有も始め、彼らは現在それをDeath Strandingの開発に用いている。

トピックス(Topics)

  • GGXの球エリアライト
  • 高さフォグ
  • 1080pにおけるAA
  • PS4 Proでの2160pチェッカーボード

GGX球エリアライト(GGX spherical area light)

  • 形状上でGGXを積分するのは難しい
  • 様々な近似アプローチ
    • パフォーマンス vs クオリティ
    • 形状固有(例えば、多角形ライトのための[Hill and Heitz 2016Hill, S. and Heitz, E. 2016. Real-Time Area Lighting: a Journey from Research to Production. Advances in Real-Time Rendering in Games course. ACM SIGGRAPH. https://blog.selfshadow.com/publications/s2016-advances/.])
  • 球ライトでは: ポイントライトを’たわませるwarp’安価なトリック[Karis 2013Karis, B. 2013. Real Shading in Unreal Engine 4. Physically Based Shading in Theory and Practice course. ACM SIGGRAPH. https://blog.selfshadow.com/publications/s2013-shading-course/.]
    • 歪みを引き起こす可能性があるし、改善される可能性もある

Decima Engineは恐らくエリアライトを用いるライティングをサポートする最初のゲームエンジンのひとつであった。

ただし、初期の実装はGGXをサポートしておらず、それが我々にとって制限要因となっていた。

そこで、最近、エリアライトシステムをアップグレードし始め、まずは我々の球エリアライトにGGXを追加しようとした。

しかしながら、GGXのようなマイクロファセットBRDFをエリアライトに統合することは難しく、ある種の近似的なテクニックを必要とする。

実際に、まさにそれを行うためにいくつかの異なるテクニックがここ数年で出現してきた。これらのテクニックのいくつかはパフォーマンスよりクオリティを優先しており、いくつかはその逆を優先している。そして、これらのテクニックの多くは形状タイプのただひとつに対して特に適するようになっている。我々の新しい球エリアライトでは、色々と調べた結果、Brian Karisによって述べられるライト曲げbendingトリックを選択することとした。

これは安価であり、多くの様々なゲームエンジンで上手くsuccessfully使われている。

そして、その結果はしばしばかなりまともpretty decentである。

しかし、予期せぬ形状が、特にグレージング角で、現れる可能性がある。

左側の画像でそれをはっきりと確認することができる。しかし、我々はそのテクニックを改善する方法をひとつ見つけており、右側の画像に見えるものがそれである。では、これはどうやって動作するのだろうか?

  • どうやってピクセルあたり1つのポイントライトを用いてエリアライトを近似するのだろう?

では、まずはBrian Karisのテクニックを調べるgoing overことから始めよう。その発想は単一のポイントライトで右図のような結果を近似したいということである。

そして、反射ベクトルの方向でエリアライトの外周perimeterに向かってポイントライトを動かすことでそれを行うことができる。こんな風に…

  • ピクセルの反射ベクトルに向かってポイントライトを動かすことで[Karis 2013Karis, B. 2013. Real Shading in Unreal Engine 4. Physically Based Shading in Theory and Practice course. ACM SIGGRAPH. https://blog.selfshadow.com/publications/s2013-shading-course/.]
    • Phongモデルで素晴らしい[Picott 1992Picott, K. P. 1992. Extensions of the linear and area lighting models. IEEE Computer Graphics and Applications 12, 2, 31–38. 10.1109/38.124286.]
    • マイクロファセットモデルではあまり良くない。依然としてピーク応答を’取り逃がす’可能性がある

これはPhongベースのライティングではとてもうまく機能し、1992年にはすでに述べられていた。

しかし、GGXのようなマイクロファセットベースのモデルではそのように常にうまく機能するわけではない。

これはポイントライトに対して選択される位置が最も支配的な影響を常に良く表すわけではないためである。

言い換えれば、ここでの一般的なアイデアはピクセルにピークを持ち込むことであるが、この方法だとピクセルは依然としてピークを取り損なう可能性がある。

  • アイデア:その応答を最大化するために外周上でポイントライトを動かす
    • 支配的係数dominant factor: NHN \cdot H

代わりにしたいことは、エリアライトの外周における異なる位置にポイントライトを動かすことである。

そして、我々はポイントライトが最も強くピクセルを照らすであろう位置を選択する。

Fresnel項、ジオメトリ項、NLN \cdot L項を無視すると、これは法線NNとハーフベクトルHHが最も近いときに起こる。

言い換えれば、我々はNHN \cdot Hを最大化するライトベクトルを探したい。

この問題を解決するため、ちょっとした数学が必要になる。なので、必要な変数を定義するところから始めよう。

法線NN、視線ベクトルVV、反射ベクトルRRがあるとする。

そして、球エリアライトがあるとする。

このライトの中心が単位方向LcL_cにあると仮定しよう。

そして、その距離上の外周の半径に等しい正弦ssで球の形状を定義しよう。

T=R(LR)LR(LR)LL=1s2Lc+sT\begin{gather} T = \frac{R - (L \cdot R) L}{\|R - (L \cdot R) L\|} \\ L = \sqrt{1 - s^2} L_c + sT \end{gather}NH=HL+VL+VN \cdot H = H \cdot \frac{L + V}{\|L + V\|}

さて、LcL_cと正規直交でありつつRRを指す単位ベクトルTTも定義しよう。

ようは、TTLcL_cに垂直な平面へ投影した後に再び正規化された反射ベクトルRRである。

この単位ベクトルTTを使うことで、ライトの外周を指し、反射ベクトルRRに最も近い単位ベクトルLLを簡単に見つけることができる。

すでに得ている正弦ssssから計算できる余弦を用いてLcL_cLLに回転することでこれを行う。

そして、このLLはハーフベクトルHHを計算するのに用いることができる。

これがまさにKarisのアプローチで用いられるハーフベクトルである。

  • 代わりに、LHL \cdot Hを最大化するためにLLを曲げる
B=T×LcL=1s2Lc+s(cos(φ)T+sin(φ)B)\begin{gather} B = T \times L_c \\ L = \sqrt{1 - s^2} L_c + s(\cos(\varphi) T + \sin(\varphi) B) \end{gather}
  • どのようなφ\varphiNHN \cdot Hを最大化するのだろう?

我々はBBと呼ぶ別のベクトルを導入することでこのアイデアを容易に拡張できる。

BB従接線bi-tangentであり、LcL_cTTBBと共に正規直交基底を作る

エリアライトの外周にある任意の点を指す正規化されたライト方向LLを定義するためにこれを用いることができ…

…ある角度φ\varphiにより外周上を回転するそれを含んでいる。

元々の問題に立ち返ってみると、このトリックは今や最も大きいNHN \cdot Hへと導くφ\varphiを見つけることである。

この方法で、陰影付けしたいピクセルに対するより支配的なスペキュラ応答を見つけるだろう。

そして今度は、これがエリアライトにより良い近似をもたらす。

  • NHN \cdot Hを最大化するφ\varphiを解くのは難しい
    • sqrt、sin、cos…
  • 代わりに、等価な問題を解く
    • φ\varphiの代わりにx=tan(φ/2)x = \tan(\varphi / 2)を解く
    • NHN \cdot Hの代わりにf(x)=(NH)2f(x) = (N \cdot H)^2を最大化する
  • f(x)f(x)有理多項式rational polynomialとして書き直すことができる
    • f(x)=ax4+bx3+cx2+dx2+egx4+hx3+ix2+jx2+kf(x) = \frac{ax^4 + bx^3 + cx^2 + dx^2 + e}{gx^4 + hx^3 + ix^2 + jx^2 + k}
  • 繰り返しf(x)f(x)を最大化する…

φ\varphiを直接解くことはむしろ難しい。

これは関係する大量の平方根、正弦、余弦が存在するためである。

しかし、より解きやすい等価な問題にこの問題を再定式化できる。

例えば、角度φ\varphiを解く代わりに、半分のφ\varphiの正接であるxxを解くことができる。

この導出はここで示すには少々長ったらしいtediousが、一般的な半角の公式を用いることで、すべての正弦と余弦を取り除くことができるだろう。

更に推し進めて、NHN \cdot Hの代わりに(NH)2(N \cdot H)^2を最大化しようとすることができる。これは残りの平方根から我々を解放する。

そして、最後に残るものはf(x)f(x)と呼ばれる比較的単純な有理多項式である。

そして、今すべきことはこのf(x)f(x)を最大化するxxを見つけることであり、それはすなわち(NH)2(N \cdot H)^2を最大化する。

残念ながら、これは未だに解析的に解くには非自明な問題である。

しかし、未だに反復的なアプローチを用いて数値的にこれを解くことができる。

  • xxの初期推定値から始める
    • x0=0x_0 = 0
    • f(x0)f(x_0)はKarisのアプローチにおける(NH)2(N \cdot H)^2に等しい
  • ニュートン法を用いてx1x_1を計算する
    • xn+1=xnf(xn)f(xn)x_{n+1} = x_{n} - \frac{f'(x_n)}{f''(x_n)}
  • 我々の最終的な(NH)2(N \cdot H)^2としてf(x1)f(x_1)を用いる

反復的なアプローチは推測から始まり、それを改善しようとする。

では、Karisのアプローチから始めよう。これはx=0x = 0で始めることに等しい。

そして、x1x_1を得るためにニュートン法のイテレーションを一回だけ適用する。

これはすでに最大値にかなり近くなるので、我々は単純に最終的な推定値として用いることとする。

x1x_1f(x)f(x)に接続することは我々が探していた(NH)2(N \cdot H)^2をもたらし、GGX関数への入力として直接これを用いる。

NHN \cdot H最大化なし[Karis 2013Karis, B. 2013. Real Shading in Unreal Engine 4. Physically Based Shading in Theory and Practice course. ACM SIGGRAPH. https://blog.selfshadow.com/publications/s2013-shading-course/.]

float GetNoHSquared(float radiusTan, float NoL, float NoV, float VoL) {
    // radiusTanがディレクショナルライトである場合、radiusCosは事前計算できる。
    float radiusCos = rsqrt(1.0 + radiusTan * radiusTan);

    // Rがディスク内に収まる場合、早期離脱する。
    float RoL = 2.0 * NoL * NoV - VoL;
    if (RoL >= radiusCos)
        return 1.0;

    float rOverLengthT = radiusCos * radiusTan * rsqrt(1.0 - RoL * RoL);
    float NoTr = rOverLengthT * (NoV - RoL * NoL);
    float VoTr = rOverLengthT * (2.0 * NoV * NoV - 1.0 - RoL * RoL);

    // 曲がったライトベクトルに基づいて(N.H)^2を計算する。
    float newNoL = NoL * radiusCos + NoTr;
    float newVoL = VoL * radiusCos + VoTr;
    float NoH = HoV + newHoL;
    float HoH = 2.0 * newVoL + 2.0;
    return max(0.0, NoH * NoH / HoH);
}

実装がどれだけ複雑かを感じてもらうため、これがHLSLでどうなるかをここに示す。

これはKarisのアプローチの実装であり、すべてスカラの計算である。

NHN \cdot H最大化あり

float GetNoHSquared(float radiusTan, float NoL, float NoV, float VoL) {
    // radiusTanがディレクショナルライトである場合、radiusCosは事前計算できる。
    float radiusCos = rsqrt(1.0 + radiusTan * radiusTan);

    // Rがディスク内に収まる場合、早期離脱する。
    float RoL = 2.0 * NoL * NoV - VoL;
    if (RoL >= radiusCos)
        return 1.0;

    float rOverLengthT = radiusCos * radiusTan * rsqrt(1.0 - RoL * RoL);
    float NoTr = rOverLengthT * (NoV - RoL * NoL);
    float VoTr = rOverLengthT * (2.0 * NoV * NoV - 1.0 - RoL * RoL);

    // dot(cross(N, L), V)を計算する。これはすでに計算され、利用可能である。
    float triple = sqrt(saturate(1.0 - NoL * NoL - NoV * NoV - VoL * VoL + 2.0 * NoL * NoV * VoL));

    // 曲がったライトベクトルを改善するためにニュートン法のイテレーションを1回行う。
    float NoBr = rOverLengthT * triple, VoBr = rOverLengthT * (2.0 * triple * NoV);
    float NoLVTr = NoL * radiusCos + NoV + NoTr, VoLVTr = VoL * radiusCos + 1.0 + VoTr;
    float p = NoBr * VoLVTr, q = NoLVTr * VoLVTr, s = VoBr * NoLVTr;
    float xNum = q * (-0.5 * p + 0.25 * VoBr * NoLVTr);
    float xDenom = p * p + s * ((s - 2.0 * p)) + NoLVTr * ((NoL * radiusCos + NoV) * VoLVTr * VoLVTr + q * (-0.5 * (VoLVTr + VoL * radiusCos) - 0.5));
    float twoX1 = 2.0 * xNum / (xDenom * xDenom + xNum * xNum);
    float sinTheta = twoX1 * xDenom;
    float cosTheta = 1.0 - twoX1 * xNum;
    NoTr = cosTheta * NoTr + sinTheta * NoBr;  // NoTrを更新するために新しいTを使う
    VoTr = cosTheta * VoTr + sinTheta * VoBr;  // VoTrを更新するために新しいTを使う

    // 曲がったライトベクトルに基づいて(N.H)^2を計算する。
    float newNoL = NoL * radiusCos + NoTr;
    float newVoL = VoL * radiusCos + VoTr;
    float NoH = HoV + newHoL;
    float HoH = 2.0 * newVoL + 2.0;
    return max(0.0, NoH * NoH / HoH);
}

そして、これがニュートン法のイテレーションを1回分追加したときにどうなるかを示す。

ご覧の通り、これは割と多くの命令の追加を行うが、ライトシェーダ全体と比較すれば、これは比較的小さい変更であるのかもしれない。

これがどれだけ使われるかによって、パフォーマンスの違いはもしかしたら目立つかもしれないし、事実上タダになるかもしれない。

ここではNHN \cdot H最大化ありとなしでの太陽の反射を確認する。

ご覧の通り、小さな角度でより自然に見える反射になっているので、かなりうまく機能している。

しかしながら、このスクリーンショットがHorizon Zero Dawnで作られているとしても、このテクニックはHorizonを出荷した後に出来上がったことは言及しておくべきだろう。

しかし、内部使用のために増設retro-fitされ、他のタイトルのために使う用意が整っている。

高さフォグ(Height fog)

  • 背景
    • フォトリアリスティックな大気atmosphereを必要とする
    • 事前計算された大気散乱モデル[Bruneton and Neyret 2008Bruneton, E. and Neyret, F. 2008. Precomputed atmospheric scattering. Computer Graphics Forum 27, 4, 1079–1086. 10.1111/j.1467-8659.2008.01245.x. https://inria.hal.science/inria-00288758/en.; Elek 2009Elek, O. 2009. Rendering Parametrizable Planetary Atmospheres with Multiple Scattering in Real-Time. Natural Phenomena & Materials. Central European Seminar on Computer Graphics. https://old.cescg.org/CESCG-2009/papers/PragueCUNI-Elek-Oskar09.pdf.]はフォトリアリスティックなゲーム[Hillaire 2016Hillaire, S. 2016. Physically Based Sky, Atmosphere and Cloud Rendering in Frostbite. Physically Based Shading in Theory and Practice course. ACM SIGGRAPH. https://blog.selfshadow.com/publications/s2016-shading-course/.]に対する最適解のひとつである
    • 空気遠近aerial perspectiveはルックアップテーブルに依存する
    • アーティストフレンドリーな高さフォグを必要とする
  • 要約abstract
    • 解析的高さフォグモデル
      • 事前計算された散乱モデルに組み込まれる
      • 現実的な空気遠近&アーティスティックな高さフォグ
  • (コジマプロダクションによるDecima Engineの独自改造)

このセクションでは、コジマプロダクションによって作成されたDecima Engineの独自改造である高さフォグモデルを紹介しようと思う。

まずはじめに、この最適化が必要になった理由を説明させてください。

Decima Engineは雲と屋外ライティングとが接続された柔軟な大気散乱システムを持っている。

しかし、コジマプロダクションでは、フォトリアリスティックなレンダリングに最適化された大気散乱システムに対する要求があった。

事前計算された大気散乱はフォトリアリスティックなゲームに対する最適な手法のひとつである。

このモデルでは、空気遠近は事前計算されたルックアップテーブルに強く依存している。

フォトリアリスティックな大気に対してうまく機能するが、実際のゲームでの濃いdenseフォグや色付きフォグのようなアーティスティックな量を表現する必要がしばしばある。

フォトリアリスティックな空気遠近やアーティスティックな高さフォグをひとつの高さフォグモデルで表現できるなら、それはおそらく良いことだろう。これを達成するため、我々は解析的な高さフォグモデルを導入し、事前計算された大気散乱モデルと組み合わせた。この組み合わせがどのように機能するかをお見せしよう。

高さフォグ: 組み合わせ(Height fog: Combination)

  • ボリュームレンダリング方程式[Chandrasekhar 1950Chandrasekhar, S. 1950. Radiative Transfer.; Jensen 2001Jensen, H. W. 2001. Realistic image synthesis using photon mapping.]
dL(s,ω)ds=σa(s)Le(s,ω)σt(s)L(s,ω)+σs(s)p(s,ω,ω)Li(s,ω)dω\frac{dL(s, \omega)}{ds} = \sigma_a(s) L_e(s, \omega) - \sigma_t(s) L(s, \omega) + \sigma_s(s) \oint p(s, \omega, \omega') L_i(s, \omega') d\omega'

これが我々のアプローチの概要である。

ボリュームレンダリング方程式: 開始地点はボリュームレンダリング方程式であり、地球の大気条件である。 地球の大気条件はRayleigh散乱、Mie散乱、標高で空気密度が指数関数的に減少する関与媒質によって説明される。 事前計算されるモデルと解析的モデルの両方はこれらの条件を共有する。

事前計算された大気散乱モデル: 事前計算された大気散乱モデルは数値積分を行い、in-scatteringと透過率transmittanceの情報をルックアップテーブルに格納する。

高さフォグモデル: 対して、高さフォグモデルはシーンをレンダリングするときに直接フォグを計算する。 この計算は単一散乱を仮定するボリュームレンダリング方程式の解析解である。 この種の解析解は一様フォグモデル[Hoffman and Preetham 2002Hoffman, N. and Preetham, A. J 2002. make better games Rendering Outdoor Light Scattering in Real Time. Game Developers Conference. https://renderwonk.com/publications/gdc-2002/.]として知られるが、高さフォグモデルとして申し分のないsatisfactory解を見つけることはできなかった。

なので、独立してそれを導出した。

それを導出するため、単一の散乱と単一のスケールハイトscale-heightを仮定した。

ここで、スケールハイトは標高でフォグ密度がどれだけ減少するかを述べるためのパラメータである。

組み合わせ: 我々のアプローチでは、事前計算された大気散乱モデルから高さフォグモデルへ天空光skylight太陽光sunlightの情報を提供した。

以上で基本的な概要を終了する。

次のスライドでは、この組み合わせに注目しようと思う。

地球外からextraterrestrialの太陽光

事前計算された大気散乱モデル

Linscatter(LUT)L_{inscatter}^{(LUT)}LbackgroundL_{background}

Lsun(LUT)L_{sun}^{(LUT)}LsunL_{sun}

Lsky(LUT)L_{sky}^{(LUT)}LambL_{amb}

高さフォグモデル

L=Lbackgroundα(y)+(Lsun+Lamb)(1α(y))L = L_{background} \alpha(y) + (L_{sun} + L_{amb})(1 - \alpha(y))

我々が仮定する組み合わせはボリュームレンダリング方程式の階層的な近似である。

まず、太陽光があると仮定しよう。

この地球外からの太陽光は地球に降り注ぎ、空気と相互作用する。

我々の事前計算されたモデルは多重散乱の情報を生成し、LUTのLinscatterL_{inscatter}LsunL_{sun}LskyL_{sky}として格納する。

高さフォグモデルは事前計算されたLUTから背景放射background radiation、太陽光、天空光を受け取る。

適切に事前計算ツールを開発するならば、この種のLUTを達成することは自明のタスクのように思われる。

しかし、問題とは、特に一様でないフォグに対して、高さフォグのような解を得られるか否かである。

続くスライドでは、事前計算されたモデルと高さフォグモデルを詳細に説明しようと思う。

高さフォグ: 事前計算(Height Fog: Precomputation)

我々の事前計算された大気散乱モデルは特別なことは何もないが、LUTは典型的なモデルと異なる。

我々の実装は主にいくつかの最適化[Hillaire 2016Hillaire, S. 2016. Physically Based Sky, Atmosphere and Cloud Rendering in Frostbite. Physically Based Shading in Theory and Practice course. ACM SIGGRAPH. https://blog.selfshadow.com/publications/s2016-shading-course/.]を伴う Bruneton and Neyret [2008Bruneton, E. and Neyret, F. 2008. Precomputed atmospheric scattering. Computer Graphics Forum 27, 4, 1079–1086. 10.1111/j.1467-8659.2008.01245.x. https://inria.hal.science/inria-00288758/en.]Elek [2009Elek, O. 2009. Rendering Parametrizable Planetary Atmospheres with Multiple Scattering in Real-Time. Natural Phenomena & Materials. Central European Seminar on Computer Graphics. https://old.cescg.org/CESCG-2009/papers/PragueCUNI-Elek-Oskar09.pdf.] のモデルに基づく。

事前計算アルゴリズムの詳細については、このスライドに示された参考文献を参照してください。

典型的な事前計算された大気モデルはin-scatterと透過率のLUTを持つ。

しかし、我々は、太陽光と天空光の情報を解析的高さフォグモデルに直接与えたいので、透過率LUTを太陽光と天空光のLUTに置き換える。

ところで、我々は2種の天候をLUTに事前計算した。

ここで、“2種の天候”とは2つの異なる散乱設定を意味する。

これは、ベースとして快晴と濃霧の気象条件があれば、実行時の事前計算なしに様々な気象条件を説明できることを発見したためである。

  • 我々のLUT(一例)
    • Linscatter(LUT)(θs,θv,y,t)L_{inscatter}^{(LUT)}(\theta_s, \theta_v, y, t): (8, 64, 8, 4)、128KB
    • Lsun(LUT)(θs,y,t)L_{sun}^{(LUT)}(\theta_s, y, t): (64, 1, 4)、4KB
    • Lsky(LUT)(θs,y,t)L_{sky}^{(LUT)}(\theta_s, y, t): (64, 1, 4)、4KB

これが我々のLUTの一例である。

これらのサイズは地面上やその近くにいるゲーム用に最適化されている。

3つのLUTとして散乱情報を持っている。

次のスライドでは、これらのLUTが高さフォグモデルでどのように使われるかを見ていこう。

高さフォグ: 解析的モデル(Height fog: Analytic model)

  • 背景放射(空)[Elek 2009Elek, O. 2009. Rendering Parametrizable Planetary Atmospheres with Multiple Scattering in Real-Time. Natural Phenomena & Materials. Central European Seminar on Computer Graphics. https://old.cescg.org/CESCG-2009/papers/PragueCUNI-Elek-Oskar09.pdf.]
    • i{Rayleigh,Mie}i \in \{\text{Rayleigh}, \text{Mie}\}
    • pip_i: 位相関数[][-]
Lbackground=ipi(θsv)Linscatter(LUT)iL_{background} = \sum_i p_i(\theta_sv)L_{inscatter}^{(LUT)i}
  • 高さフォグ
    • βsi\beta_{si}: 散乱係数[m1][m^{-1}]
    • βai\beta_{ai}: 吸収係数[m1][m^{-1}]
    • βti\beta_{ti}: 消散extinction係数[m1][m^{-1}]
      • βti=βsi+βai\beta_{ti} = \beta_{si} + \beta_{ai}
    • HH: スケールハイト[m][m]
    • ss: カメラまでの距離[m][m]
L=Lbackgroundα+(Lsun+Lamb)(1α)Lsun=iβsipi(θsv)iβtiLsun(LUT)Lamb=iβsiiβtiLsky(LUT)α=exp(iβtiHey(0)Hy(s)y(0)(1e1H(y(s)y(0)))s)\begin{gather} L = L_{background} \alpha + (L_{sun} + L_{amb})(1 - \alpha) \\ L_{sun} = \frac{\sum_i \beta_{si} p_i(\theta_sv)}{\sum_i \beta_{ti}} L_{sun}^{(LUT)} \\ L_{amb} = \frac{\sum_i \beta_{si}}{\sum_i \beta_{ti}} L_{sky}^{(LUT)} \\ \alpha = \exp \left( -\frac{\int_i \beta_{ti} He^{-\frac{y(0)}{H}}}{y(s) - y(0)} \left( 1 - e^{-\frac{1}{H}(y(s) - y(0))} \right) s \right) \end{gather}

高さフォグモデルに入る前に、空について一言。

背景放射、すなわち空はin-scatterのLUTと位相関数でできている。

これは Elek [2009Elek, O. 2009. Rendering Parametrizable Planetary Atmospheres with Multiple Scattering in Real-Time. Natural Phenomena & Materials. Central European Seminar on Computer Graphics. https://old.cescg.org/CESCG-2009/papers/PragueCUNI-Elek-Oskar09.pdf.] のモデルと同じである。

それじゃあ、解析的高さフォグモデルを見ていこう。

このモデルは背景と太陽光+アンビエントをα\alphaでブレンドするという形を取る。

LbackgroundL_{background}は背景放射、すなわち空を意味する。

LsunL_{sun}は太陽光の単一散乱成分を述べる。

そして、LambL_{amb}は空からの一定の環境光の成分である。

α\alphaは背景とのブレンド係数である。標高によるフォグ密度の減少はここで説明される。

一見すると、このアルファブレンディング構造はゲームのために調整されたモデルのように見える。

しかし、ボリュームレンダリング方程式からこれを直接導出することができる。

このモデルの導出に興味があるなら、このセッション後に声をかけてください。

/*
    高さフォグのin-scatterと透過率を計算する
        使い方:
        float3 background;
        ...
        float inscatter, transmittance;
        HeightFog(.., inscatter, transmittance);
        background = background * transmittance + inscatter;
*/
void HeightFog(
    HeightFogParam inParam,          // フォグのパラメータ
    float          inDistance,       // 距離 [m]
    float          inCameraPosY,     // カメラ位置の標高 [m]
    float          inWorldPosY,      // ワールド位置の標高 [m]
    float          inSoV,            // dot(sun_dir, view_dir) [-]
    out float3     outInscatter,     // in-scatter [-]
    out float3     outTransmittance) // 透過率 [-]
{
    const float3 beta_t = inParam.mBetaRs + (inParam.mBetaMs + inParam.mBetaMa);

    // 透過率
    float t = max(1e-2, (inCameraPosY - inWorldPosY) / inParam.mScaleHeight);
    t = (1.0 - exp(-t)) / t * exp(-inWorldPosY / inParam.mScaleHeight);
    float3 transmittance = exp(-inDistance * t * beta_t);

    // Inscatter
    float3 single_r = inParam.mAlbedoR * inParam.mBetaRs * Rayleigh(inSoV);
    float3 single_m = inParam.mAlbedoM * inParam.mBetaMs * Mie(inSoV, inParam.mMieAsymmetry);
    float3 inscatter = inParam.mSunColor * (single_r + single_m);
    inscatter += inParam.mAmbColor * (inParam.mBetaRs + inParam.mBetaMs);

    outInscatter = inscatter * (1.0 - transmittance);
    outTransmittance = transmittance;
}

float Rayleigh(float mu) {
    return 3.0 / 4.0 * 1.0 / (4.0 * PI) * (1.0 + mu * mu);
}

float Mie(float mu, float g) {
    // Henyey-Greensteinの位相関数
    return (1.0 - g * g) / ((4.0 * PI) * pow(1.0 + g * g - 2.0 * g * mu, 1.5));
}

struct HeightFogParam {
    float3 mBetaRs;        // Rayleigh散乱の散乱係数 [1/m]
    float  mBetaMs;        // Mie散乱の散乱係数 [1/m]
    float  mBetaMa;        // Mie散乱の吸収係数 [1/m]
    float  mMieAsymmetry;  // Mie散乱の非対称性《asymmetry》ファクタ [-]
    float  mScaleHeight;   // スケールハイト [-]
    float3 mAlbedoR;       // Rayleigh散乱色の制御パラメータ [-]
    float3 mAlbedoM;       // Mie散乱色の制御パラメータ [-]
    float3 mSunColor;      // [-]
    float3 mAmbColor;      // [-]
}

これが高さフォグモデルのサンプルコードである。

高さフォグ: 結果(Height fog: Results)

これがその結果である。

左端から順に説明していこうと思う。

左端は最終結果である。

次は背景放射である。

次はα\alphaを乗算した背景放射である。透過率α\alphaが日没の色を引き起こしているかを確認できる。

次は太陽光の要素である。

右端はアンビエントの要素である。

右の3つを合わせると、左の結果になる。

これはDecima Engineによるサンプルシーンである。

アーティスティックな大気の導入に伴う晴れから曇りにかけての空気遠近を確認できる。

この結果を以て、単一の高さフォグモデルで現実的な空気遠近とアーティスティックな高さフォグを達成する。

1080pでのAA(AA in 1080p)

  • モーフォロジカルAAだけでは複合的な’自然’のシーンに対して十分ではない
    • 曲がっているか、サブピクセル幅
    • すべての草木vegetationはアルファテストされる
    • 透明
  • モーフォロジカルAAは高価となり得る
  • FXAAを補うためにTAAを用いる
テクニック空のみ草のみ
AAなし*0.31ms0.31ms
FXAA0.35ms0.69ms
SMAA0.66ms2.08ms
FXAA+TAA0.65ms0.99ms
  • 我々のすべてのAAテクニックは個別のフルスクリーンUIバッファオーバーレイも適用する。

スーパーサンプリング、マルチサンプリング、ポストプロセッシング、テンポラルAAを含む、アンチエイリアシングを行うとても様々な方法がある。

Horizonでは、FXAAのみを使おうとした…

…そして、しばらくの間はSMAAを使っていたが、ポストプロセッシングに純粋に基づくいかなるテクニックも上手くいかないであろうwouldn’t cut itことは明らかであった。

これらのテクニックは綺麗で明確な入力からジャギーを除去するという点ではかなり良いが、Horizonではそういったものはあまりない。

代わりに、我々のAAソリューションは大きく曲がり、アンダーサンプルされ、曖昧で、アルファテストされ、アルファブレンドされる入力をうまく処理copeする必要がある。

ただし、加えてに言うなら、ポストプロセッシングテクニックは想定以上に高価であったり、シーン依存であったりする可能性もある。

例えば、SMAAはHorizonでは草を覗き込むと最大2ミリ秒かかることがある。これは空を見上げるよりも3倍遅い。

明らかに、モーフォロジカルポストプロセッシングテクニックの代わりに、例えば、マルチサンプリングテクニックか解析的テクニックを検討することが可能だったかもしれない。

しかし、我々はトータルで1ミリ秒以下のコストとなる何かを探していたので、単純にそれがもたらす複雑さやパフォーマンスのオーバーヘッドを受け入れることはできなかった。

そこで、その他大勢のように、複数フレームにかけてアンチエイリアシングを効果的にに行うテンポラルアンチエイリアシングに方向転換した。

そして、最終的には、FXAAと我々のTAAのバリエーションの組み合わせに決定し、できるだけ効果的にそれぞれの強みを用いる。それを使って、これを得る…

そして、必要な最適化ののち、FXAA+TAA+UI合成システムはPS4において1080pでフレームあたり最大1ミリ秒かかっている。

より詳細な比較を見ていこう。

強調したエリアに注目しよう。

これはAAテクニックを使っていないエリアを拡大したものである。

ここには、階段の上のジャギー、石の間のアンダーサンプリング、髪の粗いルックがはっきりと見える。

そして、これはFXAAありのものである。若干役立っているが、階段stair stepsの間の水平な線のような、明らかな直線でジャギーを隠すのみである。

これはSMAAありのものである。これは前のものより若干良い。

だが、依然としてこれらのエリアを修正していない。

そして、これがFXAA+TAAのものである。TAAは奇数フレームと偶数フレームとで異なるサンプル位置を使い、2フレームの後にこれを解決する。どれだけアンダーサンプルされたエリアが閉じられ、滑らかになったかに注目してほしい。

暗い斜めの線の周囲の若干のハローhaloにも恐らく気が付くだろう。これは少しだけTAAの出力を鮮鋭化しているためである。

我々のTAAはさらなる詳細をサンプリングするのに役立つだけではない。

時間的な安定性にも役立つ。

ここにHorizonの典型的な環境の短いクリップがある。

少し拡大してみよう。

これはAAなしである。短い草と長い草のルック両方のノイズとフリッカーの具合に注目してほしい。

これはSMAAである。これは明らかに役立っているが、まだかなりノイジーである。

これはTAAである。すべてのルックがどれだけ落ち着いているかに注目してほしい。

  • TAAは前フレームのデータが必要であるが、Horizonは以下があった
    • 大量の薄いジオメトリ
    • 他すべての後に適用されるAA
    • 低クオリティのモーションベクトル

TAAは多くを助けてくれた。しかし、確実に動作させることはちょっとした挑戦であった。

それはすべてのTAA実装がある種の履歴の再利用の形式に依存しているためである。

そして、これは一般的に、都度累積した大量のフレームによって行われる。そして、これは上手く機能する可能性がある。

しかし、Horizonではうまく行かなかった。

その理由は、我々のAAがとても薄いジオメトリにしばしば適用されたり、さらに、大量の視覚効果やすべてのポストエフェクトがすでにオーバーレイされた後にのみAAを適用するためである。

そこには被写界深度や口径食のような効果が含まれる。

最後に、我々はハーフ解像度のモーションバッファをTAAの再投影の基盤としており、これは恐らく理想的ではない。

ある種のオブジェクトはモーションベクトルをまったく出力しない。

  • TAAは前フレームのデータが必要であるが、Horizonは以下があった
    • 大量の薄いジオメトリ
    • 他すべての後に適用されるAA
    • 低クオリティのモーションベクトル
  • フィードバックループを伴う累積した履歴がない
    • 現在と手前のレンダリングそのままを入力として取るだけ

これらの理由のため、我々はあまりにも前のフレームを使わないことを好む。

そして、前フレームからの出力を累積する何かの代わりに、出力ではなく、前フレームからAAシステムへ入力のみを再利用することを選択した。

これは、前フレームと現フレームと生のレンダリング結果を用い、それ以外は用いない。

そうすると、誤ったリジェクトを引き起こす長いゴーストのtrailを軽減し、2フレームのみで安定した最終結果に到達する。これにより、我々のテクニックの応答性をとても高速にする。

  • TAA: 2つのフレームでピクセルあたり4xエッジサンプル、適応的な1D鮮鋭化付き

我々の解決法の詳細を見ていこう。

ひとつに、ピクセル中心でレンダリングを一切せず、ピクセルのエッジでのみ行う。

実際には、奇数フレームではピクセル中心の水平の間を、奇数フレームではピクセル中心の垂直の間をレンダリングする。

この正確なパターンにおいてカメラをジッタリングすることによりこれを行い、EQAAのようなマルチサンプルハードウェアのいかなる形式にも依存しない。

つまり、ピクセルシェーダの処理や出力を変えずとも良い。

ジッタしたフレームをレンダリングした後、すべてのポストエフェクトを適用し、ピクセルのエッジにおけるこれらのサンプルでFXAAを実行する。

FXAAの後、そのサンプルはピクセルのエッジからピクセルの中心へリサンプルされる。

各ピクセルの中心に最も近い2つのサンプルを単純に平均化することでこれを行うことができるかもしれない。

しかし代わりに、我々はテクスチャブラーを打ち消すためのネガティブローブを持つ4タップのリサンプリングカーネルを用いる。

これは若干Catmull-Romリサンプリングのようだが、より安価であり、鮮鋭化量は局所的なコントラストに基づいて決定される

最終解決は鮮鋭化した現フレームと鮮鋭化した前フレームをマージしようとする。

履歴が許容される場合、これは事実上ピクセルあたり4つのデータのサンプルを与える。

履歴がリジェクトされる場合、ピクセルあたり2つのFXAAですでに扱われたサンプルが使える。

実装の詳細として、実際にはこれらすべてを行うのに2パスを実行するだけである。

まず、我々はFXAAパスを実行する。

そして、第2パスで、現フレームを鮮鋭化し、2つの履歴バッファのひとつに出力する。

その間に、同じシェーダ内から、前フレームの鮮鋭化バージョンを含むもう一方の履歴バッファからも読み取り、それを用いてリジェクトまたは再投影のいずれかを行う。

履歴からのピクセルが再投影される場合、現在の鮮鋭化したフレームと半々でプレンドされ、最終バックバッファに出力される。

履歴サンブルがリジェクトされる場合、できるだけ上手に潜在的なエイリアシングを隠すために、鮮鋭化せずに現フレームのピクセルを単純に出力する。

  • サンプルパターンはFLIPQUAD[Akenine-Möller 2002Akenine-Möller, T. 2002. FLIPQUAD: Low-Cost Multisampling Rasterization.]に似ている、が
    • より鮮鋭
    • 追加のMIPMAPロジックなし
    • MSAA/EQAAハードウェアを必要としない
    • 線やグリッドからジャギーを除去する点で劣る
      • FXAAで解決される
  • [Drobot 2014Drobot, M. 2014. Hybrid reconstruction anti-aliasing. Advances in Real-Time Rendering in Games course. ACM SIGGRAPH. https://advances.realtimerendering.com/s2014/drobot/HRAA_notes_final.pdf.]で議論される

あなたは、2つのフレームを上手くマージしたり鮮鋭化を無視したりしようとして、左図に見えるサンプルパターンを実質的に取得することに気づいたかもしれない。

ある点では、特許取得済のFLIPQUADテクニックにすこし似ている。これもピクセルのエッジにあるサンプルを使う。

しかし、

  • 我々のパターンは、サンプルが中心により近いので、少しだけ鮮鋭である。
  • 2フレームかけてFLIPQUADパターンをレンダリングするのと異なり、正しいMIPMAPサンプリングを得るのに追加のロジックや機能を必要としないが、これは2015年にMichal Drobotによってすでに示唆されていた。
  • 我々のバターンのもうひとつの利点は正しいサンブル位置でレンダリングするのにマルチサンプル機能を必要としないことである。我々はフレームごとにカメラをジッタリングすることを必要とするだけである。
  • しかし、最大の欠点は直線や矩形のような非常に規則的な直線のパターンからジャギーを取り除くという点においては劣るということであるが、これは我々が適用するFXAAパスにより大幅に修正される。

以上。テンポラル再投影や近傍ベースリジェクションの基準のようなTAAソリューションの他の側面では、いくつかのより昔ながらのアプローチを用いたので、ここでは触れない。

Ps4 Proでの2160pチェッカーボード(2160p checkerboard on PS4 Pro)

比較してみよう。

左は前に示したのと同じ画像である。これはFXAA+TAAソリューションを用いる1080pのスクリーンショットを拡大したものである。

真ん中は2160pで理想として得られるであろうものである。これは、特に16xスーパーサンプリングを用いるとき、PS4 Proにおいて30fpsで動作しないだろう。

なので、これはパフォーマンスが問題にならなかった場合に理想として達成するであろうものを示している。

右は、PS4 Proにおいて30fpsを得られるかもしれないものである。これは1080pのと同じAAテクニックを用いるネイティブ1512pの画像である。

1080pより明らかに鮮鋭であるが、リファレンスでは見える多くのディテールが失われていることに注目してほしい。

では、我々のチェッカーボードレンダリングとこれを比較するとどうだろう?

(左に見える)我々のチェッカーボード実装もネイティブ1512pモードのように30fpsで動作するが、明らかに更に詳細に描かれている。実際に、リファレンスにも描かれている詳細はすべて示されている。

しかし、16xスーパーサンプルしたリファレンスとチェッカーボードの結果の鮮鋭さを比べると、差異が見られる。

これは、鮮鋭さよりもディテールを重視する、という我々がチェッカーボードレンダリングを行う方法に固有の特性である。

我々はその2つから選ぶことを強いられる場合にはこのやり方を好む。

鮮鋭さは近くから像を見ているときには素晴らしい。

しかし、実際のディテールは、見ている距離の更に広い範囲に渡って、見えているか、少なくとも示唆されている傾向にある。

そして、これは、例えば1080pのTVにダウンスケールしなければならないとき、最終的な画像品質へより大きく貢献する。

我々のチェッカーボードソリューションは時間的に極めて安定である。

ここに簡単な比較がある。

FXAA+TAAを伴う1080pで、前に示したのと同じシーンから始めよう。

そして、これは2160pチェッカーボードレンダリングでレンダリングした。

そして、ビジュアルクオリティについてどれだけ似ているかに注目してほしい。

そして再び、1080p。

そして、2160p。

  • 典型的なチェッカーボードレンダリング
    • フレームごとにピクセルの50%をレンダリングする
    • 毎フレームサンプル位置を互い違いにする
    • 隙間を’利口にintelligently’埋めるためにネイティブ解像度のヒントをレンダリングする
      • 深度バッファ
      • トライアングルインデックスバッファ
      • アルファテストのカバレッジ

さて、ではこれはどうやって動作するのだろ?

チェッカーボードレンダリングでは、一般的にピクセルの50%をシェーディングする。

もう50%は次のフレームでシェーディングされる。

つまり、理想的には、2フレーム後にネイティブ解像度の結果を得る。

しかし、動的なシーンだとこの処理が幾分か複雑になる。

これがフレームごとにネイティブ解像度のヒントもレンダリングするであろう理由である。

ヒントは深度、または、プリミティブIDのようなものである。

そして、これをさらに複雑にするのが、

アルファテストおよびアルファブレンドされるオブジェクトは依然としてフレームごとにネイティブ解像度でサンプルおよび処理される必要があるかもしれない、というものだ。

  • ネイティブ解像度のヒントは2160p @ 30HzのHorizonに対しては高価すぎる
    • UIと最終バックバッファのみネイティブ解像度である
    • すべてがネイティブ解像度ヒントなしで2160pチェッカーボードを実行する
    • 標準の解決を破壊する…

そのネイティブ解像度のオーバーヘッドのすべてがHorizonを30fpsで実行することを妨げている。

明らかに、我々は他の多くのタイトルがしていることを行うことができたかもしれない。それは、代わりとしてチェッカーボードの1800pで単純にレンダリングすることである。

そして、それは多分同様かそれ以上の鮮鋭さをもたらしただろうが、その画像はディテールがより少なく、よりフィルタのかかったようなルックになっただろう。

その代わりに、我々は、潜在的な鮮鋭さを犠牲にして、2160pの詳細度をもたらし、30fpsで動作するであろう方法を探すことを選択した。

結局それを行う方法を発見した。最終的にはすべてのネイティブ解像度のヒントを取り除いて、UIと最終バックバッファのためだけにネイティブ解像度のバッファを使った。

アルファテストされるオブジェクト、アルファブレンドされるオブジェクト、ポストエフェクトを含む、その他すべてはチェッカーボードの解像度で動作する。

もちろん、いずれにしても常に低解像度で動作するいくつかのエフェクトを除いて。

これらすべてを行い、パフォーマンスの問題を御した。しかし、我々は依然として典型的なネイティブ解像度ヒントを持たないチェッカーボードデータを解決する方法を見つけなければならない。そして、1080pでTAAを行う方法にいくつかのインスピレーションを発見した。

  • フレームごとにすべてのピクセルコーナーの50%をレンダリングする
    • そして、利用可能なコーナーを平均する
    • すべてのピクセルが同列に扱われる
      • ネイティブ解像度ヒントを必要としない
      • モーフォロジカルなトリックを必要としない
      • ディザーパターンを必要としない

通常、チェッカーボードレンダリングはピクセル中心の50%をレンダリングするのに使われる。

もう50%はネイティブ解像度ヒント、履歴バッファ、現在のチェッカーボードバッファをもとに推定される。

代わりに我々が行うことはピクセルコーナーの50%をレンダリングすることである。

2ピクセルあたり1サンプルではなく、1ピクセルあたり2サンプルで処理させる。

そうすることで、2つの最も近いコーナーのサンプルを単純に平均して、ピクセル中心での値を得ることができる。

さらには、これはネイティブ解像度ヒントやモーフォロジカルなトリックさえも必要としない。

すべてのピクセルが同じアプローチで扱われるので、チェッカーボードレンダリングにしばしば関連するディザーアーティファクトを暗黙的に軽減する。

そして、2フレーム後…

  • 2フレームで1ネイティブピクセルあたり4サンプル
    • 我々の1080pのTAAに似たAA安定性

…ピクセルあたり4サンプルを得るので、結果として1080pで得られるTAAと同じAA安定性になる。

しかし、TAAはピクセルのエッジのサンプルで動作するのに対し、これはコーナーのもので動作するという一点が異なる。

このようにチェッカーボードの解決を行うことは高速で簡潔であり、2フレームの後なら、ネイティブ2160pのレンダリングから期待するであろうすべての2160pのディテールを含んでいる。

しかし、単一のフレームのみでも、その結果はかなりちゃんとしている。

  • 2つの最も近いピクセルコーナーを平均化すると、ジャギーから本質的にinherently解放される

ここに例を示す。

左図では、ネイティブ解像度でほぼ垂直の特徴がある。

チェッカーボードレンダリングでは、実際には中央にあるものをレンダリングするのみである。

2つの最も近いピクセルコーナーを平均化することでピクセル中心を解決したあと、右にあるものを得る。

左のものにかなり近いことに注目してほしい。

異なるのは、少しだけ鮮鋭さが小さいことのみである。しかし、これは我々のテクニックに固有のものである。

  • 単一フレームの対角線が依然として問題になる

一般に、このトリックはすでにとても上手く機能している。

そして、1080pのソリューションと比べて明らかに改善している。

しかし、対角線ではさらなる改善の余地が残されているit can use some extra work

これは対角方向がチェッカーボードレンダリングにおいて最も疎な方向となるためである。

ここに見える例では、その’コーナーの解決’が対角線のジャギー的見た目のいくつかを引き起こす可能性があることがはっきり確認できる。

  • 単一フレームの対角線が依然として問題になる
  • FXAAが対角方向に適用される

これを解決するため、コーナーの解決を適用する前にFXAAを適用する。

右下のを見ると、これがかなり役立っているということがはっきりと分かる。

解決されたネイティブ解像度のピクセルではなく、チェッカーボードのピクセルにFXAAを適用することに注意してほしい。

これはその方が2倍速いというだけでなく、かなり良好に機能するためである。

特に、より一般的な水平線や垂直線ではなく対角線に焦点を当てるためにFXAAを騙すtrickためでもある。

その入力を事実上回転することでこれを行う。そして、その方法を示す。

  • FXAAと履歴の再投影は四角ピクセルへの高速アクセスを必要とする

FXAAを適用するため、また、前フレームからチェッカーボードレンダリングを再投影するため、チェッカーボードデータを直接使いたくない。

それは、チェッカーボードレンダリングではピクセルが四角でないためである。これらは引き延ばされ、一種のジグザグのように振る舞う。

ただし、より意味のある形にこれを解釈し、補間するために使えるトリックがある。

チェッカーボードのサンプル位置とそれらが生成するVoronoi領域のみを見るとき…

  • FXAAと履歴の再投影は四角ピクセルへの高速アクセスを必要とする
    • Voronoi領域の中心としてチェッカーボードのサンプルを解釈する

…45度回転したピクセルとしてチェッカーボードのサンプルを解釈できる。

  • FXAAと履歴の再投影は四角ピクセルへの高速アクセスを必要とする
    • Voronoi領域の中心としてチェッカーボードのサンプルを解釈する
    • これらのひし形は四角いピクセルに回転して戻すことができる

しかし、同様にフレームバッファを45度回転すると、結果として再び四角いピクセルとなる。

回転したフレームバッファを代わりに格納できるとしたら、単にこれを用いて、バイリニアフィルタリングでサンプルし、FXAAを適用し、再び平易な再投影を行うことができるかもしれない。

欠点は、もちろん、これがもはや通常のバッファやテクスチャのようには見えないことである。

しかし、我々はこれを修正することができる。

  • 四角の’タングラムtangram’テクスチャに詰めることができる
    • ロスのないスキーム
    • 2160x2160のテクスチャにパックされる
  • バイリニアルックアップをサポートする
  • ネイティブ解像度のUVをクランプ、回転、平行移動する
    • タングラムUVを得るため
    • 残りはハードウェアラッピングwrap aroundが行う

この回転したバッファを我々が’タングラム’と呼ぶものに変換できる。このようないわゆるパズルゲームの一種なので、我々はこれをタングラムと呼ぶ。

回転したバッファをパーツにカットし、このようにこれらをあちこちに移動することができる。

これの良いところは完璧にロスがなく、2160pのチェッカーボードデータがコンパクトな2160x2160テクスチャに再びパックさせることが可能になるという点である。

そして、これらのパーツを配置した正確な方法のために、サンプリング中に追加のロジックやシェーダ命令を使わずに、ビルトインのテクスチャラッピングハードウェアを用いてアンラップすることができる。

サンプリング中に唯一必要なことはネイティブ解像度UVを45度回転して、フレームごとに固定のオフセット値でこれらをオフセットすることである。

このタングラムに実際にはいくつかの詰物があることに気づいたかもしれない。

それはこの正確な入れ替えの副次的効果であり、ラッピングが正しく動作するようにするのに必要とされる。

しかし、結果として、タングラムは通常のチェッカーボードバッファより12%多くメモリを消費する。

とはいえ、詰物をサンプルする訳はないので、帯域幅をまったく消費しない。

  • 3つの回転したクアッドを用いてレンダリングされる
    • 正しいピクセルに’ポイントサンプリング’スナップする
    • もうひとつのフルスクリーンパスを組み合わせ可能
      • 潜在的にタダにする

詰物エリアに書き込む必要もない。

実際に、タングラムは3つの正確に位置取りされたクアッドのみを用いてレンダリングできる。

この方法で、すべてのチェッカーボードピクセルはタングラム内のひとつの位置に正確にレンダリングされる。

パイプラインに依存して、既存のフルスクリーンパス内からこの処理を適用することさえできるかもしれないので、このタングラム変換は実質的にタダとなる。

  • まとめ
    • チェッカーボード解像度でレンダリング、ポストプロセスする(1920x2160、知覚的な10.10.10.2)
    • (YCoCgの)タングラムに変換する(2160x2160)
      • 次のパスのためのYCoCg: 1回のgatherRedで4つの輝度サンプル
    • 通常のFXAAを適用する
      • タングラムのために、対角線からジャギーを除去する
    • 現在のタングラムと再投影された履歴タングラムをブレンドして、2160pに出力する

この話の終わりに、我々のチェッカーボードレンダリングアプローチをまとめよう。

  • まずネイティブ解像度ヒントを一切使わずにチェッカーボード解像度でレンダリングする。その時点ですべてのポストエフェクトを適用し、トーンマッピングを行い、sRGBかPQのいずれかに出力をエンコードし、10ビットフォーマットに結果を格納する。
  • タングラムに変換し、YCoCg色空間に格納する。
  • YCoCgタングラムは新しいタングラムを出力するFXAAパスでサンプルされる。YCoCgを使う理由はFXAAがほとんどの処理を輝度データで行うためであり、これはテクスチャギャザーあたり4つの輝度値をサンプルできるようにする。
  • FXAAパスの出力はピンポンされるバッファに格納され、現フレームと前フレームを受け取り、それらで解決を行う。

最終パスとして、現フレームと前フレームを取り、RGBに変換して、標準的な基準に基づいてリジェクトや再投影する。

履歴がリジェクトされる場合、現フレームのみをサンプルするので、実質的にピクセルあたり2サンプルを得る。

そして、履歴が受け入れられる場合、前の値と今の値を50対50でブレンドし、実質的にピクセルあたり4サンプルを得る。

概念的には、最後の部分が1080pでのTAAソリューションにかなり似ているが、鮮鋭さは持たない。

これが2ms未満でチェッカーボードモードで解決する方法である。

付録: 球エリアライト(Appendix: Spherical Area Lighting)

球GGXエリアライトのコード例

float GetGGXFromNDotHSquared(n_dot_h_squared, float alpha) {
    float alpha_squared = alpha * alpha;
    float partial_denom = n_dot_h_squared * (alpha_squared - 1.0) + 1.0;
    return alpha_squared / (PI * partial_denom * partial_denom);
}

float3 EvaluateSphericalAreaGGX(float n_dot_l, float n_dot_v, float v_dot_l, float l_dot_h, float_light_radius, float light_center_distance, float alpha, float3 fresnel0) {
    float radius_tan = max(0.001, light_radius / light_center_distance - alpha * 0.25);  // ライト半径は粗いマテリアルで小さくなる
    float n_dot_h_squared = GetNoHSquared(radius_tan, n_dot_l, n_dot_v, v_dot_l);
    float density = GetGGXFromNDotHSquared(n_dot_h_squared, alpha);
    float visibility = GetGGXVisibility(n_dot_v, n_dot_l, alpha);  // 'visibility'はGGXの幾何項4 * n_dot_v * n_dot_lで割ったもの
    float3 fresnel = GetFresnel(l_dot_h, fresnel0);  // Schlickの近似式を使う
    float3 brdf = density * visibility * fresnel;

    // Karisのアプローチと我々のアプローチの両方は、正規化がおおよそであるため、真にエネルギーを保存していない。
    // 正確さを改善しようと様々な正規化の方程式を実験している。これはそのうちのひとつである。
    float alpha_squared_l_dot_h = alpha * alpha * (l_dot_h + 0.001);
    float normalization = alpha_squared_l_dot_h / (alpha_squared_l_dot_h + 0.25 * light_radius * (3.0 * alpha + light_radius));

    // エリアライトの大きさで正規化してN dot LをかけたBRDFを返す。N dot Lはその形状やエリアライトの大きさで調整されていない。
    // 終端で0を得る単純な方法としてこれを行い、潜在的なシャドウアクネ《shadow acne》を軽減する。
    return normalization * brdf * saturate(n_dot_l);
}

付録: チェッカーボードレンダリング(Appendix: Checkerboard Rendering)

バイリニアサンプリングの使用に基づいてタングラムをサンプルするコード例

// ネイティブ解像度の出力ピクセルのUVを取得し、異なるタングラム部分/詰物エリアとブレンドしないようにするために|最も外側《outer most》のピクセルを繰り返す。
// ボーダーの距離は多少の安全な近傍サンプリングが可能になるように選ばれるが、この詳細は実装固有である。
int2 native_pos = (int2)(uv * float2(native_width, native_height));
native_pos.x = clamp(native_pos.x, 1.0, native_width - 3.0);
native_pos.y = min(native_pos.y, native_height - 3.0);

float is_odd_frame = ...;  // 奇数フレームで1、偶数フレームで0

// タングラムUVを取得し、タングラム内の最も近い2つのコーナーサンプルの中間を正確に指す。
float2 tangram_uv = float2(-1.0 + is_odd_frame + native_pos.x - native_pos.y, 2.0 + is_odd_frame + native_pos.x + native_pos.y) * (0.5 / native_height);

// 単純な解決を行う。
float4 tangram_color = tex2Dlod(tangram_texture, bilinear_sampler, tangram_uv, 0.0);

チェッカーボードからタングラムへのレンダリングの頂点シェーダを生成するコード例

struct Vertex {
    Vec3 mPos;
    Vec2 mUV;
    Vertex(const Vec3& pos, const Vec2i uv) : mPos(pos), mUV(uv) {}
};

void GetVerticesForTangramRendering(int native_width, int native_height, bool is_even_frame, Vertex* out_vertices) {
    ASSERT(native_width == (native_height * 16) / 9);
    float half_width = 0.5f * (float)native_width;
    float half_height = 0.5f * (float)native_height;

    // 3つの45度回転したクアッドを準備して、一度に各チェッカーボードピクセルをカバーするように配置する。
    for (int i = 0; i < 3; ++i) {
        float x = (float)native_width * (i == 2 ? 1.0f : 0.0f) + (is_even_frame ? -0.5f : 0.0f);
        float y = (float)native_height * (i == 1 ? -1.0f : 0.0f) + (is_even_frame ? 0.0f : 0.5f);
        out_vertices[4 * i + 0] = Vertex(Vec3(x, y, 1.0f), Vec2(0.0f, 0.0f));
        out_vertices[4 * i + 1] = Vertex(Vec3(half_width + x, half_height + y, 1.0f), Vec2(1.0f, 0.0f));
        out_vertices[4 * i + 2] = Vertex(Vec3(half_width - half_height + x, half_width + half_height + y, 1.0f), Vec2(1.0f, 1.0f));
        out_vertices[4 * i + 3] = Vertex(Vec3(-half_height + x, half_height + y, 1.0f), Vec2(0.0f, 1.0f));
    }
}

Footnotes

  1. 訳注:in-scatteringとは、周囲の光が媒質内へ入ってきて起こる散乱のことで、その散乱した光は出射方向へ反射して最終的な輝度に合算される。(参考

  2. 訳注:θsv\theta_{sv}は光源方向と視線方向とのなす角を表す。