Ray Tracing in One Weekend: Part 6 Quads
Introduction
I have been reading through this book series on Raytracing which walks the reader through the creation of a Raytracer using C++. In this series of articles, I will be going through this book and implementing the lessons in Rust instead and diving deep into the pieces.
This article will admittedly be quite short, all we will really be doing is adding a new primitive — Quads. This brevity to add a new primitive should give us a decent amount of encouragement in how we set up our RayTracer, making it extremely extensible. The foundations we create here for quad primitives can also be very very easily adapted to support triangles — the topology of choice for lots of 3D models.
The following link is the commit in my Github repo that matches the code we will go over.
Quad
Our Quad will be defined by a set of basis vectors (u,v,w) as well as a starting point q, a normal to the quad, a material, and d which represents the distance from the origin to our plane along the normal direction.
pub struct Quad {
q: Point3,
v: Vec3,
u: Vec3,
w: Vec3,
normal: Vec3,
d: f64,
mat: Arc<dyn Material>
}
To create a new quad, we get the normal vector by crossing u and v and turning into a unit vector. Our value d is computed by getting the normal component of our quad origin vector q. Finally, we compute our third basis vector w.
impl Quad {
pub fn new(q: Point3, u: Vec3, v: Vec3, mat: Arc<dyn Material>) -> Self {
let n = cross(u, v);
let normal = unit_vector(n);
let d = dot(normal, q);
let w = n / dot(n, n);
Quad {
q,
u,
v,
w,
mat,
d,
normal
}
}
}
We will now just need to implement hit detection on our quad.
impl Hittable for Quad {
fn hit(&self, ray: &crate::ray::Ray, t_min: f64, t_max: f64) -> Option<hittable::HitRecord> {
let denom = dot(self.normal, ray.direction());
// ray parallel to plane
if f64::abs(denom) < 1e-8 {
return None;
}
let t = (self.d - dot(self.normal, ray.origin())) / denom;
if t < t_min || t > t_max {
return None;
}
let intersection = ray.at(t);
let planar_hit_point = intersection - self.q;
let alpha = dot(self.w, cross(planar_hit_point, self.v));
let beta = dot(self.w, cross(self.u, planar_hit_point));
if alpha < 0.0 || alpha > 1.0 || beta < 0.0 || beta > 1.0 {
return None;
}
let mut rec = HitRecord {
t: t,
p: intersection,
mat: self.mat.clone(),
normal: Default::default(),
front_face: Default::default(),
u: alpha,
v: beta,
};
rec.set_face_normal(ray, self.normal);
Some(rec)
}
}
Step one is to check if our ray is parallel to the plane. If our ray is parallel to the plane we don’t detect a hit. We detect parallelism to the plane by checking perpendicularism to the normal using the dot product — a value of 0 means the vectors are perpendicular.
Step two is to see if the ray intersects an infinitely spanning plane between our incoming ray’s t_min and t_max. Since we know there is SOME intersection between the ray and the plane which spans infinitely. We determine where along our ray we intersect with the plane and check if it is within our bounds.
Finally, we check if the intersection is actually in the bounds of our quad which is a subsection of the infinite plane. We get the point of intersection in global space and then the quad space intersection point. We determine if the intersection is within our actual quad bounds, here alpha is the plane vector in one non-normal basis direction and beta is in the other. A point is within a quad if it falls between 0–1 in both of those vectors.
Rendering
We can create a hittable list of quads very easily like so.
fn quads() {
let mut world = HittableList::new();
let left_red = Lambertian::from_color(Color::new(1.0, 0.2, 0.2));
let back_green = Lambertian::from_color(Color::new(0.2, 1.0, 0.2));
let right_blue = Lambertian::from_color(Color::new(0.2, 0.2, 1.0));
let upper_orange = Lambertian::from_color(Color::new(1.0, 0.5, 0.0));
let lower_teal = Lambertian::from_color(Color::new(0.2, 0.8, 0.8));
world.add(Box::new(Quad::new(Point3::new(-3.0, -2.0, 5.0), Vec3::new(0.0, 0.0, -4.0), Vec3::new(0.0, 4.0, 0.0), Arc::new(left_red))));
world.add(Box::new(Quad::new(Point3::new(-2.0, -2.0, 0.0), Vec3::new(4.0, 0.0, 0.0), Vec3::new(0.0, 4.0, 0.0), Arc::new(back_green))));
world.add(Box::new(Quad::new(Point3::new(3.0, -2.0, 1.0), Vec3::new(0.0, 0.0, 4.0), Vec3::new(0.0, 4.0, 0.0), Arc::new(right_blue))));
world.add(Box::new(Quad::new(Point3::new(-2.0, 3.0, 1.0), Vec3::new(4.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 4.0), Arc::new(upper_orange))));
world.add(Box::new(Quad::new(Point3::new(-2.0, -3.0, 5.0), Vec3::new(4.0, 0.0, 0.0), Vec3::new(0.0, 0.0, -4.0), Arc::new(lower_teal))));
let eye = Point3::new(0.0, 0.0, 9.0);
let lookat = Point3::new(0.0, 0.0, 0.0);
let up = Point3::new(0.0, 1.0, 0.0);
let dist_to_focus = (eye - lookat).length();
let aperture = 0.0;
let camera = Camera::new(IMAGE_WIDTH, IMAGE_HEIGHT, SAMPLES_PER_PIXEL, MAX_DEPTH, eye, lookat, up, 80.0, ASPECT_RATIO, aperture, dist_to_focus);
camera.render(&world);
}
and produce a render like this
Conclusion
In this short and sweet article we showed how to create a new primitive — quads. This was very easy to add into our Raytracer since we built it up very modularly. Checking a quad intersection involves making sure the ray is not parallel to the quad, seeing if the quad falls between t_min and t_max of our ray and — finally — actually checking if the intersection point is within the quad bounds. To implement any other 2D shape, you would really only need to change how the last step is done. For example, to check if an intersection is within a triangle, you would assert 0 < a < 1 && 0< b < 1 && a + b ≤ 1.