12. Defocus Blur
Ray Tracing in One Weekend (v3.2.3): 12 Defocus Blur
In this chapter we implement defocus blur — the photographic effect where objects in front of or behind the focal plane appear blurred, commonly known as depth of field.
Why Defocus Blur Occurs
A pinhole camera keeps everything in perfect focus. A real camera gathers light through a large aperture (lens opening), so only objects on a specific plane (the focal plane) appear sharp. Everything else blurs.
Two terms to distinguish:
| Term | Meaning |
|---|---|
| Focal length | Distance from the lens to the sensor (film) |
| Focus distance | Distance from the lens to the plane that is in focus |
The focus distance is not necessarily equal to the focal length. In a real camera you adjust focus by moving the lens.
Thin Lens Approximation
Rather than simulating a full optical system, we use the thin lens approximation. The idea is simple:
Fire rays from the lens and aim each ray at a point on the focus plane. Ignore everything inside the camera between the lens and the sensor.
This alone is enough to produce convincingly realistic depth of field.
Generating Blur with Disk Sampling
Previously every ray originated from a single point — lookfrom. To produce defocus blur, we distribute ray origins randomly over a lens disk centered on lookfrom.
random_in_unit_disk
We use the same rejection method as random_in_unit_sphere, but fix
C++:
vec3 random_in_unit_disk() {
while (true) {
auto p = vec3(random_double(-1,1), random_double(-1,1), 0);
if (p.length_squared() >= 1) continue;
return p;
}
}Rust (added to common/src/vec3.rs):
/// Returns a random point inside the unit **disk** (z=0 plane, rejection method).
pub fn random_in_unit_disk() -> Vec3 {
use crate::utils::random_double_range;
loop {
let p = Vec3::new(
random_double_range(-1.0, 1.0),
random_double_range(-1.0, 1.0),
0.0,
);
if p.length_squared() < 1.0 {
return p;
}
}
}The only difference from random_in_unit_sphere is that the 0.0.
Updating Camera
New Parameters
We add aperture (lens aperture diameter) and focus_dist (focus distance) to Camera::new().
Scaling the Viewport
In the previous implementation the viewport was fixed at distance 1 from the origin (we just subtracted the unit vector focus_dist away so that rays are aimed at points on the focus plane.
Summary of what changed:
| Before | After | |
|---|---|---|
| horizontal | viewport_width * u | focus_dist * viewport_width * u |
| vertical | viewport_height * v | focus_dist * viewport_height * v |
| lower_left_corner | ... - w | ... - focus_dist * w |
u, v fields | Not stored (used in constructor only) | Stored (used in get_ray) |
lens_radius | None | aperture / 2.0 |
New get_ray
C++:
ray get_ray(double s, double t) const {
vec3 rd = lens_radius * random_in_unit_disk();
vec3 offset = u * rd.x() + v * rd.y();
return ray(
origin + offset,
lower_left_corner + s*horizontal + t*vertical - origin - offset
);
}Rust:
pub fn get_ray(&self, s: f64, t: f64) -> Ray {
let rd = self.lens_radius * random_in_unit_disk();
let offset = self.u * rd.x() + self.v * rd.y();
Ray::new(
self.origin + offset,
self.lower_left_corner + s * self.horizontal + t * self.vertical - self.origin - offset,
)
}rd is a random point on the disk (a 3D vector with
When aperture = 0.0, lens_radius = 0.0 → rd = (0,0,0) → offset = (0,0,0), which is identical to the previous pinhole behavior.
Differences Between C++ and Rust
| Feature | C++ | Rust |
|---|---|---|
| Vector length | (v - w).length() | (v - w).length() (identical) |
| Disk sampling | Free function | Free function (same structure) |
| Scalar × Vec | t * v | t * v (Mul<f64> impl for Vec3) |
| Field access | u, v | self.u, self.v |
Changes to the common Crate
common/src/camera.rs (full replacement)
use crate::vec3::{Point3, Vec3, cross, unit_vector, random_in_unit_disk};
use crate::ray::Ray;
use crate::utils::degrees_to_radians;
/// A positionable camera with thin-lens depth of field.
pub struct Camera {
origin: Point3,
lower_left_corner: Point3,
horizontal: Vec3,
vertical: Vec3,
u: Vec3,
v: Vec3,
lens_radius: f64,
}
impl Camera {
/// `lookfrom`: camera origin, `lookat`: point the camera looks at, `vup`: up-direction vector.
/// `vfov`: vertical field of view (degrees), `aspect_ratio`: width/height ratio.
/// `aperture`: lens aperture diameter (0.0 = pinhole), `focus_dist`: focus distance.
pub fn new(
lookfrom: Point3,
lookat: Point3,
vup: Vec3,
vfov: f64,
aspect_ratio: f64,
aperture: f64,
focus_dist: f64,
) -> Self {
let theta = degrees_to_radians(vfov);
let h = (theta / 2.0).tan();
let viewport_height = 2.0 * h;
let viewport_width = aspect_ratio * viewport_height;
let w = unit_vector(lookfrom - lookat);
let u = unit_vector(cross(vup, w));
let v = cross(w, u);
let origin = lookfrom;
let horizontal = focus_dist * viewport_width * u;
let vertical = focus_dist * viewport_height * v;
let lower_left_corner = origin - horizontal / 2.0 - vertical / 2.0 - focus_dist * w;
let lens_radius = aperture / 2.0;
Camera { origin, lower_left_corner, horizontal, vertical, u, v, lens_radius }
}
/// Returns a ray for the given `(s, t)` viewport coordinates in [0, 1].
pub fn get_ray(&self, s: f64, t: f64) -> Ray {
let rd = self.lens_radius * random_in_unit_disk();
let offset = self.u * rd.x() + self.v * rd.y();
Ray::new(
self.origin + offset,
self.lower_left_corner + s * self.horizontal + t * self.vertical - self.origin - offset,
)
}
}Updating Existing Crates
Because Camera::new() has changed its signature, all 10 existing crates (r107–r111, two crates each) must add aperture and focus_dist. Pass aperture = 0.0 and focus_dist = 1.0 to reproduce the previous pinhole behavior (focus_dist = 1.0 matches the old "place the viewport at distance 1" behavior).
// After (no blur; behavior identical to before)
let camera = Camera::new(
lookfrom,
lookat,
vup,
vfov,
aspect_ratio,
0.0, // aperture: no blur
1.0, // focus_dist: same viewport distance as before
);Implementing the r112-defocus-blur Crate
[package]
name = "r112-defocus-blur"
version = "0.1.0"
edition = "2024"
[dependencies]
common = { workspace = true }r112-defocus-blur/src/lib.rs
use std::sync::Arc;
use common::{
Camera, Color, Dielectric, Hittable, HittableList, Lambertian, Metal, Point3,
Sphere, unit_vector, write_color_gamma, Ray, random_double,
};
fn ray_color(r: &Ray, world: &dyn Hittable, depth: i32) -> Color {
if depth <= 0 {
return Color::new(0.0, 0.0, 0.0);
}
if let Some(rec) = world.hit(r, 0.001, f64::INFINITY) {
if let Some(mat) = &rec.mat {
if let Some((attenuation, scattered)) = mat.scatter(r, &rec) {
return attenuation * ray_color(&scattered, world, depth - 1);
}
}
return Color::new(0.0, 0.0, 0.0);
}
let unit_direction = unit_vector(r.direction());
let t = 0.5 * (unit_direction.y() + 1.0);
(1.0 - t) * Color::new(1.0, 1.0, 1.0) + t * Color::new(0.5, 0.7, 1.0)
}
pub fn render_image() -> String {
let aspect_ratio = 16.0_f64 / 9.0;
let image_width = 384_i32;
let image_height = (image_width as f64 / aspect_ratio) as i32;
let samples_per_pixel = 50_i32;
let max_depth = 50_i32;
let mut world = HittableList::new();
// Center: Lambertian (bluish diffuse sphere)
world.add(Box::new(Sphere::with_material(
Point3::new(0.0, 0.0, -1.0), 0.5,
Arc::new(Lambertian::new(Color::new(0.1, 0.2, 0.5))),
)));
// Ground: Lambertian (yellow-green large sphere)
world.add(Box::new(Sphere::with_material(
Point3::new(0.0, -100.5, -1.0), 100.0,
Arc::new(Lambertian::new(Color::new(0.8, 0.8, 0.0))),
)));
// Right: Metal (gold, fuzz=0)
world.add(Box::new(Sphere::with_material(
Point3::new(1.0, 0.0, -1.0), 0.5,
Arc::new(Metal::new(Color::new(0.8, 0.6, 0.2), 0.0)),
)));
// Left: dielectric (glass sphere, ref_idx=1.5)
world.add(Box::new(Sphere::with_material(
Point3::new(-1.0, 0.0, -1.0), 0.5,
Arc::new(Dielectric::new(1.5)),
)));
// Defocus-blur camera: use the distance from lookfrom to lookat as the focus distance.
let lookfrom = Point3::new(3.0, 3.0, 2.0);
let lookat = Point3::new(0.0, 0.0, -1.0);
let aperture = 2.0_f64;
let dist_to_focus = (lookfrom - lookat).length();
let camera = Camera::new(
lookfrom,
lookat,
Point3::new(0.0, 1.0, 0.0),
20.0,
aspect_ratio,
aperture,
dist_to_focus,
);
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, &world, max_depth);
}
output.push_str(&format!("{}\n", write_color_gamma(pixel_color, samples_per_pixel)));
}
}
output
}dist_to_focus = (lookfrom - lookat).length() calls the .length() method directly in Rust, which corresponds directly to C++'s (lookfrom-lookat).length().
WASM Export
Add to raytracing-demos/src/lib.rs:
// Chapter 1.12: Defocus Blur
#[wasm_bindgen]
pub fn render_defocus_blur() -> String {
r112_defocus_blur::render_image()
}