[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/.]
理論#
[Heitz et al. 2016Heitz, E., Dupuy, J., Hill, S. and Neubelt, D. 2016. Real-time polygonal-light shading with linearly transformed cosines. ACM Trans. Graph. 35, 4. 10.1145/2897824.2925895. https://eheitzresearch.wordpress.com/415-2/.]
問題#
我々がここで実際に解こうとしているものとは?
まず、BRDFがある。これは、マテリアルが特定のシェーディングポイントで光をどれだけ散乱させるかを説明する球面関数である。
このプロットは与えられるビュー方向に対するものである…
(また、相互的に、与えられるライト方向に対するものでもある)
…そして、光が散乱して目に戻ってくる方向を表現する。
注意: トーク全体を通してBRDFについて述べていることだろうが、これは実際には任意の球面関数とすることができると思われる(計算はそのまま)。
次に、我々はシェーディングポイントに到達している多角形からライティング(入射放射輝度)を持っている。
シェーディング結果(出射放射輝度)はこの球面多角形上のBRDFの積分である。
これは扱いにくい☹#
これは、単一のライト方向に対してBRDFを計算する必要のみがある、ポイントライティングよりさらに難しい。我々は今や多数の方向を考慮する必要がある。
オフラインレンダリングでは、我々はこれをモンテカルロサンプリングで解くかもしれないが、リアルタイムにとって、これは実行可能な選択肢ではない --- 遅すぎるかノイジーすぎるかのいずれかとなるだろう。
我々は代わりに閉形式の解、すなわち、サンプルする必要がなく即座に正しい答えをもたらすであろう簡単に計算できる等式を求めたい。
単純なケースが存在する#
単純な分布は閉形式で多角形上に積分できる。一様な球面分布はその一例である。
その球面多角形上の積分は多角形の立体角の計算と等価である。
これに対する閉形式表現がある: Girardの定理
もうひとつの単純な例は一様な半球分布である。
これは単なる半球に切り取られた多角形の立体角である。
より興味深い例はコサイン分布である。
ディフューズまたはランバート とも呼ぶことができる。
多角形上でその積分を計算することは放射照度(または、形態係数)をもたらす。
また、それに対する閉形式表現がある。これは、18世紀にLambertによって導出された!詳細は後ほど。
限定的すぎる☹#
こんなところでしょうか!残念ながら、これらの解は我々が必要とするものに対して限定的すぎる。
我々はすべての周波数を欲する#
我々は、鏡のようなものから半光沢のもの、粗いものまでの、広範囲のマテリアルを表現することを可能にしたい。
これは表現力の観点においてリアルタイムシェーディングモデルから期待されるであろうことの必要最小限のことである。エリアライトを導入するためにこれを諦めることは意味を成さない。
我々は異方性を欲する#
現実世界のマテリアルもまたグレージング角で強い異方性(または、‘引き伸ばされたハイライト’)を示す。
業界標準のマイクロファセットモデルは納得のいくようにこれを再現することが可能である。これは大きさを持たない光源を用いたリアルタイム実装で見慣れた効果であり、であるならば、多角形エリアライトで同じ挙動を達成することを可能としたいだろう。
入場: Linearly Transformed Cosines#
つまり、要約すると、我々は高速でノイズのない方法において一般的なBRDFと多角形光源の積を計算したいが、これを行う方法は現時点で存在しない。
これは長きに渡る悩みの種であり、ここでプレゼンテーションを止めなければならないならば失望させてしまうだろう。:)
幸いにも、我々はこの問題への解をなんとか見つけた。それが、Linearly Transformed Cosines (LTCs)である。
これは我々の論文の核となる貢献である。
私はLTCsの高レベルの概要を説明するが、(重ねて言うが)さらなる詳細はEricのスライドを参照して欲しい。
本質的に、その主なアイデアは単純な分布を取り、線形変換を適用することである。そうすることで、我々は広範囲のより洗練された’形状’(球面関数)を生成できる。
先程述べたクランプ済みのコサイン分布から始めよう。
XとYへ一様なスケーリングを適用するならば、分布のラフネスを変化させることができる。
XとYに異なる因数を用いると、異方性を生成できる。
そして、変換の左下の成分を通して、‘スキュー’を引き起こすことができる。
ランダム行列によってあらゆる種類の奇抜な挙動さえも生成できる。そのいくつかは二峰性分布を引き起こしさえもする。
そして、ご覧の通り、このアプローチは非常に高価である。
線形変換#
一般に、我々はコサイン分布を取り、任意の3x3行列を適用して、新しい分布を生み出すことができる。
その論文では、この方法で生成される分布の 類 をLinearly Transformed Cosinesと呼ぶ。
一覧表化#
では、どうのように実践でこれを用いるか?
多角形光源でGGXベースのBRDFの積分を計算したいということにしよう。まず、与えられるラフネスとビュー角度に対して、このBRDFをLTCで最も良く近似する線形変換を求める。我々はすべてのラフネスとビュー角度に対して前もってこれを行い、テーブル(=テクスチャ)に結果の行列を格納する。
実践では、これほど多くのデータを必要としない(詳細は論文を参照)。
#
そして、‘魔法のトリック’。実行時に、我々のBRDF-多角形の構成を取り…
逆変換#
そして、(現在のシェーディングポイントでビュー角度とラフネスに対する)そのBRDFのLTCフィッティングに基づく逆変換を多角形の頂点に適用する。
コサイン積分#
これはその構成を等価だがより単純な積分の問題に変化する。
これを’コサイン空間’へ変換し直すことと考えることができる。
ランバート、1760年!#
そして、我々はずっと昔の18世紀からJohann Heinrich Jambert(通称’Mr NdotL’)の研究のおかげでこれを解く方法を知っている。
面積分 → 辺積分#
実践では、直接的に面積分を計算するのではなく、球面多角形の境界上の一連の線/辺積分を計算する(辺ごとに1つ)。
与えられる辺に対して、2つの頂点とから…
…1Dの球面線積分を計算する(緑)。
これにはラジアン単位の弧の長さを含み…
直角ベクトルもあり(紫)…
…そして、これは局所的な表面の法線で内積を取っている。
そして、すべての辺でこの処理を繰り返し、結果を合計する。
これは面積分と同じ結果をもたらす。
このプレゼンテーションではこれがなぜ機能するかを説明する時間がない。これを考える方法のひとつは、流体シミュレーションのような他分野で遭遇していたかもしれない、Stokesの定理の応用である。しかし、これはほんの少し抽象的であり、そのため、この特定のケースのより直観的な取扱いについては、ここのEricの記事を参照のこと: https://hal.archives-ouvertes.fr/hal-01458129
float EdgeIntegral(float3 v1, float3 v2, float3 n) {
float theta = acos(dot(v1, v2));
float3 u = normalize(cross(v1, v2));
return theta * dot(u, n);
}
float PolyIntegral(float3 v[4], float3 n) {
float sum;
sum = EdgeIntegral(v[0], v[1]);
sum += EdgeIntegral(v[1], v[2]);
sum += EdgeIntegral(v[2], v[3]);
sum += EdgeIntegral(v[3], v[0]);
return sum / (2.0 * pi);
}とにかく、ここに見えるように、これは結果として本当にコンパクトでエレガントな実装となる。
よりシンプルにはできないだろうか?
おしまい?#
我々の仕事はこれで終わりか?もちろんそうではない…
研究山#
いいや、まだまだだ。例えるなら…
我々は研究山を降りたばかりかもしれない…
実装の暗黒塔#
…だが、我々は依然として実装の’暗黒塔’を登る必要がある。
実装#
- ラフネスとビュー角度に基づいてを探索する
- で多角形を変換する
- 多角形を半球上部にクリップする
- 辺積分を計算する
ちょっと順不同に、まずはこれらの辺積分を見ていこう
上手くできなさそうなことは何だろう?
大丈夫に見える?#
ここでは全部大丈夫に見える、よね?
明るくすると: 大規模なアーティファクト#
残念ながら、光の強度を上げると、問題が現れ始める。
float EdgeIntegral(float3 v1, float3 v2, float3 n) {
float theta = acos(dot(v1, v2));
float3 u = normalize(cross(v1, v2));
return theta * dot(u, n);
}ここに再びそのコードがある。
おかしい。上手く機能するはずではないのか?
良好だが…#
より数値計算的に安定であることを証明した数学的に等価な形式を用いることでその問題を迂回したと語ったことを恥じている。
これはその問題を部分的に修正し、デモでは良くなったが、確実にここではもっとひどくなっている。
acos: この四文字だ!#
犯人はacosだ!
acos: 悪が潜んでいる!#
皆さんのほとんどが恐らく知っている通り、これはintrinsicではない。
float acos(float inX) {
float x1 = abs(inX);
float x2 = x1 * x1;
float x3 = x2 * x1;
float s;
s = -0.2121144 * x1 + 1.5707288;
s = 0.0742610 * x2 + s;
s = -0.0187293 * x3 * s;
s = sqrt(1.0 - x1) * s;
return inX >= 0.0 ? s : pi - s;
}ここに標準の実装がある。HLSL、CUDA、OpenGL、そして、コンソールで等価なコードを確認した。基本形をもたらすsqrt(1 - x)があり、三次多項式が足される。
これはほとんどのアプリケーションに対して正確である一方、辺積分は多くの精度を必要とすることが判明している。標準のacosでは、いくつかの場合(高い強度のライティングやなめらかなレシーバー)でリンギングアーティファクトが起こり得る。
要するに、大量のトライアンドエラーの末、その解法が明らかとなった。つまり、sin(theta)全体でthetaに対するフィッティングを求めることだ。
(このフィッティングは、対象の関数におけるacosの完全に正確なCPU実装を用いて、オフラインで計算される。)
有理式フィッティング#
float EdgeIntegral(float3 v1, float3 v2, float3 n) {
float x = dot(v1, v2);
float y = abs(x);
float a = 5.42031 + (3.12829 + 0.0902326 * y) * y;
float b = 3.45068 + (4.18814 + y) * y;
float theta_sintheta = a / b;
if (x < 0.0)
theta_sintheta = pi * rsqrt(1.0 - x * x) - theta_sintheta;
float3 u = cross(v1, v2);
return theta_sintheta * dot(u, n);
}十分な正確性を得るために、これは三次の有理式フィッティングを必要とした。
これは実際にはそれほど高価ではない。つまり、今度は有理式に由来する追加の除算があるが、sin()の呼び出しを節約した。
注意: 残りの角度範囲はそこから計算できるので、0からpi/2までを必要とするのみである。
このcompound fitを行うことで、相対誤差(注)はかなり小さくなる。
旧: シータを計算するのにacosの標準シェーダ言語実装を使用
新: theta / sin(theta)のフィッティング
(注:これは実際には、相対誤差ではなく、元の関数と各近似との比である。これは、アンダーシュートやオーバーシュートを明確にする。)
有理式フィッティング | acos:悪は潜んでいる!#
では、問題に戻って…
有理式フィッティング | すべて良好!#
こちらが結果だが、かなり良くなっているように見える。
おまけ: より安価なディフューズ#
float3 EdgeIntegralDiffuse(float3 v1, float3 v2, float3 n) {
float x = dot(v1, v2);
float y = abs(x);
float theta_sintheta = 1.5708 + (-0.879406 + 0.308609 * y) * y;
if (x < 0.0)
theta_sintheta = pi * rsqrt(1.0 - x * x) - theta_sintheta;
float3 u = cross(v1, v2);
return theta_sintheta * dot(u, n);
}おまけとして、我々は、同等の正確性を必要としないので、ディフューズに対してより安価なバージョンを使う。
この場合、二次式に逃がすことができ、それによって、いくつかのMADと除算を節約する。
#
- ラフネスとビュー角度に基づいてを探索する
- で多角形を変換する
- 多角形を半球上部にクリップする
- 辺積分を計算する
それでは、次に行こう。
大丈夫に見える?#
また、すべて良好に見える。
blobby mess#
だが、より高い強度では、ハイライト形状が正しくない。
ここでの映像はないが、ビュー角度を変更するとき、特にグレージング角で、いくつかの目立つバンディングと補間の問題が起こる。
行列の正規化: 項4つ!#
これは我々が行ったトリックに実際に行き着いた。そこでは、我々は右下の値によって各行列の成分のすべてを分割した。つまり、そこは常に1であり、それ故に、格納する必要はないだろう。我々は4要素のテクスチャにその行列を対応させることを可能とすることでこれを行った。しかし、依然としてBRDFの大きさに対して5番目の成分を必要とした。そのため、シェーダにおける2回目のテクスチャフェッチが不可避であった。
結局、これはあまり節約にならず、この分割を導入することで重大な副次的効果を引き起こした。
…それは、行列成分のいくつかが最終的に(ラフネス, ビュー角度)領域上でより荒々しく変化するようになったことである。そのため、これらを上手く補間できない。
この再スケーリングを行わない場合、元の5つの成分はよりなめらかに変化し、より小さいダイナミックレンジを持つ(まだ試していないけど、成分あたり16ビット以下に逃がすことさえもできるかもしれない)。
blobby mess#
これが以前の、行列の再スケーリングを用いる結果で…
安定で、正確な形状#
こちらがその後、用いない結果である。
これは、異なる方法でデータを可視化することが役立つという良い助言である。我々は開発中にフィッティングされたLTC分布と元のBRDFを注意深く比較してきた一方で、後になるまで一覧表化した値では詳しく調べなかった。
おまけ: より安価なルックアップ#
vec2 uv = vec2(roughness, acos(dot(n, v)));↓
vec2 uv = vec2(roughness, dot(n, v));その時点では、我々はアーティファクトを減らすためにもうひとつのワークアラウンドを用いた。すなわち、cos(theta)ではなくthetaによってテーブルのルックアップをパラメータ化した。現在では、正しい修正により、もはやこれを行う必要がない。これはacosのコストを回避することができることを意味する。
#
- ラフネスとビュー角度に基づいてを探索する
- で多角形を変換する
- 多角形を半球上部にクリップする
- 辺積分を計算する
では、3つ目の問題、クリッピングについて。
今まで誤魔化してきたことは多角形の正しい結果(形態係数)を得るために、半球上部に多角形をクリップする必要があることである。
多角形のクリッピングは楽しくない…
我々はこれの様々なフレーバーを試した。
- 辺積分中の’オンザフライ’なクリッピング
- Morgan McGuireのクアッドクリッピング(https://casual-effects.com/research/McGuire2011Clipping/index.html)。これは分岐数を最小化するが、かなり多くのデータシャッフルを伴う。
- 点が地平線より上か下かに基づく巨大なswitch/if-else
最後のひとつはPS4で最速になる(とはいえ、Morganのに近い)よう調整されたが、これらすべては多数の命令や分岐を生成した。
分岐地獄#
その上、クリッピング処理は様々な辺の数、3から5まで、で結果を求める。なので、もっと多くの分岐がある!
注意深く試験し、ゲームコンソールのために生成されたアセンブリを調整することで得られるゲインはいくつかあるかもしれないが、この複雑さをすっかり回避してしまうほうが良いだろう。
#
辺積分に戻ろう。
平面に投影し(法線との内積を取ら)なければ、最終的にベクトル形式で行われる。
ベクトル形態係数#
これをベクトル形態係数、または、ベクトル放射照度と考えることができる。このベクトルをとしよう。
Fの長さ=Fの方向における形態係数#
これは非常に良い特性を持つ。すなわち、Fの長さ(ノルム)はFの方向における多角形の形態係数である。
我々は地平線にクリップされたかのように多角形の形態係数を近似するためにこれを使うことができる。
多角形→プロキシ球#
多角形の代わりに、同じ形態係数を持つ球を使うことができる。
注意: このアプローチへのインスピレーションに対してBrian Karisに感謝したい。(事実、彼はすでにこれを実際に行っていて、エリアライティングに関する一般的なEメールのやり取りの中で最終的な近似を快く共有してくれたが、これが同じ発想に基づいていたことはこの時点では明確ではなかった。基本的に我々はこれを再導出した。)
angular extent = asin(sqrt(length(F)))#
我々はFからの球のangular extentを計算することができる。
(これは球の形態係数がであるという事実に由来する)
仰角 = dot(n, normalize(F))#
その方向(または、仰角)も同様に
地平線クリッピングを伴う球[Snyder96]#
[Snyder 1996Snyder, J. M. 1996. Area Light Sources for Real-Time Graphics. https://www.microsoft.com/en-us/research/wp-content/uploads/1996/03/arealights.pdf.]
なぜこれが役立つのか?地平線でクリップされる球の形態係数に対する2、3つの(同等の)解析解がある。これはその1つである。
これを使うと、異なるangular extents(0からpi/2)と仰角の球に対するクリップされた形態係数を含む2DのLUTを事前計算できる。
実行時に、Fから計算されたextentと角度でこのテクスチャをルックアップすることができ、多角形のクリップされた形態係数の近似がもたらされる。
クリッピング#
ここに元々の高価なクリッピングを用いた結果がある。
クリッピングなし#
いずれのクリッピングも行わない場合、(他の問題も一緒になって)光源の近くで暗くなる。
プロキシ球#
ここにプロキシ球を用いた結果がある。正しい結果にかなり近い。
クリッピング#
比較のために、元々のやつをもう一度
非常に安価な近似#
float SphereIntegral(float3 F) {
float l = length(F);
return max((l * l + F.z) / (l + 1), 0);
}精度的な理由から、代わりに、元の形態係数で除算し、LUTに乗数を格納する方が良い。
LUTを完全に排除する可能性もある。John Snyderは三次Hermite曲線を組み込む2、3つの選択肢をもたらすが、これらは高価な方である。Fからのクリップされた球の形態係数の近似を計算する上記の関数は幾分か荒削りだが、効果的な代替案である。その名前は実際にはPolygonVectorFormFactorToHorizonClippedSphereFormFactorとすべきだが、余白が足りなかった。:)
おまけ: テクスチャリング#
論文では、事前にフィルタされたテクスチャによるテクスチャ付きエリアライトを行う方法もカバーした。様々な理由から、我々が用いるために選択するルックアップ方向は変換された多角形に垂直な方向である。これは上手く振る舞うが、常に正確な結果をもたらさない(例は論文を参照)。
フィルタされた境界領域#
我々はルックアップが元のテクスチャの外となるケースを扱うためにテクスチャに境界領域も追加しなければならない。
テクスチャフェッチの方向にFを使う#
Brian Karisによって提案された魅力的な代替案はルックアップ方向にFを使うことである。
これはFが常に多角形と交差するという利点を持つ。そのため、我々は事前フィルタされたテクスチャに境界を追加する必要はもはやない!
まとめ#
- 数値計算の問題を克服した
- ディフューズ+スペキュラに対する現在のパフォーマンス: 0.9ms, PS4 @ 1080p
- 実装の暗黒塔を踏破した?
- 今後: コード(Github)と注釈の更新
これらの変更のすべてを通して、我々は見た目の品質の問題と戦うことや大幅にパフォーマンスを向上させることが可能となった。
曰く、エリアライティングは依然としてポイントライティングと比べて比較的高価である。そのため、さらなる改良の余地が常にある。
我々は、より詳細な注釈でこのトークのフォローアップ --- (Fresnelのような)いくつかの追加のトピックや最適化を含む --- やソースコードの更新を行うだろう。
見て見ぬふり#
論文の焦点はエリアライトにあったが、依然として登るべき他の塔がある。
…エリアライトシャドウ!これは未解決の研究課題である。
誰かが実際にこれを解いてくれるはず!:)