Nintendo 64 Part 9: Fonts and Drawing Text
Text is not just an important part of most games, but it’s also useful for debugging, since we can print debugging messages to the screen. In this post I’m going to convert a TTF font to a bitmap, pack the resulting glyphs into a texture usable by the Nintendo 64, and write code to draw text on-screen as a collection of sprites.
I’ve done this for games before, so I dug through source code from various old projects, copied it into this project, and modified it to meet my needs for Nintendo 64 development.
Rasterizing Outline Fonts
We could rasterize fonts on the Nintendo 64. It’s certainly powerful enough, and not only is the FreeType library very portable, but FreeType can be configured to build a very minimal version that only contains the features you need. We’re not going to do that, we’re going to convert the fonts to raster images during the build process. The first step is to just convert normal font files (like TrueType or OpenType files) into bitmaps.
Most of my tooling is in Go, but font rasterization itself is mostly just FreeType library calls, so I wrote a small font rasterizer program in C. The program basically just parses command-line options, reads a font file with FreeType, rasterizes the glyphs, and prints the image data to standard output.
Here’s what happens when you run the font rasterizer program:
$ bazel run :raster -- rasterize \ -font=/Library/Fonts/Arial.ttf -size=12 char 32 3 char 33 4 char 34 5 ... char 65532 863 glyph 7 8 1 8 9 .notdef 273030303030273000000000... glyph 0 0 0 0 0 .null - glyph 0 0 0 0 3 nonmarkingreturn - glyph 0 0 0 0 3 space - glyph 2 9 1 9 3 exclam F858F858F050E040CF30BE20A... ...
It’s very ugly output, but it’s designed to be consumed by another program.
Packing Glyphs Into Textures
To cut down on the number of different textures we need to use, I’ll take these glyph bitmaps and pack them into a smaller number of textures. Nintendo 64 texture memory is very limited (only 4 KiB), so if you want to use a format with 4 bits per pixel (the smallest pixel format the system supports), you just get a single 128×64 pixel texture, or a texture with a similar size like 96×85. If I want to add a drop shadow, I’ll probably have to use an 8-bit pixel format, which cuts the available space in half.
I’ll make this work by doing three things:
- Use a rectangle packing algorithm to find an efficient way to pack many glyphs into a single texture.
- Omit glyphs from the font that I don’t need in my game.
- Use multiple textures as necessary.
The rectangle packing algorithm is something that I’ve done before in other projects, so I just ported my rectangle packing code to Go, write some simple tests, and modify it so the rectangles can be split across multiple bins.
The reference I used to write the algorithm is A thousand ways to pack the bin—a practical approach to two-dimensional rectangle bin packing by Jukka Jylänki in 2010 [CiteSeerX]. The algorithm I used is the MAXRECTS algorithm mentioned in the paper. It’s slower than the other algorithms, but since we’re not packing many rectangles, it doesn’t matter.
I like the Grenze Gotisch font, so I’m using it for this test. This command will create two images: one with the glyphs laid out on a grid, and one with the glyphs packed efficiently into a 96×85 texture:
$ bazel run :font -- \ -font path/to/GrenzeGotisch-Medium.ttf \ -size 20 \ -out-grid $PWD/grid.png \ -texture-size 96:85 \ -charset 20,41-5a,61-7a,21,2c,2e,3a,2018,2019,201c,201d \ -out-texture $PWD/texture.png \ -remove-notdef
Here are the results:
Creating a Font Format
My tool spits out a fairly simple custom font format with three sections in it:
- The character map, which is just a list of (character, glyph) pairs.
- The glyph data, which contains the coordinates for all of the glyph images and the glyph advance.
- The texture data.
// Header at the beginning of a font file.
struct font_header {
uint16_t charmap_size; // Number of font_char.
uint16_t glyph_count; // Number of font_glyph.
uint16_t texture_count; // Number of font_texture.
};
// Character map entry.
struct font_char {
uint16_t codepoint; // Unicode code point.
uint16_t glyph; // Glyph index.
};
// Information about a glyph.
struct font_glyph {
uint8_t size[2]; // Size in pixels.
int8_t offset[2]; // Sprite offset from drawing position.
uint8_t pos[2]; // Sprite location in texture.
uint8_t texindex; // Which texture.
uint8_t advance; // How far to move after drawing.
};
// Data for a single texture.
struct font_texture {
uint16_t width;
uint16_t height;
uint8_t pixels[];
};
The font file with 20 pixel height ends up being under 5 KB in size, so we could easily include much larger fonts or more fonts.
Drawing Text
Drawing a single line of text like this is fairly easy, so there’s not much code in the engine itself. Basically, the code keeps track of the current (x,y) position, and each time it draws a character it moves a little bit forward.
In the future, I’d like to at least support hard and soft line breaks.
Community Comments
Some comments from the Discord:
A: Looking through your articles I have one question
A: Why do you do this to yourself
A: Really cool stuff there, but if you ever need counseling, I know some people
Me: Isn’t everyone here doing this, on some level, because it’s difficult?
A: Touché 😛
It’s not like I love the Nintendo 64 so much that I’ll go through all these difficulties just to make a game. My nostalgia for the Nintendo 64 is pretty limited since I never owned a Nintendo 64 system, so I only got to play them at places like Blockbuster Video or watch my college roommates play Mario Kart 64.
No, the reason I’m doing this is because I like the challenge and I like the Nintendo 64 homebrew community.
B: Read your blog entries, [Dietrich]. Inspiring, but also reaffirms to me that I don't know anywhere near enough to do this without all the prior work others in the community have done.
I’m definitely not able to do this without a lot of help, too. We’re able to accomplish so much because there are so many people supporting us.
Here are some of the people that have helped me (definitely not a comprehensive list):
- CrashOveride95, who I cribbed a bunch of the build system work from.
- People in the N64Brew Discord chat.
- Tyler Rhodes, who wrote
open-source alternatives to
mild
andmakerom
. - Adam Gashlin, who wrote open-source IPL3 code alternative.
- The CEN64 developers like Tyler Stachecki, who made an emulator which is useful for development and not just playing games.
- Countless people on forums over the years, like “Trevor”, who take the time to document what they learn about Nintendo 64 development in posts like, N64 Textures and TMEM, which explains how different texture parameters work.
- Contributors to the OSDev Wiki.