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

10. 混合密度

Ray Tracing: The Rest of Your Life (v3.2.3): 10 Mixture Densities / 3.10 混合密度

前章では光源を直接サンプリングする PDF plight を導入しました。しかし光源 PDF だけに頼ると,光源方向以外からの間接照明が支配的な箇所では逆に分散が増すことがあります。コサイン重み付き PDF pcosine との 混合 PDF を使えば,どちらの極端な場合でも安定した収束が得られます。

扱うのは 4 つのトピックです。

  • 混合 PDF の数学:pmix(d)=12p0(d)+12p1(d)
  • Pdf トレイト設計:CosinePdf, HittablePdf, MixturePdf
  • scattering_pdf() の役割:鏡面散乱と拡散散乱の分岐を担う
  • コーネルボックスへの統合:r310-cornell-box クレート

混合 PDF の考え方

2 つの PDF p0p1 の混合 PDF を等重み(50:50)で定義します:

pmix(d)=12p0(d)+12p1(d)

サンプリングは確率 12p0 から,残り 12p1 から方向 d を生成し,得られた方向の確率密度を pmix(d) で評価します。どちらから生成したかに関わらず,評価には必ず混合 PDF の値を使う点が重要です。

Lambertian 面の MC 推定器は:

Loρcosθ/πpmix(d)Li(d)

ここで ρ は albedo,cosθ/π は Lambertian BRDF に cosθ を乗じた値(コードでは scattering_pdf() が返す値),pmix(d) は混合 PDF の評価値です。

Pdf トレイトの設計

common/src/pdf.rs に 3 つの型を実装しました。

common/src/pdf.rs
rust
pub trait Pdf: Send + Sync {
    fn value(&self, direction: Vec3) -> f64;
    fn generate(&self) -> Vec3;
}

CosinePdf は前章で導入した ONB(Onb)を使ってコサイン重み付き方向をサンプリングします:

common/src/pdf.rs
rust
pub struct CosinePdf { uvw: Onb }

impl Pdf for CosinePdf {
    fn value(&self, direction: Vec3) -> f64 {
        let cosine = dot(unit_vector(direction), self.uvw.w);
        if cosine <= 0.0 { 0.0 } else { cosine / PI }
    }
    fn generate(&self) -> Vec3 {
        self.uvw.local_vec(random_cosine_direction())
    }
}

HittablePdfHittablepdf_value / random メソッドに処理を委譲します。XzRectpdf_valuerandom を実装(common/src/aarect.rs)したことで,光源矩形を HittablePdf でラップするだけで光源 PDF が得られます:

common/src/pdf.rs
rust
pub struct HittablePdf {
    pub obj: Arc<dyn Hittable>,
    pub origin: Point3,
}

impl Pdf for HittablePdf {
    fn value(&self, direction: Vec3) -> f64 {
        self.obj.pdf_value(self.origin, direction)
    }
    fn generate(&self) -> Vec3 {
        self.obj.random(self.origin)
    }
}

MixturePdf は 2 つの Box<dyn Pdf> を保持し,等重み混合を実装します:

common/src/pdf.rs
rust
pub struct MixturePdf { p: [Box<dyn Pdf>; 2] }

impl Pdf for MixturePdf {
    fn value(&self, direction: Vec3) -> f64 {
        0.5 * self.p[0].value(direction) + 0.5 * self.p[1].value(direction)
    }
    fn generate(&self) -> Vec3 {
        if random_double() < 0.5 { self.p[0].generate() } else { self.p[1].generate() }
    }
}

ray_color の修正

前章(r309-cornell-box)の ray_color は光源方向を直接ハードコードしていました。本章では lights パラメータを Arc<dyn Hittable> として受け取り,scattering_pdf() の返り値で鏡面散乱と拡散散乱を分岐します:

r310-cornell-box/src/lib.rs
rust
fn ray_color(
    r: &Ray, background: Color,
    world: &dyn Hittable,
    lights: Arc<dyn Hittable>,
    depth: i32,
) -> Color {
    // ...
    let sp = mat.scattering_pdf(r, &rec, &specular_ray);
    if sp <= 0.0 {
        // Specular (Metal, Dielectric): use the scatter direction directly.
        emitted + attenuation * ray_color(&specular_ray, background, world, lights, depth - 1)
    } else {
        // 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);
        let direction = mixed.generate();
        let scattered = Ray::with_time(rec.p, direction, r.time());
        let pdf_val = mixed.value(direction);
        if pdf_val < 1e-10 {
            return emitted;
        }
        let sp2 = mat.scattering_pdf(r, &rec, &scattered);
        emitted + attenuation * sp2 / pdf_val
            * ray_color(&scattered, background, world, lights, depth - 1)
    }
}

scattering_pdf()0.0(デフォルト)を返すマテリアル(MetalDielectric)は鏡面散乱として scatter() の返した方向をそのまま使います。Lambertiancosθ / π を返すため拡散散乱として混合 PDF を使うパスを辿ります。

lights には FlipFace でラップしていない素の XzRect を渡します。FlipFacepdf_value / random を実装していないため PDF サンプリングに使えないからです。ワールドに追加する光源は引き続き FlipFace でラップし,DiffuseLight::emitted の片面発光を維持します。

C++ と Rust の違い

C++ 版 3.10 はコーネルボックスの ray_colorshared_ptr<hittable>lights パラメータを追加し,pdf 抽象基底クラスから mixture_pdf を派生させます。散乱経路の分岐には scatter_record 構造体を導入し,スペキュラーレイの格納と PDF オブジェクトの所有権を一本化します:

cpp
struct scatter_record {
    ray specular_ray;
    bool is_specular;
    color attenuation;
    shared_ptr<pdf> pdf_ptr;
};

scatterscatter_record を出力し,is_specular フラグで鏡面 / 拡散を切り替えます。

Rust 版では scatter_record を導入せず,scattering_pdf() の返り値(0.0 なら鏡面,それ以外なら拡散)を分岐条件として使います。これにより scatter インタフェースを変更することなく拡散散乱への移行を実現しています。ただし scatter_record を使わないと Lambertian::scatter が生成した方向を捨てて混合 PDF で上書きするため,spsp2 で 2 回 scattering_pdf を呼ぶことになります(sp は鏡面判定用,sp2 は拡散パスの寄与計算用)。

pdf 抽象基底クラスは Rust では trait Pdf として表現され,仮想ディスパッチには Box<dyn Pdf> を使います:

cpp
// C++: shared_ptr を渡す
auto mixed = mixture_pdf(
    make_shared<hittable_pdf>(lights, rec.p),
    make_shared<cosine_pdf>(rec.normal)
);
rust
// Rust: Box<dyn Pdf> を渡す
let mixed = MixturePdf::new(
    Box::new(HittablePdf::new(lights.clone(), rec.p)),
    Box::new(CosinePdf::new(rec.normal)),
);

光源オブジェクトの受け渡しは C++ が shared_ptr<hittable>,Rust が Arc<dyn Hittable> と対応しています。

r310-cornell-box/src/lib.rs
rust
use std::sync::Arc;
use common::{
    Camera, CosinePdf, Color, DiffuseLight, FlipFace, Hittable, HittablePdf, HittableList,
    Lambertian, MixturePdf, Pdf, Point3, Ray, RectBox, RotateY, Translate, Vec3,
    XyRect, XzRect, YzRect, random_double, write_color_gamma,
};

fn ray_color(r: &Ray, background: Color, world: &dyn Hittable, lights: Arc<dyn Hittable>, depth: i32) -> Color {
    if depth <= 0 {
        return Color::new(0.0, 0.0, 0.0);
    }
    let rec = match world.hit(r, 0.001, f64::INFINITY) {
        Some(rec) => rec,
        None => return background,
    };
    let mat = match &rec.mat {
        Some(mat) => mat.clone(),
        None => return Color::new(0.0, 0.0, 0.0),
    };
    let emitted = mat.emitted(rec.u, rec.v, rec.p, rec.front_face);
    match mat.scatter(r, &rec) {
        None => emitted,
        Some((attenuation, specular_ray)) => {
            let sp = mat.scattering_pdf(r, &rec, &specular_ray);
            if sp <= 0.0 {
                // Specular (Metal, Dielectric): use the scatter direction directly.
                emitted + attenuation * ray_color(&specular_ray, background, world, lights, depth - 1)
            } else {
                // 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);
                let direction = mixed.generate();
                let scattered = Ray::with_time(rec.p, direction, r.time());
                let pdf_val = mixed.value(direction);
                if pdf_val < 1e-10 {
                    return emitted;
                }
                let sp2 = mat.scattering_pdf(r, &rec, &scattered);
                emitted + attenuation * sp2 / pdf_val
                    * ray_color(&scattered, background, world, lights, depth - 1)
            }
        }
    }
}

/// Cornell box with two rotated blocks and a ceiling light.
///
/// Returns `(world, lights)` where `lights` is the bare `XzRect` light (without
/// `FlipFace`) so that `HittablePdf` can call `pdf_value` / `random` on it.
/// The world light is still wrapped in `FlipFace` for correct one-sided emission.
fn cornell_box() -> (HittableList, Arc<dyn Hittable>) {
    let mut objects = HittableList::new();

    let red   = Arc::new(Lambertian::new(Color::new(0.65, 0.05, 0.05)));
    let white = Arc::new(Lambertian::new(Color::new(0.73, 0.73, 0.73)));
    let green = Arc::new(Lambertian::new(Color::new(0.12, 0.45, 0.15)));
    let light = Arc::new(DiffuseLight::new(Color::new(15.0, 15.0, 15.0)));

    objects.add(Box::new(YzRect::new(0.0, 555.0, 0.0, 555.0, 555.0, green)));
    objects.add(Box::new(YzRect::new(0.0, 555.0, 0.0, 555.0, 0.0, red)));

    // Bare XzRect used by HittablePdf for geometric sampling.
    let light_rect: Arc<dyn Hittable> =
        Arc::new(XzRect::new(213.0, 343.0, 227.0, 332.0, 554.0, light.clone()));
    // FlipFace version in the world so DiffuseLight emits only downward.
    objects.add(Box::new(FlipFace::new(Box::new(
        XzRect::new(213.0, 343.0, 227.0, 332.0, 554.0, light),
    ))));

    objects.add(Box::new(XzRect::new(0.0, 555.0, 0.0, 555.0, 0.0, white.clone())));
    objects.add(Box::new(XzRect::new(0.0, 555.0, 0.0, 555.0, 555.0, white.clone())));
    objects.add(Box::new(XyRect::new(0.0, 555.0, 0.0, 555.0, 555.0, white.clone())));

    let box1: Box<dyn Hittable> = Box::new(RectBox::new(
        Point3::new(0.0, 0.0, 0.0),
        Point3::new(165.0, 330.0, 165.0),
        white.clone(),
    ));
    let box1: Box<dyn Hittable> = Box::new(RotateY::new(box1, 15.0));
    let box1: Box<dyn Hittable> = Box::new(Translate::new(box1, Vec3::new(265.0, 0.0, 295.0)));
    objects.add(box1);

    let box2: Box<dyn Hittable> = Box::new(RectBox::new(
        Point3::new(0.0, 0.0, 0.0),
        Point3::new(165.0, 165.0, 165.0),
        white,
    ));
    let box2: Box<dyn Hittable> = Box::new(RotateY::new(box2, -18.0));
    let box2: Box<dyn Hittable> = Box::new(Translate::new(box2, Vec3::new(130.0, 0.0, 65.0)));
    objects.add(box2);

    (objects, light_rect)
}

pub fn render_image() -> String {
    let aspect_ratio = 1.0_f64;
    let image_width = 300_i32;
    let image_height = (image_width as f64 / aspect_ratio) as i32;
    let samples_per_pixel = 200_i32;
    let max_depth = 50_i32;

    let (world, lights) = cornell_box();
    let background = Color::new(0.0, 0.0, 0.0);

    let lookfrom = Point3::new(278.0, 278.0, -800.0);
    let lookat   = Point3::new(278.0, 278.0, 0.0);
    let vup      = Point3::new(0.0, 1.0, 0.0);
    let dist_to_focus = 10.0_f64;
    let aperture      = 0.0_f64;

    let camera = Camera::new_with_shutter(
        lookfrom, lookat, vup, 40.0, aspect_ratio, aperture, dist_to_focus, 0.0, 1.0,
    );

    let mut output = String::new();
    output.push_str("P3\n");
    output.push_str(&format!("{} {}\n", image_width, image_height));
    output.push_str("255\n");

    for j in (0..image_height).rev() {
        for i in 0..image_width {
            let mut pixel_color = Color::new(0.0, 0.0, 0.0);
            for _ in 0..samples_per_pixel {
                let u = (i as f64 + random_double()) / (image_width - 1) as f64;
                let v = (j as f64 + random_double()) / (image_height - 1) as f64;
                let r = camera.get_ray(u, v);
                pixel_color += ray_color(&r, background, &world, lights.clone(), max_depth);
            }
            output.push_str(&format!(
                "{}\n",
                write_color_gamma(pixel_color, samples_per_pixel)
            ));
        }
    }

    output
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn render_produces_valid_ppm() {
        let output = render_image();
        let mut lines = output.lines();
        assert_eq!(lines.next(), Some("P3"));
        let dims = lines.next().expect("missing dimensions line");
        let parts: Vec<&str> = dims.split_whitespace().collect();
        assert_eq!(parts.len(), 2);
        let w: i32 = parts[0].parse().expect("bad width");
        let h: i32 = parts[1].parse().expect("bad height");
        assert!(w > 0 && h > 0);
        assert_eq!(lines.next(), Some("255"));
    }
}

まとめ

  • 2 つの PDF の等重み混合 pmix(d)=12p0(d)+12p1(d) を導入し,光源方向と半球全体の両方を効率よくカバーする重点サンプリングを実現した。
  • Pdf トレイト(value / generate)に基づく CosinePdfHittablePdfMixturePdfcommon/ に実装した。HittablePdfXzRectpdf_value / random に処理を委譲する。
  • Material::scattering_pdf() の返り値が 0.0 なら鏡面散乱(従来どおり),それ以外なら拡散散乱として混合 PDF を使う設計を採用し,scatter インタフェースを変更することなく段階的に拡散散乱を改善した。
  • FlipFacepdf_value / random を持たないため,lights には FlipFace でラップしない素の XzRect を渡す。