Optical Vignette Shading with Cosine Power in GLSL

This article explains how to create realistic optical vignette shading in GLSL using the cosine power falloff law. Optical vignetting simulates the natural attenuation of light as it passes through a lens at oblique angles toward the edges of a sensor. Below is the theoretical mathematical derivation based on physical optics, along with an optimized implementation in GLSL.

The Mathematical Model

In physical photography, natural illumination falloff toward the image perimeter obeys the Cosine-Fourth Law (\(cos^4 \theta\)), which can be generalized into an arbitrary cosine power falloff:

\[V = \cos^n(\theta)\]

Where:

  • \(V\) is the resulting vignette attenuation factor in the range \([0, 1]\).
  • \(\theta\) is the angle between the incident light ray and the optical axis.
  • \(n\) is the falloff exponent (physically, \(n = 4\) for standard flat-sensor lenses, but it can be tuned for artistic intensity).

To compute \(\cos(\theta)\) directly from 2D screen coordinates, consider the right triangle formed by the lens center, the sensor center, and the current pixel:

\[\tan(\theta) = \frac{r}{d}\]

Where:

  • \(r\) is the radial distance from the optical center (typically \((0,0)\) in centered UV space).
  • \(d\) is the simulated focal distance (or effective lens distance relative to the sensor size).

Using the trigonometric identity \(\cos(\arctan(x)) = \frac{1}{\sqrt{1 + x^2}}\), the cosine can be represented without calling expensive trigonometric functions:

\[\cos(\theta) = \frac{1}{\sqrt{1 + \left(\frac{r}{d}\right)^2}}\]

Substituting this into the cosine power formula produces the direct evaluation formula:

\[V = \left( \frac{1}{\sqrt{1 + \left(\frac{r}{d}\right)^2}} \right)^n = \left( 1 + \frac{r^2}{d^2} \right)^{-\frac{n}{2}}\]

GLSL Implementation

In GLSL, the formula can be computed efficiently by using the squared distance (dot(coord, coord)) and the pow() function, completely bypassing square roots and trigonometric operations:

// Calculates optical vignette attenuation using cosine power falloff.
// uv: Normalized texture coordinates [0.0, 1.0]
// aspect: Screen aspect ratio (width / height) to ensure circular falloff
// focalLength: Simulated lens distance relative to sensor (e.g., 0.8 to 1.5)
// power: Falloff exponent (physically 4.0, or customized for effect)
float opticalVignette(vec2 uv, float aspect, float focalLength, float power) {
    // 1. Shift UV coordinates so the center is at (0, 0)
    vec2 coord = uv - vec2(0.5);
    
    // 2. Correct for aspect ratio to maintain a circular profile
    coord.x *= aspect;
    
    // 3. Compute r^2 (squared distance from optical center)
    float r2 = dot(coord, coord);
    
    // 4. Compute tan^2(theta) = r^2 / d^2
    float tanTheta2 = r2 / (focalLength * focalLength);
    
    // 5. Evaluate V = (1 + tan^2(theta))^(-n / 2)
    float vignette = pow(1.0 + tanTheta2, -power * 0.5);
    
    return clamp(vignette, 0.0, 1.0);
}

Parameter Adjustment

  • focalLength (\(d\)): Controls the breadth of the vignette. Smaller values simulate wide-angle lenses with steeper falloff; larger values simulate telephoto lenses with minimal falloff.
  • power (\(n\)): Controls the rate of darkening. A value of 4.0 strictly matches the theoretical natural illumination falloff of a thin lens on a planar sensor. Increasing this value steepens the contrast between the center and the corners.