Limiting the Number of Child Rays
There's one potential problem lurking here. Notice that the ray_color
function is recursive. When will it stop recursing? When it fails to hit anything. In some cases, however, that may be a long time — long enough to blow the stack. To guard against that, let's limit the maximum recursion depth, returning no light contribution at the maximum depth:
diff --git a/src/camera.rs b/src/camera.rs
index 181eb64..2abb78e 100644
--- a/src/camera.rs
+++ b/src/camera.rs
@@ -1,152 +1,166 @@
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,
/// 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,
+ max_depth: 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 with_max_depth(mut self, max_depth: i32) -> Self {
+ self.max_depth = max_depth;
+
+ 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, world);
+ 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 = 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 {
+ 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.0, INFINITY)) {
let direction = random_on_hemisphere(rec.normal);
- return 0.5 * Self::ray_color(Ray::new(rec.p, direction), world);
+ return 0.5 * Self::ray_color(Ray::new(rec.p, direction), depth - 1, world);
}
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 52: [camera.rs] camera::ray_color() with depth limiting
Update the main() function to use this new depth limit:
diff --git a/src/main.rs b/src/main.rs
index 9f08807..a016213 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,16 +1,17 @@
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)
+ .with_max_depth(50)
.render(&world)
}
Listing 53: [main.rs] Using the new ray depth limiting
For this very simple scene we should get basically the same result:

Image 8: Second render of a diffuse sphere with limited bounces