Whirlpool
Foam arms that spiral into a dark eye, drawn on a flat quad by turning the angle around the center more the closer it gets.
- 01GoalDistort
- 02TechniqueSwirl, pinch & bulge
- 03ExecutionWhirlpool
Parameters
Copy for your platform
- GLSLCopy
- GodotCopy
Node chain
Built step by step, in GLSL style, with its node names.
Built step by step, in Godot style, with its node names.
No Unreal Engine 5 version of this card yet, so the nodes show the GLSL names.
No Unity version of this card yet, so the nodes show the GLSL names.
No Blender version of this card yet, so the nodes show the GLSL names.
lengthLengthlengthlengthlength UVTime01 Distance from the center
atanATan2atanatanatan In02 Angle around it
*Multiply*** In03 Twist by distance
cosCoscoscoscos In04 Arms in the turned angle
smoothstepSmoothStepsmoothstepsmoothstepsmoothstep In05 Foam lines
mixMixmixmixmix In06 Water and foam
How it works
A whirlpool is a pattern drawn in polar coordinatesA point described by its distance from a center and its angle around it, instead of by x and y. Anything that turns or repeats around a center is easy to write with them. Polar coordinates A point described by its distance from a center and its angle around it, instead of by x and y. Anything that turns or repeats around a center is easy to write with them.r = length(p), a = atan(p.y, p.x)
1. Distance from the center. length(uv - 0.5) is 0 in the middle of the quad and 0.5 at the middle of each edge.
The preview shows it as a gray ramp growing outward from a black center.
2. Angle around it. atan(p.y, p.x) is the direction of the pixel seen from the center, from -π to π. The preview
shows it as is, clamped to 0..1: black below the center, where the angle is negative, then a ramp from the right edge
that turns white after one radian.
3. Twist by distance. The falloff 1.0 - smoothstep(0.0, reach, radius) is 1 at the center and 0 at the reach and
beyond. Squared and multiplied by the twist, it says how many radians each ring turns. Squaring concentrates the turn
near the eye, as in a real vortex, where water near the drain spins fastest. Outside the reach the turn is exactly 0,
so the calm water joins the spiral without a seam. The preview is a white disc fading to black at the reach: inside
about half the reach the turn is above one radian.
4. Arms in the turned angle. cos(turned * arms) repeats a whole number of times around the circle, so each
repeat is one arm. The turned angle is the original angle plus the twist plus time. Because the twist changes with
the radius, each arm’s ridge sits at a different angle on each ring, and the ridges form spirals. The arm count is
rounded with floor: with a fractional count, the arms wouldn’t meet themselves at the seam where atan jumps from π
to -π, and a cut would show.
5. Foam lines. smoothstep(0.55, 1.0, arms) keeps only the ridge of each arm, a thin bright line. Two more
smoothsteps fade the foam out where the falloff ends, so the calm water outside has none, and near the very center,
where the arms crowd into a point that would flicker.
6. Water and foam. A ramp from deep to surface water by distance darkens the eye, and the foam is mixed over it.
Why the spin is added outside the twist
Time is added to the angle by itself, not multiplied into the twist. That turns every ring by the same angle each second, so the whole pattern rotates rigidly. If the spin grew toward the center like the twist, the inner rings would gain on the outer ones every frame and the arms would wind tighter forever, until each arm was thinner than a pixel and the center turned to noise. A real whirlpool’s water does move faster near the eye; the picture holds still because the arms here are a pattern, not tracked particles of water.
Tuning and limits
About 50 math operations per pixel, no texture reads, one pass. atan is the expensive one, a short polynomial on
most GPUs; the rest is cheap arithmetic.
- Twist sets how many turns the arms make between the reach and the eye: about twist / 2π. Above about 12 the inner turns get thinner than the foam line and start to shimmer; widen the foam’s smoothstep or blur the eye.
- A negative twist or speed turns the vortex the other way.
- The center is a singularity. At the exact center the angle is undefined;
atan(0, 0)returns 0 on most GPUs but isn’t guaranteed to. The foam fades out before it gets there, which hides both that and the crowded arms. - Swirling a texture instead of drawing arms: turn the angle the same way, rebuild a coordinate with
0.5 + radius * vec2(cos(turned), sin(turned))and sample there. That is the twirl node of node editors.
Make it in GLSL
The copied shader is a complete WebGL2 fragment shader for a quad: it declares the inputs, one uniform per param and
u_time, and writes the sRGB-encoded color.
- Draw a square quad whose vertex shader passes the UV, 0..1 from the bottom-left, as
v_uv. - Compile the copied code as the fragment shader and link it with that vertex shader.
- Each frame, set
u_timeto the seconds since the start, then draw. Set thep_uniforms once, or when a slider changes; colors go in linear. - To place the vortex somewhere else, replace the
0.5ini.uv - 0.5with avec2uniform; to make it bigger or smaller, change the reach.
For a whirlpool over your own water, return the foam amount as alpha instead of mixing it over the water, and draw
with gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA).
Make it in Godot
The shader is a canvas_item shader that assumes HDR 2D is off (the project default); it decodes the colors and encodes the result itself.
The exported shader draws on any 2D node with a material. A square ColorRect is the simplest: it has no texture of
its own and its UV covers it from corner to corner.
- Add a
ColorRectto a 2D scene or a UI and give it a square size, such as 512 by 512. - In the Inspector, set Material to a new
ShaderMaterial, and its Shader to a newShader. - Paste the copied code into the shader. The arms start turning at once: Godot’s
TIMEdrivesi.time. - Tune the params under Shader Parameters: the same names as on this card, without the
p_prefix.
To build it as a VisualShader instead (a canvas_item VisualShader on the same material):
UVminus avec2constant of 0.5: the centered coordinate. Flip its y first with aOneMinusif the spiral must turn the same way as here, since Godot’s UV starts at the top-left.Lengthof it: the radius.VectorDecomposeandATan2with y first: the angle.SmoothStepof the radius from 0 to the reach, thenOneMinus, thenMultiplyby itself and by the twist: the turn.Addthe turn andTimetimes the speed to the angle,Multiplyby the arm count and take itsCos, remapped to 0..1: the arms.SmoothStepthe arms for the foam, then twoMixnodes: deep to surface water by the radius, then the foam over it, into Color.
No Unreal Engine 5 tutorial for this card yet.
No Unity tutorial for this card yet.
No Blender tutorial for this card yet.
Code
#version 300 es// Swirl, pinch & bulge → Whirlpool, web GLSL (GLSL ES 3.00). From Creative Center, MIT license.precision highp float;uniform float u_time;uniform vec2 u_resolution; // this pass's output size in pixelsuniform float p_twist; // Twist: 6.0 (-15.0 to 15.0)uniform float p_reach; // Reach: 0.45 (0.1 to 0.7)uniform float p_speed; // Spin speed: 1.5 (-6.0 to 6.0)uniform float p_armCount; // Arms: 5.0 (1.0 to 12.0)uniform vec3 p_deep; // Deep water: vec3(0.02, 0.1, 0.18), sRGB; upload it decoded to linearuniform vec3 p_shallow; // Surface water: vec3(0.1, 0.45, 0.6), sRGB; upload it decoded to linearuniform vec3 p_foam; // Foam: vec3(0.85, 0.95, 1.0), sRGB; upload it decoded to linearin vec2 v_uv;in vec3 v_position;in vec3 v_normal;in vec3 v_view;out vec4 fragColor;struct Inputs { vec2 uv; vec3 position; vec3 normal; vec3 view; float time; vec2 fragCoord; vec2 resolution;};vec3 u_toSrgb(vec3 c) { return mix(c * 12.92, 1.055 * pow(c, vec3(1.0 / 2.4)) - 0.055, step(0.0031308, c));}vec4 effect(Inputs i) { vec2 p = i.uv - 0.5; float radius = length(p); float angle = atan(p.y, p.x); // 1 at the center, 0 at p_reach and beyond: the water outside the whirlpool isn't turned float falloff = 1.0 - smoothstep(0.0, p_reach, radius); // Squared, so the twist builds up sharply toward the eye, as in a real vortex float swirl = falloff * falloff * p_twist; // The spin turns every radius by the same angle: a spin that grew toward the center would wind the arms tighter // every second, until they were thinner than a pixel float turned = angle + swirl + i.time * p_speed; // A whole number of arms, so cos() meets itself where atan() jumps from pi to -pi float arms = 0.5 + 0.5 * cos(turned * floor(p_armCount)); float froth = smoothstep(0.55, 1.0, arms) * smoothstep(0.0, 0.3, falloff) * smoothstep(0.03, 0.15, radius); float depth = smoothstep(0.02, p_reach * 0.7, radius); vec3 water = mix(p_deep, p_shallow, depth); vec3 color = mix(water, p_foam, froth * 0.8); return vec4(color, 1.0);}void main() { Inputs i; i.uv = v_uv; i.position = v_position; // Interpolation shortens the vectors between vertices i.normal = normalize(v_normal); i.view = normalize(v_view); i.time = u_time; i.fragCoord = gl_FragCoord.xy; i.resolution = u_resolution; vec4 frag_out = effect(i); // The output is sRGB: clamped and encoded once, here, with no tone mapping; opaque, so alpha is 1 fragColor = vec4(u_toSrgb(clamp(frag_out.rgb, 0.0, 1.0)), 1.0);}// Swirl, pinch & bulge → Whirlpool, Godot 4. From Creative Center, MIT license.shader_type canvas_item;// Unshaded shows the effect's color as is, like the site's preview; scene lights don't touch itrender_mode unshaded;uniform float twist : hint_range(-15.0, 15.0, 0.1) = 6.0;uniform float reach : hint_range(0.1, 0.7, 0.01) = 0.45;uniform float speed : hint_range(-6.0, 6.0, 0.1) = 1.5;uniform float armCount : hint_range(1.0, 12.0, 1.0) = 5.0;uniform vec3 deep : source_color = vec3(0.02, 0.1, 0.18);uniform vec3 shallow : source_color = vec3(0.1, 0.45, 0.6);uniform vec3 foam : source_color = vec3(0.85, 0.95, 1.0);struct Inputs { vec2 uv; vec3 position; vec3 normal; vec3 view; float time; vec2 fragCoord; vec2 resolution;};// Godot's 2D renderer works in sRGB (HDR 2D off, the default), so the frame decodes color params and sRGB// textures to linear itself and encodes the result. With HDR 2D on, make these return c unchanged.vec3 u_toLinear(vec3 c) { return mix(c / 12.92, pow((c + 0.055) / 1.055, vec3(2.4)), step(0.04045, c));}vec3 u_toSrgb(vec3 c) { return mix(c * 12.92, 1.055 * pow(c, vec3(1.0 / 2.4)) - 0.055, step(0.0031308, c));}vec4 effect(Inputs i) { vec2 p = i.uv - 0.5; float radius = length(p); float angle = atan(p.y, p.x); // 1 at the center, 0 at reach and beyond: the water outside the whirlpool isn't turned float falloff = 1.0 - smoothstep(0.0, reach, radius); // Squared, so the twist builds up sharply toward the eye, as in a real vortex float swirl = falloff * falloff * twist; // The spin turns every radius by the same angle: a spin that grew toward the center would wind the arms tighter // every second, until they were thinner than a pixel float turned = angle + swirl + i.time * speed; // A whole number of arms, so cos() meets itself where atan() jumps from pi to -pi float arms = 0.5 + 0.5 * cos(turned * floor(armCount)); float froth = smoothstep(0.55, 1.0, arms) * smoothstep(0.0, 0.3, falloff) * smoothstep(0.03, 0.15, radius); float depth = smoothstep(0.02, reach * 0.7, radius); vec3 water = mix(u_toLinear(deep), u_toLinear(shallow), depth); vec3 color = mix(water, u_toLinear(foam), froth * 0.8); return vec4(color, 1.0);}void fragment() { Inputs i; // UV starts at the top-left; the effect expects GLSL's bottom-left i.uv = vec2(UV.x, 1.0 - UV.y); // A flat quad facing the viewer, the same as the site's 2D preview i.position = vec3(i.uv * 2.0 - 1.0, 0.0); i.normal = vec3(0.0, 0.0, 1.0); i.view = vec3(0.0, 0.0, 1.0); i.time = TIME; // FRAGCOORD's origin is the top-left too i.resolution = 1.0 / SCREEN_PIXEL_SIZE; i.fragCoord = vec2(FRAGCOORD.x, i.resolution.y - FRAGCOORD.y); vec4 frag_out = effect(i); COLOR = vec4(u_toSrgb(clamp(frag_out.rgb, 0.0, 1.0)), 1.0);}No Unreal Engine 5 code for this card yet.
No Unity code for this card yet.
No Blender code for this card yet.