11. レイトレーサーのアーキテクチャについて
Ray Tracing: The Rest of Your Life (v3.2.3): 11 Some Architectural Decisions / 3.11 レイトレーサーのアーキテクチャについて
前章でコーネルボックスのレンダリングは完成しましたが,コードをよく見ると 2 つの構造的な問題が残っています。どちらも「マテリアルが知っているべき情報をレンダラーが代わりに持っている」という根本的な責任の混在が原因です。この章では問題を整理し,次章の実装計画を立てます。
問題①:ray_color への PDF のハードコード
前章(r310-cornell-box)の ray_color の拡散散乱パスを見ると:
// Diffuse (Lambertian): mix light PDF and cosine PDF for importance sampling.
let lp = Box::new(HittablePdf::new(lights.clone(), rec.p));
let cp = Box::new(CosinePdf::new(rec.normal)); // ← マテリアルの知識
let mixed = MixturePdf::new(lp, cp);CosinePdf::new(rec.normal) の構築は,「Lambertian 材質はコサイン重み付きの PDF を使う」という知識に基づいています。しかしこれはLambertian 自身が持つべき知識であり,レンダラー(ray_color)に書くべきではありません。別の拡散材質(例えばより複雑な BRDF を持つもの)を追加するたびに ray_color を書き換える必要が生じます。
PDF の構築はマテリアルの scatter() の中で完結すべきです。
問題②:鏡面散乱と PDF の非互換
現在の設計では,scatter() が常に (attenuation, ray) を返し,scattering_pdf() が 0.0 を返すかどうかで鏡面 / 拡散を判定しています:
Some((attenuation, specular_ray)) => {
let sp = mat.scattering_pdf(r, &rec, &specular_ray);
if sp <= 0.0 {
// Specular: scatter 方向をそのまま使う
emitted + attenuation * ray_color(&specular_ray, ...)
} else {
// Diffuse: scatter 方向は捨てて MixturePdf で上書き
let cp = Box::new(CosinePdf::new(rec.normal));
...
}
}Lambertian の scatter() が生成した方向は拡散パスで無条件に捨てられ,MixturePdf::generate() の結果で上書きされます。さらに scattering_pdf() の返り値 0.0 を「鏡面フラグ」として流用しているため,本来の意味(PDF の値を返すメソッド)と役割が混在しています。
鏡面散乱と拡散散乱は根本的に異なるものであり,型レベルで区別されるべきです。
解決策:ScatterRecord
C++ 版では scatter_record 構造体を導入し,scatter() がこれを出力するよう変更します。Rust では enum を使ったより表現力の高い設計が取れます:
pub enum ScatterRecord {
/// Specular scatter: the direction is fully determined (Metal, Dielectric).
Specular { attenuation: Color, ray: Ray },
/// Diffuse scatter: a PDF object determines the direction (Lambertian).
Diffuse { attenuation: Color, pdf: Box<dyn Pdf> },
}enum の各バリアントが「鏡面か拡散か」を型として表現するため,ブール値フラグも scattering_pdf() 判定も不要になります。
Rust での設計
scatter() の既存シグネチャーは変更しません。既存の全クレートとの後方互換を維持するため,新メソッド scatter_record() を追加します:
pub trait Material: Send + Sync {
// 既存メソッド (変更なし)
fn scatter(&self, r_in: &Ray, rec: &HitRecord) -> Option<(Color, Ray)>;
// 新メソッド: デフォルト実装は scatter() 結果を Specular として包む
fn scatter_record(&self, r_in: &Ray, rec: &HitRecord) -> Option<ScatterRecord> {
self.scatter(r_in, rec).map(|(attenuation, ray)| {
ScatterRecord::Specular { attenuation, ray }
})
}
// ...
}Lambertian のみ scatter_record() をオーバーライドし,CosinePdf を内包した Diffuse バリアントを返します:
impl Material for Lambertian {
// scatter() は既存実装を維持
fn scatter_record(&self, _r_in: &Ray, rec: &HitRecord) -> Option<ScatterRecord> {
let attenuation = self.albedo.value(rec.u, rec.v, rec.p);
Some(ScatterRecord::Diffuse {
attenuation,
pdf: Box::new(CosinePdf::new(rec.normal)),
})
}
}Metal・Dielectric・Isotropic はデフォルト実装(旧 scatter() を Specular に包む)でそのまま動作します。
これにより,r312-cornell-box の ray_color から CosinePdf の構築が消えます:
// r310: ray_color が CosinePdf を知っていた
let cp = Box::new(CosinePdf::new(rec.normal)); // ← 削除される
// r312: マテリアルから PDF が渡ってくる
match mat.scatter_record(r, &rec) {
Some(ScatterRecord::Specular { attenuation, ray }) => {
emitted + attenuation * ray_color(&ray, ...)
}
Some(ScatterRecord::Diffuse { attenuation, pdf: mat_pdf }) => {
let lp = Box::new(HittablePdf::new(lights.clone(), rec.p));
let mixed = MixturePdf::new(lp, mat_pdf); // CosinePdf はここに来る
...
}
None => emitted,
}C++ と Rust の違い
C++ 版では scatter_record をフィールド経由の出力引数として設計し,scatter() のシグネチャーを変更します:
struct scatter_record {
ray specular_ray;
bool is_specular;
color attenuation;
shared_ptr<pdf> pdf_ptr;
};
// scatter() シグネチャーが変わる(破壊的変更)
virtual bool scatter(
const ray& r_in, const hit_record& rec, scatter_record& srec) const;全マテリアルの scatter() 実装を一斉に書き換えます。
Rust 版では以下の違いがあります:
| 項目 | C++ | Rust |
|---|---|---|
| 鏡面/拡散の区別 | is_specular ブール値 | enum バリアント(型安全) |
| PDF の保持 | shared_ptr<pdf> | Box<dyn Pdf> |
| 既存コードへの影響 | 破壊的変更(全マテリアル更新) | scatter_record() を追加(後方互換) |
| 既存クレートの対応 | 全クレート更新が必要 | 変更不要 |
Rust では enum のパターンマッチがコンパイル時に網羅性を保証するため,ブール値フラグが不要です。また scatter() を変えずに scatter_record() を追加することで,第1・第2編のデモクレートをすべてそのまま動かし続けられます。
まとめ
ray_colorにCosinePdfの構築がハードコードされていた問題を特定した。これはマテリアルの知識がレンダラーに漏れている設計上の問題である。- 鏡面散乱と拡散散乱を
scatter()の返り値では型レベルで区別できない問題を特定した。scattering_pdf()の返り値0.0を判定フラグに流用していた。 - Rust では
ScatterRecordenum(Specular/Diffuse2 バリアント)を導入し,新メソッドscatter_record()として追加する。既存のscatter()と全クレートは変更しない。 - 次章では
ScatterRecordをcommon/に実装し,アルミ箱とガラス球を組み合わせた新しいコーネルボックスシーンを構築する。