An Abstraction for Hittable Objects
Now, how about more than one sphere? While it is tempting to have an array of spheres, a very clean solution is to make an “abstract class” for anything a ray might hit, and make both a sphere and a list of spheres just something that can be hit. What that class should be called is something of a quandary — calling it an “object” would be good if not for “object oriented” programming. “Surface” is often used, with the weakness being maybe we will want volumes (fog, clouds, stuff like that). “hittable” emphasizes the member function that unites them. I don’t love any of these, but we'll go with “hittable”.
This hittable
abstract class will have a hit
function that takes in a ray. 1 Most ray tracers have found it convenient to add a valid interval for hits \( t_{min} \) to \( t_{max} \), so the hit only “counts” if \( t_{min} < t < t_{max} \). For the initial rays this is positive \( t \), but as we will see, it can simplify our code to have an interval \( t_{min} \) to \( t_{max} \). One design question is whether to do things like compute the normal if we hit something. We might end up hitting something closer as we do our search, and we will only need the normal of the closest thing. I will go with the simple solution and compute a bundle of stuff I will store in some structure. Here’s the abstract class:
use crate::{
ray::Ray,
vec3::{Point3, Vec3},
};
#[derive(Debug, Default, Clone, Copy)]
pub struct HitRecord {
pub p: Point3,
pub normal: Vec3,
pub t: f64,
}
pub trait Hittable {
fn hit(&self, r: Ray, ray_tmin: f64, ray_tmax: f64) -> Option<HitRecord>;
}
Listing 15: [hittable.rs] The hittable class
And here’s the sphere:
use crate::{
hittable::{HitRecord, Hittable},
ray::Ray,
vec3::{Point3, dot},
};
#[derive(Debug, Clone, Copy)]
pub struct Sphere {
center: Point3,
radius: f64,
}
impl Sphere {
pub fn new(center: Point3, radius: f64) -> Self {
Self {
center,
radius: f64::max(0.0, radius),
}
}
}
impl Hittable for Sphere {
fn hit(&self, r: Ray, ray_tmin: f64, ray_tmax: f64) -> 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 root <= ray_tmin || ray_tmax <= root {
root = (h + sqrtd) / a;
if root <= ray_tmin || ray_tmax <= root {
return None;
}
}
let t = root;
let p = r.at(t);
let rec = HitRecord {
t,
p,
normal: (p - self.center) / self.radius,
};
Some(rec)
}
}
Listing 16: [sphere.rs] The sphere class
(Note here that we use the C++ standard function std::fmax()
, which returns the maximum of the two floating-point arguments. Similarly, we will later use std::fmin()
, which returns the minimum of the two floating-point arguments.) 2