Load TrueType Fonts with FreeType and build an OpenGL Vertex Array / VBO to render text

I think these should be all the required includes. Of course you must link against freetype and opengl in order for everything to work.

#include <iostream>
#include <vector>
#include <string>
#include <gl/gl.h>
#include <gl/glu.h>
#include <ft2build.h>
#include <freetype/freetype.h>
#include <freetype/ftglyph.h>
#include <boost/shared_ptr.hpp>

Declare a couple of constants to hold the number of characters to process and the sizes of the textures.

const uint32_t m_number_of_chars = 512;
const uint32_t m_texture_max_width = 2048;

With this function you calculate the next power of two of a given number. You need this to make the height of the texture be a power of two.

inline int next_p2 ( int a )
{
	int rval=1;
	while(rval<a) rval<<=1;
	return rval;
}

We use these structs/classes to manage the Vertex Arrays / VBOs

    struct vertex_node
    {
        vertex_node():x(0.0f),y(0.0f),z(0.0f){}
        float x,y,z;
    };

    struct uv_node
    {
        uv_node():u(0.0f),v(0.0f){}
        float u,v;
    };

    template <class T>
    class buffer
    {
        public:
            buffer()
            {
                data = NULL;
                size = 0;
                name = 0;
            }

            virtual ~buffer()
            {
                deallocate();
            }

            void allocate(const uint32_t& amount)
            {
                deallocate();
                size = amount;
                data = new T[size];
            }

            void deallocate()
            {
                delete [] data;
                data = NULL;
            }

            T*		    data;
            uint32_t	name;
            uint32_t	size;
    };

    typedef buffer<vertex_node>          vertex;
    typedef boost::shared_ptr<vertex>    vertex_ptr;

    typedef buffer<uv_node>              uv;
    typedef boost::shared_ptr<uv>        uv_ptr;

This is a strip of my texture class with the relevant code to this example

class texture
{
    public:
        uint32_t m_id;

        bool create_two_channel (uint32_t width, uint32_t height, uint8_t* data)
        {
            glGenTextures(1, &m_id);
            glBindTexture( GL_TEXTURE_2D, m_id);
            glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_NEAREST);
            glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_NEAREST);

            glTexImage2D( GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_LUMINANCE_ALPHA, GL_UNSIGNED_BYTE, data );
        }
};
typedef boost::shared_ptr<texture> texture_ptr;

This class holds the information we need to draw the text and the texture

    class font
    {
        public:

            struct char_info_t
            {
                int x;
                int y;
                int width;
                int height;
                int left;
                int top;
                int advance;
                int row;

                uv_node     uv[4];
                vertex_node v[4];
                uint8_t* bitmap;
            };

            struct font_info_t
            {
                int max_height;
                std::vector<font::char_info_t> ch;
            };

            font_info_t    info;
            texture_ptr    texture;
        private:

    };
    typedef boost::shared_ptr<font>         font_ptr;

This enormous bunch is the code that loads the TTF font. It opens the file from a memory buffer. This buffer contains the file exactly as it is on the disk. I leave to you the code to load the file to the buffer. If you can follow all this code, you can also write that for yourself. :)

font_ptr load_font(const std::string& font_file_path, uint32_t font_size)
{
    /* HERE YOU MUST LOAD YOUR TTF FILE INTO THE BUFFER */
    uint32_t file_size =
    uint8_t* file_buffer =
    /* HERE YOU MUST LOAD YOUR TTF FILE INTO THE BUFFER */

    font_ptr pFont = font_ptr(new font());

    // Initialize Freetype
    FT_Library ft_library;
    FT_Face    ft_face;

    if (FT_Init_FreeType( &ft_library ))
    {
        throw std::runtime_error("FT_Init_FreeType failed");
    }

    // Load the requested face
	if (FT_New_Memory_Face( ft_library, (const FT_Byte*)file_buffer, file_size, 0, &ft_face ))
    {
        throw std::runtime_error("FT_New_Face failed (there is probably a problem with your font file)");
    }

    // Set the font size
    FT_Set_Char_Size( ft_face, font_size<< 6, font_size << 6, 96, 96);

    //Initialize the char_info array
    pFont->info.ch.resize(m_number_of_chars);
    pFont->info.max_height = 0;

    // Variables to hold the size the texture must have to hold all the bitmaps
    int max_width = 0;
    int max_rows = 0;

    // Create all the characters
    for(unsigned int ch=0; ch<m_number_of_chars; ch++)
    {
        //Load the Glyph for our character.
        if(FT_Load_Glyph( ft_face, FT_Get_Char_Index( ft_face, ch ), FT_LOAD_NO_HINTING ))
        {
            throw std::runtime_error("FT_Load_Glyph failed");
        }

        //Move the face's glyph into a Glyph object.
        FT_Glyph glyph;
        if(FT_Get_Glyph( ft_face->glyph, &glyph ))
        {
            throw std::runtime_error("FT_Get_Glyph failed");
        }

        //Convert the glyph to a bitmap.
        FT_Glyph_To_Bitmap( &glyph, FT_RENDER_MODE_NORMAL, 0, 1 );
        FT_BitmapGlyph bitmap_glyph = (FT_BitmapGlyph)glyph;

        //This reference will make accessing the bitmap easier
        FT_Bitmap& bitmap=bitmap_glyph->bitmap;

        //Get a pointer to the char info for easy access
        font::char_info_t* char_info = &pFont->info.ch[ch];
        char_info->width = bitmap.width;
        char_info->height = bitmap.rows;

        //Allocate memory for the bitmap data.
        char_info->bitmap = new uint8_t[2 * char_info->width * char_info->height];
        uint8_t* char_bmp = char_info->bitmap;

        // Fill the bitmap data
        for(int j=0; j <char_info->height;j++)
        {
            for(int i=0; i < char_info->width; i++)
            {
                char_bmp[2*(i+j*char_info->width)]= char_bmp[2*(i+j*char_info->width)+1] =
                    (i>=bitmap.width || j>=bitmap.rows) ?
                    0 : bitmap.buffer[i + bitmap.width*j];
            }
        }

        // Accumulate the width of the bitmaps. Increase the rows if we reached the width of the texture
        max_width += char_info->width;
        if (max_width>m_texture_max_width-1)
        {
            max_width = char_info->width;
            max_rows++;
        }

        // Determine what's the maximum height a given character might have
        if (char_info->height>pFont->info.max_height)
        {
            pFont->info.max_height = char_info->height;
        }

        // Store information about this character. We will use this to print it.
        char_info->row = max_rows;
        char_info->left = bitmap_glyph->left;
        char_info->top = bitmap_glyph->top;
        char_info->advance = ft_face->glyph->advance.x >> 6;
        char_info->x = max_width - char_info->width;
    }

    // Multiply the maximum height a character might have by the amount of rows and calculate the proper (power of two) height the texture must have.
    int texture_height = next_p2(pFont->info.max_height*(max_rows+1));

    // Create the temporary buffer for the texture
    uint8_t* texture_data = new uint8_t[m_texture_max_width*texture_height*2];

    // Fill the texture, set the vertex and uv array values and delete the bitmap
    for(unsigned int ch=0; ch<m_number_of_chars; ch++)
    {
        font::char_info_t* char_info = &pFont->info.ch[ch];

        char_info->y = pFont->info.max_height*char_info->row;

        fill_texture_data(ch, &pFont->info, m_texture_max_width,  texture_data);

        //(x,h)
        char_info->uv[0].u = (float)char_info->x/m_texture_max_width;
        char_info->uv[0].v = (float)(char_info->y+char_info->height)/texture_height;
        char_info->v[0].x = 0.0f;
        char_info->v[0].y = char_info->height;

        //(x,y)
        char_info->uv[1].u = (float)char_info->x/m_texture_max_width;               
        char_info->uv[1].v = (float)char_info->y/texture_height;
        char_info->v[1].x = 0.0f;
        char_info->v[1].y = 0.0f;

        //(w,y)
        char_info->uv[2].u = (float)(char_info->x+char_info->width)/m_texture_max_width; 
        char_info->uv[2].v = (float)(float)char_info->y/texture_height;
        char_info->v[2].x = char_info->width;
        char_info->v[2].y = 0.0f;

        //(w,h)
        char_info->uv[3].u = (float)(char_info->x+char_info->width)/m_texture_max_width; 
        char_info->uv[3].v = (float)(char_info->y+char_info->height)/texture_height;
        char_info->v[3].x = char_info->width;
        char_info->v[3].y = char_info->height;

        delete [] pFont->info.ch[ch].bitmap;
    }

    // Create the texture and delete the temporary buffer
    pFont->texture = texture_ptr(new texture);
    pFont->texture->create_two_channel(m_texture_max_width, texture_height, texture_data);
    delete [] texture_data;

    FT_Done_Face(ft_face);
    FT_Done_FreeType(ft_library);

	return pFont;
}

This function copies a specific char bitmap into the texture buffer at the proper location.

void fill_texture_data(uint32_t ch, font::font_info_t* font, uint32_t texture_width, uint8_t* texture_data)
{
    font::char_info_t* char_info = &font->ch[ch];
    uint8_t* char_bmp = char_info->bitmap;

    int bmp_pos = 0;
    int tex_pos = 0;

    int char_x = char_info->x;
    int char_y = char_info->y;
    int char_width = char_info->width;
    int char_height = char_info->height;

    for(int bmp_y=0; bmp_y<char_height; bmp_y++)
    {
        for(int bmp_x=0; bmp_x<char_width; bmp_x++)
        {
            bmp_pos = 2 * ( bmp_x + bmp_y * char_width);
            tex_pos = 2 * ( (char_x + bmp_x) + ( (char_y + bmp_y) * texture_width) );

            texture_data[tex_pos] = char_bmp[bmp_pos];
            texture_data[tex_pos+1] = char_bmp[bmp_pos+1];
        }
    }
}

With this class you can build a VertexArray for the string you want to draw.

class text_buffer
{
    public:
        void set(std::wstring& str, font_ptr& font)
        {
            int size = str.size();

            va = vertex_ptr(new vertex());
            va->allocate(size*4);
            
            uv = vertex_ptr(new uv());
            uv->allocate(size*4);

            int advance = 0;
            int i = 0;
            std::wstring::iterator c = str.begin();
            std::wstring::iterator const tmp_end = str.end();

            font::char_info_t* ci;

            // Fill vertex data
            for(; c != tmp_end; ++c)
            {
                ci = &font->info.ch[*c];

                va->data[i+0].x = ci->left+advance+ci->v[0].x;
                va->data[i+0].y = ci->v[0].y + (font->info.max_height-ci->top);
                va->data[i+1].x = ci->left+advance+ci->v[1].x;
                va->data[i+1].y = ci->v[1].y + (font->info.max_height-ci->top);
                va->data[i+2].x = ci->left+advance+ci->v[2].x;
                va->data[i+2].y = ci->v[2].y + (font->info.max_height-ci->top);
                va->data[i+3].x = ci->left+advance+ci->v[3].x;
                va->data[i+3].y = ci->v[3].y + (font->info.max_height-ci->top);

                advance += ci->advance;
                i+=4;
            }

            // Fill UV data
            i = 0;
            c = str.begin();
            for(; c != tmp_end; ++c)
            {
                ci = &font->info.ch[*c];

                uv->data[i+0].u = ci->uv[0].u;
                uv->data[i+0].v = ci->uv[0].v;
                uv->data[i+1].u = ci->uv[1].u;
                uv->data[i+1].v = ci->uv[1].v;
                uv->data[i+2].u = ci->uv[2].u;
                uv->data[i+2].v = ci->uv[2].v;
                uv->data[i+3].u = ci->uv[3].u;
                uv->data[i+3].v = ci->uv[3].v;

                i+=4;
            }
        }

        uv_ptr        uv;
        vertex_ptr    va;
};
typedef boost::shared_ptr<text_buffer>        text_buffer_ptr;

Finally, this is how you use the text_buffer class to construct a vertex array representing the string you want to draw.

int main()
{
    /* INITIALIZE OPENGL */

    font_ptr pFont = load_font("arial.ttf", 18);
    text_buffer_ptr tbuff = text_buffer_ptr(new text_buffer());
    std::wstring str = L"Hello World";
    tbuff->set(str, pFont);

    /* HERE YOU DRAW YOUR VERTEX ARRAY */

    return 0;
}

Hope you find this useful.

Note: Most of the FreeType loading code is taken from Nehe's Freetype article.

Note2: The code assumes the screen origin is located at the top-left corner. (See the method text_buffer::set, where it sets the position of the quads using the (font->info.max_height-ci->top) formula

About these ads

, ,

  1. #1 by feanor on October 19, 2012 - 5:49 pm

    How do you draw the text?
    If you draw it with glDrawArrays or similar how do you create the buffers?

    Beside, nice code thanks for sharing that.

    • #2 by damianpaz on October 19, 2012 - 7:35 pm

      You can draw the buffer using VAs or VBOs. You need to set an ortographic view with origin at the top-left corner.
      Enable texturing, alpha blending, disable lighting, and bind the buffers *uv* and *va* of the class text_buffer, with:

          // IF VBO
          {
             glBindBufferARB( GL_ARRAY_BUFFER_ARB, text_buffer->uv->name );
             glTexCoordPointer( 2, GL_FLOAT, 0, (char *) NULL );
      
             glBindBufferARB( GL_ARRAY_BUFFER_ARB, text_buffer->va->name );
             glVertexPointer( 3, GL_FLOAT, 0, (char *) NULL );
          }
         // ELSE VA
          {
             glTexCoordPointer( 2, GL_FLOAT, 0, text_buffer->uv->data );
             glVertexPointer( 3, GL_FLOAT, 0, text_buffer->va->data );
          }
      // And then
      
          glDrawArrays( GL_QUADS, 0, buffer->size );
      

      If you use VBO's then you must upload them to the video card and save the id of the buffer. I use the member variable *name* of the class buffer for that.

      • #3 by feanor on October 20, 2012 - 5:54 am

        Thanks! It works perfect!

  2. #4 by Anton on January 25, 2013 - 10:55 pm

    Shouldn’t line 11 in text_buffer be
    m_uv = uv_ptr(new uv()); // uv_ptr not vertext_ptr and rename of m_uv prevents name collision with uv
    ?

    • #5 by damianpaz on January 27, 2013 - 9:04 am

      Good call. I’ll update the post. Thanks.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: