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
qis a two-component noise: it says how far to push each point.ris the same again, but looked up at the position already pushed byq.- The final
fis read at the position pushed byr, then coloured with a cosine palette.
Tuning it
- The multiplier on
qandr(3.0 here) is the strength of the warp. Small values give gentle waves; large values give tight swirls. - Add
timeto 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.3above. - 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
- Fractal noise (fBm)Write fbm() with 5 octaves (amp 0.5 → halved each time, frequency doubled each time) and display fbm(uv ×…
- Smoke simulation 1: a flow field from noisePlusWrite flow(q): the curl of a slowly scrolling noise. Show it as colors: red = sideways flow, green = upward…
- Smoke simulation 2: follow the smoke back to its sourcePlusTrace each pixel back along the flow to the chimney. Where it started inside the chimney it is smoke…
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.