Simulating Chromatic Aberration in DirectX Shaders
This article explores the fundamentals of chromatic aberration as an optical phenomenon and explains how to reproduce it in real-time computer graphics. Readers will learn the physical principles behind color fringing, the mathematical logic required to simulate it as a screen-space post-processing effect, and the specific implementation steps for writing a pixel offset shader using High-Level Shader Language (HLSL) in DirectX.
Understanding Chromatic Aberration
Chromatic aberration is an optical distortion that occurs in real-world camera lenses when a lens fails to focus all wavelengths of light onto the same focal point. This happens because optical media (such as glass) have a wavelength-dependent refractive index—a property known as dispersion. Shorter wavelengths of light (such as blue) bend more sharply than longer wavelengths (such as red).
In photography, this manifests as colored borders or "fringing" along high-contrast edges, particularly near the periphery of the image where the curvature of the lens is most pronounced. In real-time rendering, simulating chromatic aberration adds a cinematic or photographic layer of realism, conveys camera damage, or enhances visual impact in stylized scenes.
Simulation via Pixel Offset Post-Processing
In digital rendering, chromatic aberration is implemented as a screen-space post-processing pass executed after the main scene has been rendered to a render target texture.
Rather than simulating complex physical light refractions, real-time renderers approximate the effect by decoupling the primary color channels (Red, Green, and Blue) of the rendered frame. By sampling the color channels at slightly different texture coordinates (\(UVs\)) across the screen, the shader artificially separates the color spectrum.
Because physical lens distortion increases radially outward from the optical center, the magnitude of the coordinate offset is calculated relative to the screen's center point \((0.5, 0.5)\). Pixels near the center remain largely aligned and sharp, while pixels near the boundaries experience the strongest separation.
HLSL Implementation in DirectX
In DirectX, the effect is written within an HLSL pixel shader running over a full-screen quad. The implementation requires the screen texture and a sampler state, along with parameters controlling the distortion strength.
1. Calculating the Radial Vector
The shader first calculates the directional vector from the center of the screen to the current pixel's texture coordinate:
float2 center = float2(0.5f, 0.5f);
float2 dir = input.UV - center;
float dist = length(dir);
2. Determining the Offset Magnitude
To mirror physical lenses, the distortion should increase non-linearly toward the edges. Normalizing the direction vector and scaling it by the distance squared creates a smooth radial curve:
float2 offset = normalize(dir) * (dist * dist) * DistortionStrength;
3. Independent Channel Sampling
The final color is constructed by sampling the scene texture three separate times. The green channel typically remains unaltered at the base UV coordinate to maintain luminance stability, while the red and blue channels are displaced in opposite directions along the radial vector:
Texture2D SceneTexture : register(t0);
SamplerState LinearSampler : register(s0);
cbuffer AberrationBuffer : register(b0)
{
float DistortionStrength;
float3 Padding;
};
struct PS_INPUT
{
float4 Position : SV_POSITION;
float2 UV : TEXCOORD0;
};
float4 PS_ChromaticAberration(PS_INPUT input) : SV_Target
{
float2 center = float2(0.5f, 0.5f);
float2 dir = input.UV - center;
float dist = length(dir);
// Prevent division by zero at the exact center
if (dist < 0.0001f)
{
return SceneTexture.Sample(LinearSampler, input.UV);
}
float2 offset = (dir / dist) * (dist * dist) * DistortionStrength;
// Sample channels independently
float r = SceneTexture.Sample(LinearSampler, input.UV - offset).r;
float g = SceneTexture.Sample(LinearSampler, input.UV).g;
float b = SceneTexture.Sample(LinearSampler, input.UV + offset).b;
float a = SceneTexture.Sample(LinearSampler, input.UV).a;
return float4(r, g, b, a);
}
Performance and Optimization Considerations
Executing three distinct texture lookups per pixel increases texture bandwidth requirements compared to a standard pass-through shader. When targeting bandwidth-constrained hardware, developers can combine chromatic aberration with other screen-space passes—such as vignette, barrel distortion, or color grading—into a single combined pixel shader pass to maximize cache efficiency and eliminate redundant render passes.