Chapter 5: Ray-Sphere Interactions
In chapter 5 of The Ray Tracer Challenge you finally get to implement something that starts to resemble a ray tracer. You implement ray, sphere, and intersection related functions and the exercise at the end ties it all together to create an image.
Finding Intersections
The book does not go into the details of the math for how to determine the intersection points of a ray and sphere. I’m glad for that, but it bugged me that I do not have an intuitive understanding of why this intersect method works.
class Sphere {
intersect(ray: Ray): IntersectionCollection {
// Transform the ray into object space
ray = ray.transform(this.transformInverse)
// Vector from sphere origin to the ray origin
const sphereToRayVec = ray.origin.sub(origin)
// Supporting characters to determine the discriminant and intersection
const a = ray.direction.dot(ray.direction)
const b = 2 * ray.direction.dot(sphereToRayVec)
const c = sphereToRayVec.dot(sphereToRayVec) - 1
// Discriminant does not intersect sphere if it is negative
const discriminant = b ** 2 - 4 * a * c
if (discriminant < 0) return new IntersectionCollection()
// Calculate the intersection points
const sqrtDiscriminant = Math.sqrt(discriminant)
const t1 = (-b - sqrtDiscriminant) / (2 * a)
const t2 = (-b + sqrtDiscriminant) / (2 * a)
return new IntersectionCollection(
new Intersection(t1, this),
new Intersection(t2, this)
)
}
}
The book does suggest some online resources for an explanation of the
math at work. I took some time to read through
this one. It includes two solutions: a geometric solution and an analytic
solution. The geometric solution made sense to me but the analytic
solution less so. Still — despite an error[1]
in that explanation — it did make some sense. One thing that helped
was realizing that the discriminant
being negative means
there is no intersection because that would require taking the square
root of a negative number.
The solution the book provides and I implemented above is the analytic solution. I would have a deeper understanding of it if it were the geometric solution, but at least I now have a better-than-tenuous idea of why this code works.
The Demo: <sphere-shadow>
The exercise at the end of the chapter is to render the shadow of a sphere by casting rays from a light source onto a “wall”. I’ve implemented that here, with the addition that you can change the position of the light source by dragging from the element.
I don’t normally test drive my demo code since it is more exploratory fun than writing code I intend to reuse. But I figured I would want to make use of the dragging interaction again, so I test-drove the creation of a TouchPad class to track all the mouse and touch events on (and off) of the element and emit only the needed move events. At some point I should add keyboard support to it as well.
Other than that there is not much new from the web technology side compared to the previous demo. Rendering of the canvas still happens in a single web worker.
What’s Next
In the demo I included an output to show the render time of the last frame. I get around 5.5ms in Chrome on my Mac Studio with an M2 Max. Firefox gets around 18.5ms and Safari around 9ms. I find this performance a little disappointing considering I feel like I have optimized things as much as I reasonably can. It makes me wonder if I should skip to targeting Web Assembly earlier than I was planning. I would like to keep the demos interactive in a real-time sort of way. Parallelization will help, but only so much on older devices with fewer CPU cores. Maybe now is the time…
-
At the time of writing this the issue with that page is that in the “Analytic Solution” section “equation 5” is a repeat of “equation 4”. It should actually be the quadratic formula:
Or, with the discriminant represented as :
If you decide to dig this deep hopefully the above can save you the intense head scratching that I went through. ↩︎