2D Value Noise in GLSL with Bicubic Interpolation
This article demonstrates how to implement two-dimensional value noise in GLSL by pairing coordinate hashing with bicubic interpolation. Value noise assigns pseudo-random scalar values to a regular integer grid and smoothly interpolates between them across continuous space. By using a hash function to deterministically generate grid values and applying a bicubic curve to the fractional coordinates, the resulting noise achieves continuous first-order derivatives (\(C^1\) continuity) without visible grid artifacts.
The Mathematical Foundation
2D value noise operates over continuous coordinates \(\mathbf{p} = (x, y)\). The evaluation process divides \(\mathbf{p}\) into:
- Integer Lattice Coordinates: \(\mathbf{i} = \lfloor\mathbf{p}\rfloor\), representing the four corners of the grid cell: \((i_x, i_y)\), \((i_x + 1, i_y)\), \((i_x, i_y + 1)\), and \((i_x + 1, i_y + 1)\).
- Fractional Coordinates: \(\mathbf{f} = \text{fract}(\mathbf{p}) = \mathbf{p} - \mathbf{i}\), representing the relative offset inside the cell, where \(\mathbf{f} \in [0, 1)^2\).
Linear interpolation across grid corners yields visible creasing along cell borders due to discontinuous first derivatives. Bicubic interpolation resolves this by passing the fractional coordinates through a cubic polynomial whose derivative is zero at both \(0\) and \(1\).
Step 1: Coordinate Hashing
A deterministic hash function maps 2D integer coordinates to a pseudo-random scalar value in the range \([0, 1]\). In GLSL, high-frequency trigonometric functions or bitwise operations are commonly used.
// Deterministic 2D-to-1D pseudo-random hash
float hash(vec2 p) {
// Dot product with arbitrary large prime-like constants
float h = dot(p, vec2(127.1, 311.7));
return fract(sin(h) * 43758.5453123);
}This function ensures that any specific integer coordinate \((x, y)\) always produces the same scalar value without requiring memory lookups or texture sampling.
Step 2: Bicubic Hermite Interpolation
Bicubic interpolation across a 4-point unit square is achieved by
evaluating the tensor product of two 1D cubic Hermite curves. The
standard cubic S-curve (the standard GLSL smoothstep
formulation) is defined as:
\[S(t) = 3t^2 - 2t^3\]
When applied to both components of \(\mathbf{f}\), the derivatives at cell boundaries (\(t = 0\) and \(t = 1\)) equal zero:
\[\frac{d}{dt}(3t^2 - 2t^3) = 6t - 6t^2 \implies S'(0) = 0, \quad S'(1) = 0\]
Because the derivatives match across boundaries, neighboring cells blend seamlessly without directional seams.
// Cubic Hermite curve applied to both X and Y components
vec2 u = f * f * (3.0 - 2.0 * f);Step 3: Complete GLSL Implementation
The full noise function computes the four corner hashes and performs a bilinear blend driven by the cubic interpolation weights:
#ifdef GL_ES
precision highp float;
#endif
// Hash function mapping integer lattice positions to [0.0, 1.0]
float hash(vec2 p) {
p = fract(p * vec2(123.34, 456.21));
p += dot(p, p + 45.32);
return fract(p.x * p.y);
}
// 2D Value Noise with Bicubic Interpolation
float valueNoise(vec2 p) {
// 1. Grid cell base coordinate and internal fractional offset
vec2 i = floor(p);
vec2 f = fract(p);
// 2. Sample random values at the four unit-cell corners
float a = hash(i + vec2(0.0, 0.0));
float b = hash(i + vec2(1.0, 0.0));
float c = hash(i + vec2(0.0, 1.0));
float d = hash(i + vec2(1.0, 1.0));
// 3. Compute bicubic Hermite weights: 3f^2 - 2f^3
vec2 u = f * f * (3.0 - 2.0 * f);
// 4. Interpolate horizontally, then vertically
return mix(
mix(a, b, u.x),
mix(c, d, u.x),
u.y
);
}Alternative: 16-Point Bicubic Spline (Catmull-Rom)
If continuous second derivatives (\(C^2\)) or non-zero boundary slopes are required, evaluate a \(4\times 4\) neighborhood using a Catmull-Rom or B-spline filter:
// 1D cubic interpolation kernel across 4 samples
float cubic(float v0, float v1, float v2, float v3, float x) {
float p = (v3 - v2) - (v0 - v1);
float q = (v0 - v1) - p;
float r = v2 - v0;
float s = v1;
return p * x * x * x + q * x * x + r * x + s;
}
// 16-sample bicubic value noise
float valueNoiseBicubic4x4(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
float rows[4];
for (int y = -1; y <= 2; y++) {
float p0 = hash(i + vec2(-1.0, float(y)));
float p1 = hash(i + vec2( 0.0, float(y)));
float p2 = hash(i + vec2( 1.0, float(y)));
float p3 = hash(i + vec2( 2.0, float(y)));
rows[y + 1] = cubic(p0, p1, p2, p3, f.x);
}
return cubic(rows[0], rows[1], rows[2], rows[3], f.y);
}The 4-sample Hermite method is computationally efficient and suitable for real-time fragment shaders, while the 16-sample spline ensures higher curve fidelity at the cost of additional hash evaluations.