CRT + VHS Effects (Compatibility Mode Safe)

This is sort of an all-in-one CRT + VHS Effect shader made up of a variety of CC0 shaders from other users. It has been reworked to be Compatibility Mode safe, making it perfect for most any project. 

Quick Setup:
1: Create a ColorRect. Under layout, set Anchors Preset to “Full Rect”.

2: Create a new shader material for the ColorRect, set its shader to my CRT shader.

3: Create a SubViewport (Must be nested inside a SubViewportContainer). Almost everything else should be a child of the SubViewport.

4: After creating the SubViewport, go back to the material on the ColorRect. Right click the tex parameter and create new ViewportTexture. It will prompt you to select a SubViewport. There should be only the one you made earlier. Select that one.

5: Adjust the size of the ViewportTexture with the SubViewport as desired.

[Click to see an image of my setup heirarchy for easy understanding.]

Shader code
// CRT + VHS Effects (Compatibility Mode Safe) V1
// ==== This shader has been built from various existing shader projects in addition to my own concepts. 
// ==== Certain aspects have been altered to allow this to work in Godot's Compatibility Mode.
//
// ATTRIBUTION:
// Harrison Allen - CRT with Luminance Preservation - https://godotshaders.com/shader/crt-with-luminance-preservation/
// LazarusOverlook - VHS Post Processing - https://godotshaders.com/shader/vhs-post-processing/
// c64cosmin - Realistic CRT Shader - https://godotshaders.com/shader/realistic-crt-shader/

shader_type canvas_item;

// ===== Source (SubViewport texture) =====
uniform sampler2D tex;

// ===== Edge pinning =====
uniform bool  edge_pinning = false; // OFF by default
uniform float edge_pin_power = 2.0; // higher = stronger pin to edges

// ===== Roll =====
uniform bool  roll = true;
uniform float roll_speed = 1.6;
uniform float roll_size = 2.0;
uniform float roll_variation = 0.919;
uniform float distort_intensity = 0.005;

// ===== Screen "breathing" (bright scenes enlarge image) =====
uniform float breathing_strength  = 0.02; // typical 0.0..0.05
uniform float breathing_softness  = 1.0;  // 0=linear, 1=smoothstep, 2=extra-smooth
uniform float breathing_bias      = 0.0;  // -0.5..0.5, shifts trigger point

// ===== VHS-style crease =====
uniform float vhs_crease_smear        = 0.2;
uniform float vhs_crease_intensity    = 0.2;
uniform float vhs_crease_jitter       = 0.10;
uniform float vhs_crease_speed        = 0.5;
uniform float vhs_crease_discolor_amt = 1.0;
uniform float vhs_bottom_thickness    = 0.025; // fraction of screen height
uniform float vhs_bottom_jitter_px    = 6.0;   // in source pixels

// ===== VHS YIQ multi-tap prefilter =====
uniform float vhs_mtap_strength = 0.0;  // 0..1 blend
uniform float vhs_chroma_twist  = 0.0;  // base I/Q rotation (radians, small)
uniform float vhs_tap_spread_px = 1.0;  // tap spacing in source pixels
uniform int   vhs_samples       = 3;    // 2..5 taps
uniform float sparkle_intensity = 0.0;  // sparse additive luma specks (0..1)

// ===== Optics & chromatic aberration =====
uniform float optics_compensation_r = -0.01;
uniform float optics_compensation_g = 0.0;
uniform float optics_compensation_b = 0.0;
uniform float aberration = 0.0;   // small +/- x shift per channel

// ===== Discolor (sat/contrast) =====
uniform bool  discolor = false;
uniform float saturation = 0.5;    // 0..1 toward gray
uniform float contrast   = 1.2;    // >1 increases contrast

// ===== Static noise =====
uniform float static_noise_intensity = 0.02; // static noise (0..1)

// ===== Core CRT parameters =====
uniform float curve = 0.0;                 // overall optics compensation curve
uniform float aspect = 0.563;              // height/width
uniform float sharpness = 0.667;
uniform float color_offset = 0.0;         
uniform float min_scanline_thickness = 0.5;
uniform float wobble_strength = 0.2;
uniform float virtual_scanlines = 324.0;   // 0 = use texture height

// Mask controls
uniform int   mask_type = 3;               // 0=Dots,1=Aperture,2=Wide,3=WideSoft,4=Slot,5=Null
uniform float mask_brightness = 1.0;       // preserve mask detail in brights (0..1)
uniform float scanline_brightness = 1.0;   // preserve scanline detail in brights (0.5..1)

// Vignette
uniform float vignette_intensity = 0.4;
uniform float vignette_opacity   = 0.5;

// ===== Phosphor defects =====
uniform bool  dead_cells_on = false;
uniform float dead_seed      = 1.0;
uniform float dead_cell_px   = 30.0;
uniform float dead_radius_px = 1.0;
uniform float dead_strength  = 1.0;  // 1 = fully dead

uniform bool  hot_cells_on   = false;
uniform float hot_seed       = 1.0;
uniform float hot_cell_px    = 50.0;
uniform float hot_radius_px  = 1.0;
uniform float hot_intensity  = 0.615;
uniform float hot_flicker    = 0.8;  // 0..1

varying float wobble;

// ========================= helpers =========================
void vertex() {
    // explicit 2π to avoid TAU issues in compatibility mode
    wobble = cos(TIME * 6.283185307179586 * 15.0) * wobble_strength / 4000.0;
}

// Given a normalized horizontal coordinate x01 in [0..1], return a "pin" weight
// that is 1.0 at the center (x=0.5) and falls to ~0.0 near the left/right edges.
// This is used to attenuate horizontal offsets (wobble, roll, chromatic aberration)
// so pixels don’t appear to slide beyond the bezel (physical frame).
float pin_factor(float x01) {
    float e = abs(x01 - 0.5) * 2.0; if (e < 0.0) e = 0.0; if (e > 1.0) e = 1.0;
    float f = 1.0 - e;
    return pow(f, edge_pin_power);
}

// Barrel/pincushion warp in normalized space, with aspect-correct x
// _aspect = height/width of the final display (so x is scaled to match y)
// _curve  = warp strength (>0 = barrel/curved outward, <0 = pincushion/inward)
vec2 warp_curve(vec2 uv, float _aspect, float _curve) {
    uv -= 0.5;
    uv.x /= _aspect;
    float warping = dot(uv, uv) * _curve;
    warping -= _curve * 0.25;
    uv /= (1.0 - warping);
    uv.x *= _aspect;
    uv += 0.5;
    return uv;
}

// Curve-aware radial optics compensation.
// Adds a small barrel/pincushion adjustment *on top of* the CRT curve warp,
// while taking the current curve into account so strength feels consistent
// across the screen.
vec2 optics_comp_curved(vec2 uv, float amount, float _aspect, float _curve) {
    vec2 d = uv - 0.5;
    vec2 e = d; e.x /= _aspect;
    float dist2 = dot(e, e);
    float denom = 1.0 - (dist2 * _curve - _curve * 0.25);
    if (denom < 0.2) denom = 0.2;
    float gain = 1.0 / denom;
    return uv + d * dist2 * amount * gain;
}

// Tiny, deterministic 2D hash → pseudo-random vec2 in [-1, +1].
// - Input: any 2D coordinate (UVs, pixel coords, grid id, etc.).
// - Process: project uv onto two different directions (dot with fixed
//   vectors) to decorrelate x/y, run a sine “hash” and keep the fractional
//   part, then remap from [0,1] to [-1,1].
vec2 rnd_hash(vec2 uv) {
    uv = vec2(dot(uv, vec2(127.1, 311.7)),
              dot(uv, vec2(269.5, 183.3)));
    return -1.0 + 2.0 * fract(sin(uv) * 43758.5453123);
}
float rnd1(vec2 uv) { return rnd_hash(uv).x * 0.5 + 0.5; }

// Rounded-edge border matte in [0,1]:
// - Returns ~1.0 for pixels well inside the screen area and fades to 0.0
//   near/over the edges, creating a soft bezel/overscan falloff.
// - Uses a rounded-rectangle signed distance approximation so the corners
//   are gently curved instead of perfectly sharp.
// - 'curve' (global) controls the corner radius / softness coupling.
float border_mask(vec2 uv) {
    float r = curve; if (r > 0.08) r = 0.08; if (r < 1e-5) r = 1e-5;
    vec2 abs_uv = abs(uv * 2.0 - 1.0) - vec2(1.0) + r;
    float dist = length(max(vec2(0.0), abs_uv)) / r;
    float sq = smoothstep(0.96, 1.0, dist);
    float outv = 1.0 - sq; if (outv < 0.0) outv = 0.0; if (outv > 1.0) outv = 1.0;
    return outv;
}

// Vignette factor in [0,1] with a center peak and edge falloff.
// - uv:     normalized coords *after* warp/optics (so the vignette follows any bend).
// - intensity: controls how tight/steep the falloff is (larger → darker corners).
// - opac:   additional overall strength multiplier (acts with intensity as a product).
float vignette_fac(vec2 uv, float intensity, float opac) {
    vec2 u = uv; u *= 1.0 - u;
    float v = u.x * u.y * 15.0;
    v = pow(v, intensity * opac);
    return v;
}

// Piecewise-accurate sRGB <-> linear conversion.
vec3 linear_to_srgb(vec3 col) {
    bvec3 cut = lessThan(col, vec3(0.0031318));
    vec3 a = (pow(col, vec3(1.0 / 2.4)) * 1.055) - 0.055;
    vec3 b = col * 12.92;
    return mix(a, b, cut);
}
vec3 srgb_to_linear(vec3 col) {
    bvec3 cut = lessThan(col, vec3(0.04045));
    vec3 a = pow((col + 0.055) / 1.055, vec3(2.4));
    vec3 b = col / 12.92;
    return mix(a, b, cut);
}

// --- YIQ helpers (mat3 columns) ---
vec3 rgb2yiq(vec3 rgb) {
    mat3 M = mat3(
        vec3(0.299,  0.596,  0.211),
        vec3(0.587, -0.274, -0.523),
        vec3(0.114, -0.322,  0.312)
    );
    return M * rgb;
}
vec3 yiq2rgb(vec3 yiq) {
    mat3 M = mat3(
        vec3(1.0,   1.0,    1.0),
        vec3(0.956, -0.272, -1.106),
        vec3(0.621, -0.647,  1.703)
    );
    return M * yiq;
}
vec3 rotate_iq(vec3 yiq, float rot) {
    float c = cos(rot);
    float s = sin(rot);
    float I2 = yiq.g * c - yiq.b * s;
    float Q2 = yiq.g * s + yiq.b * c;
    return vec3(yiq.r, I2, Q2);
}

// ====== Global-average luma (8 point sample; center excluded) for breathing ======
float global_avg_luma() {
    vec3 c1 = srgb_to_linear(texture(tex, vec2(0.2, 0.2)).rgb);
    vec3 c2 = srgb_to_linear(texture(tex, vec2(0.5, 0.2)).rgb);
    vec3 c3 = srgb_to_linear(texture(tex, vec2(0.8, 0.2)).rgb);
    vec3 c4 = srgb_to_linear(texture(tex, vec2(0.2, 0.5)).rgb);
    vec3 c5 = srgb_to_linear(texture(tex, vec2(0.8, 0.5)).rgb);
    vec3 c6 = srgb_to_linear(texture(tex, vec2(0.2, 0.8)).rgb);
    vec3 c7 = srgb_to_linear(texture(tex, vec2(0.5, 0.8)).rgb);
    vec3 c8 = srgb_to_linear(texture(tex, vec2(0.8, 0.8)).rgb);
    float y1 = dot(c1, vec3(0.2126, 0.7152, 0.0722));
    float y2 = dot(c2, vec3(0.2126, 0.7152, 0.0722));
    float y3 = dot(c3, vec3(0.2126, 0.7152, 0.0722));
    float y4 = dot(c4, vec3(0.2126, 0.7152, 0.0722));
    float y5 = dot(c5, vec3(0.2126, 0.7152, 0.0722));
    float y6 = dot(c6, vec3(0.2126, 0.7152, 0.0722));
    float y7 = dot(c7, vec3(0.2126, 0.7152, 0.0722));
    float y8 = dot(c8, vec3(0.2126, 0.7152, 0.0722));
    return (y1+y2+y3+y4+y5+y6+y7+y8) / 8.0;
}

// ====== Phosphor defect helpers ======
float _rand(vec2 p) {
    return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453123);
}
vec2 _rand2(vec2 p) {
    return vec2(_rand(p + vec2(1.7, 9.2)), _rand(p + vec2(8.3, 2.8)));
}
// Channel selection per cell (7 possible combos: R,G,B,RG,RB,GB,RGB)
vec3 _cell_channel_mask(vec2 cell, float seed) {
    float r = _rand(cell + vec2(seed * 5.1, seed * 7.3));
    int idx = int(floor(r * 7.0));
    vec3 m = vec3(0.0);
    if (idx == 0) m = vec3(1.0, 0.0, 0.0);
    else if (idx == 1) m = vec3(0.0, 1.0, 0.0);
    else if (idx == 2) m = vec3(0.0, 0.0, 1.0);
    else if (idx == 3) m = vec3(1.0, 1.0, 0.0);
    else if (idx == 4) m = vec3(1.0, 0.0, 1.0);
    else if (idx == 5) m = vec3(0.0, 1.0, 1.0);
    else               m = vec3(1.0, 1.0, 1.0);
    return m;
}
// Fixed-sparsity spawn (~1%) with soft circular coverage
float _defect_weight(vec2 frag_px, float cell_px, float seed, float radius_px) {
    vec2 cell = floor(frag_px / cell_px);
    float r = _rand(cell + vec2(seed, seed * 1.31));
    float spawn = step(0.99, r); // ~1% of cells
    vec2 ofs = _rand2(cell + vec2(seed * 2.11, -seed * 4.27));
    vec2 center = (cell + ofs) * cell_px;
    float d = length(frag_px - center);
    float aa = 1.0;
    float gate = 1.0 - smoothstep(radius_px, radius_px + aa, d);
    return gate * spawn;
}

// --------- scanline kernel (linear) with pre-defect (mul/add) ---------
vec3 scanlines(vec2 uv, vec3 pre_mul, vec3 pre_add) {
    vec2 uv0 = uv;
    vec2 tex_size = vec2(textureSize(tex, 0));
    vec2 uv_px = uv * tex_size;

    int   y = int(floor(uv_px.y - 0.5));
    float x = floor(uv_px.x);

    float ax = x - 2.0;
    float bx = x - 1.0;
    float cx = x;
    float dx = x + 1.0;
    float ex = x + 2.0;

    vec3 upper_a = texelFetch(tex, ivec2(int(ax), y), 0).rgb;
    vec3 upper_b = texelFetch(tex, ivec2(int(bx), y), 0).rgb;
    vec3 upper_c = texelFetch(tex, ivec2(int(cx), y), 0).rgb;
    vec3 upper_d = texelFetch(tex, ivec2(int(dx), y), 0).rgb;
    vec3 upper_e = texelFetch(tex, ivec2(int(ex), y), 0).rgb;

    y += 1;

    vec3 lower_a = texelFetch(tex, ivec2(int(ax), y), 0).rgb;
    vec3 lower_b = texelFetch(tex, ivec2(int(bx), y), 0).rgb;
    vec3 lower_c = texelFetch(tex, ivec2(int(cx), y), 0).rgb;
    vec3 lower_d = texelFetch(tex, ivec2(int(dx), y), 0).rgb;
    vec3 lower_e = texelFetch(tex, ivec2(int(ex), y), 0).rgb;

    upper_a = srgb_to_linear(upper_a);
    upper_b = srgb_to_linear(upper_b);
    upper_c = srgb_to_linear(upper_c);
    upper_d = srgb_to_linear(upper_d);
    upper_e = srgb_to_linear(upper_e);

    lower_a = srgb_to_linear(lower_a);
    lower_b = srgb_to_linear(lower_b);
    lower_c = srgb_to_linear(lower_c);
    lower_d = srgb_to_linear(lower_d);
    lower_e = srgb_to_linear(lower_e);

    // Apply phosphor defects BEFORE shaping
    upper_a = clamp(upper_a * pre_mul + pre_add, 0.0, 1.0);
    upper_b = clamp(upper_b * pre_mul + pre_add, 0.0, 1.0);
    upper_c = clamp(upper_c * pre_mul + pre_add, 0.0, 1.0);
    upper_d = clamp(upper_d * pre_mul + pre_add, 0.0, 1.0);
    upper_e = clamp(upper_e * pre_mul + pre_add, 0.0, 1.0);

    lower_a = clamp(lower_a * pre_mul + pre_add, 0.0, 1.0);
    lower_b = clamp(lower_b * pre_mul + pre_add, 0.0, 1.0);
    lower_c = clamp(lower_c * pre_mul + pre_add, 0.0, 1.0);
    lower_d = clamp(lower_d * pre_mul + pre_add, 0.0, 1.0);
    lower_e = clamp(lower_e * pre_mul + pre_add, 0.0, 1.0);

    vec3 beam = vec3(uv_px.x - 0.5);
    beam.r -= color_offset;
    beam.b += color_offset;

    vec3 weight_a = smoothstep(1.0, 0.0, (beam - ax) * sharpness);
    vec3 weight_b = smoothstep(1.0, 0.0, (beam - bx) * sharpness);
    vec3 weight_c = smoothstep(1.0, 0.0, abs(beam - cx) * sharpness);
    vec3 weight_d = smoothstep(1.0, 0.0, (dx - beam) * sharpness);
    vec3 weight_e = smoothstep(1.0, 0.0, (ex - beam) * sharpness);

    vec3 upper_col = upper_a * weight_a +
                     upper_b * weight_b +
                     upper_c * weight_c +
                     upper_d * weight_d +
                     upper_e * weight_e;

    vec3 lower_col = lower_a * weight_a +
                     lower_b * weight_b +
                     lower_c * weight_c +
                     lower_d * weight_d +
                     lower_e * weight_e;

    vec3 weight_scaler = vec3(1.0) / (weight_a + weight_b + weight_c + weight_d + weight_e);
    upper_col *= weight_scaler;
    lower_col *= weight_scaler;

    upper_col *= scanline_brightness;
    lower_col *= scanline_brightness;

    vec3 upper_thickness = mix(vec3(min_scanline_thickness), vec3(1.0), upper_col);
    vec3 lower_thickness = mix(vec3(min_scanline_thickness), vec3(1.0), lower_col);

    float lines = vec2(textureSize(tex, 0)).y;
    if (virtual_scanlines > 0.0) {
        lines = virtual_scanlines;
    }
    float saw = fract(uv0.y * lines + 0.5);

    vec3 upper_line = vec3(saw) / upper_thickness;
    upper_line = smoothstep(1.0, 0.0, upper_line);

    vec3 lower_line = vec3(1.0 - saw) / lower_thickness;
    lower_line = smoothstep(1.0, 0.0, lower_line);

    upper_line *= upper_col / upper_thickness;
    lower_line *= lower_col / lower_thickness;

    vec3 combined = upper_line + lower_line;

    vec3 dark_upper = smoothstep(min_scanline_thickness, 0.0, vec3(saw)) * upper_col;
    vec3 dark_lower = smoothstep(min_scanline_thickness, 0.0, vec3(1.0 - saw)) * lower_col;
    vec3 dark_combined = dark_upper + dark_lower;

    float w = 0.9 + 0.1 * clamp(scanline_brightness, 0.0, 1.0);
    return mix(dark_combined, combined, w);
}

// ===== VHS multi-tap prefilter (2..5 taps) =====
vec3 vhs_prefilter_ntap(vec2 uv, float twist, float tap_px, int samples_) {
    vec2 ts = vec2(textureSize(tex, 0)); if (ts.x < 1.0) return vec3(0.0);
    float dx = tap_px / ts.x; vec2 duv = vec2(dx, 0.0);

    vec3 s0 = srgb_to_linear(texture(tex, uv                 ).rgb);
    vec3 s1 = srgb_to_linear(texture(tex, uv - duv           ).rgb);
    vec3 s2 = srgb_to_linear(texture(tex, uv - duv * 2.0     ).rgb);
    vec3 s3 = srgb_to_linear(texture(tex, uv - duv * 3.0     ).rgb);
    vec3 s4 = srgb_to_linear(texture(tex, uv - duv * 4.0     ).rgb);

    vec3 y0 = rgb2yiq(s0), y1 = rgb2yiq(s1), y2 = rgb2yiq(s2), y3 = rgb2yiq(s3), y4 = rgb2yiq(s4);

    float Y=0.0, I=0.0, Q=0.0;
    int n = samples_; if (n < 2) n = 2; if (n > 5) n = 5;

    if (n == 2) {
        float wy0=0.60, wy1=0.40, wc0=0.50, wc1=0.50;
        Y = y0.r*wy0 + y1.r*wy1; I = y0.g*wc0 + y1.g*wc1; Q = y0.b*wc0 + y1.b*wc1;
    } else if (n == 3) {
        float wy0=0.50, wy1=0.30, wy2=0.20; float wc0=0.45, wc1=0.33, wc2=0.22;
        Y = y0.r*wy0 + y1.r*wy1 + y2.r*wy2;
        I = y0.g*wc0 + y1.g*wc1 + y2.g*wc2;
        Q = y0.b*wc0 + y1.b*wc1 + y2.b*wc2;
    } else if (n == 4) {
        float wy0=0.42, wy1=0.28, wy2=0.18, wy3=0.12; float wc0=0.40, wc1=0.26, wc2=0.20, wc3=0.14;
        Y = y0.r*wy0 + y1.r*wy1 + y2.r*wy2 + y3.r*wy3;
        I = y0.g*wc0 + y1.g*wc1 + y2.g*wc2 + y3.g*wc3;
        Q = y0.b*wc0 + y1.b*wc1 + y2.b*wc2 + y3.b*wc3;
    } else {
        float wy0=0.40, wy1=0.25, wy2=0.15, wy3=0.12, wy4=0.08;
        float wc0=0.25, wc1=0.23, wc2=0.20, wc3=0.18, wc4=0.14;
        Y = y0.r*wy0 + y1.r*wy1 + y2.r*wy2 + y3.r*wy3 + y4.r*wy4;
        I = y0.g*wc0 + y1.g*wc1 + y2.g*wc2 + y3.g*wc3 + y4.g*wc4;
        Q = y0.b*wc0 + y1.b*wc1 + y2.b*wc2 + y3.b*wc3 + y4.b*wc4;
    }

    vec3 yiq = vec3(Y, I, Q);
    if (twist != 0.0) yiq = rotate_iq(yiq, twist);

    // Sparkle
    if (sparkle_intensity > 0.0) {
        vec2 g = floor(uv * ts);
        float nsp = rnd1(g + vec2(TIME * 7.173, 3.31));
        if (nsp > 0.995) {
            float s = (nsp - 0.995) * 200.0; if (s > 1.0) s = 1.0;
            yiq.r += s * sparkle_intensity;
        }
    }

    vec3 rgb = yiq2rgb(yiq);
    if (rgb.r < 0.0) rgb.r = 0.0; if (rgb.r > 1.0) rgb.r = 1.0;
    if (rgb.g < 0.0) rgb.g = 0.0; if (rgb.g > 1.0) rgb.g = 1.0;
    if (rgb.b < 0.0) rgb.b = 0.0; if (rgb.b > 1.0) rgb.b = 1.0;
    return rgb;
}

// ----- Mask patterns -----
vec3 _pick4(int idx, vec3 a, vec3 b, vec3 c, vec3 d) {
    if (idx == 0) return a;
    if (idx == 1) return b;
    if (idx == 2) return c;
    return d;
}
vec3 _slot_color(int idx) {
    if (idx == 0)  return vec3(1,0,1);
    if (idx == 1)  return vec3(0,1,0);
    if (idx == 2)  return vec3(1,0,1);
    if (idx == 3)  return vec3(0,1,0);
    if (idx == 4)  return vec3(0,0,1);
    if (idx == 5)  return vec3(0,1,0);
    if (idx == 6)  return vec3(1,0,0);
    if (idx == 7)  return vec3(0,0,0);
    if (idx == 8)  return vec3(1,0,1);
    if (idx == 9)  return vec3(0,1,0);
    if (idx == 10) return vec3(1,0,1);
    if (idx == 11) return vec3(0,1,0);
    if (idx == 12) return vec3(1,0,0);
    if (idx == 13) return vec3(0,0,0);
    if (idx == 14) return vec3(0,0,1);
    return vec3(0,1,0);
}
vec4 generate_mask(vec2 fragcoord) {
    int mx = mask_type;
    if (mx == 0) {
        ivec2 ico = ivec2(floor(fragcoord));
        int idx = (ico.y * 2 + ico.x) % 4;
        vec3 c = _pick4(idx, vec3(1,0,0), vec3(0,1,0), vec3(0,0,1), vec3(0,0,0));
        return vec4(c, 0.25);
    } else if (mx == 1) {
        int idx = int(floor(fragcoord.x)) % 2;
        vec3 c = vec3(0.0);
        if (idx == 0) c = vec3(0,1,0);
        else          c = vec3(1,0,1);
        return vec4(c, 0.5);
    } else if (mx == 2) {
        int idx = int(floor(fragcoord.x)) % 4;
        vec3 c = _pick4(idx, vec3(1,0,0), vec3(0,1,0), vec3(0,0,1), vec3(0,0,0));
        return vec4(c, 0.25);
    } else if (mx == 3) {
        int idx = int(floor(fragcoord.x)) % 4;
        vec3 c = _pick4(idx,
            vec3(1.0, 0.125, 0.0),
            vec3(0.125, 1.0, 0.125),
            vec3(0.0, 0.125, 1.0),
            vec3(0.125, 0.0, 0.125)
        );
        return vec4(c, 0.3125);
    } else if (mx == 4) {
        ivec2 ico = ivec2(floor(fragcoord));
        int ix = ico.x % 4;
        int iy = ico.y % 4;
        int idx = iy * 4 + ix;
        vec3 c = _slot_color(idx);
        return vec4(c, 0.375);
    } else {
        return vec4(0.5);
    }
}
vec3 apply_mask(vec3 linear_color, vec2 fragcoord) {
    vec4 m = generate_mask(fragcoord);
    float dim = m.w + (1.0 - m.w) * mask_brightness;
    linear_color *= dim;

    vec3 target = linear_color / m.w;
    vec3 primary = clamp(target, 0.0, 1.0);

    vec3 highlights = target - primary;
    highlights /= (1.0 / m.w - 1.0);

    primary *= m.rgb;
    primary += highlights * (1.0 - m.rgb);
    return primary;
}

// ========================= main =========================
void fragment() {
    // Overall optics compensation curve
    vec2 uv = warp_curve(UV, aspect, curve * 0.5);

    // Wobble; optionally pin edges
    float pin = 1.0;
    if (edge_pinning) { pin = pin_factor(UV.x); }
    uv.x += wobble * pin;

    // Roll / scan
    float dx = 0.0;
    if (roll) {
        float t = TIME;
        float l1 = sin(UV.y * roll_size - (t * roll_speed)); l1 = smoothstep(0.3, 0.9, l1); l1 *= l1;
        float l2 = sin(UV.y * roll_size * roll_variation - (t * roll_speed * roll_variation)); l2 = smoothstep(0.3, 0.9, l2);
        dx = l1 * l2 * distort_intensity;
        if (edge_pinning) { dx *= pin; }
    }
    vec2 base_uv = uv + vec2(dx, 0.0);

    // ===== Breathing (bright scenes enlarge image) with softness & bias =====
    if (breathing_strength != 0.0) {
        float L = global_avg_luma();
        L = clamp(L + breathing_bias, 0.0, 1.0); // shift trigger
        float s1 = L*L*(3.0 - 2.0*L);            // smoothstep
        float s2 = s1*s1*(3.0 - 2.0*s1);         // extra smooth
        float t  = breathing_softness; if (t < 0.0) t = 0.0; if (t > 2.0) t = 2.0;
        float e  = mix(L, s1, min(t, 1.0));
        e        = mix(e, s2, max(t - 1.0, 0.0));
        float b_scale = 1.0 + breathing_strength * e;
        vec2 d = base_uv - 0.5;
        base_uv = 0.5 + d / b_scale;
    }

    // ===== VHS crease =====
    float jitter_phase = rnd1(vec2(TIME * 0.67, TIME * 0.59)) * 2.0 - 1.0;
    float tc_phase = sin(base_uv.y * 8.0 - (TIME * vhs_crease_speed + vhs_crease_jitter * jitter_phase) * 3.769911184);
    tc_phase = smoothstep(0.9, 0.96, tc_phase);
    float tc_noise = smoothstep(0.3, 1.0, rnd1(vec2(base_uv.y * 4.77, TIME)));
    float tc = tc_phase * tc_noise * vhs_crease_intensity;

    vec2 tsz = vec2(textureSize(tex, 0));
    float smear_dx = (8.0 * vhs_crease_smear) / max(tsz.x, 1.0);
    base_uv.x -= tc * smear_dx;

    float sn_phase = smoothstep(1.0 - vhs_bottom_thickness, 1.0, base_uv.y);
    float sn_jit = (rnd1(vec2(UV.y * 100.0, TIME * 10.0)) - 0.5) * (vhs_bottom_jitter_px / max(tsz.x, 1.0));
    base_uv.x += sn_phase * sn_jit;

    // ===== Phosphor defects =====
    vec3 pre_mul = vec3(1.0);
    vec3 pre_add = vec3(0.0);
    vec2 frag_px = FRAGCOORD.xy;

    if (dead_cells_on) {
        float w_dead = _defect_weight(frag_px, dead_cell_px, dead_seed, dead_radius_px);
        if (w_dead > 0.0) {
            vec2 cell_d = floor(frag_px / dead_cell_px);
            vec3 m_dead = _cell_channel_mask(cell_d, dead_seed);
            vec3 delta = m_dead * (w_dead * dead_strength);
            pre_mul = pre_mul - delta;
            if (pre_mul.r < 0.0) pre_mul.r = 0.0;
            if (pre_mul.g < 0.0) pre_mul.g = 0.0;
            if (pre_mul.b < 0.0) pre_mul.b = 0.0;
        }
    }
    if (hot_cells_on) {
        float w_hot = _defect_weight(frag_px, hot_cell_px, hot_seed, hot_radius_px);
        if (w_hot > 0.0) {
            vec2 cell_h = floor(frag_px / hot_cell_px);
            float rf = _rand(cell_h + vec2(hot_seed * 9.7, TIME * 0.37));
            float flick = (1.0 - hot_flicker) + hot_flicker * rf;
            vec3 m_hot = _cell_channel_mask(cell_h, hot_seed);
            vec3 addv  = m_hot * (w_hot * hot_intensity * flick);
            pre_add = pre_add + addv;
            if (pre_add.r > 1.0) pre_add.r = 1.0;
            if (pre_add.g > 1.0) pre_add.g = 1.0;
            if (pre_add.b > 1.0) pre_add.b = 1.0;
        }
    }

    // ===== Optics per channel + chromatic aberration (curve-aware) =====
    vec2 uv_r = optics_comp_curved(base_uv, optics_compensation_r, aspect, curve);
    vec2 uv_g = optics_comp_curved(base_uv, optics_compensation_g, aspect, curve);
    vec2 uv_b = optics_comp_curved(base_uv, optics_compensation_b, aspect, curve);

    float ca = aberration * 0.1; if (edge_pinning) { ca *= pin; }
    uv_r += vec2( ca, 0.0);
    uv_g += vec2(-ca, 0.0);

    // ===== CRT scanline kernel per channel (linear) with pre-defects =====
    vec3 col_r = scanlines(uv_r, pre_mul, pre_add);
    vec3 col_g = scanlines(uv_g, pre_mul, pre_add);
    vec3 col_b = scanlines(uv_b, pre_mul, pre_add);
    vec3 col   = vec3(col_r.r, col_g.g, col_b.b);

    // ===== VHS multi-tap prefilter blend (uses crease chroma twist) =====
    if (vhs_mtap_strength > 0.0) {
        float crease_rot = (tc_phase * 0.2 + sn_phase * 2.0) * vhs_crease_discolor_amt;
        float total_twist = vhs_chroma_twist + crease_rot;
        vec3 vhs_col = vhs_prefilter_ntap(base_uv, total_twist, vhs_tap_spread_px, vhs_samples);
        col = mix(col, vhs_col, vec3(vhs_mtap_strength));
    }

    // ===== Optional discolor (linear) =====
    if (discolor) {
        float gray = (col.r + col.g + col.b) / 3.0;
        col = mix(col, vec3(gray), vec3(saturation));
        float midpoint = pow(0.5, 2.2);
        col = (col - vec3(midpoint)) * contrast + vec3(midpoint);
    }

    // ===== Phosphor mask (linear) =====
    col = apply_mask(col, FRAGCOORD.xy);

    // ===== Static noise (linear) =====
    if (static_noise_intensity > 0.0) {
        vec2 uv_avg = (uv_r + uv_g + uv_b) / 3.0;
        vec2 tsz2 = vec2(textureSize(tex, 0));
        vec2 cell = ceil(uv_avg * tsz2) / tsz2;
        float r = rnd_hash(cell + fract(TIME)).x; if (r < 0.0) r = 0.0; if (r > 1.0) r = 1.0;
        col += vec3(r) * static_noise_intensity;
        col = clamp(col, vec3(0.0), vec3(1.0));
    }

    // ===== Vignette (linear) =====
    vec2 uv_avg2 = (uv_r + uv_g + uv_b) / 3.0;
    float border = border_mask(uv_avg2); col *= border;
    float vig = vignette_fac(uv_avg2, vignette_intensity, vignette_opacity); col *= vig;

    // Convert to sRGB
    col = linear_to_srgb(col);
    COLOR.rgb = col; COLOR.a = 1.0;
}
Live Preview
Tags
compatibility, CRT, dead cells, dead pixels, display, hot cells, hot pixels, mtap, noise, optics compensation, phosphor, Post processing, ps1, psx, realistic, retro, roll, scanlines, sparkle, tv, VHS, vignette, warp
The shader code and all code snippets in this post are under CC0 license and can be used freely without the author's permission. Images and videos, and assets depicted in those, do not fall under this license. For more info, see our License terms.

Related shaders

guest

1 Comment
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
johncode
johncode
4 months ago

Best CRT + VHS shader yet, thx for your work!