The purpose of this thread is simple -- it's a really fast and easy introduction to creating nice rendered, variable width bitmap fonts.
The basis of our fonts is AngelCode's Bitmap Font Generator (BMFont). This utility can create variable width bitmap fonts, along with ASCII files that describe the characters' properties. I suggest you toy around with this program a little first, and get a feel for how it works. Create a test font or two. The only catch is, make sure that you keep your font to only one texture page, as the code I'm about to provide doesn't cope with multiple pages (mainly because I want to have a single draw call per string rendered).
When you save a font, BMFont generates two files, a targa texture and a FNT file that describes the font. I usually save 32 bit targas (which are exported with an alpha channel) and then store them to PNG, which compress very well. If you're using the targa natively, you might be better served exporting in 8 bit and computing a color key when you load the image. You can also compress the image using DXT/S3TC, either on disk or in video memory1.
Anyway, let's take a look a segment of the FNT file that BMFont generates.
common lineHeight=32 base=25 scaleW=256 scaleH=256 pages=1
char id=0 x=149 y=113 width=9 height=19 xoffset=1 yoffset=7 xadvance=11 page=0
char id=1 x=159 y=112 width=9 height=19 xoffset=1 yoffset=7 xadvance=11 page=0
char id=2 x=169 y=112 width=9 height=19 xoffset=1 yoffset=7 xadvance=11 page=0
char id=3 x=179 y=112 width=9 height=19 xoffset=1 yoffset=7 xadvance=11 page=0
char id=4 x=189 y=112 width=9 height=19 xoffset=1 yoffset=7 xadvance=11 page=0
char id=5 x=199 y=110 width=9 height=19 xoffset=1 yoffset=7 xadvance=11 page=0
We need to parse this ASCII file into our app. First off, let's set up some data structures to deal with this information. Notice that the first line has properties common to the entire character set, while the rest of the lines have information specific to a single character. So here are our structures:
struct CharDescriptor
{
//clean 16 bytes
unsigned short x, y;
unsigned short Width, Height;
float XOffset, YOffset;
float XAdvance;
unsigned short Page;
CharDescriptor() : x( 0 ), y( 0 ), Width( 0 ), Height( 0 ), XOffset( 0 ), YOffset( 0 ),
XAdvance( 0 ), Page( 0 )
{ }
};
struct Charset
{
unsigned short LineHeight;
unsigned short Base;
unsigned short Width, Height;
unsigned short Pages;
CharDescriptor Chars[256];
};
This is pretty straightforward. CharDescriptor holds the information for a single character; Charset holds the descriptions that apply to all characters, as well as the descriptors for every character. You may have noticed that BMFont allows you to generate less than a complete set of ASCII letters, but the character array here is still 256. We'll be using the ASCII values of characters to index into that array. The CharDescriptor constructor insures that if a character is used that has no representation, nothing will be drawn.
Writing the actual parser is fairly tedious, and not something I want to discuss here. I'm going to post my C++ code for parsing the FNT file; you are free to use that code, or write your own.
bool Font::ParseFont( std::istream& Stream, Font::Charset& CharsetDesc )
{
String Line;
String Read, Key, Value;
std::size_t i;
while( !Stream.eof() )
{
std::stringstream LineStream;
std::getline( Stream, Line );
LineStream << Line;
//read the line's type
LineStream >> Read;
if( Read == "common" )
{
//this holds common data
while( !LineStream.eof() )
{
std::stringstream Converter;
LineStream >> Read;
i = Read.find( '=' );
Key = Read.substr( 0, i );
Value = Read.substr( i + 1 );
//assign the correct value
Converter << Value;
if( Key == "lineHeight" )
Converter >> CharsetDesc.LineHeight;
else if( Key == "base" )
Converter >> CharsetDesc.Base;
else if( Key == "scaleW" )
Converter >> CharsetDesc.Width;
else if( Key == "scaleH" )
Converter >> CharsetDesc.Height;
else if( Key == "pages" )
Converter >> CharsetDesc.Pages;
}
}
else if( Read == "char" )
{
//this is data for a specific char
unsigned short CharID = 0;
while( !LineStream.eof() )
{
std::stringstream Converter;
LineStream >> Read;
i = Read.find( '=' );
Key = Read.substr( 0, i );
Value = Read.substr( i + 1 );
//assign the correct value
Converter << Value;
if( Key == "id" )
Converter >> CharID;
else if( Key == "x" )
Converter >> CharsetDesc.Chars[CharID].x;
else if( Key == "y" )
Converter >> CharsetDesc.Chars[CharID].y;
else if( Key == "width" )
Converter >> CharsetDesc.Chars[CharID].Width;
else if( Key == "height" )
Converter >> CharsetDesc.Chars[CharID].Height;
else if( Key == "xoffset" )
Converter >> CharsetDesc.Chars[CharID].XOffset;
else if( Key == "yoffset" )
Converter >> CharsetDesc.Chars[CharID].YOffset;
else if( Key == "xadvance" )
Converter >> CharsetDesc.Chars[CharID].XAdvance;
else if( Key == "page" )
Converter >> CharsetDesc.Chars[CharID].Page;
}
}
}
return true;
}
So far, so good. After the parser does its thing, we have a complete, simple representation of the character details in memory. Now comes rendering. In order to render, I define a max length per string drawn (say, MAX_CHARS), and create a dynamic vertex buffer of that size. Every time a string is drawn, we lock the vertex buffer and compute all of the vertices, filling them into the buffer. We then render that buffer in one go. Note: It is important to lock the vertex buffer with the discard flag! If you do not, the pipeline will stall if you use the same font twice in a row. That goes for OpenGL/VBO too.2
How do we compute the vertices? Well, consider the psuedocode on AngelCode's page:
// Compute the source rect
Rect src;
src.left = ch.x;
src.top = ch.y;
src.right = ch.x + ch.width;
src.bottom = ch.y + ch.height;
// Compute the destination rect
Rect dst;
dst.left = cursor.x + ch.xoffset;
dst.top = cursor.y + ch.yoffset;
dst.right = dst.left + ch.width;
dst.bottom = dst.top + ch.height;
// Draw the image from the right texture
DrawRect(ch.page, src, dst);
// Update the position
cursor.x += ch.xadvance;
Their source rect will form our texture coordinates, and their destination rect will form our vertex coordinates3. We'll also have to convert source rect into [0,1] texture coordinate space. One last little detail: Notice that there is a virtual cursor position that is incremented, and that the increment value is not the same as the letter width. If you try to increment by the letter width, your letters will be smashed together. The rest is pure, simple arithmetic. This code generates clockwise OpenGL quads; in D3D it's a simple matter of copying the appropriate vertices to form a triangle list.
for( unsigned int i = 0; i < Str.size(); ++i )
{
CharX = m_Charset.Chars[Str[i]].x;
CharY = m_Charset.Chars[Str[i]].y;
Width = m_Charset.Chars[Str[i]].Width;
Height = m_Charset.Chars[Str[i]].Height;
OffsetX = m_Charset.Chars[Str[i]].XOffset;
OffsetY = m_Charset.Chars[Str[i]].YOffset;
//upper left
Verts[i*4].tu = (float) CharX / (float) m_Charset.Width;
Verts[i*4].tv = (float) CharY / (float) m_Charset.Height;
Verts[i*4].x = (float) CurX + OffsetX;
Verts[i*4].y = (float) OffsetY;
//upper right
Verts[i*4+1].tu = (float) (CharX+Width) / (float) m_Charset.Width;
Verts[i*4+1].tv = (float) CharY / (float) m_Charset.Height;
Verts[i*4+1].x = (float) Width + CurX + OffsetX;
Verts[i*4+1].y = (float) OffsetY;
//lower right
Verts[i*4+2].tu = (float) (CharX+Width) / (float) m_Charset.Width;
Verts[i*4+2].tv = (float) (CharY+Height) / (float) m_Charset.Height;
Verts[i*4+2].x = (float) Width + CurX + OffsetX;
Verts[i*4+2].y = (float) Height + OffsetY;
//lower left
Verts[i*4+3].tu = (float) CharX / (float) m_Charset.Width;
Verts[i*4+3].tv = (float) (CharY+Height) / (float) m_Charset.Height;
Verts[i*4+3].x = (float) CurX + OffsetX;
Verts[i*4+3].y = (float) Height + OffsetY;
CurX += m_Charset.Chars[Str[i]].XAdvance;
}
Keep in mind the Verts is not a system memory array; it's the pointer returned from locking the vertex buffer (glMapBuffer, IDirect3DVertexBuffer9::Lock), which has been cast to a pointer to a custom vertex structure4.
Lastly, we draw the vertex buffer (glDrawArrays, IDirect3DDevice9::DrawPrimitive). The OGL vertex count will be the length of the string times 4, and the D3D primitive count will be the number of characters times 2. ind the texture for the font (this code only supports one page, remember) and set up alpha blending (source = source alpha, dest = one minus source alpha). For coloration, set up your texture units to modulate against a constant color. Our bitmap stores the fonts as white, so it'll take on the color of whatever constant you modulate against. And that's it, really. Beautiful, variable width, nicely rendered fonts, without any major hassles.
Notes:
1) I strongly suggest you use DXT/S3TC on the texture in video memory. However, it is critically important to use DXT3. DXT1 will mangle your fonts, as it does not cope well with sharp changes in alpha.
2) To discard a vertex buffer in OpenGL, call BufferData with the same size, but a NULL pointer. Then call MapBuffer.
3) You'll notice that the vertex coordinates here are in pixels. In order to render this correctly, you'll need to define an orthographic projection such that one unit corresponds to one pixel.
4) struct FontVertex { float x, y, tu, tv; };
So, questions/comments?
[EDIT 5/2/2006] It just came to my attention that this tutorial is a lot more well known than I thought. So I revised a few parser bugs that were pointed out to me but I never fixed in the posted code.
[EDIT 1/15/2013] Still trucking! Changed some unsigned shorts to floats to support signed values and especially signed distance field rendering.