Screen-Space Lens Flare Ghost Coordinates in GLSL

This article explains the generation of screen-space lens flare ghost coordinates from high-luminance pixels using GLSL. In modern real-time rendering, secondary lens reflections—commonly called "ghosts"—are synthesized in screen space by extracting overexposed elements from an HDR scene buffer and projecting duplicate samples along a ray passing through the viewport's center. Below is a breakdown of the mathematical derivation, vector reflections, and the fragment shader logic required to implement this effect.

1. Extracting High-Luminance Features

Before generating ghosts, the rendered scene must be filtered to isolate intense light sources (such as the sun, streetlights, or specular highlights). This is achieved through a bright-pass filter that evaluates the perceived luminance of each pixel and zeroes out anything below a chosen threshold:

// Standard luminance weights for sRGB/Rec. 709
const vec3 LUMINANCE_WEIGHTS = vec3(0.2126, 0.7152, 0.0722);

vec4 getBrightPass(vec2 uv, sampler2D sceneTexture, float threshold) {
    vec4 color = texture(sceneTexture, uv);
    float luminance = dot(color.rgb, LUMINANCE_WEIGHTS);
    return max(color - threshold, vec4(0.0));
}

This bright-pass result is typically stored in a downsampled texture to reduce sampling overhead and naturally soften the resulting flares.

2. Deriving the Ghost Vector

Physical camera lens flare artifacts manifest radially across the optical axis, which aligns with the center of the viewport in normalized device coordinates (or UV space at vec2(0.5)). To reproduce this, the coordinate displacement vector must point from the current pixel toward the screen center:

// Calculate the vector from the current UV to the viewport center
vec2 texelCoords = v_uv; 
vec2 ghostVector = (vec2(0.5) - texelCoords);

Inverting the texture coordinates relative to vec2(0.5) reflects any light source across the optical axis. Scaling this vector allows the generation of multiple distinct ghost reflections along the same line.

3. Calculating Per-Ghost UV Coordinates

In the flare generation shader, an iteration loop calculates multiple sample coordinates offset along the ghostVector. A dispersal factor controls the spacing between each ghost reflection:

uniform int u_ghostCount;      // Number of ghosts (e.g., 4 to 8)
uniform float u_ghostSpacing;   // Distance factor between ghosts
uniform sampler2D u_brightPassTex;

vec4 generateGhosts(vec2 uv) {
    // Vector pointing toward the center
    vec2 ghostVec = (vec2(0.5) - uv) * u_ghostSpacing;
    vec4 result = vec4(0.0);

    for (int i = 0; i < u_ghostCount; ++i) {
        // Calculate offset coordinate for the i-th ghost
        vec2 offsetUV = fract(uv + ghostVec * float(i));

        // Calculate falloff to attenuate ghosts near the screen borders
        float distanceToCenter = length(vec2(0.5) - offsetUV);
        float weight = 1.0 - smoothstep(0.0, 0.75, distanceToCenter);

        // Accumulate sampled color multiplied by edge attenuation
        result += texture(u_brightPassTex, offsetUV) * weight;
    }

    return result;
}

The fract() function ensures coordinates wrap predictably if required, though typically coordinates falling outside \([0, 1]\) are clipped or attenuated using the edge falloff weight.

4. Edge Attenuation and Distortion

Real lenses reduce flare intensity toward the edges of the frame due to mechanical vignetting. Without attenuation, ghosts passing outside screen bounds create harsh edge artifacts. Applying a radial weight based on the distance from the sampled coordinate to the center smooths the transition:

float getEdgeAttenuation(vec2 uv) {
    // Measures distance from coordinate to screen center
    float d = length(vec2(0.5) - uv);
    return smoothstep(0.707, 0.0, d); // 0.707 is the distance to corner
}

Multiplying the sampled bright-pass values by this attenuation factor ensures that ghosts disappear progressively as their corresponding coordinates leave the viewport. The accumulated ghost buffer is subsequently blurred and additively blended over the original scene texture.