Linear wipe
A soft straight edge that sweeps across the quad at any angle, easing in and out, and back again.
- 01GoalReveal
- 02TechniqueWipes & transitions
- 03ExecutionLinear wipe
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.
clampClampclampclampclamp UVScreenTime01 Progress
smoothstepSmoothStepsmoothstepsmoothstepsmoothstep In02 Ease in and out
dotDotdotdotdot In03 Position along the wipe
mixMixmixmixmix In04 Front
smoothstepSmoothStepsmoothstepsmoothstepsmoothstep In05 Mask
How it works
The card sweeps a white mask across the quad and back, over and over. Where the mask is 1 the new picture would show, where it is 0 the old one stays, and between them is a feathered edge. Direction turns the edge to any angle, Edge softness feathers it, and Duration sets how long one sweep takes.
1. Progress
A transition needs a number that goes from 0 to 1 over its duration. Here it comes from the clock so that the preview
loops on its own: fract(i.time / p_duration) is a sawtooth that climbs from 0 to 1 every duration, and
1.0 - abs(1.0 - 2.0 * x) folds it into a triangle wave that climbs and then comes back down. Stretching that by 1.25
and clamping to 0..1 flattens the top and the bottom, so the wipe rests for a moment fully shown and fully hidden
before it turns around. In a game or a page, this whole step is replaced by one uniform that a script animates (the
platform guides show how).
2. Ease in and out
A linear progress makes the edge start at full speed and stop dead, which reads as mechanical. Running it through
smoothstepA soft threshold. It is 0 below the first edge, 1 above the second, and follows an S-shaped curve with flat ends in between. Smoothstep A soft threshold. It is 0 below the first edge, 1 above the second, and follows an S-shaped curve with flat ends in between. Easing A curve that bends a 0–1 progress so motion speeds up or slows down instead of moving at a constant rate.t = clamp((x − e0) / (e1 − e0), 0, 1); s = t² (3 − 2t)ease-in-out: e = p² (3 − 2p): the cubic
p * p * (3 - 2p) starts flat, speeds up through the middle and flattens again, so the edge accelerates away and
settles into place. Any other easing curve from the easing fundamental can go here instead.
3. Position along the wipe
The shape of a linear wipe is each pixel’s distance along one direction. Dot product Multiplies two vectors component by component and adds the results. For unit vectors it is the cosine of the angle between them, from 1 (same direction) to -1 (opposite). 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.radians turns Direction into an angle,
cos and sin turn the angle into a unit vector, and the dot productMultiplies two vectors component by component and adds the results. For unit vectors it is the cosine of the angle between them, from 1 (same direction) to -1 (opposite).dot(a, b) = |a| |b| cos θuv = gl_FragCoord.xy / resolution
That distance doesn’t run from 0 to 1 by itself. Along a diagonal, the quad’s corners are further from its center than
its edges are, by 0.5 * (|x| + |y|) of the direction vector. Dividing by twice that reach and adding 0.5 maps the
first corner the wipe touches to 0 and the last one to 1 at every angle, so a diagonal wipe takes as long as a
horizontal one and never ends early.
4. Front
The front is where the edge stands at the current moment. mix(-p_softness, 1.0, eased) moves it from one softness
before 0 to exactly 1. Starting one softness early means that at progress 0 even the feathered part of the edge is off
the quad, so the mask is cleanly 0, and at progress 1 it is cleanly 1.
5. Mask
smoothstep(front, front + p_softness, along) is 0 behind the front and 1 ahead of it, with a smooth ramp of
Edge softness in between. One minus it is the mask: the pixels the front has passed are 1. The card’s output role
is a mask, so the value is not color-encoded: the preview shows it as it is, white where the new picture would be and
black where the old one stays. A host reads it from the red channel and uses it as the opacity of a cover or as the
blend factor between two pictures.
A hard step(front, along) would work too, but on a diagonal it shows stairs, one pixel tall. Even a softness of a
few pixels hides them, which is why the softness never goes to zero.
Make it in GLSL
The copy button gives a complete WebGL2 fragment shader. Draw it on a quad or a full-screen triangle with a vertex
shader that writes v_uv from 0 at the bottom-left to 1 at the top-right. main() also reads v_position,
v_normal and v_view, so the vertex shader must declare and write them, but zeros are fine: the effect uses only
uv.
As it is, the shader loops forever on u_time, which suits a preview. A real transition runs once, when you say so.
Replace the first two lines of effect() with a uniform and drive it from JavaScript:
uniform float u_progress; // 0 = hidden, 1 = shown
// in effect(), instead of wave and progress:
float progress = u_progress;
const start = performance.now();
function frame(now) {
const t = Math.min((now - start) / 600, 1); // a 600 ms wipe
gl.uniform1f(progressLocation, t);
draw();
if (t < 1) requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
The easing stays in the shader, so the script only has to send a straight ramp.
To use the mask as a transition between two pictures rather than as an overlay, keep the shape and the edge and mix
two textures with it in your own fragment shader: fragColor = mix(texture(oldScene, uv), texture(newScene, uv), mask);.
Both scenes must be rendered to textures first, which is the screen-post version of this technique.
Make it in Godot
In a VisualShader (CanvasItem mode), build the axis from the direction with Radians, Cos and Sin into a Vector2Compose, take a Dot of it with UV minus 0.5, and feed it to a SmoothStep whose edges are the front and the front plus the softness; a OneMinus of that goes to Alpha.
Add a ColorRect named Wipe inside a CanvasLayer, set its anchors to Full Rect so it covers the window, give it a new ShaderMaterial and paste the shader into it. It is a canvas_item shader and, as exported, draws the mask itself, opaque: white where the wipe has passed, black elsewhere.
For a scene transition, turn the mask into a cover: a color whose alpha is the mask, so the game shows through where
the wipe hasn’t passed. Change the last line of fragment():
COLOR = vec4(0.0, 0.0, 0.0, frag_out.r);
Then make the wipe run once, on cue. Add a uniform and replace the first two lines of effect():
uniform float amount : hint_range(0.0, 1.0) = 0.0;
// in effect(), instead of wave and progress:
float progress = amount;
and tween it from a script, changing the scene while the screen is fully covered:
func change_scene(path: String) -> void:
var material := $CanvasLayer/Wipe.material as ShaderMaterial
var tween := create_tween()
tween.tween_method(func(v: float) -> void: material.set_shader_parameter("amount", v), 0.0, 1.0, 0.5)
await tween.finished
get_tree().change_scene_to_file(path)
tween = create_tween()
tween.tween_method(func(v: float) -> void: material.set_shader_parameter("amount", v), 1.0, 0.0, 0.5)
The CanvasLayer must live in an autoload (or survive the change some other way) so the cover stays up while the next scene loads. The ease stays inside the shader, so a linear tween is enough.
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// Wipes & transitions → Linear wipe, 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_duration; // Duration (s): 2.4 (0.5 to 6.0)uniform float p_direction; // Direction (degrees): 20.0 (0.0 to 360.0)uniform float p_softness; // Edge softness: 0.06 (0.002 to 0.4)in 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;};vec4 effect(Inputs i) { // A triangle wave, stretched past 0..1 and clamped, so the wipe rests a moment when fully shown and fully hidden float wave = 1.0 - abs(1.0 - 2.0 * fract(i.time / p_duration)); float progress = clamp(wave * 1.25 - 0.125, 0.0, 1.0); float eased = smoothstep(0.0, 1.0, progress); float angle = radians(p_direction); vec2 axis = vec2(cos(angle), sin(angle)); // The quad's corners project to between -reach and +reach on the axis, so this maps corner to corner onto 0..1 at // any angle, and the wipe takes the whole duration to cross float reach = 0.5 * (abs(axis.x) + abs(axis.y)); float along = dot(i.uv - 0.5, axis) / (2.0 * reach) + 0.5; // The front starts one softness before 0 and ends at 1, so the soft edge is fully off the quad at both ends float front = mix(-p_softness, 1.0, eased); float mask = 1.0 - smoothstep(front, front + p_softness, along); return vec4(vec3(mask), 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); // A mask shows as is: coverage in .r, not encoded; opaque, so alpha is 1 fragColor = vec4(clamp(frag_out.rgb, 0.0, 1.0), 1.0);}// Wipes & transitions → Linear wipe, 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 duration : hint_range(0.5, 6.0, 0.1) = 2.4;uniform float direction : hint_range(0.0, 360.0, 1.0) = 20.0;uniform float softness : hint_range(0.002, 0.4, 0.002) = 0.06;struct Inputs { vec2 uv; vec3 position; vec3 normal; vec3 view; float time; vec2 fragCoord; vec2 resolution;};vec4 effect(Inputs i) { // A triangle wave, stretched past 0..1 and clamped, so the wipe rests a moment when fully shown and fully hidden float wave = 1.0 - abs(1.0 - 2.0 * fract(i.time / duration)); float progress = clamp(wave * 1.25 - 0.125, 0.0, 1.0); float eased = smoothstep(0.0, 1.0, progress); float angle = radians(direction); vec2 axis = vec2(cos(angle), sin(angle)); // The quad's corners project to between -reach and +reach on the axis, so this maps corner to corner onto 0..1 at // any angle, and the wipe takes the whole duration to cross float reach = 0.5 * (abs(axis.x) + abs(axis.y)); float along = dot(i.uv - 0.5, axis) / (2.0 * reach) + 0.5; // The front starts one softness before 0 and ends at 1, so the soft edge is fully off the quad at both ends float front = mix(-softness, 1.0, eased); float mask = 1.0 - smoothstep(front, front + softness, along); return vec4(vec3(mask), 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); // A mask shows as is: coverage in .r, not encoded COLOR = vec4(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.