Skip to content
Fundamental · NoiseIntermediate

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 product of that gradient with the offset from the corner to the pixel:

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.

Further reading