ナイーブなパストレーサーは反射時にランダムな方向にトレースするため、たまたま光源に辿り着かない限り明かりに照らされず、ノイズを減らすにはかなりサンプル数を上げる必要がある。 これを明示的に光源計算することで改善できる。
smallpt_explicitを見てみる
smallptという簡潔でソースが公開されているパストレーサーがある。 同ページにその改善としてexplicit.cppで明示的に光源をサンプルするコードが公開されているので、どうやっているのか見てみた。 明示的な光源計算の計算部分:
Vec radiance(const Ray &r, int depth, unsigned short *Xi,int E=1){ |
- smallptで扱える形状は球のみで、光源も球という前提
- 交点
x
から光源を見ると必ず円形になるが、その方向をランダムに選ぶ - レイを飛ばして(シャドウレイ)遮るものがなければ光源の影響を加える
- 光源の影響は、
f * M_1_PI
がBRDF(拡散反射面なので)、l.dot(nl)
が法線との向きによる影響度、omega
が光源が占める立体角
合計した直接光の影響e
を加え、radiance
の再帰呼び出しで間接光を計算する。
影響の重複の除外法
radiance
の再帰呼び出しでは元々の動作と同様にランダム方向にトレースを続ける。
光源の影響は明示的に付加しているので、ランダムに選んだ方向が再度光源方向を調べてしまうと重複してしまう。
どうやって除外してるかというと、radiance
の最後の引数E
に0
を渡して自己発光obj.e
と乗算することで直後の光源の影響が重複しないようにしている。
光源を球以外の形状にしたい場合(矩形)
上記では光源の形状が球という前提が必要になる。 これをもっと違う形状にしたい場合にどうするか。
Rustではじめるレイトレーシング入門.pdfという解説書の第4章「モンテカルロレイトレーシング」に書かれていた。 この解説書はRay Tracing in One Weekendを元にしているらしく、 特に第4章はRay Tracing: The Rest of Your Lifeをなぞっている。
どちらもちゃんと説明が書かれているのだが、モンテカルロ積分とか数学にあまりなじんでないこともありそのままでは理解が難しかった。 自分なりの解釈を書き出してみる。
重点的サンプリング (Importance Sampling)
モンテカルロ積分で区間内を一様にサンプルするのではなく、確率密度に基づいてサンプルしてやることでノイズ(分散)が減らせる。 ていうことでこれまでもパストレの物体表面での反射時に使用していて、「影響が大きい法線方向にサンプルを増やして細かく調べて、平行方向は影響小さいのでサンプルを減らす」という理解だった。
ただそれは反射面特性としてランバートを仮定していて入射した光が所定の方向にどのくらい反射するかという特性が、面への入射角で光の影響が減衰するということと(たまたま?)打ち消しあうから、ということでしか使ったことなかったからかもしれない。
実際には確率密度関数pdf
で除算する、なので値が大きい=サンプルとして選ばれる確率密度が高い候補は個々の結果としては影響が下げられ、値が小さい=選ばれる確率密度が低い候補は影響が上げられることになる。
どうも今まで逆の感覚だったがよく考えると実は道理で、パストレーサーでいえばナイーブに一様にサンプルする場合にたまたま光源にヒットする確率はとても小さいわけだけど、それを高頻度に取り上げるよう意図的に光源方向にサンプル方向を選んで影響を確実に考慮するのと引き換えに、頻度を持ち上げた分係数を下げてやることで辻褄を合わせることになる。
その割合がpdf
で除算するってんだからうまくできてるもんだねぇ。
光源の直接サンプリング (Sampling Lights Directly)
smallpt_explicitでは光源は球という前提があったが、両著では矩形で扱っている。 「矩形が投影された立体角を計算してそこからサンプル」というのは難しそうなのでどうするかと思ったが、サンプルする点を微小面と考えて計算する。 光源上のランダムな点を一様に選び、その微小面がどのくらいの立体角に相当するかということから確率密度を計算する。
具体的には
となる (:サンプル方向(=)、:散乱の元の点、:光源上のサンプル点、:サンプル方向と光源面法線の角度、:光源の面積)。
- これがpdf・積分すると1.0になるというのは、領域の面積を打ち消す、そして距離と向きの補正のヤコビアン、ということらしい(付録で数値的に確認)
合成PDF (Mixture Densities)
smallpt_explicitでは直接光計算+再帰トレースという具合に計算しているが、重複の除外が必要でちょっと扱いにくいのではないかと思う。 のでpdfを合成する方法を使用する。
pdfは区間の積分結果が1.0になるので、複数のpdfを重み付け和で合成すればそれもまたpdfになる。 パストレーサーの例でいえば、ある確率qで光源サンプリング、残り1-qで反射面での反射特性でのサンプリングという具合に合成して、トレース方向を選択させることができる。
選んだサンプル方向に対して合成したpdfのそれぞれにおいて、逆にそのサンプル点が選ばれる確率密度を計算してやる。 そしてそれらを混ぜ合わせた線形の比率で再度合計する。 例えばサンプルした方向が光源に向かわない場合には、光源サンプリング側のpdfで選ばれた確率密度は0となる。 逆に光源に向かう方向の場合には、光源サンプリングと表面反射サンプリングのどちらからも選ばれた可能性があり、確率密度は線形合成されて高くなる。
smallptに組み込んでみる
両著とも上記のほかにPDFをクラス化したりマテリアル側に散乱のpdfをメソッド化したりしてとてつもなくうまく構造化する。 が初学者の自分にとってはあちこち処理を追ったり計算の値を確認するのが混乱してしまい、動作を理解するには難しかった。 そこでsmallpt(explicitじゃない元の)に、ベタに組み込んでみることにした。
struct Rectangle { |
- 矩形の光源を表現できるよう
Rectangle
クラスを追加し、light
グローバル変数に保持 - シーンに存在する球配列
spheres
とは別に交差判定を呼び出す - 拡散反射面だったら合成pdfでトレース方向を選択
- 確率密度関数から寄与度を算出
他にコードの変更が必要な箇所:
radiance
関数の残りのコードからobj.
を適宜削除spheres
の最後の、元のデカい光源球をコメントアウトVec
クラス:length
メソッド追加、外積%
にconst
付加- 任意:サンプリングの違いをわかりやすくするために、2x2のサブピクセルを無効にする
出力画像比較


左:元のsmallpt方式(cos重点的サンプリング)、右:光源サンプリングとcos重点サンプリングの半々 それぞれ1000spp(サンプル/ピクセル)
- 床など全体的なノイズは減っているが、明るいドットが目立ってしまう(ファイアフライノイズ)
- 半々じゃなく間接光をサンプルする割合を増やせば、ドットを減らせる
- より光源が小さければさらに効果は高い
- 同じsppだけど実行時間は元のsmallptのほうが1.8倍ほど時間がかかった (光源のアルベドが0.0なのでロシアンルーレットで打ち切られる確率が高まるからっぽい)
考察・あれこれ
- 「光源が重要」、といっても合成のウェイトをむやみに高くすればいいわけではない
- 直接光の影響は数回のサンプリングで収束してしまうので、そんなに高頻度でサンプルしたところで内容は深まらない
- 逆に広い範囲から影響のある間接光のサンプル数が減ってしまい、ピクセルあたりのサンプル数を増やしても無駄撃ちになってしまう
- マルチインポータンスサンプリング・バランスヒューリスティックでウェイトを調整?
- 光源と逆向きや隠れてる面などでは光源のサンプリングは無駄になってしまうので、シャドウレイ自体は使って有効な場合のみにした方がよいかも?
- 光源をサンプリングすることを”Next Event Estimation”というらしい、 それは意味がわかりづらくない?
- 実際のところ太陽には使えるが、電球とか窓ガラスとか屋内や人工物のシーンでは事実上必ず光源の直接サンプリングには失敗するのでそのままでは使い所が難しいかも
- ライトトレーシング、双方向パストレ?
Rectangle
の交差判定で距離t
がnan
になり得て(0.0 * inf
がnan
になる)、アーリーイグジットしてたらドツボにハマった、浮動小数点数でnan
の可能性のある場合は条件を反転すると意味が変わる可能性がある!- 変数名:rayt_rustでは
pdf_value / spdf_value
となっているがRest ofではscattering_pdf / pdf_value
となっていて混乱する- rayt_rustの
pdf_value
もマテリアルのscattering_pdf()
で計算してるしscatteringなんだろう、ということでRest ofの方に合わせてみた
- rayt_rustの
- モンテカルロ積分やら重点的サンプリングやら、考えた人も組み合わせた人も天才すぎるやろ…
- 6.2. Using a Uniform PDF Instead of a Perfect Matchで「pdfだけいじって反射方向のサンプリング方法はそのままでも同じ結果に収束する(6.3.)」ってホントに? これがサンプルを選ぶ確率とマッチしてる必要があるんじゃないの?
参考
- smallpt explicit
- Rustではじめるレイトレーシング入門.pdf
- リポジトリ:mebiusbox/rayt_rust
- Ray Tracing: The Rest of Your Life
他リンク
- パストレーシング - Computer Graphics - memoRANDOM Next Event Estimationの記述あり
- 合成PDFじゃなくて直接サンプリングの寄与を加えるっぽい?
- 光源の影響の重複をどう除外しているのかはわからなかった…
- NEEとMISの実装 (NEE編) - MochiMochi3D
- ランダム方向で光源に達した場合には寄与を与えないことで重複を除外
付録
smallpt-explitの不具合
explicitでは素のsmallptと違って光源が小さく中空に浮かぶようになっている。 これは直接光の計算するにはそうする必要があるんだろうかと疑問だったが、これだと天井も明るくなってしまいあまり見た目がよろしくないので元のsmallptと同じシーン配置にしたかった。
しかし結果がおかしく、ほぼ真っ暗になってしまう。 ちゃんとシャドウレイで光源から届くかどうかチェックしてるから動きそうなもんだけど、と原因を調べた:
- 面が裏向きでも直接光の計算が加えられてしまっている
- 光源サンプリング用の直交基底が正規化されてないため偏りが生じる
上記を修正したところ、元のシーンでも直接光を計算した場合にもナイーブなパストレと同じ結果になった (光源サンプリングの大半は失敗するので効果は薄いが)。 ソースの差分:
|
直接光のサンプル確率がpdfになっていることの確認
光源をサンプルする際のpdfというのが実際に積分すると1.0になっているのか理解できなかったので、モンテカルロ積分で確認してみた。
|