Why not just call Math.random()?
Mountains, clouds, marble veins and wood grain all look random, but never chaotic — nearby points on a terrain map have similar heights. Math.random() gives you the opposite: every call is independent of the last, which produces static, not scenery. What you need is spatial coherence — a function where nearby inputs produce nearby outputs, but the overall pattern still looks organic rather than tiled. Ken Perlin invented exactly that while working on visual effects for Tron in 1982, and it remains the default tool for procedural content four decades later.
Gradient noise, not value noise
The naive approach — value noise — assigns a random number to each integer grid point and interpolates between them. It works, but produces a blobby look with visible creases at grid boundaries, because interpolating raw values doesn't guarantee a smooth derivative. Perlin's fix was to assign each grid corner a random gradient vector instead of a scalar. The noise value at any point is the dot product of the nearest corner's gradient with the vector from that corner to the query point — corners whose gradient points toward you contribute positively, corners pointing away contribute negatively.
A fixed 256-entry permutation table, doubled to 512 entries to avoid index wrapping, supplies the "randomness": hash(x, y) = P[P[x & 255] + (y & 255)]. Because the table is fixed rather than freshly randomised, the same coordinates always produce the same noise value — essential for reproducible terrain and for evaluating noise independently on the CPU and GPU.
The fade curve that kills the creases
Linear interpolation between the four corner dot products still leaves a visible seam, because its derivative is discontinuous at grid lines. Perlin's 2002 revision uses a quintic fade curve instead of the original cubic one:
function fade(t) { return t*t*t*(t*(t*6 - 15) + 10); } // 6t⁵ − 15t⁴ + 10t³
function lerp(t, a, b) { return a + t * (b - a); }
Both f(0)=0 and f(1)=1, but the quintic curve also has zero second derivative at both ends (C² continuity), which is what removes the fine "line artefacts" that show up when the noise is used to compute normal maps. Bilinearly blending the four gradient dot products with fade(x) and fade(y) gives a single octave of smooth, natural-looking noise — but the output range is roughly ±0.707 in 2D, not ±1 as often assumed.
Fractional Brownian motion: layering octaves
One octave of noise gives gently rolling hills; real terrain has roughness at every scale simultaneously. Fractional Brownian motion (fBm) sums several octaves, each at double the frequency and half the amplitude of the last:
function fbm(x, y, octaves=6, lacunarity=2.0, gain=0.5) {
let value = 0, amplitude = 0.5, frequency = 1.0, max = 0;
for (let i = 0; i < octaves; i++) {
value += amplitude * noise2D(x * frequency, y * frequency);
max += amplitude; amplitude *= gain; frequency *= lacunarity;
}
return value / max; // normalise back to roughly ±1
}
Octave 1 contributes continent-scale hills, octave 2 adds mountain ranges, octave 3 carves cliffs and escarpments, and octave 4 onward adds fine boulders and pebbles — each layer visually subordinate to the last, exactly matching how real landscapes look at every zoom level. Two useful variants: turbulence sums |noise| instead of noise for cloud-like bolts and marble veins, and ridged multifractal uses 1 − |noise| per octave for sharp mountain ridges. Domain warping — using one fBm field to distort the input coordinates of another, a technique popularised by Iñigo Quilez — produces the twisted, erosion-like patterns seen in high-end procedural terrain.
Simplex noise and the GPU
Perlin's own 2001 revision, simplex noise, replaces the square grid with a simplex lattice — triangles in 2D, tetrahedra in 3D — cutting the corners evaluated per sample from 2ⁿ to n+1 (4→3 in 2D, 8→4 in 3D) and removing the faint axis-aligned artefacts visible at multiples of 90° in classic Perlin noise. Its patent expired in 2021, so both versions are now fully free to use. On the GPU, the permutation table is usually replaced by an arithmetic hash so the whole thing runs in a fragment shader with zero texture lookups — the standard building block for animated cloud layers, procedural marble, and terrain heightmaps computed per-pixel in real time.
Frequently asked questions
What is the actual output range of 2D Perlin noise?
Approximately −0.707 to +0.707, not −1 to +1 as commonly assumed. The extreme values are only reached exactly 45° through a gradient vector. To normalise to 0…1, use v = noise2D(x, y) * 0.707 + 0.5, or simply measure the actual range for your parameters and rescale.
Why does fBm use doubling frequency and halving amplitude?
It mimics the statistics of real terrain and turbulence, where large-scale structure carries most of the variance and each finer scale contributes proportionally less. Doubling the frequency (lacunarity ≈ 2) while halving the amplitude (gain ≈ 0.5, also called persistence) per octave adds continent-scale hills first, then mountain ranges, then cliffs, then fine roughness — each layer visually subordinate to the one before it.
Should I use classic Perlin noise or simplex noise?
Both are public domain now that the simplex patent expired in 2021. Simplex evaluates fewer grid corners (3 instead of 4 in 2D, 4 instead of 8 in 3D) and has no axis-aligned artefacts, making it faster for per-pixel shader noise. Classic Perlin noise is simpler to implement correctly for pre-computed heightmaps in JavaScript, which is why it remains the more commonly taught version.
Try it live
Every octave, gradient and fade curve above runs live in Perlin Noise — Procedural Terrain & Texture Generator. Tune octaves, persistence, lacunarity and scale and watch the heightmap update instantly, entirely in your browser.
▶ Open Perlin Noise simulation