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/cylinder intersection test.
I've also provided code at the bottom as a public domain work.
Parametric surface of a cylinder
The parametric equations (that is, equations with parameters) for a cylinder are as follows:
Ch
=
height of the cylinder
Cr
=
radius of the cylinder
Ho
=
height offset on the range [0, Ch], where 0 is aligned with an endcap
Technically, these equations describe an infinite cylinder — one with no endcaps, which extends forever in both directions. However, we can still use them to put together the tests for a finite clinder with endcaps.
You may notice that these are very similar to the parametric equations for a cone. Mathematically speaking, a cylinder is basically a cone with infinite height, and geometrically speaking, a cylinder is just a cone that never tapers. Moreover, if we ignore the Z-component, then we're literally just writing the equation for a 2D circle.
We have a parametric surface — that is, three parametric equations which define all points on the surface of the cylinder. In order to solve for any given part of the equation, however, we need an implicit surface: a surface defined by a single equation, wherein x, y, and z are on one side, and 0 is on the other side.
The above equations basically define a 2D circle anchored at (0, 0) on the XY-plane, and then extend it infinitely along the Z-axis. The circle's radius is Cr. This means that we can represent the X and Y equations in terms of any arbitrary 2D vector P2D:
[Equation 2:]||P2D||
=
Cr
If we project a 3D vector P onto the Z-axis, then we'll have just its Z-component. Remove that component, and we can then fit it into the equation above.
P
=
any point on the curved surface of the infinite cylinder
If we so desire, we can now subtract Cr from both sides of the equation in order to have one side contain only zero. As such, this is now an implicit surface, which defines a cylinder that has one endcap centered on (0, 0, 0).
In general, we'll want to run raycasts against a bounded cylinder — one with endcaps, rather than one that extends infinitely along its axis. There's no equation that can represent a bounded cylinder directly. However, we can still represent an infinite cylinder in terms of where the endcaps would be, and then use some distance checks later to enforce bounds.
Let's take the implicit surface of an infinite cylinder, and represent it in terms of the endcaps.
Cb
=
centerpoint of the cylinder's bottom endcap
Ct
=
centerpoint of the cylinder's top endcap
Cs
=
"spine" of the cylinder: a vector from the top to the bottom, equal to Ct - Cb
Ca
=
axis of the cylinder: normalized spine
Hp
=
hit position: a point that we presume is on the cylinder'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 cylinder equations; this is the distance from Hs to Cb i.e. ||Hs - Cb||.
Hp fills the role of P in Equation 3, and Hs fills the role of Pp. Hs is the hit position projected onto the cylinder's axis — its "spine." If we view the cylinder from its endcap, then Hs will be at the center of a circle; if Hp is indeed on the cylinder's curved surface, then the distance from it to Hs will equal the cylinder's radius:
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 cylinder equation above, and then attempt to solve for the hit distance Hd:
We can simplify our problem if we translate the entire coordinate system — move everything so that the base of the cylinder is (0, 0, 0). We'll do that by defining a new variable.
Rl
=
the ray's origin relative to the cylinder's base position = Ro - Cb
If we arrange that lengthwise, it'll be easier to see that as with ray/cone intersections, we've ended up with a quadratic equation. We can use the quadratic formula to find its roots — its solutions; the hit distances.
In the quadratic formula, the expression
b^2 squared - 4ac
is called the discriminant. If its value is negative, then there is no valid solution — no intersection between the ray and the curved surface of our cylinder. If its value is zero, then there's exactly one solution: you can apply the formula either way (plus or minus) and you'll get the same result. If the discriminant is a positive non-zero number, then there are two different solutions — two different points at which we have potential intersections.
Yes. "Potential." This equation actually tests for intersections between an infinite double-sided line, and an infinitely long cylinder with no endcaps. In order to test for the intersection between a ray and a bounded cylinder, we need to add some additional inequalities, and require that they be true. Let's start with one to rule out any hit positions behind the ray origin:
Hd ≥ 0
Simple enough. Next up, we need to apply bounds to our cylinder. We'll take the vector from the hit position to one of the cylinder's endcaps, project that onto the cylinder's axis, and check the resulting length against the cylinder's height.
0
≤
(Ct - Hp) • Ca
≤
||Ct - Cb||
There's one more wrinkle: we need to test the ray against the cylinder's endcaps. That'll be two ray/disc intersection checks. However, since both discs exist on parallel planes, we can actually combine some of the math. Both discs have the same normal vectors, so when we take the dot product of the ray direction with the plane normal, we can reuse that across both ray/disc checks.
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.