Writing your own font engine
Introduction
To use components that draw text such as Buttons, Labels and Textboxes you'll need to write your own font engine. There is no default font engine in the library, because they usually require dependencies on other libraries or platform dependant code.
Although writing your own font engine sounds complicated, it is in fact quite simple, as this article will show you. It will guide you through writing a simple bitmap based font engine, and hopefully explain the interface sufficiently so that you can write more complex font engines afterwards.
The code for this article is taken from the UIDemo demo application, which is included in the demo applications download. Let's jump right in!
Interfaces
You need to implement two interfaces,
Font and
AbstractFontFactory, in order to write a
font engine. The Font interface describes a
font in the library and is responsible for creating, rendering and
destroying individual fonts. The
AbstractFontFactory is responsible for
managing the fonts in your application, such as the creation of new
Font instances when the library requests
them. The Font interface defines four
abstract methods (pure virtual functions)—shown below—which you'll need to
implement.
const std::string getFontName() const;
std::size_t getSize() const;
util::Dimension getStringBoundingBox(const std::string &text) const;
void drawString(int x, int y, const std::string &text);While the method names are pretty self-explanatory, there are some
small issues to keep in mind while implementing these methods. The
getFontName method should return the name of the
font belonging to this instance of the Font
class. This means that the returned font name should be unique and used
consistently across calls to the function, because the name and font size
are used by the library to determine whether or not a font already exists
(and allocate a new font if it does not.) The unit returned by
getSize can be arbitrary, because the method
is—besides the aforementioned identification—never used inside the
library. The dimensions returned by the
getStringBoundingBox method are significant
however. They are used by the library to, for example, calculate button
dimensions. This method is called very often, so it is a good idea to
cache the dimensions of the individual characters instead of calculating
them on the fly. The last method, drawString
should be implemented so that the text is drawn to the screen at the
specified X and Y location.
The AbstractFontFactory interface has
only one abstract method. This method should create instances of your
Font implementation. The
fontName parameter is the name of the font. The
size parameter is the font size.
For example, the library could request a font named "Arial.ttf" with
12 point size and your AbstractFontFactory
implementation could proceed by loading Arial.ttf from disk and creating
an instance of your Font implementation
with 12 point size. Alternatively, it could completely ignore the request
and always return the same font, with the same font size. This is what
we'll do for our bitmap font engine.
Bitmap font engine
Our BitmapFont header file should look similar to the code fragment shown below. Note the four data members, the width and height of the font texture, the texture identifier and a base index for the display list we are going to use.
class BitmapFont : public ui::Font
{
public:
BitmapFont();
~BitmapFont();
const std::string getFontName() const;
std::size_t getSize() const;
ui::util::Dimension getStringBoundingBox(const std::string &text) const;
void drawString(int x, int y, const std::string &text);
private:
static const int textureWidth = 256;
static const int textureHeight = 256;
GLuint textureId;
GLuint base;
};
The bitmap we are going to use stores 256 characters. The first 128 characters are standard ASCII characters and the last 128 are from the ANSI Extended ASCII standard and contain mostly special characters found in some non-English languages (such as French, Swedish, Norwegian, etc). On the right an example of such a bitmap.
The constructor loads the image, loops through it and creates a display list for each character. Note that the the identifiers for the display lists correspond with the numeric values of the ASCII table. For example, uppercase 'A' has identifier 65 in the ASCII table and in the display lists. Note that we use a base number as offset in case OpenGL has already allocated other display list identifiers.
BitmapFont::BitmapFont()
{
// This uses the SDL_Image library to load a PNG file
SDL_Surface *font = IMG_Load("Font.png");
glEnable(GL_TEXTURE_2D);
glGenTextures(1,&textureId);
glBindTexture(GL_TEXTURE_2D,textureId);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, font->w, font->h, 0, GL_RGBA,
GL_UNSIGNED_BYTE, font->pixels);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER,GL_LINEAR);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
float cx;
float cy;
base = glGenLists(256);
for(int i = 0; i < 256; i++)
{
cx = float(i%16)/16.0f;
cy = float(i/16)/16.0f;
glNewList(base+i,GL_COMPILE);
glBegin(GL_QUADS);
glTexCoord2f(cx,cy+0.0625f);
glVertex2i(0,16);
glTexCoord2f(cx+0.0625f,cy+0.0625f);
glVertex2i(16,16);
glTexCoord2f(cx+0.0625f,cy);
glVertex2i(16,0);
glTexCoord2f(cx,cy);
glVertex2i(0,0);
glEnd();
glTranslated(10,0,0);
glEndList();
}
glDisable(GL_TEXTURE_2D);
SDL_FreeSurface(font);
}
BitmapFont::~BitmapFont()
{
glDeleteTextures(1,&textureId);
glDeleteLists(base,256);
}The rest of the Font implementation is quite
simple and shown below. We draw the text by translating to the X, Y
coordinates and call the display lists by using the ASCII values of our
string. The getFontName and
getSize methods return a fixed value, and the
getStringBounding method returns the width as
<number of characters> * <font size> and height as <font
size>.
void BitmapFont::drawString(int x, int y, const std::string &text)
{
glEnable(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D,textureId);
glTranslatef(static_cast<GLfloat>(x),static_cast<GLfloat>(y),0.0f);
glListBase(base);
glCallLists(static_cast<GLsizei>(text.length()),GL_UNSIGNED_BYTE,text.c_str());
glDisable(GL_TEXTURE_2D);
}
const std::string BitmapFont::getFontName() const
{
return "Bitmap";
}
std::size_t BitmapFont::getSize() const
{
return 10;
}
ui::util::Dimension BitmapFont::getStringBoundingBox(const std::string &text) const
{
int width = static_cast<int>(text.length() * getSize());
return ui::util::Dimension(width,static_cast<int>(getSize()));
}The BitmapFontFactory class definition
inherits the AbstractFontFactory interface,
implements the createFont method and stores a
pointer to a BitmapFont instance. We include a
constructor and destructor to allocate and deallocate the
BitmapFont instance.
class BitmapFontFactory : public ui::AbstractFontFactory
{
public:
ui::Font * createFont(const std::string &font, std::size_t size);
BitmapFontFactory();
~BitmapFontFactory();
private:
BitmapFont *bitmapFont;
};The implementation of BitmapFontFactory is
straightforwarded. The constructor creates an instance of
BitmapFont and the destructor deletes it. The
createFont method returns the same instance of
BitmapFont regardless of what the library requests,
so that the user interface always uses the same font.
ui::Font * BitmapFontFactory::createFont(const std::string &font, std::size_t size)
{
return bitmapFont;
}
BitmapFontFactory::BitmapFontFactory()
{
bitmapFont = new BitmapFont();
}
BitmapFontFactory::~BitmapFontFactory()
{
delete bitmapFont;
}This concludes the implementation of our bitmap font engine. The
last thing we need to is tell our Gui instance that it should use our
BitmapFontFactory, by calling the
setFontFactory method and giving an instance of
your BitmapFontFactory to it. If you followed the
“Getting Started” article you can
call the setFontFactory method on
guiInstance before rendering your user
interface.
We're all set and done now; the library should properly render your bitmap font. Using these two interfaces you can create more advanced font engines, such as the one in the UITest demo application, which uses Freetype to create fonts.