Ray-Sphere Intersection
Reading time: 18 mins.Intersecting a ray with a sphere is the simplest form of a ray-geometry intersection test, which is why many ray tracers showcase images of spheres. Its simplicity also lends to its speed. However, to ensure it works reliably, there are always a few important subtleties to pay attention to.
This test can be implemented using two methods. The first solves the problem using geometry. The second technique, often the preferred solution (because it can be reused for a wider variety of surfaces called quadric surfaces), utilizes an analytic (or algebraic, e.g., can be resolved using a closed-form expression) solution.
Geometric Solution
The geometric solution to the ray-sphere intersection test relies on simple math, mainly geometry, trigonometry, and the Pythagorean theorem. If you look at Figure 1, you will understand that to find the positions of the points P and P', which correspond to the points where the ray intersects the sphere, we need to find values for

Remember that a ray can be expressed using the following parametric form:
Where
We will start by noting that the triangle formed by the edges

We know
In other words, the dot product of
There is a second right triangle in this construction, defined by
Replacing the opposite side, adjacent side, and hypotenuse respectively with
Note that if
In the last paragraph of this section, we will demonstrate how to implement this algorithm in C++ and discuss a few optimizations to expedite the process.
Analytic Solution
Recall that a ray can be expressed using the function:
The concept behind solving the ray-sphere intersection test is that spheres can also be defined using an algebraic form. The equation for a sphere is:
Where
This equation is characteristic of what we call in mathematics, and computer graphics, an implicit function. A sphere expressed in this form is also known as an implicit shape or surface. Implicit shapes are those defined not by connected polygons (as you might be familiar with if you've modeled objects in a 3D application such as Maya or Blender) but simply in terms of equations. Many shapes (often quite simple) can be defined as functions (cubes, cones, spheres, etc.). Despite their simplicity, these shapes can be combined to create more complex forms. This is the idea behind geometry modeling using blobs, for instance (blobby surfaces are also called metaballs). But before we digress too much, let's return to the ray-sphere intersection test (check the advanced section for a lesson on Implicit Modeling).
All we need to do now is substitute equation 1 into equation 2, that is, replace
When we expand this equation, we get (equation 3):
This is essentially an equation of the form (equation 4):
with
-
When
, there are two roots, which can be calculated with: In this case, the ray intersects the sphere at two points ( and ). -
When
, there is one root, which can be calculated with: The ray intersects the sphere at one point only ( ). -
When
, there is no real solution, indicating that the ray does not intersect the sphere.
Since we have

Before we explore how to implement this algorithm in C++, let's see how we can solve the same problem when the sphere is not centered at the origin. Then, we can rewrite equation 2 as:
Where
This gives us the following:
In a more intuitive form, this means that we can translate the ray by
"Why
Computing the Intersection Point
Once we have the value for
Vec3f Phit = ray.orig + ray.dir * t_0;
Computing the Normal at the Intersection Point

There are various methods to calculate the normal of a point lying on the surface of an implicit shape. However, as mentioned in the first chapter of this lesson, one method involves differential geometry, which is mathematically quite complex. Thus, we will opt for a much simpler approach. For instance, the normal of a point on a sphere can be computed as the position of the point minus the sphere center (it's essential to normalize the resulting vector):
Vec3f Nhit = normalize(Phit - sphere.center);
Computing the Texture Coordinates at the Intersection Point
Interestingly, texture coordinates are essentially the spherical coordinates of the point on the sphere, remapped to the range [0, 1]. Recalling the previous chapter and the lesson on Geometry, the Cartesian coordinates of a point can be derived from its spherical coordinates as follows:
These equations may vary if a different convention is used. The spherical coordinates
Where
Implementing the Ray-Sphere Intersection Test in C++
Let's explore how we can implement the ray-sphere intersection test using the analytic solution. Directly applying equation 5 is feasible (and it will work) to calculate the roots. However, due to the finite precision with which real numbers are represented on computers, this formula can suffer from a loss of significance. This issue arises when
Where
bool solveQuadratic(const float &a, const float &b, const float &c, float &x0, float &x1) { float discr = b * b - 4 * a * c; if (discr < 0) return false; else if (discr == 0) x0 = x1 = -0.5 * b / a; else { float q = (b > 0) ? -0.5 * (b + sqrt(discr)) : -0.5 * (b - sqrt(discr)); x0 = q / a; x1 = c / q; } if (x0 > x1) std::swap(x0, x1); return true; }
Now, here is the complete code for the ray-sphere intersection test. For the geometric solution, we noted that we could reject the ray early if
bool intersect(const Ray &ray) const { float t0, t1; // Solutions for t if the ray intersects the sphere #if 0 // Geometric solution Vec3f L = center - ray.orig; float tca = L.dotProduct(ray.dir); // if (tca < 0) return false; float d2 = L.dotProduct(L) - tca * tca; if (d2 > radius * radius) return false; float thc = sqrt(radius * radius - d2); t0 = tca - thc; t1 = tca + thc; #else // Analytic solution Vec3f L = ray.orig - center; float a = ray.dir.dotProduct(ray.dir); float b = 2 * ray.dir.dotProduct(L); float c = L.dotProduct(L) - radius * radius; if (!solveQuadratic(a, b, c, t0, t1)) return false; #endif if (t0 > t1) std::swap(t0, t1); if (t0 < 0) { t0 = t1; // If t0 is negative, let's use t1 instead. if (t0 < 0) return false; // Both t0 and t1 are negative. } t = t0; return true; }
Note: If the scene contains multiple spheres, they are tested in the order they were added to the scene, not necessarily sorted by depth relative to the camera position. The correct approach is to track the sphere with the closest intersection distance, i.e., the smallest

Let's now see how we can implement the ray-sphere intersection test using the analytic solution. We could use equation 5 directly (you can implement it, and it will work) to calculate the roots. Still, on computers, we have a limited capacity to represent real numbers with the precision needed to calculate these roots as accurately as possible. Thus the formula suffers from the effect of a loss of significance. This happens when b and the root of the discriminant don't have the same sign but have values very close to each other. Because of the limited numbers used to represent floating numbers on the computer, in that particular case, the numbers would either cancel out when they shouldn't (this is called catastrophic cancellation) or round off to an unacceptable error (you will easily find more information related to this topic on the internet). However, equation 5 can easily be replaced with a slightly different equation that proves more stable when implemented on computers. We will use instead:
Where the sign is -1 when b is lower than 0 and 1 otherwise, this formula ensures that the quantities added for q have the same sign, avoiding catastrophic cancellation. Here is how the routine looks in C++:
bool solveQuadratic(const float &a, const float &b, const float &c, float &x0, float &x1) { float discr = b * b - 4 * a * c; if (discr < 0) return false; else if (discr == 0) x0 = x1 = - 0.5 * b / a; else { float q = (b > 0) ? -0.5 * (b + sqrt(discr)) : -0.5 * (b - sqrt(discr)); x0 = q / a; x1 = c / q; } if (x0 > x1) std::swap(x0, x1); return true; }
Finally, here is the completed code for the ray-sphere intersection test. For the geometric solution, we have mentioned that we can reject the ray early on if
bool intersect(const Ray &ray) const { float t0, t1; // solutions for t if the ray intersects #if 0 // Geometric solution Vec3f L = center - ray.orig; float tca = L.dotProduct(ray.dir); // if (tca < 0) return false; float d2 = L.dotProduct(L) - tca * tca; if (d2 > radius * radius) return false; float thc = sqrt(radius * radius - d2); t0 = tca - thc; t1 = tca + thc; #else // Analytic solution Vec3f L = ray.orig - center; float a = ray.dir.dotProduct(ray.dir); float b = 2 * ray.dir.dotProduct(L); float c = L.dotProduct(L) - radius * radius; if (!solveQuadratic(a, b, c, t0, t1)) return false; #endif if (t0 > t1) std::swap(t0, t1); if (t0 < 0) { t0 = t1; // If t0 is negative, let's use t1 instead. if (t0 < 0) return false; // Both t0 and t1 are negative. } t = t0; return true; }
Note that if the scene contains more than one sphere, the spheres are tested for any given ray in the order they were added to the scene. The spheres are thus unlikely to be sorted in depth (with respect to the camera position). The solution to this problem is to keep track of the sphere with the closest intersection distance, in other words, with the closest
