Bitmap Fonts
Last Updated: Aug 9th, 2012
OpenGL has no built in functions to render text. Fortunately, we use a sprite sheet to render text images.
A bitmap font is simply a specialized sprite sheet.
If we wanted to make "HELLO" show on the screen we would have these sprites:
Then we would take the "H" sprite, the "E" sprite, two of the "L" sprite, and finally the "O" sprite and show them on the screen one after the other.
Now that you know the basic concept, it's time to make a bitmap fonting engine.
If we wanted to make "HELLO" show on the screen we would have these sprites:
Then we would take the "H" sprite, the "E" sprite, two of the "L" sprite, and finally the "O" sprite and show them on the screen one after the other.
Now that you know the basic concept, it's time to make a bitmap fonting engine.
From LSpriteSheet.h
#include "LTexture.h" #include "LVertexData2D.h" #include <vector> //Sprite drawing origin enum LSpriteOrigin { LSPRITE_ORIGIN_CENTER, LSPRITE_ORIGIN_TOP_LEFT, LSPRITE_ORIGIN_BOTTOM_LEFT, LSPRITE_ORIGIN_TOP_RIGHT, LSPRITE_ORIGIN_BOTTOM_RIGHT };
Before we create our bitmap font, we're going to add a new feature to the LSpriteSheet class. In the previous tutorial, it had the textured quad render from the center. For the
bitmap we want to render each character sprite from the top left. So we're going to add the capability to choose the origin for the sprites.
From LSpriteSheet.h
bool generateDataBuffer( LSpriteOrigin origin = LSPRITE_ORIGIN_CENTER ); /* Pre Condition: -A loaded base LTexture -Clipping rectangles in clip array Post Condition: -Generates VBO and IBO to render sprites with -Sets given origin for each sprite -Returns true on success -Reports to console is an error occured Side Effects: -Member buffers are bound */
For the LSpriteSheet class, selecting the origin is only going to affect how the vertex data is generated.
As you can see, the default behavior for generating sprites is to have the origin be at the center of the sprite. This is what I personally experienced as being the most useful way to have them rendered.
As you can see, the default behavior for generating sprites is to have the origin be at the center of the sprite. This is what I personally experienced as being the most useful way to have them rendered.
From LSpriteSheet.cpp
bool LSpriteSheet::generateDataBuffer( LSpriteOrigin origin ) { //If there is a texture loaded and clips to make vertex data from if( getTextureID() != 0 && mClips.size() > 0 ) { //Allocate vertex data int totalSprites = mClips.size(); LVertexData2D* vertexData = new LVertexData2D[ totalSprites * 4 ]; mIndexBuffers = new GLuint[ totalSprites ]; //Allocate vertex data buffer name glGenBuffers( 1, &mVertexDataBuffer ); //Allocate index buffers names glGenBuffers( totalSprites, mIndexBuffers ); //Go through clips GLfloat tW = textureWidth(); GLfloat tH = textureHeight(); GLuint spriteIndices[ 4 ] = { 0, 0, 0, 0 }; //Origin variables GLfloat vTop = 0.f; GLfloat vBottom = 0.f; GLfloat vLeft = 0.f; GLfloat vRight = 0.f;
At the top of the generateDataBuffer() function, we generate the VBO and IBO data like before and get ready to parse the clipping rectanges.
This time we have some additional variables to set the vertex top/bottom/left/right.
This time we have some additional variables to set the vertex top/bottom/left/right.
From LSpriteSheet.cpp
for( int i = 0; i < totalSprites; ++i ) { //Initialize indices spriteIndices[ 0 ] = i * 4 + 0; spriteIndices[ 1 ] = i * 4 + 1; spriteIndices[ 2 ] = i * 4 + 2; spriteIndices[ 3 ] = i * 4 + 3; //Set origin switch( origin ) { case LSPRITE_ORIGIN_TOP_LEFT: vTop = 0.f; vBottom = mClips[ i ].h; vLeft = 0.f; vRight = mClips[ i ].w; break; case LSPRITE_ORIGIN_TOP_RIGHT: vTop = 0.f; vBottom = mClips[ i ].h; vLeft = -mClips[ i ].w; vRight = 0.f; break; case LSPRITE_ORIGIN_BOTTOM_LEFT: vTop = -mClips[ i ].h; vBottom = 0.f; vLeft = 0.f; vRight = mClips[ i ].w; break; case LSPRITE_ORIGIN_BOTTOM_RIGHT: vTop = -mClips[ i ].h; vBottom = 0.f; vLeft = -mClips[ i ].w; vRight = 0.f; break; //Also for LSPRITE_ORIGIN_CENTER default: vTop = -mClips[ i ].h / 2.f; vBottom = mClips[ i ].h / 2.f; vLeft = -mClips[ i ].w / 2.f; vRight = mClips[ i ].w / 2.f; break; }
After setting our indices, we calculate the top/bottom y offsets and the left/right x offsets for the vertex data. As you can see, this depends on which origin we pick.
From LSpriteSheet.cpp
//Top left vertexData[ spriteIndices[ 0 ] ].position.x = vLeft; vertexData[ spriteIndices[ 0 ] ].position.y = vTop; vertexData[ spriteIndices[ 0 ] ].texCoord.s = (mClips[ i ].x) / tW; vertexData[ spriteIndices[ 0 ] ].texCoord.t = (mClips[ i ].y) / tH; //Top right vertexData[ spriteIndices[ 1 ] ].position.x = vRight; vertexData[ spriteIndices[ 1 ] ].position.y = vTop; vertexData[ spriteIndices[ 1 ] ].texCoord.s = (mClips[ i ].x + mClips[ i ].w) / tW; vertexData[ spriteIndices[ 1 ] ].texCoord.t = (mClips[ i ].y) / tH; //Bottom right vertexData[ spriteIndices[ 2 ] ].position.x = vRight; vertexData[ spriteIndices[ 2 ] ].position.y = vBottom; vertexData[ spriteIndices[ 2 ] ].texCoord.s = (mClips[ i ].x + mClips[ i ].w) / tW; vertexData[ spriteIndices[ 2 ] ].texCoord.t = (mClips[ i ].y + mClips[ i ].h) / tH; //Bottom left vertexData[ spriteIndices[ 3 ] ].position.x = vLeft; vertexData[ spriteIndices[ 3 ] ].position.y = vBottom; vertexData[ spriteIndices[ 3 ] ].texCoord.s = (mClips[ i ].x) / tW; vertexData[ spriteIndices[ 3 ] ].texCoord.t = (mClips[ i ].y + mClips[ i ].h) / tH; //Bind sprite index buffer data glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, mIndexBuffers[ i ] ); glBufferData( GL_ELEMENT_ARRAY_BUFFER, 4 * sizeof(GLuint), spriteIndices, GL_STATIC_DRAW ); }
Next we set our vertex/texture coordinates and index buffer per sprite like we did before. Notice how the texture coordinates didn't change. We're still mapping the same
portion of the texture no matter what our origin is. What's changing is how we place the vertex positions when rendering.
From LSpriteSheet.cpp
//Bind vertex data glBindBuffer( GL_ARRAY_BUFFER, mVertexDataBuffer ); glBufferData( GL_ARRAY_BUFFER, totalSprites * 4 * sizeof(LVertexData2D), vertexData, GL_STATIC_DRAW ); //Deallocate vertex data delete[] vertexData; } //No clips else { if( getTextureID() == 0 ) { printf( "No texture to render with!\n" ); } if( mClips.size() <= 0 ) { printf( "No clips to generate vertex data from!\n" ); } return false; } return true; }
Finally, we finish generating our VBO and report any errors if we need to.
From LFont.h
#include "LSpriteSheet.h" class LFont : private LSpriteSheet { public: LFont(); /* Pre Condition: -None Post Condition: -Initializes spacing variables Side Effects: -None */ ~LFont(); /* Pre Condition: -None Post Condition: -Frees font Side Effects: -None */ bool loadBitmap( std::string path ); /* Pre Condition: -None Post Condition: -Loads bitmap font -Returns true on success -Reports errors to console Side Effects: -None */ void freeFont(); /* Pre Condition: -None Post Condition: -Deallocates texture and sprite sheet data Side Effects: -None */ void renderText( GLfloat x, GLfloat y, std::string text ); /* Pre Condition: -A loaded font Post Condition: -Renders text Side Effects: -Binds member texture and data buffers */ private: //Spacing variables GLfloat mSpace; GLfloat mLineHeight; GLfloat mNewLine; };
With the LSpriteSheet class augmented, let's go over the LFont class. This class inherits from LSpriteSheet (because a bitmap font is a sprite sheet) privately because we don't
want anybody messing with the sprite sheet internals.
First we have our usual constructor/destructor. Then we have loadBitmap() which loads an image to parse it for text sprites and turn it into a texture. freeFont() deallocates the font, and renderText() renders text with the font.
Our spacing variables include "mSpace" which controls how much to move when a ' ' is encountered, "mLineHeight" is the distance between the highest text pixel and the lowest text pixel, and "mNewLine" is how much to move downward when a '\n' is encountered in a string.
First we have our usual constructor/destructor. Then we have loadBitmap() which loads an image to parse it for text sprites and turn it into a texture. freeFont() deallocates the font, and renderText() renders text with the font.
Our spacing variables include "mSpace" which controls how much to move when a ' ' is encountered, "mLineHeight" is the distance between the highest text pixel and the lowest text pixel, and "mNewLine" is how much to move downward when a '\n' is encountered in a string.
From LFont.cpp
#include "LFont.h" LFont::LFont() { mSpace = 0.f; mLineHeight = 0.f; mNewLine = 0.f; } LFont::~LFont() { //Deallocate font freeFont(); }
I'm sure you can figure out what the constructor and destructor do.
From LFont.cpp
bool LFont::loadBitmap( std::string path ) { //Loading flag bool success = true; //Background pixel const GLuint BLACK_PIXEL = 0xFF000000; //Get rid of the font if it exists freeFont(); //Image pixels loaded if( loadPixelsFromFile( path ) ) {
An important thing to note is that the loadBitmap() function expects a path to a bitmap font image that only has black, white and shades of grey which black being the background
color. Any other type of bitmap font won't work with this parser. Fortunately these types of bitmap fonts are pretty standard.
First we want to deallocate any previously loaded fonts. Then we load the bitmap font's pixels so we can start parsing them.
First we want to deallocate any previously loaded fonts. Then we load the bitmap font's pixels so we can start parsing them.
From LFont.cpp
//Get cell dimensions GLfloat cellW = imageWidth() / 16.f; GLfloat cellH = imageHeight() / 16.f; //Get letter top and bottom GLuint top = cellH; GLuint bottom = 0; GLuint aBottom = 0; //Current pixel coordinates int pX = 0; int pY = 0; //Base cell offsets int bX = 0; int bY = 0; //Begin parsing bitmap font GLuint currentChar = 0; LFRect nextClip = { 0.f, 0.f, cellW, cellH }; //Go through cell rows for( unsigned int rows = 0; rows < 16; rows++ ) { //Go through each cell column in the row for( unsigned int cols = 0; cols < 16; cols++ ) {
Our bitmap font parser also assumes we have our character sprites arranged in a 16x16 grid in ASCII order (gridlines are superimposed, not in the actual file):
First thing we do after loading the pixels is divide the image width/height by 16 so we know what the dimensions are for each individual cell on the grid. Then we have the variables "top", "bottom", and "aBottom" which we'll use to calculate our spacing variables.
Since we're going to be going through the pixels of the bitmap, we have "pX"/"pY" for pixel coordinates. "bX"/"bY" are used to keep track of the pixel coordinates for the cell we are currently parsing.
"currentChar" keeps track of the ASCII value for the current cell we're parsing, and "nextClip" is the clipping rectanges for the character sprite we're currently parsing.
With all this ready, it's time to go through the cells row by row and column by column.
First thing we do after loading the pixels is divide the image width/height by 16 so we know what the dimensions are for each individual cell on the grid. Then we have the variables "top", "bottom", and "aBottom" which we'll use to calculate our spacing variables.
Since we're going to be going through the pixels of the bitmap, we have "pX"/"pY" for pixel coordinates. "bX"/"bY" are used to keep track of the pixel coordinates for the cell we are currently parsing.
"currentChar" keeps track of the ASCII value for the current cell we're parsing, and "nextClip" is the clipping rectanges for the character sprite we're currently parsing.
With all this ready, it's time to go through the cells row by row and column by column.
From LFont.cpp
//Begin cell parsing //Set base offsets bX = cellW * cols; bY = cellH * rows; //Initialize clip nextClip.x = cellW * cols; nextClip.y = cellH * rows; nextClip.w = cellW; nextClip.h = cellH;
Before we start going through the cell pixels, we calculate the base pixel offsets for the cell and initialize the next clipping rectangle to be the whole cell.
From LFont.cpp
//Find left side of character for( int pCol = 0; pCol < cellW; ++pCol ) { for( int pRow = 0; pRow < cellH; ++pRow ) { //Set pixel offset pX = bX + pCol; pY = bY + pRow; //Non-background pixel found if( getPixel32( pX, pY ) != BLACK_PIXEL ) { //Set sprite's x offset nextClip.x = pX; //Break the loops pCol = cellW; pRow = cellH; } } }
What we're doing here is going through the cell pixel column by column and row by row until we find the first pixel that isn't a background pixel:
When that pixel is found, we know we found the x offset for the character sprite.
When constructing your bitmap fonts, remember that the pixel values have to be exact. A background pixel value of R000G000B001 may look black to the naked eye, but the parser will think it's part of the character sprite.
When that pixel is found, we know we found the x offset for the character sprite.
When constructing your bitmap fonts, remember that the pixel values have to be exact. A background pixel value of R000G000B001 may look black to the naked eye, but the parser will think it's part of the character sprite.
From LFont.cpp
//Right side for( int pCol_w = cellW - 1; pCol_w >= 0; --pCol_w ) { for( int pRow_w = 0; pRow_w < cellH; ++pRow_w ) { //Set pixel offset pX = bX + pCol_w; pY = bY + pRow_w; //Non-background pixel found if( getPixel32( pX, pY ) != BLACK_PIXEL ) { //Set sprite's width nextClip.w = ( pX - nextClip.x ) + 1; //Break the loops pCol_w = -1; pRow_w = cellH; } } }
This time we start parsing the pixels from the right side of the cell to find the width of the sprite.
From LFont.cpp
//Find Top for( int pRow = 0; pRow < cellH; ++pRow ) { for( int pCol = 0; pCol < cellW; ++pCol ) { //Set pixel offset pX = bX + pCol; pY = bY + pRow; //Non-background pixel found if( getPixel32( pX, pY ) != BLACK_PIXEL ) { //New Top Found if( pRow < top ) { top = pRow; } //Break the loops pCol = cellW; pRow = cellH; } } }
When searching for the top of cell we do something a little different. For each character sprite, there's a gap between the top of the cell and the actual sprite:
We want to minimize the wasted space, but we also want our clipping rectangles to be of uniform height. If they weren't, they'd render randomly shifted up and down. So what we're going to do in this implementation is find the tallest sprite and use that are the base y offset for all of the character sprites.
We want to minimize the wasted space, but we also want our clipping rectangles to be of uniform height. If they weren't, they'd render randomly shifted up and down. So what we're going to do in this implementation is find the tallest sprite and use that are the base y offset for all of the character sprites.
From LFont.cpp
//Find Bottom for( int pRow_b = cellH - 1; pRow_b >= 0; pRow_b-- ) { for( int pCol_b = 0; pCol_b < cellW; pCol_b++ ) { //Set pixel offset pX = bX + pCol_b; pY = bY + pRow_b; //Non-background pixel found if( getPixel32( pX, pY ) != BLACK_PIXEL ) { //Set BaseLine if( currentChar == 'A' ) { aBottom = pRow_b; } //New bottom Found if( pRow_b > bottom ) { bottom = pRow_b; } //Break the loops pCol_b = cellW; pRow_b = -1; } } }
Here we're finding the lowest point of the character sprites. By finding the lowest pixel of all the sprites and finding the highest pixel for all the sprites, we can calculate
what's the maximum height needed to render one line of text. This is useful when rendering text.
We also make a point to store the bottom of the 'A' sprite. When text renders, it renders with a baseline and some characters like 'j' and 'g' go below that baseline:
We're going to use the bottom of 'A' as the baseline. You don't have to pick your baseline offset like this, but it's worked for me in the past.
We also make a point to store the bottom of the 'A' sprite. When text renders, it renders with a baseline and some characters like 'j' and 'g' go below that baseline:
We're going to use the bottom of 'A' as the baseline. You don't have to pick your baseline offset like this, but it's worked for me in the past.
From LFont.cpp
//Go to the next character mClips.push_back( nextClip ); currentChar++; } }
After we're done parsing the current cell, we add the character sprite to our clip sprites. Then we increment the ASCII value and parse the next cell until all 256 cells have been
parsed.
From LFont.cpp
//Set Top for( int t = 0; t < 256; ++t ) { mClips[ t ].y += top; mClips[ t ].h -= top; }
After we're done parsing all the cells, we know what the top pixel point for all the character sprites are. Here we lop off the extra height from all the character sprites.
From LFont.cpp
//Blend const int RED_BYTE = 1; const int GREEN_BYTE = 1; const int BLUE_BYTE = 2; const int ALPHA_BYTE = 3; //Go through pixels const int PIXEL_COUNT = textureWidth() * textureHeight(); GLuint* pixels = getPixelData32(); for( int i = 0; i < PIXEL_COUNT; i++ ) { //Get individual color components GLubyte* colors = (GLubyte*)&pixels[ i ]; //White pixel shaded by transparency colors[ ALPHA_BYTE ] = colors[ RED_BYTE ]; colors[ RED_BYTE ] = 0xFF; colors[ GREEN_BYTE ] = 0xFF; colors[ BLUE_BYTE ] = 0xFF; }
A benefit of using OpenGL is that you can get hardware accelerated alpha blending to get smooth blended text:
flat text
smooth text
What we can do is use the brightness of the pixel as it's alpha value. The darker the pixel, the more it blends into the background.
Because the bitmap font is greyscale, we can just take the red component as the brightness. Then for each pixel, we set it's RGB value to be white so the only thing shading it is it's transparency.
flat text
smooth text
What we can do is use the brightness of the pixel as it's alpha value. The darker the pixel, the more it blends into the background.
Because the bitmap font is greyscale, we can just take the red component as the brightness. Then for each pixel, we set it's RGB value to be white so the only thing shading it is it's transparency.
From LFont.cpp
//Create texture from manipulated pixels if( loadTextureFromPixels32() ) { //Build vertex buffer from sprite sheet data if( !generateDataBuffer( LSPRITE_ORIGIN_TOP_LEFT ) ) { printf( "Unable to create vertex buffer for bitmap font!\n" ); success = false; } } else { printf( "Unable to create texture from bitmap font pixels!\n" ); success = false; }
After we're done parsing the pixels and calculating the clip rectangles, we generate the texture and VBO data.
From LFont.cpp
//Set texture wrap glBindTexture( GL_TEXTURE_2D, getTextureID() ); glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER ); glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER ); //Set spacing variables mSpace = cellW / 2; mNewLine = aBottom - top; mLineHeight = bottom - top; } else { printf( "Could not load bitmap font image: %s!\n", path.c_str() ); success = false; } return success; }
Here we give the font texture a different texture wrap. "GL_CLAMP_TO_BORDER" will make sure that the character sprites at the edge of the texture don't wrap around and give
stray lines of pixels.
Lastly we want to calculate the spacing variables. "mSpace" is set be half the cell width. You don't have to set it this way but it's given me good results. "mNewline" is calculated as the space between the top pixel point and the baseline. Lastly, "mLineHeight" is the difference between the top pixel point and the lowest pixel point.
Finally, we can return the success of our function.
Lastly we want to calculate the spacing variables. "mSpace" is set be half the cell width. You don't have to set it this way but it's given me good results. "mNewline" is calculated as the space between the top pixel point and the baseline. Lastly, "mLineHeight" is the difference between the top pixel point and the lowest pixel point.
Finally, we can return the success of our function.
From LFont.cpp
void LFont::freeFont() { //Get rid of sprite sheet freeTexture(); //Reinitialize spacing constants mSpace = 0.f; mLineHeight = 0.f; mNewLine = 0.f; }
The function freeFont() calls freeTexture and, because a LFont is a LSpriteSheet, it will call the LSpriteSheet implementation and deallocate the sprite sheet data too.
From LFont.cpp
void LFont::renderText( GLfloat x, GLfloat y, std::string text ) { //If there is a texture to render from if( getTextureID() != 0 ) { //Draw positions GLfloat dX = x; GLfloat dY = y; //Move to draw position glTranslatef( x, y, 0.f ); //Set texture glBindTexture( GL_TEXTURE_2D, getTextureID() ); //Enable vertex and texture coordinate arrays glEnableClientState( GL_VERTEX_ARRAY ); glEnableClientState( GL_TEXTURE_COORD_ARRAY ); //Bind vertex data glBindBuffer( GL_ARRAY_BUFFER, mVertexDataBuffer ); //Set texture coordinate data glTexCoordPointer( 2, GL_FLOAT, sizeof(LVertexData2D), (GLvoid*) offsetof( LVertexData2D, texCoord ) ); //Set vertex data glVertexPointer( 2, GL_FLOAT, sizeof(LVertexData2D), (GLvoid*) offsetof( LVertexData2D, position ) );
Here's our renderText() function which looks like a specialized version of the renderSprite() function from LSpriteSheet. After initializing the draw positions, we translate to
the rendering point, bind the texture, and set the VBO data.
One of the benefits of using the LFont is that you can bind the texture once and render multiple times with it. Binding textures in OpenGL is an expensive operation and you should try to get the most out of your texture binding and avoid constantly swapping textures around in VRAM.
One of the benefits of using the LFont is that you can bind the texture once and render multiple times with it. Binding textures in OpenGL is an expensive operation and you should try to get the most out of your texture binding and avoid constantly swapping textures around in VRAM.
From LFont.cpp
//Go through string for( int i = 0; i < text.length(); ++i ) { //Space if( text[ i ] == ' ' ) { glTranslatef( mSpace, 0.f, 0.f ); dX += mSpace; } //Newline else if( text[ i ] == '\n' ) { glTranslatef( x - dX, mNewLine, 0.f ); dY += mNewLine; dX += x - dX; } //Character else { //Get ASCII GLuint ascii = (unsigned char)text[ i ]; //Draw quad using vertex data and index data glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, mIndexBuffers[ ascii ] ); glDrawElements( GL_QUADS, 4, GL_UNSIGNED_INT, NULL ); //Move over glTranslatef( mClips[ ascii ].w, 0.f, 0.f ); dX += mClips[ ascii ].w; } } //Disable vertex and texture coordinate arrays glDisableClientState( GL_TEXTURE_COORD_ARRAY ); glDisableClientState( GL_VERTEX_ARRAY ); } }
Here's our for loop that goes through the string and renders character sprites.
There are two special cases. If we have a space, there's no image to render so we just translate over and shift the rendering point. If we have a newline we translate back to the base x offset, translate down one newline, and update the render point.
If we have an actual character sprite to draw, we get its ASCII value and bind the character sprite's IBO so we can render with it. Then we translate past the character sprite so we can render the next character until there are no more characters.
There are two special cases. If we have a space, there's no image to render so we just translate over and shift the rendering point. If we have a newline we translate back to the base x offset, translate down one newline, and update the render point.
If we have an actual character sprite to draw, we get its ASCII value and bind the character sprite's IBO so we can render with it. Then we translate past the character sprite so we can render the next character until there are no more characters.
From LUtil.cpp
bool loadMedia() { //Load Font if( !gFont.loadBitmap( "20_bitmap_fonts/lazy_font.png" ) ) { printf( "Unable to load bitmap font!\n" ); return false; } return true; }
In loadMedia(), we load our bitmap font.
From LUtil.cpp
void render() { //Clear color buffer glClear( GL_COLOR_BUFFER_BIT ); glLoadIdentity(); //Render red text glColor3f( 1.f, 0.f, 0.f ); gFont.renderText( 0.f, 0.f, "The quick brown fox jumps\nover the lazy dog" ); //Update screen glutSwapBuffers(); }
And in render(), we render our text.
Using, glColor() we can give our text color when rendering it.
Using, glColor() we can give our text color when rendering it.