Rendering text, not that simple at all. (part 1)

It was a kind of surprise to discover that there’s no straightforward way to render text using OpenGL, for people new to graphics programming like me drawing a text looks like a very primitive functionality which I expect to be available by default as functions like glDrawText(some context,const char*).

But that’s not the case, and rendering text just happen to be a very non trivial task! My first implementation of a function able to render text was very straightforward, which was making me feel confident about how simple could be drawing text in OpenGL, here’s the code:


void ui::draw_string(uint32_t x_pos,
                     uint32_t y_pos,
                     const std::string& text)
{
    glRasterPos2i(x_pos,
                  y_pos);
    for( std::size_t i=0; i < text.size(); i++)
    {
        glutBitmapCharacter(GLUT_BITMAP_TIMES_ROMAN_10,
                            text[i]); // Print a character on the screen
    }
}

Isn’t this simple? glRasterPos2i modify the raster position at which the following pixel operations are going to be performed, and glutBitmatCharacter is a GLUT library function which just draw the bitmap corresponding to the character in text[i] using the font TimesRoman size 10.

This simple piece of code has a lot of limitation, in the amount of fonts you can use, in the way the texts is rendered (fonts,colors, scale, effects, &c) and is substantially good only for very simple applications.

When I was looking for a more ‘advanced’ method for drawing text I discovered that there’s no simple way for doing that! I found different libraries which in a way or another can accomplish this task but I found that most of them are too complex for my applications or didn’t had the interfaces I needed.

If I have to spent time learning a new over complicated library only for the purpose of drawing text then probably is better to write a simple library myself, at the end I do not need that many fancy functionalities, and for sure I need to learn as much as possible about OpenGL to continue my project.

For this reason I started searching the web for help, in particular I found this website to be extremely useful and very well organized, have a look yourself if you wish to learn something about OpenGL.

My tiny text rendering functionality using FreeType.

I want it to be simple and easy to use, at the moment  I do not need any fancy thing but:

  • Ability to set the position where to draw the text
  • Ability to choose multiple font, colors &c
  • Ability apply some transformation on the text, for example scaling it.
  • No compatibility issue with GLFW (I use GLFW heavily)

I do not need support for each possible character set, implementing a rendering function which support more than the standard 128 ASCII symbols is actually pretty difficult so I will stick only to 128 characters.

To implement this feature I will use the FreeType font render library, which is very powerful and not necessarily that complex for the use I’m doing with it. The code I’m going to show you is not necessarily a state of the art implementation, I’m still myself learning how to work with OpenGL, FreeType, GLFW &c, so keep in mind that they can be much better ways of doing what I’m doing.

What I’m going to do is extract from my code the parts needed to make it possible to explain what is happening, the full source is available here.

The first thing you need to do to make any use of the FreeType library is load it!


    FT_Library   freetype_lib;
    FT_Error error = FT_Init_FreeType(&freetype_lib);
    if(error){
        ERR("Unable to load the freetype library, error code: ",
            error);
    }

This code is extracted from my repo for the purpose of showing here how to load the library, the call to FT_Init_FreeType returns a FT_Error type which if is zero if the operation succeed, if is not zero the code report to the logger an error message and eventually perform any other corrective action; The corrective action my code is taking actually is throwing an exception 🙂

Next we need to open and load a font, for this purpose a valid font path need to be provided, the very basic code looks like this:

    FT_Face font_face;
    FT_Error error = FT_New_Face(freetype_lib,
                                 font_name.c_str(),
                                 0,
                                 &font_face);
    if(error)
    {
        ERR("Failed to load the font: ",
            font_name);
    }

Where font_name is a std::string with the path of the font you are willing to load. Again if the operation fails the return code will contain a not zero value.

Once we have the font loaded the next step is to extract the glyphs for each character and generate a texture that can be used later to render text. Each glyph contains the information needed to properly draw the character, those information are going to be used later to identify the proper position of the next symbol we’re going to render.

Glyph metrics
Glyph metrics

All the details about the glyphs are contained in FT_Facem, as you can see there are a lot of values,we will see some details about some of them shortly.  Since I don’t want to load each and every time I need to render a text the glyphs, and I don’t want as well to generate each time the texture then I will save those texture information in a structure and will extract those information when needed.

Since I want to load multiple fonts I’ll need to store multiple character set with the relative textures, for this reason I’m collecting all those information in a specifically designed object, the font_texture_loader!

// Holds all state information relevant to a character as loaded using FreeType
struct character_data {
    GLuint TextureID;   // ID handle of the glyph texture
    glm::ivec2 Size;    // Size of glyph
    glm::ivec2 Bearing;  // Offset from baseline to left/top of glyph
    FT_Pos Advance;    // Horizontal offset to advance to next glyph
};

/*
 * Contains the information required to render a font set
 */
struct font_texture
{
    std::string font_name;
    std::unordered_map<GLchar,character_data> charset;
};

using font_texture_ptr = std::shared_ptr<font_texture>;

/*
 * Instead of passing everywhere the font name
 * the code will identify the various font by
 * a unique ID
 */
using font_type_id = int;

class font_texture_loader
{
    FT_Library   freetype_lib;
    font_type_id next_id;
    std::unordered_map<font_type_id,font_texture_ptr> fonts;
    font_type_id default_font_id;
public:
    font_texture_loader();
    std::pair<font_type_id,font_texture_ptr> load_new_textureset(const std::string& font_name);
    font_texture_ptr get_texture(font_type_id id);
    font_type_id get_default_font_id();
};

For each loaded font the code generate a unique ID then store the relevant information in the fonts hash map, where for each ID I can load a font_texture struct which in turn contains all the texture loaded for the given font.font_texture_loader load a default texture from a default location, the ID for the default font is returned by get_default_font_id.

As you have noticed the code is not storing explicitly any texture but instead just a number. This number is mapped by OpenGL to a real texture whenever it is necessary, note also that I’m explicitly saving the Advance field from the glyph, this is important since this field will be constantly used to calculate the position of the next character that is going to be rendered.

The glyphs are loaded and the texture are generated by the following code:

new_font = std::make_shared<font_texture>();
new_font->font_name = font_name;
// Set size of the glyph
FT_Set_Pixel_Sizes(font_face, 0, 48);

//Make sure that the proper alignment is set
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);

// Load first 128 characters of ASCII set
for (GLubyte current_char = 0; current_char < 128; current_char++)
{ 
    //Load character glyph 
    if(FT_Load_Char(font_face, current_char, FT_LOAD_RENDER)) 
    { 
        ERR("Failed to load the Glyph for the requested font!"); 
        return {0,nullptr}; 
    } 
    //Generate texture 
    GLuint texture; 
    glGenTextures(1, &texture); 
    glBindTexture(GL_TEXTURE_2D, texture); 
    glTexImage2D( GL_TEXTURE_2D, 0, GL_RED, font_face->glyph->bitmap.width,
        font_face->glyph->bitmap.rows,
        0,
        GL_RED,
        GL_UNSIGNED_BYTE,
        font_face->glyph->bitmap.buffer
        );
    // Set texture options
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    // Now store character for later use
    character_data character = {
        texture,
        glm::ivec2(font_face->glyph->bitmap.width,
                font_face->glyph->bitmap.rows),
        glm::ivec2(font_face->glyph->bitmap_left,
                font_face->glyph->bitmap_top),
        font_face->glyph->advance.x
    };
    new_font->charset.insert({current_char,character});
}

glBindTexture(GL_TEXTURE_2D, 0);
FT_Done_Face(font_face);

new_font_id = next_id++;
LOG1("Font ",font_name," loaded properly!");
fonts.insert({new_font_id,
              new_font});

Basically the code creates a font_texture object which is populated with the 128 characters from the ASCII set, for each character a new texture is generated and stored in character_data, in this way when I need it I can easily retrieve all the texture for the strings I’m willing to draw.

Note how the data used to generate the texture are retrieved from the glyph structure, for each loaded character it contains a bitmap buffer with the image of the symbol. The size of this image depends on the value provided to FT_Set_Pixel_Sizes.

Note that this code has the limitation of not being able to handle symbols which are not in the range of characters from the ASCII set, as I wrote at the very beginning of this post I’m accepting this limitation since the use of this code will be limited to rather simple text rendering purpose.

For details about the purpose of each specific function you should really have a look here or here, the purpose of this post is not to describe in details what OpenGL is doing, but to show you how I can use the OpenGL functionalities to implement a simple text rendering class.

Shaders

In order to render anything using modern OpenGL you have to use at least two shaders.

Shaders are small but not necessarily simple (and not necessarily that small) applications which runs on the GPU, they are written in GLSL a language which is in a certain way very similar to C. A shader pick some input make some transformation on it and produce an output, in a moder OpenGL pipeline  in order to render things we need to provide two of them:

  • vertex shader, which tranform each 3D coordinate in a way that allow the vertex to be rendered in the proper position on the screen
  • fragment shader, which allow us to apply colors to pixels.

This is a very simplistic explanation of what they are for, but I think at this point is sufficient.

Once we have written our shaders we need to compile them, link them and then use them before drawing anything in our rendering loop, the vertex and fragment shaders will be used by the GPU to calculate the proper location and color of each pixel (Let’s make this explanation simple)

Compilation and linking are performed at runtime, let’s see how looks like my simple class for handling shaders:

class my_small_shaders
{
    GLuint vertex_shader,
           fragment_shader,
           shader_program;
    GLchar log_buffer[512];
    void load_shader_generic(GLuint& shader_target,
                             const std::string& body,
                             GLenum shader_type);
public:
    my_small_shaders();
    void load_vertex_shader(const std::string& body);
    void load_fragment_shader(const std::string& body);
    bool create_shader_program();
    void use_shaders();
};

Here I’m trying to make it very simple, before using a shader the code must be loaded, a shader program must be create and the code must be linked. To load the shader the functions load_vertex_shader and load_fragment_shader are used, then a call to create_shader_program will compile and link the shaders in order to make it available for later usage, the call to use_shader during a drawing phase cause the shaders to be used to render the current frame.

Since the code is identical for both the vertex and fragment loader, the class have a generic function load_shader_generic which work for both,  to choose which particular shader one want’s to load a proper target must be provided (GL_VERTEX_SHADER or GL_FRAGMENT_SHADER). Here’s the code:

void my_small_shaders::load_shader_generic(GLuint &shader_target,
                                           const std::string &body,
                                           GLenum shader_type)
{
    const char* body_ptr = body.c_str();
    LOG3("Compiling shader: ",
         shader_type);
    shader_target = glCreateShader(shader_type);
    glShaderSource(shader_target,
                   1,
                   &body_ptr,
                   nullptr);
    glCompileShader(shader_target);
    GLint success;
    glGetShaderiv(shader_target,
                  GL_COMPILE_STATUS,
                  &success);
    if(!success){
        ERR("Compilation failed!");
        glGetShaderInfoLog(shader_target,
                           512,
                           nullptr,
                           log_buffer);
        ERR("Detailed information: ",log_buffer);
    }
}

Note how OpenGL will map the loaded shader with a number ID, whenever a reference to a shader is needed then is sufficient to provide the ID handle.

The sequence glShaderSource and glCompileShader will load and compile the shader code, the call to glGetShaderiv is used to retrieve the status of the compilation (GL_COMPILE_STATUS), if for some reason the compilation failed the code will print a diagnostic.

Once we know that the shader compiles without problems we can attempt to create the shader program and attach to it our two shaders, then we need to link the program to make sure is usable, this is the code:

bool my_small_shaders::create_shader_program()
{
    LOG3("Creating the shader program");
    shader_program = glCreateProgram();
    //Attach our two shaders
    glAttachShader(shader_program,
                   vertex_shader);
    glAttachShader(shader_program,
                   fragment_shader);
    //Link the shaders
    glLinkProgram(shader_program);
    //Check for errors
    GLint success;
    glGetProgramiv(shader_program,
                  GL_LINK_STATUS,
                  &success);
    if(!success){
        ERR("Linking failed!");
        glGetShaderInfoLog(shader_program,
                           512,
                           nullptr,
                           log_buffer);
        ERR("Detailed information: ",log_buffer);
        return false;
    }
    return true;
}

Again the code check with glGetProgramiv if the linking succeed, if for some reason it fails a diagnostic is printed. The function glGetShaderInfoLog allows the programmer to obtain detailed information about the last failure, an error message (if available) will be copied to the buffer log_buffer.

Using the shaders is actually very simple, the implementation of use_shaders is the following:

void my_small_shaders::use_shaders()
{
    glUseProgram(shader_program);
}

Where glUseProgram make sure that our shaders are going to be used in the current rendering phase.

Partial conclusion.

Now I’m ready to show you how to render some text using OpenGL, but unfortunately this post is already very long! So I decided to split the material in two parts, the next part will describe what is left.

Stay tuned on your favorite blog!

Thanks for reading.

Leave a Reply