February 3, 2013

Rendering Text in OpenGL Using FreeType Fonts

EDIT (04.14.2013): Tutorial updated to handle new-lines (\n character)

I finally decided to tackle the rendering of TrueType fonts in my engine. It proved to be a much bigger challenge than I had originally anticipated; I perused over dozens of outdated guides, tutorials, code snippets, and documentation files to finally achieve something legible on screen. I decided to provide a complete guide to this process so that if anyone should decide to follow in my footsteps, they will know where to go.


FreeType 2 Installation

OpenGL works with pixels, and TrueType fonts are not stored like image files, so we need the FreeType 2 library to create font bitmaps. If you are well-versed in 3rd party API installation, feel free to skip this section. I have only performed the compilation using Visual Studio 2010 and 2012.
  • Download the latest headers for FreeType 2 from SourceForge. As of this write-up, the latest version is 2.4.11.
  • Unzip the archive and go to the builds folder to locate your platform. I will be demonstrating this with the win32/vc2010 build.
  • Open freetype.sln in Visual Studio. If you are running VS2012, you will need to update the solution to use the latest compiler and libraries.
  • Select the Release candidate and go to Build->Build Solution.
  • The compiled .lib file will be located in objs/win32/vc2010/freetype[version].lib, where [version] is "2411" as of this writing.
  • Copy this file to somewhere in your current project directory's library path.
  • Copy the contents of the include/ folder to somewhere in your current project directory's include path.

Basic Structures

We are going to define some structures in order to make handling the fonts a bit easier. We need a rectangle to keep track of dimensions, a color for the text color, an a 2D vector to track position. Also, I'm going to include my vertex structure, representing a vertex to send to the VBO for rendering. This is pretty standard and I assume if you've made it this far in OpenGL, you have one of your own.

/// Various data structures for simplifying font handling.
#ifndef TYPES_HPP
#define TYPES_HPP
/// Represents a point in 2D rectangular-coordinate space.
struct vector2_t
{
float x, y;
/// Default constructor that sets components to 0.
vector2_t() : x(0), y(0) {}
/// Constructs a 2D vector from components.
vector2_t(float x, float y) : x(x), y(y) {}
/// Copy constructor.
vector2_t(const vector2_t& Copy) : x(Copy.x), y(Copy.y) {}
/// Assignment operator.
inline vector2_t& operator=(const vector2_t& Copy)
{ x = Copy.x; y = Copy.y; return (*this); }
};
/// A rectangle at a certain point in space.
struct rect_t
{
rect_t(float x, float y, int w, int h) : x(x), y(y), w(w), h(h) {}
rect_t() : x(0.f), y(0.f), w(0), h(0) {}
float x, y;
int w, h;
};
/**
* Represents a 32-bit floating point color (RGBA).
* Limited to the range [0, 1].
**/
struct color4f_t
{
color4f_t() : r(1), g(1), b(1), a(1) {}
color4f_t(float r, float g, float b, float a) :
r(r), g(g), b(b), a(a) {}
float r, g, b, a;
};
struct vertex2_t
{
vector_t position; ///< Vertex position
vector_t tc; ///< Vertex texture coordinates
color4f_t color; ///< Vertex color
};
#endif // TYPES_HPP
view raw Types.hpp hosted with ❤ by GitHub

Font.hpp


This will be the main font header that is used to render text. To load a .ttf font file, we call LoadFromFile(). Here, the mp_glyphTextures map will be filled out in order to create {character: glyph} pairs, which are later used in rendering to find offset values and bind the character bitmaps. The Glyph struct contains a rectangle representing the various offsets (a g should be drawn lower than a b, for example), and the bitmap texture. When RenderText() is called, each character in the given string will be rendered using the unique texture found in the dictionary. The m_FontRender may throw some people off. This is just a very basic shader wrapper class I have written, and I'm sure anyone following this tutorial has a similar wrapper of their own. I will go into more detail as to what the shader does later. Here is the class outline:

// Font class header.
#ifndef FONT_HPP
#define FONT_HPP
// Storing {char:glyph} pairs.
#include <map>
// Include files for FreeType 2.
#include "ft2build.h"
#include "freetype/freetype.h"
#include "freetype/ftglyph.h"
// The various type structures we created earlier.
#include "Types.hpp"
// Dirty macro to determine the offset of a field within a struct.
// Used to determine the offsets of data in the GPU buffers.
#define VBO_OFFSET(count, vertex, field) \
(void*)((count * sizeof(vertex)) + (long int)&(((vertex*)NULL)->field))
class CFont
{
public:
CFont();
~CFont();
/**
* Initializes the FreeType library.
* This should be called prior to ANY other font operations.
* Since it is static, no instance of CFont needs to be present,
* simply do:
* if(!CFont::Initialize())
* std::cerr << "FreeType 2 failed to initialize!\n";
*
* @return TRUE on success, FALSE on failure.
**/
static bool Initialize();
/**
* Loads a TrueType font.
* Here, all of the printable characters in the ASCII table
* are loaded as bitmaps, then are turned into renderable
* textures in GPU memory. Their dimensions are stored for
* rendering later on.
*
* @param std::string Font filename
* @paarm uint16_t Font size (optional=12)
*
* @pre CFont::Initialize() must have been called.
*
* @return TRUE if everything went smoothly,
* FALSE otherwise.
**/
bool LoadFromFile(const std::string& filename,
const uint16_t size = 12);
/**
* Resizes the font.
* This stores a new font size and calls LoadFromFile()
* again, regenerating new bitmaps and textures for all
* characters.
* Since this can be a relatively expensive operation,
* it should be used sparingly. If you think you're going
* to be resizing a font often, consider caching multiple
* instances of CFont.
*
* @param uint16_t New font size
*
* @return Value of CFont::LoadFromFile()
**/
bool Resize(const uint32_t size);
/**
* Renders text on the current framebuffer.
* Given a string, this function will render it at
* the given position. A VBO will be created, vertex
* information generated, and then each character will
* be rendered using its texture.
* The position parameter marks the bottom of the "line"
* to be drawn on, so parts of characters such as 'j'
* or 'q' will fall below the line. Think of a sheet of
* paper.
* A rectangle is returned, in case it is useful to know
* the overall dimensions of the text rendered.
*
* @param std::string Text to render
* @param math::vector2_t Position to start rendering
*
* @return Rectangle representing rendered text dimensions.
*
* @see GetTextWidth()
* @see GetTextHeight()
**/
math::rect_t RenderText(const std::string& text,
const math::vector2_t& Pos);
/**
* Sets text color; the default is white.
**/
void SetFontColor(const float r, const float g, const float b);
private:
static FT_Library s_Library;
static bool s_loaded;
// Glyph representing data for each character.
struct Glyph
{
// OpenGL texture handle for the bitmap
uint32_t texture;
// Dimensions and offsets.
rect_t dim;
};
// {Char: Glyph} dictionary.
std::map<char, Glyph> mp_glyphTextures;
FT_Face m_FontFace;
Shader m_FontRender;
color4f_t m_Color;
std::string m_filename;
uint16_t m_size;
bool m_loaded, m_ready;
};
#endif // FONT_HPP
view raw Font.hpp hosted with ❤ by GitHub
The comments should explain everything else in a pretty detailed fashion. Now on to the implementation.

Font.cpp

This a detailed explanation of the implementation of the CFont class.

Initialize()

Here the TrueType library is initialized. This can be called explicitly via CFont::Initialize(), but it will also be called by default when a CFont instance is created.
bool CFont::Initialize()
{
if(s_loaded) return true;
if(FT_Init_FreeType(&s_Library) != 0) return false;
return (s_loaded = true);
}
view raw Font.cpp hosted with ❤ by GitHub

CFont()

This is the class constructor. Here, we will load the shader and verify that the TrueType library is initialized.

CFont::CFont() : m_ready(false), m_loaded(false)
{
// Initialize FreeType.
if(!CFont::Initialize()) return;
// Load the shader.
if(!m_FontRender.LoadFromFile("Default.vs", "FontRender.fs"))
return;
// Give the font shader an identity matrix for the model-view
// and the projection matrix that was created with the window.
m_FontRender.Bind();
if(!m_FontRender.SetMatrix("modelview", math::IDENTITY_MATRIX) ||
!m_FontRender.SetMatrix("proj", CWindow::GetProjectionMatrix()))
return;
m_FontRender.Unbind();
m_ready = true;
}
view raw Font.cpp hosted with ❤ by GitHub

LoadFromFile()

Now we must load the font. Filename is a path to a TrueType font (.ttf) and size is the size of the font, in pixels.

bool CFont::LoadFromFile(const std::string& filename, const uint16_t size)
{
// Make sure everything is ready.
if(!m_ready || !CFont::s_loaded) return false;
// Create a new font face.
if(FT_New_Face(s_Library, filename.c_str(), 0, &m_FontFace) != 0)
return false;
// Set the size of the font face.
// Since the function expects a size in 1/64 pixels, we multiply
// by 64 (same as left-shifting 6 bits) before passing.
// The 96 represents a 96-dpi font bitmap.
if(FT_Set_Char_Size(m_FontFace, size << 6, size << 6, 96, 96) != 0)
return false;
// Load all printable characters.
// If you visit an ASCII table (like www.asciitable.com) you will see
// that the only valid values for printing are the space character all
// the way up to the tilde (~).
for(size_t i = ' '; i <= '~'; ++i)
{
FT_Glyph glyph;
// Locate the index of the character in the font face.
uint32_t index = FT_Get_Char_Index(m_FontFace, i);
if(index == 0) continue;
// Load the glyph into the font face.
FT_Load_Glyph(m_FontFace, index, FT_LOAD_RENDER);
// Render the glyph as a mono-chrome bitmap.
FT_Render_Glyph(m_FontFace->glyph, FT_RENDER_MODE_NORMAL);
// Put the glyph in the glyph slot into an actual glpyh struct.
FT_Get_Glyph(m_FontFace->glyph, &glyph);
// Small shortcuts
FT_GlyphSlot slot = m_FontFace->glyph;
FT_Bitmap& bitmap = slot->bitmap;
// Bitmap dimensions
uint32_t w = bitmap.width;
uint32_t h = bitmap.rows;
uint32_t texture = 0;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
// We need to copy the data over to a new buffer in order
// to properly pass it to the GPU.
unsigned char* data = new unsigned char[w * h];
memset(data, NULL, w * h * sizeof(unsigned char));
memcpy(data, bitmap.buffer, sizeof(unsigned char) * w * h);
// Alignment for pixels needs to be at 1 byte.
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, 0, GL_RED, w, h,
GL_UNSIGNED_BYTE, data);
// Restore default alignment value. I haven't actually tested this
// part so it may or may not actually be the default.
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
// Delete bitmap buffers
delete[] data;
FT_Done_Glyph(glyph);
// Our custom glyph structure.
CFont::Glyph render_glyph;
render_glyph.pTexture = pTexture;
// The dimensions are organized as follows:
// x: total bitmap width
// y: total bitmap height
// w: advancement to the next character
// h: height offset from the top of the character to the line
// w and h are represented in 1/64ths of a pixel so we need
// to convert them to accurate on-screen pixels.
render_glyph.dim = math::rect_t(w, h,
slot->advance.x >> 6,
slot->metrics.horiBearingY >> 6);
// Assign the glyph to its slot in the dictionary.
mp_glyphTextures[i] = render_glyph;
}
// Clean up the font face.
FT_Done_Face(m_FontFace);
// Store the filename internally in-case we need to Resize() later.
m_filename = filename;
// We're done!
return (m_loaded = true);
}
view raw Font.cpp hosted with ❤ by GitHub

RenderText()

text is, obviously, the text we want to render. Currently, this only supports one line at a time, so characters like '\r' will not render properly. Actually they likely won't render at all and will cause a crash. You should add bounds checking to ensure that the provided char is within the range of render-able bitmaps. Pos is the position where to start rendering. It is not the top-left corner of the text, but rather is on the line to be written on. Imagine a piece of paper: Pos is the line which you're writing on.

rect_t CFont::RenderText(const std::string& text, const vector2_t& Pos)
{
// VAO and buffers that will contain our text vertex data.
uint32_t vao = 0, vbo = 0, ibo = 0;
// Track total text size.
rect_t Size(Pos.x, Pos.y, 0, 0);
if(!m_loaded || !CFont::s_loaded || text.empty()) return Size;
// Vertex buffer size, index buffer size.
uint16_t vlen = text.length() << 2;
uint16_t ilen = text.length() * ((1 << 2) + 2);
// Create buffers and zero them.
vertex2_t* verts = new vertex2_t[vlen];
uint16_t* inds = new uint16_t[ilen];
memset(inds, NULL, sizeof(uint16_t) * ilen);
memset(verts, NULL, sizeof(vertex2_t) * vlen);
// Track width and max height.
int max_w = 0, max_h = 0;
// The x-position to start the next character at.
int32_t last_w = Pos.x;
// The y-position of the starting character, for new-lines.
float y = Pos.y;
// Fill up buffers. Each character needs 4 vertices, so
// we increment by 4 each iteration then compensate for
// that throughout the loop.
for(size_t i = 0; i < vlen; i += 4)
{
// Shortcut.
char c = text[i >> 2];
// Handle new-lines by starting from the x position
// again and increasing Y by the height of an arbitrary
// large letter (here H).
if(c == '\n')
{
last_w = Pos.x;
y += mp_glyphTextures['H'].dim.y + mp_glyphTextures['H'].dim.h;
}
// We want to make sure the letters are in the range
// that we've loaded (ASCII printables).
char letter = (c > '~' || c < ' ') ? ' ' : c;
// Retrieve dimensions from the dictionary.
// Since we're doing i += 4, the index in the text string
// would be text[i / 4].
rect_t Dim = mp_glyphTextures[letter].dim;
float w = last_w;
float h = Dim.y;
last_w = w + Dim.w; // Increase until next char by the
// bitmap's horizontal advance value.
// [i] : top left
// [i + 1] : top right
// [i + 2] : bottom right
// [i + 3] : bottom left
verts[i].Position = math::vector2_t(w, y- Dim.h);
verts[i+1].Position = math::vector2_t(last_w, y - Dim.h);
verts[i+2].Position = math::vector2_t(last_w, y - Dim.h + h);
verts[i+3].Position = math::vector2_t(w, y - Dim.h + h);
// Load up the bitmap texture coordinates moving counter-clockwise
// from the origin.
verts[i].TexCoord = math::vector2_t(0, 0);
verts[i+1].TexCoord = math::vector2_t(1, 0);
verts[i+2].TexCoord = math::vector2_t(1, 1);
verts[i+3].TexCoord = math::vector2_t(0, 1);
// The vertices all use the font color.
// See CFont::SetColor()
for(size_t j = i; j < i + 4; ++j)
verts[j].Color = m_Color;
// A basic textured quad uses the indices [0, 1, 3, 3, 2, 1]
// assuming that verts[0] is the top-left, and the rest are
// calculated going clock-wise.
// The index in the buffer that we need is i / 4 * 6, since
// i / 4 gives us the current character, and each character
// needs 6 indices.
int x = (i >> 2) * 6;
// Assign the values.
inds[x] = i;
inds[x+1] = i + 1;
inds[x+2] = i + 3;
inds[x+3] = i + 3;
inds[x+4] = i + 2;
inds[x+5] = i + 1;
// Keep track of the overall dimensions.
max_w += Dim.w;
max_h = (max_h > Dim.h + h) ? max_h : Dim.h + h;
}
// Tally up dimensions.
Size.w = max_w;
Size.h = max_h;
// Enable font-rendering shader.
m_FontRender.Bind();
// Create GPU buffers for vertex/index data
glGenBuffers(1, &vao);
glGenBuffers(1, &vbo);
glGenBuffers(1, &ibo);
glBindVertexArray(vao);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo);
// Enable the vertex attributes for position, texcoord, and color.
// See the shader for details.
glEnableVertexAttribArray(0);
glEnableVertexAttribArray(1);
glEnableVertexAttribArray(2);
// Give data to GPU.
glBufferData(GL_ARRAY_BUFFER, sizeof(vertex2_t) * vlen, verts, GL_STATIC_DRAW);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(uint16_t) * ilen, inds, GL_STATIC_DRAW);
// Vertices are arranged in memory like so:
// [ p0, p1, t0, t1, c0, c1, c2, c3 ]
// Specify vertex position arrangement.
// According to the diagram shown above, the vertex position
// would start at index 0.
// See the VBO_OFFSET macro in CFont.hpp.
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE,
sizeof(vertex2_t),
VBO_OFFSET(0, vertex2_t, Position));
// Specify texture coordinate position arrangement.
// According to the diagram, texture coordinates
// start at index 2.
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE,
sizeof(vertex2_t), VBO_OFFSET(0, vertex2_t, TexCoord));
// Specify the color arrangement.
// Starting at index 4.
glVertexAttribPointer(2, 4, GL_FLOAT, GL_FALSE,
sizeof(vertex2_t), VBO_OFFSET(0, vertex2_t, Color));
// Enable blending so that the text doesn't have a black background.
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
// Draw each character with its texture enabled.
for(size_t i = 0; i < text.length(); ++i)
{
// Make invalid characters just spaces (' ')
char c = (text[i] > '~' || text[i] < ' ') ? ' ' : text[i];
glBindTexture(GL_TEXTURE_2D, mp_glyphTextures[c]);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT,
(void*)(sizeof(uint16_t) * i * 6));
}
// Delete GPU buffers.
glBufferData(GL_ARRAY_BUFFER, 0, NULL, GL_STATIC_DRAW);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, 0, NULL, GL_STATIC_DRAW);
// Unbind all the things.
glBindTexture(GL_TEXTURE_2D, 0);
glDisableVertexAttribArray(0);
glDisableVertexAttribArray(1);
glDisableVertexAttribArray(2);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
m_FontRender.Unbind();
// Delete all buffers.
glDeleteVertexArrays(1, &vao);
glDeleteBuffers(1, &vbo);
glDeleteBuffers(1, &ibo);
// Delete old buffers in RAM.
delete[] verts;
delete[] inds;
// Give back the total dimensions of the text rendered.
return Size;
}
view raw Font.cpp hosted with ❤ by GitHub

FontRender.fs

There is some shader magic necessary in order to properly render the text. Since TrueType gives us a monochrome bitmap, we need to specify that we want to use the r value (specified when we said GL_R8 and GL_RED in the glTexImage2D call in CFont::LoadFromFile() in the incoming texture fragment for all four components for the output fragment.

#version 330 core
// Our texture
uniform sampler2D texture;
// Incoming attributes from the vertex shader
smooth in vec2 fs_texc;
smooth in vec4 fs_color;
// Output color
out vec4 out_color;
void main()
{
out_color = texture2D(texture, fs_texc).rrrr * fs_color;
}
view raw FontRender.fs hosted with ❤ by GitHub
There you have it! The rest of the methods in CFont should be fairly easy to implement. I mean, I'm sure it's fairly self-explanatory that CFont::SetColor() just modifies CFont::m_Color to reflect the new RGB values, and that Resize() just calls CFont::LoadFromFile() again with the new size specified.

You can see the raw class files directly from my IronClad rendering engine in their entirety on my Github here (header) and here (implementation). Hopefully I will be modifying this system soon to use a texture atlas to contain all of the glyph bitmaps in a single texture and load a VBO with custom texture coordinates on-demand. Feel free to add any comments, questions, or suggestions below!

2 comments:

  1. Thnx for providing this information to the web.

    my page; tutors melbourne

    ReplyDelete
  2. Thank you, much appreciated.

    ReplyDelete