Skip to content
Shape · Circles, boxes & linesBeginner

Soft dot

A round dot whose edge goes from crisp to a soft glow with one slider.

UVTimeSurface
Open in Playground
  1. 01GoalShape
  2. 02TechniqueCircles, boxes & lines
  3. 03ExecutionSoft dot
Full-screen quad

Parameters

0.50
0.080
#ffcc59
#12141f

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.

  1. uv * 2.0 - 1.0MultiplyAdduv * 2.0 - 1.0uv * 2.0 - 1.0uv * 2.0 - 1.0
    UV
    Time

    01 Center the coordinates

  2. lengthLength2Dlengthlengthlength

    02 Distance to the center

  3. smoothstepSmoothStepsmoothstepsmoothstepsmoothstep

    03 Soft edge

  4. mixMixmixmixmix

    04 Color

How it works

The dot is the smallest useful shape a fragment shader can draw, and it already holds the whole idea of drawing with math: measure how far each pixel is from something, then decide with a threshold.

Center the coordinates. The quad’s UV runs from 0 to 1 with the origin in the bottom-left corner. uv * 2.0 - 1.0 moves the origin to the middle and makes each axis run from -1 to 1. The preview is wider than it is tall, though, so one unit across would be longer on screen than one unit up, and a circle would come out as an oval. Multiplying x by the aspect ratio (width over height) makes a unit the same length in both directions.

Measure the distance. length(p) is the length of the vector from the center to the pixel: 0 in the middle, growing evenly in every direction. Every pixel at the same distance forms a circle, so the dot is already there; it just has no edge yet.

Cut the edge. A threshold turns the distance into coverage. step(radius, d) would switch from inside to outside inside a single pixel, and along a curved edge that leaves a staircase of jagged pixels. A smoothstep from radius - soft to radius + soft goes from 0 to 1 gradually instead, and 1.0 - … flips it so the inside is 1. With a large softness that ramp is the soft glow; with a small one it shrinks to an anti-aliased edge.

The softness never drops below one pixel: p spans 2 units over the preview’s height, so 2.0 / height is one pixel in those units, and max() keeps the softness at least that large, so the ramp from radius - soft to radius + soft always spans at least two pixels. Without it, dragging Softness to 0 brings back the jagged edge.

Color it. The coverage is a number from 0 to 1, so it can choose between two colors with mix: the background where it is 0, the dot color where it is 1, and a blend along the edge.

The same four steps make most 2D shapes. Swap length(p) for the distance to a box or a line and the threshold doesn’t change. Use the coverage as alpha instead of mixing, and the dot becomes a mask over anything.

Make it in GLSL

The copied code is a complete WebGL2 fragment shader. It draws on anything that covers the canvas and gives it UVs.

  1. Draw a rectangle that covers the canvas: two triangles from -1 to 1 in clip space.
  2. In the vertex shader, write v_uv running from 0 to 1 across the rectangle, plus v_position, v_normal and v_view. The fragment shader reads all four, so WebGL2 refuses to link unless the vertex shader writes them; constants are fine for the last three.
  3. Each frame, set u_resolution to the drawing buffer’s size in pixels and u_time to the seconds since you started. This card only reads the resolution.
  4. Set p_radius and p_softness, and upload p_dotColor and p_background decoded from sRGB to linear (c <= 0.04045 ? c / 12.92 : pow((c + 0.055) / 1.055, 2.4) per channel).
  5. Draw with blending off. The shader writes an opaque, sRGB-encoded color, which a default WebGL2 canvas shows as is.

To use the dot as a mask instead, return disc as alpha and draw with gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA).

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.

  1. Add a ColorRect to a 2D or UI scene and set its anchors preset to Full Rect, so it covers the viewport.
  2. In the Inspector, set Material to a new ShaderMaterial, then its Shader to a new Shader, and paste the code into the shader editor.
  3. Change the radius, softness and colors under Shader Parameters; the dot updates in the editor.

In a VisualShader, the chain is UV, a MultiplyAdd (times 2, minus 1) and a multiply by the aspect, then Length2D, SmoothStep with the edges set to the radius minus and plus the softness, a OneMinus, and a Mix between the two 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

soft-dot.fragGLSL
#version 300 es// Circles, boxes & lines → Soft dot, 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.05 to 1.0)uniform float p_softness; // Softness: 0.08 (0.0 to 0.5)uniform vec3 p_dotColor; // Dot color: vec3(1.0, 0.8, 0.35), sRGB; upload it decoded to linearuniform vec3 p_background; // Background: vec3(0.07, 0.08, 0.12), 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) {	// Centered, with x scaled by the aspect ratio: one unit is as long across as up, so the dot stays round	vec2 p = (i.uv * 2.0 - 1.0) * vec2(i.resolution.x / i.resolution.y, 1.0);	float d = length(p);	// p spans 2 units over the height, so 2 / height is one pixel: softness never drops below it, so the ramp	// always spans at least two pixels and never stair-steps	float soft = max(p_softness, 2.0 / i.resolution.y);	float disc = 1.0 - smoothstep(p_radius - soft, p_radius + soft, d);	vec3 color = mix(p_background, p_dotColor, disc);	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);}
soft-dot.gdshaderGodot shader
// Circles, boxes & lines → Soft dot, 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.05, 1.0, 0.01) = 0.5;uniform float softness : hint_range(0.0, 0.5, 0.005) = 0.08;uniform vec3 dotColor : source_color = vec3(1.0, 0.8, 0.35);uniform vec3 background : source_color = vec3(0.07, 0.08, 0.12);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) {	// Centered, with x scaled by the aspect ratio: one unit is as long across as up, so the dot stays round	vec2 p = (i.uv * 2.0 - 1.0) * vec2(i.resolution.x / i.resolution.y, 1.0);	float d = length(p);	// p spans 2 units over the height, so 2 / height is one pixel: softness never drops below it, so the ramp	// always spans at least two pixels and never stair-steps	float soft = max(softness, 2.0 / i.resolution.y);	float disc = 1.0 - smoothstep(radius - soft, radius + soft, d);	vec3 color = mix(u_toLinear(background), u_toLinear(dotColor), disc);	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