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 Pixels with Multiple Samples

For a single pixel composed of multiple samples, we'll select samples from the area surrounding the pixel and average the resulting light (color) values together.

First we'll update the write_color() function to account for the number of samples we use: we need to find the average across all of the samples that we take. To do this, we'll add the full color from each iteration, and then finish with a single division (by the number of samples) at the end, before writing out the color. To ensure that the color components of the final result remain within the proper \( [0,1] \) bounds, we'll add and use a small helper function: interval::clamp(x). 1 2

diff --git a/src/interval.rs b/src/interval.rs
index 509fddf..482d922 100644
--- a/src/interval.rs
+++ b/src/interval.rs
@@ -1,39 +1,43 @@
 #[derive(Debug, Clone, Copy)]
 pub struct Interval {
     pub min: f64,
     pub max: f64,
 }
 
 impl Default for Interval {
     fn default() -> Self {
         Self::EMPTY
     }
 }
 
 impl Interval {
     pub const EMPTY: Self = Self {
         min: f64::INFINITY,
         max: f64::NEG_INFINITY,
     };
 
     pub const UNIVERSE: Self = Self {
         min: f64::NEG_INFINITY,
         max: f64::INFINITY,
     };
 
-    pub fn new(min: f64, max: f64) -> Self {
+    pub const fn new(min: f64, max: f64) -> Self {
         Self { min, max }
     }
 
     pub fn size(&self) -> f64 {
         self.max - self.min
     }
 
     pub fn contains(&self, x: f64) -> bool {
         self.min <= x && x <= self.max
     }
 
     pub fn surrounds(&self, x: f64) -> bool {
         self.min < x && x < self.max
     }
+
+    pub const fn clamp(&self, x: f64) -> f64 {
+        x.clamp(self.min, self.max)
+    }
 }

Listing 43: [interval.rs] The interval::clamp() utility function


Here's the updated write_color() function that incorporates the interval clamping function:

diff --git a/src/color.rs b/src/color.rs
index c645ca2..1615d55 100644
--- a/src/color.rs
+++ b/src/color.rs
@@ -1,15 +1,18 @@
-use crate::vec3::Vec3;
+use crate::prelude::*;
 
 pub type Color = Vec3;
 
 pub fn write_color(mut out: impl std::io::Write, pixel_color: Color) -> std::io::Result<()> {
     let r = pixel_color.x();
     let g = pixel_color.y();
     let b = pixel_color.z();
 
-    let rbyte = (255.999 * r) as i32;
-    let gbyte = (255.999 * g) as i32;
-    let bbyte = (255.999 * b) as i32;
+    // Translate the [0,1] component values to the byte range [0,255].
+    const INTENSITY: Interval = Interval::new(0.000, 0.999);
+    let rbyte = (256.0 * INTENSITY.clamp(r)) as i32;
+    let gbyte = (256.0 * INTENSITY.clamp(g)) as i32;
+    let bbyte = (256.0 * INTENSITY.clamp(b)) as i32;
 
+    // Write out the pixel color components.
     writeln!(out, "{rbyte} {gbyte} {bbyte}")
 }

Listing 44: [color.rs] The multi-sample write_color() function


Now let's update the camera class to define and use a new camera::get_ray(i,j) function, which will generate different samples for each pixel. This function will use a new helper function sample_square() that generates a random sample point within the unit square centered at the origin. We then transform the random sample from this ideal square back to the particular pixel we're currently sampling.

diff --git a/src/camera.rs b/src/camera.rs
index 73dc5cc..f181b03 100644
--- a/src/camera.rs
+++ b/src/camera.rs
@@ -1,110 +1,151 @@
 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,
 
     /// 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,
 }
 
 impl Default for Camera {
     fn default() -> Self {
         Self {
             aspect_ratio: 1.0,
             image_width: 100,
+            samples_per_pixel: 10,
             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(),
         }
     }
 }
 
 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 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 pixel_center = self.pixel00_loc
-                    + (i as f64) * self.pixel_delta_u
-                    + (j as f64) * self.pixel_delta_v;
-                let ray_direction = pixel_center - self.center;
-                let r = Ray::new(self.center, ray_direction);
-
-                let pixel_color = Self::ray_color(r, world);
-                write_color(std::io::stdout(), pixel_color)?;
+                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, 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 = Point3::new(0.0, 0.0, 0.0);
 
         // Determine viewport dimensions.
         let focal_length = 1.0;
         let viewport_height = 2.0;
         let viewport_width =
             viewport_height * (self.image_width as f64) / (self.image_height as f64);
 
         // Calculate the vectors across the horizontal and down the vertical viewport edges.
         let viewport_u = Vec3::new(viewport_width, 0.0, 0.0);
         let viewport_v = Vec3::new(0.0, -viewport_height, 0.0);
 
         // 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 - Vec3::new(0.0, 0.0, focal_length) - viewport_u / 2.0 - viewport_v / 2.0;
         self.pixel00_loc = viewport_upper_left + 0.5 * (self.pixel_delta_u + self.pixel_delta_v);
     }
 
+    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.
+
+        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_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 ray_color(r: Ray, world: &impl Hittable) -> Color {
         if let Some(rec) = world.hit(r, Interval::new(0.0, INFINITY)) {
             return 0.5 * (rec.normal + Color::new(1.0, 1.0, 1.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 45: [camera.rs] Camera with samples-per-pixel parameter


(In addition to the new sample_square() function above, you'll also find the function sample_disk() in the Github source code. This is included in case you'd like to experiment with non-square pixels, but we won't be using it in this book. sample_disk() depends on the function random_in_unit_disk() which is defined later on.)

Main is updated to set the new camera parameter.

diff --git a/src/main.rs b/src/main.rs
index 27377f1..9f08807 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,15 +1,16 @@
 use code::{camera::Camera, hittable_list::HittableList, prelude::*, sphere::Sphere};
 
 fn main() -> std::io::Result<()> {
     let mut world = HittableList::new();
 
     world.add(Rc::new(Sphere::new(Point3::new(0.0, 0.0, -1.0), 0.5)));
     world.add(Rc::new(Sphere::new(Point3::new(0.0, -100.5, -1.0), 100.0)));
 
     env_logger::init();
 
     Camera::default()
         .with_aspect_ratio(16.0 / 9.0)
         .with_image_width(400)
+        .with_samples_per_pixel(100)
         .render(&world)
 }

Listing 46: [main.rs] Setting the new samples-per-pixel parameter


Zooming into the image that is produced, we can see the difference in edge pixels.

Before and after antialiasing

Image 6: Before and after antialiasing



  1. For this purpose, version 1.50 of Rust introduced the f64::clamp(self, min, max) function (the C++17 standard introduced a similar function called std::clamp(v, lo, hi)).

  2. The function is const, meaning it can be used to initialise const variables. This will be demonstrated in the next listing.