Soft dot
A round dot whose edge goes from crisp to a soft glow with one slider.
- 01GoalShape
- 02TechniqueCircles, boxes & lines
- 03ExecutionSoft dot
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.
uv * 2.0 - 1.0MultiplyAdduv * 2.0 - 1.0uv * 2.0 - 1.0uv * 2.0 - 1.0 UVTime01 Center the coordinates
lengthLength2Dlengthlengthlength In02 Distance to the center
smoothstepSmoothStepsmoothstepsmoothstepsmoothstep In03 Soft edge
mixMixmixmixmix In04 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 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 / resolutionuv * 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. Vector A group of two to four numbers handled as one value (vec2, vec3, vec4). The same type holds a position, a direction, a color or a UV.length(p) is the length of the vectorA group of two to four numbers handled as one value (vec2, vec3, vec4). The same type holds a position, a direction, a color or a UV.
Cut the edge. A threshold turns the distance into coverage. 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.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
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.t = clamp((x − e0) / (e1 − e0), 0, 1); s = t² (3 − 2t)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.
- Draw a rectangle that covers the canvas: two triangles from -1 to 1 in clip space.
- In the vertex shader, write
v_uvrunning from 0 to 1 across the rectangle, plusv_position,v_normalandv_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. - Each frame, set
u_resolutionto the drawing buffer’s size in pixels andu_timeto the seconds since you started. This card only reads the resolution. - Set
p_radiusandp_softness, and uploadp_dotColorandp_backgrounddecoded from sRGB to linear (c <= 0.04045 ? c / 12.92 : pow((c + 0.055) / 1.055, 2.4)per channel). - 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.
- Add a
ColorRectto a 2D or UI scene and set its anchors preset to Full Rect, so it covers the viewport. - In the Inspector, set Material to a new
ShaderMaterial, then its Shader to a newShader, and paste the code into the shader editor. - 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
#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);}// 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.