12. PDF の管理方法の整理
Ray Tracing: The Rest of Your Life (v3.2.3): 12 Cleaning Up PDF Management / 3.12 PDF の管理方法の整理
前章で特定した 2 つの問題,「CosinePdf のハードコード」と「鏡面/拡散の非互換な分岐」を,この章で解消します。ScatterRecord enum と scatter_record() メソッドを common に追加し,シーンをアルミ箱+白い小箱 → ガラス球へと段階的に変化させながら,計 4 パターンのコーネルボックスを実装します。
ScatterRecord enum
common/src/material.rs に以下の 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> },
}Specular バリアントは散乱方向が確定している場合(Metal・Dielectric),Diffuse バリアントは PDF オブジェクトで方向を決定する場合(Lambertian)です。
Material トレイトへの追加
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 }
})
}
fn emitted(&self, _u: f64, _v: f64, _p: Point3, _front_face: bool) -> Color {
Color::new(0.0, 0.0, 0.0)
}
fn scattering_pdf(&self, _r_in: &Ray, _rec: &HitRecord, _scattered: &Ray) -> f64 {
0.0
}
}デフォルト実装は既存の scatter() を Specular として包むため,Metal・Dielectric・Isotropic はこの新メソッドを意識せず引き続き動作します。
Lambertian::scatter_record() のオーバーライド
Lambertian のみオーバーライドし,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)),
})
}
}これにより,ray_color は CosinePdf を直接知る必要がなくなります。
ray_color の更新
ray_color では scatter_record() にマッチします:
let emitted = mat.emitted(rec.u, rec.v, rec.p, rec.front_face);
match mat.scatter_record(r, &rec) {
None => emitted,
Some(ScatterRecord::Specular { attenuation, ray }) => {
emitted + attenuation * ray_color(&ray, background, world, lights, depth - 1)
}
Some(ScatterRecord::Diffuse { attenuation, pdf: mat_pdf }) => {
let lp = Box::new(HittablePdf::new(lights.clone(), rec.p));
let mixed = MixturePdf::new(lp, mat_pdf);
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 sp = mat.scattering_pdf(r, &rec, &scattered);
emitted + attenuation * sp / pdf_val
* ray_color(&scattered, background, world, lights, depth - 1)
}
}CosinePdf はもはや ray_color の import リストには現れません:
use common::{
Camera, Color, DiffuseLight, FlipFace, Hittable, HittablePdf, HittableList,
Lambertian, Metal, MixturePdf, Pdf, Point3, Ray, RectBox, RotateY, ScatterRecord,
Translate, Vec3, XyRect, XzRect, YzRect, random_double, write_color_gamma,
};
// CosinePdf はここに現れない図 3.16: アルミ箱+白い小箱
アルミ(Metal)製の背の高い箱と白い Lambertian の短い箱を配置します。ScatterRecord の両バリアント(鏡面と拡散)が正しく動作することを確認するシーンです:
// Tall aluminum box.
let aluminum = Arc::new(Metal::new(Color::new(0.8, 0.85, 0.88), 0.0));
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),
aluminum,
));
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);
// Short white Lambertian box.
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);
// Only the ceiling light is sampled for importance sampling.
let lights: Arc<dyn Hittable> =
Arc::new(XzRect::new(213.0, 343.0, 227.0, 332.0, 554.0, light));アルミ箱(fuzz = 0.0)は完全鏡面反射(Specular パス),白い箱は Lambertian 拡散(Diffuse パス)で処理されます。
球の PDF(図 3.17)
次の変更として,白い小箱の代わりにガラス球を置きます。ガラス球を lights に追加して重点サンプリングするには,Sphere の pdf_value() と random() を実装する必要があります。
点 origin から半径
立体角は
Sphere::pdf_value() の実装
fn pdf_value(&self, origin: Point3, v: Vec3) -> f64 {
if self.hit(&Ray::new(origin, v), 0.001, f64::INFINITY).is_none() {
return 0.0;
}
let distance_squared = (self.center - origin).length_squared();
let cos_theta_max = (1.0 - self.radius * self.radius / distance_squared).sqrt();
let solid_angle = 2.0 * PI * (1.0 - cos_theta_max);
1.0 / solid_angle
}Sphere::random() の実装
球が subtend する円錐内に均一にランダムな方向を生成します:
fn random(&self, origin: Point3) -> Vec3 {
let direction = self.center - origin;
let distance_squared = direction.length_squared();
let uvw = Onb::build_from_w(direction);
uvw.local_vec(random_to_sphere(self.radius, distance_squared))
}補助関数 random_to_sphere は,ローカル座標系で円錐内の方向を生成します:
fn random_to_sphere(radius: f64, distance_squared: f64) -> Vec3 {
let r1 = random_double();
let r2 = random_double();
let cos_theta_max = (1.0 - radius * radius / distance_squared).sqrt();
let z = 1.0 + r2 * (cos_theta_max - 1.0);
let phi = 2.0 * PI * r1;
let sin_theta = (1.0 - z * z).max(0.0).sqrt();
Vec3::new(phi.cos() * sin_theta, phi.sin() * sin_theta, z)
}z = 1 + r2*(cos_theta_max - 1) で Onb::local_vec で世界座標に変換します。
図 3.18: ガラス球のみをライトにしたコーネルボックス
白い小箱の代わりにガラス球(屈折率 1.5)を置き,lights にはガラス球のみを指定します。シーンは白い背の高い Lambertian ボックスとガラス球の組み合わせです:
// Tall white Lambertian box.
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,
));
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);
// Glass sphere.
let glass = Arc::new(Dielectric::new(1.5));
objects.add(Box::new(Sphere::with_material(
Point3::new(190.0, 90.0, 190.0),
90.0,
glass,
)));
// Sphere PDF only — no ceiling XzRect in lights yet.
let lights: Arc<dyn Hittable> = Arc::new(Sphere::with_material(
Point3::new(190.0, 90.0, 190.0),
90.0,
Arc::new(Dielectric::new(1.5)),
));HittableList の pdf_value / random
ガラス球と天井 XzRect の両方を lights に含めるため,HittableList::pdf_value() / random() を common/src/hittable_list.rs に追加します:
fn pdf_value(&self, origin: Point3, v: Vec3) -> f64 {
if self.objects.is_empty() {
return 0.0;
}
let weight = 1.0 / self.objects.len() as f64;
self.objects.iter().map(|o| weight * o.pdf_value(origin, v)).sum()
}
fn random(&self, origin: Point3) -> Vec3 {
if self.objects.is_empty() {
return Vec3::new(1.0, 0.0, 0.0);
}
let idx = random_int(0, self.objects.len() as i32 - 1) as usize;
self.objects[idx].random(origin)
}pdf_value() は全オブジェクトの平均 PDF 値,random() は均等確率で 1 オブジェクトを選んでその方向を返します。
図 3.19: ガラス球をライトに追加
ガラス球も PDF サンプリングの対象にするため,天井 XzRect とガラス球の両方を HittableList に格納して lights とします:
// Lights: ceiling rect + glass sphere (both sampled for importance sampling).
let mut lights = HittableList::new();
lights.add(Box::new(XzRect::new(213.0, 343.0, 227.0, 332.0, 554.0, light)));
lights.add(Box::new(Sphere::with_material(
Point3::new(190.0, 90.0, 190.0),
90.0,
Arc::new(Dielectric::new(1.5)),
)));
let lights: Arc<dyn Hittable> = Arc::new(lights);アクネの除去(図 3.20)
原典(C++)では図 3.19 のレンダリングにアクネ(輝点)が発生します。Sphere::pdf_value() が未実装のため,mixed.value(direction) がほぼゼロになり,次の除算で輝度が爆発するためです:
emitted + attenuation * sp / pdf_val // pdf_val ≈ 0 → ∞ → アクネray_color の pdf_val ガード節がアクネを除去します(図 3.20):
let pdf_val = mixed.value(direction);
if pdf_val < 1e-10 {
return emitted;
}C++ と Rust の違い
C++ 版では scatter_record 構造体を出力引数として取る形式に scatter() のシグネチャーを変更し,全マテリアルを一斉に書き換えます:
struct scatter_record {
ray specular_ray;
bool is_specular;
color attenuation;
shared_ptr<pdf> pdf_ptr;
};
virtual bool scatter(
const ray& r_in, const hit_record& rec, scatter_record& srec) const;Rust 版の主な違い:
| 項目 | C++ | Rust |
|---|---|---|
| 鏡面/拡散の区別 | is_specular ブール値 | Specular / Diffuse バリアント(型安全) |
| PDF の保持 | shared_ptr<pdf> | Box<dyn Pdf> |
| 既存コードへの影響 | scatter() 破壊的変更(全マテリアル更新) | scatter_record() を追加(後方互換) |
| 網羅性の保証 | なし | コンパイル時パターン網羅チェック |
| 図 3.19 のアクネ | 発生(Sphere::pdf_value() 未実装) | 発生しない(本実装では既実装済み) |
scatter() を変更せず scatter_record() を追加したことで,第1・第2編の全デモクレートは変更なしでそのままビルドできます。
Rust 版では Sphere::pdf_value() と Sphere::random() を図 3.18 の時点で同時に実装するため,図 3.19 でアクネが発生しません。図 3.19 と図 3.20 は視覚的にほぼ同じ見た目になりますが,アーキテクチャ上の違い(ガード節の有無)は変わりません。
完全実装
r311-cornell-box/src/lib.rs
use std::sync::Arc;
use common::{
Camera, Color, DiffuseLight, FlipFace, Hittable, HittablePdf, HittableList,
Lambertian, Metal, MixturePdf, Pdf, Point3, Ray, RectBox, RotateY, ScatterRecord,
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_record(r, &rec) {
None => emitted,
Some(ScatterRecord::Specular { attenuation, ray }) => {
emitted + attenuation * ray_color(&ray, background, world, lights, depth - 1)
}
Some(ScatterRecord::Diffuse { attenuation, pdf: mat_pdf }) => {
let lp = Box::new(HittablePdf::new(lights.clone(), rec.p));
let mixed = MixturePdf::new(lp, mat_pdf);
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 sp = mat.scattering_pdf(r, &rec, &scattered);
emitted + attenuation * sp / pdf_val
* ray_color(&scattered, background, world, lights, depth - 1)
}
}
}
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)));
objects.add(Box::new(FlipFace::new(Box::new(
XzRect::new(213.0, 343.0, 227.0, 332.0, 554.0, light.clone()),
))));
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())));
// Tall aluminum box.
let aluminum = Arc::new(Metal::new(Color::new(0.8, 0.85, 0.88), 0.0));
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),
aluminum,
));
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);
// Short white Lambertian box.
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);
// Only the ceiling light is sampled for importance sampling.
let lights: Arc<dyn Hittable> =
Arc::new(XzRect::new(213.0, 343.0, 227.0, 332.0, 554.0, light));
(objects, lights)
}
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 camera = Camera::new_with_shutter(
lookfrom, lookat, vup, 40.0, aspect_ratio, 0.0, 10.0, 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
}r312-cornell-box/src/lib.rs
use std::sync::Arc;
use common::{
Camera, Color, Dielectric, DiffuseLight, FlipFace, Hittable, HittablePdf, HittableList,
Lambertian, MixturePdf, Pdf, Point3, Ray, ScatterRecord,
Sphere, 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_record(r, &rec) {
None => emitted,
Some(ScatterRecord::Specular { attenuation, ray }) => {
emitted + attenuation * ray_color(&ray, background, world, lights, depth - 1)
}
Some(ScatterRecord::Diffuse { attenuation, pdf: mat_pdf }) => {
let lp = Box::new(HittablePdf::new(lights.clone(), rec.p));
let mixed = MixturePdf::new(lp, mat_pdf);
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 sp = mat.scattering_pdf(r, &rec, &scattered);
emitted + attenuation * sp / pdf_val
* ray_color(&scattered, background, world, lights, depth - 1)
}
}
}
// ray_color without the near-zero PDF guard (used for Figure 3.19).
fn ray_color_no_guard(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_record(r, &rec) {
None => emitted,
Some(ScatterRecord::Specular { attenuation, ray }) => {
emitted + attenuation * ray_color_no_guard(&ray, background, world, lights, depth - 1)
}
Some(ScatterRecord::Diffuse { attenuation, pdf: mat_pdf }) => {
let lp = Box::new(HittablePdf::new(lights.clone(), rec.p));
let mixed = MixturePdf::new(lp, mat_pdf);
let direction = mixed.generate();
let scattered = Ray::with_time(rec.p, direction, r.time());
let pdf_val = mixed.value(direction);
let sp = mat.scattering_pdf(r, &rec, &scattered);
emitted + attenuation * sp / pdf_val
* ray_color_no_guard(&scattered, background, world, lights, depth - 1)
}
}
}
fn make_world() -> HittableList {
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)));
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())));
// Tall white Lambertian box.
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,
));
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 glass = Arc::new(Dielectric::new(1.5));
objects.add(Box::new(Sphere::with_material(
Point3::new(190.0, 90.0, 190.0),
90.0,
glass,
)));
objects
}
fn make_camera(aspect_ratio: f64) -> Camera {
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);
Camera::new_with_shutter(lookfrom, lookat, vup, 40.0, aspect_ratio, 0.0, 10.0, 0.0, 1.0)
}
fn render(
world: &HittableList,
lights: Arc<dyn Hittable>,
camera: &Camera,
image_width: i32,
image_height: i32,
samples_per_pixel: i32,
max_depth: i32,
use_guard: bool,
) -> String {
let background = Color::new(0.0, 0.0, 0.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);
let c = if use_guard {
ray_color(&r, background, world, lights.clone(), max_depth)
} else {
ray_color_no_guard(&r, background, world, lights.clone(), max_depth)
};
pixel_color += c;
}
output.push_str(&format!(
"{}\n",
write_color_gamma(pixel_color, samples_per_pixel)
));
}
}
output
}
// Figure 3.18: glass sphere only scene; lights = glass sphere (sphere PDF only).
pub fn render_fig18() -> String {
let aspect_ratio = 1.0_f64;
let image_width = 300_i32;
let image_height = (image_width as f64 / aspect_ratio) as i32;
let world = make_world();
let lights: Arc<dyn Hittable> = Arc::new(Sphere::with_material(
Point3::new(190.0, 90.0, 190.0),
90.0,
Arc::new(Dielectric::new(1.5)),
));
let camera = make_camera(aspect_ratio);
render(&world, lights, &camera, image_width, image_height, 200, 50, true)
}
// Figure 3.19: glass sphere scene; lights = ceiling XzRect + glass sphere, no acne guard.
pub fn render_fig19() -> String {
let aspect_ratio = 1.0_f64;
let image_width = 300_i32;
let image_height = (image_width as f64 / aspect_ratio) as i32;
let world = make_world();
let light = Arc::new(DiffuseLight::new(Color::new(15.0, 15.0, 15.0)));
let mut lights_list = HittableList::new();
lights_list.add(Box::new(XzRect::new(213.0, 343.0, 227.0, 332.0, 554.0, light)));
lights_list.add(Box::new(Sphere::with_material(
Point3::new(190.0, 90.0, 190.0),
90.0,
Arc::new(Dielectric::new(1.5)),
)));
let lights: Arc<dyn Hittable> = Arc::new(lights_list);
let camera = make_camera(aspect_ratio);
render(&world, lights, &camera, image_width, image_height, 200, 50, false)
}
// Figure 3.20: glass sphere scene; lights = ceiling XzRect + glass sphere, with acne guard.
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 world = make_world();
let light = Arc::new(DiffuseLight::new(Color::new(15.0, 15.0, 15.0)));
let mut lights_list = HittableList::new();
lights_list.add(Box::new(XzRect::new(213.0, 343.0, 227.0, 332.0, 554.0, light)));
lights_list.add(Box::new(Sphere::with_material(
Point3::new(190.0, 90.0, 190.0),
90.0,
Arc::new(Dielectric::new(1.5)),
)));
let lights: Arc<dyn Hittable> = Arc::new(lights_list);
let camera = make_camera(aspect_ratio);
render(&world, lights, &camera, image_width, image_height, 200, 50, true)
}まとめ
ScatterRecordenum(Specular/Diffuse)をcommonに追加し,鏡面散乱と拡散散乱を型レベルで区別した。scatter_record()を新メソッドとして追加(既存のscatter()は変更なし)。LambertianのみオーバーライドしてCosinePdfを内包したDiffuseバリアントを返す。Metal・Dielectric・Isotropic はデフォルト実装(Specularとして包む)で動作。ray_colorからCosinePdfの直接構築が消え,マテリアルの知識がレンダラーに漏れる問題を解決した。Sphere::pdf_value()/random()をcommon/src/sphere.rsに実装し,球をlightsとして重点サンプリングできるようにした(立体角 PDF)。 HittableList::pdf_value()/random()をcommonに追加し,複数光源をHittableListとして一括管理できるようにした(天井XzRect+ガラス球)。pdf_val < 1e-10ガードで near-zero PDF によるアクネ(輝点)を除去した。- 4 つのコーネルボックスを
r311-cornell-box(図 3.16)とr312-cornell-box(図 3.18 / 3.19 / 3.20)として実装した。