Shaders · Procedural Generation · GLSL
📅 March 2026 ⏱ ≈ 12 min read 🎯 Intermediate — Advanced · Last updated: 22 June 2026

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.

TL;DR: Procedural textures are built entirely from GLSL math instead of image files. Starting from value and Perlin gradient noise, this guide layers octaves into fBm, adds Worley cellular noise, and shows how to synthesise marble, wood grain, reptile skin and domain-warped clouds — plus a full showcase fragment shader combining all of them.

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 →