Files
skills/shader-dev/techniques/procedural-noise.md
shihao 6487becf60 Initial commit: add all skills files
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 16:52:49 +08:00

17 KiB

Procedural Noise Skill

Use Cases

Procedural noise is the most fundamental technique in real-time GPU graphics. It applies to natural phenomena (fire, clouds, water, lava), terrain generation, texture synthesis, volume rendering, motion effects, and more.

Core idea: use mathematical functions to generate pseudo-random, spatially continuous signals on the GPU in real time, then produce multi-scale detail through FBM and domain warping.

Core Principles

Noise Functions

Generate random values at integer lattice points, then smoothly interpolate between them.

  • Value Noise: random scalars at lattice points + bilinear Hermite interpolation. N(p) = mix(mix(h00,h10,u), mix(h01,h11,u), v)
  • Simplex Noise: triangular lattice gradient dot products + radial falloff kernel. Skew K1=(sqrt(3)-1)/2, unskew K2=(3-sqrt(3))/6. Fewer lattice lookups, no axis-aligned artifacts.

Hash Functions

Map integer coordinates to pseudo-random values:

  • sin-based (short but precision-sensitive): fract(sin(dot(p, vec2(127.1,311.7))) * 43758.5453)
  • sin-free (cross-platform stable): fract(p * 0.1031) + dot mixing + fract

FBM (Fractal Brownian Motion)

Multi-octave noise summation: FBM(p) = sum of amplitude_i * noise(frequency_i * p)

  • Lacunarity ~2.0, Gain ~0.5, inter-octave rotation to eliminate artifacts

Domain Warping

Feed noise output back as coordinate offset: fbm(p + fbm(p)) or cascaded fbm(p + fbm(p + fbm(p)))

FBM Variant Quick Reference

Variant Formula Effect
Standard sum a*noise(p) Soft clouds
Ridged sum a*abs(noise(p)) Sharp ridges/lightning
Sinusoidal ridged sum a*sin(noise(p)*k) Periodic ridges/lava
Erosion sum a*noise(p)/(1+dot(d,d)) Realistic terrain
Ocean waves sum a*sea_octave(p) Peaked wave crests

Implementation Code

Hash Functions

// Sin-free hash (Dave Hoskins) — cross-platform stable
float hash12(vec2 p) {
    vec3 p3 = fract(vec3(p.xyx) * .1031);
    p3 += dot(p3, p3.yzx + 33.33);
    return fract((p3.x + p3.y) * p3.z);
}

vec2 hash22(vec2 p) {
    vec3 p3 = fract(vec3(p.xyx) * vec3(.1031, .1030, .0973));
    p3 += dot(p3, p3.yzx + 33.33);
    return fract((p3.xx + p3.yz) * p3.zy);
}

// Sin hash — shorter code, precision-sensitive on some GPUs
float hash(vec2 p) {
    float h = dot(p, vec2(127.1, 311.7));
    return fract(sin(h) * 43758.5453123);
}

vec2 hash2(vec2 p) {
    p = vec2(dot(p, vec2(127.1, 311.7)),
             dot(p, vec2(269.5, 183.3)));
    return -1.0 + 2.0 * fract(sin(p) * 43758.5453123);
}

Value Noise

// Hermite smooth bilinear interpolation
float noise(in vec2 x) {
    vec2 p = floor(x);
    vec2 f = fract(x);
    f = f * f * (3.0 - 2.0 * f);
    float a = hash(p + vec2(0.0, 0.0));
    float b = hash(p + vec2(1.0, 0.0));
    float c = hash(p + vec2(0.0, 1.0));
    float d = hash(p + vec2(1.0, 1.0));
    return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
}

Simplex Noise

// 2D Simplex (skewed triangular grid + h^4 falloff kernel)
float noise(in vec2 p) {
    const float K1 = 0.366025404;  // (sqrt(3)-1)/2
    const float K2 = 0.211324865;  // (3-sqrt(3))/6
    vec2 i = floor(p + (p.x + p.y) * K1);
    vec2 a = p - i + (i.x + i.y) * K2;
    vec2 o = (a.x > a.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
    vec2 b = a - o + K2;
    vec2 c = a - 1.0 + 2.0 * K2;
    vec3 h = max(0.5 - vec3(dot(a, a), dot(b, b), dot(c, c)), 0.0);
    vec3 n = h * h * h * h * vec3(
        dot(a, hash2(i + 0.0)),
        dot(b, hash2(i + o)),
        dot(c, hash2(i + 1.0))
    );
    return dot(n, vec3(70.0));
}

Standard FBM

#define OCTAVES 4
#define GAIN 0.5
mat2 m = mat2(1.6, 1.2, -1.2, 1.6);  // rotation+scale, |m|=2.0, ~36.87 deg

float fbm(vec2 p) {
    float f = 0.0, a = 0.5;
    for (int i = 0; i < OCTAVES; i++) {
        f += a * noise(p);
        p = m * p;
        a *= GAIN;
    }
    return f;
}

Manually unrolled version (slightly varying lacunarity to break self-similarity):

const mat2 mtx = mat2(0.80, 0.60, -0.60, 0.80);
float fbm4(vec2 p) {
    float f = 0.0;
    f += 0.5000 * (-1.0 + 2.0 * noise(p)); p = mtx * p * 2.02;
    f += 0.2500 * (-1.0 + 2.0 * noise(p)); p = mtx * p * 2.03;
    f += 0.1250 * (-1.0 + 2.0 * noise(p)); p = mtx * p * 2.01;
    f += 0.0625 * (-1.0 + 2.0 * noise(p));
    return f / 0.9375;
}

Ridged FBM

// abs() produces V-shaped ridges at zero crossings
float fbm_ridged(in vec2 p) {
    float z = 2.0, rz = 0.0;
    for (float i = 1.0; i < 6.0; i++) {
        rz += abs((noise(p) - 0.5) * 2.0) / z;
        z *= 2.0;
        p *= 2.0;
    }
    return rz;
}

// Sinusoidal ridged variant — lava texture
// rz += (sin(noise(p) * 7.0) * 0.5 + 0.5) / z;

Domain Warping

// Basic domain warping ("2D Clouds")
float q = fbm(uv * 0.5);
uv -= q - time;
float f = fbm(uv);

// Classic three-level cascade
vec2 fbm4_2(vec2 p) {
    return vec2(fbm4(p + vec2(1.0)), fbm4(p + vec2(6.2)));
}
float func(vec2 q, out vec2 o, out vec2 n) {
    o = 0.5 + 0.5 * fbm4_2(q);
    n = fbm6_2(4.0 * o);
    vec2 p = q + 2.0 * n + 1.0;
    float f = 0.5 + 0.5 * fbm4(2.0 * p);
    f = mix(f, f * f * f * 3.5, f * abs(n.x));
    return f;
}

// Dual-axis domain warping
float dualfbm(in vec2 p) {
    vec2 p2 = p * 0.7;
    vec2 basis = vec2(fbm(p2 - time * 1.6), fbm(p2 + time * 1.7));
    basis = (basis - 0.5) * 0.2;
    p += basis;
    return fbm(p * makem2(time * 0.2));
}

Fluid Noise

// Per-octave gradient displacement simulating fluid transport
#define FLOW_SPEED 0.6
#define BASE_SPEED 1.9
#define ADVECTION 0.77
#define GRAD_SCALE 0.5

vec2 gradn(vec2 p) {
    float ep = 0.09;
    float gradx = noise(vec2(p.x + ep, p.y)) - noise(vec2(p.x - ep, p.y));
    float grady = noise(vec2(p.x, p.y + ep)) - noise(vec2(p.x, p.y - ep));
    return vec2(gradx, grady);
}

float flow(in vec2 p) {
    float z = 2.0, rz = 0.0;
    vec2 bp = p;
    for (float i = 1.0; i < 7.0; i++) {
        p += time * FLOW_SPEED;
        bp += time * BASE_SPEED;
        vec2 gr = gradn(i * p * 0.34 + time * 1.0);
        gr *= makem2(time * 6.0 - (0.05 * p.x + 0.03 * p.y) * 40.0);
        p += gr * GRAD_SCALE;
        rz += (sin(noise(p) * 7.0) * 0.5 + 0.5) / z;
        p = mix(bp, p, ADVECTION);
        z *= 1.4;
        p *= 2.0;
        bp *= 1.9;
    }
    return rz;
}

Derivative FBM

// Value noise with analytic derivatives
vec3 noised(in vec2 x) {
    vec2 p = floor(x);
    vec2 f = fract(x);
    vec2 u = f * f * (3.0 - 2.0 * f);
    vec2 du = 6.0 * f * (1.0 - f);
    float a = hash(p + vec2(0, 0));
    float b = hash(p + vec2(1, 0));
    float c = hash(p + vec2(0, 1));
    float d = hash(p + vec2(1, 1));
    return vec3(
        a + (b - a) * u.x + (c - a) * u.y + (a - b - c + d) * u.x * u.y,
        du * (vec2(b - a, c - a) + (a - b - c + d) * u.yx)
    );
}

// Erosion FBM: higher gradient = lower contribution
float terrainFBM(in vec2 x) {
    const mat2 m2 = mat2(0.8, -0.6, 0.6, 0.8);
    float a = 0.0, b = 1.0;
    vec2 d = vec2(0.0);
    for (int i = 0; i < 16; i++) {
        vec3 n = noised(x);
        d += n.yz;
        a += b * n.x / (1.0 + dot(d, d));  // 1/(1+|grad|^2) erosion factor
        b *= 0.5;
        x = m2 * x * 2.0;
    }
    return a;
}

Quintic Noise with Analytical Derivatives

C2-continuous noise using quintic interpolation — eliminates visible grid artifacts in derivatives:

// Returns vec3(value, dFdx, dFdy) — derivatives are exact, not finite-differenced
vec3 noisedQ(vec2 p) {
    vec2 i = floor(p);
    vec2 f = fract(p);
    // Quintic interpolation for C2 continuity
    vec2 u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);
    vec2 du = 30.0 * f * f * (f * (f - 2.0) + 1.0);

    float a = hash12(i + vec2(0.0, 0.0));
    float b = hash12(i + vec2(1.0, 0.0));
    float c = hash12(i + vec2(0.0, 1.0));
    float d = hash12(i + vec2(1.0, 1.0));

    float k0 = a, k1 = b - a, k2 = c - a, k3 = a - b - c + d;
    return vec3(
        k0 + k1 * u.x + k2 * u.y + k3 * u.x * u.y,  // value
        du * vec2(k1 + k3 * u.y, k2 + k3 * u.x)       // derivatives
    );
}

FBM with Derivatives (Erosion Terrain)

Accumulates derivatives across octaves — derivative magnitude dampens amplitude, creating realistic erosion patterns:

vec3 fbmDerivative(vec2 p, int octaves) {
    float value = 0.0;
    vec2 deriv = vec2(0.0);
    float amplitude = 0.5;
    float frequency = 1.0;
    mat2 rot = mat2(0.8, 0.6, -0.6, 0.8); // inter-octave rotation

    for (int i = 0; i < octaves; i++) {
        vec3 n = noisedQ(p * frequency);
        deriv += n.yz;
        // Key: divide by (1 + dot(deriv, deriv)) for erosion effect
        value += amplitude * n.x / (1.0 + dot(deriv, deriv));
        frequency *= 2.0;
        amplitude *= 0.5;
        p = rot * p;  // rotate to break axis-aligned artifacts
    }
    return vec3(value, deriv);
}

Key insights:

  • Quintic interpolation: 6t^5 - 15t^4 + 10t^3 gives C2 continuous noise (vs Hermite's C1), eliminating visible grid artifacts in derivatives
  • Erosion FBM: The 1/(1+dot(d,d)) term causes flat areas to accumulate more detail while steep slopes stay smooth — mimicking real erosion
  • Inter-octave rotation: The 2x2 rotation matrix between octaves prevents axis-aligned patterns especially visible in ridged noise

Voronoise (Voronoi-Noise Hybrid)

Unified interpolation between value noise and Voronoi patterns:

// u=0: Value noise, u=1: Voronoi, v: smoothness (0=sharp cells, 1=smooth)
vec3 hash32(vec2 p) {
    vec3 p3 = fract(vec3(p.xyx) * vec3(.1031, .1030, .0973));
    p3 += dot(p3, p3.yxz + 33.33);
    return fract((p3.xxy + p3.yzz) * p3.zyx);
}

float voronoise(vec2 p, float u, float v) {
    float k = 1.0 + 63.0 * pow(1.0 - v, 6.0);
    vec2 i = floor(p);
    vec2 f = fract(p);
    vec2 a = vec2(0.0);
    for (int y = -2; y <= 2; y++)
    for (int x = -2; x <= 2; x++) {
        vec2 g = vec2(float(x), float(y));
        vec3 o = hash32(i + g) * vec3(u, u, 1.0);
        vec2 d = g - f + o.xy;
        float w = pow(1.0 - smoothstep(0.0, 1.414, length(d)), k);
        a += vec2(o.z * w, w);
    }
    return a.x / a.y;
}

Extremely versatile — smoothly interpolates between cellular Voronoi and continuous noise.

Preventing Aliasing in Procedural Textures

For distant surfaces, high-frequency noise octaves create moiré artifacts. Solutions:

  1. LOD-based octave count: int octaves = min(MAX_OCTAVES, int(log2(pixelSize))) — skip octaves finer than pixel size
  2. Analytical filtering: For simple patterns (checkers, stripes), use smoothstep with pixel width: smoothstep(-fw, fw, pattern) where fw = fwidth(uv)
  3. Derivative-based mip: Use textureGrad() with manually computed ray differentials for texture lookups in ray-marched scenes (see texture-mapping-advanced technique)

Complete Code Template

Ready to run in ShaderToy. Switch between standard FBM / ridged FBM / domain warping modes via #define:

// ============================================================
// Procedural Noise Skill — Complete Template
// ============================================================

// ========== Mode selection (uncomment to switch) ==========
#define MODE_STANDARD_FBM     // Standard FBM clouds
//#define MODE_RIDGED_FBM     // Ridged FBM lightning texture
//#define MODE_DOMAIN_WARP    // Domain warped organic pattern

// ========== Tunable parameters ==========
#define OCTAVES 6
#define GAIN 0.5
#define LACUNARITY 2.0
#define NOISE_SCALE 3.0
#define ANIM_SPEED 0.3
#define WARP_STRENGTH 0.4

// ========== Hash function ==========
float hash(vec2 p) {
    vec3 p3 = fract(vec3(p.xyx) * 0.1031);
    p3 += dot(p3, p3.yzx + 33.33);
    return fract((p3.x + p3.y) * p3.z);
}

// ========== Value noise ==========
float noise(in vec2 x) {
    vec2 p = floor(x);
    vec2 f = fract(x);
    f = f * f * (3.0 - 2.0 * f);
    float a = hash(p + vec2(0.0, 0.0));
    float b = hash(p + vec2(1.0, 0.0));
    float c = hash(p + vec2(0.0, 1.0));
    float d = hash(p + vec2(1.0, 1.0));
    return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
}

// ========== Rotation+scale matrix ==========
const mat2 m = mat2(1.6, 1.2, -1.2, 1.6);

// ========== Standard FBM ==========
float fbm(vec2 p) {
    float f = 0.0, a = 0.5;
    for (int i = 0; i < OCTAVES; i++) {
        f += a * (-1.0 + 2.0 * noise(p));
        p = m * p;
        a *= GAIN;
    }
    return f;
}

// ========== Ridged FBM ==========
float fbm_ridged(vec2 p) {
    float f = 0.0, a = 0.5;
    for (int i = 0; i < OCTAVES; i++) {
        f += a * abs(-1.0 + 2.0 * noise(p));
        p = m * p;
        a *= GAIN;
    }
    return f;
}

// ========== Domain warping vec2 FBM ==========
vec2 fbm2(vec2 p) {
    return vec2(fbm(p + vec2(1.7, 9.2)), fbm(p + vec2(8.3, 2.8)));
}

void mainImage(out vec4 fragColor, in vec2 fragCoord) {
    vec2 uv = (2.0 * fragCoord - iResolution.xy) / iResolution.y;
    uv *= NOISE_SCALE;
    float time = iTime * ANIM_SPEED;
    float f = 0.0;
    vec3 col = vec3(0.0);

#ifdef MODE_STANDARD_FBM
    f = 0.5 + 0.5 * fbm(uv + vec2(0.0, -time));
    vec3 sky = mix(vec3(0.4, 0.7, 1.0), vec3(0.2, 0.4, 0.6), fragCoord.y / iResolution.y);
    vec3 cloud = vec3(1.1, 1.1, 0.9) * f;
    col = mix(sky, cloud, smoothstep(0.4, 0.7, f));
#endif

#ifdef MODE_RIDGED_FBM
    f = fbm_ridged(uv + vec2(time * 0.5, time * 0.3));
    col = vec3(0.2, 0.1, 0.4) / max(f, 0.05);
    col = pow(col, vec3(0.99));
#endif

#ifdef MODE_DOMAIN_WARP
    vec2 q = fbm2(uv + time * 0.1);
    vec2 r = fbm2(uv + WARP_STRENGTH * q + vec2(1.7, 9.2));
    f = 0.5 + 0.5 * fbm(uv + WARP_STRENGTH * r);
    f = mix(f, f * f * f * 3.5, f * length(r));
    col = vec3(0.2, 0.1, 0.4);
    col = mix(col, vec3(0.3, 0.05, 0.05), f);
    col = mix(col, vec3(0.9, 0.9, 0.9), dot(r, r));
    col = mix(col, vec3(0.5, 0.2, 0.2), 0.5 * q.y * q.y);
    col *= f * 2.0;
    vec2 eps = vec2(1.0 / iResolution.x, 0.0);
    float fx = 0.5 + 0.5 * fbm(uv + eps.xy + WARP_STRENGTH * fbm2(uv + eps.xy + time * 0.1));
    float fy = 0.5 + 0.5 * fbm(uv + eps.yx + WARP_STRENGTH * fbm2(uv + eps.yx + time * 0.1));
    vec3 nor = normalize(vec3(fx - f, eps.x, fy - f));
    vec3 lig = normalize(vec3(0.9, -0.2, -0.4));
    float dif = clamp(0.3 + 0.7 * dot(nor, lig), 0.0, 1.0);
    col *= vec3(0.85, 0.90, 0.95) * (nor.y * 0.5 + 0.5) + vec3(0.15, 0.10, 0.05) * dif;
#endif

    vec2 p = fragCoord / iResolution.xy;
    col *= 0.5 + 0.5 * sqrt(16.0 * p.x * p.y * (1.0 - p.x) * (1.0 - p.y));
    fragColor = vec4(col, 1.0);
}

Common Variants

Ridged FBM

f += a * abs(noise(p));           // V-shaped ridges
f += a * (sin(noise(p)*7.0)*0.5+0.5); // Sinusoidal ridges (lava)

Domain Warped FBM

vec2 o = 0.5 + 0.5 * vec2(fbm(q + vec2(1.0)), fbm(q + vec2(6.2)));
vec2 n = vec2(fbm(4.0 * o + vec2(9.2)), fbm(4.0 * o + vec2(5.7)));
float f = 0.5 + 0.5 * fbm(q + 2.0 * n + 1.0);

Derivative Erosion FBM

vec2 d = vec2(0.0);
for (int i = 0; i < N; i++) {
    vec3 n = noised(p);
    d += n.yz;
    a += b * n.x / (1.0 + dot(d, d));
    b *= 0.5; p = m2 * p * 2.0;
}

Fluid Noise

for (float i = 1.0; i < 7.0; i++) {
    vec2 gr = gradn(i * p * 0.34 + time);
    gr *= makem2(time * 6.0 - (0.05*p.x+0.03*p.y)*40.0);
    p += gr * 0.5;
    rz += (sin(noise(p)*7.0)*0.5+0.5) / z;
    p = mix(bp, p, 0.77);
}

Ocean Wave Octave Function

float sea_octave(vec2 uv, float choppy) {
    uv += noise(uv);
    vec2 wv = 1.0 - abs(sin(uv));
    vec2 swv = abs(cos(uv));
    wv = mix(wv, swv, wv);
    return pow(1.0 - pow(wv.x * wv.y, 0.65), choppy);
}
// Bidirectional propagation in FBM:
d  = sea_octave((uv + SEA_TIME) * freq, choppy);
d += sea_octave((uv - SEA_TIME) * freq, choppy);
choppy = mix(choppy, 1.0, 0.2);

Performance & Composition

Performance optimization:

  • Reducing octave count is the most direct optimization; use fewer octaves for distant objects: int oct = 5 - int(log2(1.0 + t * 0.5));
  • Multi-level LOD: terrainL (3 oct) / terrainM (9 oct) / terrainH (16 oct)
  • Texture sampling instead of math hash: texture(iChannel0, x * 0.01).x
  • Manually unroll small loops + slightly vary lacunarity
  • Adaptive step size: float dt = max(0.05, 0.02 * t);
  • Directional derivative instead of full gradient (1 sample vs 3)
  • Early termination: if (sum.a > 0.99) break;

Common combinations:

  • FBM + Raymarching: noise-driven height/density fields, ray marching for intersection (terrain/ocean)
  • FBM + finite-difference normals + lighting: nor = normalize(vec3(f(p+ex)-f(p), eps, f(p+ey)-f(p)))
  • FBM + color mapping: different power curves mapping to RGB, e.g. flame vec3(1.5*c, 1.5*c^3, c^6) or inverse vec3(k)/rz
  • FBM + Fresnel water surface: fresnel = pow(1.0 - dot(n, -eye), 3.0)
  • Multi-layer FBM compositing: shape layer (low freq) + ridged layer (mid freq) + color layer (high freq)
  • FBM + volumetric lighting: density difference along light direction approximates illumination

Further Reading

For complete step-by-step tutorials, mathematical derivations, and advanced usage, see reference