CRT Monitor / Retro TV Effect

25 views 0 replies
Live Shader
Loading versions...

Wrote a CRT shader that I'm pretty happy with. It does scanlines, screen curvature, vignette, and a bit of chromatic aberration to sell the old-TV look. I'm using it as a post-process pass for a pixel art game and it adds a lot of character without being too heavy.

The barrel distortion is the part that took the most tuning. Too much and it looks like a fisheye lens, too little and you can't tell it's curved. The 0.15 value I landed on feels right for a 4:3 aspect ratio CRT.

precision mediump float;

uniform vec2 iResolution;
uniform float iTime;

// Barrel distortion to simulate curved CRT glass
vec2 crtCurve(vec2 uv, float amount) {
    uv = uv * 2.0 - 1.0;
    vec2 offset = abs(uv.yx) / vec2(6.0, 4.0);
    uv = uv + uv * offset * offset * amount;
    uv = uv * 0.5 + 0.5;
    return uv;
}

void main() {
    vec2 uv = gl_FragCoord.xy / iResolution.xy;
    
    // Apply barrel distortion
    vec2 crtUV = crtCurve(uv, 0.15);
    
    // Kill pixels outside the curved screen area
    if (crtUV.x < 0.0 || crtUV.x > 1.0 || crtUV.y < 0.0 || crtUV.y > 1.0) {
        gl_FragColor = vec4(0.0);
        return;
    }
    
    // Chromatic aberration: offset R and B channels slightly
    float aberration = 0.003;
    float r = 0.5 + 0.5 * sin(crtUV.x * 12.0 + iTime * 0.7);
    float g = 0.5 + 0.5 * sin(crtUV.y * 10.0 - iTime * 0.5);
    float b = 0.5 + 0.5 * sin((crtUV.x + crtUV.y) * 8.0 + iTime * 1.1);
    
    // Shift channels for chromatic split
    vec2 uvR = crtCurve(uv + vec2(aberration, 0.0), 0.15);
    vec2 uvB = crtCurve(uv - vec2(aberration, 0.0), 0.15);
    float rr = 0.5 + 0.5 * sin(uvR.x * 12.0 + iTime * 0.7);
    float bb = 0.5 + 0.5 * sin((uvB.x + uvB.y) * 8.0 + iTime * 1.1);
    vec3 col = vec3(rr, g, bb);
    
    // Scanlines
    float scanline = sin(crtUV.y * iResolution.y * 1.5) * 0.5 + 0.5;
    scanline = pow(scanline, 1.8) * 0.3 + 0.7;
    col *= scanline;
    
    // Horizontal sync wobble (subtle)
    float flicker = 1.0 + 0.015 * sin(iTime * 60.0 + crtUV.y * 40.0);
    col *= flicker;
    
    // Vignette darkening at edges
    vec2 vig = crtUV * (1.0 - crtUV);
    float vigAmount = vig.x * vig.y * 15.0;
    vigAmount = pow(vigAmount, 0.25);
    col *= vigAmount;
    
    // Phosphor glow: faint RGB subpixel pattern
    float subpx = mod(gl_FragCoord.x, 3.0);
    vec3 phosphor = vec3(
        step(0.5, subpx) * step(subpx, 1.5),
        step(1.5, subpx) * step(subpx, 2.5),
        step(subpx, 0.5) + step(2.5, subpx)
    );
    col = mix(col, col * phosphor * 1.5, 0.08);
    
    // Slight warm tint like an old monitor
    col *= vec3(1.05, 1.0, 0.92);
    
    gl_FragColor = vec4(col, 1.0);
}

The phosphor subpixel effect is optional but if you zoom in it gives you that RGB stripe pattern you see on old CRT screens. On a pixel art game running at low res it's a nice touch.

One thing I haven't figured out is how to do the rolling horizontal bar artifact that old TVs get when the v-hold is drifting. Anyone tried that?