Currently I am implementing text rendering into my game engine using the FreeType
library by following the tutorial found here: https://learnopengl.com/In-Practice/2D-Game/Render-text. My current implementation is not working correctly, you can see the result of my implementation in the following Images (note that I am rendering the text “Test test”, and you can see 8 distinct cubic shapes one for each letter with a space in between the words, with the last 4 cubic shapes being smaller and shaped differently compared to the capitalized version of the word, so it looks like it is at least close to rendering the string “Test test”):


Firstly, there are some obvious issues, to start with you can see the “text” is drawn in a projection perspective rather than orthographic, this is on purpose however because my system already draws with projection perspective and if the text was stuck flat to my screen in a 2D
manner I fail to see how the perspective it is drawn in would change anything.
That leads to the next problem, the “text” is not stuck to my screen in a 2D
manner (like HUD elements in a game), it appears to be floating in 3D
space, although if I look at the “Text” from exactly side on it will vanish, so it does not appear to have any depth (z
axis), only a position on the x
and y
axis. Also if I go past the side on point and look from behind the “text” vanishes.
And finally the most obvious issue, the glyphs
clearly are not rendered correctly, as you cant see the actual shape of the letters, instead just the cubic space containing the letter.
My implementation is as follows: (disclaimer: My engine is to big to explain every little thing that is going on so this question is showing minimal code and only relevant code to this issue, described in quite a high level manner)
In my engine the scene is created using a scene graph of GameObject
‘s each with GameComponent
‘s, therefore I create a TextRendererObject
and add to it a TextRenderer
component and add it to the scene in the following code:
Entity *textRendererObject = new Entity(...); TextRenderer *Text; Text = new TextRenderer(50, 50); Text->Load("font/arial.ttf", 240); textRendererObject->AddComponent(Text); AddToScene(textRendererObject);
The TextRenderer
constructor, a struct
that the .h
file defines (used in load
function) and the load
function are as follows respectively:
TextRenderer::TextRenderer(GLuint width, GLuint height) : TextShader("text")//creates text shader (text.glsl) { GLuint VAO, VBO; SetIsTextRenderer(true); glGenVertexArrays(1, &this->VAO); glGenBuffers(1, &this->VBO); glBindVertexArray(this->VAO); glBindBuffer(GL_ARRAY_BUFFER, this->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); }
/// Holds all state information relevant to a character as loaded using FreeType struct Character { 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 GLuint Advance; // Horizontal offset to advance to next glyph }; //... std::map<GLchar, Character> Characters;
void TextRenderer::Load(std::string font, GLuint fontSize) { // First clear the previously loaded Characters this->Characters.clear(); // Then initialize and load the FreeType library FT_Library ft; if (FT_Init_FreeType(&ft)) // All functions return a value different than 0 whenever an error occurred printf("ERROR::FREETYPE: Could not init FreeType Library");//std::cout << "ERROR::FREETYPE: Could not init FreeType Library" << std::endl; // Load font as face FT_Face face; if (FT_New_Face(ft, font.c_str(), 0, &face)) printf("ERROR::FREETYPE: Failed to load font");//std::cout << "ERROR::FREETYPE: Failed to load font" << std::endl; // Set size to load glyphs as FT_Set_Pixel_Sizes(face, 0, fontSize); // Disable byte-alignment restriction glPixelStorei(GL_UNPACK_ALIGNMENT, 1); // Then for the first 128 ASCII characters, pre-load/compile their characters and store them for (GLubyte c = 0; c < 128; c++) { // Load character glyph if (FT_Load_Char(face, c, FT_LOAD_RENDER)) { printf("ERROR::FREETYTPE: Failed to load Glyph");//std::cout << "ERROR::FREETYTPE: Failed to load Glyph" << std::endl; continue; } // Generate texture GLuint texture; glGenTextures(1, &texture); glBindTexture(GL_TEXTURE_2D, texture); glTexImage2D( GL_TEXTURE_2D, 0, GL_RED, face->glyph->bitmap.width, face->glyph->bitmap.rows, 0, GL_RED, GL_UNSIGNED_BYTE, 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 character = { texture, glm::ivec2(face->glyph->bitmap.width, face->glyph->bitmap.rows), glm::ivec2(face->glyph->bitmap_left, face->glyph->bitmap_top), face->glyph->advance.x }; Characters.insert(std::pair<GLchar, Character>(c, character)); } glBindTexture(GL_TEXTURE_2D, 0); // Destroy FreeType once we're finished FT_Done_Face(face); FT_Done_FreeType(ft); }
The shader (text.glsl
) that is created when the Textrenderer
object is created is as follows:
#include "common.glh" varying vec2 texCoord0; varying vec3 worldPos0; #if defined(VS_BUILD) attribute vec3 position; attribute vec2 texCoord; uniform mat4 T_model; uniform mat4 T_MVP; void main() { gl_Position = T_MVP * vec4(position.xy, 0.0, 1.0); texCoord0 = texCoord; worldPos0 = (T_model * vec4(position.xy, 0.0, 1.0)).xyz; } #elif defined(FS_BUILD) uniform sampler2D H_text; uniform vec3 H_textColor; DeclareFragOutput(0, vec4); void main() { vec4 sampled = vec4(1.0, 1.0, 1.0, texture2D(H_text, texCoord0).r); vec4 color = vec4(H_textColor, 1.0) * sampled; SetFragOutput(0, sampled * color); } #endif
Following this set-up of the textRenderObject
game object, its Text
game component and the text.glsl
shader, every frame the following render function is called:
void TextRenderer::RenderTextRenderer(...) { this->TextShader.Bind();//"text.glsl" created earlier this->TextShader.UpdateUniformsTextRenderer(...); RenderText("TEST test", 100, 100, 1);//responsible for drawing }
UpdateUniformsTextRenderer(...)
is responsible for setting the values of the uniform
s in text.glsl
and is as follows:
void Shader::UpdateUniformsTextRenderer(Transform* transform, const RenderingEngine& renderingEngine, const Camera& camera) { Matrix4f worldMatrix = transform->GetTransformation(); Matrix4f projectedMatrix = camera.GetViewProjection() * worldMatrix; for (unsigned int i = 0; i < m_shaderData->GetUniformNames().size(); i++) { std::string uniformName = m_shaderData->GetUniformNames()[i]; std::string uniformType = m_shaderData->GetUniformTypes()[i]; if (uniformName.substr(0, 2) == "T_") { if (uniformName == "T_MVP") SetUniformMatrix4f(uniformName, projectedMatrix); else if (uniformName == "T_model") SetUniformMatrix4f(uniformName, worldMatrix); else throw "Invalid Transform Uniform: " + uniformName; } else if (uniformName.substr(0, 2) == "H_") { if (uniformName == "H_text") {//Texture used to draw text int samplerSlot = renderingEngine.GetSamplerSlot(uniformName); SetUniformi(uniformName, samplerSlot); } else if (uniformName == "H_textColor") SetUniformVector3f(uniformName, Vector3f(1, 0, 0));//red } } }
And finally the function RenderText
that actually draws the text is as follows:
void TextRenderer::RenderText(std::string text, GLfloat x, GLfloat y, GLfloat scale) { glActiveTexture(GL_TEXTURE0); glBindVertexArray(this->VAO); // Iterate through all characters std::string::const_iterator c; for (c = text.begin(); c != text.end(); c++) { Character ch = Characters[*c]; GLfloat xpos = x + ch.Bearing.x * scale; GLfloat ypos = y + (this->Characters['H'].Bearing.y - ch.Bearing.y) * scale; GLfloat w = ch.Size.x * scale; GLfloat h = ch.Size.y * scale; // Update VBO for each character GLfloat vertices[6][4] = { { xpos, ypos + h, 0.0, 1.0 }, { xpos + w, ypos, 1.0, 0.0 }, { xpos, ypos, 0.0, 0.0 }, { xpos, ypos + h, 0.0, 1.0 }, { xpos + w, ypos + h, 1.0, 1.0 }, { xpos + w, ypos, 1.0, 0.0 } }; // Render glyph texture over quad glBindTexture(GL_TEXTURE_2D, ch.TextureID); // Update content of VBO memory glBindBuffer(GL_ARRAY_BUFFER, this->VBO); glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertices), vertices); // Be sure to use glBufferSubData and not glBufferData glBindBuffer(GL_ARRAY_BUFFER, 0); // Render quad glDrawArrays(GL_TRIANGLES, 0, 6); // Now advance cursors for next glyph x += (ch.Advance >> 6) * scale; // Bitshift by 6 to get value in pixels (1/64th times 2^6 = 64) } glBindVertexArray(0); glBindTexture(GL_TEXTURE_2D, 0); }
And that’s my implementation, can anyone see where I have gone wrong? Any feedback is much appreciated.