Controls
Master Opacity
0.15Effect Speed
1.0Grain Scale
1.0Grain Intensity
0.30Clouds Scale
5.0Clouds Intensity
0.70Scanlines
3.0Luma Jitter
2.0Chroma Shift
1.0Refraction
0.5Refraction Scale
3.0Scratches
0.20Scratches Density
0.5Description
Simulates the imperfections of analog film and vintage display monitors. Rather than a static overlay, we procedurally generate multiple layers of "artifacts" that interact with the image.
First, we apply UV Refraction. Before generating any noise, we slightly distort our coordinate system using large-scale value noise that drifts over time. This makes the film grain and scratches appear to "float" on a warping liquid surface (a "wet plate" effect):
float distort = valueNoise(uv * u_scale + vec2(0.0, time));
vec2 distortedUV = uv + (distort - 0.5) * strength;
The primary texture comes from High-Frequency Grain. We use a hash function to generate random brightness values for every pixel. To make it feel organic, we quantize the time variable, ensuring the grain pattern holds for a few frames before switching, mimicking 24fps film:
float t0 = floor(u_time * 24.0) / 24.0;
float grain = gaussianHash(gl_FragCoord.xy, t0);
We layer Low-Frequency Dirt (blotches) on top. These are larger, softer noise clouds that simulate uneven chemical development or lens smudges. We use the distorted UVs here so the dirt patches move with the surface refraction:
float blotch = valueNoise(distortedUV * u_blotchScale);
blotch = (blotch - 0.5) * 2.0;
To simulate old CRT monitors or projector rolls, we add Scanlines & Jitter. Scanlines are simple sine waves based on the Y-coordinate. Jitter creates random horizontal shifts for entire rows of pixels:
float scan = sin(pixel.y * 3.14) * intensity;
float rowJitter = hash(floor(pixel.y / size), time);
Finally, we add Scratches. These are vertical high-value noise spikes. We simulate depth by calculating a "shadow" for each scratch—a second dark line slightly offset to the right. This gives the scratches a physical, engraved appearance:
float scratch = step(0.99, noise(x));
float shadow = step(0.99, noise(x + offset));
float finalScratch = scratch - (shadow * 0.5);