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.
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:
Sometimes it came down to looking really closely at the pixels. But which is better? I don’t know.
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.
Here’s the output. Pixels are either black or white:
For antialiasing we want to smoothly transition between colors, so that it looks like this:
But how much? If we do too much, it looks blurry:
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:
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].
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:
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.