Pulsing ring
A thin ring whose radius breathes in and out with a sine of time.
- 01GoalShape
- 02TechniqueCircles, boxes & lines
- 03ExecutionPulsing ring
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.
lengthLength2Dlengthlengthlength UVTime01 Distance to the center
sinSinsinsinsin In02 Radius over time
absAbsabsabsabs In03 Distance to the line
smoothstepSmoothStepsmoothstepsmoothstepsmoothstep In04 Cut the line
mixMixmixmixmix In05 Color
How it works
The ring is the Soft dot with two changes: the threshold measures the distance to a line instead of to an area, and that line moves with time.
Distance to the center. As in the Soft dot, the 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 / resolutionlength(p) gives each pixel’s distance from the middle.
A radius that breathes. sin turns a steadily growing number into a smooth swing between -1 and 1 and back, once
every 2π. Feeding it i.time * speed makes that swing repeat over time, faster with a larger speed. Multiplying by the
pulse size scales the swing, and adding the base radius centers it there: the radius now moves between
radius - pulse and radius + pulse and slows down at both ends, which is what makes the motion read as breathing
rather than bouncing.
Distance to the line. d - radius is negative inside the circle and positive outside, so a threshold on it makes a
disc. abs() folds the inside over onto the outside: the result is how far the pixel is from the circle’s line, from
either side. Now pixels on the line are at 0 and both directions grow away from it.
Cut the line. A 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.t = clamp((x − e0) / (e1 − e0), 0, 1); s = t² (3 − 2t)2.0 / height is one pixel in these units), so the edges are
anti-aliased on both sides of the ring and the line keeps the same thickness at any radius.
Color it. mix puts the ring color where the coverage is 1 and the background where it is 0.
The width stays constant as the ring grows, because the width is measured in the shader, after the movement. Scaling a
ring sprite instead would thicken the line along with it. To add a glow, mix in a second, wider smoothstep at lower
strength; to fade the ring as it grows, multiply the coverage by 1.0 - (pulsed - p_radius + p_pulse) / (2.0 * p_pulse).
Make it in GLSL
The copied code is a complete WebGL2 fragment shader, drawn on a rectangle that covers the canvas.
- Draw two triangles covering the canvas in clip space, with a vertex shader that writes
v_uvfrom 0 to 1 and constants forv_position,v_normalandv_view(the fragment shader declares all four). - Every frame, set
u_timeto the seconds since you started (performance.now() / 1000minus the start time) andu_resolutionto the drawing buffer’s size. - Set the four float uniforms, and upload
p_ringColorandp_backgrounddecoded from sRGB to linear. - Draw with blending off, from a
requestAnimationFrameloop so the ring keeps moving.
A page that shows the ring only sometimes should stop the loop while it is hidden; the shader runs for every pixel of the canvas each frame, whether the ring is on screen or not.
Make it in Godot
The shipped shader assumes HDR 2D is off (the default). With it on, make u_toLinear and u_toSrgb return their input unchanged.
The copied code is a complete canvas_item shader; i.time reads Godot’s TIME.
- Add a
ColorRectand set its anchors preset to Full Rect. - Set its Material to a new
ShaderMaterialwith a newShader, and paste the code. - Adjust the radius, line width, pulse size, speed and colors under Shader Parameters. The ring animates in the editor too.
In a VisualShader, the radius is the Time input times the speed, through Sin, times the pulse size, plus the radius. Subtract it from the Length2D of the centered UV, take the Abs, and cut it with SmoothStep around the line width, then OneMinus and Mix the colors.
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// Circles, boxes & lines → Pulsing ring, 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_radius; // Radius: 0.5 (0.1 to 0.9)uniform float p_width; // Line width: 0.04 (0.005 to 0.3)uniform float p_pulse; // Pulse size: 0.1 (0.0 to 0.4)uniform float p_speed; // Speed: 3.0 (0.0 to 10.0)uniform vec3 p_ringColor; // Ring color: vec3(0.35, 0.85, 1.0), sRGB; upload it decoded to linearuniform vec3 p_background; // Background: vec3(0.05, 0.06, 0.1), 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 * 2.0 - 1.0) * vec2(i.resolution.x / i.resolution.y, 1.0); float d = length(p); // sin swings between -1 and 1, so the radius breathes by p_pulse either side of p_radius float pulsed = p_radius + p_pulse * sin(i.time * p_speed); // abs() measures from the circle's line on both sides: the distance to a ring, not to a disc float band = abs(d - pulsed); float pixel = 2.0 / i.resolution.y; float ring = 1.0 - smoothstep(p_width - pixel, p_width + pixel, band); vec3 color = mix(p_background, p_ringColor, ring); 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);}// Circles, boxes & lines → Pulsing ring, 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 radius : hint_range(0.1, 0.9, 0.01) = 0.5;uniform float width : hint_range(0.005, 0.3, 0.005) = 0.04;uniform float pulse : hint_range(0.0, 0.4, 0.01) = 0.1;uniform float speed : hint_range(0.0, 10.0, 0.1) = 3.0;uniform vec3 ringColor : source_color = vec3(0.35, 0.85, 1.0);uniform vec3 background : source_color = vec3(0.05, 0.06, 0.1);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 * 2.0 - 1.0) * vec2(i.resolution.x / i.resolution.y, 1.0); float d = length(p); // sin swings between -1 and 1, so the radius breathes by pulse either side of radius float pulsed = radius + pulse * sin(i.time * speed); // abs() measures from the circle's line on both sides: the distance to a ring, not to a disc float band = abs(d - pulsed); float pixel = 2.0 / i.resolution.y; float ring = 1.0 - smoothstep(width - pixel, width + pixel, band); vec3 color = mix(u_toLinear(background), u_toLinear(ringColor), ring); 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.
Further reading
- Shapes (opens in a new tab)Book
- Shaping functions (opens in a new tab)Book
- sin (opens in a new tab)Reference
- abs (opens in a new tab)Reference
- smoothstep (opens in a new tab)Reference
- Sine wave (opens in a new tab)Reference