rendering super-smooth scalable bitmap fonts
Improved Alpha-Tested Magnification for Vector Textures and Special Effects. It allows you to render bitmap fonts without jagged edges even at high magnifications. This article describes how to implement the technique in libgdx.
Introduction
Traditional bitmap fonts work fine if the pixels in the font map 1:1 onto screen pixels. However, they look bad when rotated, and increasingly worse when scaled up. Either you end up seeing individual pixels, or you turn on linear interpolation and end up with a smudgy blur instead.
Using a distance field font lets you render text that remains crisp even under rotations and other arbitrary transforms, even blown up to a large magnification, without notable extra run-time cost. You can see the difference below:
The same technique can also be used to draw symbols, logos, anything. The major drawback is that it works only for monochrome images; it is not possible to use this technique for arbitrary color images.
com.badlogic.gdx.tests.BitmapFontDistanceFieldTest in the gdx-tests project. It was used to produce the above screenshot. There’s also class if you want to jump directly to usage.
How does it work?
signed distance field. The rightmost column in the screenshot above shows what our font image looks like after pre-processing. The pre-processor takes a black and white image as input, with a black background and a white glyph. For each white pixel, it computes the distance to the closest black pixel, and vice versa. For black pixels, the distance for black pixels is then negated, and the result is normalized to the range 0-1. This gives us a smooth, continuous field, with 0.5 exactly on the edge of the original glyph, decreasing to 0.0 as we move farther away, and increasing to 1.0 when we move towards the inside. We then set up alpha testing to output a pixel only when the alpha is greater than 0.5. With a texture that uses nearest-neighbour interpolation, this will look exactly the same as our input image. However, the distance field image is much better suited to linear interpolation than a traditional font image is: compare the third and fourth columns in the picture above.
Generating the font
bitmap fonts, but with different settings.
- Fire up Hiero and choose your font and attributes as normal.
- Hiero page.)
- minus twice the spread. If you used a spread of 4, you’d set -8 for both X and Y. This is necessary because the padding increases the spacing between glyphs at rendering time.
- -
-
Loading the font
There is no magic to loading the font into your game. You just need to make sure that you enable linear filtering on the texture:
To make your font look better when down-scaled (less than 1:1 size), you can also turn on mipmapping:Texture texture = new Texture(Gdx.files.internal("myfont.png"));texture.setFilter(TextureFilter.Linear, TextureFilter.Linear);
Texture texture = new Texture(Gdx.files.internal("myfont.png"), true); // true enables mipmapstexture.setFilter(TextureFilter.MipMapLinearNearest, TextureFilter.Linear); // linear filtering in nearest mipmap image
MipMapLinearNearestor the slower but smootherMipMapLinearLinear. Then create the font:
Note: Before LibGDX 1.6.0 (May 2015), if you’re replacing a “regular” font by a distance field font, be aware that the font metrics are not the same. In particular, the extra padding causes the baseline to shift downwards, so you’ll need to compensate by drawing your text higher. As of commit c976f463, padding should be compensated for automatically.BitmapFont font = new BitmapFont(Gdx.files.internal("myfont.fnt"), new TextureRegion(texture), false);
Rendering with a shader
page on shaders.SpriteBatchexpects:
The secret sauce is in the fragment shader. But even here, there’s not much to it:uniform mat4 u_projTrans;attribute vec4 a_position;attribute vec2 a_texCoord0;attribute vec4 a_color;varying vec4 v_color;varying vec2 v_texCoord;void main() { gl_Position = u_projTrans * a_position; v_texCoord = a_texCoord0; v_color = a_color;}
#ifdef GL_ESprecision mediump float;#endifuniform sampler2D u_texture;varying vec4 v_color;varying vec2 v_texCoord;const float smoothing = 1.0/16.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, v_color.a * alpha);}
font.vertandfont.frag, you can load the shader as usual:ShaderProgram fontShader = new ShaderProgram(Gdx.files.internal("font.vert"), Gdx.files.internal("font.frag"));if (!fontShader.isCompiled()) { Gdx.app.error("fontShader", "compilation failed:\n" + fontShader.getLog());}
SpriteBatchand are between abegin()andend()call, is really straightforward:spriteBatch.setShader(fontShader);font.draw(spriteBatch, "Hello smooth world!", 10, 10);spriteBatch.setShader(null);
Customizing the shader
distanceis a value between 0 and 1, with 0 being far away from the letter, 0.5 being right on the edge, and 1 being well inside it. Thesmoothstepfunction in the shader above is mapping values well below 0.5 to 0, and values well above 0.5 to 1, but gives a smooth transition around 0.5 to provide antialiasing. The softness of this transition is configured by thesmoothingconstant, which you should tweak to be correct for your font and scale.smoothingvalue for crisp fonts is0.25f / (spread * scale), wherespreadis the value you used when generating the font, andscaleis the scale you’re drawing it at (how pixels in the distance field font are mapped to screen pixels). If the scale is not constant, you can pass it in via auniformvariable.distancevariable in the shader. Here are some possibilities. I haven’t tested any of these; if you find bugs, please update this wiki page!Adding an outline
distanceis betweenoutlineDistanceand0.5....const float outlineDistance; // Between 0 and 0.5, 0 = thick outline, 0.5 = no outlineconst vec4 outlineColor;...void main() { float distance = texture2D(u_texture, v_texCoord).a; float outlineFactor = smoothstep(0.5 - smoothing, 0.5 + smoothing, distance); vec4 color = mix(outlineColor, v_color, outlineFactor); float alpha = smoothstep(outlineDistance - smoothing, outlineDistance + smoothing, distance); gl_FragColor = vec4(color.rgb, color.a * alpha);}
Adding a drop shadow
Here, we sample the texture a second time, slightly offset from the first. The second application gets a lot more smoothing applied to it, and is rendered “behind” the actual text....const vec2 shadowOffset; // Between 0 and spread / textureSizeconst float shadowSmoothing; // Between 0 and 0.5const vec4 shadowColor;...void main() { float distance = texture2D(u_texture, v_texCoord).a; float alpha = smoothstep(0.5 - smoothing, 0.5 + smoothing, distance); vec4 text = vec4(v_color.rgb, v_color.a * alpha); float shadowDistance = texture2D(u_texture, v_texCoord - shadowOffset).a; float shadowAlpha = smoothstep(0.5 - shadowSmoothing, 0.5 + shadowSmoothing, shadowDistance); vec4 shadow = vec4(shadowColor.rgb, shadowColor.a * shadowAlpha); gl_FragColor = mix(shadow, text, text.a);}
Using distance fields for arbitrary images
The generator used by Hiero can also be used as a stand-alone command line tool, to process pre-existing black and white images. Run it from an unzipped libGDX distribution directory as follows: Windows:
Linux: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
This will print usage instructions: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
Generates a distance field image from a black and white input image.The distance field image contains a solid color and stores the distancein the alpha channel.The output file format is inferred from the file name.Command line arguments: INFILE OUTFILE [OPTION...]Possible options: --color rrggbb color of output image (default: ffffff) --downscale n downscale by factor of n (default: 1) --spread n edge scan distance (default: 1)
spreadis defined in terms of pixels on the input image, not the downscaled output image. To get the same result, multiply it bydownscale. Also note that the defaults are probably not very helpful, and you’ll want to specify both--downscaleand--spread:java -cp extensions/gdx-tools.jar:gdx.jar com.badlogic.gdx.tools.distancefield.DistanceFieldGenerator --downscale 32 --spread 128 logo.png logo-df.png
