Chapter 4 - Adding a Sphere
I'd previously made the decision to move the color function into the
Ray class simply because a ray's color can be expected to be constant
for a given scene, and I envision all this code running in the context
of a constant scene. Chapter 4 modifies Ray's color function to
call an additional check called hit_sphere, so I've baked that
function into the Ray class as well. This clearly isn't sustainable,
since binding scene logic to the Ray class is madness. But for now,
it's great, and I'll change it when need be.
The modification to color is straightforward: if the ray would hit a
sphere centered at {0, 0, -1} with a radius of 0.5, return red.
Otherwise, compute the value for the blue/white gradient we defined in
Chapter 3.
function Ray:color()
   if (self:hit_sphere(Vector3:new{0, 0, -1}, 0.5)) then
      return Vector3:new{1, 0, 0}
   end
   unit_direction = self:direction():unit_vector()
   t = 0.5 * (unit_direction:y() + 1)
   return Vector3:new{1, 1, 1} * (1 - t) + Vector3:new{0.5, 0.7, 1} * t
end
Next, we need to define hit_sphere, which is responsible for whether
or not the Ray intersects the sphere.
The original text has a great explanation on how to approach this for
implementing a numerical method in a ray tracer. If you consider a
sphere centered at {cx, cy, cz} with a radius R, the text leverages
the insight that points on that sphere must satisfy
(x-cx)² + (y-cy)² + (z-cz))² = R², where the point {x,y,z} is the point under consideration.  This equation is simply subtracting the position of the center of the sphere from the candidate point prior to checking the if the point is the correct distance from the center.
The equation does look a bit busy, though, because it is a scalar equation.  If we instead leverage our vector-based abstractions, it should be as simple as our intuition. To accomplish this, the text observes that, if we let c represent the center of the sphere, point p is on the sphere if (p-c) · (p-c) = R². Here, we simply lean on the dot-product to bury the details of how each dimension (x, y, and z) are calculated.
This is a useful insight, but we need to somehow map its notion of p onto our notion of a Ray, which takes a parameter, t. If we simply substitute p(t) in for p, and then utilize p(t) = A + tB, we obtain (A + tB - C) · (A + tB - C)) = R².
-- Parameters:
--   center: Vector3
--   radius: number
-- Returns: boolean
--
-- Returns true iff this Ray intersects the sphere specified
-- by the provided center and radius, otherwise false.
function Ray:hit_sphere(center, radius)
   offset = self:origin() - center
   a = self:direction():dot(self:direction())
   b = offset:dot(self:direction()) * 2.0
   c = offset:dot(offset) - radius * radius
   discriminant = b * b - a * c * 4
   return discriminant > 0
end
Running this, we obtain the expected result: a red ball in the middle of the screen!