Tube and Capsule Area Light Specular in GLSL

This article explains the Representative Point algorithm used to calculate real-time specular highlights for tube and capsule area lights in GLSL. You will learn how the technique approximates area light integration by finding the closest point between a reflection vector and a finite line segment, how to adjust for the light's radius, and how to implement the algorithm inside a standard physically based shading model.

The Representative Point Technique

Directly integrating the Cook-Torrance microfacet specular BRDF over an area light source is too computationally expensive for real-time rendering. The standard solution, popularized by Brian Karis in Unreal Engine 4, is the Representative Point Method.

Instead of integrating across the entire surface of the tube or capsule, the algorithm finds a single point on the light source that contributes the most to the specular reflection along the ideal mirror reflection direction \(\mathbf{R}\). Once this point is determined, it is treated as a standard point light source, and the surface roughness is adjusted to account for the light's physical radius and preserve energy conservation.

Step-by-Step Algorithm Breakdown

A capsule or tube light is defined by two endpoints, \(\mathbf{A}\) and \(\mathbf{B}\), and a radius \(r\). The segment vector is \(\mathbf{L}_d = \mathbf{B} - \mathbf{A}\).

1. Finding the Closest Point on the Segment to the Ray

Given a shading point \(\mathbf{P}\), view vector \(\mathbf{V}\), surface normal \(\mathbf{N}\), and the reflection vector \(\mathbf{R} = \text{reflect}(-\mathbf{V}, \mathbf{N})\), the goal is to find the parameter \(t \in [0, 1]\) along the line segment that minimizes the distance to the reflection ray.

  1. Compute vectors from the shading position to the segment endpoints: \[\mathbf{L}_0 = \mathbf{A} - \mathbf{P}\] \[\mathbf{L}_1 = \mathbf{B} - \mathbf{P}\] \[\mathbf{L}_d = \mathbf{L}_1 - \mathbf{L}_0\]

  2. Project the reflection vector \(\mathbf{R}\) onto the line segment to find the intersection of the ray and segment planes: \[t = \frac{(\mathbf{R} \cdot \mathbf{L}_0)(\mathbf{R} \cdot \mathbf{L}_d) - (\mathbf{L}_0 \cdot \mathbf{L}_d)}{|\mathbf{L}_d|^2 - (\mathbf{R} \cdot \mathbf{L}_d)^2}\]

  3. Clamp \(t\) to the range \([0, 1]\) to restrict the representative point to the segment bounds.

2. Accounting for Capsule Radius

The parameter \(t\) yields the closest point along the central 1D axis of the light: \[\mathbf{P}_{\text{segment}} = \mathbf{L}_0 + t \cdot \mathbf{L}_d\]

Because a capsule has a volume defined by radius \(r\), the highlight must appear on the surface of the capsule rather than its interior line. The point is projected toward the reflection ray: \[\mathbf{L}_{\text{center}} = \mathbf{P}_{\text{segment}}\] \[\mathbf{P}_{\text{rep}} = \mathbf{L}_{\text{center}} - \mathbf{R} \cdot \text{clamp}\left(\frac{\mathbf{L}_{\text{center}} \cdot \mathbf{R}}{|\mathbf{R}|}, -r, r\right)\]

Alternatively, normalizing the vector from \(\mathbf{P}_{\text{segment}}\) to the ray and offsetting by \(r\) provides an effective approximation.

3. Roughness Modification for Energy Conservation

Because an area light broadens the specular lobe, evaluating a point light at \(\mathbf{P}_{\text{rep}}\) with the original material roughness \(\alpha\) would produce a specular highlight that is too sharp and overly intense. Karis applies an empirical roughness expansion based on the angular extent of the light:

\[\alpha' = \text{clamp}\left(\alpha + \frac{r}{2 d}, 0.0, 1.0\right)\]

where \(d = |\mathbf{P}_{\text{rep}}|\) is the distance from the shading point to the representative point.

GLSL Implementation

Below is a complete GLSL function implementing the representative point calculation for a capsule light:

vec3 getTubeLightRepresentativePoint(vec3 worldPos, vec3 R, vec3 lightA, vec3 lightB, float radius) {
    vec3 L0 = lightA - worldPos;
    vec3 L1 = lightB - worldPos;
    vec3 Ld = L1 - L0;

    // Vector projections to locate the closest point on the ray
    float R_dot_L0 = dot(R, L0);
    float R_dot_Ld = dot(R, Ld);
    float L0_dot_Ld = dot(L0, Ld);
    float Ld_dot_Ld = dot(Ld, Ld);

    // Solve for t along the segment
    float denom = Ld_dot_Ld - R_dot_Ld * R_dot_Ld;
    float t = 0.0;
    if (denom > 1e-5) {
        t = (R_dot_L0 * R_dot_Ld - L0_dot_Ld) / denom;
    }
    t = clamp(t, 0.0, 1.0);

    // Base point along the central bone
    vec3 P_seg = L0 + t * Ld;

    // Project onto capsule surface along the reflection ray
    vec3 centerToRay = dot(P_seg, R) * R - P_seg;
    vec3 closestPoint = P_seg + centerToRay * clamp(radius / max(length(centerToRay), 1e-5), 0.0, 1.0);

    return closestPoint;
}

vec3 evaluateCapsuleSpecular(vec3 N, vec3 V, vec3 worldPos, vec3 lightA, vec3 lightB, float radius, float roughness, vec3 F0) {
    vec3 R = reflect(-V, N);
    
    // Calculate representative point
    vec3 L_rep = getTubeLightRepresentativePoint(worldPos, R, lightA, lightB, radius);
    float dist = length(L_rep);
    vec3 L = L_rep / dist;
    vec3 H = normalize(V + L);

    // Expand roughness based on light radius and distance
    float alpha = roughness * roughness;
    float alphaPrime = clamp(alpha + (radius / (2.0 * dist)), 0.0, 1.0);

    // Standard Cook-Torrance evaluation using modified roughness alphaPrime
    float NdotL = clamp(dot(N, L), 0.0, 1.0);
    float NdotV = clamp(dot(N, V), 0.0, 1.0);
    float NdotH = clamp(dot(N, H), 0.0, 1.0);
    float VdotH = clamp(dot(V, H), 0.0, 1.0);

    // Normal Distribution Function (GGX)
    float a2 = alphaPrime * alphaPrime;
    float denomNDF = (NdotH * NdotH * (a2 - 1.0) + 1.0);
    float D = a2 / (3.14159265 * denomNDF * denomNDF);

    // Geometric Attenuation (Schlick-GGX)
    float k = (alphaPrime + 1.0) * (alphaPrime + 1.0) / 8.0;
    float G = (NdotV / (NdotV * (1.0 - k) + k)) * (NdotL / (NdotL * (1.0 - k) + k));

    // Fresnel (Schlick)
    vec3 F = F0 + (1.0 - F0) * pow(1.0 - VdotH, 5.0);

    // Energy-conserving area light normalization factor
    float sphereFactor = alpha / alphaPrime;
    
    return (D * G * F) / max(4.0 * NdotV * NdotL, 1e-4) * (sphereFactor * sphereFactor);
}

By substituting the static light position with the calculated representative position and scaling the roughness parameter, the specular highlight stretches smoothly along the length and circumference of the tube light with minimal overhead.