Chromatic aberration
Purpose
A "sample" isn't a single atomic thing — it's three independent lookups for R, G, B. Offset those lookups differently and you get the prismatic color-fringe that defines lens error, cheap VHS, and digital glitch.
Key insight
Up to now we've treated each sample as one call returning one color. In practice the GPU returns three numbers at once (plus alpha), but nothing stops the shader from doing three separate samples and keeping only one channel of each.
If the red sample is at UV + a tiny radial offset, the green sample at UV, and the blue sample at UV − that offset, the three channels disagree on where things are — and the eye reads that disagreement as rainbow fringing on every edge. Real lenses do this naturally because different wavelengths refract differently; most CRT, film, and retro looks bolt it on because it sells analog-ness cheaply.
Break it
Crank fringe to the maximum. The image separates into three clearly distinguishable red, green, and blue ghosts. Teaches: the three samples are literally three copies of the image. At subtle offsets your brain fuses them into a single image with fringes; at large offsets the fusion fails and you see the three independent lookups that were there the whole time. This is the cleanest possible demonstration that an RGB sample is three samples stapled together.
Direct Claude
uniform float u_fringe;
vec3 scene(vec2 uv) {
vec3 col = mix(vec3(0.18, 0.10, 0.28), vec3(0.95, 0.48, 0.32), uv.y);
col = mix(col, vec3(0.25, 0.60, 0.85), smoothstep(0.55, 0.98, uv.y));
float sun = 1.0 - smoothstep(0.09, 0.11, length(uv - vec2(0.5, 0.55)));
col = mix(col, vec3(1.0, 0.93, 0.68), sun);
float mntn = 0.40 + 0.05 * sin(uv.x * 18.0) + 0.08 * sin(uv.x * 5.0 + 2.0);
col = mix(col, vec3(0.10, 0.06, 0.20), step(uv.y, mntn) * step(uv.y, 0.5));
if (uv.y < 0.5) {
float yy = (0.5 - uv.y);
float hz = smoothstep(0.004, 0.0, abs(fract(pow(yy, 0.7) * 8.0 - u_time * 0.25) - 0.5));
float vl = smoothstep(0.004, 0.0, abs(fract((uv.x - 0.5) / max(yy, 0.02) * 4.0) - 0.5));
col = mix(col, vec3(0.95, 0.30, 0.75), max(hz, vl) * 0.9);
}
return col;
}
void main() {
vec2 uv = gl_FragCoord.xy / u_resolution;
// Radial offset direction (from center). This makes CA strongest at the edges —
// the physically accurate behavior of a real lens.
vec2 dir = uv - 0.5;
// COORDINATE LAYER — three separate samples, each at a different offset.
// COLOR LAYER — keep only one channel from each sample and recombine.
float r = scene(uv + dir * u_fringe).r;
float g = scene(uv).g;
float b = scene(uv - dir * u_fringe).b;
gl_FragColor = vec4(r, g, b, 1.0);
}