Text Rendering in Games
Most game engines I’m aware of use some form of texture atlas to draw text segments via OpenGL or some equivalent. For this approach, as a pre-processing step, individual characters are usually output to a larger atlas image, along with some meta-data such as the placement of each character, and information about spacing. Ideally, wasted space is minimized by packing the characters efficiently.
Some advantages to this approach include:
-
It’s fast. To visualize a string (or several of them), a buffer of triangles representing the characters can usually be drawn with a single call, referencing a single texture.
-
It’s simple. Using existing tools, the details of dealing with fonts and their myriad peculiarities can be avoided, essentially reducing the problem to just working with static “sprites.”
-
There’s no need to include the original font files, which many licenses disallow.
However, some disadvantages come along for the ride:
-
Because text is often rendered (by graphics applications, or libraries such as FreeType) in a way that’s uniquely suitable for a specific, requested size, scaling the character “sprites” later generally looks awful. One way around this is to pre-generate the same character data at multiple sizes.
-
Depending on localization needs, pre-generating atlases for a wide variety of character sets, each at multiple sizes, can quickly become unwieldy.
As with most things, the best approach depends on one’s needs. For cases where the disadvantages above weigh too heavily, another technique to consider would be something like what’s described in this article by Wolfire Games. The author suggests delaying text rendering as long as possible, and only rendering exactly what’s required using a library such as FreeType. So instead of using “sprites” for each character, you’d generate a single, full-sized “sprite” per text segment.
This approach is visually appealing (avoids the scaling problem), and potentially eliminates wasted memory on unused character data. Depending on how it’s used however, it can be a lot slower, both due to the actual character rendering, and pushing it out as a texture. I’ve implemented something similar in the past, and while the results were great for a decent range of things– particularly with some clever caching– the performance in some of my specific use cases with a lot of dynamic text didn’t quite cut it.
The approach I prefer at the moment is a bit of a hybrid (which is also hinted at in the above article.) Similar to above, I still use an atlas for the draw calls, but it doesn’t need to be fully pre-generated. If the text renderer encounters a character for a particular font family and size combination that it doesn’t already have cached, it renders it out to the texture atlas (using FreeType) at full resolution.
In C++-ish psuedo-code, this basically looks like:
void Text_renderer::write( ... )
{
...
for (int i=0; i<length; i++) {
Character c = str[i];
Character_info* char_info = find_character(font_settings, c);
if (char_info is missing) {
char_info = build_character(font_settings, c);
update_character_atlas(char_info);
}
// actually use char_info;
}
}
In the above example, Character_info holds the required information (placement, spacing, etc) to render any character that’s ever been encountered. The texture atlas gets updated as needed.
To minimize overhead, if the application knows some range of characters it definitely intends to render, it can always pre-generate those from the start. In fact, it allows for mixing fully pre-generated texture atlases with on-the-fly rendering, since the Character_info is where everything relevant eventually ends up, regardless of how or when it got there. This also addresses the case where the original font files can’t be distributed with the application.
Below is a screenshot of the OpenGL-rendered text using different fonts and sizes:
I hope the translations are accurate, the test phrase is supposed to say “One language is never enough.”
And below is what a corresponding texture atlas looks like, updated on the fly, as new characters of different styles and sizes are encountered. It would likely be much larger in a real application. For packing the rectangles, I implemented the algorithm described here.
While this system is a bit more complex than the fully pre-generated texture atlas approach– mostly in terms of book-keeping– I’ve been happy with both the performance and visual quality.