Analytical Ray-Cone Intersection in GLSL
Analytically computing the intersection between a ray and a cone in a GLSL shader requires framing the cone as an implicit geometric surface, substituting the parametric ray equation into this implicit form to derive a quadratic equation, solving for the distance parameter \(t\), and validating the result against height and directional bounds. This process allows real-time ray marchers and ray tracers to render sharp, mathematically exact conical primitives without relying on polygonal approximations or step-based signed distance fields.
Geometric Formulation
A parametric ray is defined by its origin \(\mathbf{O}\) and normalized direction \(\mathbf{D}\):
\[\mathbf{P}(t) = \mathbf{O} + t\mathbf{D}, \quad t \ge 0\]
An infinite double cone with apex \(\mathbf{V}\), normalized axis vector \(\mathbf{A}\), and half-angle \(\theta\) satisfies the condition that the angle between any vector on the surface \((\mathbf{P} - \mathbf{V})\) and the axis \(\mathbf{A}\) equals \(\theta\):
\[\frac{(\mathbf{P} - \mathbf{V}) \cdot \mathbf{A}}{\|\mathbf{P} - \mathbf{V}\|} = \cos\theta\]
Squaring both sides eliminates the square root from the vector magnitude and yields the implicit quadratic equation for an infinite double cone:
\[((\mathbf{P} - \mathbf{V}) \cdot \mathbf{A})^2 = \|\mathbf{P} - \mathbf{V}\|^2 \cos^2\theta\]
Deriving the Quadratic Equation
Let \(\mathbf{\Delta} = \mathbf{O} - \mathbf{V}\). Substituting \(\mathbf{P}(t) - \mathbf{V} = \mathbf{\Delta} + t\mathbf{D}\) into the implicit equation:
\[(t(\mathbf{D} \cdot \mathbf{A}) + (\mathbf{\Delta} \cdot \mathbf{A}))^2 = (t^2(\mathbf{D} \cdot \mathbf{D}) + 2t(\mathbf{D} \cdot \mathbf{\Delta}) + (\mathbf{\Delta} \cdot \mathbf{\Delta})) \cos^2\theta\]
Since \(\mathbf{D}\) is normalized, \(\mathbf{D} \cdot \mathbf{D} = 1\). Expanding and rearranging terms into the standard quadratic form \(a t^2 + b t + c = 0\) gives:
\[a = (\mathbf{D} \cdot \mathbf{A})^2 - \cos^2\theta\] \[b = 2 \left( (\mathbf{D} \cdot \mathbf{A})(\mathbf{\Delta} \cdot \mathbf{A}) - (\mathbf{D} \cdot \mathbf{\Delta})\cos^2\theta \right)\] \[c = (\mathbf{\Delta} \cdot \mathbf{A})^2 - (\mathbf{\Delta} \cdot \mathbf{\Delta})\cos^2\theta\]
Boundary and Height Constraints
Because the implicit equation describes a double cone extending infinitely in both directions along \(\mathbf{A}\), the solutions must be filtered:
- Single Sheet Constraint: For a cone extending only in the positive direction of \(\mathbf{A}\), the projection of the hit point onto the axis must be positive: \((\mathbf{P}(t) - \mathbf{V}) \cdot \mathbf{A} > 0\).
- Height Truncation: For a finite cone of height \(h\), the projection must satisfy \(0 \le (\mathbf{P}(t) - \mathbf{V}) \cdot \mathbf{A} \le h\).
- Caps (Optional): If the cone is closed at its base, a standard ray-plane intersection can be evaluated at distance \(h\) along \(\mathbf{A}\), bounded by radius \(r = h \tan\theta\).
Surface Normal Calculation
The gradient of the implicit function provides the surface normal at intersection point \(\mathbf{P}\):
\[\nabla f(\mathbf{P}) = 2 ((\mathbf{P} - \mathbf{V}) \cdot \mathbf{A})\mathbf{A} - 2(\mathbf{P} - \mathbf{V})\cos^2\theta\]
Normalizing the outward-facing vector gives:
\[\mathbf{N} = \text{normalize}\left( (\mathbf{P} - \mathbf{V}) - \frac{(\mathbf{P} - \mathbf{V}) \cdot \mathbf{A}}{\cos^2\theta}\mathbf{A} \right)\]
GLSL Implementation
struct Cone {
vec3 apex;
vec3 axis; // Normalized axis direction
float cosTheta; // cos(half-angle)
float height; // Maximum height along axis
};
bool intersectCone(
vec3 ro,
vec3 rd,
Cone cone,
out float hitT,
out vec3 hitNormal
) {
vec3 delta = ro - cone.apex;
float dDotA = dot(rd, cone.axis);
float deltaDotA = dot(delta, cone.axis);
float dDotDelta = dot(rd, delta);
float deltaDotDelta = dot(delta, delta);
float cos2 = cone.cosTheta * cone.cosTheta;
float a = dDotA * dDotA - cos2;
float b = 2.0 * (dDotA * deltaDotA - dDotDelta * cos2);
float c = deltaDotA * deltaDotA - deltaDotDelta * cos2;
float discriminant = b * b - 4.0 * a * c;
if (discriminant < 0.0) {
return false;
}
float sqrtDisc = sqrt(discriminant);
float t0 = (-b - sqrtDisc) / (2.0 * a);
float t1 = (-b + sqrtDisc) / (2.0 * a);
if (t0 > t1) {
float temp = t0;
t0 = t1;
t1 = temp;
}
// Test intersections against height bounds
float tCandidates[2] = float[2](t0, t1);
for (int i = 0; i < 2; ++i) {
float t = tCandidates[i];
if (t > 0.0) {
vec3 p = ro + t * rd;
float projection = dot(p - cone.apex, cone.axis);
if (projection >= 0.0 && projection <= cone.height) {
hitT = t;
// Analytical normal computation
vec3 tipToPoint = p - cone.apex;
vec3 normal = normalize(tipToPoint - (projection / cos2) * cone.axis);
// Orient normal against incoming ray
hitNormal = (dot(rd, normal) < 0.0) ? normal : -normal;
return true;
}
}
}
return false;
}