12. Cleaning Up PDF Management
Ray Tracing: The Rest of Your Life (v3.2.3): 12 Cleaning Up PDF Management / 3.12 Cleaning Up PDF Management
This chapter resolves the two problems identified previously: the hard-coded CosinePdf and the incompatible handling of specular and diffuse scattering. I add the ScatterRecord enum and scatter_record() method to common, then implement four variations of the Cornell box while evolving the scene from an aluminum and white-box arrangement to one with a glass sphere.
ScatterRecord enum
Add the following enum to common/src/material.rs:
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> },
}The Specular variant represents a fully determined scattering direction, as used by Metal and Dielectric. The Diffuse variant represents a direction determined by a PDF object, as used by Lambertian.
Extending the Material Trait
Leave scatter() unchanged and add a new scatter_record() method:
pub trait Material: Send + Sync {
fn scatter(&self, r_in: &Ray, rec: &HitRecord) -> Option<(Color, Ray)>;
// New method: the default implementation wraps scatter() as 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
}
}Because the default implementation wraps the existing scatter() result in Specular, Metal, Dielectric, and Isotropic continue to work without any awareness of the new method.
Overriding Lambertian::scatter_record()
Only Lambertian overrides the method, returning a Diffuse variant containing a CosinePdf:
impl Material for Lambertian {
// Keep the existing scatter() implementation unchanged
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)),
})
}
}As a result, ray_color no longer needs direct knowledge of CosinePdf.
Updating ray_color
ray_color now matches on the result of 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 no longer appears in the imports for ray_color:
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 does not appear hereFigure 3.16: Aluminum and White Boxes
Place a tall aluminum (Metal) box beside a short white Lambertian box. This scene verifies that both ScatterRecord variants—specular and diffuse—work correctly:
// 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));The aluminum box with fuzz = 0.0 follows the perfectly specular Specular path, while the white box follows the Lambertian Diffuse path.
The Sphere PDF (Figure 3.17)
Next, replace the short white box with a glass sphere. Adding that sphere to lights for importance sampling requires implementations of Sphere::pdf_value() and Sphere::random().
Consider the cone subtended by a sphere of radius origin, as shown in Figure 3.17. Its maximum cosine is
The solid angle is
Implementing 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
}Implementing Sphere::random()
Generate a uniformly random direction inside the cone subtended by the sphere:
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))
}The helper function random_to_sphere generates a direction within the cone in local coordinates:
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)
}The expression z = 1 + r2*(cos_theta_max - 1) samples Onb::local_vec transforms the result into world coordinates.
Figure 3.18: Using Only the Glass Sphere as a Light Target
Replace the short white box with a glass sphere of refractive index 1.5 and specify only that sphere in lights. The scene combines a tall white Lambertian box with the glass sphere:
// 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 and HittableList::random
To include both the glass sphere and the ceiling XzRect in lights, add HittableList::pdf_value() and HittableList::random() to 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() returns the average PDF value across all objects, while random() selects one object with equal probability and returns a direction sampled from it.
Figure 3.19: Adding the Glass Sphere to the Sampling Targets
To sample the glass sphere as well, store both the ceiling XzRect and the sphere in a HittableList and use it as 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);Removing Acne (Figure 3.20)
In the original C++ progression, the Figure 3.19 render develops acne, or bright speckles. Because Sphere::pdf_value() is not yet implemented there, mixed.value(direction) becomes nearly zero and the following division makes the radiance explode:
emitted + attenuation * sp / pdf_val // pdf_val ≈ 0 → ∞ → acneThe pdf_val guard in ray_color removes the acne in Figure 3.20:
let pdf_val = mixed.value(direction);
if pdf_val < 1e-10 {
return emitted;
}Differences Between C++ and Rust
The C++ version changes the signature of scatter() to accept a scatter_record structure as an output argument, requiring all materials to be rewritten at once:
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;The principal differences in the Rust version are:
| Aspect | C++ | Rust |
|---|---|---|
| Specular/diffuse distinction | is_specular Boolean | Specular / Diffuse variants (type-safe) |
| PDF storage | shared_ptr<pdf> | Box<dyn Pdf> |
| Effect on existing code | Breaking scatter() change to every material | Additive scatter_record() method |
| Exhaustiveness guarantee | None | Compile-time exhaustive pattern checking |
| Acne in Figure 3.19 | Occurs because Sphere::pdf_value() is missing | Does not occur because it is already implemented |
By adding scatter_record() without changing scatter(), every demo crate from Books 1 and 2 continues to build unchanged.
The Rust version implements Sphere::pdf_value() and Sphere::random() together at Figure 3.18, so the acne does not appear in Figure 3.19. Figures 3.19 and 3.20 therefore look almost identical, although the architectural difference—the absence or presence of the guard—remains.
Complete Implementations
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)
}Summary
- I added the
ScatterRecordenum withSpecularandDiffusevariants tocommon, distinguishing specular from diffuse scattering at the type level. - I added
scatter_record()as a new method while leaving the existingscatter()unchanged. OnlyLambertianoverrides it to return aDiffusevariant containing aCosinePdf;Metal,Dielectric, andIsotropicuse the default implementation that wraps their results inSpecular. - Direct construction of
CosinePdfdisappeared fromray_color, resolving the leakage of material knowledge into the renderer. - I implemented
Sphere::pdf_value()andSphere::random()incommon/src/sphere.rs, allowing a sphere inlightsto be importance-sampled with the solid-angle PDF. - I added
HittableList::pdf_value()andHittableList::random()tocommon, allowing multiple sampling targets—the ceilingXzRectand glass sphere—to be managed together in oneHittableList. - The
pdf_val < 1e-10guard removes bright-speckle acne caused by a near-zero PDF. - I implemented four Cornell-box variations in
r311-cornell-boxfor Figure 3.16 andr312-cornell-boxfor Figures 3.18, 3.19, and 3.20.