SDF antialiasing

 from Red Blob Games’s Blog
Blog post: 22 Sep 2024

Last time I was looking at letter spacing with my renderer to see how it compared to Google Chrome on Mac. But while doing that I noticed that their antialiasing looked nicer than mine. So I tweaked parameters, including antialias edge width, gamma, and threshold bias.

My rendererGoogle's renderer
My renderer (first) vs Google’s (second)

I got results that were close to Google Chrome on Mac, but beyond that it became unclear which variant was actually better. I kept tweaking parameters:

tweak 0tweak 1tweak 2tweak 3tweak 4tweak 5
Many attempts to tweak parameters

Sometimes it came down to looking really closely at the pixels. But which is better? I don’t know.

My rendererGoogle's renderer
My renderer (first) vs Google’s (second)

I realized after a while that this is a rabbit hole. I had to force myself out of this endless tweaking cycle. Besides, Chrome on Mac is different from other browser + windowing systems, so I shouldn’t spend all my time trying to match it.

But two months later, I revisited antialiasing, because I needed to better understand it to implement halos and drop shadows. This happens a lot with my experiments. I’ll discover something, then I’ll tweak a lot, then I’ll put it away for a while, and later I can come back to it and try to understand what I did.

To implement antialiasing let’s look at the mapping from distance to color.

Distance maps to either black or white

Here’s the output. Pixels are either black or white:

The letter e rendered with no antialiasing

For antialiasing we want to smoothly transition between colors, so that it looks like this:

The letter e rendered with antialiasing

But how much? If we do too much, it looks blurry:

The letter e rendered with more antialiasing

We want it to scale based on the output pixels, not the input distance field. And that means we need to know something about the number of pixels on screen.

On the msdfgen page there’s a shader that includes antialiasing. They’re measuring screenPxRange representing how many pixels in the output corresponds to one “distance unit” in the signed distance field input.

If we want antialiasing to occur over edge_blur_px pixels in the output, we can divide edge_blur_px ÷ screenPxRange to find out what signed distance range this represents. For example if we want to antialias over 2 pixels, and screenPxRange is 8 px / distanceunit, then 2 px ÷ 8 px / distanceunit is ¼ distanceunits. The msdfgen code will antialias between 0 and +¼. Another option would be to antialias between −⅛ and +⅛.

This is what the blending looks like:

Color blends between black and white

The slope of that line is screenPxRange / edge_blur_px.

I wanted to get a better sense of what edge_blur_px should be. Over how many pixels should I apply antialiasing? I had previously tweaked it, then made the antialiasing width an interactive parameter to tweak faster. When I revisited it a few weeks later, I realized it’d be better to see all the outputs at once instead of using interactivity to see one at a time. For more on one interactive vs multiple non-interactive visualization, see Bret Victor’s page about “Ladder of Abstraction”[1].

Testing a range of antialiasing widths

I had set edge_blur_px to 1.0 in my previous tweaking, and this confirms that 1.0 is a reasonable choice. Lower values from 0.5 down look a little blocky, and higher values from 2.0 up look a little blurry. I decided to zoom into that range:

Testing a range of antialiasing widths

These may all look the same at first, but look closely with a magnifying glass and you’ll see differences. From eyeballing this, I think maybe 1.2 or 1.3 might be the best choice, at least for large text. I don’t have any explanation for this. Could be it 1.25? Could it be sqrt(1.5)? If I looked at other sizes and characters, would I conclude it has to be higher, like sqrt(2) or 1.5? Is there signal processing math to prove the best value? Would it be different with gamma correction? Should I use smoothstep instead of linearstep? I don’t know. I’ll set it to 1.2 for now.

I’m quite happy with what I have, but not happy with how long it took. Two months went by between the first and last image on this blog post. I had many fixes and tweaks in those two months. I’ll describe those in the next few posts.

Email me , or tweet @redblobgames, or comment: