2D toolkit · lesson 2 of 10 · Intermediate · about 5 min
Supersampling: when fwidth is not enough
You will build: Replace the single sample with 2 x 2 supersampling to remove the moir from the rings: four samples at plus or minus a quarter of a pixel (px = fwidth(p.x)), averaged.
fwidth works when your picture is a shape with an edge: you know where the edge is, so you can soften it. But some patterns are not like that: stripes that repeat faster than the pixels, noise, a checkerboard in the distance. Here a pixel covers many stripes at once, and a single yes-or-no sample gives wild moiré patterns, bands that are not in the picture at all.
The general fix is supersampling: run the pattern several times per pixel at slightly different positions and average the results. A pixel half covered by white now averages to gray, with no need to know where the edge is.
float px = fwidth(p.x); // size of one pixel float s = 0.0; s += pat(p + float2(-0.25, -0.25) * px); s += pat(p + float2( 0.25, -0.25) * px); s += pat(p + float2(-0.25, 0.25) * px); s += pat(p + float2( 0.25, 0.25) * px); float m = s * 0.25; // 2 x 2 samples, averaged
The offsets are a quarter of a pixel either way: the four samples sit in the four quadrants of the pixel. That is called 2×2 supersampling (SSAA).
The price
Four samples means the shader runs four times: a pattern that cost one millisecond now costs four. That is why games do not use it for the whole screen. They use cheaper tricks: MSAA (multisample only along triangle edges, done by the hardware), FXAA / SMAA (a filter that finds edges in the finished picture and blurs them) and TAA (use the previous frames as extra samples). Inside a pixel shader you can only do what you control yourself: soft edges with fwidth, or a few samples averaged as here.
The numbers you need
Everything the picture depends on is given, so the challenge is the code, not guessing.
- Pattern
pat(p) = step(0.5, frac(length(p) * 120.0)): provided, rings far too thin for one sample- Pixel size
px = fwidth(p.x)- Offsets
(-0.25, -0.25), (0.25, -0.25), (-0.25, 0.25), (0.25, 0.25), each times px- Average
m = (sum of the four) * 0.25- Colors
dark float3(0.06, 0.07, 0.12), light float3(0.95, 0.9, 0.75)
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