OpenGL:Tutorials:Font System

From GPWiki
Jump to: navigation, search

This page documents a rather simple font system for OpenGL (using C++). It consists of two parts - a command-line tool to generate font files from true type fonts, and a component you add to a program to actually load and draw the font. Blob Shepherd uses this font system.

While there are certain advantages to using the technique described below, some may wish to support loading of TrueType fonts within their OpenGL programs. James Turk has has put together a class (based on this page) that does just that in GLFT_Font.

oglBMfont

Using oglBMfont and AngelCode's BMfont tool you can easily extract a bitmap from a TrueType font and integrate the usage of OpenGL fonts in your application through this simple C class.


Fonttool

fonttool is the command-line tool that you use to create a font file. What this basically does is:

  • Load in the font using the FreeType 2 library
  • Decide how big the texture should be
  • Write all needed characters into the texture and store information on where they are
  • Put the information about the characters into a file together with the texture

Here is the full source code for fonttool, with comments to explain what is happening:

#include <string>
#include <fstream>
#include <iostream>
#include <vector>
 
// FreeType requires this stuff to include the correct headers
#include <ft2build.h>
#include FT_FREETYPE_H
#include FT_GLYPH_H
 
/**
 * A font file contains the following:
 * 2 chars: "F0" (Font, version 0)
 * size_t: Texture width
 * size_t: Texture height
 * size_t: Line height
 * size_t: Number of characters
 * number of characters * 6: One GlyphEntry struct per character
 * texture width * texture height: The texture data
 *
 * This tool completely ignores endian issues, it should work on a
 * Mac, but font files are not portable between Macs and Intels.
 */
 
// Every glyph/character has such a struct in the output file. This
// contains it's ascii code, the width of the character and the x 
// and y coordinates where this character can be found in the texture.
struct GlyphEntry
{
  unsigned char ascii, width;
  unsigned short x, y;
};
 
// Convenience function for writing simple objects to files.
template<class T, class S>
void Write_Object(const T& to_write, S& out)
{
  out.write(reinterpret_cast<const char*>(&to_write), sizeof(T));
}
 
// This function does all the work.
void Create_Font(const std::string& fontfile, size_t font_size,
                 const std::string& outfile)
{
  // These are the characters that get stored. The last one ('\xFF')
  // indicates the picture used to draw 'unknown' characters.
  const std::string chars("abcdefghijklmnopqrstuvwxyz"
                          "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
                          "1234567890~!@#$%^&*()-=+;:"
                          "'\",./?[]|\\ <>`\xFF");
  // Margins around characters to prevent them from 'bleeding' into
  // each other.
  const size_t margin = 3;
  size_t image_height = 0, image_width = 256;
 
  // This initializes FreeType
  FT_Library library;
  if (FT_Init_FreeType(&library) != 0)
    throw "Could not initialize FreeType2 library.";
 
  // Load the font
  FT_Face face;
  if (FT_New_Face(library, fontfile.c_str(), 0, &face) != 0)
    throw "Could not load font file.";
 
  // Abort if this is not a 'true type', scalable font.
  if (!(face->face_flags & FT_FACE_FLAG_SCALABLE) or
      !(face->face_flags & FT_FACE_FLAG_HORIZONTAL))
    throw "Error setting font size.";
 
  // Set the font size
  FT_Set_Pixel_Sizes(face, font_size, 0);
 
  // First we go over all the characters to find the max descent
  // and ascent (space required above and below the base of a
  // line of text) and needed image size. There are simpler methods
  // to obtain these with FreeType but they are unreliable.
  int max_descent = 0, max_ascent = 0;
  size_t space_on_line = image_width - margin, lines = 1;
 
  for (size_t i = 0; i < chars.size(); ++i){
    // Look up the character in the font file.
    size_t char_index = FT_Get_Char_Index(face, static_cast<unsigned int>(chars[i]));
    if (chars[i] == '\xFF')
      char_index = 0;
 
    // Render the current glyph.
    FT_Load_Glyph(face, char_index, FT_LOAD_DEFAULT);
    FT_Render_Glyph(face->glyph, FT_RENDER_MODE_NORMAL);
 
    size_t advance = (face->glyph->metrics.horiAdvance >> 6) + margin;
    // If the line is full go to the next line
    if (advance > space_on_line){
      space_on_line = image_width - margin;
      ++lines;
    }
    space_on_line -= advance;
 
    max_ascent = std::max(face->glyph->bitmap_top, max_ascent);
    max_descent = std::max(face->glyph->bitmap.rows -
                           face->glyph->bitmap_top, max_descent);
  }
 
  // Compute how high the texture has to be.
  size_t needed_image_height = (max_ascent + max_descent + margin) * lines + margin;
  // Get the first power of two in which it fits.
  image_height = 16;
  while (image_height < needed_image_height)
    image_height *= 2;
 
  // Allocate memory for the texture, and set it to 0
  unsigned char* image = new unsigned char[image_height * image_width];
  for (size_t i = 0; i < image_height * image_width; ++i)
    image[i] = 0;
 
  // Allocate space for the GlyphEntries
  std::vector<GlyphEntry> entries(chars.size());
  // These are the position at which to draw the next glyph
  size_t x = margin, y = margin + max_ascent;
 
  // Drawing loop
  for (size_t i = 0; i < chars.size(); ++i){
    size_t char_index = FT_Get_Char_Index(face, static_cast<unsigned int>(chars[i]));
    if (chars[i] == '\xFF')
      char_index = 0;
 
    // Render the glyph
    FT_Load_Glyph(face, char_index, FT_LOAD_DEFAULT);
    FT_Render_Glyph(face->glyph, FT_RENDER_MODE_NORMAL);
 
    // See whether the character fits on the current line
    size_t advance = (face->glyph->metrics.horiAdvance >> 6) + margin;
    if (advance > image_width - x){
      x = margin;
      y += (max_ascent + max_descent + margin);
    }
 
    // Fill in the GlyphEntry
    entries[i].ascii = chars[i];
    entries[i].width = advance - 3;
    entries[i].x = x;
    entries[i].y = y - max_ascent;
 
    // Copy the image gotten from FreeType onto the texture
    // at the correct position
    for (size_t row = 0; row < face->glyph->bitmap.rows; ++row){
      for (size_t pixel = 0; pixel < face->glyph->bitmap.width; ++pixel){
        image[(x + face->glyph->bitmap_left + pixel) +
              (y - face->glyph->bitmap_top + row) * image_width] =
          face->glyph->bitmap.buffer[pixel + row * face->glyph->bitmap.pitch];
      }
    }
 
    x += advance;    
  }
 
  // Write everything to the output file (see top of this
  // file for the format specification)
  std::ofstream out(outfile.c_str(), std::ios::binary);
  out.put('F'); out.put('0');
  Write_Object(image_width, out);
  Write_Object(image_height, out);
  Write_Object(max_ascent + max_descent, out);
  Write_Object(chars.size(), out);
 
  // GlyphEntries
  for (size_t i = 0; i < chars.size(); ++i)
    Write_Object(entries[i], out);
  // Texture data
  for (size_t i = 0; i < image_width * image_height; ++i)
    out.put(image[i]);
 
  delete[] image;
 
  FT_Done_FreeType(library);
  std::cout << "Wrote " << outfile << ", " << image_width << 
               " by " << image_height << " pixels.\n";
}
 
// Main interprets the arguments and handles errors.
int main(int args, char** argv)
{
  std::cout << argv[0] << ": ";
  try{
    // Default size
    size_t size = 11;
 
    if (args < 3){
      throw "Need at least two arguments - font file and output file.";
    }
    else{
      if (args > 3){
        size_t arg_size = std::atoi(argv[3]);
        if (arg_size != 0)
          size = arg_size;
      }
      Create_Font(argv[1], size, argv[2]);
    }
    return 0;
  }
  catch(const char*& error){
    std::cout << "Error - " << error << "\n";
    return 1;
  }
}

Note that the texture has only one channel, the intensity of the character at that point. This means it has to be loaded into OpenGL as an alpha texture (GL_ALPHA). When drawing you blend the glyphs onto the screen.

Using fonts in OpenGL

The system to load and display the fonts needs to do a few things:

  • Load the font texture
  • Know where each character/glyph is located in the texture
  • Know the size of glyphs
  • Draw text to the screen

In C++ this can be conveniently done with a Font class. Objects of this class load in a font when they are created, and release the texture and memory they use when destroyed. It's declaration might look like this:

class Font
{
public:
  Font(const char* file);
  ~Font();
 
  // The line height is constant
  size_t Line_Height() const;
  // Knowing the width of a character or a string can be useful if you
  // want your UI to look good at all.
  size_t Char_Width(unsigned char c) const;
  size_t String_Width(const std::string& str) const;
  // Draw a string at a given position.
  void Draw_String(const std::string& str, float x, float y);
 
private:
  // Information about a glyph. Tex_y2 can be calculated from tex_y1
  // and _tex_line_height (see below). Advance is the width of the
  // glyph in screen space.
  struct Glyph
  {
    float tex_x1, tex_y1, tex_x2;
    size_t advance;
  };
  // An array to store the glyphs.
  Glyph* _glyphs;
  // A table to quickly get the glyph belonging to a character.
  Glyph* _table[256];
  // The line height, in screen space and in texture space, and the
  // OpenGL id of the font texture.
  size_t _line_height, _texture;
  float _tex_line_height;
};

The constructor of this class does the most work, it has to load the font file and interpret it's contents. Here is a possible way to do this:

// Helper function to read a piece of data from a stream.
template<class T, class S>
void Read_Object(T& to_read, S& in)
{
  in.read(reinterpret_cast<char*>(&to_read), sizeof(T));
}
 
// This is how glyphs are stored in the file.
struct Glyph_Buffer
{
  unsigned char ascii, width;
  unsigned short x, y;
};
 
Font::Font(const char* filename)
  : _line_height(0),
    _texture(0),
    _tex_line_height(0)
{
  // Open the file and check whether it is any good (a font file
  // starts with "F0")
  std::ifstream input(filename, std::ios::binary);
  if (input.fail() || input.get() != 'F' || input.get() != '0')
    throw std::runtime_error("Not a valid font file.");
 
  // Get the texture size, the number of glyphs and the line height.
  size_t width, height, n_chars;
  Read_Object(width, input);
  Read_Object(height, input);
  Read_Object(_line_height, input);
  Read_Object(n_chars, input);
  _tex_line_height = static_cast<float>(_line_height) / height;
 
  // Make the glyph table.
  _glyphs = new Glyph[n_chars];
  for (size_t i = 0; i != 256; ++i)
    _table[i] = NULL;
 
  // Read every glyph, store it in the glyph array and set the right
  // pointer in the table.
  Glyph_Buffer buffer;
  for (size_t i = 0; i < n_chars; ++i){
    Read_Object(buffer, input);
    _glyphs[i].tex_x1 = static_cast<float>(buffer.x) / width;
    _glyphs[i].tex_x2 = static_cast<float>(buffer.x + buffer.width) / width;
    _glyphs[i].tex_y1 = static_cast<float>(buffer.y) / height;
    _glyphs[i].advance = buffer.width;
 
    _table[buffer.ascii] = _glyphs + i;
  }
  // All chars that do not have their own glyph are set to point to
  // the default glyph.
  Glyph* default_glyph = _table[(unsigned char)'\xFF'];
  // We must have the default character (stored under '\xFF')
  if (default_glyph == NULL)
    throw std::runtime_error("Font file contains no default glyph");
  for (size_t i = 0; i != 256; ++i){
    if (_table[i] == NULL)
      _table[i] = default_glyph;
  }
 
  // Store the actual texture in an array.
  unsigned char* tex_data = new unsigned char[width * height];
  input.read(reinterpret_cast<char*>(tex_data), width * height);
 
  // Generate an alpha texture with it.
  glGenTextures(1, &_texture);
  glBindTexture(GL_TEXTURE_2D, _texture);
  glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
  glTexImage2D(GL_TEXTURE_2D, 0, GL_ALPHA8, width, height, 0, GL_ALPHA,
               GL_UNSIGNED_BYTE, tex_data);
  // And delete the texture memory block
  delete[] tex_data;
}

The destructor is a lot simpler...

Font::~Font()
{
  // Release texture object and glyph array.
  glDeleteTextures(1, &_texture);
  delete[] _glyphs;
}

And the functions to get character sizes are no rocket science either:

size_t Font::Line_Height() const
{
  return _line_height;
}
size_t Font::Char_Width(unsigned char c) const
{
  return _table[c]->advance;
}
 
size_t Font::String_Width(const std::string& str) const
{
  size_t total = 0;
  for (size_t i = 0; i != str.size(); ++i)
    total += Char_Width(str[i]);
  return total;
}

This only leaves Draw_String. This function just goes over every character in a string and draws them next to each other. It is left up to the caller to set the projection matrix to orthogonal with 1 unit = 1 pixel, and to enable texturing and blending. The text is drawn using OpenGL's current color.

void Font::Draw_String(const std::string& str, float x, float y)
{
  glBindTexture(GL_TEXTURE_2D, _texture);
 
  // Simply draw quads textured with the current glyph for every
  // character, updating the x position as we go along.
  glBegin(GL_QUADS);
  for (size_t i = 0; i != str.size(); ++i){
    Glyph* glyph = _table[str[i]];
 
    glTexCoord2f(glyph->tex_x1, glyph->tex_y1);
    glVertex2f(x, y);
    glTexCoord2f(glyph->tex_x1, glyph->tex_y1 + _tex_line_height);
    glVertex2f(x, y + _line_height);
    glTexCoord2f(glyph->tex_x2, glyph->tex_y1 + _tex_line_height);
    glVertex2f(x + glyph->advance, y + _line_height);
    glTexCoord2f(glyph->tex_x2, glyph->tex_y1);
    glVertex2f(x + glyph->advance, y);
 
    x += glyph->advance;
  }
  glEnd();
}

Possible improvements

This system works, and is quite easy to use. The reason the tool to create font files is a separate program is that this way you won't have to link FreeType with your main program, and loading a font is very fast. If you want your program to be able to load and use arbitrary true type fonts you will have to integrate the functionality of fonttool into your main program.

The file size for fonts is not a real problem at the moment (unless the characters are very big), but it can easily be made smaller by compression. If you use zlib, compressing files is very easy.

Something that always comes up when drawing fonts is line breaking. If you want to show a piece of text on more than one line you'll have to break it up into separate lines. To have things look professional you'll want the line breaks to happen at spaces in the text, not in the middle of words. It would be possible to write a line breaking system that works with this font system to support for example a Broken_String object that contains a string broken up in lines of a specific width and has functions to make drawing the string easy.