Skip to content
Copied!
published on 2026-09-17

11. レイトレーサーのアーキテクチャについて

前章でコーネルボックスのレンダリングは完成しましたが,コードをよく見ると 2 つの構造的な問題が残っています。どちらも「マテリアルが知っているべき情報をレンダラーが代わりに持っている」という根本的な責任の混在が原因です。この章では問題を整理し,次章の実装計画を立てます。

問題①:ray_color への PDF のハードコード

前章(r310-cornell-box)の ray_color の拡散散乱パスを見ると:

r310-cornell-box/src/lib.rs
rust
// 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 を返すかどうかで鏡面 / 拡散を判定しています:

r310-cornell-box/src/lib.rs
rust
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 を使ったより表現力の高い設計が取れます:

common/src/material.rs
rust
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() を追加します:

common/src/material.rs
rust
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 バリアントを返します:

common/src/material.rs
rust
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)),
        })
    }
}

MetalDielectricIsotropic はデフォルト実装(旧 scatter()Specular に包む)でそのまま動作します。

これにより,r312-cornell-boxray_color から CosinePdf の構築が消えます:

rust
// 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() のシグネチャーを変更します:

cpp
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_colorCosinePdf の構築がハードコードされていた問題を特定した。これはマテリアルの知識がレンダラーに漏れている設計上の問題である。
  • 鏡面散乱と拡散散乱を scatter() の返り値では型レベルで区別できない問題を特定した。scattering_pdf() の返り値 0.0 を判定フラグに流用していた。
  • Rust では ScatterRecord enum(Specular / Diffuse 2 バリアント)を導入し,新メソッド scatter_record() として追加する。既存の scatter() と全クレートは変更しない。
  • 次章では ScatterRecordcommon/ に実装し,アルミ箱とガラス球を組み合わせた新しいコーネルボックスシーンを構築する。