#47 - FreeTypeFont C++ class for OpenGL

Date: 2019-02-09 12:00 - C++

Class to create a font to draw text using the freetype font library in C++ with OpenGL.

//FreeTypeFont.h
#pragma once

#include <GL/glew.h>
#include <glm/glm.hpp>

#include <ft2build.h>
#include FT_FREETYPE_H
#include FT_GLYPH_H

#include <memory>
#include <string>
#include <vector>

namespace YourNamespace {
	class Shader;
	class FreeTypeFont {
		static YourNamespace::Shader fontShader;
		static bool makeChar(FT_Face face, char ch, std::vector<float>& data, GLuint texId, int* advance);
		static void updateMVP();
	public:
		static std::shared_ptr<FreeTypeFont> create(const std::string& fontFile, unsigned int fontHeight);
	private:
		GLuint* textures;
		int* advanceX;
		GLuint vaoId;
		GLuint vboId;
		int _height;
	public:
		void render(float x, float y, const std::string& text, glm::vec4& color = glm::vec4(1.0f, 0.0f, 0.0f, 1.0f));
		int length(const std::string& text);
		int height() const { return _height; }
		~FreeTypeFont();
	};
}
//FreeTypeFont.cpp
#include "FreeTypeFont.h"

#include <glm/gtc/matrix_transform.hpp>

#include "Logger.h"
#include "Shader.h"
#include "Application.h"
#include "EventManager.h"
#include "Event.h"

#include "FTLibraryErrors.h"

using namespace YourNamespace;

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

Shader FreeTypeFont::fontShader;

bool FreeTypeFont::makeChar(FT_Face face, char ch, std::vector<float>& data, GLuint texId, int* advance) {
	FT_Error error = 0;
	if (error = FT_Load_Glyph(face, FT_Get_Char_Index(face, ch), FT_LOAD_FORCE_AUTOHINT)) {
		LOG_ERROR("FT_Load_Glyph failed: " + FreeType::getError(error));
		return false;
	}

	FT_Glyph glyph;
    if (error = FT_Get_Glyph(face->glyph, &glyph)) {
		LOG_ERROR("FT_Get_Glyph failed: " + FreeType::getError(error));
		return false;
	}

	if (glyph->format != FT_GLYPH_FORMAT_BITMAP) {
		if (error = FT_Glyph_To_Bitmap(&glyph, FT_RENDER_MODE_NORMAL, 0, 1)) {
			LOG_ERROR("FT_Glyph_To_Bitmap failed: " + FreeType::getError(error));
			return false;
		}
	}

    FT_BitmapGlyph bitmapGlyph = (FT_BitmapGlyph)glyph;
	FT_Bitmap& bitmap = bitmapGlyph->bitmap;
	glBindTexture(GL_TEXTURE_2D, texId);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

	int width = next_p2( bitmap.width );
	int height = next_p2( bitmap.rows );
	GLubyte* expanded_data = new GLubyte[width * height];
	for (int j = 0; j < height; j++) {
		for(int i = 0; i < width; i++){
			expanded_data[i+j*width] = (i>=bitmap.width || j>=bitmap.rows) ? 0 : bitmap.buffer[i + bitmap.width*j];
		}
	}
	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RED, GL_UNSIGNED_BYTE, expanded_data);
	delete[] expanded_data;
	float x = (float)bitmap.width / (float)width;
	float y = (float)bitmap.rows / (float)height;

	float tx = (float)bitmapGlyph->left;
	float ty = - (float)bitmapGlyph->top;
	GLfloat vertexData[] = {
		tx, ty, 0.0f, 0.0f,
		tx, ty + (float)bitmap.rows, 0.0f, y,
		tx + (float)bitmap.width, ty, x, 0.0f,
		tx + (float)bitmap.width, ty + (float)bitmap.rows, x, y
	};
	int size = sizeof(vertexData) / sizeof(float);
	for (int i = 0; i < size; i++)
		data.push_back(vertexData[i]);
	*(advance) = face->glyph->advance.x >> 6;
	FT_Done_Glyph(glyph);
	return true;
}

std::shared_ptr<FreeTypeFont> FreeTypeFont::create(const std::string& fontFile, unsigned int fontHeight) {
	FT_Error error = 0;
	FT_Library library;
	if (error = FT_Init_FreeType(&library)) {
		LOG_ERROR("Error initializing FreeType Library: " + FreeType::getError(error));
		return nullptr;
	}

	FT_Face face;
	if (error = FT_New_Face(library, fontFile.c_str(), 0, &face)) {
		LOG_ERROR("Error loading font file: " + FreeType::getError(error));
		FT_Done_FreeType(library);
		return nullptr;
	}

	FT_Set_Char_Size(face, fontHeight << 6, fontHeight << 6, 96, 96);

	GLuint * textures = new GLuint[128];
	int* advanceX = new int[128];
	GLuint vaoId, vboId;
	std::vector<float> data;

	glGenTextures(128, textures);
	glGenVertexArrays(1, &vaoId);
	glBindVertexArray(vaoId);
	glGenBuffers(1, &vboId);
	glBindBuffer(GL_ARRAY_BUFFER, vboId);
	for(unsigned char i = 0; i < 128; i++) {
		if (!makeChar(face, i, data, textures[i], &advanceX[i])) {
			FT_Done_Face(face);
			FT_Done_FreeType(library);
			return nullptr;
		}
	}

	glEnableVertexAttribArray(0);
	glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 16, 0);
	glEnableVertexAttribArray(1);
	glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 16, (GLvoid*)8);
	glBufferData(GL_ARRAY_BUFFER, data.size() * sizeof(float), (GLvoid*)(&(*data.begin())), GL_STATIC_DRAW);
	FT_Done_Face(face);
	FT_Done_FreeType(library);

	if (!fontShader.compiled()) {
		fontShader.setAttribLocation("vertexPosition", 0);
		fontShader.setAttribLocation("vertexTexCoords", 1);
		if (!fontShader.loadFromFile(GL_VERTEX_SHADER, "shaders/fontShader.vert") ||
			!fontShader.loadFromFile(GL_FRAGMENT_SHADER, "shaders/fontShader.frag") ||
			!fontShader.createLinkProgram()) {
			LOG_FATAL("Failed to create shader for FreeTypeFont!");
			throw std::runtime_error("Failed to create shader for FreeTypeFont!");
		}

		fontShader.use();
		fontShader.uniform("tex0", 0);
		fontShader.uniform("color", 1.0f, 0.0f, 0.0f, 1.0f);
		updateMVP();
		EventManager::default()->addHandler(HANDLER_FOR(WindowResize), [] (Event* ev) {
			updateMVP();
		});
	}

	std::shared_ptr<FreeTypeFont> font = std::make_shared<FreeTypeFont>();
	font->textures = textures;
	font->advanceX = advanceX;
	font->vaoId = vaoId;
	font->vboId = vboId;
	font->_height = fontHeight;
	return font;
}

void FreeTypeFont::updateMVP() {
	if (!fontShader.compiled())
		return;
	glm::mat4 MVP = glm::ortho(0.0f, (float)Application::windowWidth(), (float)Application::windowHeight(), 0.0f);
	fontShader.use();
	fontShader.uniform("MVP", MVP);
}

void FreeTypeFont::render(float x, float y, const std::string& text, glm::vec4& color) {
	glBindVertexArray(vaoId);
	glEnable(GL_BLEND);
	glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
	fontShader.use();
	fontShader.uniform("translate", x, y);
	fontShader.uniform("color", color);

	glActiveTexture(GL_TEXTURE0);
	for (size_t i = 0; i < text.length(); i++) {
		char ch = text[i];
		glBindTexture(GL_TEXTURE_2D, textures[ch]);
		glDrawArrays(GL_TRIANGLE_STRIP, ch*4, 4);
		fontShader.uniform("translate", (float)(x += advanceX[ch]), y);
	}

	glBindTexture(GL_TEXTURE_2D, NULL);
	glDisable(GL_BLEND);
}
int FreeTypeFont::length(const std::string& text) {
	int k = 0;
	for (size_t i = 0; i < text.length(); i++)
		k += advanceX[text[i]];
	return k;
}

FreeTypeFont::~FreeTypeFont() {
	glDeleteTextures(128, textures);
	delete[] textures;
	delete[] advanceX;
}

// FTLibraryErrors.h
#pragma once

#include <ft2build.h>
#include FT_FREETYPE_H

#include <string>
#include <vector>

#undef __FTERRORS_H__
#define FT_ERROR_START_LIST ;
#define FT_ERROR_END_LIST ;
#define FT_ERRORDEF(e, v, s) errors[v] = s;

class FTErrors {
public:
	std::map<int, std::string> errors;
	FTErrors() {
		#include FT_ERRORS_H
	}
};

namespace FreeType {
	FTErrors ft_errors;
	std::string getError(FT_Error error) {
		return ft_errors.errors[error];
	}
}

Previous snippet | Next snippet