ライトトレーシングが完全に一致した(拡散反射のみ)

2025-12-16

以前パストレーサーで直接光の計算をできるようにすることで光源が小さくてもそれなりに計算が早く行えるようになった。 ただ光源が覆われた間接照明のようなシーンではほぼ失敗して無効化されてしまうという課題がある。

そういう場合のために双方向パストレーシングという手法があるが、いきなり実装するのは難しいのでまずライトトレーシングにチャレンジしてみた。

ライトトレーサー

640x480, 1,000,000,000サンプル, 3分56秒, M1MacbookAir (8コア)

ライトトレーシングとは?

レイトレーシングやパストレーシング(Path Tracing、以降パストレ)ではカメラ(視点)からレイを飛ばす。 実際の物理挙動としては追跡方向が逆なんだけど、そうすることで必ず視界に入る影響のみを追跡するので効率的に計算できるという利点がある。

ライトトレーシング(Light Tracing、以降ライトレ)では実世界と同様に光源からレイを飛ばしてトレースして、カメラへの影響を計算する。 カメラに映らない領域を追跡してしまい無駄になる可能性が高いが、光源が覆われていて視点側からは偶然に到達するのが難しいようなシーンの場合に効果があることもあるだろう。

  • コースティクスの追跡が得意、という利点もある

カメラへの影響をどうやって加えるか?

「カメラに実体を持たせて、レイが交差判定でヒットしたら影響を考慮する」、というのが実世界に即したナイーブな計算方法だと思う。 ただそれだと起きる確率が低いから収束に時間がかかりそう。 双方向パストレへの準備も考えてパスを強制的に特定の方向につなげる場合の計算をさせたいので、 パストレでの直接光計算と似た感じでカメラに向けて直接レイを飛ばすようにする。

交点からカメラに向かってレイを飛ばして、遮られなければ第一段階突破。 そして仮想的なカメラの投影面(イメージセンサー)を考えて、レイがどのピクセル相当位置に当たるかを求めてそれが有効であれば影響を加える。

  • シーン内にカメラの実体はなく、レイとの直接の交差判定は行わない
  • カメラは理想化したピンホールカメラで、大きさのない一点で表す(レンズはなく、従って屈折とか焦点もなし)

パスをカメラ方向に結ぶ際に、直接光の計算と同じように交点の半球状での向きに対する確率密度を考慮する必要があるかと思ったが、それはなくていいっぽい?

  • 理由を考えるに、パストレの場合は一方向のみのトレースを(逆向きに)していて通常のトレースと別に光源方向を調べてしまうと二方向からの影響になってしまうのを調整する必要があるのに対し、 ライトレでは光源から発した光の経路をトレースしてるので分岐させても反射特性の係数を掛けて弱まっているので問題ない?

積分値の求め方

パストレでは各ピクセルの色を求めるために何回もレイを撃って平均を計算している。 これはモンテカルロ積分による数値積分を行っているということになる。 期待値=積分結果を求めるためにサンプル数で除算する。

ライトレではどうやるか、ピクセルごとのレイのヒット回数をカウントしてそれで平均するのかと思ったがどうやらそうではないらしい。 パストレではサンプリング処理がピクセルごとに行われるのに対して、ライトレでは光源からレイを撃つだけなのでピクセルごとにはならず、イメージセンサー全体への影響という期待値を計算していることになるっぽい。 なので各ピクセルすべてを、撃ったレイの数で除算する。

  • 直感的には割る数が大きくなりすぎるんじゃない?と思うんだけど、次の「カメラ内部の係数」で釣り合いが取れる
  • パストレではサンプル数がピクセルごと(Sample Per Pixel, spp)なのに対し、 ライトレでは光源から撃つ全体のレイの数、ということからも明らかか(わかってみれば)

カメラ内部の係数

パストレの直接光の計算では光源の発する放射輝度に対して距離の二乗で除算することで影響を求めていた。 ライトレも同じ感じかと思ったらどうやらそれだけではダメですごく苦労した。 どうやら確率密度の変数変換を行う必要があるらしい。

なぜかというとカメラの各センサーがどのくらい受光するかを求める必要があって、 でそれにはカメラに入射した方向ではなく投影面の二次元平面上での積分に変換する必要があるとかなんとか (隠されたプライマリレイのpdfを考慮する必要があるらしい→付録)。

  • そういう説明はなかなか解説されてないのとパストレでは存在しないので、そんな必要があるということ自体を認識するのに非常に苦労した…

反射面ごとの係数

光源のトレースをカメラにつなく際には、光の入射方向とカメラへの方向から材質の反射特性(BRDF)を計算して掛けてやればよいはず? (でもうまくいかず。拡散反射ではいいが鏡面反射や透過材質ではなんかおかしい…)

拡散反射

ランバート反射のBRDFは入射・出射方向に関わらずあらゆる方向に対して\(1 / \pi\)となる。 その計算方法でパストレの計算結果とたぶん完全に一致した(目測)。

鏡面反射

smallptでは鏡面反射や屈折材質が完全に滑らかで、反射によるブレがない像が作られる。 ライトレでカメラに接続する場合には完全に滑らかだとその方向になる可能性が全くないので、影響を与えられず真っ黒になってしまう。 なのでブレを含む材質にする必要がある。

ちょっと手抜きでBlinn-Phong:ハーフベクトルのべき乗を使ってみたが、どうにも計算が合わなかった。 拡散反射では必要なかった、法線とカメラベクトルの内積値と、法線とライトベクトルの内積値で除算する必要があり? またそれでもshininessを下げて粗い表面にすると球の縁が明るくなってしまう。

行き詰まったので代わりにPBRマテリアルでのメタリックに変更してみたところ、それなりにそれっぽい結果になった。 ただ遮蔽項Gはカメラ(出射)方向とライト(入射)方向の両方向で計算すると滑らかな場合に暗くなりすぎ、カメラ方向だけにすると粗い場合におかしくなる。 まったく適当だがブレンドさせるようにしてみた。

透過材質

透過材質の場合に扱われるフレネル反射や全反射は鏡面反射と同じ処理を行うとして、屈折の場合を計算したいがこれもよくわからなかった。 透過材質の場合のBRDF(BSDF)で簡易的に使えるモデル?がよくわからなかった。 Blinn-Phongでブラそうにも反射じゃないからハーフベクトルが求められない。 適当に屈折後のベクトルの法線方向以外を反転させた方向から入射したものが反射した、と考えてみることにした。

BRDF(BSDF)を掛ける必要があると思うが Blinn-PhongのBRDFといわれるものを使ってもうまくいかず。 なぜか修正PhongのBRDFを用いたところ、ちょっと暗いがそれほど不自然にならなくなったので、それにしてみた(さらに結果を適当に/2している)。 この辺まったく理論的に則ってない…。

ソース

例によってsmallptを参考に、ライトトレーシング方式に変更:gist

  • レイの追跡:main関数から呼び出すshot_light関数内でループ処理
  • connect_to_cameraで交点からカメラに接続し、スクリーン(センサー)に蓄積
  • 光源からのレイの飛ばし方:光源の球体表面から半球状にコサイン分布
    • 開始点からもカメラに接続して、光源自体が明るく見えるようにする
  • 鏡面反射や透過材質はレイのトレース時は完全に滑らかな反射・屈折で、カメラに接続する際だけブレを入れている
  • smallptの変更が必要な項目:
    • カメラが実際には壁の奥にいてプライマリレイを飛ばす際に補正しているという問題があるので、Frntの壁の+170+296にする
    • 光源が天井を貫く巨大な球になっていて都合が悪いので、smallptのexplit版と同様に室内に収まる小さな球に変更する
    • Vecクラスにlengthメソッドを追加、normメソッドを副作用なしに、crossメソッドをconst
  • OpenMPでの並列化:
    • 当初出力画像用のバッファ1画面分だけで各スレッドで結果を書き込む部分に#pragma omp criticalを指定して競合を回避させたが、それだとシングルスレッドより遅かった。スレッドごとにバッファを分けて最後に合成することでマルチスレッドの恩恵を受けられた
    • #pragma omp parallelはループ1回ごとにタスクキューになって空いたスレッドから順に取り出して処理するものかと想像していたが、どうも単に回数をスレッド数で分割して処理するっぽい?

雑感

  • ライトレの実際の実装や解説がなかなかなくて(あっても理解できなくて)苦労した…
  • カメラへの接続はBRDFを掛けるんじゃないのか?計算方法が結局完全には理解できず…ここは未解決の課題として残った
  • パストレで光源を見にいくことをNext Event Estimation(NEE)と呼んだが、ライトレでカメラを見にいくこともNEEというらしい
  • しかしこう、ライトレはパストレと同じ問題(レンダリング方程式)に対する逆のアプローチなので当然とはいえ、ちゃんと計算結果が同じ結果に収束する(であろう)ことが確認できたのには感心した!

リンク

  • smallpt 毎度ベースのパストレーサー
  • edubptソース解説あり
    • ライトトレーシングの実装もあるが双方向パストレの一部としての実装なので、理解が追いついてない
      • カメラを実体として扱っていて、NEEはしてない?
      • 薄レンズも組み込んであり、さらに理解が追いつかない
  • The Missing Primary Ray PDF in Path Tracing · A Graphics Guy’s Note パストレーサーではインポータンス関数の係数でプライマリレイのpdfが隠されているが、ライトトレーサーではそうはいかないという解説
  • PBRT book: 16.1.1 Sampling Cameras 対応する箇所はここか?これに当たるしかないのだが理解できない
  • スライドpdf: Bidirectional path tracing, Saarland University
    • “Jacobian for a pinhole camera”, “Integral formulation for a light tracer”の説明スライドあり

BRDF

付録

カメラ内部の確率密度

The Missing Primary Ray PDF in Path Tracing · A Graphics Guy’s NoteのPrimary Ray PDF節より:

出力する画像の縦幅を\(h\)、縦の視野角を\(fov\)とすると、仮想的な投影面への距離\(d\)は、

$$ \begin{align*} d : \frac{h}{2} &= \cos\left\lparen\frac{fov}{2}\right\rparen : \sin\left\lparen\frac{fov}{2}\right\rparen \\ d &= \frac{h}{2 \tan\left\lparen\frac{fov}{2}\right\rparen} \end{align*} $$

任意のピクセル内の位置とカメラの視線方向との角度を\(\theta\)とすると、斜辺の距離\(r\)は

$$ r = \frac{d}{\cos \theta} $$

各ピクセルの面積は1なのでピクセル上のサンプリング点の面積も1、なのでサンプリングの立体角のpdfは

$$ \begin{align*} pdf_w &= \frac{r^2}{\cos \theta} \\ &= \frac{d^2}{\cos^3 \theta} \\ &= \left\lbrace \frac{h}{2 tan\left\lparen\frac{fov}{2}\right\rparen} \right\rbrace^2 \frac{1}{\cos^3 \theta} \end{align*} $$

  • 前半の項は画像サイズと視野角が決まれば固定なので、あらかじめ(または後で)処理できる
  • 参照サイトによれば最終的にはcosの3乗じゃなくて4乗らしいがもう1つはなんの分かわからず…