Shader concept

Domain warping: swirling, organic patterns in a shader

Domain warping means bending the coordinates before you look something up: you feed noise into the position that goes into another noise. It turns plain noise into swirling, marble-like, flowing patterns.

Running live in your browser. The code below is exactly what draws it.

The idea

Normally you compute fbm(p). With domain warping you compute fbm(p + offset), where offset is itself made from noise. Every point is pushed to a different place before it is looked up, so straight features turn into curves and swirls. Doing it twice (warping the warp) gives the deep, folded look of marble or ink in water.

How the picture above is made

  1. q is a two-component noise: it says how far to push each point.
  2. r is the same again, but looked up at the position already pushed by q.
  3. The final f is read at the position pushed by r, then coloured with a cosine palette.

Tuning it

  • The multiplier on q and r (3.0 here) is the strength of the warp. Small values give gentle waves; large values give tight swirls.
  • Add time to the inputs of the inner noise to make it flow.
  • Use fewer octaves while you experiment: each level of warping multiplies the cost.

Common mistakes

  • Using the same offset for both components of the warp, which only slides the pattern diagonally. Give each component its own constant, like the 5.2, 1.3 above.
  • Warping too hard, which makes mush. Start with strength 1 and increase.

The code, in every shader language

The same example for each language family. Open one to see it.

HLSL Unity, Unreal
float hash21(float2 p)
{
    float3 p3 = frac(float3(p.x, p.y, p.x) * 0.1031);
    p3 += dot(p3, p3.yzx + 33.33);
    return frac((p3.x + p3.y) * p3.z);
}
float noise2(float2 p)
{
    float2 i = floor(p);
    float2 f = frac(p);
    f = f * f * (3.0 - 2.0 * f);
    float a = hash21(i);
    float b = hash21(i + float2(1.0, 0.0));
    float c = hash21(i + float2(0.0, 1.0));
    float d = hash21(i + float2(1.0, 1.0));
    return lerp(lerp(a, b, f.x), lerp(c, d, f.x), f.y);
}
float fbm2(float2 p)
{
    float v = 0.0;
    float amp = 0.5;
    for (int i = 0; i < 5; i++)
    {
        v += amp * noise2(p);
        p = p * 2.03 + float2(5.1, 1.7);
        amp *= 0.5;
    }
    return v;
}
float3 palette(float t)
{
    return 0.5 + 0.5 * cos(6.28318 * (t + float3(0.0, 0.1, 0.2)));
}

float4 frag(float2 uv)
{
    float2 p = (uv - 0.5) * float2(aspect, 1.0) * 2.5;
    float2 q = float2(fbm2(p + time * 0.1), fbm2(p + float2(5.2, 1.3)));
    float2 r = float2(fbm2(p + 3.0 * q + float2(1.7, 9.2) + time * 0.12),
                      fbm2(p + 3.0 * q + float2(8.3, 2.8)));
    float f = fbm2(p + 3.0 * r);
    float3 col = palette(f * 1.2 + 0.1);
    col *= 0.35 + 0.9 * f;
    return float4(col, 1.0);
}
GLSL Shadertoy, three.js, OpenGL
float hash21(vec2 p)
{
    vec3 p3 = fract(vec3(p.x, p.y, p.x) * 0.1031);
    p3 += dot(p3, p3.yzx + 33.33);
    return fract((p3.x + p3.y) * p3.z);
}
float noise2(vec2 p)
{
    vec2 i = floor(p);
    vec2 f = fract(p);
    f = f * f * (3.0 - 2.0 * f);
    float a = hash21(i);
    float b = hash21(i + vec2(1.0, 0.0));
    float c = hash21(i + vec2(0.0, 1.0));
    float d = hash21(i + vec2(1.0, 1.0));
    return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
}
float fbm2(vec2 p)
{
    float v = 0.0;
    float amp = 0.5;
    for (int i = 0; i < 5; i++)
    {
        v += amp * noise2(p);
        p = p * 2.03 + vec2(5.1, 1.7);
        amp *= 0.5;
    }
    return v;
}
vec3 palette(float t)
{
    return 0.5 + 0.5 * cos(6.28318 * (t + vec3(0.0, 0.1, 0.2)));
}

vec4 frag(vec2 uv)
{
    vec2 p = (uv - 0.5) * vec2(aspect, 1.0) * 2.5;
    vec2 q = vec2(fbm2(p + time * 0.1), fbm2(p + vec2(5.2, 1.3)));
    vec2 r = vec2(fbm2(p + 3.0 * q + vec2(1.7, 9.2) + time * 0.12),
                      fbm2(p + 3.0 * q + vec2(8.3, 2.8)));
    float f = fbm2(p + 3.0 * r);
    vec3 col = palette(f * 1.2 + 0.1);
    col *= 0.35 + 0.9 * f;
    return vec4(col, 1.0);
}
WGSL WebGPU, Bevy
fn hash21(p: vec2f) -> f32
{
    var p3 = fract(vec3f(p.x, p.y, p.x) * 0.1031);
    p3 += dot(p3, p3.yzx + 33.33);
    return fract((p3.x + p3.y) * p3.z);
}
fn noise2(p: vec2f) -> f32
{
    let i = floor(p);
    var f = fract(p);
    f = f * f * (3.0 - 2.0 * f);
    let a = hash21(i);
    let b = hash21(i + vec2f(1.0, 0.0));
    let c = hash21(i + vec2f(0.0, 1.0));
    let d = hash21(i + vec2f(1.0, 1.0));
    return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
}
fn fbm2(pIn: vec2f) -> f32
{
    var p = pIn;
    var v = 0.0;
    var amp = 0.5;
    for (var i = 0; i < 5; i++)
    {
        v += amp * noise2(p);
        p = p * 2.03 + vec2f(5.1, 1.7);
        amp *= 0.5;
    }
    return v;
}
fn palette(t: f32) -> vec3f
{
    return 0.5 + 0.5 * cos(6.28318 * (t + vec3f(0.0, 0.1, 0.2)));
}

fn frag(uv: vec2f) -> vec4f
{
    let p = (uv - 0.5) * vec2f(aspect, 1.0) * 2.5;
    let q = vec2f(fbm2(p + time * 0.1), fbm2(p + vec2f(5.2, 1.3)));
    let r = vec2f(fbm2(p + 3.0 * q + vec2f(1.7, 9.2) + time * 0.12), fbm2(p + 3.0 * q + vec2f(8.3, 2.8)));
    let f = fbm2(p + 3.0 * r);
    var col = palette(f * 1.2 + 0.1);
    col *= 0.35 + 0.9 * f;
    return vec4f(col, 1.0);
}
Godot Godot 4 shading language
shader_type canvas_item;

float hash21(vec2 p)
{
    vec3 p3 = fract(vec3(p.x, p.y, p.x) * 0.1031);
    p3 += dot(p3, p3.yzx + 33.33);
    return fract((p3.x + p3.y) * p3.z);
}
float noise2(vec2 p)
{
    vec2 i = floor(p);
    vec2 f = fract(p);
    f = f * f * (3.0 - 2.0 * f);
    float a = hash21(i);
    float b = hash21(i + vec2(1.0, 0.0));
    float c = hash21(i + vec2(0.0, 1.0));
    float d = hash21(i + vec2(1.0, 1.0));
    return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
}
float fbm2(vec2 p)
{
    float v = 0.0;
    float amp = 0.5;
    for (int i = 0; i < 5; i++)
    {
        v += amp * noise2(p);
        p = p * 2.03 + vec2(5.1, 1.7);
        amp *= 0.5;
    }
    return v;
}
vec3 palette(float t)
{
    return 0.5 + 0.5 * cos(6.28318 * (t + vec3(0.0, 0.1, 0.2)));
}

vec4 frag(vec2 uv)
{
    vec2 p = (uv - 0.5) * vec2(aspect, 1.0) * 2.5;
    vec2 q = vec2(fbm2(p + TIME * 0.1), fbm2(p + vec2(5.2, 1.3)));
    vec2 r = vec2(fbm2(p + 3.0 * q + vec2(1.7, 9.2) + TIME * 0.12),
                      fbm2(p + 3.0 * q + vec2(8.3, 2.8)));
    float f = fbm2(p + 3.0 * r);
    vec3 col = palette(f * 1.2 + 0.1);
    col *= 0.35 + 0.9 * f;
    return vec4(col, 1.0);
}

void fragment()
{
    // Godot's UV starts at the top-left, the lessons use bottom-left
    COLOR = frag(vec2(UV.x, 1.0 - UV.y));
}
Metal iOS, macOS
float hash21(float2 p)
{
    float3 p3 = fract(float3(p.x, p.y, p.x) * 0.1031);
    p3 += dot(p3, p3.yzx + 33.33);
    return fract((p3.x + p3.y) * p3.z);
}
float noise2(float2 p)
{
    float2 i = floor(p);
    float2 f = fract(p);
    f = f * f * (3.0 - 2.0 * f);
    float a = hash21(i);
    float b = hash21(i + float2(1.0, 0.0));
    float c = hash21(i + float2(0.0, 1.0));
    float d = hash21(i + float2(1.0, 1.0));
    return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
}
float fbm2(float2 p)
{
    float v = 0.0;
    float amp = 0.5;
    for (int i = 0; i < 5; i++)
    {
        v += amp * noise2(p);
        p = p * 2.03 + float2(5.1, 1.7);
        amp *= 0.5;
    }
    return v;
}
float3 palette(float t)
{
    return 0.5 + 0.5 * cos(6.28318 * (t + float3(0.0, 0.1, 0.2)));
}

float4 frag(float2 uv)
{
    float2 p = (uv - 0.5) * float2(aspect, 1.0) * 2.5;
    float2 q = float2(fbm2(p + time * 0.1), fbm2(p + float2(5.2, 1.3)));
    float2 r = float2(fbm2(p + 3.0 * q + float2(1.7, 9.2) + time * 0.12),
                      fbm2(p + 3.0 * q + float2(8.3, 2.8)));
    float f = fbm2(p + 3.0 * r);
    float3 col = palette(f * 1.2 + 0.1);
    col *= 0.35 + 0.9 * f;
    return float4(col, 1.0);
}

Practise it in the lessons

Start with "Fractal noise (fBm)"

Questions

Who came up with domain warping?

It is an old technique, popularised for shaders by Inigo Quilez, who published a well-known article on warping fBm.

Is domain warping expensive?

Each level multiplies the number of noise lookups. The example uses three levels of five-octave fBm, about 25 noise calls per pixel with two components each. It is fine for a background; reduce octaves for mobile.

Related concepts