Skip to main content
GameDev.net gamedev.net

Radial Chromatic Aberration

by GameDev.net · GLSL ES 3.00 (WebGL2) · 30 Aug 2026

Use in your engine

Run the shader to adjust these controls.

What it demonstrates

Chromatic aberration separates red, green, and blue samples along a radial direction. The procedural source frame contains hard pale bars, coloured circles, a fine grid, and moving highlights, making even a small channel offset easy to inspect without relying on an external image.

Source Revision 1

Author notes are linked to specific lines.

Shader inputs

void mainImage(out vec4 fragColor, in vec2 fragCoord)

Called once per pixel. Write the colour to fragColor.

iResolution vec3
Viewport size in pixels (z is the pixel aspect ratio).
iTime float
Seconds since the shader started.
iTimeDelta float
Seconds since the previous frame.
iFrameRate float
Frames per second, smoothed.
iFrame int
Frames rendered since the start.
iMouse vec4
Mouse position: xy while held, zw of the last click.
iDate vec4
Year, month, day, and seconds within the day.
iChannel0 sampler2D
Texture bound to channel 0.
iChannel1 sampler2D
Texture bound to channel 1.
iChannel2 sampler2D
Texture bound to channel 2.
iChannel3 sampler2D
Texture bound to channel 3.
iChannelResolution vec3[4]
Pixel size of each bound channel texture.
iChannelTime float[4]
Playback time of each channel, in seconds.
iSampleRate float
Audio sample rate, always 44100.
Common
vec3 renderSource(vec2 p, float time) {
    vec3 color = mix(vec3(0.025, 0.04, 0.08), vec3(0.22, 0.07, 0.16), p.y + 0.5);
    vec2 grid = abs(fract((p + 0.5) * 12.0) - 0.5);
    color += vec3(0.08, 0.14, 0.20) * (1.0 - smoothstep(0.015, 0.035, min(grid.x, grid.y)));
    float ring = abs(length(p - vec2(-0.26, 0.08)) - 0.16);
    color = mix(color, vec3(1.4, 0.82, 0.24), 1.0 - smoothstep(0.008, 0.018, ring));
    vec2 q = abs(p - vec2(0.24, -0.10));
    float box = max(q.x - 0.17, q.y - 0.12);
    color = mix(color, vec3(0.18, 1.15, 0.92), 1.0 - smoothstep(0.0, 0.012, abs(box)));
    float bar = 1.0 - smoothstep(0.018, 0.030, abs(p.x + 0.02));
    color += vec3(1.2) * bar * smoothstep(0.43, 0.35, abs(p.y));
    vec2 spark = vec2(0.30 * sin(time * 0.7), 0.30 * cos(time * 0.5));
    color += vec3(1.8, 1.4, 0.8) * (1.0 - smoothstep(0.01, 0.035, length(p - spark)));
    return color;
}

vec2 sourceUv(vec2 fragCoord) {
    return (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
}
Buffer A1
1 void mainImage(out vec4 fragColor, in vec2 fragCoord) {
2 vec2 p = sourceUv(fragCoord);
3 fragColor = vec4(renderSource(p, iTime), 1.0);
4 }
5
Main Image3
1 uniform float uSeparation; // @param 0.0..12.0 = 4.0 "Separation"
2 uniform float uFalloff; // @param 1.0..3.0 = 2.0 "Radial falloff"
3 uniform vec2 uCenter; // @param 0.35..0.65 = 0.5, 0.5 "Optical center"
4
5 void mainImage(out vec4 fragColor, in vec2 fragCoord) {
6 vec2 uv = fragCoord / iResolution.xy;
7 vec2 radial = uv - uCenter;
8 vec2 offset = radial * pow(max(length(radial), 1e-6), uFalloff - 1.0) * uSeparation / iResolution.xy;
9 vec3 centerSample = texture(iChannel0, uv).rgb;
10 float red = texture(iChannel0, uv + offset).r;
11 float green = centerSample.g;
12 float blue = texture(iChannel0, uv - offset).b;
13 float edgeFade = smoothstep(0.0, 0.08, min(min(uv.x, 1.0 - uv.x), min(uv.y, 1.0 - uv.y)));
14 vec3 color = mix(centerSample, vec3(red, green, blue), edgeFade);
15 fragColor = vec4(color, 1.0);
16 }
17

Inputs for this pass

  • iChannel0 Buffer A
  • iChannel1 Empty
  • iChannel2 Empty
  • iChannel3 Empty

Learn from this shader

How it works

Buffer A renders the clean scene. The image pass measures a fragment's vector from the optical centre, squares its radius, and scales that vector into an offset. Red is sampled slightly farther outward, green at the original coordinate, and blue inward. Because displacement increases toward the corners, the centre stays readable while edges develop the familiar lens fringe. A border fade prevents clamped edge pixels from becoming long colour streaks. The effect uses only three texture reads per pixel, so it remains practical as a small camera flourish.

Try changing

Raise Separation until the three silhouettes become obvious, then reduce it until the fringe only accents high-contrast edges. Move Falloff toward one for a broad linear spread or toward three to confine separation to the corners. Adjust Center to imitate a lens whose optical axis is not exactly at screen centre.

Using it in a game

Bind the rendered scene colour where Buffer A is used here. Apply subtle values during normal play, stronger values for impacts, teleportation, or damaged optics. Run the effect after tone mapping when you want display-space colour fringes, and before interface rendering so text and HUD elements remain crisp. Chromatic aberration is most convincing in motion and easiest to overuse, so expose its strength to camera effects rather than materials.

Explore the techniques

Continue with curated explanations and progressively related examples.

LicenseMIT
Views0
Forks0

Discussion

Loading comments...