VHS Shader

Create a ColorRect and attach the following shader to it:

Shader code
shader_type canvas_item;

uniform sampler2D screen_texture : hint_screen_texture, repeat_disable, filter_linear;

group_uniforms Signal_Distortion;
uniform float smearing : hint_range(0.0, 1.0) = 0.5;
uniform float color_offset : hint_range(-0.05, 0.05) = 0.005;
uniform float sharpening : hint_range(0.0, 1.0) = 0.4;
uniform float static_noise : hint_range(0.0, 1.0) = 0.08;

group_uniforms Glass_Crack;
uniform vec2 crack_center = vec2(0.15, 0.85);
uniform float crack_intensity : hint_range(0.0, 1.0) = 0.4;
uniform float crack_size : hint_range(0.0, 1.0) = 0.35;

group_uniforms Tape_Artifacts;
uniform float vertical_jitter : hint_range(0.0, 0.1) = 0.002;
uniform float scanline_opacity : hint_range(0.0, 1.0) = 0.1;
uniform float vignette_intensity : hint_range(0.0, 1.0) = 0.3;

float noise(vec2 p) {
	return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453123);
}

// logic for the shattered glass displacement
vec2 get_crack_distortion(vec2 uv, vec2 center, float strength, float size) {
    vec2 dir = uv - center;
    float dist = length(dir);
    
    if (dist < size) {
        // create angular "shards"
        float angle = atan(dir.y, dir.x);
        float shard = floor(angle * 8.0); // 8 main crack directions
        
        // displacement based on shard id and distance
        float d = noise(vec2(shard, 1.0)) * strength * (1.0 - (dist / size));
        return dir * d;
    }
    return vec2(0.0);
}

vec3 rgb_to_yiq(vec3 c) {
	return vec3(dot(c, vec3(0.299, 0.587, 0.114)), dot(c, vec3(0.596, -0.274, -0.322)), dot(c, vec3(0.211, -0.523, 0.312)));
}

vec3 yiq_to_rgb(vec3 c) {
	return vec3(dot(c, vec3(1.0, 0.956, 0.621)), dot(c, vec3(1.0, -0.272, -0.647)), dot(c, vec3(1.0, -1.106, 1.703)));
}

void fragment() {
	vec2 uv = SCREEN_UV;
	float t = TIME;
	
	// 1. GLASS CRACK DISPLACEMENT
	vec2 glass_distort = get_crack_distortion(uv, crack_center, crack_intensity, crack_size);
	uv += glass_distort;
	
	// 2. ANALOG JITTER
	uv.y += (noise(vec2(t, 0.0)) - 0.5) * vertical_jitter;

	// 3. SMEARED CHROMA
	vec3 yiq_pure = rgb_to_yiq(texture(screen_texture, uv).rgb);
	float y = yiq_pure.x;
	float i_acc = 0.0;
	float q_acc = 0.0;
	
	int samples = 12;
	for(int n = 0; n < samples; n++) {
		float x_offset = (float(n) * (smearing * 0.01)) + color_offset;
		vec3 s = rgb_to_yiq(texture(screen_texture, uv - vec2(x_offset, 0.0)).rgb);
		i_acc += s.y;
		q_acc += s.z;
	}
	
	vec3 final_yiq = vec3(y, i_acc / float(samples), q_acc / float(samples));

	// 4. EDGE SHARPENING
	float neighbor = rgb_to_yiq(texture(screen_texture, uv + vec2(0.001, 0.0)).rgb).x;
	final_yiq.x += (y - neighbor) * sharpening;

	vec3 color = yiq_to_rgb(final_yiq);

	// 5. DRAW CRACK LINES
	float dist_to_center = length(SCREEN_UV - crack_center);
	if (dist_to_center < crack_size) {
		float crack_line = step(0.98, sin(atan(SCREEN_UV.y - crack_center.y, SCREEN_UV.x - crack_center.x) * 8.0 + noise(SCREEN_UV) * 2.0));
		float line_mask = (1.0 - (dist_to_center / crack_size));
		color += crack_line * line_mask * 0.5;
	}

	// 6. VHS ARTIFACTS
	color += (noise(uv + t) - 0.5) * static_noise;
	color -= sin(uv.y * 800.0) * scanline_opacity * y;
	color *= smoothstep(0.8, 0.8 - vignette_intensity, distance(SCREEN_UV, vec2(0.5)));

	COLOR = vec4(color, 1.0);
}
Live Preview
Tags
CRT, retro, VHS
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

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments