Photon Mappingの実装の記録
青い本、Photon Tracingで最初のDifufse面で格納して終わり or Diffuseに当たる度に格納(つまり1つのフォトンが複数回フォトンマップに格納されるか否か)のどちらなのかが不明確だが、p.103で
L(S|D)*Dがフォトンマップによって記述される全ての経路である.
とあるのでDiffuseに当たる度に格納する模様.
のp.17にもそのようにアルゴリズムが記述されている.
青い本、Photon Tracingでpdfで割る操作の記述がないが、そうしないとUnbiasedにならないはず
(おそらく青い本ではDiffuseでLambertと完全Specularしか扱っていないから分母分子で相殺されて出てこない)
pbrtだとそこら辺はちゃんと書いてあるし、そこそこの定式化もなされている.
もっと理論的にしっくりしたものないかなと思って論文探したらこれが良さそう.
カメラ側からのパスの生成において、密度推定を半径内の点サンプリングに置き換えていてなるほどなあという感じ. この論文はVCMなどの発展手法につながる.kdtreeの実装
汎用的にしたいのでtemplate, C++20のconceptを使ってみる
struct Photon {
Vec3 flux;
Vec3 position;
Vec3 wi;
// implement Point
static constexpr int dim = 3;
float operator[](int i) const { return position[i]; }
Photon() {}
Photon(const Vec3& flux, const Vec3& position, const Vec3& wi)
: flux(flux), position(position), wi(wi) {}
};
template <typename T>
concept Point = requires(T& x, int i) {
{ T::dim } -> std::convertible_to<int>; // dimension
{ x[i] } -> std::convertible_to<float>; // element access
};
template <typename PointT>
requires Point<PointT>
class KdTree {
private:
struct Node {
int axis; // separation axis(x=0, y=1, z=2)
int idx; // index of median point
int leftChildIdx; // index of left child
int rightChildIdx; // index of right child
Node() : axis(-1), idx(-1), leftChildIdx(-1), rightChildIdx(-1) {}
};
std::vector<Node> nodes; // array of tree nodes
const PointT* points; // pointer to array of points
int nPoints;
};
Photon Tracingで光源から飛ばすフォトンのthroughputをどう初期化するか.
-
: 光源上でサンプリングされた点x_0 -
: Photon Tracingで最初にヒットした点x_1 -
: 光源上の点サンプリングのpdfp(x_0) -
: 光源上での方向サンプリングのpdfp(\vec{\omega_0}) -
: 点\beta(x_0, x_1) から点x_0 までのthroughputx_1
throughputは以下のようにすれば良い(https://pbr-book.org/3ed-2018/Light_Transport_III_Bidirectional_Methods/Stochastic_Progressive_Photon_Mapping#eq:particle-weight-definition)
ここで密度関数の変換公式
を使うと
Photon Tracingを実装したのでPhoton Mapを可視化してみる.
カメラ側からレイを飛ばし、最初の交差点の近傍にあるPhotonのthroughputをそのまま画像にする.
なんか偏っている気がする. というか光源部分が光ってしまっているのはおかしい.
光源上の点での方向サンプリングがバグっていた
修正すると以下のような画像が得られた。それっぽい
カメラからレイトレをし、点
この点から方向
-
: 追跡したPhotonの合計数(近傍Photonの数ではない点に注意)N_p -
: 近傍Photon数N_{neighbor} -
: 近傍Photonの点r からの最大距離x -
:\vec{\omega_j} 番目の近傍Photonの入射方向j -
:\beta_j 番目の近傍Photonのthroughputj
この式は以下で単位円上で定数となるようなカーネルを用いたものに対応する.
絵は出るようになったがなんか暗い
Path Tracingのリファレンスと比べても明らかに暗い
maxDepth=1でPath Tracingのリファレンスと比較するとBiasなさそうに見える
ということはPhoton Tracingのdepth > 1以降で何かがおかしい.
色々バグっていたので修正したところPath Tracingのリファレンスとよく一致してきた.
色ムラが見えるのは直接照明もPhoton Map経由で計算しているためだと思われる(間接照明のみPhoton Mapで計算することが多い)
なおバグは
- Photon Tracing時のPhotonの追加, 乱数生成がスレッドセーフでない
- KdTreeのK最近傍探索でqueueをpopし忘れて同じノードしか返ってこない
など. Throughputの計算は問題なさそうだった.
(フォトン数: 1000000, 密度推定に使うフォトン数: 100)
なお密度推定に使うフォトン数を1(つまり最近傍にあるフォトンしか使わない)にすると以下のようになる。ノイズが多くてパストレっぽい感じの見た目になる。
影になっている部分はフォトン密度が割とスカスカなのが分かる。
で与えられる.
この式からPhoton Mappingで使われるような
この論文中では
そしてPhoton Mappingで使われるような
となる。ここで
なおBalloon Estimatorは論文中に
When Applied globally, the esimate typically does not integrate to 1 and thus is usually not itself a density, even when
is. K
とある通り、一般的に推定した密度関数が積分して1にならないらしい。これがPhoton Mappingのバイアスの出処なのかなと思ったりする。
直接照明の計算は明示的な光源サンプリング(Next Event Estimation)、間接照明の計算はFinal Gathering(さらにレイを飛ばしてその先でPhoton Map使って放射輝度推定)にしてみた。
以下はFinal Gatheringやらない、やる場合の比較。
Final Gatheringやるとバイアスが目立たなくなることが分かる。
ここら辺の話は青い本だと8.2.1, 9.4辺りに書いてある。
pbrtだと
For diffuse surfaces, it’s generally worth using photons, though it can be worthwhile to trace one last bounce. This approach is called final gathering; if photons are used only after a diffuse bounce, then any errors from insufficient photon density are generally less visible, though more camera paths may need to be traced to eliminate noise. (See Exercise 16.8 for further discussion of final gathering.)
の1.3も分かりやすい.
Final gatheringで完全鏡面のあるシーンをレンダリングしたところ以下のように暗くなってしまった。
左: Final gatheringなし
真ん中: Final gatheringあり
右: Path tracing
何故こんなことが起きるかというと、Final gatheringではLSDEのようなパスを表現できないため。Final gatheringでレイを飛ばした先がSpecularだとそこにはPhotonがないため放射輝度の推定ができない。
そこで集光模様フォトンマップ(Caustics Photon Map)を別に用意する必要がある(青い本 9.3.1)。集光模様フォトンマップはSpecularに当たった後にDiffuseに当たったPhotonを格納する。
Final gatheringを使う時のレンダリングでは集光模様フォトンマップからの放射輝度推定も加える。するとLSDEのようなパスも表現できるようになる。
以下はFinal gatheringありの時に集光模様フォトンマップを使うようにした場合。
暗くなっているのが改善されているのが分かる。しかしバイアスが目立つ。完全鏡面のようにSpecularでフォトンが拡散してしまうような場合は相性が良くないのかもしれない。
ガラス球のシーンで比較したもの。
パストレの方が分散低い気がする。特にPhoton Mappingではガラス面に写り込んだ光源が綺麗に描画できていない。これはガラス面で反射か屈折かのRussian Rouletteをしているが、それのサンプル数が足りないため。
サンプル数をパストレと同じ10000に設定してしまうとPhoton Mappingの計算時間がとても長くなってしまう。
青い本では綺麗に描画しているが、どうやっているんだろう? Russian Rouletteやらずに2本のレイを追跡しているのか?
Pocolさんの実装を見るとdepthが浅いところでは反射と屈折の両方追跡していることが分かる。
一般的に
Final Gatheringやった場合の完全鏡面のシーンがリファレンスと比べてまだ暗いことに気づいた。
以下はリファレンスとの差分。球の下辺りに若干差がある。
これは集光模様フォトンマップを作成する時にSpecularに当たった後の最初のDiffuse面でPhotonの追跡を打ち切ってしまっているため、LSDSDEのようなパスをFinal gathering時に表現できないことが原因だった。
打ち切りをやめたらリファレンスと一致した。
青い本を見ると集光模様フォトンマップはSpecularに当たった後の最初のDiffuse面で打ち切って良いと書いてあるが、本が間違っているのか自分の実装が何か違うのかは分からない。
Final GatheringでIndirect Illumination計算する時に、飛ばしたレイが当たった場所がSpecularの時は再帰的に追跡して、最終的に当たったどこかのDiffuse面で密度推定やるようにしたら、Caustics Photon Map作成時にSpecularに当たった後の最初のDiffuse面でPhotonの追跡を打ち切ってもリファレンスと一致するようになった。
つまりこの方法でもLSDSDEみたいなパスが表現できている。
青い本は明示的に書いてないけどこれをやっているんではないだろうか。
モデル読み込みを実装し、水入りコーネルボックスをレンダリングしてみた。
Causticsがめちゃくちゃ綺麗に出てくれて感動
別の水入りコーネルボックス。
ガラス下の部分がパストレと比べて明るい。
原因はCaustics Photon Map作成時にLS+Dではなく、LDSのようなパスも含めてしまっていることだった。これによってLDSDSEのようなパスがFinal Gatheringでダブルカウントされる。
修正したがまだ明るいように見える。まだダブルカウントしているパスがあるんだろうか。
とりあえず一段落したので実装を公開
Shockerさんとholeさんからガラス下の明るさがリファレンスと一致しないのは、BTDF評価時のScalling Factor辺りに原因があるのではないかという意見をもらった。
- Veach, Eric. Robust Monte Carlo methods for light transport simulation. Stanford University, 1998.
の5.2や
が参考になる。
放射輝度の補正は視点からトレーシングする時だけ行い、Photon Tracing時には放射輝度の補正を行ってはいけないことが書いてある。
この修正を入れたところリファレンスと一致するようになった。
Shading Normal(頂点法線をUV座標で補完して算出する法線)を使っている場合、反射方程式は以下の形になっている。
-
: 点N_s(x) でのShading Normalx
ここで元々の反射方程式のCosine項はGeometric Normal(三角形の面法線)が使われるべきである。例えばShading Normalは実際の物体形状と一致していないので投影面積は実際とずれてしまう。
反射方程式をGeometric Normalで書き直してあげると
-
: 点N_g(x) でのGeometric Normalx
Shading Normalを使った最初の式は本来これになるべきである。そこで2つの式を一致させるために
Shading Normalを使う時はこのBSDFを使うべきである。カメラ側からトレーシングする場合はこれを反射方程式に代入してあげると、単純にBSDFやコサイン項をShading Normalで評価するだけで良いので、特に既存の処理に変更を入れる必要はない。
一方でPhoton Tracing時にはBSDFの形が
これをGeometric Normalを使った反射方程式に代入すると
カメラ側からトレーシングする場合と異なり、
実際使わないとリファレンスと若干差異が出てしまう。以下の画像では左下の集光模様がリファレンスと比べてボヤけてしまっているのが分かる。
コサイン項の補正を入れてあげると以下のようにリファレンスに近づく。
参考資料
- Veach, Eric. Robust Monte Carlo methods for light transport simulation. Stanford University, 1998 の5.3
下の画像では完全鏡面の球の右下辺りがリファレンスと比べて若干明るくなっていることが判明。
これはLight leakによるものだということが分かった。
これを防ぐようにしたところ、明るさは減って大体リファレンスと一致した。
色々とハマりポイントがあるので、それっぽい見た目が出てもなかなか安心できない。信頼できるリファレンスと比較することの重要性を感じる。