Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Generating Sample Rays

Without defocus blur, all scene rays originate from the camera center (or lookfrom). In order to accomplish defocus blur, we construct a disk centered at the camera center. The larger the radius, the greater the defocus blur. You can think of our original camera as having a defocus disk of radius zero (no blur at all), so all rays originated at the disk center (lookfrom).

So, how large should the defocus disk be? Since the size of this disk controls how much defocus blur we get, that should be a parameter of the camera class. We could just take the radius of the disk as a camera parameter, but the blur would vary depending on the projection distance. A slightly easier parameter is to specify the angle of the cone with apex at viewport center and base (defocus disk) at the camera center. This should give you more consistent results as you vary the focus distance for a given shot.

Since we'll be choosing random points from the defocus disk, we'll need a function to do that: random_in_unit_disk(). This function works using the same kind of method we use in random_unit_vector(), just for two dimensions.

diff --git a/src/vec3.rs b/src/vec3.rs
index df0939e..d4352e1 100644
--- a/src/vec3.rs
+++ b/src/vec3.rs
@@ -1,176 +1,190 @@
 use std::{
     fmt::Display,
     ops::{Add, AddAssign, Div, DivAssign, Index, IndexMut, Mul, MulAssign, Neg, Sub},
 };
 
 #[derive(Debug, Default, Clone, Copy)]
 pub struct Vec3 {
     pub e: [f64; 3],
 }
 
 pub type Point3 = Vec3;
 
 impl Vec3 {
     pub fn new(e0: f64, e1: f64, e2: f64) -> Self {
         Self { e: [e0, e1, e2] }
     }
 
     pub fn x(&self) -> f64 {
         self.e[0]
     }
 
     pub fn y(&self) -> f64 {
         self.e[1]
     }
 
     pub fn z(&self) -> f64 {
         self.e[2]
     }
 
     pub fn length(&self) -> f64 {
         f64::sqrt(self.length_squared())
     }
 
     pub fn length_squared(&self) -> f64 {
         self.e[0] * self.e[0] + self.e[1] * self.e[1] + self.e[2] * self.e[2]
     }
 }
 
 impl Neg for Vec3 {
     type Output = Self;
 
     fn neg(self) -> Self::Output {
         Self::Output {
             e: self.e.map(|e| -e),
         }
     }
 }
 
 impl Index<usize> for Vec3 {
     type Output = f64;
 
     fn index(&self, index: usize) -> &Self::Output {
         &self.e[index]
     }
 }
 
 impl IndexMut<usize> for Vec3 {
     fn index_mut(&mut self, index: usize) -> &mut Self::Output {
         &mut self.e[index]
     }
 }
 
 impl AddAssign for Vec3 {
     fn add_assign(&mut self, rhs: Self) {
         self.e[0] += rhs.e[0];
         self.e[1] += rhs.e[1];
         self.e[2] += rhs.e[2];
     }
 }
 
 impl MulAssign<f64> for Vec3 {
     fn mul_assign(&mut self, rhs: f64) {
         self.e[0] *= rhs;
         self.e[1] *= rhs;
         self.e[2] *= rhs;
     }
 }
 
 impl DivAssign<f64> for Vec3 {
     fn div_assign(&mut self, rhs: f64) {
         self.mul_assign(1.0 / rhs);
     }
 }
 
 impl Display for Vec3 {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         write!(f, "{} {} {}", self.e[0], self.e[1], self.e[2])
     }
 }
 
 impl Add for Vec3 {
     type Output = Self;
 
     fn add(self, rhs: Self) -> Self::Output {
         Self::Output {
             e: [
                 self.e[0] + rhs.e[0],
                 self.e[1] + rhs.e[1],
                 self.e[2] + rhs.e[2],
             ],
         }
     }
 }
 
 impl Sub for Vec3 {
     type Output = Self;
 
     fn sub(self, rhs: Self) -> Self::Output {
         Self::Output {
             e: [
                 self.e[0] - rhs.e[0],
                 self.e[1] - rhs.e[1],
                 self.e[2] - rhs.e[2],
             ],
         }
     }
 }
 
 impl Mul for Vec3 {
     type Output = Self;
 
     fn mul(self, rhs: Self) -> Self::Output {
         Self::Output {
             e: [
                 self.e[0] * rhs.e[0],
                 self.e[1] * rhs.e[1],
                 self.e[2] * rhs.e[2],
             ],
         }
     }
 }
 
 impl Mul<f64> for Vec3 {
     type Output = Self;
 
     fn mul(self, rhs: f64) -> Self::Output {
         Self::Output {
             e: [self.e[0] * rhs, self.e[1] * rhs, self.e[2] * rhs],
         }
     }
 }
 
 impl Mul<Vec3> for f64 {
     type Output = Vec3;
 
     fn mul(self, rhs: Vec3) -> Self::Output {
         rhs.mul(self)
     }
 }
 
 impl Div<f64> for Vec3 {
     type Output = Self;
 
     fn div(self, rhs: f64) -> Self::Output {
         self * (1.0 / rhs)
     }
 }
 
 #[inline]
 pub fn dot(u: Vec3, v: Vec3) -> f64 {
     u.e[0] * v.e[0] + u.e[1] * v.e[1] + u.e[2] * v.e[2]
 }
 
 #[inline]
 pub fn cross(u: Vec3, v: Vec3) -> Vec3 {
     Vec3::new(
         u.e[1] * v.e[2] - u.e[2] * v.e[1],
         u.e[2] * v.e[0] - u.e[0] * v.e[2],
         u.e[0] * v.e[1] - u.e[1] * v.e[0],
     )
 }
 
 #[inline]
 pub fn unit_vector(v: Vec3) -> Vec3 {
     v / v.length()
 }
+
+#[inline]
+pub fn random_in_unit_disk() -> Vec3 {
+    loop {
+        let p = Vec3::new(
+            rand::random_range(-1.0..1.0),
+            rand::random_range(-1.0..1.0),
+            0.0,
+        );
+        if p.length_squared() < 1.0 {
+            return p;
+        }
+    }
+}

Listing 85: [vec3.rs] Generate random point inside unit disk


Now let's update the camera to originate rays from the defocus disk:

diff --git a/src/camera.rs b/src/camera.rs
index 44da965..6b51a5b 100644
--- a/src/camera.rs
+++ b/src/camera.rs
@@ -1,220 +1,259 @@
 use crate::{hittable::Hittable, prelude::*};
 
 pub struct Camera {
     /// Ratio of image width over height
     pub aspect_ratio: f64,
     /// Rendered image width in pixel count
     pub image_width: i32,
     // Count of random samples for each pixel
     pub samples_per_pixel: i32,
     // Maximum number of ray bounces into scene
     pub max_depth: i32,
     // Vertical view angle (field of view)
     pub vfov: f64,
     /// Point camera is looking from
     pub lookfrom: Point3,
     /// Point camera is looking at
     pub lookat: Point3,
     /// Camera-relative "up" direction
     pub vup: Vec3,
+    /// Variation angle of rays through each pixel
+    pub defocus_angle: f64,
+    /// Distance from camera lookfrom point to plane of perfect focus
+    pub focus_dist: f64,
 
     /// Rendered image height
     image_height: i32,
     // Color scale factor for a sum of pixel samples
     pixel_samples_scale: f64,
     /// Camera center
     center: Point3,
     /// Location of pixel 0, 0
     pixel00_loc: Point3,
     /// Offset to pixel to the right
     pixel_delta_u: Vec3,
     /// Offset to pixel below
     pixel_delta_v: Vec3,
     /// Camera frame basis vector - right
     u: Vec3,
     /// Camera frame basis vector - up
     v: Vec3,
     /// Camera frame basis vector - opposite view direction
     w: Vec3,
+    /// Defocus disk horizontal radius
+    defocus_disk_u: Vec3,
+    /// Defocus disk vertical radius
+    defocus_disk_v: Vec3,
 }
 
 impl Default for Camera {
     fn default() -> Self {
         Self {
             aspect_ratio: 1.0,
             image_width: 100,
             samples_per_pixel: 10,
             max_depth: 10,
             vfov: 90.0,
             lookfrom: Point3::new(0.0, 0.0, 0.0),
             lookat: Point3::new(0.0, 0.0, -1.0),
             vup: Point3::new(0.0, 1.0, 0.0),
+            defocus_angle: 0.0,
+            focus_dist: 10.0,
             image_height: Default::default(),
             pixel_samples_scale: Default::default(),
             center: Default::default(),
             pixel00_loc: Default::default(),
             pixel_delta_u: Default::default(),
             pixel_delta_v: Default::default(),
             u: Default::default(),
             v: Default::default(),
             w: Default::default(),
+            defocus_disk_u: Default::default(),
+            defocus_disk_v: Default::default(),
         }
     }
 }
 
 impl Camera {
     pub fn with_aspect_ratio(mut self, aspect_ratio: f64) -> Self {
         self.aspect_ratio = aspect_ratio;
 
         self
     }
 
     pub fn with_image_width(mut self, image_width: i32) -> Self {
         self.image_width = image_width;
 
         self
     }
 
     pub fn with_samples_per_pixel(mut self, samples_per_pixel: i32) -> Self {
         self.samples_per_pixel = samples_per_pixel;
 
         self
     }
 
     pub fn with_max_depth(mut self, max_depth: i32) -> Self {
         self.max_depth = max_depth;
 
         self
     }
 
     pub fn with_vfov(mut self, vfov: f64) -> Self {
         self.vfov = vfov;
 
         self
     }
 
     pub fn with_lookfrom(mut self, lookfrom: Point3) -> Self {
         self.lookfrom = lookfrom;
 
         self
     }
 
     pub fn with_lookat(mut self, lookat: Point3) -> Self {
         self.lookat = lookat;
 
         self
     }
 
     pub fn with_vup(mut self, vup: Vec3) -> Self {
         self.vup = vup;
 
         self
     }
 
+    pub fn with_defocus_angle(mut self, defocus_angle: f64) -> Self {
+        self.defocus_angle = defocus_angle;
+
+        self
+    }
+
+    pub fn with_focus_dist(mut self, focus_dist: f64) -> Self {
+        self.focus_dist = focus_dist;
+
+        self
+    }
+
     pub fn render(&mut self, world: &impl Hittable) -> std::io::Result<()> {
         self.initialize();
 
         println!("P3");
         println!("{} {}", self.image_width, self.image_height);
         println!("255");
 
         for j in 0..self.image_height {
             info!("Scanlines remaining: {}", self.image_height - j);
             for i in 0..self.image_width {
                 let mut pixel_color = Color::new(0.0, 0.0, 0.0);
                 for _sample in 0..self.samples_per_pixel {
                     let r = self.get_ray(i, j);
                     pixel_color += Self::ray_color(r, self.max_depth, world);
                 }
                 write_color(std::io::stdout(), self.pixel_samples_scale * pixel_color)?;
             }
         }
         info!("Done.");
 
         Ok(())
     }
 
     fn initialize(&mut self) {
         self.image_height = {
             let image_height = (self.image_width as f64 / self.aspect_ratio) as i32;
             if image_height < 1 { 1 } else { image_height }
         };
 
         self.pixel_samples_scale = 1.0 / self.samples_per_pixel as f64;
 
         self.center = self.lookfrom;
 
         // Determine viewport dimensions.
-        let focal_length = (self.lookfrom - self.lookat).length();
         let theta = self.vfov.to_radians();
         let h = f64::tan(theta / 2.0);
-        let viewport_height = 2.0 * h * focal_length;
+        let viewport_height = 2.0 * h * self.focus_dist;
         let viewport_width =
             viewport_height * (self.image_width as f64) / (self.image_height as f64);
 
         // Calculate the u,v,w unit basis vectors for the camera coordinate frame.
         self.w = unit_vector(self.lookfrom - self.lookat);
         self.u = unit_vector(cross(self.vup, self.w));
         self.v = cross(self.w, self.u);
 
         // Calculate the vectors across the horizontal and down the vertical viewport edges.
         let viewport_u = viewport_width * self.u; // Vector across viewport horizontal edge
         let viewport_v = viewport_height * -self.v; // Vector down viewport vertical edge
 
         // Calculate the horizontal and vertical delta vectors from pixel to pixel.
         self.pixel_delta_u = viewport_u / self.image_width as f64;
         self.pixel_delta_v = viewport_v / self.image_height as f64;
 
         // Calculate the location of the upper left pixel.
         let viewport_upper_left =
-            self.center - (focal_length * self.w) - viewport_u / 2.0 - viewport_v / 2.0;
+            self.center - (self.focus_dist * self.w) - viewport_u / 2.0 - viewport_v / 2.0;
         self.pixel00_loc = viewport_upper_left + 0.5 * (self.pixel_delta_u + self.pixel_delta_v);
+
+        // Calculate the camera defocus disk basis vectors.
+        let defocus_radius = self.focus_dist * f64::tan((self.defocus_angle / 2.0).to_radians());
+        self.defocus_disk_u = self.u * defocus_radius;
+        self.defocus_disk_v = self.v * defocus_radius;
     }
 
     fn get_ray(&self, i: i32, j: i32) -> Ray {
-        // Construct a camera ray originating from the origin and directed at randomly sampled
-        // point around the pixel location i, j.
+        // Construct a camera ray originating from the defocus disk and directed at a randomly
+        // sampled point around the pixel location i, j.
 
         let offset = Self::sample_square();
         let pixel_sample = self.pixel00_loc
             + ((i as f64 + offset.x()) * self.pixel_delta_u)
             + ((j as f64 + offset.y()) * self.pixel_delta_v);
 
-        let ray_origin = self.center;
+        let ray_origin = if self.defocus_angle <= 0.0 {
+            self.center
+        } else {
+            self.defocus_disk_sample()
+        };
         let ray_direction = pixel_sample - ray_origin;
 
         Ray::new(ray_origin, ray_direction)
     }
 
     fn sample_square() -> Vec3 {
         // Returns the vector to a random point in the [-.5,-.5]-[+.5,+.5] unit square.
         Vec3::new(
             rand::random::<f64>() - 0.5,
             rand::random::<f64>() - 0.5,
             0.0,
         )
     }
 
     fn _sample_disk(radius: f64) -> Vec3 {
         // Returns a random point in the unit (radius 0.5) disk centered at the origin.
         radius * random_in_unit_disk()
     }
 
+    fn defocus_disk_sample(&self) -> Point3 {
+        // Returns a random point in the camera defocus disk.
+        let p = random_in_unit_disk();
+
+        self.center + (p[0] * self.defocus_disk_u) + (p[1] * self.defocus_disk_v)
+    }
+
     fn ray_color(r: Ray, depth: i32, world: &impl Hittable) -> Color {
         // If we've exceeded the ray bounce limit, no more light is gathered.
         if depth <= 0 {
             return Color::new(0.0, 0.0, 0.0);
         }
 
         if let Some(rec) = world.hit(r, Interval::new(0.001, INFINITY)) {
             if let Some((scattered, attenuation)) = rec.mat.scatter(r, rec.clone()) {
                 return attenuation * Self::ray_color(scattered, depth - 1, world);
             }
             return Color::new(0.0, 0.0, 0.0);
         }
 
         let unit_direction = unit_vector(r.direction());
         let a = 0.5 * (unit_direction.y() + 1.0);
         (1.0 - a) * Color::new(1.0, 1.0, 1.0) + a * Color::new(0.5, 0.7, 1.0)
     }
 }

Listing 86: [camera.rs] Camera with adjustable depth-of-field


Using a large aperture:

diff --git a/src/main.rs b/src/main.rs
index f7deb5e..51d420b 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,56 +1,58 @@
 use code::{
     camera::Camera,
     hittable_list::HittableList,
     material::{Dielectric, Lambertian, Metal},
     prelude::*,
     sphere::Sphere,
 };
 
 fn main() -> std::io::Result<()> {
     let mut world = HittableList::new();
 
     let material_ground = Rc::new(Lambertian::new(Color::new(0.8, 0.8, 0.0)));
     let material_center = Rc::new(Lambertian::new(Color::new(0.1, 0.2, 0.5)));
     let material_left = Rc::new(Dielectric::new(1.5));
     let material_bubble = Rc::new(Dielectric::new(1.0 / 1.5));
     let material_right = Rc::new(Metal::new(Color::new(0.8, 0.6, 0.2), 1.0));
 
     world.add(Rc::new(Sphere::new(
         Point3::new(0.0, -100.5, -1.0),
         100.0,
         material_ground,
     )));
     world.add(Rc::new(Sphere::new(
         Point3::new(0.0, 0.0, -1.2),
         0.5,
         material_center,
     )));
     world.add(Rc::new(Sphere::new(
         Point3::new(-1.0, 0.0, -1.0),
         0.5,
         material_left,
     )));
     world.add(Rc::new(Sphere::new(
         Point3::new(-1.0, 0.0, -1.0),
         0.4,
         material_bubble,
     )));
     world.add(Rc::new(Sphere::new(
         Point3::new(1.0, 0.0, -1.0),
         0.5,
         material_right,
     )));
 
     env_logger::init();
 
     Camera::default()
         .with_aspect_ratio(16.0 / 9.0)
         .with_image_width(400)
         .with_samples_per_pixel(100)
         .with_max_depth(50)
         .with_vfov(20.0)
         .with_lookfrom(Point3::new(-2.0, 2.0, 1.0))
         .with_lookat(Point3::new(0.0, 0.0, -1.0))
         .with_vup(Point3::new(0.0, 1.0, 0.0))
+        .with_defocus_angle(10.0)
+        .with_focus_dist(3.4)
         .render(&world)
 }

Listing 87: [main.rs] Scene camera with depth-of-field


We get:

Spheres with depth-of-field

Image 22: Spheres with depth-of-field