A Font Library for OpenGL

by Steve Baker

Introduction

The FNT library was originally written to allow PUI programs to produce text using texture maps instead of bitmaps because the latter are typically very slow on consumer-grade 3D hardware.

An Example.

This example program is probably all you need in the way of documentation - so it's the first thing we'll talk about:

   /* Declarations */

   fntRenderer texout ;

   /* Load some fonts */

   fntTexFont  TimesRoman ( "times_roman.txf" ) ;
   fntTexFont  Courier    ( "courier.txf" ) ;

   /* Select a font and pointsize to render with... */

   texout . setFont      ( & TimesRoman ) ;
   texout . setPointSize ( 24 ) ;

   /* Print "Hello" and "World" */

   texout . begin () ;
     texout . start2f ( 50.0f, 80.0f ) ;
     texout . puts ( "Hello" ) ;
     texout . start2f ( 50.0f, 50.0f ) ;
     texout . puts ( "World" ) ;
   texout . end () ;
 

fntInit

All programs that use the FNT library should call 'fntInit()' sometime after they have established a valid OpenGL rendering context and before they make any other FNT library calls.

The Classes

There are three external classes used in FNT:

  class fntFont         -- An abstract base class from which all
                           kinds of font representations could be
                           derived.
  class fntTexFont      -- A fntFont that uses texture mapping.
  class fntRenderer     -- A class that draws text using a fntFont.

class fntFont

Classes derived from the fntFont class all describe fonts. This is an abstract base class from which fntTexFont is derived. Other fntFont sub-classes could also be derived in the future. (This means that you cannot declare a fntFont - but all classes derived from fntFont will obey this description.)

This class appears quite complex since constructing and/or querying the contents of a texture font is a complicated business - and consequently, there is a lot of API to support your ability to do that. Fortunately, most FNT applications will just load a font from disk and use fntFont::begin()/ fntFont::puts()/ fntFont::end() functionality.


  class fntFont
  {
  public:
    fntFont () ;
    ~fntFont () ;
    void putch ( sgVec3 curpos, float pointsize, float slant, char  c ) ;
    void puts  ( sgVec3 curpos, float pointsize, float slant, char *s ) ;
    void begin () ;
    void end   () ;
    void getBBox ( char *s, float pointsize, float slant,
                            float *left, float *right,
                            float *bot , float *top  ) ;

    int  load ( char *fname, GLenum mag = GL_NEAREST,
                             GLenum min = GL_LINEAR_MIPMAP_LINEAR ) ;

    void setFixedPitch ( int fix ) ;
    int   isFixedPitch ()          ;

    void  setWidth     ( float w ) ;
    void  setGap       ( float g ) ;

    float getWidth     () ;
    float getGap       () ;

    int   hasGlyph ( char c ) ;
  } ;

fntFont::getBBox() returns the top, bottom, left and right extents (in OpenGL units) of the string 's' if it were drawn at the specified pointsize and slant. This routine knows how to deal with newline characters.

fntFont::putch() draws the character 'c' at the cursor position specified be 'curpos' (with the specified pointsize and slant) and advances the cursor to the right hand edge of the character it just rendered.

fntFont::puts() draws the entire string 's' at the cursor position specified be 'curpos' (with the specified pointsize and slant) and advances the cursor to the right hand edge of the last character it rendered.

Both putch and puts will automatically switch the case of letters from upper to lower or vice versa if the required character is not present in the font but the letter with the reverse case is present. Other missing characters will simply generate no output and won't update the cursor position. If a space character is not defined in the font, then a half-pointsize gap will be generated instead.

puts (but NOT putch) knows how to deal with newline characters, it drops the text down to the next line - leaving a one-third pointsize gap between lines.

fntFont::begin()/end() since redundant mode changes are costly in OpenGL, applications may optionally call fntFont::begin() before rendering some text using a specified font and call fntFont::end() at the end. Just like a glBegin()/glEnd() pair, there are some fairly restrictive rules about what you can do between a fntFont::begin() and a fntFont::end():

You may call fntFont::putch() and/or fntFont::puts() without entering a fntFont::begin()/fntFont::end() state - but if you do that, each call will result in OpenGL state switching - which may well be redundant.

int load ( char *filename ) loads a font from disk, returns TRUE on success, FALSE for failure.

int load ( char *filename, GLenum mag = GL_NEAREST, GLenum min = GL_LINEAR_MIPMAP_LINEAR )...you can optionally specify the OpenGL texture magnification and minification filters - this is sometimes necessary to get the clearest possible text at certain point sizes. Experiment!

void setFixedPitch ( int fixed ) if 'fixed' is TRUE, forces the font to be fixed-pitch (so each letter or 'Glyph' is a standard width), if 'fixed' is false then variable character widths are possible.

void setWidth ( float w ) For a fixed width font, this sets the width of each character. If the actual characters are wider than this, they will overlap, if they are narrower then there will be a gap.

void setGap ( float g ) Set the gap between letters, can be negative or positive.

The following routines allow you to query the font's setup:


    int   isFixedPitch ()
    float getWidth     ()
    float getGap       ()

int hasGlyph() returns TRUE if the font contains a glyph (graphic image) for a given ASCII character, FALSE otherwise. Fonts that have only uppercase (or only lowercase) letters will still return TRUE for corresponding characters of the opposite case because the putch and puts routines will automatically case-convert in that case.

class fntTexFont

fntTexFont is inherited from fntFont. All functions of a fntFont are implemented in fntTextFont using a texture map.

  class fntTexFont
  {
    public:
      fntTexFont () ;
      fntTexFont ( char *fname,
                   GLenum mag = GL_NEAREST,
                   GLenum min = GL_LINEAR_MIPMAP_LINEAR ) ;
      ~fntTexFont () ;
      void putch ( sgVec3 curpos, float pointsize, float slant, char  c ) ;
      void puts  ( sgVec3 curpos, float pointsize, float slant, char *s ) ;
      void begin () ;
      void end   () ;
      void getBBox ( char *s, float pointsize, float slant,
			      float *left, float *right,
			      float *bot , float *top  ) ;

      int  load ( char *fname,
                  GLenum mag = GL_NEAREST,
                  GLenum min = GL_LINEAR_MIPMAP_LINEAR ) ;

      void setFixedPitch ( int fix ) ;
      int   isFixedPitch ()          ;

      void  setWidth     ( float w ) ;
      void  setGap       ( float g ) ;

      float getWidth     () ;
      float getGap       () ;

      void setGlyph ( char c,
		    float tex_left, float tex_right,
		    float tex_bot , float tex_top  ,
		    float vtx_left, float vtx_right,
		    float vtx_bot , float vtx_top  ) ;
    
      int  getGlyph ( char c,
		    float *tex_left = NULL, float *tex_right = NULL,
		    float *tex_bot  = NULL, float *tex_top   = NULL,
		    float *vtx_left = NULL, float *vtx_right = NULL,
		    float *vtx_bot  = NULL, float *vtx_top   = NULL) ;
    } ;

int load ( char *filename ) loads a font from disk using the extension of the filename to determine what format it is stored in. Currently, the only supported format is Mark Kilgards 'texfont' format - which has the '.txf' filename extension. Returns TRUE for success, FALSE for failure. Details of the TXF file format can be found in Marks's document about Textured Fonts

Just as with all fntFont derived classes, you can optionally specify the OpenGL texture filter options for the loaded texture.

Each font is made up of zero or more 'Glyphs' (character shapes) packed into a single texture map. If you didn't use 'load' to load the font from disk, you'll have to define where each one lies on the map and how big the quadrilateral it is to be drawn on is:


    void setGlyph ( char c,
		    float tex_left, float tex_right,
		    float tex_bot , float tex_top  ,
		    float vtx_left, float vtx_right,
		    float vtx_bot , float vtx_top  ) ;
    
Where 'c' is the character we are defining, 'tex_*' is the left, right, top and bottom of the image of that character in the texture map. Since texture coordinates are in the range 0..1, these will typically be quite small numbers. 'vtx_*' is the left, right, top and bottom of the character's rectangle in a coordinate system that has (0,0) at the baseline of the character cell, and (1,1) at the top-right corner of the tallest, widest character in the font. Hence, a lower-case 'y' would have a negative 'vtx_bot'.

You can also query all this information:


    int  getGlyph ( char c,
		    float *tex_left, float *tex_right,
		    float *tex_bot , float *tex_top  ,
		    float *vtx_left, float *vtx_right,
		    float *vtx_bot , float *vtx_top  ) ;

fntFont::getGlyph() returns TRUE if the character has been defined in this font, FALSE otherwise.

class fntRenderer

This class is the one most people are going to be using to render text in FNT. Most applications will declare a single fntRenderer class for all their text needs.

  class fntRenderer
  {
  public:
    fntRenderer ()

    void start3fv ( sgVec3 pos ) ;
    void start2fv ( sgVec2 pos ) ;
    void start2f  ( float x, float y ) ;
    void start3f  ( float x, float y, float z ) ;

    void getCursor ( float *x, float *y, float *z )

    fntFont *getFont () ;
    void     setFont ( fntFont *f ) ;

    void begin () ;
    void end   () ;

    void putch ( char  c ) ;
    void puts  ( char *s ) ;
  } ;

fntRenderer::setFont() is used to tell the renderer which font is current. fntRenderer::getFont() lets you find out which font it is. You may not call fntRenderer::setFont() between a fntRenderer::begin() and fntRenderer::end() pair.

By default, uppercase characters of all fonts are one OpenGL unit high. You can make characters larger or smaller by setting fntRenderer::setPointSize(). You can also slant the characters to form italic or oublique fonts using fntRenderer::setSlant(). (The slant measures by how many OpenGL units the tops of uppercase characters are sloped to the right). You can call either setPointSize or setSlant between fntRenderer::begin() and fntRenderer::end().

The various kinds of fntRendered::start*() calls are akin to the OpenGL glVertex* commands and they determine where the next chunk of text will be drawn. Since this coordinate is updated as text is drawn, you'll need to call fntRenderer::getCursor() to find out where the next chunk of text will be drawn. You can call fntRenderer::start*(), fntRenderer::putch() and fntRenderer::puts() between fntRenderer::begin()/fntRenderer::end() calls.

Making New TXF Fonts.

The '.txf' font format was designed by Mark Kilgard - who also produced some tools for dealing with them.

Check Marks's document about Textured Fonts for further information. Of particular interest is Mark's "gentexfont" program that can create a TXF format font from an X-windows font.

You can find out which X-fonts are stored on your machine using the /usr/X11/bin/xlsfonts program.

There are over a dozen sample TXF fonts stored in examples/src/fnt/data.


Valid HTML 4.0!
Steve J. Baker. <sjbaker1@airmail.net>