The Perlin Noise Algorithm

The Perlin Noise Algorithm

🌐 Auf Deutsch lesen
πŸ“… 2026-06-29 ✍️ Andreas Wittmann πŸ‘οΈ ... algorithm graphics computer-graphics

Plain random numbers are the wrong kind of randomness for most graphics and simulation work. A grid where every cell is an independent rand() % 256 looks like television static β€” useful for nothing. Clouds, terrain, marble, fire, and countless procedural textures instead need randomness that is smooth: neighboring points should have similar values, while distant points can differ completely. Ken Perlin developed exactly that in 1983 (originally for the movie Tron), and it won him a Technical Achievement Award from the Academy of Motion Picture Arts and Sciences in 1997.

The idea

Lay an invisible grid over the plane, and give every grid corner a fixed pseudo-random gradient vector β€” a direction, not a value. To find the noise value at an arbitrary point, first find which grid cell it falls into, then compute the dot product between each of that cell's four corner gradients and the vector pointing from that corner to the query point. That gives four numbers, one per corner, each describing how much that corner's gradient "points toward" or "away from" the query point.

Those four numbers then get blended together with a weighted average based on how close the query point is to each corner β€” smoothly, not linearly, using a curve (the fade function below) that eases in and out instead of changing at a constant rate. That smoothing is what keeps the result continuous across cell boundaries: two points on either side of a grid line get nearly the same blend of nearby gradients, so the noise never jumps.

The gradients themselves come from a fixed permutation table: a shuffled list of the numbers 0–255, indexed by grid coordinates to pick one of a small set of gradient directions per corner. Since the table is fixed once and reused everywhere, the same input coordinates always produce the same noise value β€” Perlin noise is a deterministic function of (x, y), not actually random at all once the table is built.

Implementation

#include "plot.hpp"
#include <math.h>
#include <stdlib.h>

int perm[512];

void init_permutation() {
    int p[256];
    for (int i = 0; i < 256; i++) p[i] = i;

    srand(1);
    for (int i = 255; i > 0; i--) {
        int j = rand() % (i + 1);
        int tmp = p[i];
        p[i] = p[j];
        p[j] = tmp;
    }
    for (int i = 0; i < 512; i++) perm[i] = p[i & 255];
}

double fade(double t) {
    return t * t * t * (t * (t * 6 - 15) + 10);
}

double lerp(double t, double a, double b) {
    return a + t * (b - a);
}

double grad(int hash, double x, double y) {
    switch (hash & 7) {
        case 0: return  x + y;
        case 1: return  x - y;
        case 2: return -x + y;
        case 3: return -x - y;
        case 4: return  x;
        case 5: return -x;
        case 6: return  y;
        default: return -y;
    }
}

double perlin(double x, double y) {
    int xi = (int)floor(x) & 255;
    int yi = (int)floor(y) & 255;
    double xf = x - floor(x);
    double yf = y - floor(y);

    double u = fade(xf);
    double v = fade(yf);

    int aa = perm[perm[xi] + yi];
    int ab = perm[perm[xi] + yi + 1];
    int ba = perm[perm[xi + 1] + yi];
    int bb = perm[perm[xi + 1] + yi + 1];

    double x1 = lerp(u, grad(aa, xf, yf), grad(ba, xf - 1, yf));
    double x2 = lerp(u, grad(ab, xf, yf - 1), grad(bb, xf - 1, yf - 1));

    return lerp(v, x1, x2);
}

// Fractal Brownian motion: several octaves of Perlin noise at increasing
// frequency and decreasing amplitude, summed together.
double fbm(double x, double y, int octaves) {
    double total = 0;
    double amplitude = 1.0;
    double frequency = 1.0;
    double maxValue = 0;

    for (int i = 0; i < octaves; i++) {
        total += perlin(x * frequency, y * frequency) * amplitude;
        maxValue += amplitude;
        amplitude *= 0.5;
        frequency *= 2.0;
    }

    return total / maxValue;
}

void frame() {
    static bool ready = false;
    if (!ready) {
        init_permutation();
        ready = true;
    }

    int cell = 4;
    for (int py = 0; py < plot_height; py += cell) {
        for (int px = 0; px < plot_width; px += cell) {
            double n = fbm(px / 80.0, py / 80.0, 4);

            int gray = (int)((n + 1.0) * 0.5 * 255.0);
            if (gray < 0) gray = 0;
            if (gray > 255) gray = 255;

            PlotColor(gray, gray, gray);
            PlotFilledRectangle(px, py, px + cell, py + cell);
        }
    }
}
Open in full editor →

init_permutation builds the shuffled table once (guarded by the ready flag in frame(), since this sample has no setup() of its own β€” see below). grad picks one of eight fixed directions based on a hash value and returns the dot product with (x, y) directly, which is a common simplification of Perlin's original 3D gradient set for two dimensions. perlin does the four-corner lookup-and-blend described above; fade is the smoothing curve, lerp the weighted blend.

fbm (fractal Brownian motion) is what actually makes the result look like clouds rather than a blurry blob: it samples perlin several times at doubling frequency and halving amplitude, and adds the results together. The first octave provides the broad shape, and each following octave adds finer detail on top β€” the same idea behind adding overtones to a fundamental frequency in sound.

This sample defines no setup(), so the default PlotCanvas(640, 480) applies automatically β€” the same pattern used by the Bresenham line and midpoint circle samples. frame() draws the noise as a grid of 4Γ—4 pixel blocks rather than one draw call per pixel, mapping each block's fbm value from its native [-1, 1]-ish range to a grayscale brightness.

Walking through an example

Consider the noise value at x = 0.5, y = 0.5 β€” the exact center of the grid cell between corners (0,0), (1,0), (0,1), (1,1). Each corner's contribution is its gradient's dot product with the vector to (0.5, 0.5) relative to that corner:

corner vector to point example gradient dot product
(0,0) (0.5, 0.5) (1, 1) direction β†’ x+y 0.5 + 0.5 = 1.0
(1,0) (βˆ’0.5, 0.5) (1, βˆ’1) direction β†’ xβˆ’y βˆ’0.5 βˆ’ 0.5 = βˆ’1.0
(0,1) (0.5, βˆ’0.5) (βˆ’1, 1) direction β†’ βˆ’x+y βˆ’0.5 βˆ’ 0.5 = βˆ’1.0
(1,1) (βˆ’0.5, βˆ’0.5) (βˆ’1, βˆ’1) direction β†’ βˆ’xβˆ’y 0.5 + 0.5 = 1.0

At the exact midpoint, fade(0.5) = 0.5 for both axes, so the blend weights every corner equally. Averaging the four dot products above gives 0, which is exactly what should happen at a point equidistant from all four corners with these particular (illustrative) gradients: no single corner's direction dominates. Off-center points weight the nearer corners more heavily, which is where the actual shape of the noise comes from.

Why the smoothing matters

Two design choices in this algorithm are easy to get wrong, and both happen to be visible if you do:

Conclusion

Perlin noise turns a fixed table of pseudo-random directions into a continuous function that looks organic at any zoom level and any point, while staying entirely deterministic β€” the same (x, y) always produces the same value, with no state to store beyond the permutation table itself. That combination β€” natural-looking, infinitely dense, and cheap to recompute rather than store β€” is why it still underlies so much procedural content, four decades after it was built for a single movie.