In the last blog post I wrote about how I wanted to test the parameters of msdfgen[1], which generates multi-signed distance fields for fonts and other shapes. While testing the emrange
parameter, I found lots of bugs in my renderer.
I also wanted to try out msdfgen’s new asymmetric range parameter, aemrange
. The distance field goes both inside and outside the font edge, but if I want outlines, I want it to go pretty far outside the font. I made a visualization to show the range:
From this we can see that the interior has far fewer contour lines than the exterior. That’s because fonts are relatively thin. If it were a large object like a solid circle, it would have many contour lines on the interior.
The encoding of the distance field maps distances to a value 0.0–1.0, which gets stored in the texture as 0–255. By default, the encoding sets distance=0 to value=0.5 which will be 127.5. Let’s look at how much of the font space is used by each encoded value. Blue means I’m using that value for the interior of the font. Green means I’m using it for the exterior of the font. Gray means I’m not using that value.
This histogram tells us two things:
- Of all the pixels in the distance field encoding, most of them are used (blue or green). There’s not a lot of waste representing values that I don’t need (gray). This is good.
- Of all the 0–255 values in the encoding, around half of them are used. There’s a lot of waste on the right side where no pixels use that value. This is bad.
Most of the values I care about are on the left half. MSDFgen version 1.3 adds support for an asymmetric encoding. I switched from -emrange 0.8 (which corresponds to -0.4 to +0.4) to -aemrange -0.7 0.1, keeping the full range at 0.8.
Does this improve the quality?
It did not improve quality! If anything it’s slightly worse. What’s going on?
Keeping the full range at 0.8 was the problem. I had shifted the range, which extended the contour lines out farther, but didn’t make them more accurate:
- Of all the pixels in the distance field encoding, fewer of them than before are being used (blue or green), and a lot more are wasted (gray). This is bad.
- Of all the 0–255 values in the encoding, around half of them are used. There’s less wasted on the right side but more wasted on the left side. This is bad.
I instead want to stretch the range, with -aemrange -0.4 0.1:
- Of all the pixels in the distance field encoding, most of them are being used (blue or green). This is good.
- Of all the 0–255 values in the encoding, msot of them are being used. This is good.
The output does look better. The contour lines don’t go out farther; instead they are spaced more closely together, which means more precision. The curves are smoother.
Asymmetry helps by throwing away the range that is unused. I also wanted to check for the opposite problem: that some value is needed but doesn’t fit in the 0–255 range. I added code to the shader to detect this:
if (distances.g >= 1.0) gl_FragColor = vec4(1, 0.5, 0, 1);
and here’s what it looks like if I cut off too much of the range:
This is an example of a shader “assert” that helps me figure out when something is wrong.
What is the optimal aemrange
? It took me way too long to realize that I should set the low and high endpoints independently:
- Low endpoint is based on the maximum range I want for outline, halo, and other special effects.
- High endpoint is based on the maximum distance value inside the font, as calculated by
msdfgen
.
Setting the range this way should maximize font rendering quality. In both cases the range needs to be extended slightly beyond the limit so that the texture interpolation can find an accurate value. But I wasn’t so smart. I tweaked repeatedly until I found values that were good enough. Writing this blog post helped me understand what is going on, and I’ll be able to choose better next time.