Ray Capsule Intersection Algorithm in GLSL

This article explains the mathematical formulation and GLSL implementation for computing the intersection of a 3D ray with a capsule. A capsule is geometrically defined as a cylinder bounded by two points and capped by two hemispheres of equal radius. By breaking the primitive down into its cylindrical body and spherical endcaps, we derive the quadratic equations needed to detect collisions, determine the nearest entry distance, and output surface normals in a fragment shader.

Geometric Definitions

A capsule is defined by two endpoints \(A\) and \(B\), representing the central axis segment, and a radius \(r\).

A ray is defined parametrically by an origin \(O\) and a normalized direction vector \(D\):

\[P(t) = O + tD, \quad t \ge 0\]

A point \(P\) lies on the capsule's surface if its shortest Euclidean distance to the line segment \(AB\) equals \(r\).

Mathematical Formulation

To find the intersection parameter \(t\), the capsule is evaluated as an infinite cylinder along the axis \(AB\), constrained to the segment, and capped by two spheres centered at \(A\) and \(B\).

Let:

  • \(\vec{ba} = B - A\)
  • \(\vec{oa} = O - A\)

Projecting the ray onto the axis gives the relative height along the segment:

\[h(t) = \frac{(\vec{oa} + tD) \cdot \vec{ba}}{\|\vec{ba}\|}\]

1. The Cylindrical Body

The squared distance from a point on the ray \(P(t)\) to the infinite line passing through \(AB\) is given by:

\[\|(\vec{oa} + tD) - \text{proj}_{\vec{ba}}(\vec{oa} + tD)\|^2 = r^2\]

Multiplying by \(\|\vec{ba}\|^2\) to avoid divisions yields a standard quadratic equation in terms of \(t\):

\[a t^2 + 2b t + c = 0\]

Where:

  • \(a = \|\vec{ba}\|^2 - (D \cdot \vec{ba})^2\)
  • \(b = \|\vec{ba}\|^2 (\vec{oa} \cdot D) - (\vec{oa} \cdot \vec{ba})(D \cdot \vec{ba})\)
  • \(c = \|\vec{ba}\|^2 \|\vec{oa}\|^2 - (\vec{oa} \cdot \vec{ba})^2 - r^2 \|\vec{ba}\|^2\)

The discriminant is computed as \(\Delta = b^2 - a c\).

  • If \(\Delta < 0\), the ray misses the infinite cylinder.
  • If \(\Delta \ge 0\), the candidate distance to the cylinder surface is:

\[t_{\text{cyl}} = \frac{-b - \sqrt{\Delta}}{a}\]

The point is valid only if it falls within the segment bounds:

\[0 \le (\vec{oa} + t_{\text{cyl}} D) \cdot \vec{ba} \le \|\vec{ba}\|^2\]

2. The Spherical Caps

If the intersection falls outside this range, or if the ray misses the cylinder body entirely, the ray may intersect one of the two spherical caps:

  • Base Cap (Sphere at \(A\)): Solve \(\|O + tD - A\|^2 = r^2\)
  • Tip Cap (Sphere at \(B\)): Solve \(\|O + tD - B\|^2 = r^2\)

For an arbitrary sphere centered at \(C\) with radius \(r\):

\[t^2 + 2( (O - C) \cdot D )t + (\|O - C\|^2 - r^2) = 0\]

The valid hit is the smallest positive \(t\) across the cylinder body and the appropriate hemisphere.

GLSL Implementation

The following GLSL function computes the ray-capsule intersection. It returns a vec4 where .xyz contains the surface normal at the hit point and .w contains the hit distance \(t\) (or -1.0 if no intersection occurs).

vec4 intersectCapsule(vec3 ro, vec3 rd, vec3 pa, vec3 pb, float r) {
    vec3 ba = pb - pa;
    vec3 oa = ro - pa;

    float baba = dot(ba, ba);
    float bard = dot(ba, rd);
    float baoa = dot(ba, oa);
    float rdoa = dot(rd, oa);
    float oaoa = dot(oa, oa);

    // Cylinder body quadratic coefficients
    float a = baba - bard * bard;
    float b = baba * rdoa - baoa * bard;
    float c = baba * oaoa - baoa * baoa - r * r * baba;
    float h = b * b - a * c;

    if (h >= 0.0) {
        float t = (-b - sqrt(h)) / a;
        float y = baoa + t * bard;

        // Check if hit is within the finite cylinder segment
        if (t > 0.0 && y > 0.0 && y < baba) {
            vec3 hitPoint = ro + t * rd;
            vec3 normal = (hitPoint - (pa + ba * (y / baba))) / r;
            return vec4(normal, t);
        }
    }

    // Check spherical caps
    // Cap at pa (y <= 0.0)
    vec3 oc = (bard <= 0.0) ? oa : ro - pb;
    b = dot(rd, oc);
    c = dot(oc, oc) - r * r;
    h = b * b - c;

    if (h > 0.0) {
        float t = -b - sqrt(h);
        if (t > 0.0) {
            vec3 center = (bard <= 0.0) ? pa : pb;
            vec3 normal = (ro + t * rd - center) / r;
            return vec4(normal, t);
        }
    }

    return vec4(0.0, 0.0, 0.0, -1.0);
}

Performance Considerations

  • Precomputation: If the capsule endpoints are static, terms such as ba and baba can be precomputed outside the shader to save arithmetic instructions.
  • Normalization: Ensure rd is normalized before calling the function; non-unit direction vectors invalidate the quadratic derivations.
  • Ray Marching Alternative: When rendering multiple capsules or blending shapes, evaluating an analytical distance field (SDF) using sphere-tracing is often more flexible than exact analytical raycasting.