What Are Noise Functions, and Why Do Games Need Them?
Open nearly any modern game with procedural content — Minecraft's endless landscapes, No Man's Sky's alien worlds, Spelunky's generated caves — and you're looking at noise functions doing the heavy lifting. Noise functions are mathematical tools that produce values appearing random yet remaining smooth and continuous: they change gradually rather than jumping erratically between adjacent points.
This quality is called coherence. A truly random sequence has no relationship between adjacent values. Noise functions, by contrast, guarantee that nearby inputs return similar outputs while distant inputs diverge — exactly the behavior we observe in natural phenomena like mountain ranges, cloud formations, and ocean surfaces. This makes them indispensable anywhere you need structured randomness: terrain, textures, animation variation, weather systems, and much more.
The Problem with Pure Randomness
Suppose you want to generate terrain height values for a grid of points. Your first instinct might be to call Math.random() for each vertex:
for (let i = 0; i < vertices.length; i++) {
vertices[i].y = Math.random() * maxHeight;
}The result is visual chaos — a jagged salt-and-pepper surface with no resemblance to real terrain. The problem is that pseudo-random number generators are memoryless: each call is statistically independent of every other. There is no correlation between a point at position $(x, y)$ and its neighbor at $(x+1, y)$.
What we need is a function $f(x, y)$ satisfying four properties:
- Deterministic: the same input always produces the same output
- Coherent: nearby inputs produce similar outputs (spatial smoothness)
- Varied: values span the full output range across large distances
- Non-periodic: no obvious repeating tile patterns at typical scales
Value noise — interpolating between random scalar values at grid corners — satisfies the first three but produces a blocky, plastic-looking result because the interpolation gradient is discontinuous at cell boundaries. Ken Perlin's 1983 breakthrough solved this with a fundamentally different approach: gradient noise.
Perlin Noise: A Gradient-Based Approach
Perlin developed his noise algorithm while working on the film Tron, frustrated with the artificial look of computer graphics textures. His insight was elegant: instead of assigning random scalar values to grid corners (which produces blocky value noise), assign random gradient vectors and compute contributions via dot products.
Step 1 — The Integer Grid and Permutation Table
The input space is divided into unit-sized cells. Each integer grid point $(ix, iy)$ receives a pseudo-random gradient vector determined by a fixed permutation table $P$ — a carefully shuffled array of integers 0–255. Because the table is fixed at initialization, the same gradient is always assigned to the same grid point, making the function perfectly deterministic:
$$\vec{g}_{ix,iy} = \text{gradients}\bigl[P\bigl[(P[ix \bmod 256] + iy) \bmod 256\bigr]\bigr]$$
The gradient vectors are typically chosen from a small canonical set (for 2D: the four axis-aligned and four diagonal directions) rather than being fully random. This avoids clustering artifacts and keeps gradient magnitudes consistent.
Step 2 — Dot Products at Each Corner
For an input point $\mathbf{p} = (x, y)$, identify the four surrounding integer grid corners. For each corner, compute the offset vector $\vec{d}$ pointing from that corner toward $\mathbf{p}$, then take the dot product with that corner's gradient:
$$n_{00} = \vec{g}_{\lfloor x \rfloor,\, \lfloor y \rfloor} \cdot (x - \lfloor x \rfloor,\; y - \lfloor y \rfloor)$$
This dot product encodes how much each corner's gradient "points toward" the query point. If the offset perfectly aligns with the gradient, the contribution is positive and large; if they oppose, it is negative. This is the mechanism that creates smooth directional variation rather than the blocky plateaus of value noise.
Step 3 — The Fade Curve
Linearly interpolating the four dot products produces visible seams at cell boundaries because the gradient of the interpolated surface is discontinuous there. Perlin's solution is a carefully chosen smoothstep polynomial applied to the fractional position before interpolation:
$$f(t) = 6t^5 - 15t^4 + 10t^3$$
This quintic polynomial was chosen because both $f'(t)$ and $f''(t)$ equal zero at $t = 0$ and $t = 1$. Continuous second derivatives across cell boundaries mean the noise surface is visually seamless no matter how finely you examine it.
function fade(t) {
return t * t * t * (t * (t * 6 - 15) + 10);
}
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);
}Step 4 — Bilinear Interpolation
Apply the fade curve to the fractional position $(u, v) = (f(x - \lfloor x \rfloor),\; f(y - \lfloor y \rfloor))$, then bilinearly interpolate the four corner dot products:
$$\text{noise}(x, y) = \text{lerp}\bigl(\text{lerp}(n_{00},\, n_{10},\, u),\; \text{lerp}(n_{01},\, n_{11},\, u),\; v\bigr)$$
The output lies in approximately $[-1, 1]$ and, crucially, passes through zero at every integer grid point (since all offset vectors are zero there). The full implementation in JavaScript looks like this:
function noise2D(x, y) {
const X = Math.floor(x) & 255;
const Y = Math.floor(y) & 255;
const xf = x - Math.floor(x);
const yf = y - Math.floor(y);
const u = fade(xf);
const v = fade(yf);
const aa = perm[perm[X ] + Y ];
const ab = perm[perm[X ] + Y + 1];
const ba = perm[perm[X + 1] + Y ];
const bb = perm[perm[X + 1] + Y + 1];
return lerp(
lerp(grad(aa, xf, yf ), grad(ba, xf - 1, yf ), u),
lerp(grad(ab, xf, yf - 1), grad(bb, xf - 1, yf - 1), u),
v
);
}Each call costs only a handful of array lookups, multiplications, and additions — making it practical to evaluate at thousands of points per frame even in JavaScript.
Fractional Brownian Motion: Layering Octaves
A single octave of Perlin noise produces gentle, large-scale variation — useful for broad terrain shapes but lacking the intricate detail of real landscapes. Nature is self-similar across scales: mountains have ridges, ridges have rocks, rocks have pebbles. To replicate this, we sum multiple layers of noise at progressively higher frequencies and lower amplitudes — a technique called Fractional Brownian Motion (fBm):
$$f(\mathbf{p}) = \sum_{i=0}^{N-1} p^{\,i} \cdot \text{noise}\!\left(\mathbf{p} \cdot L^{i}\right)$$
Each layer is called an octave. Three parameters control the character of the result:
- Octaves ($N$): how many layers to sum. More octaves add finer detail at the cost of compute.
- Persistence ($p$): how quickly amplitude decreases each octave. $p = 0.5$ means each octave contributes half the amplitude of its predecessor. Lower persistence produces smoother, rounder shapes; higher persistence emphasizes jagged fine detail.
- Lacunarity ($L$): how quickly frequency increases each octave. The conventional value of $2.0$ doubles the frequency each step, adding detail at half the scale. Higher lacunarity produces denser, more chaotic detail.
function fbm(x, y, octaves, persistence, lacunarity) {
let value = 0, amplitude = 1, frequency = 1, maxValue = 0;
for (let i = 0; i < octaves; i++) {
value += noise2D(x * frequency, y * frequency) * amplitude;
maxValue += amplitude;
amplitude *= persistence;
frequency *= lacunarity;
}
return value / maxValue; // Normalize to approximately [-1, 1]
}With 6–8 octaves, persistence of 0.5, and lacunarity of 2.0, fBm produces strikingly realistic terrain height maps. This exact formula drives Minecraft's terrain engine, Unreal Engine's Landscape system, and countless indie procedural generation tools.
Simplex Noise: The Sequel
In 2001 Ken Perlin introduced Simplex noise as a higher-performance alternative. The key change: instead of partitioning space into hypercubes (4 corners in 2D, 8 in 3D, $2^N$ in $N$ dimensions), Simplex noise uses a simplex grid. A simplex is the simplest possible polytope in $N$ dimensions — a triangle in 2D, a tetrahedron in 3D — with only $N+1$ corners rather than $2^N$.
This change yields several advantages:
- Lower complexity: $O(N^2)$ gradient evaluations vs. $O(N \cdot 2^N)$ — crucial for 4D and higher-dimensional noise (used for 3D animated noise: $x, y, z, \text{time}$).
- Better isotropy: The square grid of classic Perlin noise introduces subtle directional biases visible as faint diagonal artifacts. The triangular simplex grid distributes contributions more uniformly.
- No dimensional artifacts: Perlin noise can show faint axis-aligned banding in some configurations; Simplex noise does not.
The original Simplex noise implementation was covered by US patent 6,867,776 (expired 2022), which prompted the community to develop OpenSimplex and OpenSimplex2 as patent-free alternatives. For most game development purposes, well-implemented Perlin fBm and OpenSimplex2 produce virtually indistinguishable results; the choice comes down to library availability and whether you need higher-dimensional evaluation.
Domain Warping: Noise on Noise
One of the most powerful techniques for organic-looking results is domain warping, popularized by graphics researcher Inigo Quilez. Instead of evaluating noise at the raw input coordinates, you first displace those coordinates using another noise evaluation:
$$q_x = f(\mathbf{p}), \qquad q_y = f(\mathbf{p} + \vec{o})$$ $$\text{result} = f\bigl(\mathbf{p} + s \cdot \vec{q}\bigr)$$
where $\vec{o}$ is an arbitrary offset to decorrelate the two noise calls and $s$ is a warp strength scalar. The result appears to flow and swirl, producing shapes reminiscent of marble veins, lava, fluid turbulence, and alien rock formations. Multi-pass domain warping (warp the warped coordinates again) produces even more dramatic organic forms. This technique is widely used in procedural texture shaders for AAA game production.
Applications Across Game Genres
Noise functions appear throughout virtually every genre:
- Terrain and world generation: Height maps, biome temperature and humidity fields, river and coastline placement. Minecraft stacks multiple noise functions at different scales to determine elevation, cave density, and biome type independently.
- Procedural textures: Wood grain, marble, rust, clouds, fire, water caustics — all synthesizable without source images by evaluating noise in 2D or 3D space. 3D noise avoids texture-mapping seams entirely.
- Animation variation: Adding slowly-drifting noise offsets to idle animations, foliage sway, ambient creature movement, or particle velocities eliminates the mechanical look of looped cycles.
- Cave and dungeon carving: Thresholding a 3D noise field (keeping voxels where $f(x,y,z) > 0.5$) generates interconnected tunnel networks. No Man's Sky uses this for planetary cave systems at planetary scale.
- Weather and atmosphere: Cloud coverage maps, fog density volumes, wind direction fields — all benefit from spatially coherent variation that changes slowly over both space and time (4D noise).
- Procedural shaders: GLSL noise runs on the GPU and generates per-pixel surface detail, displacement, and color variation at full frame rate, enabling animated water, lava, and energy shield effects.
GPU Implementation in GLSL
For real-time applications you often want noise executing on the GPU. The following compact GLSL fragment is a common starting point (based on Stefan Gustavson's work):
// 2D gradient noise — returns value in roughly [-1, 1]
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.5453);
}
float noise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
vec2 u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0); // fade
return mix(
mix(dot(hash2(i + vec2(0,0)), f - vec2(0,0)),
dot(hash2(i + vec2(1,0)), f - vec2(1,0)), u.x),
mix(dot(hash2(i + vec2(0,1)), f - vec2(0,1)),
dot(hash2(i + vec2(1,1)), f - vec2(1,1)), u.x),
u.y
);
}
float fbm(vec2 p, int octaves) {
float v = 0.0, a = 0.5;
for (int i = 0; i < 8; i++) {
if (i >= octaves) break;
v += a * noise(p);
p *= 2.0; a *= 0.5;
}
return v;
}GPU noise is essential for animated water surfaces, volumetric cloud rendering, parallax displacement mapping, and procedural material variation — tasks where per-pixel evaluation at 60 fps would be impossible in JavaScript but runs trivially on a modern GPU.
Choosing the Right Noise Function
Not all noise is equal for every purpose:
- Perlin noise: The workhorse — widely supported, well-understood, good visual quality. Use this as your default when reaching for a library.
- OpenSimplex2: Better isotropy and performance in 3D+. Preferred when 4D animated noise is needed or when you can see the directional banding in Perlin output.
- Value noise: Simplest possible implementation (interpolate random scalars at grid points). Useful for quick prototyping; noticeably blockier than gradient noise.
- Worley / Cellular noise: Produces cell-like patterns by measuring distance to the nearest scattered seed point — ideal for rock textures, skin, leather, cracked surfaces, and irregular tile patterns.
- Blue noise: Maximally uniform random distributions without clumping, used for sampling (shadow rays, ambient occlusion), dithering, and poisson-disk object placement in levels.
For the vast majority of game development needs — terrain, procedural textures, animation, weather — fBm built on Perlin or OpenSimplex2 noise hits the sweet spot of quality, performance, and implementation simplicity. Experiment with the interactive demo below to build intuition for how each parameter shapes the output before integrating noise into your own projects.
Comments