Constant VHS

Source
const VHS: &str = r#"
float fx_hash(vec2 p) {
    return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453);
}
vec3 fx_ycc(vec3 c) {
    return vec3(dot(c, vec3(0.299, 0.587, 0.114)),
                dot(c, vec3(-0.168736, -0.331264, 0.5)) + 0.5,
                dot(c, vec3(0.5, -0.418688, -0.081312)) + 0.5);
}
vec3 fx_rgb(vec3 y) {
    float cb = y.y - 0.5;
    float cr = y.z - 0.5;
    return vec3(y.x + 1.402 * cr, y.x - 0.344136 * cb - 0.714136 * cr, y.x + 1.772 * cb);
}
void main() {
    vec2 uv = gl_FragCoord.xy / u_res;
    vec4 src = texture(tex, uv);
    float full = p6 >= 0.5 ? 0.0 : 1.0;   // "colour bleed only" keeps just the chroma stage
    float noise = clamp(p0, 0.0, 1.0) * full;
    float bleed = clamp(p1, 0.0, 1.0);
    float scan = clamp(p2, 0.0, 1.0) * full;
    float jit = clamp(p3, 0.0, 1.0) * full;
    float head = clamp(p4, 0.0, 1.0) * full;
    float ring = clamp(p5, 0.0, 1.0);
    float wear = clamp(p7, 0.0, 1.0) * full;
    float sc = max(u_scale, 0.1);
    float line = floor(gl_FragCoord.y / sc);
    float t = u_time;
    float band = 1.0 - smoothstep(0.0, max(0.05 * head, 0.001), 1.0 - uv.y);
    band *= step(0.001, head);
    float j = (fx_hash(vec2(line, floor(t * 30.0))) - 0.5) * jit * 0.02
            + sin(line * 0.31 + t * 6.0) * jit * 0.002
            + band * head * 0.08 * (fx_hash(vec2(line, floor(t * 12.0))) * 2.0 - 1.0);
    vec2 suv = clamp(vec2(uv.x + j, uv.y), vec2(0.0), vec2(1.0));
    vec3 base = texture(tex, suv).rgb;
    float bpx = bleed * 6.0 * sc / max(u_res.x, 1.0);
    vec3 b1 = texture(tex, clamp(suv - vec2(bpx, 0.0), 0.0, 1.0)).rgb;
    vec3 b2 = texture(tex, clamp(suv - vec2(bpx * 2.0, 0.0), 0.0, 1.0)).rgb;
    vec3 y = fx_ycc(base);
    y.yz = mix(y.yz, fx_ycc((b1 + b2) * 0.5).yz, bleed);
    y.x += (y.x - fx_ycc(b1).x) * ring * 1.5;
    y.x *= 1.0 - scan * 0.5 * (0.5 + 0.5 * cos(gl_FragCoord.y * 3.14159265 / sc));
    y.x += (fx_hash(gl_FragCoord.xy + vec2(t * 97.0, t * 31.0)) - 0.5) * noise * 0.25;
    if (wear > 0.0 && fx_hash(vec2(floor(line * 0.5), floor(t * 6.0))) > 1.0 - wear * 0.06) {
        float drop = fx_hash(vec2(floor(uv.x * 40.0), line));
        y.x = mix(y.x, 0.75 + 0.25 * drop, 0.7);
        y.yz = mix(y.yz, vec2(0.5), 0.8);
    }
    y.x = mix(y.x, 0.45 + 0.55 * fx_hash(gl_FragCoord.xy + vec2(t)), band * 0.8);
    y.yz = mix(y.yz, vec2(0.5), band);
    vec4 res = vec4(clamp(fx_rgb(y), 0.0, 1.0), src.a);
    float m = u_has_mask > 0.5 ? texture(u_mask, uv).r : 1.0;
    out_color = mix(src, res, m);
}
"#;
Expand description

One-pass VHS: per-line tracking jitter, chroma bleed to the right of an edge, luma noise, scanlines, sharpening ringing, a head-switching band at the bottom and tape-wear dropouts. Cheap approximations of what ntsc-rs models properly - no line-rate filtering, no real comb filter.