Subpixel accurate GPU rendering of text using a glyph atlas
This post introduces a simple way to achieve high-quality rendering of subpixel positioned glyphs without requiring a special pixel shader to perform fancy interpolation. Note that this is about subpixel positioning, not subpixel rendering (but the topics are somewhat related).
I'll start off with some background/motivation and end with a description of the technique.
Glyph atlas brief
Rendering glyphs from a glyph atlas is probably the most common way to render text in OpenGL and Direct3D. Individual glyphs are rasterized into a texture atlas, on-demand or in a preprocess, from a vector representation, such as a TrueType font. To render text on the screen, each glyph is drawn as a screen-aligned quad using the graphics processor.
To achieve high-quality text rendering, the texels in the glyph atlas and pixels on the screen should align perfectly. Misaligned glyphs will make text blurry, if filtering is active, or have an "uneven" appearance. Glyphs are therefore typically rendered at whole pixels. A result of rendering the string "aaaa" with this method is shown in Figure 1.
Why subpixel positioning?
The distance between glyphs in a word is called glyph advances. Vector-based fonts have a resolution independent set of advances, meaning that a pixel-aligned position for each glyph can be a rather crude approximation of the intended layout, depending on how they are calculated. One possibility, that would preserve the intended lengths of words, is to compute the location of each glyph in high precision and round to whole pixels. This will unfortunately cause the text to have a jittery appearance with varying distances between the same set of glyphs. The other option is to pixel align glyph advances, which results in good-looking and legible text. However, the length error along the line is magnified with each added glyph (see Figure 2).
The issue also comes up when an application targets multiple resolutions, zoom levels, and display densities, while wanting to maintain the same layout for text at all resolutions.
Subsampling a prefiltered representation
Because lines of text flow in the horizontal direction (x), the error doesn't accumulate in the vertical direction (y). As a result, it is typically fine to round to pixels along y. (Languages where the converse is true can of course simply swap the handling of the two coordinates. The technique can also be extended to both directions at the cost of memory and rasterization effort.) Note that the rasterization phase is free to rasterize the glyph at any single subpixel y-location, such as aligned with the baseline of the font. With this in mind, a method to render glyphs at subpixel positions along x is presented below.
The idea is to create a glyph atlas with multiple versions of each glyph (Figure 3). Each version represents a different subpixel offset. We will stick to four subpixel versions here which will allow each glyph pixel to be packed into 32 bits (four 8-bit alpha texels). The texels are arranged in interleaved form (Figure 4).
While it is intuitive to interleave four separately offset versions of a glyph, it is a relatively slow approach. A faster method is to render the glyph scaled 4x along x and then apply a box filter in that direction with a kernel width of a whole pixel (four texels).
The glyph in Figure 4 represents a prefiltered representation of the glyph that we can subsample to achieve subpixel positioning. We simply introduce four times the number of texels in each glyph quad and offset them by 3/8 pixel along x so the first texel is properly aligned with the sample center (see Figure 5).
As of now, this approach provides four subpixel locations for each glyph. The nice thing is that the approach works well with ordinary linear filtering (see Figure 6).
That's it. You have already seen the final result in Figure 2. I have successfully used this technique to render high-quality text with exactly the same layout at multiple resolutions in my secret project. Make sure to keep a lookout for the next post if this first attempt of mine was interesting to you.
EDIT: It turns out that this technique have been used in stb for some time: oversample.