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.