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:
- Grid Partitioning: Deconstruct the continuous coordinate into integer cell coordinates (the lattice corners) and fractional coordinates (the relative position inside the cell).
- 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.
- 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.
- 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;