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 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.
// 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;
}- Source texturesRGB
- Defocus disk gatherOne fullscreen render pass
- Generate disk samples
- Average in linear light
- Mix the effect
- Encode to sRGB
- Display output
Why these steps are here
- Cover the disk, not only its rim. Square-root radial placement gives approximately uniform area density.
- Keep the pattern deterministic. Golden-angle rotation avoids a strongly repeated axis without temporal noise.
- Normalize the sum. Dividing by the tap count keeps a uniform region at the same energy.
- Prefilter wide samples. A radius-derived mip level prevents individual taps from appearing as repeated hard-edged copies when the disk becomes large.
- 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.