Grain belongs in the midtones
Adding uniform noise to a frame is the standard way to fake film, and it is the reason faked film usually reads as a dirty screen instead. Real grain has structure, and the structure is not subtle once you know to look for it.
The middle panel has grain crawling all over the shadows. The right panel leaves the shadows almost clean, puts the most grain through the midtones, and eases off again in the highlight. That is where film actually puts it, and it is the difference between "shot on film" and "there is something wrong with this screen".
Why the midtones
Grain is a counting phenomenon. Film is a layer of silver halide crystals, and a given exposure develops some fraction of them. The visible grain is the statistical variation in which ones happened to develop.
In a deep shadow almost nothing developed, so there is very little variation to see.
In a blown highlight nearly everything developed, so again there is little left to
vary. The variance is largest in between, where roughly half the crystals flipped and
the outcome is most uncertain. That is a binomial process, and its standard deviation
goes as the square root of p(1-p):
float lum = luminance(color);
float amt = strength * sqrt(lum * (1.0 - lum)); // peaks at lum = 0.5
One sqrt, and the grain stops looking like an overlay.
Grain does not scale with your resolution
A grain is a physical object of a fixed size on the negative. Render it per-pixel and it becomes finer as your output gets bigger, so the same footage grains differently at 1080p and 4K, and a 4K master downsampled for delivery has almost no grain left at all.
float grainScale = resolutionHeight / 320.0; // pick a reference height
vec2 gp = fragCoord / grainScale; // sample at a FIXED size
Tie it to a reference height and it stays the same physical size at every output resolution, which is what actually matches the reference.
The two other details
- Color film grains per layer. The three emulsion layers are independent, so the noise is not monochrome. Fully independent per channel is too strong; a partial decorrelation reads right.
- Grain has a size, so it is not white noise. Per-pixel random values are too fine and read as sensor noise. Value noise at a few pixels per cell has the clumping that actual grain has.
Rules of thumb
- Amplitude follows
sqrt(lum*(1-lum)). Peak in the midtones, near zero at both ends. - Fix the grain size in physical units, not pixels, or it changes with resolution.
- Use noise with a cell size. White noise reads as digital, not film.
- Apply after the tonemap and grade, with the dither, at the very end.
- Animate it on the frame clock, not continuously, or it shimmers rather than flickers.
One email when something new goes up. No newsletter, no schedule, nothing else.
Double opt-in, so watch for a confirmation email. Unsubscribe any time.