24. リアルタイムの影・アンビエントオクルージョン・大域照明

第11章では、シャドウマップ、バイアス、PCF、そしてアンビエントオクルージョンの入り口を学びました。
第12章では、レンダリング方程式、ラジオシティ法、フォトンマッピング法、イメージベーストライティングを学び、最後にゲームが代わりに何をしているかを短く挙げました。
どちらの章も続きを約束していました:遮蔽物からの距離と共に正しく広がる半影を持つソフトシャドウ、きちんとしたアンビエントオクルージョン、そしてリアルタイムのエンジンが実際に使っている大域照明の工夫です。
此の章で其の約束を果たします。

何故此れが大事なのでしょうか?
ゲームには60フレーム毎秒で1フレーム約16.7 msしか無く、照明に使えるのは其の一部です。
此の章の手法は全て、レンダリング方程式の一部を安く計算する物です:光源の可視性(影)、「空」の可視性(アンビエントオクルージョン)、他の面で跳ね返った光(大域照明)。其々にコスト、品質、シーンのどれだけが動いて良いかの間のトレードオフがあります。
此れは第2部の内容で検定のシラバスを超えますが、其の2つの項目、影付け(第11章)と大域照明計算(第12章)、そしてマッピングのライトマップと環境マップを深めます。
第23章のモンテカルロ推定と重点的サンプリング、第12章の放射照度と放射輝度を使います。

24.1 問題を分ける

第12章のレンダリング方程式を思い出しましょう。法線𝐧を持つ点𝐱について:

Lo(𝐱,ωo)=Le+∫Ωfr(ωi,ωo)Li(𝐱,ωi)(𝐧⋅ωi)dωi

リアルタイムのレンダラーは入射光Liを2つに分け、全く別の道具で扱います:

  • 直接光は光源から真っ直ぐ届きます。積分は光源についての和になり、難しいのは各光源の可視性Vだけです。此れが影です(24.2〜24.4節)。
  • 間接光は少なくとも1回他の面で跳ね返っています。全方向から届くので半球上の積分になります。此れが大域照明で(24.6〜24.9節)、アンビエントオクルージョン(24.5節)は其の可視性の部分の一番安い近似です。

此の分け方と交差するもう1つの分け方があります:事前計算(ベイク)出来る物と、毎フレーム計算しなければならない物です。
ベイクは実行時に安く、綺麗な結果が出ますが、動かない物にしか使えません。リアルタイムGIの歴史の多くは、GPUが速くなるにつれて仕事を前者から後者へ移して来た歴史です。

24.2 カスケードシャドウマップ

11.4節の最後に、太陽のシャドウマッピングの一番の弱点を見ました:透視エイリアシングです。
シャドウマップはテクセルを均等に配りますが、カメラは近くの地面を遠くの地面よりずっと高い密度で見るので、250 mの視距離を1枚で覆うと、テクセルはカメラの近くでは大き過ぎ、遠くでは無駄に小さくなります。

標準的な解決策がカスケードシャドウマップ(CSM)です。
カメラの視錐台を視線方向に沿って距離C0=n<C1<⋯<CN=fでN個(カスケード。普通は3つか4つ)に切り、其々に自分のシャドウマップを与えて、其のスライスにぴったり合わせます。
近いスライスは小さいのでテクセルも小さく、遠いスライスのテクセルは大きいですが遠くにあります。
ピクセルをシェーディングする時、シェーダーはピクセルのビュー深度を含むスライスのカスケードを選びます(継ぎ目を隠す為に、1つのカスケードの最後の数パーセントを次のカスケードと混ぜる事も多いです)。

分割の位置

分割距離には3つの標準的な方式があります:

Ciuni=n+(f−n)iN,Cilog=n(fn)i/N,Ci=λCilog+(1−λ)Ciuni

均等分割は各カスケードに同じ深度の幅を与えます。
対数分割は各カスケードの遠い側と近い側の距離の比を同じにします:Ci/Ci−1=(f/n)1/N。
実用分割方式は2つを0から1の重みλで混ぜます。2006年にZhangらが並列分割シャドウマップ(parallel-split shadow maps)の為に提案し、Rouslan DimitrovのNVIDIAのカスケードシャドウマップのホワイトペーパー(2007年)も其れでカスケードを配置しています。

何故対数分割が理想なのでしょうか?
大まかなモデルで分かります。
カメラの垂直視野角が90°、画像の高さが1080ピクセルとします。
距離zでは視野の高さは2ztan45°=2zなので、1ピクセルはz/540を覆います。
カスケードの2048 × 2048のシャドウマップは其のスライスを覆わなければならず、スライスは遠い端Ciで約2Ciの幅なので、1テクセルは約2Ci/2048=Ci/1024を覆います。
一番悪いのはテクセルが一番大きく見えるスライスの近い端、z=Ci−1です:

texelpixel≈Ci/1024Ci−1/540=0.527CiCi−1

従ってカスケードの品質は比Ci/Ci−1だけで決まり、対数分割は其の比を全てのカスケードで同じにします。

計算例。 n=1 m、f=256 m、N=4個のカスケード。

  • 均等:Ci=1+255⋅i4で、1、64.75、128.5、192.25、256。
  • 対数:f/n=256、2561/4=4なので、1、4、16、64、256。
  • 実用(λ=0.5):2つの平均で、4+64.752=34.375、続いて72.25、128.125、つまり1、34.375、72.25、128.125、256。

均等・実用・対数の各方式による、1から256の間の4つのカスケードの分割距離

比とテクセル対ピクセルの見積もりは次の通りです:

方式 C1/C0 C2/C1 C3/C2 C4/C3 最悪のテクセル/ピクセル
均等 64.75 1.985 1.496 1.332 34.1(カスケード1)
対数 4 4 4 4 2.11(全て)
実用、λ=0.5 34.375 2.102 1.773 1.998 18.1(カスケード1)

均等分割では、カメラの近くで1つのシャドウテクセルが約34ピクセルを覆います。プレイヤーの目の前の影がカクカクになり、一方で最後のカスケードは解像度を無駄にします(比0.527⋅1.332=0.70、テクセルがピクセルより小さい)。
対数分割は完璧に釣り合っていますが、最初のカスケードは1〜4 mしか覆わないので、プレイヤーが見る物の殆どは後のカスケードに入り、小さな最初のスライスは光源空間でぴったり合わせるのが難しくなります。
実用分割方式は其の中間で、λは調整用のつまみです。
4つの比の積は、方式に関係無く常にf/n=256である事に注意して下さい。品質は移動させられるだけで、作り出す事は出来ません。
ニアプレーンが非常に大事なのも其の為です:nを0.1 mから1 mにするとf/nは10分の1になります。

安定カスケード

光源の視錐台は毎フレーム、カメラのスライスに合わせ直されるので、カメラが動いたり回ったりすると、テクセルの格子がテクセルの端数だけ動き、遮蔽物が少しずつ違う位置で標本化され、影の輪郭が這う様にちらつきます:ちらつき(シマリング)です。
安定カスケードは2つの規則で此れを直します:

  • 各カスケードを、ぴったりの箱ではなくスライスのバウンディング球で合わせます。球の大きさはカメラが回っても変わらないので、テクセルの大きさが一定に保たれます。
  • 光源空間での視錐台の位置を整数テクセルにスナップします。すると、テクセルの格子は常に整数テクセルずつしか動かず、全ての遮蔽物が全く同じ様にラスタライズされます。

計算例。 カスケードのバウンディング球の半径が32 mなら、マップは64 mを2048テクセルで覆い、1テクセルは64/2048=0.03125 mです。
視錐台の中心が光源空間でx=10.37 mになったら、其れは10.37/0.03125=331.84テクセルです。スナップすると⌊331.84⌋=331テクセル、つまり331⋅0.03125=10.34375 mになります。
球はスライスより大きいので解像度を少し無駄にしますが、其れが安定の代償です。

其の他の細かい点

カメラのスライスの外にある遮蔽物(太陽とスライスの間の高いビル)もシャドウマップに入らなければならないので、其の深度は光源の視錐台のニアプレーンにクランプされ(「パンケーキング」)、11.4節の深度バイアスはテクセルの大きさが違うのでカスケード毎に調整する必要があります(深度バイアスは深度精度と共に第19章で扱います)。
他の手法(歪めたシャドウマップや、巨大なマップのうち見えるテクセルだけを読み込む仮想シャドウマップ)もありますが、執筆時点ではカスケードが共通の基本です。
OpenGLシリーズの第23章では1枚のシャドウマップを作りました。カスケードは同じパスをN個の光源行列で繰り返す物です。

24.3 PCSSによるソフトシャドウ

11.1節では相似な三角形で半影の幅を求めました:幅Lの光源が高さhLに、遮蔽物の縁が高さhoにあれば、w=LhohL−hoです。
PCF(11.4節)は全ての影を同じ量だけぼかすので不自然に見えます。本物の影は接地点で鋭く、離れる程柔らかくなります(コンタクトハードニング)。
PCSS(percentage-closer soft shadows)は、2005年にRandima Fernando(NVIDIA)が発表した手法で、半影の幅をシャドウマップ自身から推定し、其の大きさのカーネルでPCFを行います。

3つの段階

距離は光源から其の軸に沿って測ります:受け手の点までをdr、遮蔽物(ブロッカー)までをdbとします。
光源、遮蔽物、受け手を平行な平面と見なすと、第11章の式は次の様になります(hL=dr、hL−ho=dbを代入)

wp=wlightdr−dbdb

PCSSは此れをピクセル毎に評価します:

  1. ブロッカー探索。受け手のテクセルの周りのシャドウマップの領域を見て、受け手より光源に近い深度を平均します。其の平均がdbです。近いテクセルが1つも無ければ、点は完全に照らされているので其処で終わります。
  2. 半影の推定。式でwpを計算します。
  3. フィルタリング。シャドウマップのテクセルに換算した幅wpのカーネルでPCFを行います。

探索領域はどれだけの大きさが必要でしょうか?
受け手の点に影を落とせるのは、受け手の点から光源への角錐の中にある遮蔽物だけです。
光源の中心から見ると、其の角錐の断面は光源の近くで一番大きく、光源のカメラのニアプレーン(距離n)では幅wlightdr−ndrになります。此れをシャドウマップの単位にした物が探索の幅です。

横から見た図:高さ4にある幅1の面光源、高さ1の遮蔽物の縁、受け手の点から光源への緑の錐、遮蔽物上の探索領域、床の上の幅0.33の半影

計算例。 第11章の数値を使います:高さ4 mにある幅1 mの天井の照明と、高さ1 mのテーブルの縁です。
テーブルの影の縁近くの床の点ではdr=4です。
例えば16個のテクセルでブロッカー探索をすると、4より近い物が5個見つかり、深度は3.00、3.00、3.00、2.90、3.10(少しノイズの乗ったテーブルの天板)だったとします。すると

db=3.00+3.00+3.00+2.90+3.105=3.0,wp=1⋅4−33=0.333m

で、正に11.1節の0.33 mです。
床に置かれた箱(db=3.9)ではwp=1⋅0.13.9=0.0256 mで、殆どハードシャドウです。
次にテクセルに換算します。
スポットライトのマップが床の上の8 mを1024テクセルで覆うなら、其処での1テクセルは8/1024=0.0078 mで、テーブルの半影には幅0.333/0.0078≈43テクセルのカーネルが必要です。
43 × 43の完全なPCFはピクセル毎に1849回の参照になるので、実際の実装ではカーネル上に散らばった16〜64個のサンプル(ポアソンディスクや回転させたパターン)を使い、結果をデノイズするかぼかします。

太陽の様な平行光源では、光源の幅の代わりに其の見かけの角度の大きさを使います:太陽の円盤の直径は約0.53°なので、wp=(dr−db)⋅2tan(0.265°)=0.00925(dr−db)です。
太陽が真上にあれば、高さ10 mの街灯の先端の影の半影は約9 cmで、根元は鋭くなります。

上から見た床と、細い柱と浮いた板の影:7×7固定カーネルのPCF、PCSS、面光源への256本のレイによるレイトレーシングの参照

図では、固定カーネルのPCFは柱の根元から先端まで同じ輪郭になり、PCSSと参照はどちらも根元で鋭く、先端で広がっています。
其れでもPCSSは近似です:ブロッカーの深度を平均すると全てのブロッカーを1枚の平面として扱う事になり、傾いた受け手の上の大きな探索領域では受け手自身をブロッカーと間違える事があります(図では其れを避ける為に、床の平面自身の深度と比べる「受け手平面の深度バイアス」を使っています)。

シェーダーでは

光源の軸に沿ったリニアな深度を保存するシャドウマップ(式に其の儘使えます)を持つスポットライトのPCSSは次の様になります。
lightSizeUvは光源の幅をニアプレーンでの光源の視錐台の幅で割った物、nearPlaneは其の平面までの距離です。どちらの探索も7 × 7のサンプルを使います。

// Percentage-closer soft shadows: 1 = fully lit, 0 = fully in shadow.
// The shadow map stores linear depth along the light's axis.
float pcss(Texture2D<float> shadowMap, SamplerState pointSampler, float2 uv, float zReceiver,
           float lightSizeUv, float nearPlane, float bias) {
  // 1. blocker search: the receiver-to-light pyramid, cut at the near plane
  float searchUv = lightSizeUv * (zReceiver - nearPlane) / zReceiver;
  float blockerSum = 0.0;
  int blockers = 0;
  for (int y = -3; y <= 3; ++y) {
    for (int x = -3; x <= 3; ++x) {
      float z = shadowMap.SampleLevel(pointSampler, uv + float2(x, y) * (searchUv / 6.0), 0);
      if (z < zReceiver - bias) {
        blockerSum += z;
        blockers += 1;
      }
    }
  }
  if (blockers == 0) return 1.0;
  // 2. penumbra width on the receiver, projected back to the near plane
  float zBlocker = blockerSum / float(blockers);
  float penumbraUv = lightSizeUv * (zReceiver - zBlocker) / zBlocker * nearPlane / zReceiver;
  // 3. PCF with that kernel
  float lit = 0.0;
  for (int y = -3; y <= 3; ++y) {
    for (int x = -3; x <= 3; ++x) {
      float z = shadowMap.SampleLevel(pointSampler, uv + float2(x, y) * (penumbraUv / 6.0), 0);
      lit += (zReceiver - bias > z) ? 0.0 : 1.0;
    }
  }
  return lit / 49.0;
}
// Percentage-closer soft shadows: 1 = fully lit, 0 = fully in shadow.
// The shadow map stores linear depth along the light's axis.
float pcss(sampler2D shadowMap, vec2 uv, float zReceiver, float lightSizeUv, float nearPlane,
           float bias) {
  // 1. blocker search: the receiver-to-light pyramid, cut at the near plane
  float searchUv = lightSizeUv * (zReceiver - nearPlane) / zReceiver;
  float blockerSum = 0.0;
  int blockers = 0;
  for (int y = -3; y <= 3; ++y) {
    for (int x = -3; x <= 3; ++x) {
      float z = textureLod(shadowMap, uv + vec2(x, y) * (searchUv / 6.0), 0.0).r;
      if (z < zReceiver - bias) {
        blockerSum += z;
        blockers += 1;
      }
    }
  }
  if (blockers == 0) return 1.0;
  // 2. penumbra width on the receiver, projected back to the near plane
  float zBlocker = blockerSum / float(blockers);
  float penumbraUv = lightSizeUv * (zReceiver - zBlocker) / zBlocker * nearPlane / zReceiver;
  // 3. PCF with that kernel
  float lit = 0.0;
  for (int y = -3; y <= 3; ++y) {
    for (int x = -3; x <= 3; ++x) {
      float z = textureLod(shadowMap, uv + vec2(x, y) * (penumbraUv / 6.0), 0.0).r;
      lit += (zReceiver - bias > z) ? 0.0 : 1.0;
    }
  }
  return lit / 49.0;
}
// Percentage-closer soft shadows: 1 = fully lit, 0 = fully in shadow.
// The shadow map stores linear depth along the light's axis.
float pcss(Texture2D<float> shadowMap, SamplerState pointSampler, float2 uv, float zReceiver,
           float lightSizeUv, float nearPlane, float bias) {
  // 1. blocker search: the receiver-to-light pyramid, cut at the near plane
  float searchUv = lightSizeUv * (zReceiver - nearPlane) / zReceiver;
  float blockerSum = 0.0;
  int blockers = 0;
  for (int y = -3; y <= 3; ++y) {
    for (int x = -3; x <= 3; ++x) {
      float z = shadowMap.SampleLevel(pointSampler, uv + float2(x, y) * (searchUv / 6.0), 0);
      if (z < zReceiver - bias) {
        blockerSum += z;
        blockers += 1;
      }
    }
  }
  if (blockers == 0) return 1.0;
  // 2. penumbra width on the receiver, projected back to the near plane
  float zBlocker = blockerSum / float(blockers);
  float penumbraUv = lightSizeUv * (zReceiver - zBlocker) / zBlocker * nearPlane / zReceiver;
  // 3. PCF with that kernel
  float lit = 0.0;
  for (int y = -3; y <= 3; ++y) {
    for (int x = -3; x <= 3; ++x) {
      float z = shadowMap.SampleLevel(pointSampler, uv + float2(x, y) * (penumbraUv / 6.0), 0);
      lit += (zReceiver - bias > z) ? 0.0 : 1.0;
    }
  }
  return lit / 49.0;
}

オフセットは−3から3に幅の6分の1を掛けた物なので、7 × 7の格子は丁度カーネル1つ分の幅に広がります。
問題はコストです:ピクセル毎に98回の参照で、大きな光源では更に増えます。
其の為、次の考え方、テクスチャの様に事前にフィルタリング出来るシャドウマップが広まりました。

24.4 フィルタリング可能なシャドウマップ、コンタクトシャドウ、レイトレーシングの影

第11章では(0.35と0.62の9つの深度で)、比較の前に深度を平均してもソフトシャドウにならない事を示しました。比較は階段関数なので、フィルタリングは其の後で行わなければなりません。
其の為PCFでは、ミップマップも、保存された値の双線型フィルタリングも、安い分離可能なぼかしも使えません。
フィルタリング可能なシャドウマップは別の物、平均を取ってもテクセルの面積のどれだけが照らされているかを表す物を保存します。

分散シャドウマップ

分散シャドウマップ(VSM。William DonnellyとAndrew Lauritzen、2006年)は、テクセル毎に深度zと其の2乗z2の2つの値を保存します。
線型のフィルタリング(ぼかし、ミップマップ、双線型)の後、テクセルは其のフットプリント上の平均、つまり深度の分布の最初の2つのモーメントを持ちます:

M1=E[z],M2=E[z2],μ=M1,σ2=M2−M12

深度tの受け手が照らされる割合は、フットプリント内の深度が其の手前に無い確率P(z≥t)です。
分布は分からず、平均と分散しか分かりませんが、片側のチェビシェフの不等式が上界を与えます:

P(z≥t)≤pmax(t)=σ2σ2+(t−μ)2(t>μ)

t≤μではpmax=1です。
平らな受け手の上に平らな遮蔽物が1つある場合、此の上界は正確で、影の輪郭では其れがよくある状況です。

計算例。 第11章の9つの保存された深度(0.35が5つ、0.62が4つ)と、t=0.60の受け手を使います:

M1=5⋅0.35+4⋅0.629=0.47,M2=5⋅0.1225+4⋅0.38449=0.2389
σ2=0.2389−0.472=0.0180,pmax=0.01800.0180+(0.60−0.47)2=0.01800.0349=0.516

本当の割合(PCFの結果)は49=0.444で、上界は当然少し其の上になります。
利点は、VSMがフィルタの大きさに関係無く、フィルタリングされた1回の参照から2つのモーメントを得る事です。マップ上でフレーム毎に1回(分離可能なガウシアンで)ぼかすか、ミップマップを使えば、43テクセルのぼかしも3テクセルのぼかしと同じコストです。

光漏れ(ライトブリーディング)

弱点は遮蔽物が2つある時に現れます。
フットプリントの半分が深度0.2(高い遮蔽物)、残りの半分が0.5(低い遮蔽物)で、t=0.8の受け手は両方の後ろにあるとします。完全に暗くなるべきです。

μ=0.35,M2=0.04+0.252=0.145,σ2=0.145−0.1225=0.0225
pmax(0.8)=0.02250.0225+0.452=0.10,pmax(0.55)=0.02250.0225+0.22=0.36

低い遮蔽物のすぐ後ろの受け手は光の36%を受け取ります。影の中に見える光の輪で、光漏れ(ライトブリーディング)と呼ばれます。

深度0.2と0.5の2つの遮蔽物について、受け手の深度の関数としてのチェビシェフの上界と真の照らされる割合:真の値が0の所で、上界は0.55で0.36、0.8で0.1

よくある対策は光漏れの低減です:pmaxを[a,1]から[0,1]に写してクランプします。aは0.2〜0.4程度です。
a=0.4なら0.36も0.10も0になりますが、本物の半影も全て暗く細くなるので、aも調整用のつまみです。
VSMには高い精度(M2−M12はほぼ等しい2つの数の引き算なので32ビット浮動小数点数)と、深度マップの2倍のメモリも必要です。

指数シャドウマップとモーメントシャドウマップ

指数シャドウマップ(ESM。Annenら、2008年)は階段関数を指数関数で置き換えます。
遮蔽物の後ろの受け手ではe−c(t−z)=ecze−ctは0に近く、eczは線型にフィルタリング出来るので、マップにはeczを保存します。
深度が0〜1なら、定数cは浮動小数点数の範囲で制限されます:ec≤3.4×1038からc≤88.7で、80程度がよく使われます。
c=80では、遮蔽物の0.25後ろの受け手はe−20=2×10−9で暗くなりますが、0.01しか後ろに無い受け手はe−0.8=0.45です:ESMは丁度接地点で光が漏れます。
モーメントシャドウマップ(Christoph PetersとReinhard Klein、2015年)はzからz4までの4つのモーメントを保存し、其処からより厳しい上界を復元します。テクセル毎に4つの値で、光漏れはずっと少なくなります。

コンタクトシャドウ

シャドウマップのテクセルは数センチメートルの幅があり、バイアスは影を接地点から遠ざけます(ピーターパン現象、11.4節)。
コンタクトシャドウは画面空間での短いレイマーチングを加えます:各ピクセルから光源に向かって8〜16回の小さな歩みを進め、各歩みの深度を深度バッファと比べます。目に見える面の後ろに入った歩みがあれば、近くに遮蔽物があるという事です。
安く、折り目やカップの下の隙間を埋めてくれますが、画面上の物しか見えず、深度バッファの厚みを仮定する必要があります。

レイトレーシングの影とデノイズ

ハードウェア・レイトレーシング(DXRやVulkanのレイトレーシング。執筆時点では殆どのデスクトップGPUと現行のゲーム機にあります)があれば、影は11.5節と全く同じ様に計算出来ます:面光源上のランダムな点に向けたシャドウレイです。
ピクセル毎に1本のレイは正しいけれどもノイズの多い半影を与えるので(第23章のモンテカルロのノイズ)、其の後にデノイザーを掛けます。各ピクセルを空間的に近傍と(輪郭が残る様に深度と法線で導きながら)、時間的には自分の履歴と(第20章のTAAの様に動きベクトルで再投影して)平均するフィルタです。

計算例。 指数移動平均による時間方向の蓄積hnew=(1−α)hold+αs、α=0.1を考えます。
分散σ2の独立なサンプルに対して、hの定常状態の分散は

α2σ21−(1−α)2=α2−ασ2=0.11.9σ2=σ219

なので、履歴はピクセル毎に19個のサンプルの様に働きます。代償は物が動いた時の遅れ(ゴースト)です。
レイトレーシングの影には、テクセルの大きさによるアクネも、カスケードも、バイアスの調整もありません。コストは光源の大きさではなく数に比例します。代償はレイとデノイザーです。

24.5 アンビエントオクルージョンを深く

11.5節ではレイでアンビエントオクルージョンを紹介しました:半球にレイを飛ばし、何本が抜けるかを数えるのです。
今度はきちんと定義し、ゲームが深度バッファから其れをどう計算するかを見ます。

定義

法線𝐧を持つ点𝐱のアンビエントオクルージョン(AO、環境遮蔽)は、半球のうち見えている部分の余弦で重み付けした割合です:

A(𝐱)=1π∫ΩV(𝐱,ω)(𝐧⋅ω)dω

Vは、𝐱から方向ωへのレイが(或る最大距離まで)抜ければ1、何かに当たれば0です。
∫Ω(𝐧⋅ω)dω=π(第12章)なので、1πで正規化しています。
何故余弦なのでしょうか?
放射輝度Lの一様な空の下では、放射照度はE=∫VL(𝐧⋅ω)dω=πLAなので、ランバート面の放射輝度は遮られない時の値の丁度A倍になります。AOは、跳ね返りの光が無い一様な空に対する正確な答えなのです。
其れ以外では近似なので、AOは間接光(環境光)にだけ掛け、直接光には決して掛けません。直接光の可視性は影が既に扱っています。
変種のアンビエントオブスキュランス(Zhukovら、1998年)は、0か1のVを当たった距離と共に増える関数で置き換え、遠くの形状ではあまり暗くならない様にします。ゲームのAOの殆どは実はオブスキュランスです。

計算例:余弦が大事な理由。 半球が仰角hより下で全周遮られているとします(丸い穴の底の点)。見える方向は法線からの角度がθ<θ0=90°−hの方向です。
余弦で重み付けすると、

A=1π∫02π∫0θ0cosθsinθdθdφ=sin2θ0=cos2h

h=30°ならA=0.75です。環境光の4分の1しか失われません。遮られるのは浅い角度の方向で、其れらの寄与は小さいからです。
代わりに方向を一様に数えると(単純な立体角の割合)、1−sinh=0.5、半分になります。
AOをレイで標本化する時、余弦で重み付けた方向(第23章の重点的サンプリング)を使えば推定値は単に抜けたレイの割合になり、一様な方向を使うなら各レイを其の余弦で重み付けしなければなりません。

スクリーンスペースアンビエントオクルージョン

スクリーンスペースアンビエントオクルージョン(SSAO)は、2007年にCrytekのMartin MittringがCrysisの為に発表した手法で、深度バッファだけを使います:

  1. ピクセルのビュー空間の位置を其の深度から復元します(逆投影は第19章で導出します)。
  2. 其の周りの半径rの小さな球の中にN個のサンプル点を取ります。
  3. 各サンプルを画面に投影し、其処の深度バッファを読みます。保存された面がサンプルの手前(カメラに近い)にあれば、サンプルは形状の中にあるので、遮られたと数えます。
  4. A≈1−遮られた数Nとし、ノイズを取る為に結果をぼかします。

Mittringの版は球全体を標本化し、小さなテクスチャから読んだランダムな平面でサンプルパターンを鏡映してピクセル毎に変えていました。
球には知られた副作用があります:平らな面では球の下半分が床の中に入るので、平らな壁が約50%遮られた事になり、初期のSSAOの灰色で輪郭の目立つ見た目になります。
後の版は法線に沿った半球を標本化します。其の為には(ディファードシェーディングの様に)法線バッファが必要です。

カメラが真下を見る横からの図:平らな床の上の点の周りの球の中の12個のサンプルのうち6個が面の下、段差の横の法線方向の半球の中の8個のサンプルのうち3個が段差の中

計算例。 左の図では、球の12個のサンプルのうち6個が平らな床の下にあります:遮るものの無い床なのにA=1−612=0.5です。
右の図では、高さ0.8の段差の横の半球の8個のサンプルのうち3個が段差の中にあります:A=1−38=0.625で、本当に段差による暗さです。
範囲チェックは手前の物体が遥か後ろの背景を暗くする(輪の様な縁取り)のを防ぎ、ピクセル毎の回転によるノイズは小さなぼかしで取り除きます。

// Hemisphere SSAO for one pixel, in view space (the camera looks down -z, so larger z is closer).
// viewZ stores the view-space z of the visible surface; t, b, n form the pixel's tangent frame;
// kernel holds 16 offsets in the unit hemisphere around +z.
float ssao(Texture2D<float> viewZ, SamplerState pointSampler, float4x4 proj, float3 p,
           float3 t, float3 b, float3 n, float3 kernel[16], float radius) {
  float occluded = 0.0;
  for (int i = 0; i < 16; ++i) {
    float3 s = p + (t * kernel[i].x + b * kernel[i].y + n * kernel[i].z) * radius;
    float4 clip = mul(proj, float4(s, 1.0));
    float2 uv = clip.xy / clip.w * 0.5 + 0.5; // flip v here if the API's texture origin needs it
    float surfaceZ = viewZ.SampleLevel(pointSampler, uv, 0);
    float inRange = (abs(p.z - surfaceZ) < radius) ? 1.0 : 0.0;
    occluded += (surfaceZ > s.z + 0.02) ? inRange : 0.0;
  }
  return 1.0 - occluded / 16.0;
}
// Hemisphere SSAO for one pixel, in view space (the camera looks down -z, so larger z is closer).
// viewZ stores the view-space z of the visible surface; t, b, n form the pixel's tangent frame;
// kernel holds 16 offsets in the unit hemisphere around +z.
float ssao(sampler2D viewZ, mat4 proj, vec3 p, vec3 t, vec3 b, vec3 n, vec3 kernel[16],
           float radius) {
  float occluded = 0.0;
  for (int i = 0; i < 16; ++i) {
    vec3 s = p + (t * kernel[i].x + b * kernel[i].y + n * kernel[i].z) * radius;
    vec4 clip = proj * vec4(s, 1.0);
    vec2 uv = clip.xy / clip.w * 0.5 + 0.5; // flip v here if the API's texture origin needs it
    float surfaceZ = textureLod(viewZ, uv, 0.0).r;
    float inRange = (abs(p.z - surfaceZ) < radius) ? 1.0 : 0.0;
    occluded += (surfaceZ > s.z + 0.02) ? inRange : 0.0;
  }
  return 1.0 - occluded / 16.0;
}
// Hemisphere SSAO for one pixel, in view space (the camera looks down -z, so larger z is closer).
// viewZ stores the view-space z of the visible surface; t, b, n form the pixel's tangent frame;
// kernel holds 16 offsets in the unit hemisphere around +z.
float ssao(Texture2D<float> viewZ, SamplerState pointSampler, float4x4 proj, float3 p,
           float3 t, float3 b, float3 n, float3 kernel[16], float radius) {
  float occluded = 0.0;
  for (int i = 0; i < 16; ++i) {
    float3 s = p + (t * kernel[i].x + b * kernel[i].y + n * kernel[i].z) * radius;
    float4 clip = mul(proj, float4(s, 1.0));
    float2 uv = clip.xy / clip.w * 0.5 + 0.5; // flip v here if the API's texture origin needs it
    float surfaceZ = viewZ.SampleLevel(pointSampler, uv, 0);
    float inRange = (abs(p.z - surfaceZ) < radius) ? 1.0 : 0.0;
    occluded += (surfaceZ > s.z + 0.02) ? inRange : 0.0;
  }
  return 1.0 - occluded / 16.0;
}

地平線ベースのAOとGTAO

点のサンプルは情報を無駄にします:深度バッファの1行だけで、ピクセルの周りの面の断面全体が分かるからです。
地平線ベースのアンビエントオクルージョン(HBAO。Louis Bavoil、Miguel Sainz、Rouslan Dimitrov、NVIDIA、2008年)は画面空間で幾つかの方向に進み、各方向で地平線角、つまり深度バッファがまだ視界を遮る一番高い仰角を見つけます。
地平線より下は全て遮られ、上は全て開いているので、1回の歩みで半球の1つの断面(スライス)全体の可視性が得られます。
HBAOは見える部分を一様な重みで積分します。GTAO(ground-truth ambient occlusion。Jorge Jimenez、Xian-Chun Wu、Angelo Pesce、Adrian Jarabo、Activision、2016年)は、スライス内の余弦で重み付けした積分が閉じた式で解ける事を示しました。

視線方向を含むスライスで、2つの地平線角θ1≤0≤θ2を法線から測ります(法線がスライス内にある場合。GTAOは、法線の射影と視線ベクトルの成す角γを使って一般の場合も扱います)。
γ=0では、スライスの可視性は

a^=14(1−cos2θ1)+14(1−cos2θ2)

で、AOはスライスの方向(ピクセル毎に2〜4個。ピクセルとフレーム毎に回転させ、其の後デノイズ)についてのa^の平均です。
確かめましょう:開いた半球ではθ1=−90°、θ2=90°で、a^=24+24=1です。
両側の地平線が仰角30°ならθ=±60°、cos120°=−0.5で、a^=0.375+0.375=0.75。上の穴と同じcos230°です。

段差の横のスライス:開いた側の地平線1は−90°、段差の角に向かう地平線2は36.87°、其の間の見える範囲、法線と開いた側に傾いたベントノーマル。スライスの可視性は0.68

計算例。 図では左側が開いていて(θ1=−90°)、(0.6,0.8)にある段差の角が右の地平線を決めます:tanθ2=0.6/0.8、θ2=36.87°で、cos2θ2=cos2θ2−sin2θ2=0.64−0.36=0.28です:

a^=14(1+1)+14(1−0.28)=0.5+0.18=0.68

GTAOは更に、マルチバウンスの近似(明るい隅では光が壁の間で跳ね返るので、普通のAOは暗くし過ぎます。レイトレーシングの参照に合わせたAとアルベドの小さな3次式で補正します)と、ベントノーマル、つまり半球の遮られていない部分の平均の方向を加えます。図では段差から約28°離れる様に傾いています。
ベントノーマルで放射照度を参照すると、光が実際に来られる方向から光が来る様になり、開いた方向の円錐は反射の安いスペキュラーオクルージョンにもなります。

ベイクしたAOとレイトレーシングのAO

静的な物体では、AOをオフラインでAOマップに焼き込めます(glTF 2.0のオクルージョンテクスチャは赤チャンネルを使います)。但し近くの他の物体の事は知りません。
レイトレーシングのAO(RTAO)はピクセル毎に1〜2本の短いレイ(AOは近距離の効果なので0.5〜2 m程度)を本物のシーンに飛ばしてデノイズします。画面空間の制限はありませんが、レイのコストが掛かります。

24.6 ベイクした大域照明とライトプローブ

今度は跳ね返った光其の物です。
一番古いリアルタイムの答えはオフラインで計算する事です:第12章のラジオシティ法やパストレーシング法を1回だけ走らせ、結果を保存します。

ライトマップ

ライトマップ(11.10節)は、静的な面の事前計算した放射照度を、専用の重ならないUVを持つテクスチャに保存します。
現代のベイカーはGPUのパストレーサー(第23章)で其れを計算し、テクセル毎に複数の値を保存します:指向性ライトマップは入射光の主な方向、又は幾つかの球面調和関数の係数を加え、法線マップがベイクした光に反応し続ける様にします。
ライトマップは壁や床には完璧ですが、動く物は照らせず、メモリは面の面積に比例します。

ライトプローブ

動く物体(キャラクタ、乗り物、扉)はライトプローブ(第12章)で照らします:其処に全方向から届く光を保存する、空間内の標本点です。
プローブは球面上の関数を保存し、拡散の照明に必要なのは、あり得る全ての法線に対する其の放射照度だけです。
標準的なコンパクトな表現が球面調和関数です。

球面調和関数

球面調和関数(SH)は、周期的な信号にとってのサインとコサインの様な物を球面上で考えた物です:周波数の順に並んだ関数の基底です。
バンドl=0,1,2,…に分かれ、バンドlには2l+1個の関数があります。バンド0からLまでを使うと(L+1)2個の関数になるので、「L1」は4個、「L2」は9個です。
L2までの実数の球面調和関数を、単位方向の成分(x,y,z)の多項式で書くと次の様になります。
此のシリーズのy軸が上の規約に合わせてyを極軸(「上」)にしています。多くの文献はzを使いますが、文字を入れ替えれば同じ関数です。

バンド 関数 定数
l=0 Y0,0=0.282095 121/π
l=1 0.488603x, Y1,0=0.488603y, 0.488603z 3/(4π)
l=2 1.092548xy, 1.092548yz, 1.092548xz 1215/π
l=2 Y2,0=0.315392(3y2−1) 145/π
l=2 0.546274(x2−z2) 1415/π

此れらは正規直交です:∫YiYjdωはi=jなら1、其れ以外は0です。
其の為、射影は簡単です。正規直交基底での内積によるベクトルの座標の計算と全く同じです(線型代数の第4章):

ci=∫S2L(ω)Yi(ω)dω,L(ω)≈∑iciYi(ω)

実際には、積分はキューブマップのテクセルを其々の立体角で重み付けした和か、モンテカルロ推定です。

何故放射照度には9つの数で足りるのでしょうか?
放射照度は放射輝度と、0で切った余弦max(𝐧⋅ω,0)の畳み込みで、或る軸の周りに対称な関数との畳み込みは各バンドを単に定数倍するだけです(ファンク・ヘッケの定理)。
Ravi RamamoorthiとPat Hanrahan(2001年)は其の倍率を求めました:

E(𝐧)=∑iA^l(i)ciYi(𝐧),A^0=π,A^1=2π3,A^2=π4,A^3=0,A^4=−π24

倍率は速く小さくなるので(1より上の奇数バンドは完全に消えます)、余弦は強いローパスフィルタで、彼らの論文は9つの係数で放射照度の平均誤差が約1%になる事を示しました。
色のチャンネル毎なので、プローブ1つにつき27個の数です。

計算例。 天頂で一番明るく、地平線の下は黒い空:L(ω)=max(y,0)。
yの周りに対称なので、0でない係数を持つのは「帯球」関数Y0,0,Y1,0,Y2,0だけです。
上半球での∫ykdω=2πk+1を使います:

c0,0=0.282095∫ydω=0.282095⋅π=0.8862
c1,0=0.488603∫y2dω=0.488603⋅2π3=1.0233
c2,0=0.315392∫(3y3−y)dω=0.315392(3π2−π)=0.4954

放射輝度を復元します:L1は0.282095⋅0.8862+0.488603⋅1.0233y=0.25+0.5yです。
天頂では0.75(本当は1)、地平線では0.25(本当は0)、天底では−0.25:負の放射輝度で、打ち切ったフーリエ級数のリンギングのSH版です。
L2は0.315392⋅0.4954(3y2−1)=0.15625(3y2−1)を加え、1.0625、0.094、0.0625になります:良くはなりますが、まだ目に見えて違います。
次に、真上を向いた法線(y=1)の放射照度です。正確な値は∫y⋅ydω=2π3=2.0944です:

E=π⋅0.25+2π3⋅0.5+π4⋅0.15625⋅2=0.7854+1.0472+0.2454=2.0780

L2は0.8%低く、L1だけでは1.8326で12.5%低くなります。
水平な法線では正確な放射照度は23=0.6667で、L2は0.7854−0.1227=0.6627です。
つまり9つの係数は、放射輝度は下手にしか再現しないのに、放射照度は上手く再現します。
此れが要点です:SHプローブは拡散の光の為の物で、鋭い反射には24.7節のリフレクションプローブが必要です。

緯度経度形式の、空・太陽・暗い地面の環境と、リンギングのあるL1とL2の再構成、下の段は正確な放射照度とL1・L2の放射照度で、L2はよく一致する

図の環境(空、小さく明るい太陽、暗い地面)では、L2の放射照度は正確な値と平均1.5%、最大6.1%、L1は平均7.0%、最大23.7%違います。
同じ射影と評価をC++で書くと次の様になります:

#include <cmath>
#include <cstdio>
#include "linalg.hh"

// Real spherical harmonics up to L2, with y as the polar ("up") axis.
void shBasis(Vec3 d, float y[9]) {
  y[0] = 0.282095f;
  y[1] = 0.488603f * d.x;
  y[2] = 0.488603f * d.y;
  y[3] = 0.488603f * d.z;
  y[4] = 1.092548f * d.x * d.y;
  y[5] = 1.092548f * d.y * d.z;
  y[6] = 0.315392f * (3.0f * d.y * d.y - 1.0f);
  y[7] = 1.092548f * d.x * d.z;
  y[8] = 0.546274f * (d.x * d.x - d.z * d.z);
}

// Irradiance from 9 coefficients: E(n) = sum_l A_l c_lm Y_lm(n).
float shIrradiance(const float c[9], Vec3 n) {
  const float pi = 3.14159265f;
  const float a1 = 2 * pi / 3, a2 = pi / 4;
  const float a[9] = {pi, a1, a1, a1, a2, a2, a2, a2, a2};
  float y[9];
  shBasis(n, y);
  float e = 0;
  for (int k = 0; k < 9; ++k) e += a[k] * c[k] * y[k];
  return e;
}

int main() {
  // Project the sky L = max(y, 0) with a midpoint rule over theta (from +y) and phi.
  const float pi = 3.14159265f;
  const int nt = 256, np = 512;
  float c[9] = {};
  for (int i = 0; i < nt; ++i) {
    float th = (i + 0.5f) * pi / nt;
    for (int j = 0; j < np; ++j) {
      float ph = (j + 0.5f) * 2 * pi / np;
      Vec3 d{std::sin(th) * std::cos(ph), std::cos(th), std::sin(th) * std::sin(ph)};
      float dw = std::sin(th) * (pi / nt) * (2 * pi / np);
      float radiance = std::fmax(d.y, 0.0f);
      float y[9];
      shBasis(d, y);
      for (int k = 0; k < 9; ++k) c[k] += radiance * y[k] * dw;
    }
  }
  printf("c00 %.4f  c10 %.4f  c20 %.4f\n", c[0], c[2], c[6]);
  printf("E(up) %.4f  E(side) %.4f  E(down) %.4f\n", shIrradiance(c, {0, 1, 0}),
         shIrradiance(c, {1, 0, 0}), shIrradiance(c, {0, -1, 0}));
  return 0;
}

c00 0.8862  c10 1.0234  c20 0.4954
E(up) 2.0781  E(side) 0.6627  E(down) -0.0164

下向きの小さな負の放射照度もSHのリンギングです。エンジンは其れをクランプするか、高いバンドを少し弱めます(「窓掛け」)。
PBRのマテリアルは此の放射照度を拡散の部分に使い、鏡面の部分にはプリフィルタした環境マップが必要です(第22章)。

イラディアンスボリュームとプローブの格子

プローブは何処に置けば良いでしょうか?
イラディアンスボリューム(Greger、Shirley、Hubbard、Greenberg、1998年)は其れらを規則的な3次元の格子に置きます。アーティストやツールが手で置く事もあり、光が変わる所には密に、開けた空間には疎らに置きます。
プローブの間の点は、其の格子のセルの8つのプローブを3線型補間します:セル内の端数の位置を(fx,fy,fz)とすると、角(i,j,k)∈{0,1}3のプローブの重みは

wijk=(ifx+(1−i)(1−fx))(jfy+(1−j)(1−fy))(kfz+(1−k)(1−fz))

計算例。 (fx,fy,fz)=(0.25,0.5,0.75):角(0,0,0)は0.75⋅0.5⋅0.25=0.09375、角(0,0,1)は0.75⋅0.5⋅0.75=0.28125、角(1,0,1)は0.25⋅0.5⋅0.75=0.09375を得て、8つの重みの和は1です。
補間したSHの係数(プローブの係数の線型結合)を、上と同じ様に法線で評価します。

典型的な不具合は光漏れです。
2つのプローブの間の壁は補間を止めないので、日の当たる部屋の隣の暗い部屋の点は壁の向こうの明るいプローブを拾い、暗い部屋が壁沿いに光ってしまいます。
左の図では明るいプローブが壁を通して暗い部屋に漏れ(暗いプローブは明るい側を暗くし)、右では各プローブの重みに可視性の判定(保存した距離に対するチェビシェフの判定。24.8節のDDGIを参照)を掛けて、壁が再び部屋を分けています。

明るい部屋と暗い部屋の間に壁があり、プローブの格子がある平面図:双線型補間は壁を通して光が漏れ、可視性の重みが其れを止める。右は参照

もっと単純な対策:セルが壁を跨がない様にプローブを置く、面の後ろのプローブの重みを下げる、参照位置を法線に沿ってずらす。

24.7 反射:プローブと画面空間

光沢や鏡の反射には放射照度よりずっと細かい角度の情報が必要なので、SHでは足りません。

視差補正付きのリフレクションプローブ

リフレクションプローブ(反射プローブ)は、シーンの1点で撮影し、幾つかのラフネスの段階についてプリフィルタしたキューブマップです(第22章。OpenGLシリーズの第24章で作ります)。
11.9節では、環境マップは周囲が無限に遠いと仮定する事を注意しました。
部屋の中では其れは大きく間違います:右の壁の近くの点は、自分がプローブの位置に立っているかの様に反射方向でプローブを参照し、部屋の違う部分を見てしまいます。

視差補正は部屋の単純な代理形状(プロキシ)、普通は箱で此れを直します:シェーディングする点からの反射レイと箱の交点を求め、プローブから其の交点への方向でキューブマップを参照するのです。
此の考えはGPU Gems(Kevin Bjorke、2004年)に現れ、Sébastien LagardeとAntoine Zanuttini(SIGGRAPH 2012)がゲームに広めました。

xが−4から4、zが−3から3の部屋の箱の平面図:中心のプローブ、2, 0の点p、4, 2.67で壁に当たる反射レイ、プローブから其の点への補正した参照と、2.25, 3に届く補正なしの参照

計算例(上から見るのでxとzだけ)。
部屋の箱は−4≤x≤4、−3≤z≤3、プローブは原点、点は𝐩=(2,0)で反射方向は𝐫=(0.6,0.8)です。
レイ𝐩+t𝐫は一番近い出口の平面から箱を出るので、2つの出口までの距離の小さい方を取ります:

tx=4−20.6=3.333,tz=3−00.8=3.75,t=3.333,𝐡=(2+3.333⋅0.6,3.333⋅0.8)=(4,2.667)

補正した参照方向は𝐡−𝐜=(4,2.667)、正規化すると(0.832,0.555)で、𝐫とは19.4°違います。
プローブから𝐫で補正せずに参照すると壁の点(2.25,3)が見え、正しい点から2 m近く離れています。
箱は代理に過ぎないので家具はやはり間違った所に映り、プローブの位置に近い点程良い結果になります。エンジンは重なり合う複数のプローブを混ぜます。

スクリーンスペースリフレクション

スクリーンスペースリフレクション(SSR)は、代わりに反射レイを深度バッファの中で辿ります:画面上のレイの射影に沿って進み(最小深度のピラミッド「Hi-Z」で速くする事が多いです)、レイが深度バッファの後ろに入った所で其のピクセルのライティング済みの色を再利用します。
画面上の物には正確です(ネオンサインを映す濡れた道路)が、画面に無い物(カメラの後ろの空、隠れた面)は映せないので、画面の端では薄れて、リフレクションプローブに切り替えます。
スクリーンスペースGI(SSGI)は、ピクセル毎に多数の短いレイで同じ歩みを拡散の跳ね返りに使い、同じ死角を持ちます。

24.8 動的な大域照明

ベイクした光は、開く扉や沈む太陽に付いて行けません。
動的GIは、跳ね返った光(の一部)を毎フレーム計算し直します。

反射シャドウマップ

光の最初の跳ね返りは、光が直接照らす面から始まり、其れは正にシャドウマップが見ている物です。
反射シャドウマップ(RSM。Carsten DachsbacherとMarc Stamminger、2005年)は、深度の他に、各テクセルのワールド位置、法線、反射した放射束を保存します。そして各テクセルが、シーンの他の部分にとって小さな光源、仮想点光源(VPL)として働きます。

反射した放射束Φp、位置𝐱p、法線𝐧pを持つランバート面のテクセルは、小さなラジオシティのパッチ(第12章)の様に放射します:法線から角度θpの方向への放射強度はΦpπcosθpなので、法線𝐧を持つ点𝐱に加わる放射照度は

Ep(𝐱)=Φpπmax(0,cosθp)max(0,cosθx)‖𝐱−𝐱p‖2

計算例。 放射照度1 W/m²の日光の中にある、アルベド0.8の10 cm × 10 cmの床のテクセルはΦp=0.8⋅1⋅0.01=0.008 Wを反射します。
𝐱p=(0,0,0)、𝐧p=(0,1,0)で、𝐧=(−1,0,0)の壁の点𝐱=(1,1,0)を照らします。
距離は2、両方の余弦は12=0.707で、

Ep=0.008π⋅0.707⋅0.7072=0.00255⋅0.25=0.00064W/m2

ピクセル毎に此の様なテクセルを数百個足し合わせると(マップへのピクセルの射影の周りを重点的に標本化します)、反射した面の色を持つ最初の跳ね返り、つまりリアルタイムのカラーブリーディングが得られます。
RSMはVPLと受け手の間の可視性を無視するので、光が壁を通り抜けます。ライトプロパゲーションボリューム(LPV。Anton Kaplanyan、Crytek、2009年)はVPLをSH係数の粗い3次元格子に注入し、セルからセルへ光を伝播させます。此れなら粗い遮蔽の格子を考慮出来ます。

ボクセルコーントレーシング

ボクセルコーントレーシング(VCT。Cyril Crassin、Fabrice Neyret、Miguel Sainz、Simon Green、Elmar Eisemann、2011年。NVIDIAは後に其の実装をVXGIとして提供しました)は、毎フレーム(又は差分で更新して)シーンをボクセル化し、各ボクセルから出て行く光の3次元テクスチャを完全なミップマップのピラミッドと共に作ります。
多数のレイの代わりに、少数の円錐(コーン)を辿ります:拡散の光には半球を覆う数本の太い円錐、光沢反射には1本の細い円錐です。
円錐に沿ってフットプリントは距離と共に大きくなるので、シェーダーはどんどん粗いミップレベルを標本化し、第28章のボリュームレンダリングの様に手前から奥へ合成します。

計算例。 0.25 mのボクセルと開き角60°の円錐:距離tで円錐の幅はd=2ttan30°=1.155tで、其れに合うボクセルのミップレベルはlog2(d/0.25)です。
t=1 mではlog24.62=2.21、t=4 mでは4.21。距離が2倍になる毎に丁度1レベル増えます。
其の為、円錐は少ないサンプルで遠くまで届きます。此れが手法の強みで、弱みはボクセルを最新に保つコストと、粗いボクセルが薄い壁を通して起こす光漏れです。

DDGI:レイトレーシングで更新するプローブ

DDGI(dynamic diffuse global illumination。Zander Majercik、Jean-Philippe Guertin、Derek Nowrouzezahrai、Morgan McGuire、2019年)は24.6節のプローブの格子を使い、全てのプローブを毎フレーム、レイトレーシングで更新します:

  • 各プローブはランダムに回転させた螺旋パターンで数十〜数百本のレイ(論文では32〜256本を試しています)を飛ばし、当たった点を直接光と前のフレームのプローブで照らします。こうして光はフレームからフレームへ跳ね返り続けます。
  • 放射照度を小さな八面体マップ(8 × 8テクセル)に、各方向の当たった点までの平均距離と距離の2乗の平均をもう1つのマップ(16 × 16)に保存します。
  • 新しい結果はヒステリシスαで混ぜ込みます(論文は0.85〜0.98を使っています):new=α⋅old+(1−α)⋅traced。

距離のモーメントは、プローブから見た分散シャドウマップです。
点をシェーディングする時、周りの8つのプローブは其々、点がプローブから見え得るかをチェビシェフの不等式で調べ、見えなさそうなら重みを下げます。プローブの格子の図で光漏れを止めているのは此れです。

計算例。 或る点の方向へのプローブの平均距離がμ=2.0 m、距離の2乗の平均が4.1なら、σ2=4.1−4=0.1です。
点は3.0 m離れているので、間に壁がありそうです:pmax=0.10.1+12=0.091で、プローブの重みは9%に下がります。
ヒステリシスについては、α=0.95で光源が急に点いたとします:kフレーム後、プローブは新しい値の1−0.95kに達し、14フレームで51%、45フレーム(60 fpsで0.75秒)で90%です。
執筆時点では、NVIDIAはDDGIをRTXGI SDKで配布しています。

24.9 レイトレーシングのGI、ReSTIR、ハイブリッドなパイプライン

ハードウェア・レイトレーシングがあれば、第12章の正直な方法が可能になります:ピクセル毎に数本のレイを飛ばし、1〜2回跳ね返らせ、デノイズするのです。
問題はサンプルの予算で、ピクセル毎に1〜2本のレイという事も多いので、各レイがどのサンプルを取るかが全てになります。
近年最も影響の大きかった考えは、ピクセル同士で良いサンプルを共有させる事です。

リサンプル重点的サンプリング

先ず、多数の光源からの直接光を考えます。
ピクセルにとって理想の光源サンプルは、光源の実際の寄与fに比例する確率で引いた物です(第23章の重点的サンプリング)が、fには可視性が含まれ、光源毎にレイが1本必要です。
リサンプル重点的サンプリング(RIS)は2段階で働きます:

  1. 単純な元の分布p(例えば一様に選んだ光源)から、M個の安い候補x1,…,xMを引きます。
  2. 其々に重みwi=p^(xi)p(xi)を与えます。p^は安いけれどもfに近い目標関数(シャドウレイ無しの光源の寄与)です。そして確率wi∑wjで候補を1つyとして選びます。

其の後、yにだけ1本のシャドウレイを飛ばし、次の推定量を使います

⟨L⟩=f(y)W,W=1p^(y)⋅1M∑j=1Mwj

Wは選ばれたサンプルにとっての1pの役割を果たします。
候補は、保存せずに1つずつ選ぶ事が出来ます。重み付きリザーバサンプリングです:今の選択、累積和wsum、個数Mを持っておき、重みwの新しい候補毎に其れをwsumに足して、確率wwsumで選択を置き換えます。
最後には、各候補は丁度wiwsumの確率で残っています。

計算例。 シーンには100個の光源があるのでp=1100です。
4つの候補の目標値がp^=0.02,0.30,0.05,0.13なので重みはw=2,30,5,13、乱数はu=0.7,0.4,0.6,0.9です:

候補 w wsum wwsum u 残る?
1 2 2 1 0.7 はい
2 30 32 0.9375 0.4 はい、1を置き換え
3 5 37 0.1351 0.6 いいえ
4 13 50 0.26 0.9 いいえ

リザーバには候補2が残り、W=10.30⋅504=41.67です。
シャドウレイで光源が見えればf(y)=p^(y)=0.30で、推定値は0.30⋅41.67=12.5です。此れは丁度1M∑wj、候補のp^/pの平均で、影を考えない光の合計∑all lightsp^の推定値になっています。
候補1〜4で終わる確率は250,3050,550,1350 = 0.04、0.6、0.1、0.26でした。強い光源が殆どの場合に勝ち、飛ばしたシャドウレイは1本だけです。

ReSTIR:空間と時間での再利用

ReSTIR(Spatiotemporal Reservoir Resampling。Benedikt Bitterli、Chris Wyman、Matt Pharr、Peter Shirley、Aaron Lefohn、Wojciech Jarosz、SIGGRAPH 2020)は、リザーバ自体がM個の候補の要約であり、リザーバは併合出来る事に注目しました:他のリザーバの選んだサンプルを、重みp^here(y′)⋅W′⋅M′の1つの候補として扱い、其の個数M′を足すのです。
毎フレーム、全てのピクセルは:

  1. 幾つかの候補から新しいリザーバを作り、
  2. 前のフレームの同じ面の点のリザーバを併合し(時間方向の再利用。動きベクトルで見つけます)、
  3. ランダムに選んだ幾つかの隣のピクセルのリザーバを併合します(空間方向の再利用)。此の時、隣にとって強い光源が此処では弱いかも知れないので、p^を自分の位置で評価し直します。

数フレーム後には、各ピクセルは実質的に数千の候補の中から選び、其れでもシャドウレイは1〜2本しか飛ばしません。
論文は最大340万個の発光三角形を持つシーンをリアルタイムでレンダリングし、同じ誤差に従来の最先端の手法より6〜60倍速く到達したと報告しています。
注意が2つあります:時間方向の個数Mには上限を付けなければならず(フレーム毎の個数の約20倍が多いです)、さもないと履歴が支配して画像の反応が鈍くなります。そして隣のピクセルが違う形状を見ている時、単純な併合には偏りが出るので、論文は安い偏りのある版と丁寧な偏りの無い版を示しています。
同じ考えを跳ね返りの経路に適用したReSTIR GI(Ouyangら、2021年)は間接レイの当たった点をリサンプルし、ピクセル毎に1本の経路のパストレーシングによるGIを、デノイズ後に使える物にします。

執筆時点のハイブリッドなパイプライン

実際のエンジンは此れらの手法を幾つも組み合わせます。例を幾つか挙げます(製品はすぐに変わります。大事なのは考え方です):

  • Unreal Engine 5のLumenはサーフェスキャッシュを持ちます:メッシュの面を幾つかの方向から捉えた物(「カード」)で、其のライティングを数フレームに渡って更新し、レイが当たった時に一からシェーディングせずにライティングを参照出来る様にします。レイは先ず画面を試し、次に符号付き距離場に対するソフトウェア・レイトレーシング(第21章)、対応していればハードウェア・レイトレーシングを使います。
  • 拡散の光にはDDGI風のレイトレーシングのプローブ(例えばNVIDIAのRTXGI)、鏡面の部分にはリフレクションプローブかレイトレーシングの反射を使う、プローブベースのGI。
  • 一部のゲームのパストレーシングのモード。例えばCyberpunk 2077の「RT: Overdrive」モード(2023年)は、多数の光源にReSTIRの実装であるNVIDIAのRTXDIを使い、デノイズとアップスケーリング(第29章)と組み合わせています。

典型的なフレームでも、カスケードシャドウマップとコンタクトシャドウ、GTAO、ベイクしたライトマップ、キャラクタ用のプローブ、プローブに切り替わるSSRを組み合わせる事がまだあります。

24.10 手法の比較

手法 計算する物 動的? コスト 典型的なアーティファクト
カスケードシャドウマップ 太陽の可視性 完全に N回の深度パス ちらつき、継ぎ目、バイアスの調整
PCSS ソフトシャドウの可視性 完全に ブロッカー探索+大きなPCF 重なったブロッカーで不正確、コスト
VSM / ESM / MSM フィルタリング可能な可視性 完全に テクセル毎に2〜4個の値 光漏れ/接地点での漏れ
レイトレーシングの影 正確な可視性 完全に 光源毎のレイ+デノイザー ノイズ、ゴースト
SSAO / HBAO / GTAO 近距離の空の可視性 完全に 数ms以下、画面空間 縁取り、画面外の遮蔽物の欠落
ライトマップ 静的な面の拡散GI いいえ(ベイク) メモリ、ベイク時間 動く物を照らせない
SHプローブ/イラディアンスボリューム 点での拡散GI ベイク又は更新 プローブ毎に9 × 3個の値 光漏れ、低周波のみ
リフレクションプローブ(視差補正) 光沢反射 ベイク又は再撮影 キューブマップ プロキシから外れた所の視差のずれ
SSR / SSGI 画面からの反射/跳ね返り 完全に 画面空間の歩み 画面外の内容の欠落
RSM / LPV 最初の拡散の跳ね返り 完全に VPLの和/格子の伝播 可視性が無いか粗い、光漏れ
ボクセルコーントレーシング 拡散と光沢のGI 完全に ボクセル化+円錐 光漏れ、ボクセルのコスト
DDGI 拡散GI、複数回の跳ね返り 完全に プローブ毎のレイ 遅れ(ヒステリシス)、プローブの配置
レイトレーシングのGI+ReSTIR 直接光と間接光 完全に ピクセル毎に1〜2本のレイ+デノイザー ノイズ、時間方向のアーティファクト

検定のポイント

此れらはシラバス項目の影付け(第11章)と大域照明計算(第12章)に繋がります:

  • 半影の幅は遮蔽物と受け手の間の距離と共に広がります:wp=wlight(dr−db)/db。ソフトシャドウは接地点で鋭くなります。PCFだけでは決まった量しかぼかさず、PCSSはブロッカーの平均深度から幅を推定します。
  • シャドウマップのフィルタリングは比較の後に行わなければなりません(PCF)。マップがフィルタリング可能な物(VSMのモーメント)を保存する場合は別です。
  • アンビエントオクルージョンは半球のうち見える部分の余弦で重み付けした割合で、環境光(間接光)だけを暗くします。
  • ライトマップは静的な面の事前計算した照明を、ライトプローブは動く物体の為に空間内の点での光を保存し、環境(反射)マップは周囲が遠い事を仮定します。
  • ラジオシティ法の様な拡散のGIは視点非依存でベイク出来ますが、鏡面の効果は視点に依存し、フレーム毎の手法(プローブ、SSR、レイトレーシング)が必要です。

24.11 まとめ

  • カスケードシャドウマップは視界を分割します。対数分割は透視エイリアシングを釣り合わせ、実用分割方式は其れを均等分割とλで混ぜます。バウンディング球とテクセル単位のスナップがちらつきを止めます。
  • PCSSはブロッカーの平均深度を求め、半影wlight(dr−db)/dbを推定し、其の幅のPCFを行って、接地点で硬くなるソフトシャドウを作ります。
  • VSMはzとz2を保存し、ぼかす事が出来、チェビシェフの上界σ2/(σ2+(t−μ)2)を使います。重なった遮蔽物は光漏れを起こします。ESM、モーメントシャドウマップ、コンタクトシャドウ、デノイズしたレイトレーシングの影が道具箱を完成させます。
  • AOは1π∫Vcosθdωです。SSAOは深度バッファを標本化し、HBAOは地平線を見つけ、GTAOは各スライスを解析的に積分します。AOはベイクやレイトレーシングでも計算出来ます。
  • ライトマップは静的なGIをベイクします。SHプローブは、余弦のローパスの倍率π,2π/3,π/4のお陰で、チャンネル毎に9つの係数で放射照度を保存します。プローブの格子は3線型補間し、可視性の重みが無いと光が漏れます。
  • リフレクションプローブにはプロキシの箱による視差補正が必要です。SSRとSSGIは画面を再利用しますが、画面外の内容は見えません。
  • RSMはシャドウマップのテクセルを仮想点光源にし、ボクセルコーントレーシングはボクセルのミップピラミッドを標本化し、DDGIはチェビシェフの可視性と共にレイトレーシングでプローブを更新します。
  • ReSTIRはリザーバを使ったリサンプル重点的サンプリングでサンプルを選び、其れをピクセル間やフレーム間で共有します。

24.12 用語

English 日本語
direct light, indirect light 直接光、間接光
baking, baked lighting ベイク(事前計算した照明)
cascaded shadow maps (CSM) カスケードシャドウマップ(CSM)
perspective aliasing 透視エイリアシング
uniform, logarithmic, practical split scheme 均等分割、対数分割、実用分割方式
shimmering ちらつき(シマリング)
stable cascades, texel snapping 安定カスケード、テクセル単位のスナップ
percentage-closer soft shadows (PCSS) PCSS(パーセンテージクローサーソフトシャドウ)
blocker search ブロッカー探索
penumbra estimation 半影の推定
contact hardening コンタクトハードニング
variance shadow map (VSM) 分散シャドウマップ(VSM)
moment モーメント(積率)
Chebyshev's inequality チェビシェフの不等式
light bleeding 光漏れ(ライトブリーディング)
exponential shadow map (ESM) 指数シャドウマップ(ESM)
moment shadow map モーメントシャドウマップ
contact shadow コンタクトシャドウ
denoiser, temporal accumulation デノイザー、時間方向の蓄積
ambient occlusion (AO) アンビエントオクルージョン(環境遮蔽)
ambient obscurance アンビエントオブスキュランス
screen-space ambient occlusion (SSAO) スクリーンスペースアンビエントオクルージョン(SSAO)
horizon-based ambient occlusion (HBAO) 地平線ベースのアンビエントオクルージョン(HBAO)
ground-truth ambient occlusion (GTAO) GTAO
horizon angle 地平線角
bent normal ベントノーマル
specular occlusion スペキュラーオクルージョン
light map, directional light map ライトマップ、指向性ライトマップ
light probe ライトプローブ
spherical harmonics (SH) 球面調和関数(SH)
band バンド(次数)
irradiance, radiance 放射照度、放射輝度
irradiance volume イラディアンスボリューム
trilinear interpolation 3線型補間
light leaking 光漏れ
reflection probe リフレクションプローブ(反射プローブ)
parallax correction 視差補正
screen-space reflections (SSR) スクリーンスペースリフレクション(SSR)
screen-space global illumination (SSGI) スクリーンスペースGI(SSGI)
reflective shadow map (RSM) 反射シャドウマップ(RSM)
virtual point light (VPL) 仮想点光源(VPL)
light propagation volumes (LPV) ライトプロパゲーションボリューム(LPV)
voxel cone tracing ボクセルコーントレーシング
dynamic diffuse global illumination (DDGI) DDGI
hysteresis ヒステリシス
resampled importance sampling (RIS) リサンプル重点的サンプリング(RIS)
weighted reservoir sampling 重み付きリザーバサンプリング
ReSTIR ReSTIR
surface cache サーフェスキャッシュ

24.13 課題

  1. ★ n=1、f=625、N=4について、均等、対数、実用(λ=0.75)の分割距離と、各カスケードの比Ci/Ci−1を求めて下さい。各方式で品質を制限するのはどのカスケードですか?
  2. ★ 幅0.6 mの面光源が、5 m離れた床を照らします。椅子の下でのブロッカー探索ではブロッカーの平均距離が4 m、床に置かれた本の下では4.9 mでした。両方の半影の幅と、床で1テクセルが1 cmを覆う時の其のテクセル数を求めて下さい。
  3. ★★ 3 × 3のVSMのフットプリントに、深度0.4のテクセルが6つ、0.7のテクセルが3つあります。M1、M2、σ2を求め、t=0.7とt=0.9の受け手についてpmaxを求めて下さい。どちらの場合も真の照らされる割合と比べ、どちらの結果が光漏れかを説明して下さい。
  4. ★★ γ=0のGTAOのスライスの地平線がθ1=−30°とθ2=75°です。a^と各側の寄与を求めて下さい。どちらの側が自分の半分をより多く失いますか?又、高い地平線が角度から想像される以上にコストが大きいのは何故ですか?
  5. ★★★ 4つの候補のリザーバを手で動かして下さい:一様に選ぶ50個の光源、目標値p^=0.10,0.04,0.25,0.01、乱数u=0.3,0.9,0.5,0.2。どの光源が残り、光源が見える場合のWと推定値は幾つで、各候補が残る確率は幾つですか?次に、空間的な隣のリザーバを併合する前に何故此のピクセルのp^で重みを付け直す必要があるのか、時間方向のMに何故上限を付けるのかを説明して下さい。

選択問題

問1 ★ 次のうち、リアルタイムの影付けの手法について正しい物はどれか。

あ 均等なカスケード分割では、全てのカスケードの遠い側と近い側の距離の比が同じになり、透視エイリアシングに対して理想的である。
い 分散シャドウマップは平均の深度だけを保存するので、影の判定の前にぼかす事が出来ない。
う 固定カーネルのPCFは、遮蔽物と受け手の間の距離に応じて半影を正しく広げる。
え PCSSは、探索領域で見つけた遮蔽物の深度を平均し、光源の大きさと遮蔽物及び受け手の距離から半影の幅を推定するので、接地点の近くでは影が鋭いまま保たれる。
お 光源の視錐台を整数テクセルにスナップすると、カメラが動いた時にカスケードのちらつきが増える。

問2 ★★ 次のうち、アンビエントオクルージョンについて正しい物はどれか。

あ アンビエントオクルージョンは光源の位置に依存するので、光源が動く度に変わる。
い Crysis(2007年)のSSAOは、レイトレーシング用のハードウェアでシーンの三角形に対してレイを飛ばしていた。
う GTAOは画面空間の各スライスで2つの地平線角を見つけ、スライス内の余弦で重み付けした可視性を解析的に計算するので、標本化するのはスライスの方向だけである。
え 余弦で重み付けすると、全周で仰角30°の地平線は環境光の半分を遮る。
お ベイクしたAOマップは、普通は直射日光に掛けて落ち影を作る。

問3 ★★ 次のうち、リアルタイムの大域照明について正しい物はどれか。

あ 遠くの環境からの放射照度は、L2まで(色のチャンネル毎に9係数)の球面調和関数で良く近似出来る。0で切った余弦との畳み込みが高いバンドを強く弱めるからである。
い 反射シャドウマップは、カメラからボクセルの格子を通してレイを辿る事で間接光を計算する。
う DDGIのプローブは放射照度だけを保存し、可視性を一切調べないので、壁を通した光漏れを減らせない。
え スクリーンスペースリフレクションは、画面の外にある物や他の物の陰に隠れた物も映せる。
お ReSTIRは毎フレーム、シーンの全ての光源にシャドウレイを飛ばし、一番明るい物を残す。

24.14 実践課題

小さなシーンを手で照らします:カメラの為にカスケードを計画し、図から点のアンビエントオクルージョンを測り、6つの数からL1のライトプローブを作ります。
方眼紙、定規、分度器、鉛筆、2色のペン、電卓を用意して下さい。

パートA:カスケード。
カメラはn=2 mからf=162 mまでを見て、N=4個のカスケードを使います。

  1. 計算する。 均等、対数、実用(λ=0.5)の分割距離を求めます。
  2. 描く。 0から162 mまでの横棒を3本描き(1 cm = 10 m)、24.2節の図の様に各方式の分割に印を付けます。
  3. 比。 各カスケードについてCi/Ci−1と、24.2節の大まかなテクセル対ピクセルの係数0.527Ci/Ci−1を求めます。各方式の最悪のカスケードに丸を付けて下さい。

パートB:図から求めるアンビエントオクルージョン。
横から見た図(xは右、yは上、1単位 = 2 cm):床y=0、法線(0,1)を持つ点P=(0,0)、左にある低い箱(Pに一番近い上の角が(−1,0.5)で、箱はx≤−1、y≤0.5を占めます)、そして右のx=2から始まる高さ2の壁です。AOの半径は3です。

  1. 描く。 シーンと、Pの周りの半径3の半円と、Pから角(−1,0.5)及び(2,2)への2本の地平線を描きます。両方の角が半径の内側にある事を確かめて下さい。
  2. 測る。 両方の地平線の仰角を分度器で測り、次にarctanで計算し、法線からの角度θ1<0<θ2に直します。
  3. 計算する。 GTAOの式(γ=0)でスライスの可視性a^を求めます。次に見える範囲を1色目のペンで塗り、より開いた側に傾いたベントノーマルを目分量で描きます。

パートC:6方向からのL1のライトプローブ。
プローブが6つの軸方向の放射輝度を測りました:+y(空)から3、−y(地面)から1、±xと±zからは其々2です。
6つの値を球面の標本と見なし、其々が立体角4π6を代表するとします。

  1. 射影する。 c0,0=4π6∑kLk⋅0.282095、c1,y=4π6∑kLk⋅0.488603dk,y(xとzも同様)。dk,yは方向kのy成分です。
  2. 再構成する。 4つの係数から6方向の放射輝度を、そして上、下、横を向いた法線について放射照度E(𝐧)=πc0,0⋅0.282095+2π3(c1,xnx+c1,yny+c1,znz)⋅0.488603を求めます。
  3. 光漏れを確かめる。 プローブは廊下のx=2にあり、明るく照らされた部屋のx=0にある2つ目のプローブの放射照度は30で、其の間のx=1.5に壁があり、シェーディングする点はx=1.8です。横向きの放射照度をx=2のプローブの値として、2つのプローブの間を線型補間し、次に遠いプローブの重みに可視性の係数0.1を掛けて繰り返して下さい(重みは正規化し直します)。

自分で確認する方法:

  • パートA:4つの比の積はどの方式でもf/n=81です。対数分割は2、6、18、54、162で、比は全て3です。
  • パートB:左の地平線の仰角はarctan(0.5/1)=26.57°、右は45°です。tan63.43°=2なのでcos126.87°=−0.6が丁度成り立ち、a^は0.6と0.7の間のきりの良い数になります。
  • パートC:6つの値は丁度1次関数L=2+yなので、L1の再構成は6つの値を全て返し、放射照度は2π+2π3nyと等しくなる筈です。
  • パートC:可視性で重み付けした結果は、単純な補間より、近いプローブの値にずっと近くなる筈です。


コメント(0)

まだコメントはありません。