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.

The picture produced by the shader in the lesson "Antialiasing with fwidth"
The target picture. In the lesson you write the shader that draws it, and a match bar shows how close you are.

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 coding

Go deeper

Part of the learning path: From zero to ray marching.