Advanced text rendering

If you know how to process fonts and render text onto the screen, it’s time to have a look at how to customise the way that it’s done. This will give a lot more flexibility in terms of how it is rendered so that you don’t get the same boring style of text everywhere.

After writing this page, I just realised that it’s only available in the unstable branch, so be sure to switch to that before using this library.

Text colour

The first thing to consider when customising your text is the colour. It renders as white text by default which naturally is going to look pretty bad on a white background.

To change it, we use this data structure alongside the rdpq_font_style() function.

typedef struct rdpq_fontstyle_s {
	color_t color; // Font body colour. Works on any font
	color_t outline_color; // Font outline colour. Remember to set the outline in mkfont!
} rdpq_fontstyle_t;

Here’s an example using two different colours:

// Outside the loop
rdpq_font_t *fnt1 = rdpq_font_load_builtin(FONT_BUILTIN_DEBUG_MONO);
rdpq_text_register_font(1, fnt1);

rdpq_font_style(fnt1, 0, &(rdpq_fontstyle_t){
	.color = RGBA32(255, 255, 255, 255),
	.outline_color = RGBA32(0, 0, 0, 255),
	});

rdpq_font_t *fnt2 = rdpq_font_load_builtin(FONT_BUILTIN_DEBUG_VAR);
rdpq_text_register_font(2, fnt2);

rdpq_font_style(fnt2, 0, &(rdpq_fontstyle_t){
	.color = RGBA32(0, 0, 0, 255),
	.outline_color = RGBA32(255, 255, 255, 255),
	});

// Inside the loop
rdpq_text_printf(
	NULL, 1, 20, 20,
	"This is some text using monospace font\nWhite colour, black outline"
	);

rdpq_text_printf(
	NULL, 2, 20, 50,
	"This is some text using variable width font\nBlack colour, white outline"
	);

Note: In this example I’m using two fonts with their colour set at the start, but it’s entirely possible to use rdpq_font_style() to set the colour of the font in the middle of the frame is you want to use the same font in two different colours in the same frame.

Using custom fonts

The default font might work fine for debugging or for some kind of retro-themed pixelart game, but generally you’re going to want to use your own font for displaying text on screen.

Creating your font64 file

Libdragon comes with a utility called mkfont. It’s a simple program that converts a .ttf, .bmf or .otf font file into a .font64 file that is usable by your Nintendo 64 project. The basic usage would be to have all your fonts in a /src/fonts/ folder in your source directory and add something like this to your Makefile:

FONTS_DIR=$(SOURCE_DIR)/fonts
FONTS_FILESYSTEM=$(FILESYSTEM_DIR)/fonts
FONTS_FILES=$(wildcard $(FONTS_DIR)/*.ttf)
FONTS_FONTS64=$(subst fonts,filesystem/fonts,$(FONTS_FILES:.ttf=.font64))

$(FONTS_FILESYSTEM)/%.font64: $(FONTS_DIR)/%.ttf
	@mkdir -p $(dir $@)
	@echo "    [FONT] $@"
	$(TOOLS_MKFONT) $(MKFONT_FLAGS) -o $(FONTS_FILESYSTEM) "$<"

Customising font64 with flags

There are some flags that you can add to mkfont to customise your .font64 file. These are only available for TTF and OTF formats.

-s/--size <pt> determines the size (in points) of the font. It will default to whatever the default size is for that particular font. You cannot change the size of the font on the fly, so this is where you can set it to what you want.

-r/--range <start-stop> is the range of unicode characters to convert. Useful if you have a very big font but are only going to use a limited amount of characters.

--monochrome is a flag to exclude any colours in the font (if any)

--outline <width> adds an outline of the specified width (in pixels). It allows fractional values.

--char-spacing <width> determines the spacing between characters. Defaults to zero.

Importing and displaying your font

Once you have the .font64 file added into your DFS, it’s time to load it into your program. This is done via the rdpq_font_load() function. All you need to do is input the font’s location in the DFS and it’ll load it up for you. Then you can use it just like the default fonts.

// At scene load
rdpq_font_t *fnt1 = rdpq_font_load("rom:/fonts/Pacifico.font64");
rdpq_text_register_font(1, fnt1);

// In the main loop
rdpq_text_printf(
	NULL, 1, 20, 50,
	"This is some text using Pacifico font"
	);

Unloading your font

Once you’re done using the font, you can free the font like this:

rdpq_font_free(fnt1);

Text settings

When rendering text, so far we’ve used the default settings. If you want to change the way text is displayed, you need to understand the rdpq_textparms_t data structure.

typedef struct rdpq_textparms_s {
	int16_t style_id;
	int16_t width;
	int16_t height;
	rdpq_align_t align;
	rdpq_valign_t valign;
	int16_t indent;
	int16_t char_spacing;
	int16_t line_spacing;
	rdpq_textwrap_t wrap;
	int16_t *tabstops;
	bool disable_aa_fix;
	bool preserve_overlap;
} rdpq_textparms_t;

Let’s go through them one by one.

int16_t style_id is the style ID (colour) that was defined by rdpq_font_style() earlier.

int16_t width and int16_t height define the dimensions of the textbox. If left at zero, the box is unbounded. Behaviour when text reaches the end is defined by .wrap (see below). Here is an example of the box width increasing every frame:

rdpq_text_printf(
	&(rdpq_textparms_t){
		.width = 50+(l%150),
		.height = 100,
	}, 1, 20, 20,
	"This is some text using Pacifico font"
	);
l++;

rdpq_align_t align and rdpq_valign_t valign set the text alignment of your text within the textbox. These are the options (you can also use the numbers 0-2 respectively:

  • ALIGN_LEFT
  • ALIGN_CENTER
  • ALIGN_RIGHT
  • VALIGN_TOP
  • VALIGN_CENTER
  • VALIGN_BOTTOM
rdpq_text_printf(
	&(rdpq_textparms_t){
		.width = 280,
		.height = 200,
		.align  = l/64 % 3,
		.valign  = l/192 % 3,
	}, 1, 20, 20,
	"Some text"
	);

int16_t indent determines how many pixels to indent the first line of a piece of text. It’s best to use this with some kind of word wrapping.

rdpq_text_printf(
	&(rdpq_textparms_t){
		.width = 280,
		.indent = l%100,
		.wrap = WRAP_WORD,
	}, 1, 20, 20,
	"Some text to show how indentation works in libdragon"
	);

int16_t char_spacing and int16_t line_spacing denote the amount of space between characters and lines respectively. Negative values are allowed and will make them closer together or even reverse their order.

rdpq_text_printf(
	&(rdpq_textparms_t){
		.width = 280,
		.wrap = WRAP_WORD,
		.char_spacing = l/5%10
	}, 1, 20, 20,
	"Some text to show how character spacing works in libdragon"
	);

rdpq_text_printf(
	&(rdpq_textparms_t){
		.width = 280,
		.wrap = WRAP_WORD,
		.line_spacing = l/5%10
	}, 1, 20, 100,
	"Some long text to show how line spacing works in libdragon"
	);

rdpq_textwrap_t wrap determines the wrap type. By default, the text finishes at the end of the text box or the screen, but that can be changed by amending this parameter to one of these options:

  • WRAP_NONE
  • WRAP_ELLIPSES
  • WRAP_CHAR
  • WRAP_WORD
Source
rdpq_text_printf(
	&(rdpq_textparms_t){
		.width = 280,
		.wrap = WRAP_NONE,
	}, 1, 20, 20,
	"Some text to show how WRAP_NONE text wrap works in libdragon"
	);

rdpq_text_printf(
	&(rdpq_textparms_t){
		.width = 280,
		.wrap = WRAP_ELLIPSES,
	}, 1, 20, 50,
	"Some text to show how WRAP_ELLIPSES text wrap works in libdragon"
	);

rdpq_text_printf(
	&(rdpq_textparms_t){
		.width = 280,
		.wrap = WRAP_CHAR,
	}, 1, 20, 80,
	"Some text to show how WRAP_CHAR text wrap works in libdragon"
	);

rdpq_text_printf(
	&(rdpq_textparms_t){
		.width = 280,
		.wrap = WRAP_WORD,
	}, 1, 20, 110,
	"Some text to show how WRAP_WORD text wrap works in libdragon"
	);

int16_t *tabstops is an array that determines the location where each tab stops and starts. These are X coordinates from the left of the display, not the start of the text or the previous tab. Any undefined tabstops will default to zero.

// Outside the main loop
int16_t tabs[] = {20,0,0,0,0,0};

// Inside the main loop
// Spread the tabs a little bit
for (int i=1; i<6; i++) {
	tabs[i] = tabs[i-1] + 10 + (l/5%20);
}

// Tabbed text
rdpq_text_printf(
	&(rdpq_textparms_t){
		.width = 280,
		.tabstops = tabs,
	}, 1, 20, 20,
	"0\t10\t20\t30\t40\t50\t60\t70\t80"
	);

// Untabbed text
rdpq_text_printf(
	&(rdpq_textparms_t){
		.width = 280,
	}, 1, 20, 40,
	"0\t10\t20\t30\t40\t50\t60\t70\t80"
	);

bool disable_aa_fix disables an anti-aliasing fix, which improves performance a little bit. bool preserve_overlap forces overlapping glyphs to display properly, but takes longer to render. I couldn’t manage to get these two parameters to affect anything though, so it’s best to leave them at default for best performance.

Printing methods

So far, we’ve been using the rdpq_text_printf() function to render text to the frame buffer. However there are a few different functions that can be used depending on your use case. They all take the same base of rdpq_text_print() and expand on it.

rdpq_text_print() is the simplest use case. It basically takes an array of UTF8-formatted text and prints it out to screen as-is. Here’s the parameter breakdown:

rdpq_textmetrics_t rdpq_text_print(
	const rdpq_textparms_t *parms, // Text parameters as described above
	uint8_t font_id, // ID of the font being used
	float x0, // X coordinate
	float y0, // Y coordinate
	const char *utf8_text // UTF8-encoded text string
	);

rdpq_text_printn() is the same as the function above, but adds another parameter to limit the number of characters that are printed. In fact, print is just a wrapper for printn where the number of characters is the full length of the string.

rdpq_textmetrics_t rdpq_text_printn(
    const rdpq_textparms_t *parms, uint8_t font_id, float x0, float y0, const char *utf8_text,
	int nbytes // The number of bytes (not characters) to print out
	);

rdpq_text_printf() works similarly to the printf() function in C’s standard stdio.h. Instead of a plain string of characters, it takes in a format string with variable placeholders which are then filled by the later arguments.

rdpq_textmetrics_t rdpq_text_printf(
	const rdpq_textparms_t *parms, uint8_t font_id, float x0, float y0,
	const char *utf8_fmt, // Format string
	... // Variables used in the format string
	);

rdpq_text_vprintf() uses a va_list to enable a variable amount of arguments in a custom function that you make. Have a look at vprintf() in the standard C library to see it in use.

rdpq_textmetrics_t rdpq_text_vprintf(
	const rdpq_textparms_t *parms, uint8_t font_id, float x0, float y0, 
	const char *utf8_fmt, // Format string
	va_list va // Variable argument list data type
	);

Here’s an example in action:

Source
// Auxiliary vprintf function
void my_print_function(const char *format, ...) {
	va_list args;
	va_start(args, format);
	rdpq_text_vprintf(
		NULL, 1, 20.0, 80.0,
		format, // Format string
		args // Variable argument list data type
		);
	va_end(args);
}

// Inside the main loop
// Regular text print
rdpq_text_print(
	NULL, 1, 20, 20,
	"rdpq_text_print: Print some plain text"
	);

// Truncated text print
rdpq_text_printn(
	NULL, 1, 20, 40,
	"rdpq_text_printn: Print some truncated text",
	40
	);

// Formatted text
rdpq_text_printf(
	NULL, 1, 20, 60,
	"rdpq_text_printf: %s",
	"Print some formatted text"
	);

// Custom text print function
my_print_function(
	"rdpq_text_vprintf: %s",
	"Print some va_list text"
);

Switching fonts within a string

It is possible to change the way that fonts work mid-print in Libdragon by using a few escape codes.

  • $xx to switch fonts
  • ^xx to switch font styles

Here’s an example. Note that this is all done in one contiguous string.

Source
// Registering the fonts
rdpq_font_t *fnt1 = rdpq_font_load_builtin(FONT_BUILTIN_DEBUG_MONO);
rdpq_font_t *fnt2 = rdpq_font_load("rom:/fonts/Pacifico.font64");
rdpq_text_register_font(1, fnt1);
rdpq_text_register_font(2, fnt2);

// Register the styles
rdpq_font_style(fnt1, 0, &(rdpq_fontstyle_t){
	.color = RGBA32(255, 255, 255, 255),
});
rdpq_font_style(fnt1, 1, &(rdpq_fontstyle_t){
.color = RGBA32(255, 0, 0, 255),
});
rdpq_font_style(fnt1, 2, &(rdpq_fontstyle_t){
	.color = RGBA32(0, 255, 0, 255),
});
rdpq_font_style(fnt1, 3, &(rdpq_fontstyle_t){
	.color = RGBA32(0, 0, 255, 255),
});
rdpq_font_style(fnt1, 4, &(rdpq_fontstyle_t){
	.color = RGBA32(0, 0, 0, 255),
	.outline_color = RGBA32(255, 255, 255, 255),
});
rdpq_font_style(fnt2, 5, &(rdpq_fontstyle_t){
	.color = RGBA32(128, 0, 128, 255),
});

// Inside the main loop

// Print the formatted text
rdpq_text_print(
	&(rdpq_textparms_t){
		.width = 280,
		.wrap = WRAP_WORD,
	}, 1, 20, 20,
	"Now we're white, ^01and now we're red! "
	"^02Why not some green, ^03or some blue? "
	"^04Or inverted is fine too! $02How about"
	"a different font,^05 in a different colour?"
	);

Text metrics

You might have noticed that all of the text printing functions return a rdpq_textmetrics_t data structure. It’s meant to tell you what happened after the text was written in the sense of how much text was written.

typedef struct rdpq_textmetrics_t {
	float advance_x;                ///< X pen advance after rendering the text 
	float advance_y;                ///< Y pen advance after rendering the text
	int utf8_text_advance;          ///< Number of bytes rendered in the UTF-8 text
	int nlines;                     ///< Number of lines rendered (including wrapped lines)
} rdpq_textmetrics_t;

In most cases it’s not necessary, but it can be useful when printing twice in the same paragraph, or if you want to place a sprite at the end of a text.

Search

Subscribe to the mailing list

Follow N64 Squid

  • RSS Feed
  • YouTube

Random featured posts