Otto Chrons bio photo

Otto Chrons

Jack of all trades, master of some.

Twitter LinkedIn Github

OpenGL is a framework for rendering 3D graphics (and 2D to some degree), with absolutely no support whatsoever for rendering text. If you want text in your 3D application, you have to do it all by yourself.

Rendering options

For rendering text there are basically two options:

  1. Vector rendering
  2. Bitmap rendering

In vector rendering the individual glyphs (graphical shapes of characters) are transformed into a set of triangles that are then rendered using the 3D pipeline. This is doable for a small number of characters that are displayed in relatively large sizes, but for large amounts of small text it’s simply not feasible and the quality is rather poor.

In bitmap rendering, on the other hand, the font is rasterized beforehand to a bitmap at a specified size, and used as such. On desktop operating systems, the vector fonts are first rasterized into bitmaps before drawn to the screen to optimize quality and performance. The problem here is that you need many different bitmaps to support fonts at various sizes and that can add up quite quickly, especially if you consider supporting extended Unicode character sets like Chinese or Japanese.

Scalable fonts

Many games and other OpenGL applications render fonts at “screen resolution”, meaning that the characters always have a fixed size in pixels. This, however, presents problems with today’s variety of screen resolutions and DPIs. A font that looks just fine on a 24” monitor at a 1080p resolution, might look rather tiny on a 5” screen with the same resolution!

If you want to support many different devices at different resolutions, you need a font that can scale up and down without losing too much of its quality. In an optimal case you would have a built-in font rasterizer, that would dynamically render fonts at a required size for each device. That is not very practical for most applications, so you’ll have to manage with a single bitmap font.

Basic bitmap font rendering

A bitmap font is typically a “texture atlas” (a bitmap consisting of hundreds of individual glyph images) and your code addresses individual glyphs using appropriate texture coordinates. To keep this example simple, I’ve “prerendered” some text into a bitmap (in Photoshop), so there is no need to play with the individual glyphs. The same principles apply anyway.

Font texture with transparency, on a black background

Below you can find an interactive WebGL canvas with which you can test the effect of zooming in and out on the quality of the text. The rendering is using standard bilinear filtering (trilinear would be smoother, but it gives rather mushy results at small sizes) and the fragment shader is very simple:

varying vec2 vTextureCoord;

uniform sampler2D uSampler;

void main(void) {
  gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0) *
    texture2D(uSampler, vec2(vTextureCoord.s, vTextureCoord.t));
}
Interactive sandbox
Zoom

As you can notice, the font works quite well when it’s rendered closed to its original size, but starts to show artifacts when it’s too large or too small. Also when the text size is changing, there is a lot of visible aliasing at small pixel sizes. Scaling up brings jagged edges, even though the font is rendered at a relatively high resolution.

Signed Distance Field rendering

The game company Valve published an article at SIGGRAPH 2007 presenting a new way to render fonts and decals at high magnification. The article describes the method very well, including some fancier rendering enhancements made possible by the method.

The font texture for SDF looks fuzzy as it contains information about the distance to the border of the glyph, instead of the glyph pixels themselves. This information can be used in the shader to do accurate alpha testing to render a very close approximation of the original glyph form.

Font texture for distance field rendering

The shader uses a smoothing parameter to increase quality at small sizes, so play around with it and the zoom slider in the interactive sandbox below. As you can see, the readability of the text is much better than with the basic method, especially at extreme text sizes.

Interactive sandbox
Zoom
Smoothing

Fragment shader code for above is like this:

varying vec2 vTextureCoord;

uniform sampler2D uSampler;
uniform float smoothing;

void main(void) {
    float distance = texture2D(uSampler, vTextureCoord).a;
    float alpha = smoothstep(0.5 - smoothing, 0.5 + smoothing, distance);
    gl_FragColor = vec4(0.0, 0.0, 0.0, alpha);
}

Just for fun, here’s another shader that gives you a nice outline. Note that for optimal quality you would have to adjust outline parameters as well as the smoothing.

Interactive sandbox
Zoom
Smoothing

Outline shader code:

varying vec2 vTextureCoord;

uniform sampler2D uSampler;
uniform float smoothing;

void main(void) {
    vec4 outlineColor = vec4(0.0, 0.0, 0.0, 1.0);
    vec4 baseColor = vec4(1.0, 0.0, 0.0, 1.0);
    float distance = texture2D(uSampler, vTextureCoord).a;
    if( distance > 0.45 && distance < 0.5) {
        float oFactor = smoothstep(0.50, 0.45, distance);
        baseColor = mix(baseColor, outlineColor, oFactor);
    } else if (distance <= 0.45 ) {
        baseColor = outlineColor;
    }
    baseColor.a *= smoothstep(0.3 - smoothing, 0.3 + smoothing, distance);
    gl_FragColor = baseColor;
}