BitmapFont refactoring

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:

  1. 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.
  2. 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.
  3. 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.
  4. 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 Array<Glyph> and a FloatArray for the positions.
  5. 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 setText, setMultilineText, setWrappedText, and similar addXxx methods are replaced by setText and 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 getBounds are done. Instead, give the string to GlyphLayout and get the bounds using its width and height fields. 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.
  • markupEnabled and setScale have been moved to BitmapFontData. These are really BitmapFontData settings, not per BitmapFont settings.
  • BitmapFont computeGlyphAdvancesAndPositions and computeVisibleGlyphs are 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:

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.

If you’re interested in the changes for these features, you can check out PRs #2955 and #2973.

5 thoughts on “BitmapFont refactoring

  1. Nice!

    Would be cool if there was a guide on the wiki about supporting Japanese/ Korean/ Chinese, since it seems to be a bit of a complex subject. Even talking to Japanese people “What font characters would I need for someone to hold a conversation in a game”, doesn’t really have straight forward answers.

    So I use Ubuntu fonts For ENG, RUS, POL, GER, POR, SPN, CZK, FRE, code2000 for Thai, but may never find one that supports Korean/ Japanese/ Chinese (and can be texture packed at a reasonable size by heiro).

  2. Awesome, that is really a big deal!
    And how about load and render multiple true type fonts (other ttf files fill holes in the main ttf) in a simple call?

  3. Just a question.. I am doing this for a while now, but when I set my text’s position, it appears offscreen.

    for example:

    game.font.draw(game.batch, “Loading…”, 0, 0);

    It appears too low for the screen..

Leave a Reply

Your email address will not be published.