Domain warping
Offset a noise's coordinates by another noise, for flowing, folded shapes like smoke, marble and liquid.
How it works
Moving a pattern means changing the coordinate you feed it: pattern(p + offset) slides it, as the 2D transforms
fundamental shows. Domain warping uses an offset that is different at every point, and takes that offset from noise
itself. The pattern gets pushed one way here and another way there, and straight features bend into swirls and folds
that look like smoke, ink in water, marble or flowing lava.
The “domain” is the input space of a function. Warping it means distorting the space before the function reads it, rather than changing the function.
One warp
Take an fBm, a sum of noise octavesOne layer of noise in a fractal sum. Each octave usually has twice the frequency and half the amplitude of the one before, adding finer and fainter detail. Octave One layer of noise in a fractal sum. Each octave usually has twice the frequency and half the amplitude of the one before, adding finer and fainter detail.fbm(p) = Σ gainᵏ · noise(lacunarityᵏ · p)
vec2 q = vec2(fbm(p), fbm(p + vec2(3.7, 8.1))); // a smooth random vector field
float n = fbm(p + 4.0 * q); // the fBm, read through the warp
The offset vec2(3.7, 8.1) only makes the two components independent; any offset larger than a few cells works. The
4.0 is the warp’s strength: at 0 you get the plain fBm back, around 1 the shapes wobble, and from 3 to 6 they fold
over themselves. Because q is smooth, neighboring pixels get similar offsets, so the result stays smooth too: the
space is stretched and folded, not torn.
Warping the warp
The idea nests. Warp the vector field itself with a second level, then read the final fBm through that:
vec2 q = vec2(fbm(p), fbm(p + vec2(3.7, 8.1)));
vec2 r = vec2(fbm(p + 4.0 * q + vec2(6.4, 2.9)), fbm(p + 4.0 * q + vec2(1.6, 7.5)));
float n = fbm(p + 4.0 * r);
Each level makes the swirls more intricate, with thin filaments wrapping around each other. Two levels is usually where it stops paying off. The cost adds up quickly: one level is three fBm calls, two levels are five, and with five octaves each, that is 25 noise evaluations per pixel. On a phone at full screen that matters; render it at lower resolution, use fewer octaves for the inner levels (they only need to be smooth, not detailed), or bake it into a texture if it doesn’t need to move.
Color from the warp
The intermediate vectors are useful beyond the final value. length(q) says how strongly each point was pushed, and
r.x and r.y are smooth fields that follow the same folds. Mixing colors by them, as well as by n, gives the
streaked, multi-toned look of marble or nebulae:
float tone = clamp(n * 1.5 + 0.5, 0.0, 1.0);
vec3 color = mix(vec3(0.10, 0.12, 0.30), vec3(0.85, 0.45, 0.20), tone);
color = mix(color, vec3(0.95, 0.90, 0.80), clamp(length(q), 0.0, 1.0) * 0.5);
color = mix(color, vec3(0.20, 0.50, 0.55), clamp(abs(r.x) * 2.0, 0.0, 1.0) * 0.4);
Animating
Add time to one of the inner offsets, such as fbm(p + vec2(3.7, 8.1) + 0.1 * time). The warp field drifts, and the
final pattern churns and flows in place instead of sliding across the screen, the look of smoke and clouds. Adding time
to p directly slides everything at once and loses that.
Warping other things
The warped coordinate works with any pattern, not only noise. Warp the UV before reading a texture and the image ripples like a reflection on water. Warp the input of stripes or rings and they bend into marble veins or wood grain. Warp the point passed to a distance field and a hard shape gets an organic edge. The rule is always the same: compute an offset field, add it to the coordinate, and keep its strength small enough that the result still reads as the original pattern.