Lens flare

How unintended reflections between optical surfaces turn a bright source into veiling light and a line of coloured ghosts — and how to approximate that pattern with bounded procedural sprites.

Visible effect

Stray light lowers contrast and repeats the bright source

A bright lamp, the sun, or a strong reflection can add light that does not belong to the intended image. Contrast washes out near the source, while translucent discs, rings, or aperture-like ghosts may cross the frame on a line through the optical centre.

Unlike diffraction stars, these ghosts do not primarily reveal light bending around aperture edges. They reveal unintended paths through reflecting lens, filter, cover-glass, and sensor surfaces.

Physics

Weak interface reflections create unintended optical paths

Intended image ray and one unintended double-reflection path Most light travels through coated lens elements to the image plane. A small fraction reflects between two optical surfaces and reaches a displaced, defocused position.bright sourcepartially reflecting surfacesimage planeintended image pathdouble-reflection path
Anti-reflection coatings reduce but do not eliminate interface reflections. A ray that reflects twice can still travel forward to the sensor, arriving at the wrong position and with a broad footprint.

Every air–glass, glass–glass, filter, cover-glass, and sensor interface can return a small part of the incident light. Two reflections can redirect that light forward again. Because this path has different power, aperture clipping, and conjugates from the intended path, it forms a displaced and usually defocused ghost.

Many weak paths can overlap as polygonal or circular ghosts and as broad veiling glare. Coatings, element curvature and spacing, stop position, sensor reflectivity, and the source position determine the real pattern.

Mathematics

A line of sprites plus a local veiling halo

A physical model would trace each relevant surface pair through a particular lens prescription. This bounded image-space model instead places authored sprites between the optical centrec and the point mirrored from the selected light:

Each sprite Sᵢ has an authored radius, attenuation, and coating tint Tᵢ. G adds a broad halo at the chosen light position. A is the explicit intensity control. Placement does not inspect the underlying source pixel: these parameters describe an artistic approximation, not recovered lens surfaces.

Shader

GLSL bounded procedural ghost composite

One fragment pass constructs at most eight deterministic ring sprites at the selected placement and adds their linear-light energy to the source.

lensFlareApproximationGLSL
// WHAT: Build a controllable lens-flare contribution from procedural ghosts.
// HOW: Place tinted disks and rings along the light-to-centre axis, add a broad
// halo at the light position, then return the linear-light flare contribution.
// WHY: The construction exposes the visual logic of internal reflections
// without claiming to trace a particular lens prescription.
const int FLARE_MAX_GHOSTS = 8;

float srgbToLinearChannel(float value) {
  if (value <= 0.04045) return value / 12.92;
  return pow((value + 0.055) / 1.055, 2.4);
}

vec3 srgbToLinear(vec3 value) {
  return vec3(
    srgbToLinearChannel(value.r),
    srgbToLinearChannel(value.g),
    srgbToLinearChannel(value.b)
  );
}

float ghostScale(int index) {
  // A deterministic irregular sequence keeps repeated ghosts from looking cloned.
  return 0.58 + mod(float(index) * 0.37, 0.9);
}

vec3 ghostTint(int index, float chroma) {
  int phase = index % 3;
  vec3 tint = phase == 0
    ? vec3(1.0, 0.48, 0.24)
    : (phase == 1 ? vec3(0.34, 0.78, 1.0) : vec3(0.72, 1.0, 0.42));
  return mix(vec3(1.0), tint, clamp(chroma, 0.0, 1.0));
}

vec3 lensFlareApproximation(
  vec2 sourceUv,
  vec2 pixelStep,
  vec2 lightPosition,
  int ghostCount,
  float radiusPx,
  float chroma,
  float intensity
) {
  if (intensity <= 0.0 || ghostCount < 1) {
    return vec3(0.0);
  }

  vec3 lightLinear = vec3(1.0);
  vec2 safePixelStep = max(pixelStep, vec2(0.000001));
  vec2 opticalCentre = vec2(0.5);
  vec3 flare = vec3(0.0);
  float countDenominator = float(max(ghostCount - 1, 1));

  for (int index = 0; index < FLARE_MAX_GHOSTS; index += 1) {
    if (index >= ghostCount) break;
    float t = float(index) / countDenominator;
    // Ghosts lie on the axis opposite the light, converging through frame centre.
    vec2 centre = opticalCentre + (opticalCentre - lightPosition) * t;
    float spriteRadius = max(1.0, radiusPx * ghostScale(index));
    float distancePx = length((sourceUv - centre) / safePixelStep);
    float normalizedRadius = distancePx / spriteRadius;
    // A faint disk plus a stronger rim suggests a reflected aperture image.
    float disk = 1.0 - smoothstep(0.55, 1.0, normalizedRadius);
    float ring = 1.0 - smoothstep(0.03, 0.18, abs(normalizedRadius - 0.78));
    float sprite = disk * 0.12 + ring * 0.34;
    float attenuation = mix(1.0, 0.32, t);
    flare += lightLinear * ghostTint(index, chroma) *
      sprite * attenuation;
  }

  // The source-adjacent halo is broader and softer than the discrete ghosts.
  float haloDistancePx = length(
    (sourceUv - lightPosition) / safePixelStep
  );
  float halo = exp(-haloDistancePx / max(radiusPx * 2.6, 1.0));
  flare += lightLinear * halo * 0.22;
  // The caller adds this linear contribution to the decoded source image.
  return flare * intensity;
}
Processing pipelineBoxes mark actual render-pass boundaries.
  1. Source texturesRGB
  2. Procedural flare compositeOne fullscreen render pass
    • Read the manual flare position
    • Place ghost centres and procedural rings
    • Composite additively in linear light
    • Encode to sRGB
  3. Display output

Why these steps are here

  1. Keep placement independent. Angle and distance remain usable over their full ranges instead of acting as a one-pixel highlight picker.
  2. Use the optical-centre line. Mirrored sprite placement preserves the characteristic movement as the source crosses the frame.
  3. Vary footprint and tint deterministically. Stable authored differences suggest several reflection paths without frame-to-frame noise.
  4. Add in linear light. Flare contributes stray energy; alpha-over or gamma-space mixing would darken and misweight the result.
  5. Bound the loop. Eight procedural sprites keep the cost predictable and avoid an external sprite texture.

Notes

  • The source direction and distance are manual. Distance is normalized to the visible frame boundary, keeping the selected light and mirrored ghost endpoint inside the image. A production pipeline can replace them with a thresholded highlight extraction pass, scene metadata, or a tracked light.
  • This model does not trace a lens prescription, reproduce aperture clipping, model sensor-stack reflections, or predict calibrated ghost positions.
  • Real flare can originate outside the recorded frame and can vary with focus, zoom, aperture, filters, dirt, coatings, and wavelength.
  • Diffraction stars, blooming, halation, and local tone-mapping halos can coexist with flare. They should not be folded into the same control merely because all appear near highlights.
  • The bounded artistic model uses deterministic procedural rings rather than external sprite assets.

References

ARRI Master Prime lens brochure — manufacturer documentation linking anti-reflex coating and light traps to reduced flare and veiling glare.

Nikon MicroscopyU — Introduction to microscope objectives — manufacturer educational reference on coma, astigmatism, field curvature, chromatic correction, and flare.