2D Classic Perlin Noise Algorithm in GLSL

This article provides a concise technical breakdown and implementation guide for generating 2D classic Perlin noise using gradient hash vectors in OpenGL Shading Language (GLSL). Classic Perlin noise produces organic, continuous pseudo-random patterns by subdividing 2D space into a regular grid, assigning pseudo-random gradient vectors to each grid vertex via a hash function, computing the dot products between these gradients and local displacement vectors, and smoothly blending the results using an S-curve interpolation function.

The Algorithm Pipeline

The 2D classic Perlin noise algorithm processes any continuous coordinate vector vec2 p through four primary steps:

  1. Grid Partitioning: Deconstruct the continuous coordinate into integer cell coordinates (the lattice corners) and fractional coordinates (the relative position inside the cell).
  2. Gradient Vector Generation: Compute a deterministic pseudo-random unit or pseudo-unit 2D gradient vector at each of the four surrounding integer grid corners using a hashing function.
  3. Distance Vector Dot Products: Calculate four displacement vectors spanning from the four lattice corners to the fractional position, then compute the dot product between each displacement vector and its corresponding corner gradient.
  4. Interpolation: Smooth the fractional coordinates using a quintic polynomial fade curve, and bilinearly blend the four dot product scalar values.

Step-by-Step Mathematical Formulation

1. Coordinate Decomposition

Given an input point \(\mathbf{p} = (x, y)\):

  • Integer grid coordinate: \(\mathbf{i} = \lfloor\mathbf{p}\rfloor\)
  • Fractional offset: \(\mathbf{f} = \mathbf{p} - \mathbf{i}\) where \(\mathbf{f} \in [0, 1)^2\)

2. Hash-Based Gradient Generation

A deterministic hash function maps integer lattice points \((i_x, i_y)\) to pseudo-random direction vectors \(\mathbf{g}\). In GLSL, high-frequency trigonometric functions or modular arithmetic simulate an arbitrary permutation table: \[\mathbf{g}(\mathbf{v}) = (\cos(\theta), \sin(\theta)) \quad \text{where} \quad \theta = 2\pi \cdot \text{hash}(\mathbf{v})\]

3. Dot Products

For each of the four bounding corners \(\mathbf{c} \in \{(0,0), (1,0), (0,1), (1,1)\}\):

  • Displacement vector: \(\mathbf{d}_{\mathbf{c}} = \mathbf{f} - \mathbf{c}\)
  • Influence value: \(s_{\mathbf{c}} = \mathbf{g}(\mathbf{i} + \mathbf{c}) \cdot \mathbf{d}_{\mathbf{c}}\)

4. Quintic Fade Curve and Interpolation

Ken Perlin’s improved polynomial fade curve eliminates second-derivative discontinuities at grid boundaries: \[u(t) = 6t^5 - 15t^4 + 10t^3\]

Using \(\mathbf{u} = u(\mathbf{f})\), the values are blended: \[s_{\text{bottom}} = \text{lerp}(s_{(0,0)}, s_{(1,0)}, u_x)\] \[s_{\text{top}} = \text{lerp}(s_{(0,1)}, s_{(1,1)}, u_x)\] \[\text{Noise}(\mathbf{p}) = \text{lerp}(s_{\text{bottom}}, s_{\text{top}}, u_y)\]


Complete GLSL Implementation

// Deterministic hash to generate a pseudo-random unit gradient vector
vec2 hashGradient(vec2 p) {
    // Large prime multipliers for pseudo-random distribution
    float angle = fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123) * 6.28318530718;
    return vec2(cos(angle), sin(angle));
}

// 2D Classic Perlin Noise function
float perlinNoise(vec2 p) {
    // 1. Grid cell and fractional position
    vec2 i = floor(p);
    vec2 f = fract(p);

    // 2. Gradients at four grid corners
    vec2 g00 = hashGradient(i + vec2(0.0, 0.0));
    vec2 g10 = hashGradient(i + vec2(1.0, 0.0));
    vec2 g01 = hashGradient(i + vec2(0.0, 1.0));
    vec2 g11 = hashGradient(i + vec2(1.0, 1.0));

    // 3. Dot products with displacement vectors
    float n00 = dot(g00, f - vec2(0.0, 0.0));
    float n10 = dot(g10, f - vec2(1.0, 0.0));
    float n01 = dot(g01, f - vec2(0.0, 1.0));
    float n11 = dot(g11, f - vec2(1.0, 1.0));

    // 4. Quintic interpolation curve (C2 continuity)
    vec2 u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);

    // 5. Bilinear interpolation across x, then y
    return mix(
        mix(n00, n10, u.x),
        mix(n01, n11, u.x),
        u.y
    );
}

Output Range and Normalization

The resulting output of perlinNoise(vec2 p) ranges between approximately \(-\frac{\sqrt{2}}{2}\) and \(+\frac{\sqrt{2}}{2}\) (roughly \([-0.707, 0.707]\)). To remap this result to a standard normalized unsigned range \([0.0, 1.0]\) for texture coordinate operations or color values, apply an affine transform:

float normalizedNoise = perlinNoise(uv * scale) * 0.5 + 0.5;