Skip to main content
GameDev.net gamedev.net

Foam with Flow Mapping

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

Use in your engine

Run the shader to adjust these controls.

What it demonstrates

Flow mapping can animate detailed foam without moving geometry or simulating fluid. This demo builds a procedural texture, scrolls two copies through staggered phases, and uses the continuous result in both a shoreline band and moving wave crests. The split makes it clear that flow supplies motion while masks decide where foam belongs.

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 Image4
1 uniform vec2 uFlow; // @param -1.0..1.0 = 0.65, 0.22 "Flow"
2 uniform float uSpeed; // @param 0.0..1.5 = 0.45 "Speed"
3 uniform float uShoreWidth; // @param 0.03..0.35 = 0.14 "Shore width"
4 uniform float uFoam; // @param 0.0..1.5 = 1.0 "Foam amount"
5
6 float hash21(vec2 p) {
7 p = fract(p * vec2(123.34, 456.21));
8 p += dot(p, p + 45.32);
9 return fract(p.x * p.y);
10 }
11
12 float valueNoise(vec2 p) {
13 vec2 cell = floor(p);
14 vec2 local = fract(p);
15 local = local * local * (3.0 - 2.0 * local);
16 float a = hash21(cell);
17 float b = hash21(cell + vec2(1.0, 0.0));
18 float c = hash21(cell + vec2(0.0, 1.0));
19 float d = hash21(cell + vec2(1.0));
20 return mix(mix(a, b, local.x), mix(c, d, local.x), local.y);
21 }
22
23 float foamTexture(vec2 p) {
24 float broad = valueNoise(p * 5.0);
25 float detail = valueNoise(p * 11.0 + broad * 2.0);
26 return smoothstep(0.47, 0.72, broad * 0.62 + detail * 0.38);
27 }
28
29 void mainImage(out vec4 fragColor, in vec2 fragCoord) {
30 vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
31 float flowLength = length(uFlow);
32 vec2 flow = vec2(0.0);
33 if (flowLength > 0.0001) flow = uFlow / flowLength * 0.45;
34 float phase = fract(iTime * uSpeed * 0.22);
35 float phaseB = fract(phase + 0.5);
36 float phaseBlend = abs(phase * 2.0 - 1.0);
37 float flowed = mix(foamTexture(uv - flow * phase), foamTexture(uv - flow * phaseB), phaseBlend);
38
39 float coast = uv.y + 0.28 + sin(uv.x * 3.1) * 0.07;
40 float shoreBand = 1.0 - smoothstep(0.0, uShoreWidth, abs(coast));
41 float shoreFoam = shoreBand * smoothstep(0.32, 0.67, flowed);
42
43 float waves = sin(dot(uv, vec2(8.0, 3.0)) - iTime * 1.2);
44 waves += 0.55 * sin(dot(uv, vec2(-5.0, 10.0)) + iTime * 0.8);
45 float crestBand = smoothstep(0.72, 1.25, waves);
46 float crestFoam = crestBand * smoothstep(0.46, 0.75, flowed) * smoothstep(0.55, -0.2, coast);
47 float foamMask = max(shoreFoam, crestFoam) * uFoam;
48
49 float depth = smoothstep(-0.12, 0.55, coast);
50 vec3 water = mix(vec3(0.08, 0.48, 0.55), vec3(0.015, 0.10, 0.24), depth);
51 water *= 0.9 + waves * 0.035;
52 vec3 color = mix(water, vec3(0.91, 0.98, 0.96), clamp(foamMask, 0.0, 1.0));
53 fragColor = vec4(color, 1.0);
54 }
55

Learn from this shader

How it works

A compact value-noise function supplies irregular cells at two scales. A guarded length check turns a zero Flow vector into no movement and safely normalizes every nonzero direction. Each copy travels the same fixed cycle distance, but its cycle begins half a period from the other. Speed changes only the phase rate, so apparent scrolling scales linearly. A triangular weight crossfades when coordinates wrap, hiding the reset that one scrolling sample would reveal. The shoreline is a narrow distance band around a curved coast. Crests accept the brightest parts of the flowed field, and their maximum becomes the final foam mask over a water gradient.

Try changing

Rotate Flow and watch texture detail travel independently from the crest directions. Raise Speed until the phase cycling becomes obvious, then lower it to a believable current. Shore width controls the breaking band, while Foam amount can reveal the separate masks or merge them into a stormier surface.

Using it in a game

Replace the procedural field with a repeating foam texture and provide a flow vector from vertex colors, a flow map, or gameplay data. Feed shoreline distance from depth intersection and crest strength from the water normal or displacement. Two samples cost more than one, but avoid visible UV snapping and remain practical for broad water surfaces.

Explore the techniques

Continue with curated explanations and progressively related examples.

LicenseMIT
Views0
Forks0

Discussion

Loading comments...