Text Rendering in the QML Scene Graph

Some time ago, Gunnar presented to you what the new QML Scene Graph is all about. As mentioned in that article, one of the new features is a new technique for text rendering based on distance field alpha testing. This technique allows us to leverage all the power of OpenGL and have text like we never had before in Qt: scalable, sub-pixel positioned and sub-pixel antialiased... and at almost no cost.

First, here is a video showing the improvements this technique brings:

http://www.youtube.com/watch?v=M4waDSXWGGw

Distance Field... Huh?

Now, most of you are probably wondering what a distance field is. Also known as distance transform or distance map, it is a derived representation of a digital image that maps each pixel to a value representing the distance to the closest edge of the glyph. On a range from 0.0 to 1.0, a pixel on the edge of the glyph should have a value of 0.5, a pixel outside the shape should have a value going towards 0.0 as the distance from the nearest edge increases, a pixel inside the shape should have a value going towards 1.0 as the distance from the nearest edge increases.

[gallery link="file" include="6193,6197,6202" order="DESC"]

A distance field can be generated either from a high-resolution image or from a vector-based representation of the glyph. The first solution is typically based on a brute-force method and can potentially be very slow, thus it is hardly conceivable to use this solution in our case when we have dozens of glyphs (or even hundreds in the case of Chinese) to generate at once. Some tools (like this one) allow to pre-render a set of glyphs from a given font into a file to be then used at run-time, but we didn't choose this solution as it gives poor flexibility to developers.

We chose instead to use a vector-based representation of the glyph to generate the distance data. Put simply, we extrude the outline of the glyph by a fixed distance on the inside and on the outside and fill the area between the extruded outlines with a distance gradient. We store the result in a single 8-bit channel of a 64x64 pixels cell contained in a bigger texture. By doing so we manage, thanks to Kim, to generate a distance field in less than a millisecond per glyph on a mobile device (on average), making any vector font usable dynamically at run-time.

How Does It Work

This technique allows to take advantage of the native bilinear interpolation performed by the GPU on the texture. The distance from the edge can be accurately interpolated, allowing to reconstruct the glyph at any scale factor. All we need to do is alpha-testing: pixels are shown or discarded depending on a threshold, typically 0.5 as it is the value at the edge of the glyph. The result is a glyph with sharp outlines at any level of zoom, as if they were vector graphics. The only flaw is that it cuts off sharp corners, but this is negligible considering how bad a magnified glyph looks when this technique is not used.

[gallery include="6263,6264" link="file" order="DESC"]

This technique provides a great visual improvement while not affecting performance at runtime as everything is done "for free" by the graphics hardware.

Improvements

High-Quality Anti-Aliasing

Using the same distance field representation of the glyph, we can also do high-quality anti-aliasing using a single line of shader code:

varying highp vec2 sampleCoord;
uniform sampler2D texture;
uniform lowp vec4 color;
uniform highp float distMin;
uniform highp float distMax;
void main() {
gl_FragColor = color * smoothstep(distMin, distMax, texture2D(texture, sampleCoord).a);
}

 

Instead of using a single threshold to do alpha-testing, we now use two distance thresholds that the shader uses to soften the edges. The input distance field value is interpolated between the two thresholds with the smoothstep function to remove aliasing artifacts. The width of the soft region can be adjusted by changing the distance between the two thresholds. The more the glyph is minified, the wider the soft region is. The more the glyph is magnified, the thinner the soft region is.

When the GPU is powerful enough (meaning desktop GPUs) we can even do sub-pixel anti-aliasing, it is just about adding some lines of shader code. Instead of using the distance data to compute the output pixel's alpha, we use the data of the neighboring pixels to compute each color component of the output pixel separately. Five texture samples are then needed instead of one. The red component averages the three left-most distance field values, the green component averages the three middle distance field values and the blue component averages the three right-most distance field values. Because this requires more processing power, sub-pixel anti-aliasing is currently disabled on mobile platforms. Anyway, the high pixel density displays that equips mobile devices nowadays make the use of sub-pixel anti-aliasing pointless.

Special Effects

In addition to anti-aliasing, the distance field can be used, again with just a few lines of shader code, to do some special effects like outlining, glows or drop shadows. We are then able to implement the three styles provided by the QML Text element (outline, raised and sunken) using that technique. For example to do outlining, we just have to use a different color for the texels which are between two distance values. Effectively, it makes the use of these effects faster, nicer and scalable (scaling a styled text in QtQuick 1.0, not Scene Graph based, gives very poor quality).

Implementation Details

For a given font, we rasterize and cache each glyph (or actually its distance field representation) only once at a size of 64x64 pixels. The same texture is then used to render the glyph at any size by scaling it appropriately. In comparison, the native font rendering used by QPainter implies caching a separate version of the glyph for each size used in the application. This leads to a lower graphics memory consumption when using different sizes of the same font.

To be able to scale freely the glyphs, we need to disable font hinting to have correct glyph positions at any level of zoom. As a consequence glyphs are also sub-pixel positioned (i.e. not pixel aligned).

Hack It Your Way

It you haven't already, grab the Qt 5 repositories and look for the files starting with qsgdistancefield in the QtDeclarative module.

  • For debugging purposes you can disable distance field text rendering with the QML_DISABLE_DISTANCEFIELD environment variable.
  • On desktop platforms sub-pixel antialiasing is enabled by default, pass the --text-gray-antialiasing option to qmlscene to use the standard gray antialiasing.

For more reading on the topic have a look at this paper from Valve.


Blog Topics:

Comments