Procedural Textures in GLSL — noise, fBm, marble, wood, domain warping
A procedural texture is computed entirely from maths — no image files, no memory bandwidth, infinite resolution and seamless tiling at zero cost. Starting from a handful of GLSL noise primitives you can synthesise marble veins, wood grain, reptile skin, cloud banks and alien terrain, all inside a single fragment shader.
1. Noise primitives — value vs gradient noise
All procedural textures start with a noise function mapping coordinates to smooth pseudo-random values. Two families dominate real-time work:
- Value noise: assign a random scalar to each lattice corner; interpolate smoothly. Fast but produces a blocky "pillow" artefact because gradient information is lost at cell borders.
- Gradient noise (Perlin / Simplex): assign a random unit gradient vector to each corner; evaluate the dot product with the distance vector; interpolate. Avoids value-noise banding and produces natural-looking variation.
// --- Value noise (GLSL) ---
float hash21(vec2 p) {
p = fract(p * vec2(127.1, 311.7));
p += dot(p, p.yx + 19.19);
return fract(p.x * p.y);
}
float valueNoise(vec2 p) {
vec2 i = floor(p), f = fract(p);
float a = hash21(i),
b = hash21(i + vec2(1,0)),
c = hash21(i + vec2(0,1)),
d = hash21(i + vec2(1,1));
vec2 u = f * f * (3.0 - 2.0 * f); // smoothstep curve
return mix(mix(a,b,u.x), mix(c,d,u.x), u.y);
}
3t²−2t³ with
6t⁵ − 15t⁴ + 10t³ (Ken Perlin's improvement) ensures
first and second derivatives are zero at cell borders,
giving a noticeably smoother result.
2. Perlin gradient noise in GLSL
The key operation: at each lattice corner select a pseudo-random gradient and dot it with the vector from that corner to the sample point. Smooth interpolation of the four dot products gives the noise value.
// Gradient noise — quintic fade, analytic derivatives optional
vec2 gradHash(vec2 p) {
float n = dot(p, vec2(127.1, 311.7));
n = fract(sin(n) * 43758.5453);
return normalize(vec2(
fract(n * 127.1) * 2.0 - 1.0,
fract(n * 311.7) * 2.0 - 1.0
));
}
float perlinNoise(vec2 p) {
vec2 i = floor(p), f = fract(p);
vec2 u = f*f*f*(f*(f*6.0-15.0)+10.0); // quintic fade
float a = dot(gradHash(i + vec2(0,0)), f - vec2(0,0)),
b = dot(gradHash(i + vec2(1,0)), f - vec2(1,0)),
c = dot(gradHash(i + vec2(0,1)), f - vec2(0,1)),
d = dot(gradHash(i + vec2(1,1)), f - vec2(1,1));
return mix(mix(a,b,u.x), mix(c,d,u.x), u.y) * 0.5 + 0.5; // remap [-1,1]→[0,1]
}
Perlin noise has a known limitation: its power spectrum peaks at a fixed spatial frequency. Combining multiple octaves (the next section) adds the multi-scale detail found in natural phenomena.
3. Fractional Brownian Motion (fBm)
fBm sums several scaled copies of the same noise function, each at a higher frequency and lower amplitude. The defining parameter is the Hurst exponent H which controls the roughness.
Standard defaults: lacunarity = 2.0 (double frequency each octave)
amplitude = 0.5 (halve amplitude each octave, H = 0.5)
Rougher terrain: amplitude = 0.6–0.7 (H closer to 0, more high-freq energy)
Smoother clouds: amplitude = 0.4–0.45 (H closer to 1)
float fbm(vec2 p, int octaves) {
float value = 0.0;
float amp = 0.5;
float freq = 1.0;
for (int i = 0; i < octaves; i++) {
value += amp * perlinNoise(p * freq);
freq *= 2.0; // lacunarity
amp *= 0.5; // persistence (gain)
}
return value;
}
// Usage in a terrain height-map (6 octaves ≈ 11 noise evals)
float h = fbm(uv * 4.0, 6);
- Ridge noise: replace
noise()with1.0 − abs(noise() * 2.0 − 1.0)to turn gentle hills into sharp ridgelines. - Billow noise:
abs(noise() * 2.0 − 1.0)produces a lumpy cauliflower pattern, good for cumulus clouds.
4. Worley / cellular noise
Worley noise (Steven Worley, 1996) is distance-based: for each fragment, find the closest and second-closest feature points seeded pseudo-randomly in cells of a regular grid. The distances F1 and F2 yield a natural cell structure — stone tiles, reptile scales, cracked mud.
vec2 worley(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
float F1 = 1e9, F2 = 1e9;
for (int dy = -1; dy <= 1; dy++) {
for (int dx = -1; dx <= 1; dx++) {
vec2 nb = i + vec2(dx, dy);
vec2 jitter = hash21(nb) * 0.9; // randomize feature point within cell
vec2 r = vec2(dx, dy) + jitter - f;
float d = dot(r, r); // squared Euclidean distance
if (d < F1) { F2 = F1; F1 = d; }
else if (d < F2) { F2 = d; }
}
}
return sqrt(vec2(F1, F2));
}
// Useful combinations:
// F1 — filled cells (stones)
// F2 - F1 — cell edges (cracks, reptile skin)
// 1.0 - F1 — inverted (bubbles, foam)
sin/fract
halves the math count.
5. Marble — turbulence and sine stripes
Real marble consists of nearly parallel veins warped by geological pressure. In the shader: create sine-wave stripes along one axis, then perturb the input position with fBm turbulence before evaluating the sine.
marble(x) = 0.5 + 0.5 · sin( x.y · stripesPerUnit + turb · strength )
float turbulence(vec2 p, int oct) {
float v = 0.0, amp = 0.5, freq = 1.0;
for (int i = 0; i < oct; i++) {
v += amp * abs(perlinNoise(p * freq) * 2.0 - 1.0); // absolute value
freq *= 2.0; amp *= 0.5;
}
return v;
}
float marblePattern(vec2 uv) {
float turb = turbulence(uv, 6);
return 0.5 + 0.5 * sin(uv.y * 8.0 + turb * 5.0);
}
// Colour: lerp from dark marble to bright vein colour
vec3 marble = mix(vec3(0.1,0.12,0.15), vec3(0.9,0.87,0.82), marblePattern(uv));
Adjust the stripe frequency (8.0), turbulence strength (5.0) and colour palette to produce white Carrara marble, verde antico or black and gold varieties.
6. Wood grain — radial rings with fBm perturbation
Wood grain is concentric annual rings centred on a growth axis. The distance from the centre drives a stripe pattern; fBm perturbation breaks the perfect symmetry:
float woodPattern(vec2 uv) {
// Radial distance from growth axis (point at origin)
float radius = length(uv);
// Perturb with fBm so rings aren't perfect circles
radius += fbm(uv * 3.0, 4) * 0.4;
// Sine rings: number of rings proportional to ringFreq
float ring = fract(radius * 6.0);
// Smooth step to create alternating dark/light bands
return smoothstep(0.2, 0.3, ring) - smoothstep(0.7, 0.8, ring);
}
// Earthy wood palette
vec3 darkWood = vec3(0.25, 0.14, 0.06);
vec3 lightWood = vec3(0.65, 0.45, 0.22);
vec3 wood = mix(darkWood, lightWood, woodPattern(uv));
Adding a fine-grained noise at higher frequency on top of the ring pattern simulates the wood-fibre micro-texture visible in close-up renders.
7. Domain warping — Inigo Quilez's technique
Domain warping (Quilez, 2003) is one of the most powerful procedural texture tools: perturb the input coordinates with noise before querying noise again. A two-level warp produces extraordinarily complex, organic patterns from simple maths:
g(p) = fBm(p + f(p)*k) ← input warped once
h(p) = fBm(p + g(p)*k) ← input warped twice
h is used as the final pattern; k controls warp strength (≈ 1–4)
vec2 fbm2(vec2 p, int oct) {
return vec2(fbm(p, oct), fbm(p + vec2(3.7, 1.3), oct)); // 2D noise
}
float domainWarp(vec2 p) {
vec2 q = fbm2(p, 5); // first warp
vec2 r = fbm2(p + q * 2.0, 5); // second warp
return fbm(p + r * 2.0, 5); // final eval
}
// Used for: clouds, nebulae, flame, lava, alien terrain
8. Full showcase shader
The fragment shader below combines everything: marble, wood rings,
reptile-skin via Worley, and a domain-warped cloud layer.
Select which pattern via a uMode uniform.
// --- Full procedural texture showcase (GLSL ES 3.00) ---
#version 300 es
precision highp float;
uniform float uTime;
uniform vec2 uResolution;
uniform int uMode; // 0=marble 1=wood 2=reptile 3=cloud
out vec4 fragColor;
// ---- Helpers -------------------------------------------------------
float hash(vec2 p) {
p = fract(p * vec2(127.1, 311.7));
p += dot(p, p.yx + 19.19);
return fract(p.x * p.y);
}
vec2 gradHash(vec2 p) {
float n = fract(sin(dot(p, vec2(127.1,311.7))) * 43758.5453);
return normalize(vec2(fract(n*127.1)*2.0-1.0, fract(n*311.7)*2.0-1.0));
}
float gnoise(vec2 p) {
vec2 i=floor(p), f=fract(p);
vec2 u=f*f*f*(f*(f*6.0-15.0)+10.0);
float a=dot(gradHash(i ),f ),
b=dot(gradHash(i+vec2(1,0)),f-vec2(1,0)),
c=dot(gradHash(i+vec2(0,1)),f-vec2(0,1)),
d=dot(gradHash(i+vec2(1,1)),f-vec2(1,1));
return mix(mix(a,b,u.x),mix(c,d,u.x),u.y)*0.5+0.5;
}
float fbm(vec2 p) {
float v=0.0,a=0.5;
for(int i=0;i<6;i++){v+=a*gnoise(p);p*=2.0;a*=0.5;}
return v;
}
float turb(vec2 p) {
float v=0.0,a=0.5;
for(int i=0;i<6;i++){v+=a*abs(gnoise(p)*2.0-1.0);p*=2.0;a*=0.5;}
return v;
}
float worleyF1(vec2 p) {
vec2 i=floor(p); vec2 f=fract(p); float F1=1e9;
for(int dy=-1;dy<=1;dy++) for(int dx=-1;dx<=1;dx++){
vec2 nb=i+vec2(dx,dy), r=vec2(dx,dy)+hash(nb)*0.9-f;
F1=min(F1,dot(r,r));
}
return sqrt(F1);
}
float worleyF2mF1(vec2 p) {
vec2 i=floor(p); vec2 f=fract(p); float F1=1e9,F2=1e9;
for(int dy=-1;dy<=1;dy++) for(int dx=-1;dx<=1;dx++){
vec2 nb=i+vec2(dx,dy), r=vec2(dx,dy)+hash(nb)*0.9-f;
float d=dot(r,r);
if(delse if(dreturn sqrt(F2)-sqrt(F1);
}
// ---- Patterns -------------------------------------------------------
vec3 pMarble(vec2 uv) {
float t = 0.5+0.5*sin(uv.y*8.0+turb(uv)*5.0);
return mix(vec3(0.1,0.12,0.15), vec3(0.9,0.87,0.82),t);
}
vec3 pWood(vec2 uv) {
float r = length(uv*0.8) + fbm(uv*3.0)*0.5;
float ring = smoothstep(0.2,0.3,fract(r*6.0))-smoothstep(0.7,0.8,fract(r*6.0));
return mix(vec3(0.25,0.14,0.06), vec3(0.65,0.45,0.22),ring);
}
vec3 pReptile(vec2 uv) {
float cells = worleyF2mF1(uv*5.0);
vec3 col = mix(vec3(0.05,0.17,0.08), vec3(0.2,0.6,0.15),
smoothstep(0.0,0.25,cells));
return col * (1.0 - worleyF1(uv*5.0)*0.6);
}
vec3 pCloud(vec2 uv) {
vec2 q = vec2(fbm(uv+uTime*0.05), fbm(uv+vec2(3.7,1.3)));
vec2 r = vec2(fbm(uv+q*2.0), fbm(uv+q*2.0+vec2(1.7,9.2)));
float f = fbm(uv + r*2.0);
vec3 sky = vec3(0.05,0.08,0.18);
vec3 cloud = vec3(0.8, 0.85, 1.0);
return mix(sky, cloud, smoothstep(0.3,0.7,f));
}
// ---- Main -----------------------------------------------------------
void main() {
vec2 uv = (gl_FragCoord.xy - 0.5*uResolution) / uResolution.y * 3.0;
vec3 col;
if (uMode==0) col = pMarble(uv);
else if (uMode==1) col = pWood(uv);
else if (uMode==2) col = pReptile(uv);
else col = pCloud(uv);
fragColor = vec4(col, 1.0);
}
🌊 Ray Marching & SDFs
Combine procedural textures with distance-field geometry — marble spheres, wood-grain boxes and cloud volumes without a single mesh.