Perlin noise
Purpose
Perlin noise is a reliable way to get a number that looks random but is smooth for every point in space — and it's the foundational signal primitive from which clouds, terrain, marble, fire, and almost every "natural" shader are built.
Key insight
White noise (raw random) is useless for most natural effects because neighbors are unrelated — it looks like static. Perlin is a function that, given a coordinate, returns a value that's random-feeling but continuous: if two points are close, their Perlin values are close.
The underlying mechanism is a grid of hidden random gradient vectors interpolated smoothly between grid corners, but the important part for an art director is the behavior: deterministic (same input, same output — cacheable, reproducible), smooth (no popping), tunable in frequency (scale the coordinate up for tighter variation). This smoothness is what makes Perlin the raw material for everything organic.
Break it
Compare grain = 1 to grain = 25. At 1 it looks like rolling hills or soft clouds; at 25 it looks like TV static even though the underlying function is still smooth. Teaches: "smooth" and "noisy-looking" are scale-relative. Perlin at huge frequency has the same per-cell smoothness as Perlin at small frequency, but your pixels are no longer resolving that smoothness — you're only catching widely separated samples, which visually resembles noise again. This is why FBM (next lesson) matters: one frequency is never enough.
Direct Claude
uniform float u_grain;
// Pseudo-random unit gradient at a lattice point.
vec2 hash2(vec2 p) {
p = vec2(dot(p, vec2(127.1, 311.7)),
dot(p, vec2(269.5, 183.3)));
return -1.0 + 2.0 * fract(sin(p) * 43758.5453123);
}
// Classic 2D Perlin gradient noise.
float perlin(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
vec2 u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0); // quintic smoothing
float a = dot(hash2(i + vec2(0.0, 0.0)), f - vec2(0.0, 0.0));
float b = dot(hash2(i + vec2(1.0, 0.0)), f - vec2(1.0, 0.0));
float c = dot(hash2(i + vec2(0.0, 1.0)), f - vec2(0.0, 1.0));
float d = dot(hash2(i + vec2(1.0, 1.0)), f - vec2(1.0, 1.0));
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
}
void main() {
vec2 uv = (gl_FragCoord.xy - 0.5 * u_resolution) / min(u_resolution.x, u_resolution.y);
// SIGNAL LAYER — one Perlin call. grain scales the coordinate first.
float n = perlin(uv * u_grain);
float v = 0.5 + 0.5 * n;
gl_FragColor = vec4(vec3(v), 1.0);
}