Gradient (Perlin) noise
Random slopes at the grid corners instead of random values, for noise that hides its grid; simplex noise as a variant.
How it works
Value noise stores a random height at each grid corner, so its peaks and valleys sit on the corners and the grid shows. Gradient noise, which Ken Perlin introduced in 1985, stores a random direction at each corner instead: the slope the surface has there. Its value at every corner is exactly zero, and the features fall between the corners, which hides the lattice far better. It is the noise behind most procedural textures: wood, marble, terrain, water.
Gradients and offsets
The structure is the same as value noise: find the cell, look at its four corners, blend with a fade curve. What
changes is what each corner contributes. Each corner gets a random unit vector, its gradient. The corner’s
contribution is the dot productMultiplies two vectors component by component and adds the results. For unit vectors it is the cosine of the angle between them, from 1 (same direction) to -1 (opposite). Dot product Multiplies two vectors component by component and adds the results. For unit vectors it is the cosine of the angle between them, from 1 (same direction) to -1 (opposite).dot(a, b) = |a| |b| cos θ
vec2 gradient(vec2 cell) {
float angle = hash21(cell) * 6.2831853; // a random direction
return vec2(cos(angle), sin(angle));
}
float gradientNoise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
float a = dot(gradient(i), f);
float b = dot(gradient(i + vec2(1.0, 0.0)), f - vec2(1.0, 0.0));
float c = dot(gradient(i + vec2(0.0, 1.0)), f - vec2(0.0, 1.0));
float d = dot(gradient(i + vec2(1.0, 1.0)), f - vec2(1.0, 1.0));
vec2 u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0); // quintic fade
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
}
Each corner’s term is a tilted plane: zero at the corner, rising in the gradient’s direction and falling against it. The fade curve blends the four planes. Near a corner, that corner’s plane dominates, so the noise passes through zero there with the corner’s slope. The bumps form between corners, where the planes disagree.
The result is centered on zero and, with unit gradients in 2D, stays within about ±0.7. Remap it before showing it
or using it as a mask: n * 0.7 + 0.5 gives roughly 0..1. Signed output is often what you want, though: added to a
coordinate, it pushes in both directions equally.
The fade here is the quintic one. Perlin’s 2002 revision of his algorithm made that change, because the cubic’s discontinuous second derivative shows as creases once the noise is lit. The same revision picked gradients from a small fixed set (in 3D, the twelve directions toward the edges of a cube) instead of fully random ones, which is cheaper and avoids clumps of similar gradients. The random angle above is the simplest version to read; a fixed set of eight directions picked by the hash works as well.
Simplex noise
Gradient noise on a square grid has two costs that grow with dimension. Every pixel blends 2ⁿ corners: 4 in 2D, 8 in 3D, 16 in 4D. And the square grid still leaves a slight bias along the axes. Simplex noise, Perlin’s 2001 follow-up, uses the simplest shape that tiles space instead: triangles in 2D, tetrahedra in 3D. That is n + 1 corners per pixel: 3 in 2D, 4 in 3D, 5 in 4D.
Two ideas make it work. To find the triangle, skew the plane so the triangle grid lines up with a square grid, take
floor there, and unskew; one comparison then says which of the cell’s two triangles holds the pixel. And instead of
blending the corners with a fade curve, each corner’s contribution fades to zero with distance on its own, by a
radial falloff, and the contributions are summed:
float simplexNoise(vec2 p) {
const float F = 0.3660254; // (sqrt(3) - 1) / 2: skews the plane onto a square grid
const float G = 0.2113249; // (3 - sqrt(3)) / 6: skews it back
vec2 i = floor(p + (p.x + p.y) * F); // the skewed cell
vec2 d0 = p - i + (i.x + i.y) * G; // offset from its first corner
// which triangle
vec2 o = d0.x > d0.y ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
vec2 d1 = d0 - o + G; // offset from the middle corner
vec2 d2 = d0 - 1.0 + 2.0 * G; // offset from the last corner
vec3 w = max(0.5 - vec3(dot(d0, d0), dot(d1, d1), dot(d2, d2)), 0.0);
w = w * w * w * w; // falloff: zero at distance sqrt(0.5)
vec3 n = vec3(dot(gradient(i), d0),
dot(gradient(i + o), d1),
dot(gradient(i + 1.0), d2));
return 99.0 * dot(w, n); // scaled to about -1..1
}
The 99 only normalizes the output for unit gradients; it changes with the falloff and the gradient set. In 2D simplex noise is about as fast as the square version, and its value is that the pattern has no visible axis. In 3D and 4D (noise that changes over time, or volumes) it is much cheaper, which is where it matters most. Its look is slightly different: rounder blobs, a little more contrast. The patent that covered simplex noise in three or more dimensions expired in 2022, so engines and libraries now ship it freely.
Which to use
Value noise is the cheapest and fine for masks and anything moving fast. Gradient noise is the default for textures: its features are evenly sized and don’t line up with the grid. Simplex noise is the pick in 3D or 4D, or when the faint axis alignment of square-grid noise shows. All three are building blocks: layered in octaves they become fBm, and fed back into their own coordinates they become domain warping.