Files
skills/shader-dev/reference/domain-warping.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

Domain Warping — Detailed Reference

This document contains the complete step-by-step tutorial, mathematical derivations, and advanced usage for domain warping techniques. See SKILL.md for the condensed version.

Prerequisites

  • GLSL Basics: uniform variables, built-in functions (mix, smoothstep, fract, floor, sin, dot)
  • Vector Math: dot product, matrix multiplication, 2D rotation matrix
  • Noise Function Concepts: understanding the basic principle of value noise (lattice interpolation)
  • fBM (Fractal Brownian Motion): superposition of multiple noise layers at different frequencies/amplitudes
  • ShaderToy Environment: meaning of iTime, iResolution, fragCoord

Implementation Steps in Detail

Step 1: Hash Function

What: Implement a hash function that maps 2D integer coordinates to a pseudo-random float.

Why: This is the foundation of noise functions — producing deterministic "random" values at each lattice point. The sin-dot trick compresses 2D input to 1D then takes the fractional part, using sin's high-frequency oscillation to produce a chaotic distribution.

Code:

float hash(vec2 p) {
    p = fract(p * 0.6180339887); // Golden ratio pre-perturbation
    p *= 25.0;
    return fract(p.x * p.y * (p.x + p.y));
}

Note: The classic fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453) version can also be used, but the sin-free version above is more stable in precision on some GPUs.

Step 2: Value Noise

What: Implement 2D value noise — take hash values at integer lattice points and interpolate between them with Hermite smoothing.

Why: Value noise is the simplest continuous noise, producing smooth, jump-free output suitable as the foundation for fBM. Hermite interpolation f*f*(3.0-2.0*f) ensures the derivative is zero at lattice points, avoiding the angular appearance of linear interpolation.

Code:

float noise(vec2 p) {
    vec2 i = floor(p);
    vec2 f = fract(p);
    f = f * f * (3.0 - 2.0 * f); // Hermite smooth interpolation

    return mix(
        mix(hash(i + vec2(0.0, 0.0)), hash(i + vec2(1.0, 0.0)), f.x),
        mix(hash(i + vec2(0.0, 1.0)), hash(i + vec2(1.0, 1.0)), f.x),
        f.y
    );
}

Step 3: fBM (Fractal Brownian Motion)

What: Superpose multiple noise layers at different frequencies/amplitudes to create fractal noise with self-similar properties.

Why: A single noise layer is too uniform. fBM superimposes multiple "octaves" to simulate nature's fractal structures. Each layer doubles in frequency (lacunarity ~ 2.0), halves in amplitude (persistence = 0.5), and uses a rotation matrix to break lattice alignment.

Code:

const mat2 mtx = mat2(0.80, 0.60, -0.60, 0.80); // Rotation ~36.87°, for decorrelation

float fbm(vec2 p) {
    float f = 0.0;
    f += 0.500000 * noise(p); p = mtx * p * 2.02;
    f += 0.250000 * noise(p); p = mtx * p * 2.03;
    f += 0.125000 * noise(p); p = mtx * p * 2.01;
    f += 0.062500 * noise(p); p = mtx * p * 2.04;
    f += 0.031250 * noise(p); p = mtx * p * 2.01;
    f += 0.015625 * noise(p);
    return f / 0.96875; // Normalize: sum of all amplitudes
}

Using lacunarity values of 2.01~2.04 rather than exact 2.0 is to avoid visual artifacts caused by lattice regularity. This is a widely adopted trick in classic implementations.

Step 4: Domain Warping (Core)

What: Use fBM output as a coordinate offset, recursively nesting to form multi-level warping.

Why: This is the core of the entire technique. fbm(p) generates a scalar field; adding it to the coordinate p is equivalent to "pulling and stretching space according to the noise field's shape." Multi-level nesting makes the deformation more complex and organic — each warping level operates in space already deformed by the previous level.

Code:

float pattern(vec2 p) {
    return fbm(p + fbm(p + fbm(p)));
}

This single line is the classic three-level domain warping. It can be decomposed for understanding:

float pattern(vec2 p) {
    float warp1 = fbm(p);           // Level 1: noise in original space
    float warp2 = fbm(p + warp1);   // Level 2: noise in first-level warped space
    float result = fbm(p + warp2);  // Level 3: final value in second-level warped space
    return result;
}

Step 5: Time Animation

What: Inject iTime into specific fBM octaves so the warp field evolves over time.

Why: Directly offsetting all octaves causes uniform translation, lacking organic feel. The classic approach is to inject time only in the lowest frequency (first layer) and highest frequency (last layer) — low frequency drives overall flow, high frequency adds detail variation.

Code:

float fbm(vec2 p) {
    float f = 0.0;
    f += 0.500000 * noise(p + iTime);  // Lowest frequency with time: slow overall flow
    p = mtx * p * 2.02;
    f += 0.250000 * noise(p); p = mtx * p * 2.03;
    f += 0.125000 * noise(p); p = mtx * p * 2.01;
    f += 0.062500 * noise(p); p = mtx * p * 2.04;
    f += 0.031250 * noise(p); p = mtx * p * 2.01;
    f += 0.015625 * noise(p + sin(iTime)); // Highest frequency with time: subtle detail motion
    return f / 0.96875;
}

Step 6: Coloring

What: Map the scalar output of the warp field to colors.

Why: Domain warping outputs a scalar field (0~1 range) that needs to be mapped to visually meaningful colors. The classic method uses a mix chain — interpolating between multiple preset colors using the warp value.

Code:

vec3 palette(float t) {
    vec3 col = vec3(0.2, 0.1, 0.4);                              // Deep purple base
    col = mix(col, vec3(0.3, 0.05, 0.05), t);                    // Dark red
    col = mix(col, vec3(0.9, 0.9, 0.9), t * t);                  // White at high values
    col = mix(col, vec3(0.0, 0.2, 0.4), smoothstep(0.6, 0.8, t));// Blue highlights
    return col * t * 2.0;                                         // Overall brightness modulation
}

Common Variants in Detail

Variant 1: Multi-Resolution Layered Warping

Difference from the basic version: Uses different octave counts for different warping layers — coarse layers use 4 octaves (fast, low frequency), detail layers use 6 octaves (fine, high frequency). Outputs vec2 for two-dimensional displacement rather than scalar offset. Intermediate variables participate in coloring, producing richer color gradients.

Key modified code:

// 4-octave fBM (coarse layer)
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;
}

// 6-octave fBM (fine layer)
float fbm6(vec2 p) {
    float f = 0.0;
    f += 0.500000 * noise(p); p = mtx * p * 2.02;
    f += 0.250000 * noise(p); p = mtx * p * 2.03;
    f += 0.125000 * noise(p); p = mtx * p * 2.01;
    f += 0.062500 * noise(p); p = mtx * p * 2.04;
    f += 0.031250 * noise(p); p = mtx * p * 2.01;
    f += 0.015625 * noise(p);
    return f / 0.96875;
}

// vec2 output version (independent displacement per axis)
vec2 fbm4_2(vec2 p) {
    return vec2(fbm4(p + vec2(1.0)), fbm4(p + vec2(6.2)));
}
vec2 fbm6_2(vec2 p) {
    return vec2(fbm6(p + vec2(9.2)), fbm6(p + vec2(5.7)));
}

// Layered warping chain
float func(vec2 q, out vec2 o, out vec2 n) {
    q += 0.05 * sin(vec2(0.11, 0.13) * iTime + length(q) * 4.0);
    o = 0.5 + 0.5 * fbm4_2(q);           // Level 1: coarse displacement
    o += 0.02 * sin(vec2(0.13, 0.11) * iTime * length(o));
    n = fbm6_2(4.0 * o);                  // Level 2: fine displacement
    vec2 p = q + 2.0 * n + 1.0;
    float f = 0.5 + 0.5 * fbm4(2.0 * p); // Level 3: final scalar field
    f = mix(f, f * f * f * 3.5, f * abs(n.x)); // Contrast enhancement
    return f;
}

// Coloring uses intermediate variables o, n
vec3 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(n, n));         // n magnitude drives white
col = mix(col, vec3(0.5, 0.2, 0.2), 0.5 * o.y * o.y);   // o.y drives brown
col = mix(col, vec3(0.0, 0.2, 0.4), 0.5 * smoothstep(1.2, 1.3, abs(n.y) + abs(n.x)));
col *= f * 2.0;

Variant 2: Turbulence / Ridge Warping (Electric Arc / Plasma Effect)

Difference from the basic version: Takes the absolute value of noise abs(noise - 0.5) inside fBM, producing sharp ridge textures instead of smooth waves. Dual-axis independent fBM displacement (separate x/y offsets) combined with reverse time drift creates turbulence.

Key modified code:

// Turbulence / ridged fBM
float fbm_ridged(vec2 p) {
    float z = 2.0;
    float rz = 0.0;
    for (float i = 1.0; i < 6.0; i++) {
        rz += abs((noise(p) - 0.5) * 2.0) / z; // abs() produces ridge folding
        z *= 2.0;
        p *= 2.0;
    }
    return rz;
}

// Dual-axis independent displacement
float dualfbm(vec2 p) {
    vec2 p2 = p * 0.7;
    // Opposite time drift in two directions creates turbulence
    vec2 basis = vec2(
        fbm_ridged(p2 - iTime * 0.24),  // x axis drifts left
        fbm_ridged(p2 + iTime * 0.26)   // y axis drifts right
    );
    basis = (basis - 0.5) * 0.2;         // Scale to small displacement
    p += basis;
    return fbm_ridged(p * makem2(iTime * 0.03)); // Slow overall rotation
}

// Electric arc coloring (division creates high-contrast light/dark)
vec3 col = vec3(0.2, 0.1, 0.4) / rz;

Variant 3: Domain Warping with Pseudo-3D Lighting

Difference from the basic version: Estimates screen-space normals from the warp field using finite differences, then applies directional lighting, giving the 2D warp field a 3D relief appearance. Combined with color inversion and square compression to produce a characteristic dark tone.

Key modified code:

// Screen-space normal estimation (finite differences)
float e = 2.0 / iResolution.y; // Sample spacing = 1 pixel
vec3 nor = normalize(vec3(
    pattern(p + vec2(e, 0.0)) - shade,  // df/dx
    2.0 * e,                             // Constant y (controls normal tilt)
    pattern(p + vec2(0.0, e)) - shade    // df/dy
));

// Dual-component lighting
vec3 lig = normalize(vec3(0.9, 0.2, -0.4));
float dif = clamp(0.3 + 0.7 * dot(nor, lig), 0.0, 1.0);
vec3 lin = vec3(0.70, 0.90, 0.95) * (nor.y * 0.5 + 0.5);  // Hemisphere ambient light
lin += vec3(0.15, 0.10, 0.05) * dif;                         // Warm diffuse

col *= 1.2 * lin;
col = 1.0 - col;       // Color inversion
col = 1.1 * col * col;  // Square compression, increases dark contrast

Variant 4: Flow Field Iterative Warping (Gas Giant Planet Effect)

Difference from the basic version: Instead of directly nesting fBM, computes the fBM gradient field and iteratively advances coordinates via Euler integration. Simulates fluid advection, producing vortex-like planetary atmospheric banding.

Key modified code:

#define ADVECT_ITERATIONS 5 // Adjustable: iteration count, more = more pronounced vortices

// Compute fBM gradient (finite differences)
vec2 field(vec2 p) {
    float t = 0.2 * iTime;
    p.x += t;
    float n = fbm(p, t);
    float e = 0.25;
    float nx = fbm(p + vec2(e, 0.0), t);
    float ny = fbm(p + vec2(0.0, e), t);
    return vec2(n - ny, nx - n) / e; // 90° rotated gradient = streamline direction
}

// Iterative flow field advection
vec3 distort(vec2 p) {
    for (float i = 0.0; i < float(ADVECT_ITERATIONS); i++) {
        p += field(p) / float(ADVECT_ITERATIONS);
    }
    return vec3(fbm(p, 0.0)); // Sample at the advected coordinates
}

Variant 5: 3D Volumetric Domain Warping (Explosion / Fireball Effect)

Difference from the basic version: Extends domain warping from 2D to 3D, using 3D fBM to displace a sphere's distance field, then rendering via sphere tracing or volumetric ray marching. Produces volcanic eruptions, solar surface, and other volumetric effects.

Key modified code:

#define NOISE_FREQ 4.0     // Adjustable: noise frequency
#define NOISE_AMP -0.5     // Adjustable: displacement amplitude (negative = inward bulging feel)

// 3D rotation matrix (for decorrelation)
mat3 m3 = mat3(0.00, 0.80, 0.60,
              -0.80, 0.36,-0.48,
              -0.60,-0.48, 0.64);

// 3D value noise
float noise3D(vec3 p) {
    vec3 fl = floor(p);
    vec3 fr = fract(p);
    fr = fr * fr * (3.0 - 2.0 * fr);
    float n = fl.x + fl.y * 157.0 + 113.0 * fl.z;
    return mix(mix(mix(hash(n+0.0),   hash(n+1.0),   fr.x),
                   mix(hash(n+157.0), hash(n+158.0), fr.x), fr.y),
               mix(mix(hash(n+113.0), hash(n+114.0), fr.x),
                   mix(hash(n+270.0), hash(n+271.0), fr.x), fr.y), fr.z);
}

// 3D fBM
float fbm3D(vec3 p) {
    float f = 0.0;
    f += 0.5000 * noise3D(p); p = m3 * p * 2.02;
    f += 0.2500 * noise3D(p); p = m3 * p * 2.03;
    f += 0.1250 * noise3D(p); p = m3 * p * 2.01;
    f += 0.0625 * noise3D(p); p = m3 * p * 2.02;
    f += 0.03125 * abs(noise3D(p)); // Last layer uses abs for added detail
    return f / 0.9375;
}

// Sphere distance field + domain warping displacement
float distanceFunc(vec3 p, out float displace) {
    float d = length(p) - 0.5; // Sphere SDF
    displace = fbm3D(p * NOISE_FREQ + vec3(0, -1, 0) * iTime);
    d += displace * NOISE_AMP;  // fBM displaces the surface
    return d;
}

Performance Optimization Deep Dive

Bottleneck Analysis

The main performance bottleneck of domain warping is repeated noise sampling. Three warping levels times 6 octaves = 18 noise samples per pixel, plus finite differences for lighting (2 additional full warping computations), totaling up to 54 noise samples/pixel.

Optimization Techniques

  1. Reduce octave count: Using 4 octaves instead of 6 shows little visual difference but improves performance by ~33%

    // Use 4 octaves for coarse layers, only 6 octaves for fine layers
    
  2. Reduce warping depth: Two-level warping fbm(p + fbm(p)) already produces organic results, saving ~33% performance over three levels

  3. Use sin-product noise instead of value noise: sin(p.x)*sin(p.y) is completely branch-free with no memory access, suitable for mobile

    float noise(vec2 p) {
        return sin(p.x) * sin(p.y); // Minimal version, no hash needed
    }
    
  4. GPU built-in derivatives instead of finite differences: Saves 2 extra full warping computations

    // Use dFdx/dFdy instead of manual finite differences (slightly lower quality but 3x faster)
    vec3 nor = normalize(vec3(dFdx(shade) * iResolution.x, 6.0, dFdy(shade) * iResolution.y));
    
  5. Texture noise: Pre-bake noise textures and use texture() instead of procedural noise, converting computation to memory reads

    float noise(vec2 x) {
        return texture(iChannel0, x * 0.01).x;
    }
    
  6. LOD adaptation: Reduce octave count for distant pixels

    int octaves = int(mix(float(NUM_OCTAVES), 2.0, length(uv) / 5.0));
    
  7. Supersampling strategy: Only use 2x2 supersampling when anti-aliasing is needed (4x performance cost)

    #if HW_PERFORMANCE == 0
    #define AA 1
    #else
    #define AA 2
    #endif
    

Combination Suggestions with Complete Code Examples

Combining with Ray Marching

The scalar field generated by domain warping can serve directly as an SDF displacement function, deforming smooth geometry into organic forms. Used for flames, explosions, alien creatures, etc.

float sdf(vec3 p) {
    return length(p) - 1.0 + fbm3D(p * 4.0) * 0.3;
}

Combining with Polar Coordinate Transform

Perform domain warping in polar coordinate space to produce vortices, nebulae, spirals, and other effects.

vec2 polar = vec2(length(uv), atan(uv.y, uv.x));
float shade = pattern(polar);

Combining with Cosine Color Palette

The cosine palette a + b*cos(2*pi*(c*t+d)) is more flexible than a fixed mix chain. By adjusting four vec3 parameters, you can quickly switch color schemes.

vec3 palette(float t) {
    vec3 a = vec3(0.5); vec3 b = vec3(0.5);
    vec3 c = vec3(1.0); vec3 d = vec3(0.0, 0.33, 0.67);
    return a + b * cos(6.28318 * (c * t + d));
}

Combining with Post-Processing Effects

  • Bloom/Glow: Blur and overlay high-brightness areas to enhance glow effects
  • Tone Mapping: col = col / (1.0 + col) to compress HDR range
  • Chromatic Aberration: Sample the warp field at offset positions for R/G/B channels separately
float r = pattern(uv + vec2(0.003, 0.0));
float g = pattern(uv);
float b = pattern(uv - vec2(0.003, 0.0));

Combining with Particle Systems / Geometry

The domain warping scalar field can drive particle velocity fields, mesh vertex displacement, or UV animation deformation — not limited to pure fragment shader usage.