Vignette

Vignette darkens or brightens the image edges with a position-dependent multiplicative mask anchored at the center. It is one of the simplest adjustments in AgX but a staple of nearly every film-look preset because it draws the viewer's eye toward the subject.

How it works

The mask is built from the pixel's normalized distance to the image center. For each pixel (x, y) in a w × h image, the algorithm computes:

dx = (x - half_w) * inv_x
dy = (y - half_h) * inv_y
d²  = dx² + dy²
base   = clamp(1 - d², 0, 1)
factor = base²
multiplier = 1 + strength * (1 - factor)
output_channel = clamp(input_channel * multiplier, 0, 1)

with strength = amount / 100.0. At the center, factor = 1.0 and the multiplier is exactly 1.0 (no change). As factor falls toward 0.0 near the edges, the multiplier approaches 1.0 + strength, so the border darkens uniformly across RGB for negative amount and brightens for positive amount. The trailing per-channel clamp handles the small slice of values that would otherwise leave the displayable range.

The shape is chosen by the inv_x/inv_y precompute. Elliptical mode uses inv_x = 1 / half_w and inv_y = 1 / half_h, so the four edge midpoints reach d² = 1 at the same time and the fall-off matches the image aspect ratio. Circular mode uses a single radius — R = max(half_w, half_h) — for both axes. On a non-square image that leaves the short edges less affected at their midpoints (because they sit closer to the center than R) and the corners more affected (because they extend past the circle boundary; the clamp keeps factor from going negative there).

The squaring factor = base² gives a soft fall-off with a slightly stronger core than a linear 1 - d² would, and avoids a hard ring at the boundary. AgX hard-codes this curve rather than exposing it as a slider — see "Why we chose it" below.

On the CPU path, VignettePrecomputed::new caches half_w, half_h, the per-axis reciprocals, and the normalized strength once per render. apply_vignette_pre reuses those cached values, so the hot path is a handful of multiplies plus the clamp. The GPU shader reproduces the same mask equation but recomputes the geometry terms per invocation rather than sharing a struct.

Why we chose it

This is a creative vignette (a stylistic effect applied late in the pipeline), not a lens-correction vignette (undoing optical falloff early). The two have different placements: lens correction runs on linear-light data near the start of the pipeline, before tonal and color adjustments, so it cancels a physical artifact before later math amplifies it. Creative vignette runs in the gamma Rec.2020 working space late in the pipeline, after every tonal and color adjustment, so it shapes the final perceptual image the way a darkroom dodge or a software wash would. AgX puts this stage right before the final gamma-to-linear conversion, matching where Lightroom and Capture One place their "Effects" vignette.

The two-parameter API (amount, shape) is deliberate. Lightroom and Capture One expose midpoint, feather, roundness, and highlight priority, but most preset authors only ever touch amount and shape, and the additional parameters compound surprises in batch processing. The hard-coded factor = base² curve gives a result that consistently reads as "vignette" without tuning. If a preset library later demands midpoint or feather, they can be added as opt-in Option<f32> fields without breaking existing presets.

Two shape options cover the common cases. Elliptical falloff is the right default because it darkens all four image edges evenly. Circular falloff approximates real lens image-circle behavior — the lens projects a disc onto the rectangular sensor, so the corners receive less light than the centers of the long edges — and is useful for recreating that look on already-corrected files. Off-center placement is intentionally not supported; the effect is anchored to the image midpoint.

Parameters and constants

Parameter / constantValueRoleSensitivity
amount (preset)f32, expected -100.0..=+100.0, default 0.0Strength. Negative darkens, positive brightens, 0.0 is identity (early-out).Linear in the multiplier — ±50 halves or doubles the edge brightness; ±100 reaches 0.0 or 2.0 at the corner before clamping. Values outside the expected range extrapolate rather than error out.
shape (preset)enum Elliptical (default) or CircularFalloff geometry.Elliptical = even edge darkening regardless of aspect ratio; Circular = stronger short-edge / corner effect on non-square images.
Falloff exponent2 (hardcoded — factor = base * base)Smoothness of the radial transition.Higher exponents push the effect toward the edges, leaving more of the center untouched; lower exponents spread the falloff inward. Not exposed because adjusting it in a preset rarely beats tweaking amount.
Circular radius ruleR = max(half_w, half_h)Single radius for both axes in Circular mode.Picking min instead would clip the corners exactly to the image; picking max is the convention that mimics real lenses.

Output is per-channel-clamped to [0.0, 1.0] after multiplication so a strong brightening or out-of-range upstream value cannot push a channel past displayable bounds.

Beyond the expected range: vignette does not preset-validate amount, so out-of-range values reach the algorithm directly. The formula extrapolates linearly — amount = 200 gives a corner multiplier of 3.0 before clamping, amount = -200 gives -1.0 (everything in the corner clamps to black). Values past ±100 quickly saturate against the per-channel clamp and stop being visually proportional. shape accepts only the two enum variants; anything else fails preset parsing.

Preset-slider mapping

[vignette]
amount = -30.0          # darkens edges
shape  = "circular"     # optional; defaults to "elliptical"

amount maps linearly to strength: strength = amount / 100.0. A preset that omits [vignette] entirely, or sets amount = 0.0, takes the early-out path in apply_vignette and skips the multiplication loop. Preset composition (merge/materialize) treats the two fields independently — a child preset can override amount without touching shape.

Source

The CPU and GPU implementations follow the same mask equation. The CPU path precomputes VignettePrecomputed once per render; the GPU path recomputes the per-pixel geometry inline.

References

No canonical external paper applies — this is a standard creative-vignette formulation rather than a published algorithm. The motivation and parameter choices are documented in docs/plans/2026-03-18-vignette-design.md.

See also