Domain warping
Purpose
If you offset a noise's input coordinate with another noise, you get molten, curling, wavy, turbulent results. This trick — "noise warping noise" — is how almost all organic, stream-like, flowing textures are made.
Key insight
So far the coordinate layer has been simple: scale it, snap it, sine-offset it. Domain warping puts noise in the coordinate layer itself. Evaluate noise at the pixel's UV to get a small 2D offset, then add that offset to the UV and evaluate another noise at the warped coordinate.
The first noise decides where each point asks; the second noise decides what each point gets. Because the warp is smooth, neighboring pixels still get nearby-ish answers — but the warp bends contours into rivers, veins, and whorls. The technique is dirt cheap and transforms any noise from "clouds" to "lava," "smoke," or "marble" with one extra function call.
Break it
Start at turbulence = 0 and slowly raise to maximum. Watch the contours of the noise — the iso-lines where brightness is constant. At 0 they're rounded blobs; as turbulence rises they bend, stretch, then pinch and curl into the paisley/flame-tongue shapes that are the visual signature of warping. Teaches: domain warping operates on contours, not on brightness. You're bending the map of equal values; whatever lives along those contours (marble veins, flame edges) bends with them.
Direct Claude
uniform float u_turbulence;
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);
}
float perlin(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
vec2 u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);
float a = dot(hash2(i + vec2(0.0, 0.0)), f - vec2(0.0, 0.0));
float b = dot(hash2(i + vec2(1.0, 0.0)), f - vec2(1.0, 0.0));
float c = dot(hash2(i + vec2(0.0, 1.0)), f - vec2(0.0, 1.0));
float d = dot(hash2(i + vec2(1.0, 1.0)), f - vec2(1.0, 1.0));
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
}
float fbm(vec2 p) {
float total = 0.0;
float amp = 0.5;
float freq = 1.0;
for (int i = 0; i < 5; i++) {
total += amp * perlin(p * freq);
freq *= 2.0;
amp *= 0.5;
}
return total;
}
void main() {
vec2 uv = (gl_FragCoord.xy - 0.5 * u_resolution) / min(u_resolution.x, u_resolution.y);
vec2 p = uv * 2.5;
// COORDINATE LAYER — the offset itself is noise. Two noise calls
// (different seeds via +100) give a 2D offset vector.
vec2 offset = vec2(fbm(p + vec2(0.0, 0.0)),
fbm(p + vec2(100.0, 100.0)));
// SIGNAL LAYER — final FBM evaluated at the warped coordinate.
float n = fbm(p + offset * u_turbulence);
float v = 0.5 + 0.5 * n;
gl_FragColor = vec4(vec3(v), 1.0);
}