Skip to main content
GameDev.net gamedev.net

Domain-Warped Noise

by GameDev.net · GLSL ES 3.00 (WebGL2) · 25 Aug 2026

Use in your engine

Run the shader to adjust these controls.

What it demonstrates

Layered noise is isotropic: statistically the same in every direction, and after a while every fbm surface looks like the same grey cloud. Domain warping breaks that by distorting the coordinate before sampling rather than post processing the result. The field gains flow, swirls, and stretched filaments while the noise function itself is untouched.

Source Revision 1

Author notes are linked to specific lines.

Shader inputs

void mainImage(out vec4 fragColor, in vec2 fragCoord)

Called once per pixel. Write the colour to fragColor.

iResolution vec3
Viewport size in pixels (z is the pixel aspect ratio).
iTime float
Seconds since the shader started.
iTimeDelta float
Seconds since the previous frame.
iFrameRate float
Frames per second, smoothed.
iFrame int
Frames rendered since the start.
iMouse vec4
Mouse position: xy while held, zw of the last click.
iDate vec4
Year, month, day, and seconds within the day.
iChannel0 sampler2D
Texture bound to channel 0.
iChannel1 sampler2D
Texture bound to channel 1.
iChannel2 sampler2D
Texture bound to channel 2.
iChannel3 sampler2D
Texture bound to channel 3.
iChannelResolution vec3[4]
Pixel size of each bound channel texture.
iChannelTime float[4]
Playback time of each channel, in seconds.
iSampleRate float
Audio sample rate, always 44100.
Main Image6
1 uniform float uWarpStrength; // @param 0.0..1.5 = 0.7 "Warp strength"
2 uniform int uOctaves; // @param 1..8 = 5 "Octaves"
3 uniform float uScale; // @param 1.0..8.0 = 3.0 "Base scale"
4
5 float hash21(vec2 cell) {
6 return fract(sin(dot(cell, vec2(12.9898, 78.233))) * 43758.5453);
7 }
8
9 float valueNoise(vec2 p) {
10 vec2 cell = floor(p);
11 vec2 local = fract(p);
12 vec2 weight = local * local * (3.0 - 2.0 * local);
13 float bottom = mix(hash21(cell), hash21(cell + vec2(1.0, 0.0)), weight.x);
14 float top = mix(hash21(cell + vec2(0.0, 1.0)), hash21(cell + vec2(1.0, 1.0)), weight.x);
15 return mix(bottom, top, weight.y);
16 }
17
18 float fbm(vec2 p) {
19 float sum = 0.0;
20 float amplitude = 1.0;
21 float normalization = 0.0;
22 for (int i = 0; i < 8; i++) {
23 if (i >= uOctaves) break;
24 sum += amplitude * valueNoise(p);
25 normalization += amplitude;
26 amplitude *= 0.5;
27 p *= 2.0;
28 }
29 return sum / max(normalization, 0.0001);
30 }
31
32 void mainImage(out vec4 fragColor, in vec2 fragCoord) {
33 vec2 uv = fragCoord / iResolution.xy;
34 uv.x *= iResolution.x / iResolution.y;
35 vec2 p = uv * uScale + vec2(iTime * 0.03, 0.0);
36 vec2 warp = vec2(fbm(p + vec2(1.7, 9.2)), fbm(p + vec2(8.3, 2.8)));
37 float field = fbm(p + uWarpStrength * warp);
38 vec3 deep = vec3(0.05, 0.10, 0.20);
39 vec3 mid = vec3(0.20, 0.48, 0.60);
40 vec3 bright = vec3(0.95, 0.86, 0.62);
41 vec3 color = mix(deep, mid, smoothstep(0.25, 0.55, field));
42 color = mix(color, bright, smoothstep(0.60, 0.85, field));
43 fragColor = vec4(color, 1.0);
44 }
45

Learn from this shader

How it works

The shader evaluates fbm twice at offset coordinates to build a two component vector, then adds a scaled version of it to the coordinate used for the final fbm call. Because the offset is itself smooth, nearby fragments are displaced by similar amounts and the field stays continuous instead of tearing. The constant offsets matter: without them both components would be identical and the warp could only push along one diagonal. Warp strength is measured in noise units, so a value near one samples up to roughly one lattice cell away.

Try changing

Hold Warp strength at zero to see the plain fbm underneath, then raise it slowly. Small values give gentle flow; large values fold the field into itself and produce the marbled look. Raise Octaves and note that warping preserves fine detail rather than smearing it. In a fork, feed the warped result back through another warp for the classic two level version, or drive the offsets from time for a swirling flow.

Using it in a game

This is the cheapest way to make procedural smoke, lava, magic effects, nebulae, and stylised water look directed rather than random. Cost is the honest catch: two extra fbm evaluations means roughly three times the work of the field alone, so bake to a flowmap or a texture when the effect covers much of the screen.

Explore the techniques

Continue with curated explanations and progressively related examples.

LicenseMIT
Views1
Forks0

Discussion

Loading comments...