Shaders · Procedural Generation · GLSL
📅 March 2026 ⏱ ≈ 12 min read 🎯 Intermediate — Advanced

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 (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);
}
Quintic fade curves: replacing 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.

fBm(x) = Σi=0..N-1 amplitudei · noise(lacunarityi · x)

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);

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)
Performance: checking only the 3×3 neighbourhood (9 cells) guarantees finding the closest point for reasonably jittered feature points. On some GPUs using a permutation hash array instead of 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.

turbulence(x) = Σ amplitudei · |noise(lacunarityi · x)|

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:

f(p) = fBm(p) ← ordinary fBm
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
Performance note: double domain warp calls fBm three times. At 5 octaves each that is 15 noise evaluations per fragment. On mid-range mobile GPUs, keep octave counts lower (3–4) or use value noise instead of gradient noise.

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.

Read ray marching →