Waving flag
A striped flag that ripples away from its pole, drawn on a flat quad by bending the UV with a travelling sine.
- 01GoalDistort
- 02TechniqueUV warping
- 03ExecutionWaving flag
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.
sinSinsinsinsin UVTimeTextureScreen01 Travelling wave
+Add+++ In02 Bend the UV
stepStepstepstepstep In03 Stripes in the bent UV
cosCoscoscoscos In04 Shade the folds
mixMixmixmixmix In05 Color the cloth
mixMixmixmixmix In06 Over the sky
How it works
The quad never moves. Every pixel asks “which point of the flag’s design belongs here?”, and the shader answers with a
UVA 2D position on a surface or the screen, usually from 0 to 1 on both axes. Shaders draw shapes and patterns as functions of it. UV coordinates A 2D position on a surface or the screen, usually from 0 to 1 on both axes. Shaders draw shapes and patterns as functions of it.uv = gl_FragCoord.xy / resolution
1. A travelling wave. sin(fromPole * frequency - time * speed) is a sine across the width of the flag. Subtracting
time moves its crests to the right, away from the pole, at speed / frequency UV units per second. The preview shows
the positive half of the wave; the negative half is clamped to black.
2. Bend the UV. The wave becomes a vertical offset, wave * amplitude * fromPole, added to the UV. The factor
fromPole is 0 at the pole and grows toward the free end, so the edge tied to the pole stays straight and the far edge
swings the most, as real cloth does. The preview shows the warped UV as red and green: its lines of equal green are
wavy now.
3. Stripes in the bent UV. The flag is a rectangle of the warped space, remapped to 0..1 as cloth. Stripes are
step(0.5, fract(cloth.y * bands * 0.5)): fract repeats the coordinate, step splits each repeat into two colors.
Because they are drawn from cloth, not from the original UV, they follow the wave, and so do the top and bottom
edges of the flag.
4. Shade the folds. Bending alone looks like a ribbon of paper sliding up and down. Cloth catches the light, and how
much depends on its slope. The offset is sin(phase) * amplitude * fromPole, and its slope is about
cos(phase) * amplitude * frequency * fromPole. Clamped to -1..1, that slope makes 1.0 + 0.35 * slope brighten the
parts of the wave tilted one way and darken the others. That brightness is a fake light with no normal or light vector,
and it costs one cosine, a few multiplies and a clamp. It fades out near the pole, where the cloth barely moves, and
vanishes when the amplitude is 0 and the flag hangs flat.
5 and 6. Color. mix picks the field or the stripe color, multiplied by the fold shading, and the result is
placed over the sky wherever the warped point falls inside the flag rectangle. The pole is drawn last, in the unwarped
UV, so it stays rigid.
Tuning and limits
About 60 math operations per pixel, most of them in the soft edges: each band call is two smoothsteps. No texture
reads, one pass. A texture flag costs about the same, with one texture() call at warped in place of the stripes.
- Amplitude times frequency sets the slope. The offset only depends on x, so the picture can’t fold over itself,
but where
amplitude * frequency * fromPolepasses about 2 the stripes tilt so steeply that they turn into thin slivers and shimmer. The defaults reach about 1.3 at the free end. - The silhouette is fake. The edge waves because each pixel tests whether its warped point is inside the rectangle; the quad itself is still a rectangle. Seen from the side, it is a flat card. Then use vertex displacement.
- Add a second, smaller wave at another frequency for a less mechanical look; a little noise in the phase makes the flag gusty.
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 quad (two triangles, or one large triangle) 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 whenever a slider changes; colors go in linear. - To use your own flag image instead of stripes, add a
sampler2Duniform and replacefabric’smixwithtexture(image, cloth).rgb. Clamp its wrap mode, so the picture doesn’t repeat at the edge.
The shader has no discard and writes alpha 1, so the sky color fills the quad. For a flag over your own background,
return inside as alpha 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 that has a material. A 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 size it. - In the Inspector, set Material to a new
ShaderMaterial, and its Shader to a newShader. - Paste the copied code into the shader. The flag starts moving at once: Godot’s
TIMEdrivesi.time. - Tune the params under Shader Parameters. They are 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), the steps map one to one:
UV, with aVectorDecomposeto get x and y; flip y with aOneMinus.Timetimes the speed, subtracted fromfromPoletimes the frequency, intoSin: the wave.Multiplythe wave by the amplitude andfromPole, andAddit to y: the warped UV.FractandStepon the remapped y: the stripes.Cosof the same phase,Multiplyby the amplitude, the frequency andfromPole, thenClampto -1..1: the fold shading.Mixthe two colors by the stripes, thenMixover the sky by the inside mask, 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// UV warping → Waving flag, 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_amplitude; // Amplitude: 0.12 (0.0 to 0.3)uniform float p_frequency; // Frequency: 14.0 (2.0 to 40.0)uniform float p_speed; // Speed: 4.0 (0.0 to 12.0)uniform float p_bands; // Stripes: 5.0 (1.0 to 13.0)uniform vec3 p_field; // Field color: vec3(0.12, 0.27, 0.55), sRGB; upload it decoded to linearuniform vec3 p_stripe; // Stripe color: vec3(0.95, 0.78, 0.25), sRGB; upload it decoded to linearuniform vec3 p_backdrop; // Sky: vec3(0.62, 0.78, 0.9), sRGB; upload it decoded to linearuniform vec3 p_pole; // Pole: vec3(0.28, 0.24, 0.2), 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));}// A soft-edged 0..1 window, so the flag's outline doesn't stair-step as it moves.float band(float x, float from, float to) { return smoothstep(from - 0.004, from + 0.004, x) * (1.0 - smoothstep(to - 0.004, to + 0.004, x));}vec4 effect(Inputs i) { // The cloth is fixed at the pole and free at the far end, so the wave grows with distance from the pole float fromPole = max(i.uv.x - 0.1, 0.0); float phase = fromPole * p_frequency - i.time * p_speed; float wave = sin(phase); vec2 warped = i.uv + vec2(0.0, wave * p_amplitude * fromPole); vec2 cloth = (warped - vec2(0.1, 0.2)) / vec2(0.8, 0.6); float inside = band(cloth.x, 0.0, 1.0) * band(cloth.y, 0.0, 1.0); float stripes = step(0.5, fract(cloth.y * floor(p_bands) * 0.5)); // The offset's slope: cloth tilted toward the light is brighter, cloth tilted away darker float fold = 1.0 + 0.35 * clamp(cos(phase) * p_amplitude * p_frequency * fromPole, -1.0, 1.0); vec3 fabric = mix(p_field, p_stripe, stripes) * fold; float staff = band(i.uv.x, 0.085, 0.1) * band(i.uv.y, 0.05, 0.84); vec3 color = mix(mix(p_backdrop, fabric, inside), p_pole, staff); 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);}// UV warping → Waving flag, 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 amplitude : hint_range(0.0, 0.3, 0.005) = 0.12;uniform float frequency : hint_range(2.0, 40.0, 0.5) = 14.0;uniform float speed : hint_range(0.0, 12.0, 0.1) = 4.0;uniform float bands : hint_range(1.0, 13.0, 1.0) = 5.0;uniform vec3 field : source_color = vec3(0.12, 0.27, 0.55);uniform vec3 stripe : source_color = vec3(0.95, 0.78, 0.25);uniform vec3 backdrop : source_color = vec3(0.62, 0.78, 0.9);uniform vec3 pole : source_color = vec3(0.28, 0.24, 0.2);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));}// A soft-edged 0..1 window, so the flag's outline doesn't stair-step as it moves.float band(float x, float from, float to) { return smoothstep(from - 0.004, from + 0.004, x) * (1.0 - smoothstep(to - 0.004, to + 0.004, x));}vec4 effect(Inputs i) { // The cloth is fixed at the pole and free at the far end, so the wave grows with distance from the pole float fromPole = max(i.uv.x - 0.1, 0.0); float phase = fromPole * frequency - i.time * speed; float wave = sin(phase); vec2 warped = i.uv + vec2(0.0, wave * amplitude * fromPole); vec2 cloth = (warped - vec2(0.1, 0.2)) / vec2(0.8, 0.6); float inside = band(cloth.x, 0.0, 1.0) * band(cloth.y, 0.0, 1.0); float stripes = step(0.5, fract(cloth.y * floor(bands) * 0.5)); // The offset's slope: cloth tilted toward the light is brighter, cloth tilted away darker float fold = 1.0 + 0.35 * clamp(cos(phase) * amplitude * frequency * fromPole, -1.0, 1.0); vec3 fabric = mix(u_toLinear(field), u_toLinear(stripe), stripes) * fold; float staff = band(i.uv.x, 0.085, 0.1) * band(i.uv.y, 0.05, 0.84); vec3 color = mix(mix(u_toLinear(backdrop), fabric, inside), u_toLinear(pole), staff); 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.