🏔️ Perlin Noise: The Algorithm Behind Natural-Looking Randomness

Pure random noise looks like TV static — every pixel independent, no structure, no coherence. Natural phenomena are different: clouds have large-scale puffs and small-scale wisps. Terrain has mountain ranges and rocky details. Ken Perlin's 1983 algorithm generates noise with this multi-scale, coherent character — and it earned him a Technical Academy Award.

The Problem with White Noise

White noise assigns a completely independent random value to every point in space. Its power spectrum is flat — all frequencies contribute equally. Natural textures are quite different: they have most of their power at low frequencies (large-scale features) with decreasing power at higher frequencies (small details). The power spectrum of natural textures follows approximately P(f) ~ 1/f² — a characteristic of fractal, self-similar structures.

Interpolated value noise — placing random values on a grid and interpolating between them — provides coherence at the grid scale but shows obvious grid-aligned artifacts when the frequency matches the grid spacing. Perlin noise solves this by using random gradients rather than random values.

Gradient Noise: Perlin's Insight

Classic Perlin noise assigns a random unit gradient vector to each integer grid point. The noise value at a point P is computed by:

  1. Finding the surrounding grid corners (4 in 2D, 8 in 3D)
  2. For each corner, computing the dot product of the corner's gradient with the vector from the corner to P
  3. Smoothly interpolating these dot products using a fade function
// Ken Perlin's improved fade function (2002)
// 6t^5 - 15t^4 + 10t^3 — zero first and second derivatives at 0 and 1
function fade(t) {
  return t * t * t * (t * (t * 6 - 15) + 10);
}

// Dot product of gradient with offset vector
function grad(hash, x, y) {
  const h = hash & 3;
  const u = h < 2 ? x : y;
  const v = h < 2 ? y : x;
  return ((h & 1) ? -u : u) + ((h & 2) ? -v : v);
}

The fade function is critical. The original Perlin noise used a cubic 3t² − 2t³ (smoothstep), which has zero first derivative at 0 and 1 but non-zero second derivative — creating visible discontinuities in normal maps and bump maps. The 2002 quintic 6t⁵ − 15t⁴ + 10t³ has zero first and second derivatives at the endpoints, producing C² continuity and eliminating the artifacts.

Using gradients rather than values eliminates the grid-aligned banding that plagues value noise. The dot product construction ensures that the noise passes smoothly through zero at every grid point (since the offset vector has zero length there), creating a pleasant "zero-crossings at lattice points" property that avoids the bias issues of interpolated value noise.

Octaves, Persistence, and Fractal Noise

A single octave of Perlin noise has one characteristic frequency — determined by the grid spacing. To create multi-scale, fractal-like texture, we sum multiple octaves with increasing frequency and decreasing amplitude:

function fbm(x, y, octaves, persistence, lacunarity) {
  let value = 0;
  let amplitude = 1.0;
  let frequency = 1.0;
  let maxValue = 0;

  for (let i = 0; i < octaves; i++) {
    value += perlin(x * frequency, y * frequency) * amplitude;
    maxValue += amplitude;
    amplitude *= persistence;   // typically 0.5
    frequency *= lacunarity;    // typically 2.0
  }
  return value / maxValue;  // normalize to [-1, 1]
}

With persistence = 0.5 and lacunarity = 2.0, each octave doubles the frequency and halves the amplitude. After N octaves, the finest features are 2^N times smaller than the coarsest. The power spectrum follows P(f) ~ f^(-2·log₂(1/persistence)) — for persistence = 0.5, this gives P(f) ~ f⁻², matching natural texture statistics.

Varying persistence changes character: high persistence (0.7–0.9) gives bold, high-contrast textures with prominent fine detail. Low persistence (0.2–0.4) gives smooth, gently rolling surfaces where coarse features dominate. Turbulence uses the absolute value of each octave — |noise(x,y)| — creating sharp ridges and cloud-like billowing shapes.

Simplex Noise: Perlin's 2001 Improvement

Perlin's 2001 simplex noise addresses two weaknesses of the original: computational complexity (O(2^N) grid corners in N dimensions becomes expensive for 4D+) and directional artifacts (the square/cubic grid has preferential directions).

Simplex noise uses a simplex lattice — the simplest geometric shape in N dimensions. In 2D, simplices are equilateral triangles (3 corners) rather than squares (4 corners). In 3D, tetrahedra (4 corners) rather than cubes (8 corners). In N dimensions, (N+1) corners rather than 2^N. The complexity savings for high-dimensional noise are dramatic: 4D simplex noise requires 5 gradient evaluations versus 16 for the original algorithm.

The simplex lattice also has better isotropy — the distance from any interior point to the nearest corner varies less than for a square lattice, reducing directional bias in the output. Simplex noise has become the standard for high-performance GPU shaders and game engines.

Applications: Terrain, Clouds, and Particle Systems

Terrain generation stacks octaves with domain warping — feeding the noise output back as an offset to the input: height(p) = fbm(p + fbm(p)). This "warped" fractal noise creates the meandering valleys and overhanging cliffs characteristic of realistic mountains. Erosion simulation (hydraulic and thermal) further refines the terrain by moving material from high gradients to low gradients, sharpening ridges and smoothing valleys.

Animated noise drives many effects in the simulators on this site. Fluid turbulence uses 3D or 4D noise (adding time as a dimension) to generate spatially coherent velocity perturbations. Sand grain displacement uses 2D noise scrolling in the wind direction. The aurora borealis effect uses domain-warped noise for the characteristic curtain shapes.

The terrain simulation uses fractal Perlin noise with erosion to generate realistic landscapes in real time. The sand simulation uses noise-driven wind fields to control particle movement patterns.