CU_MDX_BitmapFont.zip (79.3 KiB, 4,246 hits)


  CUnitFramework.zip (101.1 KiB, 18,093 hits)

Extract the C-Unit Framework
inside the project (not solution) directory.

In this tutorial we will create_ a bitmap font system:

Text

Previously, we rendered text using the DirectX Font class. While the Font class is an easy way to render text, it doesn’t have quite the performance that I would like and it is quite limited in the aesthetic quality of the fonts. As a result, I decided to implement a bitmap font system in the C-Unit Framework.

A letter printed with a bitmap font is simply a quad (two triangles) with the texture of a letter slapped onto it. To create_ our text, we will need a texture that contains all the characters that we will want to render. We will then assign each letter on the texture its own set of texture coordinates in order to access each letter and place them on their corresponding quads. Bitmap fonts usually come in two flavors: single-width fonts and multi-width fonts. Single-width fonts have each character evenly spaced on the texture. This allows us to calculate the texture coordinates in our program. While this is easier than multi-width fonts, it does not look as nice, since fonts that are not meant to be monospace, will be rendered in monospace. Multi-width fonts, on the other hand, are spaced differently according to letter. For example, an ‘i’ will take up less space than a ‘W’. In order to use multi-width fonts, we will need to create_ a texture that stores all the characters, and then create_ some sort of data sheet that provides all the dimensions of each character on the texture. Luckily, there are free programs to do this for us.

The C-Unit Framework creates bitmap fonts with the help of the Angelcode Bitmap Font Generator. The Angelcode Bitmap Font Generator is a nice tool that will arrange the characters of a font onto a texture and then create_ a data sheet that provides all the information we need to render the characters onto quads. The output looks like the following:

Angelcode Output

Angelcode Data

To store all this information, we’ll create_ a few small classes.

class BitmapCharacterSet
{
    public int LineHeight;
    public int Base;
    public int RenderedSize;
    public int Width;
    public int Height;
    public BitmapCharacter[] Characters;

    /// <summary>Creates a new BitmapCharacterSet</summary>
    public BitmapCharacterSet()
    {
        Characters = new BitmapCharacter[256];
        for ( int i = 0; i < 256; i++ )
        {
            Characters[i] = new BitmapCharacter();
        }
    }
}

/// <summary>Represents a single bitmap character.</summary>
public class BitmapCharacter : ICloneable
{
    public int X;
    public int Y;
    public int Width;
    public int Height;
    public int XOffset;
    public int YOffset;
    public int XAdvance;
    public List<Kerning> KerningList = new List<Kerning>();

    /// <summary>Clones the BitmapCharacter</summary>
    /// <returns>Cloned BitmapCharacter</returns>
    public object Clone()
    {
        BitmapCharacter result = new BitmapCharacter();
        result.X = X;
        result.Y = Y;
        result.Width = Width;
        result.Height = Height;
        result.XOffset = XOffset;
        result.YOffset = YOffset;
        result.XAdvance = XAdvance;
        result.KerningList.AddRange( KerningList );
        return result;
    }
}

/// <summary>Represents kerning information for a character.</summary>
public class Kerning
{
    public int Second;
    public int Amount;
}

/// <summary>Individual string to load into vertex buffer.</summary>
struct StringBlock
{
    public string Text;
    public RectangleF TextBox;
    public BitmapFont.Align Alignment;
    public float Size;
    public ColorValue Color;
    public bool Kerning;

    /// <summary>Creates a new StringBlock</summary>
    /// <param name="text">Text to render</param>
    /// <param name="textBox">Text box to constrain text</param>
    /// <param name="alignment">Font alignment</param>
    /// <param name="size">Font size</param>
    /// <param name="color">Color</param>
    /// <param name="kerning">true to use kerning, false otherwise.</param>
    public StringBlock( string text, RectangleF textBox, BitmapFont.Align alignment, 
        float size, ColorValue color, bool kerning )
    {
        Text = text;
        TextBox = textBox;
        Alignment = alignment;
        Size = size;
        Color = color;
        Kerning = kerning;
    }
}

The BitmapCharacterSet class represents the entire BitmapCharacter alphabet. It contains the dimensions of the texture, the size the characters were created with, as well the line height, which we will use to determine the space between lines.

The BitmapCharacter class represents a single character. It contains all the information found in the generated .fnt file. To see what all these values represent, check out Angelcode’s page. They’re all just used to place and align the character in a rendered string.

The Kerning class stores any kerning information about a character. Kerning adjusts the distance between characters to make the text look more evenly spaced. For example, the letters “ll” would usually have a different kern amount than the letters “oo”. Not all fonts have kerning information but the C-Unit Framework supports kerning when that information is available.

The StringBlock class represents a single piece of text. Each of the colored sections of text in the screenshot above are separate StringBlocks. Each StringBlock can have its own color, size, bounding rectangle, alignment, and whether to use kerning.

To start implementing the bitmap font system, we’ll create_ a simple Quad class that will represent a polygon in screen space.

namespace CUnit
{
public class Quad : ICloneable
{
protected TransformedColoredTextured[] m_vertices;

public Quad() Toggle
{
// Empty
}


/// <summary>Creates a new Quad</summary>
/// <param name=”topLeft”>Top left vertex.</param>
/// <param name=”topRight”>Top right vertex.</param>
/// <param name=”bottomLeft”>Bottom left vertex.</param>
/// <param name=”bottomRight”>Bottom right vertex.</param>
public Quad( TransformedColoredTextured topLeft, TransformedColoredTextured topRight, TransformedColoredTextured bottomLeft, TransformedColoredTextured bottomRight ) Toggle
{
m_vertices = new TransformedColoredTextured[6];
m_vertices[0] = topLeft;
m_vertices[1] = bottomRight;
m_vertices[2] = bottomLeft;
m_vertices[3] = topLeft;
m_vertices[4] = topRight;
m_vertices[5] = bottomRight;
}


/// <summary>Gets and sets the vertices.</summary>
public TransformedColoredTextured[] Vertices Toggle
{
get { return m_vertices; }
set { value.CopyTo( m_vertices, 0 ); }
}


/// <summary>Gets the top left vertex.</summary>
public TransformedColoredTextured TopLeft Toggle
{
get { return m_vertices[0]; }
}


/// <summary>Gets the top right vertex.</summary>
public TransformedColoredTextured TopRight Toggle
{
get { return m_vertices[4]; }
}


/// <summary>Gets the bottom left vertex.</summary>
public TransformedColoredTextured BottomLeft Toggle
{
get { return m_vertices[2]; }
}


/// <summary>Gets the bottom right vertex.</summary>
public TransformedColoredTextured BottomRight Toggle
{
get { return m_vertices[5]; }
}


/// <summary>Gets and sets the X coordinate.</summary>
public float X Toggle
{
get { return m_vertices[0].X; }
set
{
float width = Width;
m_vertices[0].X = value;
m_vertices[1].X = value + width;
m_vertices[2].X = value;
m_vertices[3].X = value;
m_vertices[4].X = value + width;
m_vertices[5].X = value + width;
}
}


/// <summary>Gets and sets the Y coordinate.</summary>
public float Y Toggle
{
get { return m_vertices[0].Y; }
set
{
float height = Height;
m_vertices[0].Y = value;
m_vertices[1].Y = value + height;
m_vertices[2].Y = value + height;
m_vertices[3].Y = value;
m_vertices[4].Y = value;
m_vertices[5].Y = value + height;
}
}


/// <summary>Gets and sets the width.</summary>
public float Width Toggle
{
get { return m_vertices[4].X – m_vertices[0].X; }
set
{
m_vertices[1].X = m_vertices[0].X + value;
m_vertices[4].X = m_vertices[0].X + value;
m_vertices[5].X = m_vertices[0].X + value;
}
}


/// <summary>Gets and sets the height.</summary>
public float Height Toggle
{
get { return m_vertices[2].Y – m_vertices[0].Y; }
set
{
m_vertices[1].Y = m_vertices[0].Y + value;
m_vertices[2].Y = m_vertices[0].Y + value;
m_vertices[5].Y = m_vertices[0].Y + value;
}
}


/// <summary>Gets the X coordinate of the right.</summary>
public float Right Toggle
{
get { return X + Width; }
}


/// <summary>Gets the Y coordinate of the bottom.</summary>
public float Bottom Toggle
{
get { return Y + Height; }
}


/// <summary>Gets and sets the Quad’s color.</summary>
public int Color Toggle
{
get { return m_vertices[0].ColorValue; }
set
{
for ( int i = 0; i < 6; i++ )
{
m_vertices[i].ColorValue = value;
}
}
}


/// <summary>Writes the Quad to a string</summary>
/// <returns>String</returns>
public override string ToString() Toggle
{
string result = “X = “ + X.ToString();
result += “nY = “ + Y.ToString();
result += “nWidth = “ + Width.ToString();
result += “nHeight = “ + Height.ToString();
return result;
}


/// <summary>Clones the Quad.</summary>
/// <returns>Cloned Quad</returns>
public object Clone() Toggle
{
return new Quad( m_vertices[0], m_vertices[4], m_vertices[2], m_vertices[5] );
}


}
}

The Quad class is simple a wrapper for an array of TransformedColoredTextured vertices. It provides an easy way to create_ and manipulate a quad in screen space. The Quad class is a generic screen space quad that can be used to display 2D graphics (like my gui) or bitmapped text. The C-Unit bitmap font system requires a little more information with its Quads, so I created another class that inherits from Quad, called FontQuad:

namespace CUnit
{
    /// <summary>Quad used to render bitmapped fonts</summary>
    public class FontQuad : Quad
    {
        private int m_lineNumber;
        private int m_wordNumber;
        private float m_sizeScale;
        private BitmapCharacter m_bitmapChar = null;
        private char m_character;
        private float m_wordWidth;

        /// <summary>Creates a new FontQuad</summary>
        /// <param name="topLeft">Top left vertex</param>
        /// <param name="topRight">Top right vertex</param>
        /// <param name="bottomLeft">Bottom left vertex</param>
        /// <param name="bottomRight">Bottom right vertex</param>
        public FontQuad( TransformedColoredTextured topLeft, TransformedColoredTextured topRight,
            TransformedColoredTextured bottomLeft, TransformedColoredTextured bottomRight )
        {
            m_vertices = new TransformedColoredTextured[6];
            m_vertices[0] = topLeft;
            m_vertices[1] = bottomRight;
            m_vertices[2] = bottomLeft;
            m_vertices[3] = topLeft;
            m_vertices[4] = topRight;
            m_vertices[5] = bottomRight;
        }

        /// <summary>Gets and sets the line number.</summary>
        public int LineNumber
        {
            get { return m_lineNumber; }
            set { m_lineNumber = value; }
        }

        /// <summary>Gets and sets the word number.</summary>
        public int WordNumber
        {
            get { return m_wordNumber; }
            set { m_wordNumber = value; }
        }

        /// <summary>Gets and sets the word width.</summary>
        public float WordWidth
        {
            get { return m_wordWidth; }
            set { m_wordWidth = value; }
        }

        /// <summary>Gets and sets the BitmapCharacter.</summary>
        public BitmapCharacter BitmapCharacter
        {
            get { return m_bitmapChar; }
            set { m_bitmapChar = (BitmapCharacter)value.Clone(); }
        }

        /// <summary>Gets and sets the character displayed in the quad.</summary>
        public char Character
        {
            get { return m_character; }
            set { m_character = value; }
        }

        /// <summary>Gets and sets the size scale.</summary>
        public float SizeScale
        {
            get { return m_sizeScale; }
            set { m_sizeScale = value; }
        }
    }
}

The FontQuad class stores a few more pieces of information that we’ll use when we create_ the the bitmap font system. For example, we’ll use the LineNumber and WordWidth properties when justifying text. Also, since the texture will print fonts at a preset size, the SizeScale property will allow us to scale the character to achieve any size we want. The class that performs all these calculations is BitmapFont:

/// <summary>Bitmap font wrapper.</summary>
public class BitmapFont
{
    public enum Align { Left, Center, Right };
    private BitmapCharacterSet m_charSet;
    private List<FontQuad> m_quads;
    private List<StringBlock> m_strings;
    private string m_fntFile;
    private string m_textureFile;
    private Texture m_texture = null;
    private VertexBuffer m_vb = null;
    private const int MaxVertices = 4096;
    private int m_nextChar;

    /// <summary>Creates a new bitmap font.</summary>
    /// <param name="faceName">Font face name.</param>
    public BitmapFont( string fntFile, string textureFile )
    {
        m_quads = new List<FontQuad>();
        m_strings = new List<StringBlock>();
        m_fntFile = fntFile;
        m_textureFile = textureFile;
        m_charSet = new BitmapCharacterSet();
        ParseFNTFile();
    }

    /// <summary>Parses the FNT file.</summary>
    private void ParseFNTFile()
    {
        string fntFile = Utility.GetMediaFile( m_fntFile );
        StreamReader stream = new StreamReader( fntFile );
        string line;
        char[] separators = new char[] { ' ', '=' };
        while ( ( line = stream.ReadLine() ) != null )
        {
            string[] tokens = line.Split( separators );
            if ( tokens[0] == "info" )
            {
                // Get rendered size
                for ( int i = 1; i < tokens.Length; i++ )
                {
                    if ( tokens[i] == "size" )
                    {
                        m_charSet.RenderedSize = int.Parse( tokens[i + 1] );
                    }
                }
            }
            else if ( tokens[0] == "common" )
            {
                // Fill out BitmapCharacterSet fields
                for ( int i = 1; i < tokens.Length; i++ )
                {
                    if ( tokens[i] == "lineHeight" )
                    {
                        m_charSet.LineHeight = int.Parse( tokens[i + 1] );
                    }
                    else if ( tokens[i] == "base" )
                    {
                        m_charSet.Base = int.Parse( tokens[i + 1] );
                    }
                    else if ( tokens[i] == "scaleW" )
                    {
                        m_charSet.Width = int.Parse( tokens[i + 1] );
                    }
                    else if ( tokens[i] == "scaleH" )
                    {
                        m_charSet.Height = int.Parse( tokens[i + 1] );
                    }
                }
            }
            else if ( tokens[0] == "char" )
            {
                // New BitmapCharacter
                int index = 0;
                for ( int i = 1; i < tokens.Length; i++ )
                {
                    if ( tokens[i] == "id" )
                    {
                        index = int.Parse( tokens[i + 1] );
                    }
                    else if ( tokens[i] == "x" )
                    {
                        m_charSet.Characters[index].X = int.Parse( tokens[i + 1] );
                    }
                    else if ( tokens[i] == "y" )
                    {
                        m_charSet.Characters[index].Y = int.Parse( tokens[i + 1] );
                    }
                    else if ( tokens[i] == "width" )
                    {
                        m_charSet.Characters[index].Width = int.Parse( tokens[i + 1] );
                    }
                    else if ( tokens[i] == "height" )
                    {
                        m_charSet.Characters[index].Height = int.Parse( tokens[i + 1] );
                    }
                    else if ( tokens[i] == "xoffset" )
                    {
                        m_charSet.Characters[index].XOffset = int.Parse( tokens[i + 1] );
                    }
                    else if ( tokens[i] == "yoffset" )
                    {
                        m_charSet.Characters[index].YOffset = int.Parse( tokens[i + 1] );
                    }
                    else if ( tokens[i] == "xadvance" )
                    {
                        m_charSet.Characters[index].XAdvance = int.Parse( tokens[i + 1] );
                    }
                }
            }
            else if ( tokens[0] == "kerning" )
            {
                // Build kerning list
                int index = 0;
                Kerning k = new Kerning();
                for ( int i = 1; i < tokens.Length; i++ )
                {
                    if ( tokens[i] == "first" )
                    {
                        index = int.Parse( tokens[i + 1] );
                    }
                    else if ( tokens[i] == "second" )
                    {
                        k.Second = int.Parse( tokens[i + 1] );
                    }
                    else if ( tokens[i] == "amount" )
                    {
                        k.Amount = int.Parse( tokens[i + 1] );
                    }
                }
                m_charSet.Characters[index].KerningList.Add( k );
            }
        }
        stream.Close();
    }

    /// <summary>Call when the device is created.</summary>
    /// <param name="device">D3D device.</param>
    public void OnCreateDevice( Device device )
    {
        m_texture = new Texture( device, Utility.GetMediaFile( m_textureFile ),
            m_charSet.Width, m_charSet.Height, 0, Usage.None, Format.Dxt3, Pool.Managed, 
            Filter.Linear, Filter.Linear, 0, false, null );
    }

    /// <summary>Call when the device is destroyed.</summary>
    public void OnDestroyDevice()
    {
        if ( m_texture != null )
        {
            m_texture.Dispose();
            m_texture = null;
        }
    }

    /// <summary>Call when the device is reset.</summary>
    /// <param name="device">D3D device.</param>
    public void OnResetDevice( Device device )
    {
        m_vb = new VertexBuffer( device, MaxVertices * TransformedColoredTextured.StrideSize,
            Usage.Dynamic | Usage.WriteOnly, TransformedColoredTextured.Format, 
            Pool.Default, null );
    }

    /// <summary>Call when the device is lost.</summary>
    public void OnLostDevice()
    {
        if ( m_vb != null )
        {
            m_vb.Dispose();
            m_vb = null;
        }
    }

    /// <summary>Adds a new string to the list to render.</summary>
    /// <param name="text">Text to render</param>
    /// <param name="textBox">Rectangle to constrain text</param>
    /// <param name="alignment">Font alignment</param>
    /// <param name="size">Font size</param>
    /// <param name="color">Color</param>
    /// <param name="kerning">true to use kerning, false otherwise.</param>
    /// <returns>The index of the added StringBlock</returns>
    public int AddString( string text, RectangleF textBox, Align alignment, float size, 
        ColorValue color, bool kerning )
    {
        StringBlock b = new StringBlock( text, textBox, alignment, size, color, kerning );
        m_strings.Add( b );
        int index = m_strings.Count - 1;
        m_quads.AddRange( GetProcessedQuads( index ) );
        return index;
    }

    /// <summary>Removes a string from the list of strings.</summary>
    /// <param name="i">Index to remove</param>
    public void ClearString( int i )
    {
        m_strings.RemoveAt( i );
    }

    /// <summary>Clears the list of strings</summary>
    public void ClearStrings()
    {
        m_strings.Clear();
        m_quads.Clear();
    }

    /// <summary>Renders the strings.</summary>
    /// <param name="device">D3D Device</param>
    public void Render( Device device )
    {
        if ( m_strings.Count <= 0 )
        {
            return;
        }
            
        // Add vertices to the buffer
        GraphicsBuffer<TransformedColoredTextured> gb = 
            m_vb.Lock<TransformedColoredTextured>( 0, 6 * m_quads.Count, LockFlags.Discard );

        foreach ( FontQuad q in m_quads )
        {
            gb.Write( q.Vertices );
        }

        m_vb.Unlock();

        // Set render states
        device.SetRenderState( RenderStates.ZEnable, false );
        device.SetRenderState( RenderStates.FillMode, (int)FillMode.Solid );
        device.SetRenderState( RenderStates.ZBufferWriteEnable, false );
        device.SetRenderState( RenderStates.FogEnable, false );
        device.SetRenderState( RenderStates.AlphaTestEnable, false );
        device.SetRenderState( RenderStates.AlphaBlendEnable, true );
        device.SetRenderState( RenderStates.SourceBlend, (int)Blend.SourceAlpha );
        device.SetRenderState( RenderStates.DestinationBlend, (int)Blend.InvSourceAlpha );

        // Blend Texture and Vertex alphas
        device.SetTextureState( 0, TextureStates.ColorArgument1, (int)TextureArgument.Current );
        device.SetTextureState( 0, TextureStates.AlphaArgument1, (int)TextureArgument.Texture );
        device.SetTextureState( 0, TextureStates.AlphaArgument2, (int)TextureArgument.Diffuse );
        device.SetTextureState( 0, TextureStates.AlphaOperation, (int)TextureOperation.Modulate );

        // Set sampler states
        device.SetSamplerState( 0, SamplerStates.MinFilter, (int)Filter.Linear );
        device.SetSamplerState( 0, SamplerStates.MagFilter, (int)Filter.Linear );
        device.SetSamplerState( 0, SamplerStates.MipFilter, (int)Filter.Linear );

        // Render
        device.VertexFormat = TransformedColoredTextured.Format;
        device.SetTexture( 0, m_texture );
        device.SetStreamSource( 0, m_vb, 0, TransformedColoredTextured.StrideSize );
        device.DrawPrimitives( PrimitiveType.TriangleList, 0, 2 * m_quads.Count );
    }

    /// <summary>Gets the list of Quads from a StringBlock all ready to render.</summary>
    /// <param name="index">Index into StringBlock List</param>
    /// <returns>List of Quads</returns>
    public List<FontQuad> GetProcessedQuads( int index )
    {
        if ( index >= m_strings.Count || index < 0 )
        {
            throw new Exception( "String block index out of range." );
        }

        List<FontQuad> quads = new List<FontQuad>();
        StringBlock b = m_strings[index];
        string text = b.Text;
        float x = b.TextBox.X;
        float y = b.TextBox.Y;
        float maxWidth = b.TextBox.Width;
        Align alignment = b.Alignment;
        float lineWidth = 0f;
        float sizeScale = b.Size / (float)m_charSet.RenderedSize;
        char lastChar = new char();
        int lineNumber = 1;
        int wordNumber = 1;
        float wordWidth = 0f;
        bool firstCharOfLine = true;

        float z = 0f;
        float rhw = 1f;

        for ( int i = 0; i < text.Length; i++ )
        {
            BitmapCharacter c = m_charSet.Characters1];
            float xOffset = c.XOffset * sizeScale;
            float yOffset = c.YOffset * sizeScale;
            float xAdvance = c.XAdvance * sizeScale;
            float width = c.Width * sizeScale;
            float height = c.Height * sizeScale;

            // Check vertical bounds
            if ( y + yOffset + height > b.TextBox.Bottom )
            {
                break;
            }

            // Newline
            if ( text[i] == '\n' || text[i] == '\r' || ( lineWidth + xAdvance >= maxWidth ) )
            {
                if ( alignment == Align.Left )
                {
                    // Start at left
                    x = b.TextBox.X;
                }
                if ( alignment == Align.Center )
                {
                    // Start in center
                    x = b.TextBox.X + ( maxWidth / 2f );
                }
                else if ( alignment == Align.Right )
                {
                    // Start at right
                    x = b.TextBox.Right;
                }

                y += m_charSet.LineHeight * sizeScale;
                float offset = 0f;

                if ( ( lineWidth + xAdvance >= maxWidth ) && ( wordNumber != 1 ) )
                {
                    // Next character extends past text box width
                    // We have to move the last word down one line
                    char newLineLastChar = new char();
                    lineWidth = 0f;
                    for ( int j = 0; j < quads.Count; j++ )
                    {
                        if ( alignment == Align.Left )
                        {
                            // Move current word to the left side of the text box
                            if ( ( quads[j].LineNumber == lineNumber ) &&
                                ( quads[j].WordNumber == wordNumber ) )
                            {
                                quads[j].LineNumber++;
                                quads[j].WordNumber = 1;
                                quads[j].X = x + ( quads[j].BitmapCharacter.XOffset * sizeScale );
                                quads[j].Y = y + ( quads[j].BitmapCharacter.YOffset * sizeScale );
                                x += quads[j].BitmapCharacter.XAdvance * sizeScale;
                                lineWidth += quads[j].BitmapCharacter.XAdvance * sizeScale;
                                if ( b.Kerning )
                                {
                                    m_nextChar = quads[j].Character;
                                    Kerning kern = m_charSet.Characters[newLineLastChar].KerningList.Find( FindKerningNode );
                                    if ( kern != null )
                                    {
                                        x += kern.Amount * sizeScale;
                                        lineWidth += kern.Amount * sizeScale;
                                    }
                                }
                            }
                        }
                        else if ( alignment == Align.Center )
                        {
                            if ( ( quads[j].LineNumber == lineNumber ) &&
                                ( quads[j].WordNumber == wordNumber ) )
                            {
                                // First move word down to next line
                                quads[j].LineNumber++;
                                quads[j].WordNumber = 1;
                                quads[j].X = x + ( quads[j].BitmapCharacter.XOffset * sizeScale );
                                quads[j].Y = y + ( quads[j].BitmapCharacter.YOffset * sizeScale );
                                x += quads[j].BitmapCharacter.XAdvance * sizeScale;
                                lineWidth += quads[j].BitmapCharacter.XAdvance * sizeScale;
                                offset += quads[j].BitmapCharacter.XAdvance * sizeScale / 2f;
                                float kerning = 0f;
                                if ( b.Kerning )
                                {
                                    m_nextChar = quads[j].Character;
                                    Kerning kern = m_charSet.Characters[newLineLastChar].KerningList.Find( FindKerningNode );
                                    if ( kern != null )
                                    {
                                        kerning = kern.Amount * sizeScale;
                                        x += kerning;
                                        lineWidth += kerning;
                                        offset += kerning / 2f;
                                    }
                                }
                            }
                        }
                        else if ( alignment == Align.Right )
                        {
                            if ( ( quads[j].LineNumber == lineNumber ) &&
                                ( quads[j].WordNumber == wordNumber ) )
                            {
                                // Move character down to next line
                                quads[j].LineNumber++;
                                quads[j].WordNumber = 1;
                                quads[j].X = x + ( quads[j].BitmapCharacter.XOffset * sizeScale );
                                quads[j].Y = y + ( quads[j].BitmapCharacter.YOffset * sizeScale );
                                lineWidth += quads[j].BitmapCharacter.XAdvance * sizeScale;
                                x += quads[j].BitmapCharacter.XAdvance * sizeScale;
                                offset += quads[j].BitmapCharacter.XAdvance * sizeScale;
                                float kerning = 0f;
                                if ( b.Kerning )
                                {
                                    m_nextChar = quads[j].Character;
                                    Kerning kern = m_charSet.Characters[newLineLastChar].KerningList.Find( FindKerningNode );
                                    if ( kern != null )
                                    {
                                        kerning = kern.Amount * sizeScale;
                                        x += kerning;
                                        lineWidth += kerning;
                                        offset += kerning;
                                    }
                                }
                            }
                        }
                        newLineLastChar = quads[j].Character;
                    }

                    // Make post-newline justifications
                    if ( alignment == Align.Center || alignment == Align.Right )
                    {
                        // Justify the new line
                        for ( int k = 0; k < quads.Count; k++ )
                        {
                            if ( quads[k].LineNumber == lineNumber + 1 )
                            {
                                quads[k].X -= offset;
                            }
                        }
                        x -= offset;

                        // Rejustify the line it was moved from
                        for ( int k = 0; k < quads.Count; k++ )
                        {
                            if ( quads[k].LineNumber == lineNumber )
                            {
                                quads[k].X += offset;
                            }
                        }
                    }
                }
                else
                {
                    // New line without any "carry-down" word
                    firstCharOfLine = true;
                    lineWidth = 0f;
                }

                wordNumber = 1;
                lineNumber++;
                    
            } // End new line check

            // Don't print these
            if ( text[i] == '\n' || text[i] == '\r' || text[i] == '\t' )
            {
                continue;
            }

            // Set starting cursor for alignment
            if ( firstCharOfLine ) 
            {
                if ( alignment == Align.Left )
                {
                    // Start at left
                    x = b.TextBox.Left;
                }
                if ( alignment == Align.Center )
                {
                    // Start in center
                    x = b.TextBox.Left + ( maxWidth / 2f );
                }
                else if ( alignment == Align.Right )
                {
                    // Start at right
                    x = b.TextBox.Right;
                }
            }

            // Adjust for kerning
            float kernAmount = 0f;
            if ( b.Kerning && !firstCharOfLine )
            {
                m_nextChar = (char)text[i];
                Kerning kern = m_charSet.Characters[lastChar].KerningList.Find( FindKerningNode );
                if ( kern != null )
                {
                    kernAmount = kern.Amount * sizeScale;
                    x += kernAmount;
                    lineWidth += kernAmount;
                    wordWidth += kernAmount;
                }
            }

            firstCharOfLine = false;

            // Create the vertices
            TransformedColoredTextured topLeft = new TransformedColoredTextured(
                x + xOffset, y + yOffset, z, rhw, b.Color.ToArgb(), 
                (float)c.X / (float)m_charSet.Width,
                (float)c.Y / (float)m_charSet.Height );
            TransformedColoredTextured topRight = new TransformedColoredTextured(
                topLeft.X + width, y + yOffset, z, rhw, b.Color.ToArgb(), 
                (float)( c.X + c.Width ) / (float)m_charSet.Width,
                (float)c.Y / (float)m_charSet.Height );
            TransformedColoredTextured bottomRight = new TransformedColoredTextured(
                topLeft.X + width, topLeft.Y + height, z, rhw, b.Color.ToArgb(), 
                (float)( c.X + c.Width ) / (float)m_charSet.Width,
                (float)( c.Y + c.Height ) / (float)m_charSet.Height );
            TransformedColoredTextured bottomLeft = new TransformedColoredTextured(
                x + xOffset, topLeft.Y + height, z, rhw, b.Color.ToArgb(), 
                (float)c.X / (float)m_charSet.Width,
                (float)( c.Y + c.Height ) / (float)m_charSet.Height );

            // Create the quad
            FontQuad q = new FontQuad( topLeft, topRight, bottomLeft, bottomRight );
            q.LineNumber = lineNumber;
            if ( text[i] == ' ' && alignment == Align.Right )
            {
                wordNumber++;
                wordWidth = 0f;
            }
            q.WordNumber = wordNumber;
            wordWidth += xAdvance;
            q.WordWidth = wordWidth;
            q.BitmapCharacter = c;
            q.SizeScale = sizeScale;
            q.Character = text[i];
            quads.Add( q );

            if ( text[i] == ' ' && alignment == Align.Left )
            {
                wordNumber++;
                wordWidth = 0f;
            }

            x += xAdvance;
            lineWidth += xAdvance;
            lastChar = text[i];

            // Rejustify text
            if ( alignment == Align.Center )
            {
                // We have to recenter all Quads since we addded a 
                // new character
                float offset = xAdvance / 2f;
                if ( b.Kerning )
                {
                    offset += kernAmount / 2f;
                }
                for ( int j = 0; j < quads.Count; j++ )
                {
                    if ( quads[j].LineNumber == lineNumber )
                    {
                        quads[j].X -= offset;
                    }
                }
                x -= offset;
            }
            else if ( alignment == Align.Right )
            {
                // We have to rejustify all Quads since we addded a 
                // new character
                float offset = 0f;
                if ( b.Kerning )
                {
                    offset += kernAmount;
                }
                for ( int j = 0; j < quads.Count; j++ )
                {
                    if ( quads[j].LineNumber == lineNumber )
                    {
                        offset = xAdvance;
                        quads[j].X -= xAdvance;
                    }
                }
                x -= offset;
            }
        }
        return quads;
    }

    /// <summary>Gets the line height of a StringBlock.</summary>
    public float GetLineHeight( int index )
    {
        if ( index < 0 || index > m_strings.Count )
        {
            throw new Exception( "StringBlock index out of range." );
        }
        return m_charSet.LineHeight * ( m_strings[index].Size / m_charSet.RenderedSize );
    }

    /// <summary>Search predicate used to find nodes in m_kerningList</summary>
    /// <param name="node">Current node.</param>
    /// <returns>true if the node's name matches the desired node name, false otherwise.</returns>
    private bool FindKerningNode( Kerning node )
    {
        return ( node.Second == m_nextChar );
    }

    /// <summary>Gets the font texture.</summary>
    public Texture Texture
    {
        get { return m_texture; }
    }
}

The BitmapFont class performs all the necessary initializations and calculations to print bitmap fonts. Since BitmapFont contains both a dynamic VertexBuffer and a Texture, we can see the normal resource On*Device methods. Initializing BitmapFont is as easy as calling the constructor with the name of the fnt file and the image generated by the Angelcode Bitmap Font Generator. The constructor calls ParseFNTFile which performs some basic text file I/O to store all the information into Lists of the data storage class described earlier.

The AddString, ClearString, and ClearStrings methods simply add or clear strings from the BitmapFont’s list of StringBlocks to render. The AddString method returns the index of the generated StringBlock, which you can use to either delete the StringBlock later with the ClearString method or get the List of raw FontQuads generated by GetProcessedQuads for use in a different Vertex Buffer.

The GetProcessedQuads method is the main method of BitmapFont. It performs all the calculations required to create_ and align each character of a StringBlock. The calcaulations are a bit long and took some pen and paper work so it’s probably best to look through the code yourself to understand it. Here is a list of items that we have to consider in the calculations:

  • Text is generated character by character. When a character extends past the left or right edge of the bounding rectangle, we have to move all the characters of that same word to the next line.
  • When text is center-aligned, whenever we add a new character, we have to recenter the text on the current line. When this line extends past the width of the bounding rectangle, we have to move the last word onto a new line, center the new line, and recenter the previous line from which the word was removed.
  • When text is right-aligned, we add new characters on the right edge of the current line and then shift all the characters of this line to the left. When we have to create_ a new line, we move the last word to the new line, right-justify the new line, and rejustify the previous line from which the word was removed.
  • Whenever a letter extends passed the bottom of the bounding rectangle, stop building the quads. If you see text being cut off, this is probably the reason why. Just extend the height of the bounding rectangle.
  • In order to support arbitrary sizes, we create_ a size scale by dividing the desired rendering size by the size at which the text was generated in the texture. We then multiply all the placement values of BitmapCharacter by this size scale.
  • When kerning should be used, look up the kerning information for the current character and adjust the cursor position.

When we render all the text, we first have to set a few renderstates in order for the text to appear correctly. Since we don’t want the text to have any Z-depth information, we disable RenderStates.ZEnable and RenderStates.ZBufferWriteEnable. We also want to make sure we render the text in FillMode.Solid. The alpha blending renderstates are used to render only the textured letter and not its background. The SamplerStates just improve the visual quality of the text when we scale it to new sizes. Note that since we are changing all these RenderStates, we have to change them back in our program to make sure any geometry that we render there is rendered correctly. We’ll usually want to reenable RenderStates.ZEnable and RenderStates.ZBufferWriteEnable and if we’re not using alpha transparency, disable RenderStates.AlphaBlendEnable.

That’s pretty much all there is to using BitmapFont. Some notes in using BitmapFont: Whenever you want to update_ text, clear all the strings with ClearStrings before adding new text or else the new text will just be appended to the list of text to render. Also, you may have noticed the following line of code in the tutorials:

// Only need to rebuild the text when the FPS updates
if ( m_fps != m_framework.FPS ) {
    m_fps = m_framework.FPS;
    BuildText();
}

Since the fps only updates every half second, there’s no point in building all the text every frame. We increase performance by only building the text when it changes.

Some notes on the Angelcode Bitmap Font Generator: I noticed I got better results by adding about 2 pixels of padding and 1 pixel of spacing around each character. Without these space buffers, I was seeing some distortion in scaled text. Also, the BitmapFont class only supports fonts where all the characters are on 1 page, so make sure you keep the text on one texture by either reducing the font size or increasing the dimensions of the texture. And finally, make sure you select “Bit depth 32″ in order to have an alpha channel, which is necessary to render the fonts properly.