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

A Data Structure to Describe Ray-Object Intersections

The hit_record is to avoid a bunch of arguments so we can stuff whatever info we want in there. You can use arguments instead of an encapsulated type, it’s just a matter of taste. Hittables and materials need to be able to reference the other's type in code so there is some circularity of the references. In C++ we add the line class material; to tell the compiler that material is a class that will be defined later. Since we're just specifying a pointer to the class, the compiler doesn't need to know the details of the class, solving the circular reference issue. 1

diff --git a/src/hittable.rs b/src/hittable.rs
index 1b65b92..000dc1d 100644
--- a/src/hittable.rs
+++ b/src/hittable.rs
@@ -1,27 +1,43 @@
-use crate::prelude::*;
+use crate::{
+    material::{Lambertian, Material},
+    prelude::*,
+};
 
-#[derive(Debug, Default, Clone, Copy)]
+#[derive(Clone)]
 pub struct HitRecord {
     pub p: Point3,
     pub normal: Vec3,
+    pub mat: Rc<dyn Material>,
     pub t: f64,
     pub front_face: bool,
 }
 
+impl Default for HitRecord {
+    fn default() -> Self {
+        Self {
+            p: Default::default(),
+            normal: Default::default(),
+            mat: Rc::new(Lambertian::default()),
+            t: Default::default(),
+            front_face: Default::default(),
+        }
+    }
+}
+
 impl HitRecord {
     pub fn set_face_normal(&mut self, r: Ray, outward_normal: Vec3) {
         // Sets the hit record normal vector.
         // NOTE: the parameter `outward_normal` is assumed to have unit length.
 
         self.front_face = dot(r.direction(), outward_normal) < 0.0;
         self.normal = if self.front_face {
             outward_normal
         } else {
             -outward_normal
         };
     }
 }
 
 pub trait Hittable {
     fn hit(&self, r: Ray, ray_t: Interval) -> Option<HitRecord>;
 }

Listing 59: [hittable.rs] Hit record with added material pointer 2


hit_record is just a way to stuff a bunch of arguments into a class so we can send them as a group. When a ray hits a surface (a particular sphere for example), the material pointer in the hit_record will be set to point at the material pointer the sphere was given when it was set up in main() when we start. When the ray_color() routine gets the hit_record it can call member functions of the material pointer to find out what ray, if any, is scattered.

To achieve this, hit_record needs to be told the material that is assigned to the sphere.

diff --git a/src/sphere.rs b/src/sphere.rs
index a2710b4..2b0026a 100644
--- a/src/sphere.rs
+++ b/src/sphere.rs
@@ -1,56 +1,61 @@
 use crate::{
     hittable::{HitRecord, Hittable},
+    material::{Lambertian, Material},
     prelude::*,
 };
 
-#[derive(Debug, Clone, Copy)]
+#[derive(Clone)]
 pub struct Sphere {
     center: Point3,
     radius: f64,
+    mat: Rc<dyn Material>,
 }
 
 impl Sphere {
     pub fn new(center: Point3, radius: f64) -> Self {
         Self {
             center,
             radius: f64::max(0.0, radius),
+            // TODO: Initialize the material pointer `mat`.
+            mat: Rc::new(Lambertian::default()),
         }
     }
 }
 
 impl Hittable for Sphere {
     fn hit(&self, r: Ray, ray_t: Interval) -> Option<HitRecord> {
         let oc = self.center - r.origin();
         let a = r.direction().length_squared();
         let h = dot(r.direction(), oc);
         let c = oc.length_squared() - self.radius * self.radius;
 
         let discriminant = h * h - a * c;
         if discriminant < 0.0 {
             return None;
         }
 
         let sqrtd = f64::sqrt(discriminant);
 
         // Find the nearest root that lies in the acceptable range.
         let mut root = (h - sqrtd) / a;
         if !ray_t.surrounds(root) {
             root = (h + sqrtd) / a;
             if !ray_t.surrounds(root) {
                 return None;
             }
         }
 
         let t = root;
         let p = r.at(t);
         let mut rec = HitRecord {
             t,
             p,
+            mat: self.mat.clone(),
             ..Default::default()
         };
         let outward_normal = (p - self.center) / self.radius;
         rec.set_face_normal(r, outward_normal);
 
         Some(rec)
     }
 }

Listing 60: [sphere.rs] Ray-sphere intersection with added material information



  1. In Rust traits needs to be imported using the use keyword. Circular references are not a problem in Rust, since the use keyword does not simply copy the header file in comparison to the C++ include macro.

  2. HitRecord can no longer derive Default since Rc<dyn Material> can not implement Default. Instead, the default material is set to a Lambertian.