
I was playing around with some retro aesthetics for a 2D side-scroller project and wanted to get that cozy, worn-out look of old analog film. I couldn't find a single shader that did everything I wanted, so I ended up putting this one together.
It handles a bunch of features:
- Toggling between classic sepia, a warmer technicolor style, a washed-out faded look, and a grainy black-and-white mode.
- Animated film grain and vertical scratches that flicker at random.
- Procedural dust specks and curved hair fibers that pop up on random frames.
- Projector bulb flicker, subtle chromatic aberration, and lens distortion (barrel bulge).
To use it, just throw a ColorRect over your scene, set the mouse filter to Ignore, and apply this as a ShaderMaterial. All of the key settings (grain density, scratch speed, lens distortion strength, and colors) are exposed as parameters so you can tweak them directly in the inspector to fit your game's vibe.
Let me know if you run into any issues or have ideas on how to optimize it. Hope you find it useful!
shader_type canvas_item;
render_mode unshaded;
/*
============================================================================
PREMIUM 2D VINTAGE FILM & RETRO SHADER
============================================================================
For Godot 4.x (CanvasItem / Screen-space post-process)
*/
uniform sampler2D screen_texture : hint_screen_texture, repeat_disable, filter_nearest;
// ============================================================================
// COLOR & TONE CONTROLS
// ============================================================================
group_uniforms ColorGrading;
// 0 = Normal, 1 = Sepia, 2 = Warm/Technicolor, 3 = Faded/Washed, 4 = Monochrome
uniform int color_mode : hint_range(0, 4) = 1;
uniform float brightness : hint_range(0.0, 2.0) = 1.0;
uniform float contrast : hint_range(0.0, 2.0) = 1.05;
uniform float saturation : hint_range(0.0, 2.0) = 0.8;
uniform float vignette_strength : hint_range(0.0, 2.0) = 0.8;
uniform float vignette_softness : hint_range(0.0, 1.0) = 0.45;
uniform vec3 vignette_color : source_color = vec3(0.08, 0.05, 0.02);
// ============================================================================
// ANALOG FILM ARTIFACTS
// ============================================================================
group_uniforms FilmArtifacts;
uniform float grain_strength : hint_range(0.0, 0.5) = 0.12;
uniform float grain_size : hint_range(1.0, 4.0) = 1.5;
uniform float scratch_amount : hint_range(0.0, 1.0) = 0.35;
uniform float scratch_speed : hint_range(0.0, 10.0) = 3.0;
uniform float dust_amount : hint_range(0.0, 1.0) = 0.4;
uniform float flicker_strength : hint_range(0.0, 0.3) = 0.08;
// ============================================================================
// OPTICS & DISTORTION
// ============================================================================
group_uniforms Optics;
uniform float chromatic_aberration : hint_range(0.0, 10.0) = 2.0;
uniform float lens_distortion : hint_range(0.0, 0.3) = 0.05;
uniform float blur_amount : hint_range(0.0, 2.0) = 0.2;
// ============================================================================
// UTILITY FUNCTIONS
// ============================================================================
// 1D Hash
float hash11(float n) {
return fract(sin(n) * 43758.5453123);
}
// 2D Hash
float hash12(vec2 p) {
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);
}
// 3D Noise for organic grain
float noise3d(vec3 p) {
vec3 ip = floor(p);
vec3 fp = fract(p);
fp = fp * fp * (3.0 - 2.0 * fp);
float n000 = hash12(ip.xy + vec2(0.0, 0.0) + ip.z * 17.0);
float n100 = hash12(ip.xy + vec2(1.0, 0.0) + ip.z * 17.0);
float n010 = hash12(ip.xy + vec2(0.0, 1.0) + ip.z * 17.0);
float n110 = hash12(ip.xy + vec2(1.0, 1.0) + ip.z * 17.0);
float n001 = hash12(ip.xy + vec2(0.0, 0.0) + (ip.z + 1.0) * 17.0);
float n101 = hash12(ip.xy + vec2(1.0, 0.0) + (ip.z + 1.0) * 17.0);
float n011 = hash12(ip.xy + vec2(0.0, 1.0) + (ip.z + 1.0) * 17.0);
float n111 = hash12(ip.xy + vec2(1.0, 1.0) + (ip.z + 1.0) * 17.0);
float mix0 = mix(mix(n000, n100, fp.x), mix(n010, n110, fp.x), fp.y);
float mix1 = mix(mix(n001, n101, fp.x), mix(n011, n111, fp.x), fp.y);
return mix(mix0, mix1, fp.z);
}
// Lens barrel distortion
vec2 distort_uv(vec2 uv, float k) {
if (k <= 0.0) return uv;
vec2 cc = uv - 0.5;
float dist = dot(cc, cc);
return 0.5 + cc * (1.0 + k * dist);
}
// Apply Vintage Tone curves
vec3 apply_vintage_grading(vec3 color, int mode) {
if (mode == 1) {
vec3 sepia = vec3(
dot(color, vec3(0.393, 0.769, 0.189)),
dot(color, vec3(0.349, 0.686, 0.168)),
dot(color, vec3(0.272, 0.534, 0.131))
);
color = mix(color, sepia, 0.9);
}
else if (mode == 2) {
color.r = pow(color.r, 0.9);
color.g = color.g * 0.95;
color.b = pow(color.b, 1.1) * 0.9;
color = mix(color, color * vec3(1.1, 1.0, 0.85), 0.5);
}
else if (mode == 3) {
color = color * 0.8 + 0.1;
color = pow(color, vec3(0.9));
float gray = dot(color, vec3(0.299, 0.587, 0.114));
color = mix(color, vec3(gray), 0.35);
}
else if (mode == 4) {
float gray = dot(color, vec3(0.299, 0.587, 0.114));
color = vec3(gray) * vec3(0.95, 0.93, 0.88);
}
return color;
}
void fragment() {
vec2 base_uv = SCREEN_UV;
vec2 px_size = SCREEN_PIXEL_SIZE;
float t = TIME;
// 1. Lens Distortion
vec2 distorted_uv = distort_uv(base_uv, lens_distortion);
vec2 uv = clamp(distorted_uv, 0.0, 1.0);
bool out_of_bounds = (distorted_uv.x < 0.0 || distorted_uv.x > 1.0 || distorted_uv.y < 0.0 || distorted_uv.y > 1.0);
if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) {
COLOR = vec4(vignette_color, 1.0);
return;
}
// 2. Chromatic Aberration & Blur
vec3 tex_color;
if (chromatic_aberration > 0.0) {
vec2 shift = px_size * chromatic_aberration;
vec3 r_chan = vec3(0.0);
vec3 g_chan = vec3(0.0);
vec3 b_chan = vec3(0.0);
float samples = 3.0;
float b_offset = blur_amount * 0.5;
for (float i = -1.0; i <= 1.0; i += 1.0) {
vec2 blur_shift = vec2(i) * px_size * b_offset;
r_chan += textureLod(screen_texture, uv + shift + blur_shift, 0.0).rgb;
g_chan += textureLod(screen_texture, uv + blur_shift, 0.0).rgb;
b_chan += textureLod(screen_texture, uv - shift + blur_shift, 0.0).rgb;
}
tex_color = vec3(r_chan.r / samples, g_chan.g / samples, b_chan.b / samples);
} else {
tex_color = textureLod(screen_texture, uv, 0.0).rgb;
}
// 3. Vintage Grading & Tone Controls
vec3 color = apply_vintage_grading(tex_color, color_mode);
color = (color - 0.5) * contrast + 0.5 + (brightness - 1.0);
float luma = dot(color, vec3(0.299, 0.587, 0.114));
color = mix(vec3(luma), color, saturation);
// 4. Projector Flicker
float frame_idx = floor(t * 18.0);
float flicker = hash11(frame_idx) * hash11(frame_idx + 12.5);
color *= 1.0 - (flicker * flicker_strength);
// 5. Film Scratches (Vertical Lines)
if (scratch_amount > 0.0) {
float scratch_seed = floor(t * scratch_speed);
for (int i = 0; i < 3; i++) {
float scratch_x = hash11(scratch_seed + float(i) * 97.3);
if (hash11(scratch_seed + float(i) * 13.7) < scratch_amount) {
float dist_to_scratch = abs(uv.x - scratch_x);
float scratch_line = smoothstep(0.0015, 0.0, dist_to_scratch);
float scratch_noise = noise3d(vec3(uv.x * 100.0, uv.y * 15.0, t * 10.0));
scratch_line *= step(0.3, scratch_noise);
float scratch_color = (hash11(scratch_seed + float(i) * 53.2) > 0.4) ? 1.0 : 0.0;
color = mix(color, vec3(scratch_color), scratch_line * 0.65);
}
}
}
// 6. Dust Specks and Hair Fibers
if (dust_amount > 0.0) {
float dust_seed = floor(t * 12.0);
float num_specks = 8.0 * dust_amount;
for (float i = 0.0; i < num_specks; i += 1.0) {
vec2 speck_pos = vec2(hash11(dust_seed + i * 23.4), hash11(dust_seed + i * 87.1));
float dist = length(uv - speck_pos);
float speck_size = 0.001 + 0.002 * hash11(dust_seed + i * 45.3);
float speck = smoothstep(speck_size, speck_size * 0.4, dist);
float hair_seed = dust_seed + i * 39.8;
if (hash11(hair_seed) > 0.7) {
float theta = t * 0.1 + i * 2.0;
vec2 dir = vec2(cos(theta), sin(theta));
vec2 hair_proj = uv - speck_pos;
float hair_dist = length(hair_proj - clamp(dot(hair_proj, dir), -0.015, 0.015) * dir);
speck += smoothstep(0.0006, 0.0, hair_dist) * 0.8;
}
color = mix(color, vec3(0.03), clamp(speck, 0.0, 1.0) * 0.85);
}
}
// 7. Film Grain
if (grain_strength > 0.0) {
vec2 grain_uv = FRAGCOORD.xy / grain_size;
float grain = hash12(grain_uv + vec2(t * 57.1, t * 83.4));
color += (grain - 0.5) * grain_strength * (0.6 + 0.4 * luma);
}
// 8. Lens Vignette
vec2 vignette_uv = uv - 0.5;
float d = length(vignette_uv);
float vignette = smoothstep(vignette_softness, vignette_softness + (1.5 - vignette_softness), d * vignette_strength);
color = mix(color, vignette_color, vignette);
if (out_of_bounds) {
COLOR = vec4(vignette_color, 1.0);
} else {
COLOR = vec4(clamp(color, 0.0, 1.0), 1.0);
}
}Join the discussion! Log in to share your thoughts.
Log In to Comment