10. 混合密度
Ray Tracing: The Rest of Your Life (v3.2.3): 10 Mixture Densities / 3.10 混合密度
前章では光源を直接サンプリングする PDF
扱うのは 4 つのトピックです。
- 混合 PDF の数学:
Pdfトレイト設計:CosinePdf,HittablePdf,MixturePdfscattering_pdf()の役割:鏡面散乱と拡散散乱の分岐を担う- コーネルボックスへの統合:
r310-cornell-boxクレート
混合 PDF の考え方
2 つの PDF
サンプリングは確率
Lambertian 面の MC 推定器は:
ここで scattering_pdf() が返す値),
Pdf トレイトの設計
common/src/pdf.rs に 3 つの型を実装しました。
pub trait Pdf: Send + Sync {
fn value(&self, direction: Vec3) -> f64;
fn generate(&self) -> Vec3;
}CosinePdf は前章で導入した ONB(Onb)を使ってコサイン重み付き方向をサンプリングします:
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())
}
}HittablePdf は Hittable の pdf_value / random メソッドに処理を委譲します。XzRect に pdf_value と random を実装(common/src/aarect.rs)したことで,光源矩形を HittablePdf でラップするだけで光源 PDF が得られます:
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> を保持し,等重み混合を実装します:
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() の返り値で鏡面散乱と拡散散乱を分岐します:
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(デフォルト)を返すマテリアル(Metal,Dielectric)は鏡面散乱として scatter() の返した方向をそのまま使います。Lambertian は cosθ / π を返すため拡散散乱として混合 PDF を使うパスを辿ります。
lights には FlipFace でラップしていない素の XzRect を渡します。FlipFace は pdf_value / random を実装していないため PDF サンプリングに使えないからです。ワールドに追加する光源は引き続き FlipFace でラップし,DiffuseLight::emitted の片面発光を維持します。
C++ と Rust の違い
C++ 版 3.10 はコーネルボックスの ray_color に shared_ptr<hittable> の lights パラメータを追加し,pdf 抽象基底クラスから mixture_pdf を派生させます。散乱経路の分岐には scatter_record 構造体を導入し,スペキュラーレイの格納と PDF オブジェクトの所有権を一本化します:
struct scatter_record {
ray specular_ray;
bool is_specular;
color attenuation;
shared_ptr<pdf> pdf_ptr;
};scatter が scatter_record を出力し,is_specular フラグで鏡面 / 拡散を切り替えます。
Rust 版では scatter_record を導入せず,scattering_pdf() の返り値(0.0 なら鏡面,それ以外なら拡散)を分岐条件として使います。これにより scatter インタフェースを変更することなく拡散散乱への移行を実現しています。ただし scatter_record を使わないと Lambertian::scatter が生成した方向を捨てて混合 PDF で上書きするため,sp と sp2 で 2 回 scattering_pdf を呼ぶことになります(sp は鏡面判定用,sp2 は拡散パスの寄与計算用)。
pdf 抽象基底クラスは Rust では trait Pdf として表現され,仮想ディスパッチには Box<dyn Pdf> を使います:
// C++: shared_ptr を渡す
auto mixed = mixture_pdf(
make_shared<hittable_pdf>(lights, rec.p),
make_shared<cosine_pdf>(rec.normal)
);// 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
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 の等重み混合
を導入し,光源方向と半球全体の両方を効率よくカバーする重点サンプリングを実現した。 Pdfトレイト(value/generate)に基づくCosinePdf,HittablePdf,MixturePdfをcommon/に実装した。HittablePdfはXzRectのpdf_value/randomに処理を委譲する。Material::scattering_pdf()の返り値が0.0なら鏡面散乱(従来どおり),それ以外なら拡散散乱として混合 PDF を使う設計を採用し,scatterインタフェースを変更することなく段階的に拡散散乱を改善した。FlipFaceはpdf_value/randomを持たないため,lightsにはFlipFaceでラップしない素のXzRectを渡す。