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.
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 recipe, we will cover the entire process of how to generate a distance field font and how to render it in Libgdx.
For this recipe, 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.
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.
java -cp gdx.jar;gdx-natives.jar;gdx-backend-lwjgl.jar;gdx-backend-lwjgl-natives.jar;extensionsgdx-toolsgdx-tools.jar com.badlogic.gdx.tools.hiero.Hiero
4.0
seems to be a sweet spot.The following is the Hiero UI showing a font texture with a Distance field effect applied to it:
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, just like the ones in Chapter 3, Advanced 2D Graphics. 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); }
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: " + 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:
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:
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:
This produces a clean result.
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;extensionsgdx-toolsgdx-tools.jar com.badlogic.gdx.tools.distancefield.DistanceFieldGenerator
The distance field font generator takes the following parameters:
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).
18.191.235.8