Radial Chromatic Aberration
by GameDev.net · GLSL ES 3.00 (WebGL2) · 30 Aug 2026
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.
Shader inputs
void mainImage(out vec4 fragColor, in vec2 fragCoord)
Called once per pixel. Write the colour to fragColor.
-
iResolutionvec3 - Viewport size in pixels (z is the pixel aspect ratio).
-
iTimefloat - Seconds since the shader started.
-
iTimeDeltafloat - Seconds since the previous frame.
-
iFrameRatefloat - Frames per second, smoothed.
-
iFrameint - Frames rendered since the start.
-
iMousevec4 - Mouse position: xy while held, zw of the last click.
-
iDatevec4 - Year, month, day, and seconds within the day.
-
iChannel0sampler2D - Texture bound to channel 0.
-
iChannel1sampler2D - Texture bound to channel 1.
-
iChannel2sampler2D - Texture bound to channel 2.
-
iChannel3sampler2D - Texture bound to channel 3.
-
iChannelResolutionvec3[4] - Pixel size of each bound channel texture.
-
iChannelTimefloat[4] - Playback time of each channel, in seconds.
-
iSampleRatefloat - 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
Main Image3
uniform float uSeparation; // @param 0.0..12.0 = 4.0 "Separation"
uniform float uFalloff; // @param 1.0..3.0 = 2.0 "Radial falloff"
uniform vec2 uCenter; // @param 0.35..0.65 = 0.5, 0.5 "Optical center"
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = fragCoord / iResolution.xy;
vec2 radial = uv - uCenter;
vec2 offset = radial * pow(max(length(radial), 1e-6), uFalloff - 1.0) * uSeparation / iResolution.xy;
vec3 centerSample = texture(iChannel0, uv).rgb;
float red = texture(iChannel0, uv + offset).r;
float green = centerSample.g;
float blue = texture(iChannel0, uv - offset).b;
float edgeFade = smoothstep(0.0, 0.08, min(min(uv.x, 1.0 - uv.x), min(uv.y, 1.0 - uv.y)));
vec3 color = mix(centerSample, vec3(red, green, blue), edgeFade);
fragColor = vec4(color, 1.0);
}
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.
Discussion