Defocus

Why a scene point becomes a circle of confusion when its image plane does not coincide with the sensor — and what a uniform disk blur can and cannot reproduce.

Visible effect

Points spread and fine detail loses contrast

Fine edges lose contrast, small details spread into neighbouring pixels, and a point highlight becomes a finite patch instead of a point.

Physical defocus is not necessarily uniform across the frame. Scene points at the focused distance can remain sharp while nearer or farther points form different circles of confusion.

Physics

The sensor intersects a converging or diverging ray cone

A sensor behind the best-focus plane records a circle of confusion Parallel rays pass through a lens, converge to one point at the best image plane, then diverge before reaching a displaced sensor. Their footprint on the sensor is a circle rather than a point.lensbest focussensor planecircle ofconfusion cimage distance v
The lens still brings one scene point to one best-focus point, but the sensor sits behind that plane. The ray cone has opened again by the time it reaches the sensor, so the point is recorded as a circle of confusion.

A lens forms the sharp image of each scene point at a particular image distance. When the sensor does not coincide with that plane, it intersects the ray bundle before or after convergence. The bundle then covers a finite area called the circle of confusion.

Mathematics

Sensor displacement sets the circle of confusion

In a paraxial cone model, let D be the effective pupil diameter, v the distance to the best image plane, andv_s the actual sensor distance. Similar triangles give the circle-of-confusion diameter:

The diameter is zero at best focus, grows with aperture diameter, and grows with the absolute image-plane error. A real camera derives a different value for each scene depth through the lens equation. The playground deliberately maps one chosen display-space radius to every pixel so the convolution can be inspected in isolation.

Shader

GLSL disk gather

This implementation gathers deterministic samples over a circular kernel. It approximates an image-wide circle of confusion and keeps the accumulated energy normalized.

defocusDiskGLSL
// WHAT: Approximate circular defocus by averaging source energy over a disk.
// HOW: Place taps with a golden-angle spiral, prefilter wide radii with a mip
// level, decode every sample to linear light, then normalize the sum.
// WHY: Equal-area disk samples approximate a circular point-spread function
// without the rings or directional bias of a regular sample grid.
const int DEFOCUS_MAX_SAMPLES = 96;
const float DEFOCUS_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)
  );
}

vec3 defocusDisk(
  sampler2D source,
  vec2 sourceUv,
  vec2 sampleStep,
  float radiusPx,
  int sampleCount
) {
  vec3 accumulated = vec3(0.0);
  float count = float(max(sampleCount, 1));
  // Wide disks need prefiltering because the bounded tap count is intentionally sparse.
  float sampleLod = max(0.0, log2(max(radiusPx, 1.0)) - 2.0);

  for (int index = 0; index < DEFOCUS_MAX_SAMPLES; index += 1) {
    if (index >= sampleCount) break;
    // sqrt converts a uniform fraction into uniform area across the disk.
    float fraction = (float(index) + 0.5) / count;
    float sampleRadius = sqrt(fraction);
    float angle = float(index) * DEFOCUS_GOLDEN_ANGLE;
    vec2 disk = vec2(cos(angle), sin(angle)) * sampleRadius;
    vec3 encoded = textureLod(
      source,
      sourceUv + disk * sampleStep * radiusPx,
      sampleLod
    ).rgb;
    accumulated += srgbToLinear(encoded);
  }

  // Average in linear light so blur conserves display-referred energy.
  return accumulated / count;
}
Processing pipelineBoxes mark actual render-pass boundaries.
  1. Source texturesRGB
  2. Defocus disk gatherOne fullscreen render pass
    • Generate disk samples
    • Average in linear light
    • Mix the effect
    • Encode to sRGB
  3. Display output

Why these steps are here

  1. Cover the disk, not only its rim. Square-root radial placement gives approximately uniform area density.
  2. Keep the pattern deterministic. Golden-angle rotation avoids a strongly repeated axis without temporal noise.
  3. Normalize the sum. Dividing by the tap count keeps a uniform region at the same energy.
  4. Prefilter wide samples. A radius-derived mip level prevents individual taps from appearing as repeated hard-edged copies when the disk becomes large.
  5. Accumulate in linear light. The runtime decodes each sRGB sample before averaging and encodes once at output.

The tap count rises with radius and remains capped at 96 for predictable browser cost.

Notes

  • Defocus and focus breathing are different effects: defocus changes the point-spread function, while breathing changes magnification as focus moves.
  • Aperture shape changes out-of-focus highlights; that is separated into the aperture-bokeh effect rather than hidden in this model.
  • A gather blur cannot reconstruct correct foreground occlusion from a flat image, even when a depth map supplies varying radii.
  • Large radii need more samples or a multi-pass approximation. Production renderers often trade exact disk shape for stable cost.

References

Edmund Optics — Gauging depth of field — connects acceptable blur, resolution, aperture, and object displacement.