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.

Image 6: Before and after antialiasing