Home » Community » Forums » Graphics Programming and Theory » Quick tutorial: Variable width bitmap fonts
Intel sponsors gamedev.net search:
Control Panel Register Bookmarks Who's Online Active Topics Stats FAQ Search

Add Forum to Favorites |  Send Topic To a Friend | View Forum FAQ | Track this topic


 Last Thread Next Thread 
 Quick tutorial: Variable width bitmap fonts
Post New Topic  Post Reply 


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;
	unsigned short XOffset, YOffset;
	unsigned short 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/06] 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.

[Edited by - Promit on May 2, 2006 6:55:41 PM]

 User Rating: 1898   |  Rate This User  Send Private MessageView ProfileView Journal Report this Post to a Moderator | Link

Why not submit it for a sweet snippet?

 User Rating: 1015    Report this Post to a Moderator | Link

Quote:
Original post by Anonymous Poster
Why not submit it for a sweet snippet?


The turnover time between submission and posting is quite long. Besides, in a week or two it'll be available in a different, as-of-yet unannounced venue.

 User Rating: 1898   |  Rate This User  Send Private MessageView ProfileView Journal Report this Post to a Moderator | Link

I love AngelCode's bitmap font format. Your tutorial will open up a few eyes I am sure. Nice work.

....
Brent Gunning | My Journal | My Facebook | My Twitter

 User Rating: 1430   |  Rate This User  Send Private MessageView ProfileView Journal Report this Post to a Moderator | Link

Maybe I'm missing something but is there any reason that your not doing any kerning? With out kerning being done certian letter pairs will always look a little odd for example "LI" can look like "L I" with out the propper kernining values or they can also appear to be too close to eachother. Kerning values are something you should be able to extract from the true type font on the export.

It's something you may want to consider doing along with a little bit of leding to adjust the space between multiple lines of text so that it's readable.


 User Rating: 1041   |  Rate This User  Send Private MessageView Profile Report this Post to a Moderator | Link

Kerning information and line height is exported from BMFont and stored in the .fnt file. Promit already handles kerning and line height would only be an issue with multiple lines, which is fairly simple to implement, but not necessary for most non-rpg games.

....
Brent Gunning | My Journal | My Facebook | My Twitter

 User Rating: 1430   |  Rate This User  Send Private MessageView ProfileView Journal Report this Post to a Moderator | Link

All times are ET (US)

Post Reply
 Last Thread Next Thread 
Forum Rules:
You may not post new threads
You may post replies
You may not edit your posts
You may not use HTML in your posts
Jump To:
Administrative Options: