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

Simplifying the Ray-Sphere Intersection Code

Let’s revisit the ray-sphere function:

use code::{
    color::{Color, write_color},
    ray::Ray,
    vec3::{Point3, Vec3, dot, unit_vector},
};

fn hit_sphere(center: Point3, radius: f64, r: Ray) -> Option<f64> {
    let oc = center - r.origin();
    let a = dot(r.direction(), r.direction());
    let b = -2.0 * dot(r.direction(), oc);
    let c = dot(oc, oc) - radius * radius;
    let discriminant = b * b - 4.0 * a * c;

    (discriminant >= 0.0).then(|| (-b - f64::sqrt(discriminant)) / (2.0 * a))
}

fn ray_color(r: Ray) -> Color {
    if let Some(t) = hit_sphere(Point3::new(0.0, 0.0, -1.0), 0.5, r) {
        let n = unit_vector(r.at(t) - Vec3::new(0.0, 0.0, -1.0));
        return 0.5 * Color::new(n.x() + 1.0, n.y() + 1.0, n.z() + 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)
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Image

    const ASPECT_RATIO: f64 = 16.0 / 9.0;
    const IMAGE_WIDTH: i32 = 400;

    // Calculate the image height, and ensure that it's at least 1.
    const IMAGE_HEIGHT: i32 = {
        let image_height = (IMAGE_WIDTH as f64 / ASPECT_RATIO) as i32;
        if image_height < 1 { 1 } else { image_height }
    };

    // Camera

    let focal_length = 1.0;
    let viewport_height = 2.0;
    let viewport_width = viewport_height * (IMAGE_WIDTH as f64) / (IMAGE_HEIGHT as f64);
    let camera_center = Point3::new(0.0, 0.0, 0.0);

    // 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.
    let pixel_delta_u = viewport_u / IMAGE_WIDTH as f64;
    let pixel_delta_v = viewport_v / IMAGE_HEIGHT as f64;

    // Calculate the location of the upper left pixel.
    let viewport_upper_left =
        camera_center - Vec3::new(0.0, 0.0, focal_length) - viewport_u / 2.0 - viewport_v / 2.0;
    let pixel00_loc = viewport_upper_left + 0.5 * (pixel_delta_u + pixel_delta_v);

    // Render

    env_logger::init();
    println!("P3");
    println!("{IMAGE_WIDTH} {IMAGE_HEIGHT}");
    println!("255");

    for j in 0..IMAGE_HEIGHT {
        log::info!("Scanlines remaining: {}", IMAGE_HEIGHT - j);
        for i in 0..IMAGE_WIDTH {
            let pixel_center =
                pixel00_loc + (i as f64) * pixel_delta_u + (j as f64) * pixel_delta_v;
            let ray_direction = pixel_center - camera_center;
            let r = Ray::new(camera_center, ray_direction);

            let pixel_color = ray_color(r);
            write_color(std::io::stdout(), pixel_color)?;
        }
    }
    log::info!("Done.");

    Ok(())
}

Listing 13: [main.rs] Ray-sphere intersection code (before)


First, recall that a vector dotted with itself is equal to the squared length of that vector.

Second, notice how the equation for \( b \) has a factor of negative two in it. Consider what happens to the quadratic equation if \( b = 2h \):

\[ \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}\] \[= \frac{-(-2h) \pm \sqrt{(-2h)^2 - 4ac}}{2a}\] \[= \frac{2h \pm 2 \sqrt{h^2 - ac}}{2a}\] \[= \frac{h \pm \sqrt{h^2 - ac}}{a}\]

This simplifies nicely, so we'll use it. So solving for \( h \):

\[b = -2 \mathbf{d} \cdot (\mathbf{C} - \mathbf{Q}) \] \[b = -2h \] \[h = \frac{b}{-2} = \mathbf{d} \cdot (\mathbf{C} - \mathbf{Q}) \]

Using these observations, we can now simplify the sphere-intersection code to this:

diff --git a/src/main.rs b/src/main.rs
index 405ca4b..1f26e3d 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,82 +1,82 @@
 use code::{
     color::{Color, write_color},
     ray::Ray,
     vec3::{Point3, Vec3, dot, unit_vector},
 };
 
 fn hit_sphere(center: Point3, radius: f64, r: Ray) -> Option<f64> {
     let oc = center - r.origin();
-    let a = dot(r.direction(), r.direction());
-    let b = -2.0 * dot(r.direction(), oc);
-    let c = dot(oc, oc) - radius * radius;
-    let discriminant = b * b - 4.0 * a * c;
+    let a = r.direction().length_squared();
+    let h = dot(r.direction(), oc);
+    let c = oc.length_squared() - radius * radius;
+    let discriminant = h * h - a * c;
 
-    (discriminant >= 0.0).then(|| (-b - f64::sqrt(discriminant)) / (2.0 * a))
+    (discriminant >= 0.0).then(|| (h - f64::sqrt(discriminant)) / a)
 }
 
 fn ray_color(r: Ray) -> Color {
     if let Some(t) = hit_sphere(Point3::new(0.0, 0.0, -1.0), 0.5, r) {
         let n = unit_vector(r.at(t) - Vec3::new(0.0, 0.0, -1.0));
         return 0.5 * Color::new(n.x() + 1.0, n.y() + 1.0, n.z() + 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)
 }
 
 fn main() -> Result<(), Box<dyn std::error::Error>> {
     // Image
 
     const ASPECT_RATIO: f64 = 16.0 / 9.0;
     const IMAGE_WIDTH: i32 = 400;
 
     // Calculate the image height, and ensure that it's at least 1.
     const IMAGE_HEIGHT: i32 = {
         let image_height = (IMAGE_WIDTH as f64 / ASPECT_RATIO) as i32;
         if image_height < 1 { 1 } else { image_height }
     };
 
     // Camera
 
     let focal_length = 1.0;
     let viewport_height = 2.0;
     let viewport_width = viewport_height * (IMAGE_WIDTH as f64) / (IMAGE_HEIGHT as f64);
     let camera_center = Point3::new(0.0, 0.0, 0.0);
 
     // 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.
     let pixel_delta_u = viewport_u / IMAGE_WIDTH as f64;
     let pixel_delta_v = viewport_v / IMAGE_HEIGHT as f64;
 
     // Calculate the location of the upper left pixel.
     let viewport_upper_left =
         camera_center - Vec3::new(0.0, 0.0, focal_length) - viewport_u / 2.0 - viewport_v / 2.0;
     let pixel00_loc = viewport_upper_left + 0.5 * (pixel_delta_u + pixel_delta_v);
 
     // Render
 
     env_logger::init();
     println!("P3");
     println!("{IMAGE_WIDTH} {IMAGE_HEIGHT}");
     println!("255");
 
     for j in 0..IMAGE_HEIGHT {
         log::info!("Scanlines remaining: {}", IMAGE_HEIGHT - j);
         for i in 0..IMAGE_WIDTH {
             let pixel_center =
                 pixel00_loc + (i as f64) * pixel_delta_u + (j as f64) * pixel_delta_v;
             let ray_direction = pixel_center - camera_center;
             let r = Ray::new(camera_center, ray_direction);
 
             let pixel_color = ray_color(r);
             write_color(std::io::stdout(), pixel_color)?;
         }
     }
     log::info!("Done.");
 
     Ok(())
 }

Listing 14: [main.rs] Ray-sphere intersection code (after)