この記事はレイトレーシングにおけるGGX分布を使用したMicrofacetのBRDFについてまとめたものです。内容としては
となっています。
※ この記事ではBRDFの計算は接空間上で行うこと、Y-upの左手座標系を使用していることを前提としています。
isotropic(等方性)GGX
Microfacet理論
実際の物体は鏡のような滑らかな鏡面だけではなく、粗い金属面も存在しています。粗い表面では細かい傷といった目に見えないほど小さい凹凸が存在し、それぞれで光が反射することで一方向からの光も様々な方向へと乱反射することで粗い表面の質感というものが表れています。
こうした「粗さ」を持つ鏡面のBSDFを作ろうということで考えられたのがMicrofacetモデルです。MicrofacetモデルではMicrofacetと呼ばれる小さな凹凸が存在するものとして考え、そこに入った光はその小さなMicrofacetで鏡面反射するということを仮定して解析的にBSDFとして計算することができるようにしています。
実際に使用される計算式は以下のようになっています。これはBRDFのものでBTDFはまた別にあります(記事ではBRDFのみ扱います)。
- : 入射方向ベクトル
- : 出射方向ベクトル
- : ハーフベクトル
- : 法線ベクトル
ハーフベクトルはジオメトリの法線とは別にそこに小さな凹凸があるとして(Microfacet)そのミクロな面における法線を意味します。なので出射方向ベクトルを求める際はこのハーフベクトルを法線とした反射を考えます。 次の項ではBRDFに含まれている関数について述べていきます。また、これら関数は粗さのパラメーターとして0~1のを持ちます。このについて、単純に0~1を指定するのではなく、roughnessとして0~1の値を設定してそれを二乗した値を使用することが多いです。これは理論的な話ではなく、こうした方が粗さの変化が直感的に近いものになるから使われているだけです(こうしなくても問題はないです)。
フレネル項
これは物体のフレネルを司る項で、角度による反射の度合いを示します。実際の計算ではSchlick近似式と呼ばれる
という式を使用します。ここでのとはスペキュラF0とも呼ばれているやつでフレネルの最小値(面に対して真正面から見た時のフレネルの値)です。要は金属の色でして、こちら側から決定してあげれば大丈夫です((0.9,0.9,0.9)みたいに決めてもそこまで問題ないです)。
法線分布関数
法線分布関数とは与えられたに対して、を向いたMicrofacetがどれだけあるかを面積として解析的に表現する関数です。これはつまりMicrofacetがどんな形状化かを法線分布として示す関数となっています。このため、は反射の性質を根本的に決める重要な関数となっており、また定義として以下のような性質を持ちます。
は以上のような定義を満たせばよく、様々な種類があります。有名なものとしてはBeckmann分布といったものがありますが、最も実用上使われるのは題名にもあるようにGGX分布と呼ばれるものです(なぜisotropicというものがついてるのかは後述)。
GGXの法線分布関数の式は以下のようになっています。
この式のはハーフベクトルとジオメトリの法線がなす角であります。
マスキングシャドウ関数
Microfacetを考えた際、反射した光が別の凹凸によって遮られる場合というのが考えられます。こうした凹凸の遮蔽を表すのがマスキングシャドウ関数となります。当然、マイクロファセットの形状によって遮蔽される形というのは変わりますのでそれぞれ専用のGが作られています。
は一般的にSmithのMasking-shadowing関数が使われており、という関数で
2つのベクトルそれぞれで計算した積として表現できます。
GGXでははどのような関数かというと
という式でして、はラフネスの値を用いて、
この際、計算に出てきたはとジオメトリの法線がなす角です。
また、についてより正確な値が出る計算式としてheight correlated Smith shadowing-maskingというものがあります。これは直接を使って、
と記述されます。こちらを使っても特に問題はありませんのでどっちを使っても大丈夫です。
重点的サンプリング
GGXはラフネスの値によって方向性が異なるため、半球一様サンプリングは不適切です。そのため、GGX専用の重点的サンプリングを考えてあげる必要があります。 では、GGXのどの部分に対して重点的サンプリングをするかという問題があります。GGXのBRDFはラフネスが低ければ指向性を持ち、鏡面に近づきます。一方で、ラフネスが高ければ等方性が強まり、ざらざらした質感へと変化します。このようなGGXの特徴の根幹を司る部分というのは法線分布関数です。そのため、このに比例するように重点的サンプリングを考えていく必要があります。
重点的サンプリングを考える際、pdfとしてその関数は全領域で積分したら1にならなくてはいけません。幸いは定義からとすれば、式(1.3)のように積分すると1になる関数です。ですので関数|\omega_m \cdot n|をpdfとして扱い重点的サンプリングを考えます。
また、重要な点としてはハーフベクトルの関数のため、の重点的サンプリングでは出射方向ベクトルではなく、ハーフベクトルをサンプルする形になります。サンプリングは乱数[0,1]をとして、偏角と方位角を
という式によって求めます。これによってハーフベクトルのの角度が定まったのでxyz座標で
というハーフベクトルがサンプリングされます。 ですが我々が欲しいのはハーフベクトルではなく出射方向ベクトルです。ここからどうやって出射方向ベクトルを求めるかというと、Microfacet理論ではミクロ的にみれば光はMicrofacet上で鏡面反射していることが前提になっているため、ミクロな法線に相当するハーフベクトルを法線として入射ベクトルの反射方向を出射方向ベクトルとします。
こうして出射方向ベクトルをサンプリングできたのですが、大きな注意点としてpdfにヤコビアンがかかることがあります。
あくまで(1.9)のサンプリングというのはハーフベクトルを作るサンプリングであって、直接的に出射方向ベクトルをサンプリングするものではありません。出射方向ベクトルはハーフベクトルの反射変換を経て得ています。このため、出射方向ベクトルの確率密度pdfというのは変換を挟む以上ではなくなります。
変換によるpdfの変化というのはハーフベクトルと出射方向ベクトルのヤコビアンによって表され、その値というのは
となっています。これをハーフベクトルのpdfに掛けてあげれば出射方向ベクトルのpdfというのが得られ、結果としては
というpdfであることに注意してください。
実装例
実装の前にとの内積や成す角などが多用されるので関数として以下を定義しておきます
namespace BSDFMath { inline float cosTheta(const Vec3& w) { return w[1]; } inline float cos2Theta(const Vec3& w) { return w[1] * w[1]; } inline float sinTheta(const Vec3& w) { return std::sqrt(std::max(1.0f - cosTheta(w) * cosTheta(w), 0.0f)); } inline float tanTheta(const Vec3& w) { return sinTheta(w) / cosTheta(w); } inline float tan2Theta(const Vec3& w) { return tanTheta(w) * tanTheta(w); } inline float cosPhi(const Vec3& w) { float sintheta = sinTheta(w); if (sintheta == 0) return 1.0f; return std::clamp(w[0] / sintheta, -1.0f, 1.0f); } inline float sinPhi(const Vec3& w) { float sintheta = sinTheta(w); if (sintheta == 0) return 0.0f; return std::clamp(w[2] / sintheta, -1.0f, 1.0f); } inline float cosPhi2(const Vec3& w) { return cosPhi(w) * cosPhi(w); } inline float sinPhi2(const Vec3& w) { return sinPhi(w) * sinPhi(w); } }
これらは接空間で法線が(0,1,0)であることを前提として考えて計算されていることに注意してください。
では次にの実装をしていきます。事前にパラメータとしてF0の値と粗さのを持っているものとして考えていきます。 まずフレネルについてはshilick近似で
Vec3 Fresnel(const float im) const { float delta = std::max(1.0f - im, 0.0f); return F0 + (Vec3(1.0f) - F0) * std::pow(delta, 5); }
と実装できます。
次にGは以下のように実装できます
float G1(const Vec3& v)const { float delta = alpha * BSDFMath::tanTheta(v); float result = 2.0f / (1.0f + std::sqrt(1.0f + delta * delta)); return result; } float lambda(const Vec3& v) const { float delta = std::max(alpha * BSDFMath::tanTheta(v), 0.0f); return std::max((-1.0f + std::sqrt(1.0f + delta * delta)) / 2.0f, 0.0f); } float G2(const Vec3& o, const Vec3& i) const { return this->G1(o) * this->G1(i); }
height correlated Smith shadowing-maskingの場合は以下のようになります
float G2(const Vec3& o, const Vec3& i) const { \\ height correlated Smith shadowing-masking return 1.0f / (1.0f + this->lambda(o) + this->lambda(i)); }
Dの実装は以下のようになります。
float D(const Vec3& m) const { const float tan2theta = BSDFMath::tan2Theta(m); if (std::isinf(tan2theta)) return 0; const float cos4theta = BSDFMath::cos2Theta(m) * BSDFMath::cos2Theta(m); const float term = 1.0f + tan2theta / (alpha * alpha); return 1.0f / ((PI * alpha * alpha * cos4theta) * term * term); }
また、ハーフベクトルのサンプリングは以下のようになります
Vec3 samplem(float u, float v) const { float theta = std::atan(alpha * std::sqrt(v) / std::sqrt(std::max(1.0f - v, 0.0f))); float phi = 2.0f * PI * u; return Vec3(std::cos(phi) * std::sin(theta), std::cos(theta), std::sin(phi) * std::sin(theta)); }
以上の関数が実装をまとめたBRDFの実装は以下のようになります。
Vec3 samplingBSDF(const Vec3& wi, Vec3& wo, float& pdf, const std::shared_ptr<Sampler>& sampler) const override { Vec3 i = wi; Vec3 n = Vec3(0.0, 1.0, 0.0); Vec3 m = this->samplem(sampler->sample(), sampler->sample()); Vec3 o = reflect(wi, m); wo = o; if (wi[1] < 0.0f) return Vec3(0.0); float im = absdot(i, m); float in = absdot(i, n); float on = absdot(o, n); Vec3 F = this->Fresnel(im); float G_ = this->G2(i, o); float D_ = this->D(m); Vec3 brdf = F * G_ * D_ / (4.0f * in * on); pdf = D_ * BSDFMath::cosTheta(m) / (4.0f * absdot(m, o)); return brdf; }
出力画像
が小さいほど理想鏡面に近づき、一方で大きいほど粗い質感へと近づきます。でレンダリングした結果は以下のような感じになります。
のレンダリング結果は以下のようになります。ラフネスが高いとG項が小さくなるため結構暗めの質感を示すようになります。
anistropic (異方性あり)GGX
実はisotropic GGXでは表現できない金属の質感というのが存在します。例としては、シンバルや金属のコップなどでよくある直線的な線が目立つような旋盤加工した金属が挙げられます。こうした金属のMicrofacetはレコードの表面のような波状の凹凸として存在しています。
これが意味することとは「向きによって粗さが違う」ということです。そのため、当然ながら向きによって光の反射具合も変化します。こうした性質を「anistropic(異方性)」と呼びます。isotropic GGXはどんな方向にも一様な粗さを持つものを前提として考えており、こうした理由から旋盤加工した金属の質感は扱うことができないというわけです。
これに対して、異方性を取り扱えるようにしたのがanistropic (異方性あり) GGXです。anisotropic GGXはu,v方向(接空間における接戦方向それぞれ)でそれぞれ粗さをと別々に考え、法線分布などを作っています。
anistropicGGXの見た目に関してはこちらのツイートがわかりやすいです。基本的にハイライトが伸びるような見た目を示します。
異方性反射の基礎知識。
— 灰ならし (@hainarashi) 2021年6月8日
これを知っておくと、異方性に関する操作内容がイメージしやすく、扱いやすくなります。#Blender #b3d pic.twitter.com/haXZpeYuKX
異方性あり法線分布関数、シャドウマスキング関数
法線分布やシャドウマスキング関数はisotoropicGGXとそこまで変わらず、の時はちゃんとisotropicGGXのものと一致するように作られています。については以下のようになります。
ここでのとは接空間におけるハーフベクトルの方位角になります。
一方でシャドウマスキング関数そのものは変える必要はなく、に含まれるについて、
として計算することで計算が可能です。
異方性有り法線分布サンプリング
が変わった以上、重点的サンプリングについてもいくつか変更点があります。の式は以下のようになります。
ただし、実装上の注意としてはArctanが存在することです。Arctanは多価関数であり、C++では主値としてとなるようにされています。そのため、乱数に応じて出てきた値にオフセットを加える形で数値を合わせる必要があります。実装上の式としては以下のようになります。
この点にだけ注意すれば実装は(2.3)を行えば大丈夫です。pdfはisotropicと同様です。
実装例
異方性有りの場合、粗さのパラメータがの2つのパラメーターになり、 anisotropic GGXの法線分布関数は
float D(const Vec3& m) const { const float tan2theta = BSDFMath::tan2Theta(m); if (std::isinf(tan2theta)) return 0; const float cos2phi = BSDFMath::cosPhi2(m); const float sin2phi = BSDFMath::sinPhi2(m); const float cos4theta = BSDFMath::cos2Theta(m) * BSDFMath::cos2Theta(m); float term_alpha = cos2phi / (a_x * a_x) + sin2phi / (a_y * a_y); float term = 1.0f + tan2theta * term_alpha; return 1.0f / (M_PI * a_x * a_y * cos4theta * term * term); }
となります。また、シャドウマスキング関数はのの扱いを変えればいいので、
float lambda(const Vec3& v) const { float alpha = std::sqrt(BSDFMath::cosPhi2(v) * a_x * a_x + BSDFMath::sinPhi2(v) * a_y * a_y); float delta = std::max(alpha * BSDFMath::tanTheta(v), 0.0f); return std::max((-1.0f + std::sqrt(1.0f + delta * delta)) / 2.0f, 0.0f); }
となります。最後にハーフベクトルのサンプリングを以下のように実装すれば異方性GGXの完成です。
Vec3 samplem(float u, float v) const { float tanTerm = std::tan(2.0f * PI * u) * a_y / a_x; float phi = std::atan(tanTerm); if (u >= 0.75) { phi += 2.0f * PI; } else if (u > 0.25) { phi += PI; } const float cos2phi = std::cos(phi) * std::cos(phi); const float sin2phi = std::sin(phi) * std::sin(phi); const float term_alpha = cos2phi / (a_x * a_x) + sin2phi / (a_y * a_y); const float term = v / ((1.0f - v) * term_alpha); const float theta = std::atan(std::sqrt(term)); return Vec3(std::cos(phi) * std::sin(theta), std::cos(theta), std::sin(phi) * std::sin(theta)); }
出力画像
anisotropic GGXはハイライトが特定の方向に延びるという特徴があります。a_x < a_yの場合横方向にハイライトが伸び、一方でa_x > a_yの場合は縦方向にハイライトが伸びます。
実際にレンダリングしてみると以下のような感じになります。
可視法線分布サンプリング
可視法線分布とは
今までやってきた重点的サンプリングは法線分布に比例するよう行ってきましたが、BRDFの式(1.1)を見ればわかる通りBRDFの値を決める要素はだけではなく、マスキングシャドウ関数もあります。
法線分布からサンプリングをする場合、こうしたを考えないでサンプリングするので折角サンプリングした反射光がGによってかき消されてしまうことがあります。特にラフネスが高い物体の輪郭付近ではG項が小さくなりやすく、この理由から法線分布サンプリングではノイズが比較的表れてしまうという弱点が存在します。
そこで「G項に打ち消されない法線分布」というものを作り、それを元に重点的サンプリングをすればこのようなG由来のノイズが減らすことができます。この「G項に打ち消されない法線分布」は「可視法線分布」と言い、[Heitz et al. 2014]で可視法線分布サンプリングとして提案されました。
どうやってサンプリングするか
可視法線分布の定義は以下のようになっています。
大まかにみれば法線分布とシャドウマスキング関数がかかっているということでG項を考えた法線分布が単純に作れていることがわかると思います。ちゃんとこれは半球面で全積分するとこれは
1になり、ちゃんと逆関数法でのpdfの条件を満たしています。
では、これを重点的サンプリングするため逆関数を考えていくのですがスロープ表現というものを考えたりと非常に複雑で難しいことを行う必要があります。その方法はtatsyさんの導出から実装までの丁寧な記事がありますのでこちらをご参照ください
この方法は[Heitz et al. 2014b]で提唱されていたものですが、難しくまた一部正確ではなかったらしいです。 ですので、新たに[Heitz 2016]でGGXに関してより簡単かつ厳密な可視法線分布サンプリングする方法が提唱されました。こちらはスロープ表現を使用する方法よりもはるかに簡単な手法であり、私はこちらを実装しました。(Beckmannにおける手法も一応あります[Wenzel 2014])
https://hal.archives-ouvertes.fr/hal-01509746/document
サンプリングの手順
[Heitz 2016]の内容を書いていきます。なお論文ではz-upの座標系を用いていましたが、ここの説明ではy-up座標系で行おうと思いますので注意してください。
まず、最初にを,で引き延ばしてそれを正規化したものをとします。これが視点ベクトルに相当します。
この視点ベクトルから正規直交基底を作ります。これの作り方は接空間で作るようなものと同じです。
また、パラメーターとしてを以下のように定義します。
一様乱数[0,1]を用意し、極座標を次のようにサンプリングします。についてはの値によって場合分けがあることに注意してください。
このようにサンプリングした極座標というのはどこの極座標かというと以下の図で言う青と緑の半円板上での極座標です。青の円盤はに平行な半円盤であり、の時はこちらからサンプリングします。一方緑の部分はの時にサンプリングする場所となっています。
なのでこの極座標よりサンプリングする点は
となります。この点を視点ベクトル方向へと半球面上の投影し、その点をとするとは
と得られます。こうしてできたについて、最初のように異方性でまた伸ばして正規化するとようやくハーフベクトルが得られます。
以上の手順が可視法線分布で重点的サンプリングする方法になります。あとは同様にハーフベクトルと視点ベクトルから反射ベクトルを求めてあげれば良いです。 pdfについては可視法線から得ているため、(ハーフベクトルのヤコビアンは)
となります。
実装例
実装例は以下のようになります。なおBRDFの計算のマスキングシャドウ関数がheight correlatedの方であっても特に問題はありませんのでその辺は気にしなくて大丈夫です。
Vec3 samplem(const Vec3& V_, float u, float v) const { Vec3 V = normalize(Vec3(a_x * V_[0], V_[1], a_y * V_[2])); Vec3 n = Vec3(0, 1, 0); if (V[1] > 0.99) n = Vec3(1, 0, 0); Vec3 T1 = normalize(cross(V, n)); Vec3 T2 = normalize(cross(T1, V)); float r = std::sqrt(u); float a = 1.0f / (1.0f + V[1]); float phi; if (a > v) { phi = PI * v / a; } else { phi = PI * (v - a) / (1.0f - a) + PI; } float P1 = r * std::cos(phi); float P2 = r * std::sin(phi); if (a < v) P2 *= V[1]; Vec3 N = P1 * T1 + P2 * T2 + std::sqrt(std::max(1.0f - P1 * P1 - P2 * P2, 0.0f)) * V; N = normalize(Vec3(a_x * N[0], N[1], a_y * N[2])); return N; }
法線分布サンプリングとの比較
法線分布サンプリングが弱いのはが大きいのにも関わらずが小さくなる時です。これが起きやすいのは大抵ラフネスが高めで法線に対して入射ベクトルが浅い、つまり球体で言う端っこのあたりです。実際に注意深く見てみると球体の端っこのあたりはノイズが強めです。 そうしたノイズへの対策として可視法線分布サンプリングが考えられたので可視法線分布サンプリングによるGGX球体のノイズを比較してみます。サンプリング回数は共に100でRoughnessの値は0.7として両者によるサンプリング画像は
となりました。端っこの部分だけを注目するとちゃんと可視法線分布サンプリングではちゃんと先ほど述べたノイズが軽減されていることがわかると思います。
ちなみにこのサンプリングによる処理時間は200540msと198181msとほとんど等しいぐらいでした。そのため可視法線分布サンプリングは事実上より実用的なサンプリングとして使うことができると考えられます。Blenderのcyclesでも多分可視法線分布サンプリングを使っているっぽいです(おそらくスロープ空間によるサンプリングを使用しているみたいです)。
デバッグのやり方
BRDFのデバッグは中々これといったエラーを出す方法が少なく難しいですが、GGXについてはいくつかデバッグとして使える方法があるのでいくつか書いておこうと思います
法線分布関数のデバッグ
法線分布関数について、定義からを半球面で積分すると1になるという性質を持ちます。
そのため、ハーフベクトルをランダムに生成してこの積分をモンテカルロ積分してあげればちゃんと1に近づきます。この性質を利用してが正しく実装されてるかのテストすることができます。
異方性有りののデバッグ
異方性有りについてはもちろんなので上記のデバッグができます。それ以外にも、それぞれのラフネスが一致している時、である等方性に完全一致する(式的にも)ため、同一の乱数を与えたりし、内部の数値が等方性のと一致するかということでもデバッグが可能です。
シャドウマスキング関数のデバッグ
シャドウマスキング関数についてはが含まれる可視法線分布が半球面で積分すると1になるという性質から、可視法線分布の値を同様にモンテカルロ積分してちゃんと1に近づくかどうかでデバッグできます。
Blenderなどの3Dソフトでリファレンスを作る
オフラインレンダリングできる3Dソフトで大体同じようなシーンを作り、見た目がおおよそ一致しているか(特にハイライトの形状)を見比べるのがよいと思います。
参考資料
- Microfacetモデルの重点サンプリング by tatsy.
Microfacetモデルの重点サンプリング - tatsyblog
- Microfacet入門(1) by ushiostarfish
Microfacet入門(1) - ushiostarfish’s diary
- [walter 2007] Microfacet Models for Refraction through Rough Surfaces
Microfacet Models for Refraction through Rough Surfaces
- [Heitz et al. 2014a] Understanding the Masking-Shadowing Function in Microfacet-Based BRDFs
https://jcgt.org/published/0003/02/03/paper.pdf
- [Heitz et al. 2014b] Importance Sampling Microfacet-Based BSDFs using the Distribution of Visible Normals
https://hal.inria.fr/hal-00996995v1/document
- [Heitz 2016] A Simpler and Exact Sampling Routine for the GGX Distribution of Visible Normals
https://hal.archives-ouvertes.fr/hal-01509746/document
- Sampling Anisotropic Microfacet BRDF by AGraphicsGuy
Sampling Anisotropic Microfacet BRDF – A Graphics guy's note
- 8.4 Microfacet Models by Physically Based Rendering
- 異方性反射の基礎知識 @hainarashi