Coma

Why an off-axis point grows an asymmetric comet-shaped footprint — and how a primary-coma pupil map reproduces the field-dependent point spread.

Visible effect

Peripheral point lights grow bright heads and fan-shaped tails

A small light near the optical axis can remain compact while the same light near a corner opens into a bright head with a fan-shaped tail. The footprint usually points along the local radius from the image centre, and reversing the sign of the aberration reverses the tail.

Unlike geometric distortion, coma does not merely move an otherwise sharp sample. Rays from one scene point land across an asymmetric area, so reproducing the evidence requires a point-spread kernel and multiple source samples.

Physics

Pupil zones give one off-axis point unequal magnification

Pupil zones from one off-axis point form an asymmetric sensor footprint Rays from an off-axis point cross different zones of a lens. Unequal magnification prevents them from meeting at one sensor point and produces a radial comet-shaped footprint.off-axis pointpupil zonesunequal convergencecomet footprintfield radius
Rays crossing different pupil zones do not share one transverse magnification for an off-axis point. Their sensor intersections form an asymmetric footprint whose head and tail align with the local field radius.

A centred object point sees a rotationally balanced pupil. Off axis, rays crossing different pupil zones can acquire different transverse magnification. They no longer meet in one symmetric spot at the image plane; the residual is biased along the local field radius.

Mathematics

A pupil-quadratic displacement creates the asymmetric footprint

In third-order aberration notation, the coma wave term is linear in field height H and cubic in pupil radiusrₚ. Differentiating that wavefront produces a transverse ray error quadratic in pupil coordinates. The real-time model samples a unit pupil u, maps it into the local radial/tangential basis, and scales the footprint by normalized field radiusρ:

The ideal third-order field dependence is linear. The exposed powerp also permits a steeper onset for corrected lenses and makes the isolated effect easier to inspect without a lens prescription. Rmax is the marginal-ray displacement at the frame edge. The quadratic map is the normalized gradient of the primary-coma wave term, not a symmetric disk that is subsequently squeezed. Its ray density therefore forms the characteristic caustic; that bunching is part of the geometric coma spot rather than a second adjustable “asymmetry” effect.

Shader

GLSL primary-coma pupil gather

Each fragment builds an aspect-correct field basis, distributes deterministic samples over a disk, applies the quadratic primary-coma ray equation, and gathers the source in linear light.

comaPsfGLSL
// WHAT: Approximate field-dependent primary coma as an asymmetric point spread.
// HOW: Sample the pupil with a golden-angle disk, map every pupil point through
// a third-order coma term, and gather the resulting footprint in linear light.
// WHY: The mapped pupil produces the characteristic one-sided tail that a
// generic directional blur cannot explain.
const int COMA_MAX_SAMPLES = 96;
const float COMA_GOLDEN_ANGLE = 2.39996323;

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)
  );
}

vec2 comaFieldRadial(
  vec2 sourceUv,
  vec2 opticalCenter,
  float sourceAspect
) {
  vec2 field = sourceUv - opticalCenter;
  // Measure the field in aspect-correct space, then return a UV-space direction.
  vec2 isotropicField = vec2(field.x * sourceAspect, field.y);
  float fieldLength = length(isotropicField);
  vec2 radialIso = fieldLength > 0.000001
    ? isotropicField / fieldLength
    : vec2(1.0, 0.0);
  return normalize(vec2(
    radialIso.x / max(sourceAspect, 0.000001),
    radialIso.y
  ));
}

vec2 comaPupilOffset(
  vec2 pupil,
  float directionSign
) {
  // This is the primary-coma pupil map expressed in radial/tangential axes.
  float radial = pupil.x;
  float tangential = pupil.y;
  float comaRadial = (
    3.0 * radial * radial + tangential * tangential
  ) / 3.0;
  float comaTangential = 2.0 * radial * tangential / 3.0;
  return vec2(comaRadial * directionSign, comaTangential);
}

vec3 comaPsf(
  sampler2D source,
  vec2 sourceUv,
  vec2 sampleStep,
  vec2 opticalCenter,
  float sourceAspect,
  float radiusPx,
  float fieldPower,
  float directionSign,
  int sampleCount
) {
  vec2 field = sourceUv - opticalCenter;
  float fieldRadius = clamp(
    length(vec2(field.x * sourceAspect, field.y)) * 2.0,
    0.0,
    1.0
  );
  // Coma vanishes on axis and grows toward the edge according to fieldPower.
  float scale = radiusPx * pow(fieldRadius, max(fieldPower, 0.1));
  vec2 radial = comaFieldRadial(sourceUv, opticalCenter, sourceAspect);
  vec2 tangential = vec2(-radial.y, radial.x);
  float safeCount = float(max(sampleCount, 1));
  float lod = max(0.0, log2(max(scale, 1.0)) - 2.0);
  vec3 accumulated = vec3(0.0);

  for (int index = 0; index < COMA_MAX_SAMPLES; index += 1) {
    if (index >= sampleCount) break;
    // sqrt gives equal-area pupil coverage; the golden angle avoids rings.
    float pupilRadius = sqrt((float(index) + 0.5) / safeCount);
    float angle = float(index) * COMA_GOLDEN_ANGLE;
    vec2 pupil = vec2(cos(angle), sin(angle)) * pupilRadius;
    vec2 skewed = comaPupilOffset(
      pupil,
      directionSign
    );
    vec2 offsetDirection =
      skewed.x * radial + skewed.y * tangential;
    vec3 encoded = textureLod(
      source,
      sourceUv - offsetDirection * sampleStep * scale,
      lod
    ).rgb;
    accumulated += srgbToLinear(encoded);
  }

  // A fixed normalized average keeps the PSF energy independent of tap count.
  return accumulated / safeCount;
}
Processing pipelineBoxes mark actual render-pass boundaries.
  1. Source texturesRGB
  2. Field-dependent coma gatherOne fullscreen render pass
    • Build the aspect-correct field basis
    • Generate disk samples
    • Apply the primary-coma ray map
    • Scale by field position
    • Average in linear light
    • Mix and encode to sRGB
  3. Display output

Why these steps are here

  1. Sample a pupil, not a line. A two-dimensional footprint is required to form a head and opening tail.
  2. Use a local field basis. The comet rotates around the optical centre instead of pointing in one screen direction.
  3. Map the wavefront gradient. The3uᵣ² + uₜ² and 2uᵣuₜ terms are the transverse ray error of primary coma.
  4. Grow by field height. The scale vanishes on axis and increases toward the edge.
  5. Bound and normalize. Up to ninety-six deterministic taps scale with the full coma support, mip prefiltering suppresses discrete ghost copies at large radii, and averaging retains mean brightness.

Production renderers can store measured or ray-traced PSFs in a field-dependent atlas.

Notes

  • The night-road image supplies repeatable lamps and window points; the real night-drive video remains available to test the footprint on moving highlights, and the grid verifies field orientation.
  • Real coma depends on aperture, lens prescription, focus, field angle, wavelength, and correction state. The pixel radius and field-growth controls are not calibrated lens measurements.
  • Coma commonly appears with astigmatism and field curvature, but the playground keeps their kernels separate so each cause remains legible.
  • Reversing the tail demonstrates aberration sign; it does not imply that every lens flips direction at the same focus or aperture.

References

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

Edmund Optics — How aberrations affect imaging lenses — manufacturer reference for spherical, astigmatic, field-curvature, and chromatic aberrations.