Scaling friendly font rendering with distance fields

This article by David Saltares Márquez and Alberto Cejas Sánchez, the authors of Libgdx Cross-platform Game Development Cookbook, describes how we can generate a distance field font and render it in Libgdx. As a bitmap font is scaled up, it becomes blurry due to linear interpolation. It is possible to tell the underlying texture to use the nearest filter, but the result will be pixelated. Additionally, until now, if you wanted big and small pieces of text using the same font, you would have had to export it twice at different sizes. The output texture gets bigger rather quickly, and this is a memory problem.

(For more resources related to this topic, see here.)

Distance field fonts is a technique that enables us to scale monochromatic textures without losing out on quality, which is pretty amazing. It was first published by Valve (Half Life, Team Fortress…) in 2007. It involves an offline preprocessing step and a very simple fragment shader when rendering, but the results are great and there is very little performance penalty. You also get to use smaller textures!

In this article, we will cover the entire process of how to generate a distance field font and how to render it in Libgdx.

Getting ready

For this, we will load the data/fonts/oswald-distance.fnt and data/fonts/oswald.fnt files. To generate the fonts, Hiero is needed, so download the latest Libgdx package from http://libgdx.badlogicgames.com/releases and unzip it.

Make sure the samples projects are in your workspace. Please visit the link https://github.com/siondream/libgdx-cookbook to download the sample projects which you will need.

How to do it…

First, we need to generate a distance field font with Hiero. Then, a special fragment shader is required to finally render scaling-friendly text in Libgdx.

Generating distance field fonts with Hiero

  1. Open up Hiero from the command line. Linux and Mac users only need to replace semicolons with colons and back slashes with forward slashes:
    java -cp gdx.jar;gdx-natives.jar;gdx-backend-lwjgl.jar;gdx-backend-lwjgl-natives.jar;extensions\
    gdx-tools\gdx-tools.jar com.badlogic.gdx.tools.hiero.Hiero

    Select the font using either the System or File options.

  2. This time, you don't need a really big size; the point is to generate a small texture and still be able to render text at high resolutions, maintaining quality. We have chosen 32 this time.
  3. Remove the Color effect, and add a white Distance field effect.
  4. Set the Spread effect; the thicker the font, the bigger should be this value. For Oswald, 4.0 seems to be a sweet spot.
  5. To cater to the spread, you need to set a matching padding. Since this will make the characters render further apart, you need to counterbalance this by the setting the X and Y values to twice the negative padding.
  6. Finally, set the Scale to be the same as the font size. Hiero will struggle to render the charset, which is why we wait until the end to set this property.
  7. Generate the font by going to File | Save BMFont files (text)....

The following is the Hiero UI showing a font texture with a Distance field effect applied to it:

Libgdx Cross-platform Game Development Cookbook

Distance field fonts shader

We cannot use the distance field texture to render text for obvious reasons—it is blurry! A special shader is needed to get the information from the distance field and transform it into the final, smoothed result. The vertex shader found in data/fonts/font.vert is simple. The magic takes place in the fragment shader, found in data/fonts/font.frag and explained later.

First, we sample the alpha value from the texture for the current fragment and call it distance. Then, we use the smoothstep() function to obtain the actual fragment alpha. If distance is between 0.5-smoothing and 0.5+smoothing, Hermite interpolation will be used. If the distance is greater than 0.5+smoothing, the function returns 1.0, and if the distance is smaller than 0.5-smoothing, it will return 0.0. The code is as follows:

#ifdef GL_ES
precision mediump float;
precision mediump int;
#endif
 
uniform sampler2D u_texture;
 
varying vec4 v_color;
varying vec2 v_texCoord;
 
const float smoothing = 1.0/128.0;
 
void main() {
   float distance = texture2D(u_texture, v_texCoord).a;
   float alpha = smoothstep(0.5 - smoothing, 0.5 + smoothing, distance);
   gl_FragColor = vec4(v_color.rgb, alpha * v_color.a);
}

The smoothing constant determines how hard or soft the edges of the font will be. Feel free to play around with the value and render fonts at different sizes to see the results. You could also make it uniform and configure it from the code.

Rendering distance field fonts in Libgdx

Let's move on to DistanceFieldFontSample.java, where we have two BitmapFont instances: normalFont (pointing to data/fonts/oswald.fnt) and distanceShader (pointing to data/fonts/oswald-distance.fnt). This will help us illustrate the difference between the two approaches. Additionally, we have a ShaderProgram instance for our previously defined shader.

In the create() method, we instantiate both the fonts and shader normally:

normalFont = new BitmapFont(Gdx.files.internal("data/fonts/oswald.fnt"));
normalFont.setColor(0.0f, 0.56f, 1.0f, 1.0f);
normalFont.setScale(4.5f);
 
distanceFont = new BitmapFont(Gdx.files.internal("data/fonts/oswald-distance.fnt"));
distanceFont.setColor(0.0f, 0.56f, 1.0f, 1.0f);
distanceFont.setScale(4.5f);
 
fontShader = new ShaderProgram(Gdx.files.internal("data/fonts/font.vert"),
 Gdx.files.internal("data/fonts/font.frag"));
 
if (!fontShader.isCompiled()) {
   Gdx.app.error(DistanceFieldFontSample.class.getSimpleName(),
 "Shader compilation failed:\n" + fontShader.getLog());
}

We need to make sure that the texture our distanceFont just loaded is using linear filtering:

distanceFont.getRegion().getTexture().setFilter(TextureFilter.Linear, TextureFilter.Linear);

Remember to free up resources in the dispose() method, and let's get on with render(). First, we render some text with the regular font using the default shader, and right after this, we do the same with the distance field font using our awesome shader:

batch.begin();
batch.setShader(null);
normalFont.draw(batch, "Distance field fonts!", 20.0f, VIRTUAL_HEIGHT - 50.0f);
 
batch.setShader(fontShader);
distanceFont.draw(batch, "Distance field fonts!", 20.0f, VIRTUAL_HEIGHT - 250.0f);
batch.end();

The results are pretty obvious; it is a huge win of memory and quality over a very small price of GPU time. Try increasing the font size even more and be amazed at the results! You might have to slightly tweak the smoothing constant in the shader code though:

Libgdx Cross-platform Game Development Cookbook

How it works…

Let's explain the fundamentals behind this technique. However, for a thorough explanation, we recommend that you read the original paper by Chris Green from Valve (http://www.valvesoftware.com/publications/2007/SIGGRAPH2007_AlphaTestedMagnification.pdf).

A distance field is a derived representation of a monochromatic texture. For each pixel in the output, the generator determines whether the corresponding one in the original is colored or not. Then, it examines its neighborhood to determine the 2D distance in pixels, to a pixel with the opposite state. Once the distance is calculated, it is mapped to a [0, 1] range, with 0 being the maximum negative distance and 1 being the maximum positive distance. A value of 0.5 indicates the exact edge of the shape. The following figure illustrates this process:

Libgdx Cross-platform Game Development Cookbook

Within Libgdx, the BitmapFont class uses SpriteBatch to render text normally, only this time, it is using a texture with a Distance field effect applied to it. The fragment shader is responsible for performing a smoothing pass. If the alpha value for this fragment is higher than 0.5, it can be considered as in; it will be out in any other case:

Libgdx Cross-platform Game Development Cookbook

This produces a clean result.

There's more…

We have applied distance fields to text, but we have also mentioned that it can work with monochromatic images. It is simple; you need to generate a low resolution distance field transform. Luckily enough, Libgdx comes with a tool that does just this.

Open a command-line window, access your Libgdx package folder and enter the following command:

java -cp gdx.jar;gdx-natives.jar;gdx-backend-lwjgl.jar;gdx-backend-lwjgl-natives.jar;extensions\gdx-tools\
gdx-tools.jar com.badlogic.gdx.tools.distancefield.DistanceFieldGenerator

The distance field font generator takes the following parameters:

  • --color: This parameter is in hexadecimal RGB format; the default is ffffff
  • --downscale: This is the factor by which the original texture will be downscaled
  • --spread: This is the edge scan distance, expressed in terms of the input

Take a look at this example:

java […] DistanceFieldGenerator --color ff0000 --downscale 32 --spread 128 texture.png texture-distance.png

Alternatively, you can use the gdx-smart-font library to handle scaling. It is a simpler but a bit more limited solution (https://github.com/jrenner/gdx-smart-font).

Summary

In this article, we have covered the entire process of how to generate a distance field font and how to render it in Libgdx.


Further resources on this subject:


You've been reading an excerpt of:

Libgdx Cross-platform Game Development Cookbook

Explore Title
comments powered by Disqus