My friends! Let’s talk about font rendering. So far we’ve been using a relatively naive way of rendering in libgdx: for each character in the string, find the glyph, draw the glyph. This works fine for English and others, but not all. Some languages use different glyphs for the same character depending on where it appears in the word or sentence. As soon as we don’t have a one-to-one relationship between characters and glyphs, we can’t do proper rendering.
The solution for this is to transform the list of characters into a list of glyphs, then render the glyphs. The BitmapFontData, BitmapFont, and BitmapFontCache classes have been refactored to support doing that. Here’s how it works:
- GlyphLayout, a new class, is given a string of characters. It first breaks the strings into “runs”. A run is a string of characters all the same color and without newlines. The reason to break the text into runs is that each run can be “shaped” individually, which means to convert the characters into glyphs for display. When breaking the string into runs, GlyphLayout parses color markup tags (eg “thisIsWhite[red]thisIsRed”) and stores the color for each run.
- Next, the runs are shaped using a new BitmapFontData method,
getGlyphs. This takes the characters for the run and generates both a list of glyphs and a list of positions for each glyph. The default implementation just looks up the glyph for the character as has always been done. A more complex implementation could use HarfBuzz or another lib to do the shaping.
- Next, the shaped runs are laid out. If wrapping is enabled and a run’s glyphs exceed the wrap width, then glyphs are split for line wrapping using another new BitmapFontData method,
getWrapIndex. The run is ended, the remaining glyphs are moved to a new run on the next line, and the layout process repeats. Because BitmapFontData controls line wrapping, it can be customized for a particular langauge.
- At this point GlyphLayout is done. It stores the
Array<GlyphRun>and provides a width and height which is the bounds of all the runs. Previously getting text bounds required all the work of laying out the glyphs, which then had to be repeated for drawing. With GlyphLayout, the work is only done once. Each GlyphRun has
FloatArrayfor the positions.
- Next comes BitmapFontCache. This class now has an
Array<GlyphLayout>and its job is to store the vertex data for rendering the glyphs in those GlyphLayouts.
Introducing GlyphLayout and the new way of rendering glyphs required some relatively small API changes. Sorry! Since breaking the API was unavoidable, we also made a few other breaking changes. It’s better to fix up a few API changes in one go than to have to do it every time you update libgdx.
Here are the changes that may affect you:
- The BitmapFontCache
setWrappedText, and similar
addXxxmethods are replaced by
addText. The same happened to the BitmapFont draw methods, which delegates to BitmapFontCache. There are fewer methods now and they have more features. These methods all return GlyphLayout instead of TextBounds.
- BitmapFont.TextBounds and
getBoundsare done. Instead, give the string to GlyphLayout and get the bounds using its
heightfields. You can then draw the text by passing the same GlyphLayout to BitmapFont, which means the glyphs don’t have to be laid out twice like they used to.
- BitmapFont.HAlignment is gone. Align is used instead.
- Align has been moved to the utils package.
setScalehave been moved to BitmapFontData. These are really BitmapFontData settings, not per BitmapFont settings.
computeVisibleGlyphsare gone. GlyphLayout can be used instead, which provides each glyph position.
- BitmapFont has an
Array<TextureRegion>rather than a
TextureRegion. This makes it easier to add regions as needed.
Here’s a quick guide for moving to the new API:
// Old: font.draw(batch, "meow", x, y); font.drawMultiline(batch, "meow\nmeow", x, y); font.drawWrapped(batch, "meow meow meow meow meow meow", x, y, width, HAlignment.RIGHT); TextBounds bounds = font.getBounds("meow"); font.draw(batch, "meow", x + bounds.width / 3, y + bounds.height / 3); // New: font.draw(batch, "meow", x, y); font.draw(batch, "meow\nmeow", x, y); font.draw(batch, "meow meow meow meow meow meow", x, y, width, Align.right, true); GlyphLayout layout = new GlyphLayout(); // Obviously stick this in a field to avoid allocation each frame. layout.setText("meow"); font.draw(batch, layout, x + layout.width / 3, y + layout.height / 3);
Sorry for the trouble if you end up needing to change your code slightly, but this enables us to have proper font rendering in the future. HarfBuzz and FreeType can allow us to render text in any language, which is a big deal!
But wait, there’s more! Many languages don’t need glyph shaping – they can use a one-to-one mapping of characters to glyphs – but they have a different problem: they have way too many glyphs than we can hope to fit on a texture. There are only a few solutions for this, you need to render on the fly.
Rendering entire sentences to the atlas instead of individual glyphs is one approach, but this isn’t great for text input or text that changes a lot. Rendering glyphs on the fly, as they are encountered, is probably the best we can do. If the glyph atlas becomes filled, we can either add atlas pages (bad for draw calls) or empty the atlas texture and start over (fails if the screen contains more glyphs than can fit in a single page).
Rendering glyphs on the fly means using FreeType. We already have FreeTypeFontGenerator to generate a BitmapFont using FreeType, but now you can tell it to generate an “incremental” font. When the font is queried for a glyph that hasn’t been rendered yet, it renders it to the glyph atlas.
The red boxes at the top are the glyph atlas pages (purposefully made tiny for testing) and the text below is rendered using those glyphs. We can implement better packing (eg, a skyline variant) in the future, for now it uses PixmapPacker as it used to.
It will be interesting to see if this helps libgdx have better adoption in China, where currently cocos2d-x is widely used. Interestingly, cocos2d-x appears to use a “label” approach to font rendering where an entire sentence is rendered to the backing texture rather than individual glyphs.