Chromatic Aberration Polynomial in GLSL

Simulating chromatic aberration in real-time GLSL post-processing involves radially shifting individual color channels (red, green, and blue) to mimic lens dispersion. This article breaks down the standard radial polynomial equations used to displace texture coordinates based on their distance from the lens center and provides a direct, efficient GLSL implementation.

The Radial Distortion Polynomial

Optical chromatic aberration is simulated using a modified form of the Brown-Conrady radial distortion model. Instead of displacing all channels equally, each color channel is scaled by a distinct polynomial factor dependent on the radial distance from the image center.

Given a normalized texture coordinate \(\vec{uv} \in [0, 1]^2\), the center offset vector \(\vec{p}\) and radial distance \(r\) are:

\[\vec{p} = \vec{uv} - 0.5\] \[r = |\vec{p}| = \sqrt{p_x^2 + p_y^2}\]

The radial scaling factor \(S(r)\) is expressed as an even-powered polynomial:

\[S(r) = 1.0 + k_1 r^2 + k_2 r^4\]

To simulate chromatic dispersion, the distortion coefficients \(k_1\) and \(k_2\) differ per color channel. In practice, a simplified second-order approximation is typically sufficient for real-time rendering:

\[\vec{uv}_{channel} = 0.5 + \vec{p} \cdot (1.0 + k_{channel} \cdot r^2)\]

Where:

  • \(k_r\): Red channel coefficient (often positive, pushing red outward)
  • \(k_g\): Green channel coefficient (often zero or minimal, serving as the baseline)
  • \(k_b\): Blue channel coefficient (often negative, pulling blue inward)

GLSL Implementation

Below is a complete, performance-optimized fragment shader implementation that calculates polynomial displacement for each color channel.

#version 330 core

in vec2 vTexCoord;
out vec4 fragColor;

uniform sampler2D uSceneTexture;

// Dispersion intensity factors for each channel
const vec3 kCoefficients = vec3(0.05, 0.0, -0.05);

void main() {
    vec2 center = vec2(0.5);
    vec2 p = vTexCoord - center;
    float r2 = dot(p, p); // Equivalent to r^2, avoiding an expensive sqrt call

    // Calculate displaced UV coordinates using the polynomial: 1.0 + k * r^2
    vec2 uvR = center + p * (1.0 + kCoefficients.r * r2);
    vec2 uvG = center + p * (1.0 + kCoefficients.g * r2);
    vec2 uvB = center + p * (1.0 + kCoefficients.b * r2);

    // Sample texture separately for each channel
    float r = texture(uSceneTexture, uvR).r;
    float g = texture(uSceneTexture, uvG).g;
    float b = texture(uSceneTexture, uvB).b;

    fragColor = vec4(r, g, b, 1.0);
}

Performance Considerations

Using dot(p, p) directly yields \(r^2\), bypassing the square root operation and eliminating redundant mathematical overhead. For barrel or pincushion-heavy distortions, a higher-order term (\(k_2 \cdot r^4\)) can be added by squaring \(r^2\) (float r4 = r2 * r2), which introduces negligible computational cost while modeling severe edge dispersion.