Skip to content
Fundamental · NoiseIntermediate

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 octaves, and evaluate it twice, at two unrelated places, to get a random 2D vector at every point. Add that vector, scaled, to the coordinate of a third fBm:

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.

Further reading