Learning about font rendering, I was looking at text closely last time, and I noticed another issue. The shadows of each letter overlap the previous letter. That’s because I’m drawing one letter at a time. So for example in the fl, I draw the f’s letter, outline, and shadow, then I draw l’s letter, outline, and shadow. So l’s shadow is drawn on top of f’s letter.
The first thing I considered was to draw all the shadows first and then draw all the letters. But a second problem here, harder to see, is that shadows are drawn on top of the adjacent letter’s shadows, and that causes them to be darker than they should be. The distance fields for adjacent letters always overlap:
It’s only when the letters are close enough that you can see artifacts of the double rendering.
To solve both problems, I can generate a new distance field which is the min() of the distance fields for each character. The min() of signed distance fields is the union of the shapes. I used the blendEquation()[1] function with MAX_EXT
instead of the default FUNC_ADD
(it’s max instead of min because msdfgen’s encoding is inverted). The MAX_EXT
extension seems to have 100% support[2] in WebGL 1, and is always included with WebGL 2.
The output texture holds a combined distance field. I then use the distance field shader to draw that combined distance field to the map.
I could do this once per string I want to draw or once for the entire scene. I decided to do it once per string, because that allows me to use different colors and styles (thickness, outline width, shadow, halo) per string.
I ran into a few bugs with this:
- because I couldn’t figure out how to min() msdf fields, I put an sdf in the combined distance field instead of msdf, and that meant the corners of fonts got a little rounder; to compensate I increased the resolution of the combined distance field
- having a different resolution for the original and combined distance fields messed up the antialiasing; in the previous post I described a “slope” of distance field vs output pixels, but now there’s an intermediary with the slope stretched out
- the letters got out of alignment when I moved them around for the intermediate texture; in particular, I had to calculate a new baseline position accounting for the change in resolution
One way I compared parameters was by rendering them in alternating frames. Here’s an example showing that the combined distance field resolution matters (slightly):
Here’s the final result, showing that the overlapped drawing is fixed, especially at the bottom left of the second g:
The combined distance field approach solves the problem but it means I need to write each string to a separate intermediate texture, which leads to a rabbit hole of having to allocate texture space during rendering, possibly reusing textures, and dealing with gpu pipeline stalls. That’s an area I don’t understand well. Fortunately I don’t need it to be optimized for my project. But for a game project, I might choose to do the two pass approach instead, letting the shadows get drawn twice in overlapping areas. Are there better approaches? I don’t know.