Finally I’m back! September was a long and busy month, as you probably know I changed my job and had a lot of paperwork to do in relation with this event! But now it’s done! I’m on-board of a great project with great people, its gonna be cool I think.
Before sharing details about my duties and my project I would like myself to know better what I’m doing, you know, after just few days of work one have just a basic understanding of a new complex project like the one I’m involved in.
The company I’m working for is Oracle and I had no previous experience in any of the products Oracle is developing,will write a post sooner or later with some details about my current activities, what I can tell you now is that I’m going to be involved in some exciting new features for the MySQL!
Anyway, with this post I want to close the topic about rendering text in OpenGL, this is the second part of a two part post, the first half of the material is available here.
Rendering text.
In the first part of the post we ended up with having an object able to load fonts, a mechanism to load compile and link shaders and some basic knowledge about how chars are represented by glyphs.
To render text we need to put all this together and add the code responsible for the rendering of the extracted from the glyph texture. Recall that for each character we have a character_data structure which contains the relevant informations about the character, those are retrieved from the font data.
This structure character_data holds the TextureID information which can be used to render the char. (Read the first part of the post for details).
class renderable_text { GLuint VAO,VBO; font_texture_loader font_loader; shaders::my_small_shaders text_render_shader; font_texture_ptr font_texture; std::string text_string; glm::fvec2 text_position; GLfloat text_scale; glm::vec3 text_color; glm::mat4 text_projection; int window_height, window_width; void init(); void check_for_errors(); public: renderable_text(); renderable_text(const std::string& text, glm::fvec2 position, GLfloat scale, glm::vec3 color); //Use the default font void set_window_size(int height,int width); void set_text(const std::string& text); void set_position(glm::fvec2 position); void set_scale(GLfloat scale); void set_color(glm::vec3 color); void render_text(); };
renderable_text is the class that implement the text rendering functionality, it contains the font loader object and the class responsible for handling the shaders plus the code which perform the rendering, it contains also a set of useful functions which allows the user to set the text size, position &c.
The interface of the class has two constructor, the default constructor creates and object without any real text to render but initiate all the OpenGL buffers in order to be ready for rendering. The second constructor allows the user to provide an initial string with a position, color and scaling information (to specify the size of the font).
The interesting part of both the constructors is the call to the function init.
init generate the OpenGL buffers the Vertex Buffer Object and Vertex Array Object we need to use in order to render in the char texture.
void renderable_text::init() { LOG3("renderable_text::init: VBO, VBA"); glGenVertexArrays(1,&VAO); glGenBuffers(1,&VBO); glBindVertexArray(VAO); glBindBuffer(GL_ARRAY_BUFFER,VBO); glBufferData(GL_ARRAY_BUFFER, sizeof(GLfloat)*6*4, NULL, GL_DYNAMIC_DRAW); glEnableVertexAttribArray(0); glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(GLfloat), 0); glBindBuffer(GL_ARRAY_BUFFER, 0); glBindVertexArray(0); check_for_errors(); LOG3("renderable_text::init: Prepare shaders"); text_render_shader.load_vertex_shader(simple_vertex_shader); text_render_shader.load_fragment_shader(simple_fragment_shader); if(!text_render_shader.create_shader_program()){ ERR("Unable to create the shader program"); } //Use default font textures font_texture = font_loader.get_texture( font_loader.get_default_font_id() ); }
The Vertex Buffer Object is needed to move and update vertex data from our slow computer memory to the graphic card memory, the VBO is just a number which the framework use to identify a managed memory on the graphic unit memory, the real memory is handled behind the scenes by OpenGL.
The Vertex Array Object is used to manipulate vertex attribute and to store them for further usage, the ID generated by glGenVertexArrays will be used by the code to refer to specific set of attributes which can be enabled just by referencing the ID (Instead of setting up again and again the same attributes before rendering).
Those two buffers are needed to load and manipulate the vertex memory on the graphic device, the code loads the vertex mapping the VBO with a GL_ARRAY_BUFFER, in that memory the quads of the character textures are going to be stored for rendering purpose. This is exactly the purpose of glBufferData, whereas the function glVertexAttribPointer is used to specify how the vertex are stored in the vertex array buffer.
Once Init has generated the buffers it calls check_for_errors which verify if any OpenGL error was raised:
void renderable_text::check_for_errors() { int error_count = 0; GLenum error = GL_NO_ERROR; while( (error = glGetError()) != GL_NO_ERROR){ ERR("OpenGL ERROR: ", error); ++error_count; } if(error_count > 0){ throw std::runtime_error("check_for_errors: ERRORS!!"); } }
This function is brutal!
If any error flag is set the an error print is generated and then a runtime_error exception is raised. For details about glGetError click here.
After init has verified that no error occurred then it loads the shaders and the font information, from the loaded font the corresponding set of textures is extracted and saved in font_texture (font_texture_ptr), those will be used later to render the text.
Before having a look at the render_text function which is responsible for the real work of rendering the provided string let’s have a look at set_window_size. This function is invoked every time the window size changes, when this happen the code needs to make some update on the current projection in order to be sure that the text will not be distorted by the changed window proportions.
void renderable_text::set_window_size(int height, int width) { text_render_shader.use_shaders(); window_height = height; window_width = width; text_projection = glm::ortho(0.0f, static_cast<GLfloat>(window_width), 0.0f, static_cast<GLfloat>(window_height)); GLint uniform_var = glGetUniformLocation(text_render_shader.get_program(), "projection"); if(uniform_var < 0){ ERR("Unable to obtain the uniform variable: projection"); return; } glUniformMatrix4fv(uniform_var, 1, GL_FALSE, glm::value_ptr(text_projection) ); GLenum op_status = glGetError(); if(op_status != GL_NO_ERROR){ ERR("Error encountered in renderable_text::set_window_size:", op_status); } }
Here we are loading the uniform from the shader program, then the proper projection is loaded in the uniform in order to allow a correct (non distorted) rendering of the textures, for details about glUniformMatrix4fv have a look here.
To calculate the new projection the code uses glm::ortho from the GLM library, and set the left lower corner to 0,0 and the top/right corner to the new window size, this projection is then loaded to the uniform in the shader.
Finally the render_text function:
void renderable_text::render_text() { // Activate corresponding render state text_render_shader.use_shaders(); GLint uniform_var = glGetUniformLocation(text_render_shader.get_program(), "textColor"); if(uniform_var < 0){ ERR("Unable to obtain the uniform variable: textColor"); return; } glUniform3f(uniform_var, text_color.x, text_color.y, text_color.z); glActiveTexture(GL_TEXTURE0); glBindVertexArray(VAO); std::string::const_iterator c; GLfloat x = text_position.x, y = text_position.y; for (c = text_string.begin(); c != text_string.end(); c++) { character_data ch = font_texture->charset[*c]; GLfloat xpos = x + ch.Bearing.x * text_scale; GLfloat ypos = y - (ch.Size.y - ch.Bearing.y) * text_scale; GLfloat w = ch.Size.x * text_scale; GLfloat h = ch.Size.y * text_scale; // Update VBO for each character GLfloat vertices[6][4] = { { xpos, ypos + h, 0.0, 0.0 }, { xpos, ypos, 0.0, 1.0 }, { xpos + w, ypos, 1.0, 1.0 }, { xpos, ypos + h, 0.0, 0.0 }, { xpos + w, ypos, 1.0, 1.0 }, { xpos + w, ypos + h, 1.0, 0.0 } }; // Render glyph texture over quad glBindTexture(GL_TEXTURE_2D, ch.TextureID); // Update content of VBO memory glBindBuffer(GL_ARRAY_BUFFER, VBO); glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertices), vertices); glBindBuffer(GL_ARRAY_BUFFER, 0); // Render quad glDrawArrays(GL_TRIANGLES, 0, 6); // Now advance cursors for next glyph (note that advance is number of 1/64 pixels) x += (ch.Advance >> 6) * text_scale; // Bitshift by 6 to get value in pixels (2^6 = 64) } glBindVertexArray(0); glBindTexture(GL_TEXTURE_2D, 0); }
Once the shaders are loaded the code load the textColor uniform in order to setup the current text color. The RGB information about the color are stored in the text_color struct which is a vector of three elements from the GLM library.
Afterwards the function enter the main loop which iterate over all the characters of the string, for each char is calculated the position where it shall be rendered using the information in the char glyph, then the quads for the vertices are calculated as well, and stored in the vertices array.
As you can see we need six vertices to draw a quad, the reason behind this is that everything in OpenGL is a triangle, so if we want to draw a rectangle we need to define it in terms of two triangles.
Then we just bind the current texture with glBindTexture, the information about the textures is available as ID number in the character_data struct, gain in OpenGL most of the things are managed using ID’s and the framework handles the real thing behind the scenes.
The remaining code is responsible for loading the vertices into the graphic card memory and finally for the real drawing by a call to glDrawArrays. At the end, before returning to the caller, render_text unbind the current vertex array and texture.
Conclusion
I have a very small experience with OpenGL yet, so I perfectly understand that most of the things I’ve done here are not optimal and that many problems are not addressed in the best way. If you wish to have a look at the code of this post, the checkout it from my github. Be aware that I may change the way the text is rendered in the future, so the content of the repo can be different from what I’ve shown here.
Really hope to be able in few months to write down a little nice OpenGL application, if that happen you can be sure I’ll shared with you what I’ve done, but for now..
..thanks for reading!