Sub-Pixel, Gamma Correct Font Rendering
Figure 1 – Zoom in of font showing sub-pixel rendering
Before I knew anything about sub-pixel font rendering, I used to think that the blue/red edges to the font shown above were a post process effect, adding cool and warm pixels to make the font jump out more. It turns out that it’s much cleverer than that. It’s called sub-pixel rendering, and it really is rendering at a sub-pixel level. Sound impossible? Read on…
To understand sub-pixel rendering you need to understand how pixels are laid out on most LCD screens. Each pixel has 3 components, RGB, and they are laid out in strips, as show here:
Figure 2 – RGB components of pixels in an LCD screen
Someone had the great idea of pretending that the components were not RGB, but were all white, what would happen if we treated the components as pixels, then we could triple our horizontal resolution!?
Figure 3 – Treating components as if they were all white pixels, tripling the width
It may sound like a crazy idea, but it does work surprisingly well. Some people don’t like it, and say that they can see the coloured pixels, but I’ve never had that problem (probably due to my poor eye-sight) and I think it looks great.
A TTF font is stored as splines which have to be rasterised onto pixels. Now, instead of rasterising to pixels we rasterise to RGB components. Basically, we rasterise to 3 times the width. Normally, a pixel brightness is determined by how much of the pixel is covered by the font, but now we make the component brightness depend on the coverage.
Figure 4 – Example of how an ‘n’ glyph might be rasterised
Figure 4 shows how a glyph might be rasterised. The zoomed in version shows the two stems. The first stem covers a single RGB pixel which will be shown as pure white pixels. The second stem covers more than one RGB pixel, so it will show a pure blue pixel, followed by a pure white pixel, followed by a pure red pixel. This doesn’t sound great, but once it’s small enough most people will not see the coloured pixels anymore. This illustrates why you normally see blue on the left edges of a sub-pixel font and red on the right.
One thing to be aware of is that it does depend on the orientation of a screen. If you are using sub-pixel rendering on a tablet which supports rotating of the display to match the orientation then you will need to re-render your fonts. It works in the same way except that you triple the height instead of the width and assume the RGB components are laid out vertically. You will end up with red edging at the top of your font and blue at the bottom (unless they have turned it on the other side, then it will be the other way around). You also need to cope with an upside down screen (just reverse to BGR). It’s not just tablets, as programmers we often use turn our monitors on their edge to get more vertical height.
The bottom line is that if you are using sub-pixel font rendering you need to give the user a way of calibrating the font for their screen.
Gamma Correct Font Rendering
Gamma correct blending is something that is often overlooked. Many commercial software products, and even some operating systems, neglected to gamma correct their blending. This is gradually changing, more and more UI engines are supporting correct gamma blending, but we still feel the knock on effects of years of bad blending as I will describe below.
Gamma correct rendering is an issue whether you are using sub-pixel rendering or not. But if you are using sub-pixel rendering you need to take extra care as I will show below.
The important thing to remember is that the values that you get out of your rasteriser are brightness values (this is also true in sub-pixel rendering, we are just storing those brightness values in RGB components). They are not alpha values that you can simply multiply with your gamma encoded font colour. I recommend that you read my previous blog about gamma correct blending which makes this clear. Whenever we are dealing with brightness values we need to decode into linear space before we do any maths.
When rendering a font to the screen we have 3 values, the foreground colour (the colour we want the font to be), the background colour (what is already on screen) and our rasterised glyph (a bitmap of brightness values). The foreground and background colours will be in gamma encoded colour space, so they both need to be decoded, then we blend between foreground and background using the glyph brightness value, and then we gamma encode the resulting value.
Figure 5 – Difference between no gamma blending and blending with a gamma of 1.43
Figure 5 shows how gamma blending affects the look of the font. Rendering with a gamma of 1.0 (effectively not doing gamma blending) results in the antialiasing dropping off too quickly. This results in a more jagged and thinner looking font. Using a gamma of 1.43 make the font look smoother and fuller. I’ve shown a font here without sub-pixel rendering, but the effect is the same.
So, I know you’re all asking why I’ve used a gamma value of 1.43 and not the normal 2.2. The reasons are historical. Because so many programmers have neglected gamma blending for so long, people who have created fonts have tried to work around the problem of fonts looking too thin by just making the fonts thicker! Obviously it doesn’t help the jaggedness, but it does make them look the proper weight, as originally intended. The obvious problem with this is that if we want to gamma blend correctly many older fonts will look wrong. So we compromise, and use a lower gamma value, so we get a bit better antialiasing, but the fonts don’t look too heavy.
The history of font rendering is littered with hacks such as this. This article is only scratching the surface. For more details look for articles on the history of Microsofts ClearType font system. There is no right answer in how to make best use of limited pixels, and tricks such as sub pixel rendering often clash with other tricks such as hinting. Before I go on to discuss gamma correct blending of sub-pixel fonts I will briefly discuss hinting.
Hinting means snapping the control points of the splines to vertical or horizontal pixel lines. This is only done in certain cases, such as for the mid-section of an I glyph, as shown below.
This hinting is especially useful for very small fonts with very few pixels to cover. It can make the difference between a grey blurry line of two pixels, or one single white line of pixels which is much clearer.
The question is, how does hinting work with sub-pixel rendering? The answer is: not very well. The problem is that if the hinting happens at a component level (we now have 3X the width) then it will be snapping to RGB sub pixels and we will get more pure R, G and B pixels, not what we want. Alternatively, if we hint at the pixel level then we might as well not use sub-pixel rendering at all. One answer to this problem was to run a post process on the font once it had been rasterised and try and remove the worst looking coloured pixels. It seems to me that this is adding one hack onto another hack, onto another hack. Again, this makes it even more difficult for font creators to get the result they intend.
The solution that FreeType uses is simply to turn off horizontal hinting for sub-pixel rendering, and leave vertical hinting enabled (FreeType is an excellent free font rendering library used by many commercial software products). This makes sense to me, we are fixing the lack of horizontal resolution with sub-pixel rendering, so we can turn off horizontal hinting. I think that the results are excellent, and it’s what I use in my 10x Code Editor.
Gamma-correct sub pixel rendering
In my font renderer I pre-rasterise the glyphs onto a texture page, and then render glyph quads using a shader. It may be tempting to simply treat the glyph brightness values as alpha values and let OpenGL do the blending. After all, this will do gamma correct blending (see my previous article). But not so fast! There is a problem here. For sub-pixel rendering we have 3 brightness values, so we can’t just use one alpha value. Normal alpha blending blends the RGB components by one value, the alpha value. Here, we are pretending that each RGB component is a separate pixel, so each component needs to be blended separately with the destination component. We can’t just blend the source pixel with the destination pixel, we need to blend the source components with the destination components, separately.
// convert the vertex colour to linear space vec4 v_colour_linear = pow4(v_colour, inv_gamma); // convert the background colour to linear space vec4 v_background_colour_linear = pow4(v_background_colour, inv_gamma); // blend float r = tex_col.r * v_colour_linear.r + (1.0 - tex_col.r) * v_background_colour_linear.r; float g = tex_col.g * v_colour_linear.g + (1.0 - tex_col.g) * v_background_colour_linear.g; float b = tex_col.b * v_colour_linear.b + (1.0 - tex_col.b) * v_background_colour_linear.b; // gamma encode the result gl_FragColor = pow4(vec4(r, g, b, tex_col.a), gamma);
Another side effect of this separate component blending is that we can’t use sub-pixel rendering unless we know the background pixels at blend time. Usually for UI work we are rendering to a plain colour, so that can just be passed into the shader. If you are rendering text to an unknown background then you will usually need to use normal (non-sub-pixel) rendering and use alpha values and blend in the normal way.
Sub-pixel rendering treats each RGB colour component as a separate pixel. You need to be aware of the orientation of your screen to make this work. Hinting means snapping the font splines to pixels, which doesn’t work that well with sub-pixel rendering. One option is simply to disable horizontal hinting. Many font renderers don’t bother with gamma correct alpha blending. This results in more jagged fonts and thinner fonts. When gamma blending sub-pixel fonts remember to blend each RGB component separately.