Perlin Noise and fBm — how maths paints nature
Mountains, clouds, ocean floors, fire, marble — all generated from a single elegant function. Ken Perlin invented gradient noise while working on Tron (1982). Four decades later it remains the standard tool for procedural content generation.
1. Noise types: value vs. gradient
All procedural noise functions map a continuous coordinate to a
pseudo-random value that varies smoothly — unlike
Math.random(), which is discontinuous. The key property:
nearby inputs produce nearby outputs (spatial coherence).
- Value noise: Assign a random value to each integer grid point, then interpolate between them. Simple, but produces a "blobby" low-frequency look.
- Gradient noise (Perlin): Assign a random gradient vector to each grid point. The noise value at any position is the dot product of the gradient with the distance vector. Produces richer, more natural-looking patterns.
- Simplex noise: Ken Perlin's 2001 improvement — uses a simplex grid (triangles in 2D, tetrahedra in 3D) instead of a square grid, reducing computation from 2ⁿ to n+1 corners and removing the axis-aligned artifact.
2. Permutation table and hash
The critical ingredient for "random but reproducible" sampling is a
permutation table — an array
P[256] containing a shuffled sequence of integers 0…255.
The table is doubled (P[512]) to avoid modulo when
indexing with negative values.
P is a fixed permutation of 0..255, repeated twice.
For 3D: hash(x,y,z) = P[ P[ P[x&255] + (y&255) ] + (z&255) ]
This gives 256 unique hash values per dimension combination, creating a lattice that repeats every 256 units — which is fine for most use cases. The original Perlin table is fixed (not random), giving identical results across all platforms:
// Ken Perlin's original permutation table
const PERM = [
151,160,137, 91, 90, 15,131, 13,201, 95, 96,
53,194,233, 7,225,140, 36,103, 30, 69,142,
8, 99, 37,240, 21, 10, 23,190, 6,148,247,
/* ... 256 values total ... */
];
// Double to avoid index wrapping
const P = [...PERM, ...PERM];
3. Gradient vectors
Each hashed lattice point is assigned a gradient vector. In 2D, Perlin uses 8 unit vectors at 45° intervals. In 3D the original implementation uses 12 vectors pointing from the center of a cube to each of its 12 edges.
( 1, 1), (-1, 1), ( 1,-1), (-1,-1)
( 1, 0), (-1, 0), ( 0, 1), ( 0,-1)
3D gradients (12 directions):
( 1, 1, 0), (-1, 1, 0), ( 1,-1, 0), (-1,-1, 0)
( 1, 0, 1), (-1, 0, 1), ( 1, 0,-1), (-1, 0,-1)
( 0, 1, 1), ( 0,-1, 1), ( 0, 1,-1), ( 0,-1,-1)
The noise value at position (x,y) is the dot product of the grid corner's gradient with the distance vector from that corner to the query point. Corners with gradients pointing toward the query point contribute positive values; corners pointing away contribute negative values.
// 2D gradient dot product
function grad2(hash, x, y) {
const h = hash & 7; // one of 8 gradients
const u = h < 4 ? x : y;
const v = h < 4 ? y : x;
return ((h & 1) ? -u : u) + ((h & 2) ? -v : v);
}
4. Fade function and interpolation
Naive linear interpolation between the four corner dot products produces discontinuous first derivatives (visible creases at grid boundaries). Perlin uses a quintic "fade" easing curve — also called smoothstep6 — which has zero first and second derivatives at t=0 and t=1:
Cubic fade (Hermite): f(t) = 3t² − 2t³ ← C¹ smooth
Quintic fade (Perlin2002): f(t) = 6t⁵ − 15t⁴ + 10t³ ← C² smooth
Both: f(0) = 0, f(1) = 1, f'(0) = f'(1) = 0
The original 1985 Perlin paper used the cubic fade. The 2002 improved version switched to the quintic, eliminating the subtle "line artifacts" that appear with the cubic when the noise is used for normal maps or derivative estimation.
// Quintic fade (Perlin 2002)
function fade(t) {
return t * t * t * (t * (t * 6 - 15) + 10);
}
function lerp(t, a, b) {
return a + t * (b - a);
}
5. Classic 2D Perlin: full implementation
Putting it all together: find the unit square that contains the point, compute the fractional parts, fade the fractional parts, hash all four corners, compute the gradient dot products, bilinearly interpolate.
function noise2D(x, y) {
// Unit square
const X = Math.floor(x) & 255;
const Y = Math.floor(y) & 255;
// Fractional part
x -= Math.floor(x);
y -= Math.floor(y);
// Fade curves
const u = fade(x);
const v = fade(y);
// Hash corners
const a = P[X] + Y;
const aa = P[a], ab = P[a + 1];
const b = P[X+1] + Y;
const ba = P[b], bb = P[b + 1];
// Bilinear blend of gradient dot products
return lerp(v,
lerp(u, grad2(P[aa], x, y ),
grad2(P[ba], x-1, y )),
lerp(u, grad2(P[ab], x, y-1),
grad2(P[bb], x-1, y-1))
);
}
The output range is approximately −0.707 … +0.707 in
2D (not −1…1 as commonly assumed). To normalise to 0…1:
v = noise2D(x, y) * 0.707 + 0.5.
6. Fractional Brownian motion (fBm)
A single octave of Perlin noise produces smooth, gently rolling hills. Real terrain has small-scale roughness on top of large-scale structure — this is modelled by fractional Brownian motion: summing multiple noise octaves, each with doubled frequency and halved amplitude.
frequencyᵢ = lacunarity^i (lacunarity ≈ 2.0)
amplitudeᵢ = gain^i (gain ≈ 0.5, also called persistence)
function fbm(x, y, octaves = 6, lacunarity = 2.0, gain = 0.5) {
let value = 0;
let amplitude = 0.5;
let frequency = 1.0;
let max = 0; // for normalisation
for (let i = 0; i < octaves; i++) {
value += amplitude * noise2D(x * frequency, y * frequency);
max += amplitude;
amplitude *= gain;
frequency *= lacunarity;
}
return value / max; // normalise to roughly −1..1
}
// Terrain example: 512×512 heightmap
for (let y = 0; y < 512; y++) {
for (let x = 0; x < 512; x++) {
const scale = 0.004; // zoom out
heightmap[y * 512 + x] =
fbm(x * scale, y * scale, 8, 2.0, 0.5);
}
}
Variants
-
turbulence(x,y) — sum of
|noise|instead ofnoise. Produces cloud-like bolts and marble veins. -
ridged multifractal —
1 − |noise|per octave, with raised power. Produces sharp mountain ridges. - domain warping — use one fBm call to warp the input coordinates of another. Creates dramatically twisted, erosion-like patterns (technique by Inigo Quilez).
// Domain warp — Inigo Quilez style
function warpedFbm(x, y) {
// q offsets by one fBm field
const qx = fbm(x, y, 4);
const qy = fbm(x + 5.2, y + 1.3, 4);
// r offsets by q
const rx = fbm(x + 4.0*qx + 1.7, y + 4.0*qy + 9.2, 4);
const ry = fbm(x + 4.0*qx + 8.3, y + 4.0*qy + 2.8, 4);
return fbm(x + 4.0*rx, y + 4.0*ry, 5);
}
7. GLSL implementation
On the GPU the permutation table becomes a 256-pixel 1D texture (or is baked inline). Here is a compact and widely-used GLSL hash + 2D Perlin implementation by Ian McEwan / Ashima (MIT licensed) that runs in a fragment shader without any texture lookups:
// GLSL — compact 2D Perlin (hash via arithmetic)
vec2 fade2(vec2 t) {
return t*t*t*(t*(t*6.0-15.0)+10.0);
}
vec4 permute4(vec4 x) {
return mod(x*x*34.0+x, 289.0);
}
float cnoise2D(vec2 P) {
vec4 Pi = floor(P.xyxy) + vec4(0,0,1,1);
vec4 Pf = fract(P.xyxy) - vec4(0,0,1,1);
Pi = mod(Pi, 289.0);
vec4 ix = Pi.xzxz, iy = Pi.yyww;
vec4 fx = Pf.xzxz, fy = Pf.yyww;
vec4 i = permute4(permute4(ix) + iy);
vec4 gx = 2.0*fract(i/41.0) - 1.0;
vec4 gy = abs(gx) - 0.5;
gx = gx - floor(gx + 0.5);
vec2 g00 = vec2(gx.x, gy.x);
vec2 g10 = vec2(gx.y, gy.y);
vec2 g01 = vec2(gx.z, gy.z);
vec2 g11 = vec2(gx.w, gy.w);
vec4 norm = 1.79284291400159 - 0.85373472095314*vec4(
dot(g00,g00), dot(g01,g01), dot(g10,g10), dot(g11,g11));
g00 *= norm.x; g01 *= norm.y; g10 *= norm.z; g11 *= norm.w;
float n00 = dot(g00, fx.xy);
float n10 = dot(g10, fx.zy);
float n01 = dot(g01, fy.xw);
float n11 = dot(g11, fy.zw);
vec2 fade_xy = fade2(fract(P));
vec2 n_x = mix(vec2(n00,n01), vec2(n10,n11), fade_xy.x);
return 2.3 * mix(n_x.x, n_x.y, fade_xy.y);
}
cnoise2D, fBm is just a loop. In GLSL, unroll the first 4
octaves manually for performance on older mobile GPUs.
// fBm in GLSL fragment shader
float fbm(vec2 p) {
float value = 0.0;
float amplitude = 0.5;
for (int i = 0; i < 6; i++) {
value += amplitude * cnoise2D(p);
p *= 2.0;
amplitude *= 0.5;
}
return value;
}
// Usage in main():
float h = fbm(vUv * 4.0 + u_time * 0.05);
vec3 color = mix(vec3(0.0,0.1,0.4), vec3(0.9,0.95,1.0), h*0.5+0.5);
8. Simplex noise
Ken Perlin's 2001 revision replaced the square/cube grid with a simplex lattice — the simplest polytope in each dimension (a line segment in 1D, a triangle in 2D, a tetrahedron in 3D). This produces:
- Fewer grid corners to evaluate: n+1 instead of 2ⁿ (4 corners → 3 in 2D; 8 → 4 in 3D).
- No axis-aligned directional artifacts from square grid corners.
- Continuous derivative everywhere (useful for normal map generation).
| Property | Classic Perlin | Simplex |
|---|---|---|
| Corners per eval (2D) | 4 | 3 |
| Corners per eval (3D) | 8 | 4 |
| Grid artifacts | Visible at multiples of 90° | None (rotational symmetry) |
| Derivative continuity | C² (quintic fade) | C¹ (but no artifacts) |
| GLSL complexity | Medium | Slightly lower |
| Patent status | Public domain | Was patented (US6867776) — expired 2021 |
9. Applications: terrain, clouds, textures
Terrain heightmap
fBm gives the basic heightmap. Add a power curve (h = h^2.5) to sharpen peaks while keeping valleys flat. Multiply by a
"continentality mask" (another low-freq noise) to create islands
separated by ocean:
function getHeight(x, y) {
let h = fbm(x * 0.003, y * 0.003, 8) * 0.5 + 0.5;
const continent =
fbm(x*0.0008, y*0.0008, 2) * 0.5 + 0.5;
h = h * continent;
h = Math.pow(h, 2.2); // sharpen mountains
return h;
}
Animated clouds
Three-dimensional noise evaluated at
(x, y, time * speed) produces slow-moving clouds.
Threshold the value to get hard cloud edges or use a smooth ramp for
wispy edges:
// Fragment shader — animated cloud layer
float t = u_time * 0.07;
float c = fbm(vUv * 3.0 + vec2(t, t*0.4));
float cloud = smoothstep(0.45, 0.65, c * 0.5 + 0.5);
Marble & wood textures
Classic marble: use
sin(x * freq + turbulence(x,y) * scale). The turbulence
distorts the sinusoidal stripes into natural-looking veins.
// Marble vein pattern
float marble(vec2 p) {
float n = fbm(p);
return sin(p.x * 6.0 + n * 8.0) * 0.5 + 0.5;
}
// Wood rings
float wood(vec2 p) {
float r = length(p) * 12.0;
r += fbm(p * 2.0) * 4.0;
return fract(r);
}
📐 Mathematics & Fractals
The procedural terrain generator and fractal explorer are coming to the Mathematics category — using fBm and Perlin noise exactly as described here.