Skip to main content.

Ray/cone intersections

There are a lot of people out on the web who are willing to share raycasting code, but not a whole lot of them bother to actually apply a permissive license to it so that people can legally use it. What's more, a lot of them don't explain how they actually derived the math involved either.

Here, I'm going to walk through how to put together a ray/cone intersection test. I've also provided code at the bottom as a public domain work.

Parametric surface of a cone

The parametric equations (that is, equations with parameters) for a cone are as follows:

Ch
=
height of the cone
Cr
=
radius of the cone's base
Ho
=
height offset on the range [0, Ch], where 0 is aligned with the cone's base
θ
=
any angle, basically
[Equation 1:] fx( Ho , θ ) = ( Ch - Ho ) ÷ Ch × Cr × cos(θ) fy( Ho , θ ) = ( Ch - Ho ) ÷ Ch × Cr × sin(θ) fz( Ho , θ ) = Ho

Technically, these equations describe an infinite cone — one with no endcap, which extends forever in both directions. (Yes, both. When you go above the tip, the cone turns inside out and starts widening again.) However, we can still use them to put together the tests for a finite cone with an endcap.

Implicit surface of a cone

At the top of this page, we define a cone as a parametric surface — that is, we have three parametric equations (equations that take parameters) which define all points on the surface of the cone. However, if we can come up with an implicit surface — that is, a surface defined by a single equation, wherein x, y, and z are on one side, and 0 is on the other side — then we can use vector math to deal with the cone more easily.

We know from our exploration above that at the cone's base, a point lies on the cone's surface (the edge of the base) if Equation 3 is true. We can represent that equation in terms of any arbitrary 2D vector P:

[Equation 4:] ||P|| = Cr

We also know that as we move further along the cone's height, to its tip, the radius shrinks proportional to the cone's height. Therefore:

x squared  + y squared  = Cr ÷ ( Ch - z )

And of course, any equation of the form "a = b" is implicitly also "a - b = 0".

Handling a translated and rotated cone

The cone's local z-axis is its main axis, while its local x- and y-axes are its cross axes. The above relationship establishes the distance that any point on the cone's surface must have along the cross axes, given the point's distance from the base along the main axis.

Let's define some variables real quick:

Ct
=
top vertex of the cone
Cb
=
centerpoint of the cone's base
Cs
=
"spine" of the cone: a vector from the tip to the base, equal to Ct - Cb
Ca
=
axis of the cone: normalized spine
Hp
=
hit position: a point that we presume is on the cone's curved surface
Hs
=
hit spine position: the hit position projected onto Cs; this position = ((Hp - Cb) • Ca)Ca + Cb
Ho
=
the height offset from our cone equations; this is the distance from Hs to Cb i.e. ||Hs - Cb||.

Hs is what we get when we project the hit position Hp onto the cone's axis — its "spine." In other words, we have flattened the cone from 3D into 2D, and are looking at it from the spine: this is the circle graph from above. Hs is the nearest point on the cone's "spine" to Hp, so the distance between the two points is the distance from the hit position to the "spine" as a whole, and therefore also the 2D distance from the hit position to the circle's (cone's) center.

We can use two equations to check whether the hit position is on the outer edge of the cone's base. The first equation is a check between the distance and the radius; recall Equations 3 and 4. The second equation constrains this check to 2D, by requiring that there be no distance between the projected hit position and the plane we've projected it onto (otherwise it would be a point/sphere test).

||Hp - Hs|| = Cr ||Hp - Cb|| = 0

Meanwhile the distance from Hs to the center of the cone's base, Cb, is the height offset: the radius, the distance, that we want to check will be proportional to this. Recall Equation 2, which established the exact relationship between the height offset and radius; we can substitute this into our equation to go from 2D to 3D:

||Hp - Hs|| = ( ( Ch - Ho ) ÷ Ch ) Cr

We can double-check this with some simple mental tests.

  • If our hit position is at the bottom of the cone, then Ho is zero. The righthand side resolves to a division of the cylinder's height by itself, yielding 1, which is multiplied by the cylinder's radius. Ultimately, this resolves out to a 2D radius check, as expected.
  • If our hit position is at the top of the cone, then Ho = Ch. The righthand side's numerator comes out to zero, so the radius at the top of the cone is zero: a single point. This, too, is as expected.
  • If our hit position is halfway along the cone's height, then the numerator on the righthand side computes to half of the cone's height. Dividing that by the cylinder's height gives us 0.5, so we end up comparing to half of the cone's radius at its base, as expected.

Putting content on the righthand side in terms of vectors (except for the height, because leaving that as-is will make things easier later on):

||Hp - Hs|| = ( ||Ct - Hs|| ÷ Ch ) Cr

Next, we'll divide both sides by ||Ct - Hs|| so that Cr is the lone numerator on the right. (Mathematical notation sort of obscures this fact, visually, but on the righthand side, because Cr is multiplied by the entire fraction, it is effectively part of the righthand-side numerator. We can divide by the rest of the righthand-side numerator to get the radius on its own.)

||Hp - Hs|| ÷ ||Ct - Hs|| = Cr ÷ Ch

Let's split up that lefthand-side fraction.

||Hp - Hs|| = Cr ÷ Ch||Ct - Hs||

This will be a little easier if we separate the cone's height-to-radius ratio out into a different variable, and if we square our equations. Let's do both at the same time:

Cq
=
cone ratio squared
Cq = Cr squared  ÷ Ch squared  [Equation 5:] ||Hp - Hs|| squared  = Cq × ||Ct - Hs|| squared 

I think our new Equation 5 would, technically, be enough to generate a ray/cone intersection solution. However, it's a bit messy: the lefthand side uses two points, one of which is the point we want to solve for (a raycast hit position) and the other of which is itself defined in terms of the point we want to solve for. We should try to fix that — make it so that one side of the equation contains only one unknown point (the hit position), possibly alongside other known points (i.e. points that "belong to" the cone).

Right now, the equation works in terms of three points in space: Hp, Hs, and Ct. These points form a triangle, and our equation describes the lengths of two edges: Hp to Hs, and Ct to Hs. We therefore need to find some property that is inherent to all triangles, that we can use to rearrange our equation; we need one side of the equation to be the Hp to Ct edge.

Let's break out the handy-dandy Pythagorean theorem, then, which describes the relationship between the lengths of a triangle's edges:

a squared  + b squared  = c squared 

And therefore:

c squared  - b squared  = a squared 

We can redefine ||Hp - Hs|| squared , which takes the role of c squared  in the Pythagorean theorem:

||Hp - Hs|| squared  = ||Hp - Ct|| squared  - ||Ct - Hs|| squared 

Let's substitute this whole formula in, replacing ||Hp to Hs|| squared :

||Hp - Ct|| squared  - ||Ct - Hs|| squared  = Cq × ||Ct - Hs|| squared 

We can add ||Hp to Hs|| squared  to both sides, to reduce the lefthand side down to just one length:

||Hp - Ct|| squared  = Cq × ||Ct - Hs|| squared  + ||Ct - Hs|| squared  [Equation 6:] ||Hp - Ct|| squared  = (Cq + 1) × ||Ct - Hs|| squared 

This equation will lead to a cleaner solution. If we substitute the equation for a ray in place of Hp, the ray hit position, then we can solve for Hp to compute that hit position given any ray. However, there's still more cleanup we can do. Let's also try to replace Hs with something in terms of Hp. Recall that Hs is the projection of Hp onto the axis Ca.

||Ct - Hs|| squared  = ||Ct - (HpCa)Ca|| squared  ||Ct - Hs|| = ||Ct - (HpCa)Ca||

Remember that Hs is the hit position projected onto the cylinder's "spine." To compute that, we start with Hp - Cb, to get the hit position relative to the cylinder's base. We then project that onto the cylinder's axis vector:

((Hp - Cb) • Ca) × Ca

And then finally, we add the cylinder's base position back in, to move us back to world-relative coordinates:

Hs = Cb + ((Hp - Cb) • Ca) × Ca

With that in mind, let's see if we can simplify the righthand side of Equation 6. Let's start by expanding Hs:

||Ct - Hs|| squared  = ||Ct - (Cb + ((Hp - Cb) • Ca)Ca)|| squared 

Now, if we think about it, the cylinder's tip is really just the cylinder's base, plus the cylinder's height times the cylinder's axis:

||Ct - Hs|| squared  = ||(Cb + CaCh) - (Cb + ((Hp - Cb) • Ca)Ca)|| squared 

Within the righthand side, we can strip out the additions of the base on both sides. (The righthand side is taking the distance between two points; removing those additions means translating both points by the same vector, which has no effect on the distance between them and therefore is a valid way to simplify the equation.)

||Ct - Hs|| squared  = ||(CaCh) - ((Hp - Cb) • Ca)Ca|| squared 

We can also drop the multiplications by Ca. Those multiplications were of lengths by an axis vector, to scale along that axis; dropping the multiplications means that we're working with raw lengths, so we go from a ||length operator|| to parentheses:

||Ct - Hs|| squared  = (Ch - (Hp - Cb) • Ca) squared 

Finally, we can flip the righthand side: instead of measuring the projected distance from Hp to the bottom of the cylinder and then subtracting that from the cylinder's height, we can instead measure the projected distance from Hp to the top of the cylinder.

||Ct - Hs|| squared  = ((Hp - Ct)Ca) squared 

Our final cone equation, substituting out ||Ct - Hs|| squared  from Equation 6, is as follows:

[Equation 7:] ||Hp - Ct|| squared  = (Cq + 1) × ((Hp - Ct) • Ca) squared 

Keep in mind that this equation only includes the variable for the cone's tip, not the cone's base. We have effectively removed most of our vectors from the equation, and so just like with the parametric equations, this implicit equation tests whether a point lies on the curved outer surface of an infinitely tall cone anchored at its tip. That's fine, though. Testing a finite cone just involves a distance check between the point and the tip; and testing against the cone's flat endcap involves a point/disc intersection check. Our equation here tests the "unique" part of the cone.

Solving for the ray

A ray is commonly defined in terms of a starting point, or origin, and a normalized direction vector. We can therefore define the hit position in terms of the same.

Ro
=
the ray's origin
Rd
=
the ray's direction
Hp
=
the position at which the ray hits some surface
Hd
=
the hit distance: the distance from the ray origin to the hit position
Hp = Ro + RdHd

We can substitute this equation into our cone equation above, and then attempt to solve for the hit distance Hd:

||Ro + RdHd - Ct|| squared  = (Cq + 1) × ((Ro + RdHd - Ct) • Ca) squared 

On both sides of the equation, we subtract Ct from Ro. Let's simplify the equation a bit by defining that difference as a vector:

Rl
=
the ray's cone-tip-relative origin = Ro - Ct
||Rl + RdHd|| squared  = (Cq + 1) × ((Rl + RdHd) • Ca) squared 

The squared length of a vector is equal to the dot product of the vector with itself; we can use that to expand the lefthand side:

(Rl + RdHd) • (Rl + RdHd) = (Cq + 1) × ((Rl + RdHd) • Ca) squared 

We can also expand the righthand side if we keep in mind that dot products obey FOIL. The squared parentheses on the righthand side only use three terms, nor four, but we can just add a zero term and then use FOIL.

Vz
=
the vector (0, 0, 0)
(Rl + RdHd) • Ca = (Rl + RdHd) • (Ca + Vz) (Rl + RdHd) • (Ca + Vz) = RlCa + RlVz + RdHdCa + RdHdVz (Rl + RdHd) • (Ca + Vz) = RlCa + RdHdCa

So the ray-and-cone equation, with the righthand side expanded, is:

(Rl + RdHd) • (Rl + RdHd) = (Cq + 1) × (RlCa + RdHdCa) squared 

We can expand the dot product on the lefthand side using FOIL as well:

RlRl + RlRdHd + RdHdRl + RdHdRdHd = (Cq + 1) × (RlCa + RdHdCa) squared  (RlRl) + 2(RlRdHd) + (RdHd) • (RdHd) = (Cq + 1) × (RlCa + RdHdCa) squared 

We can expand the squaring on the righthand side as well:

(a + b) squared  = a squared  + 2ab + b squared  (RlRl) + 2(RlRdHd) + (RdHd) • (RdHd) = (Cq + 1) × ((RlCa) squared  + 2(RlCa)(RdHdCa) + (RdHdCa) squared )

And finally: for any vector V and scalar s:

(sV) • (sV) = (VV)s squared  (RlRl) + 2(RlRdHd) + (RdRd)Hd squared  = (Cq + 1) × ((RlCa) squared  + 2(RlCa)(RdHdCa) + (RdHdCa) squared )

Now, let's group them based on Hd. We'll start by subtracting one side of the equation from the other:

(RlRl) + 2(RlRdHd) + (RdRd)Hd squared  - (Cq + 1) × ((RlCa) squared  + 2(RlCa)(RdHdCa) + (RdHdCa) squared ) = 0

Next, we'll unpack the multiplication by (Cq + 1), and then merge the groups that are both multiplied by 2:

(RlRl) + 2(RlRdHd) + (RdRd)Hd squared  - (Cq + 1)(RlCa) squared  - 2(Cq + 1)(RlCa)(RdHdCa) - (Cq + 1)(RdHdCa) squared  = 0 (RlRl) + 2((RlRdHd) - (Cq + 1)(RlCa)(RdHdCa)) + (RdRd)Hd squared  - (Cq + 1)(RlCa) squared  - (Cq + 1)(RdHdCa) squared  = 0

Next, we want to move Hd outside of some of the dot products. Multiplying a vector by a scalar is the same as taking the dot product of that vector with another vector whose terms are all that same scalar; and the dot product between two vectors is commutative: you can swap the lefthand and righthand sides around and get the same result; therefore, for any two vectors A and B, and a scalar c,

cAB = (AB)c (b × AB) squared  = (AB) squared  × b squared  (RlRl) + 2((RlRd)Hd - (Cq + 1)(RlCa)(RdCa)Hd) + (RdRd)Hd squared  - (Cq + 1)(RlCa) squared  - (Cq + 1)(RdCa) squared Hd squared  = 0
info
(RdRd)Hd squared  - (Cq + 1)(RdCa) squared Hd squared  + 2((RlRd)Hd - (Cq + 1)(RlCa)(RdCa)Hd) - (Cq + 1)(RlCa) squared  + (RlRl) = 0
info
((RdRd) - (Cq + 1)(RdCa) squared )Hd squared  + 2((RlRd)Hd - (Cq + 1)(RlCa)(RdCa)Hd) - (Cq + 1)(RlCa) squared  + (RlRl) = 0
((RdRd) - (Cq + 1)(RdCa) squared )Hd squared  + 2((RlRd) - (Cq + 1)(RlCa)(RdCa))Hd - (Cq + 1)(RlCa) squared  + (RlRl) = 0
info
((RdRd) - (Cq + 1)(RdCa) squared )Hd squared  + 2((RlRd) - (Cq + 1)(RlCa)(RdCa))Hd + (RlRl) - (Cq + 1)(RlCa) squared  = 0

This is a quadratic equation. Here — I'll mark the different parts in different colors and show you.

ax squared  + bx + c

If Hd is the "x" in our quadratic equation, then...

((RdRd) - (Cq + 1)(RdCa) squared )Hd squared  + 2((RlRd) - (Cq + 1)(RlCa)(RdCa))Hd + (RlRl) - (Cq + 1)(RlCa) squared  = 0
a
=
(RdRd) - (Cq + 1)(RdCa) squared 
b
=
2((RlRd) - (Cq + 1)(RlCa)(RdCa))
c
=
(RlRl) - (Cq + 1)(RlCa) squared 

And of course, per the quadratic formula, the quadratic equation is true whenever Hd is defined as follows:

Hd = (-b ± √(b squared  - 4ac)) ÷ (2a)

The discrminant can be used to check how many solutions exist — that is, whether the ray intersects the cone, and if so, how many times — and is defined as follows:

b squared  - 4ac

If the discriminant is negative, then no solutions exist; there is no intersection. If the discriminant is positive, then there are two intersections. If the discriminant is zero, then there is only one intersection; if you compute the two intersection points (differing based on ± the square root of the discriminant), then they will be identical.

Recall that for any intersection, Hp = Ro + RdHd. Our process above tests for an intersection against an infinite cone, so we now need to constrain it.

The following expression will be true if a hit position is on the finite cone. We are taking the hit position's distance to the cone's tip, projecting that onto the cone's axis to get the hit position's cone-relative height offset, and testing that projected distance against the cone's height.

0 ≤ (Ct - Hp) • Ca||Ct - Cb||

The following expression will be true if the hit position is below the finite cone. In that case, you should test for a ray/disc intersection with the cone's endcap.

(Ct - Hp) • Ca > ||Ct - Cb||

The following expression will be true if the hit position is above the finite cone. In this situation, you have to ignore the hit position. Remember that odd thing I pointed out at the start: our equations don't describe just one cone. The cone isn't bounded on either side, so if you go above the tip, the cone turns inside out and begins to widen again. Essentially, the equations describe something akin to an hourglass shape; so, we have to ignore everything above the tip where the normal cone and the inside-out cone meet.

0 > (Ct - Hp) • Ca

Lastly, we also have to ignore any hit positions that result from a negative hit distance Hd, as these represent points behind the ray.

0 ≤ Hd

Free code!

The following C++ code is licensed under CC0 (full legal text / summary), and so is effectively a public domain work. You can incorporate it into your programs without the need for attribution, payment, or similar. (That said, linking back here would be polite!)

This code makes use of the GLM library for vector math. It has transitive dependencies on some of my other math, in the form of ray_disc_intersection and, through it, ray_plane_intersection.

//
// Given coefficients in a quadratic equation, this function gives you the roots 
// and returns the number of roots. If there is only one root, then both root 
// variables are set to the same value.
//
int quadratic_roots(
   const float a,
   const float b,
   const float c,
   //
   float& root_lower,
   float& root_upper
) {
   constexpr auto EPSILON = 1e-8;

   float discriminant = (b * b) - (4.0 * a * c);
   if (discriminant > EPSILON) {
      float b_term = b < EPSILON ? -b + sqrt(discriminant) : -b - sqrt(discriminant);

      root_lower = b_term / (2.0 * a); // quadratic formula
      root_upper = (2.0 * c) / b_term; // citardauq formula

      if (root_lower > root_upper)
         std::swap(root_lower, root_upper); // use of both formulae, plus this, avoids catastrophic cancellation due to floating-point limits

      return 2;
   } else if (discriminant > -EPSILON && discriminant <= EPSILON) {
      root_lower = -(b / 2.0 * a);
      root_upper = root_lower;
      return 1;
   }
   root_lower = NAN;
   root_upper = NAN;
   return 0;
}
   
bool ray_cone_intersection(
   const glm::vec3& ray_origin,
   const glm::vec3& ray_direction, // must be normalized
   
   const glm::vec3& cone_tip,
   const glm::vec3& cone_base_centerpoint,
   float cone_base_radius,

   const bool hits_from_inside_count,
   
   float& out_hit_distance
) {
   //
   // First, identify intersections between a line and an infinite cone. An infinite 
   // cone has no base and extends in both directions -- downward from the tip as you 
   // would expect, but also inside-out upward from the tip.
   //
   glm::vec3 Rl = ray_origin - cone_tip;            // Ray origin local to centerpoint
   glm::vec3 Cs = cone_tip - cone_base_centerpoint; // Cone spine
   float     Ch = glm::length(Cs);                  // Cone height
   glm::vec3 Ca = Cs / Ch;                          // Cone axis
   float     Cq = (cone_base_radius * cone_base_radius) / (Ch * Ch); // Cone ratio

   auto Rd_dot_Ca = glm::dot(ray_direction, Ca);
   auto Rl_dot_Rl = glm::dot(Rl, Rl);
   auto Rl_dot_Ca = glm::dot(Rl, Ca);

   float a = glm::dot(ray_direction, ray_direction) - (Cq + 1) * (Rd_dot_Ca * Rd_dot_Ca);
   float b = 2 * (glm::dot(ray_direction, Rl) - (Cq + 1) * Rl_dot_Ca * Rd_dot_Ca);
   float c = Rl_dot_Rl - (Cq + 1) * Rl_dot_Ca * Rl_dot_Ca;

   float hit_near;
   float hit_away;
   auto  count = quadratic_roots(a, b, c, hit_near, hit_away);
   if (count == 0) {
      //
      // There is no intersection between a line (i.e. a "double-sided" ray) and the 
      // infinite cone that matches our finite cone. This means that we cannot be 
      // hitting any part of the cone: if we were hitting the base from the inside, 
      // for example, then the "back of our ray" would be hitting the upper part of 
      // the infinite cone.
      //
      return false;
   }
   //
   // Now, we need to take our intersection points and ensure that they lie on the 
   // surface of a finite cone. If one of them is below the finite cone, then we need 
   // to check for a valid intersection with the cone's base.
   //
   if (count > 2) {
      std::unreachable(); // requires C++23 or a polyfill
   }
   bool  valid1 = true;
   bool  valid2 = true;
   //
   glm::vec3 Hp1 = ray_origin + ray_direction * hit_near;
   glm::vec3 Hp2 = ray_origin + ray_direction * hit_away;
   float     Ho1 = glm::dot(cone_tip - Hp1, Ca); // height offset
   float     Ho2 = glm::dot(cone_tip - Hp2, Ca);
   //
   int valid_count = count;
   if (hit_near < 0.0 || Ho1 < 1.0e-8 || Ho1 > Ch) {
      valid1 = false;
      --valid_count;
   }
   if (hit_away < 0.0 || Ho2 < 1.0e-8 || Ho2 > Ch) {
      valid2 = false;
      if (count > 1)
         --valid_count;
   }
   //
   if (valid_count == 0) {
      if (hits_from_inside_count) {
         //
         // The ray never hits the bounded cone's curved surface. If it originates 
         // from inside the cone and points "downward," however, it could still hit 
         // the cone's endcap from inside.
         //
         if (ray_disc_intersection(ray_origin, ray_direction, cone_base_centerpoint, Ca, cone_base_radius, hit_away)) {
            out_hit_distance = hit_away;
            return true;
         }
      }
      return false;
   }
   if (valid_count == 1) {
      //
      // The ray hits the cone's curved surface only once. This can only happen under 
      // two cases: the ray originates from inside the cone, and points outward; or 
      // the ray passes through the bounded cone once and through the cone's endcap.
      //
      if (valid2) {
         Hp1 = Hp2;
         Ho1 = Ho2;
         valid1   = true;
         hit_near = hit_away;
      }
      valid2 = ray_disc_intersection(ray_origin, ray_direction, cone_base_centerpoint, Ca, cone_base_radius, hit_away);
      //
      if (!valid2) {
         if (!hits_from_inside_count) {
            //
            // The ray only hits the finite cone's curved surface once, and never hits 
            // the cone's base. This means that the ray must be originating from inside 
            // of the cone.
            //
            return false;
         }
         out_hit_distance = hit_near;
         return true;
      }
   }
   out_hit_distance = std::min(hit_near, hit_away);
   return true;
}