2D toolkit · lesson 1 of 10 · Intermediate · about 6 min
Antialiasing with fwidth
You will build: Make the thin cyan ring smooth: ring radius 0.3, half-width 0.006, with an edge exactly one pixel soft using fwidth.
Look closely at a circle drawn with step(): the edge is a staircase. Each pixel is asked a yes-or-no question (inside or outside?) and a pixel can only be fully one color. A diagonal or curved edge cannot be drawn with squares, so you see jaggies. The fix is to let the pixels on the edge be partly covered: a blend of the two colors.
The previous lessons did this with smoothstep(0.0, 0.02, d), a blur of a fixed width. That has a catch: 0.02 is measured in your own coordinates, not in pixels. On a small preview it is about three pixels wide, on a big screen it is a fraction of a pixel and the jaggies come back; make the picture bigger and the edge gets fuzzy or sharp for no reason.
fwidth: how big is one pixel?
The GPU draws pixels in small groups, so it can compare a value with the same value at the neighbouring pixel. fwidth(x) is exactly that: how much x changes from one pixel to the next. For a distance d, fwidth(d) is the size of one pixel in distance units, wherever you are and at any resolution.
float d = length(p) - 0.3; // negative inside, positive outside float w = fwidth(d); // one pixel, in the same units as d float m = 1.0 - smoothstep(-w, w, d); // soft over about one pixel, always
Now the edge is always about one pixel soft: no staircase, and never blurry. This is how most real shader art and UI shaders draw crisp, smooth shapes, and why the heart in the daily challenge uses it.
Thin lines are the hard case. A line thinner than a pixel with step breaks into dashes. Use the distance to the line's center, subtract the half-width, and run that through the same fwidth smoothstep.
The numbers you need
Everything the picture depends on is given, so the challenge is the code, not guessing.
- Space
p = (uv - 0.5) * float2(aspect, 1.0)- Distance to the line
d = abs(length(p) - 0.3) - 0.006 (negative on the line)- Pixel size
w = fwidth(d)- Coverage
m = 1.0 - smoothstep(-w, w, d)- Colors
background float3(0.08, 0.08, 0.12), ring float3(0.4, 0.9, 1.0)
Try it in your own language
The lesson is written once and shown in every language. Pick yours: Unity (Built-in), Unity (URP), Unreal, Shadertoy / WebGL, OpenGL (LWJGL, raylib), three.js, GameMaker, LÖVE (Lua), Godot, Apple (iOS, macOS), WebGPU / Bevy.
Start codingGo deeper
Part of the learning path: From zero to ray marching.