OpenGL Tutorial 12 (QS) – FreeType Fonts – Text (Timers & 3D)

Some approaches to graphics programming (such as when creating games via the ALLEGRO game programming library) receive access to various built-in functionality that is designed to make some particular tasks easier, such as displaying text in your graphics window for example.

However, OpenGL is a relatively low-level API that doesn’t come packaged with such pre-programmed subroutines. Therefore, using OpenGL (directly that is) to create text, is appreciably more difficult. However, we similarly have access to various support libraries, that work seamlessly with OpenGL, such as the popular FreeType software development library.

The following video tutorial explains some of the most important, fundamental aspects of working with TrueType and OpenType fonts via the FreeType fonts library. The library itself makes displaying text much easier, by processing the font files, and then providing to us, the resulting bitmap image data and numerous glyph metrics – enabling us to display, and precisely control the position of, each character.

Source code: C++ from... main.cpp

#include <glad/glad.h> // GLAD: https://github.com/Dav1dde/glad GLAD 2 also works via the web-service: https://gen.glad.sh/ (leaving all checkbox options unchecked)
#include <GLFW/glfw3.h>
 
#include <ft2build.h> // https://freetype.org/freetype2/docs/tutorial/step1.html#section-1
#include FT_FREETYPE_H
 
// OpenGL Mathematics(GLM) https://github.com/g-truc/glm/blob/master/manual.md
// ------------------------------------
// GLM Headers
// ------------------
#include <glm/glm.hpp> // Include all GLM core.	
#include <glm/gtc/matrix_transform.hpp> // Specific extensions.
#include <glm/gtc/type_ptr.hpp>
 
#include <vector>
#include <iostream>
#include <fstream> // Used in "shader_configure.h" to read the shader text files.
 
#include "shader_configure.h" // Used to create the shaders.
#include "text_fonts_glyphs.h"
 
int main()
{
	// (1) GLFW: Initialise & Configure
	// -----------------------------------------
	if (!glfwInit())
		exit(EXIT_FAILURE);
 
	glfwWindowHint(GLFW_SAMPLES, 4); // Anti-aliasing
	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2);
	glfwWindowHint(GLFW_OPENGL_PROFILEGLFW_OPENGL_CORE_PROFILE);
	
	const GLFWvidmodemode = glfwGetVideoMode(glfwGetPrimaryMonitor());
 
	int monitor_width = mode->width; // Monitor's width and height.
	int monitor_height = mode->height;
 
	int window_width = (int)(monitor_width * 0.75f); // Example: monitor_width * 0.5f... will be 50% the monitor's size.
	int window_height = (int)(monitor_height * 0.75f); // Cast is simply to silence the compiler warning.
 
	GLFWwindowwindow = glfwCreateWindow(window_widthwindow_height"FreeType Fonts - Glyphs Text (3D Animation)"NULLNULL);
	// GLFWwindow* window = glfwCreateWindow(window_width, window_height, "FreeType Fonts - Glyphs Text (3D Animation)", glfwGetPrimaryMonitor(), NULL); // Full Screen Mode ("Alt" + "F4" to Exit!)
 
	if (!window)
	{
		glfwTerminate();
		exit(EXIT_FAILURE);
	}
	glfwMakeContextCurrent(window); // Set the window to be used and then centre that window on the monitor. 
	glfwSetWindowPos(window, (monitor_width - window_width) / 2, (monitor_height - window_height) / 2);
 
	glfwSwapInterval(1); // Set VSync rate 1:1 with monitor's refresh rate.
	
	// (2) GLAD: Load OpenGL Function Pointers
	// -------------------------------------------------------
	if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) // For GLAD 2 use the following instead: gladLoadGL(glfwGetProcAddress)
	{
		glfwTerminate();
		exit(EXIT_FAILURE);
	}
	glEnable(GL_DEPTH_TEST); // Enabling depth testing allows rear faces of 3D objects to be hidden behind front faces.
	glEnable(GL_MULTISAMPLE); // Anti-aliasing
	glEnable(GL_BLEND); // GL_BLEND for OpenGL transparency which is further set within the fragment shader. 
	glBlendFunc(GL_SRC_ALPHAGL_ONE_MINUS_SRC_ALPHA);	
 
	 // (3) Compile Shaders & Initialise Camera 
	// -----------------------------------------------------
	const charvert_shader_text = "../../Shaders/shader_text.vert";
	const charfrag_shader_text = "../../Shaders/shader_text.frag";
 
	Shader text_shader(vert_shader_textfrag_shader_text);
	text_shader.use();
 
	/* glm::vec3 camera_position(0.0f, 0.0f, 3.0f); // -Z is into the screen.
	glm::vec3 camera_target(0.0f, 0.0f, 0.0f);
	glm::vec3 camera_up(0.0f, 1.0f, 0.0f);
 
	glm::mat4 view = glm::lookAt(camera_position, camera_target, camera_up);
	glm::mat4 projection = glm::perspective(glm::radians(55.0f), (float)window_width / (float)window_height, 0.1f, 100.0f); */
 
	// (4) Initialise FreeType & Declare Text Objects
	// -----------------------------------------------------------
	// "How is it possible that the HorizontalAdvance of a glyph is smaller than the glyph's width?"
	// https://stackoverflow.com/questions/45304838/how-is-it-possible-that-the-horizontaladvance-of-a-glyph-is-smaller-than-the-gly	
	
	FT_Library free_type;
	FT_Error error_code = FT_Init_FreeType(&free_type);
	if (error_code)
	{
		std::cout << "\n   Error code: " << error_code << " --- " << "An error occurred during initialising the FT_Library";
		int keep_console_open;
		std::cin >> keep_console_open;
	}
	/* Text text_object1(free_type, window_width, window_height, "1234567890&.-abcdefghijklmnopqrstuvwxyz:_ABCDEFGHIJKLMNOPQRSTUVWXYZ "); // Pass a specific alphabet to be used for this specific text object.
	text_object1.create_text_message("Using OpenGL and the FreeType library to render", 110, 60, "Text Fonts/cambriaz.ttf", 70, false);
	text_object1.create_text_message("and animate TrueType fonts.", 110, 180, "Text Fonts/cambriaz.ttf", 70, false); */
 
	Text text_object2(free_typewindow_widthwindow_height"01234567890Get Rady.Timr:owns&ClBgfb"); // Declare a new text object, passing in your chosen alphabet.	
	text_object2.create_text_message("Get Ready... Timer: 000", 150, 100, "Text Fonts/arialbi.ttf", 130, true); // True indicates that the message will be modified.
 
	int num_replace = 3;
	size_t vec_size = text_object2.messages[0].characters_quads.size();
	float start_pos = text_object2.messages[0].start_x_current[vec_size - num_replace];
	
	int display_counting = 0;
	int get_ready = 0;
	bool running = false/* unsigned int view_mat_text_loc = glGetUniformLocation(text_shader.ID, "view");
	unsigned int proj_mat_text_loc = glGetUniformLocation(text_shader.ID, "projection");
	unsigned int anim_text_loc = glGetUniformLocation(text_shader.ID, "animate");
 
	glUniformMatrix4fv(view_mat_text_loc, 1, GL_FALSE, glm::value_ptr(view));
	glUniformMatrix4fv(proj_mat_text_loc, 1, GL_FALSE, glm::value_ptr(projection)); */
 
	glUniform1i(glGetUniformLocation(text_shader.ID, "alphabet_texture"), 31);
 
	glm::vec3 RGB(10.0f, 120.0f, 105.0f);
	unsigned int font_colour_loc = glGetUniformLocation(text_shader.ID, "font_colour");
	glUniform3fv(font_colour_loc, 1, glm::value_ptr(RGB));	
 
	// (5) Enter the Main-Loop
	// --------------------------------
	 /* srand((unsigned)time(NULL)); // Initialise random seed.
 
	float x_spin = 1.0f / (rand() % 10 + 1); // Generate random number between 1 and 10
	float y_spin = 1.0f / (rand() % 10 + 1);
	float z_spin = 1.0f / (rand() % 10 + 1);
	float spin_speed = (float)(rand() % 5 + 1);
 
	float spin_vary = 0.0f;
	int spin_dir = 1;
 
	glm::mat4 spinning_mat(1.0f); */
 
	while (!glfwWindowShouldClose(window)) // Main-Loop
	{
		// (6) Randomise the Model's Spinning Speed & Axis
		// ------------------------------------------------------------------
		/* spin_vary += 0.05f * spin_dir;
 
		if (spin_vary > 6 || spin_vary < 0)
		{
			spin_dir = -spin_dir; // Reverse the spinning direction.
 
			x_spin = 1.0f / (rand() % 10 + 1);
			y_spin = 1.0f / (rand() % 10 + 1);
			z_spin = 1.0f / (rand() % 10 + 1);
			spin_speed = (float)(rand() % 50 + 1) / 40;			
		}		
		spinning_mat = glm::rotate(spinning_mat, glm::radians(spin_speed), glm::normalize(glm::vec3(x_spin, y_spin, z_spin))); */
 
		// (7) Clear the Screen & Depth Buffer
		// ----------------------------------------------
		glClearColor(210.0f / 255, 240.0f / 255, 250.0f / 255, 1.0f); // This line can be moved to before the while loop.
		glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);		
		
		// (8) Draw the Alphabets & Messages
		// -----------------------------------------------
		// text_object1.draw_messages();
		// text_object1.draw_alphabets();
 
		 /* glUniformMatrix4fv(anim_text_loc, 1, GL_FALSE, glm::value_ptr(spinning_mat)); */
 
		if (!running)
			++get_ready;
		if (get_ready > 125)
			running = true;
 
		if (running)
		{
			++display_counting;
			if (display_counting == 300)
				display_counting = 0;
 
			// std::cout << "\n   display_counting: " << display_counting << " ---  display_counting % 10: " << display_counting % 10 << " ---  display_counting / 10 % 10: "
				// << display_counting / 10 % 10 << " --- display_counting / 100 % 10: " << display_counting / 100 % 10;
 
			unsigned num1 = display_counting / 100 % 10; // Left digit.
			unsigned num2 = display_counting / 10 % 10;
			unsigned num3 = display_counting % 10;
 
			float advance1 = text_object2.messages[0].alphabet_vec[num1].glyph_advance_x;
			float advance2 = advance1 + (text_object2.messages[0].alphabet_vec[num2].glyph_advance_x);
			
			text_object2.messages[0].characters_quads.resize(vec_size - num_replace);
			text_object2.messages[0].text_start_x = start_pos;
 
			text_object2.process_text_index(text_object2.messages[0]num1, 0); // Important: the number of calls to: process_text_index(...) must = "num_replace_characters"
			text_object2.process_text_index(text_object2.messages[0]num2advance1);
			text_object2.process_text_index(text_object2.messages[0]num3advance2);
			
			text_object2.update_buffer_data_message(text_object2.messages[0], (int)(vec_size - num_replace));
		}	
		text_object2.draw_messages(0);		
		text_object2.draw_alphabets();  
 
		glfwSwapBuffers(window);
		glfwPollEvents();
	}
 
	// (9) Exit the Application
	// ------------------------------
	// FT_Done_Face(text_object1.face);
	FT_Done_Face(text_object2.face);
	FT_Done_FreeType(free_type);		
	glDeleteProgram(text_shader.ID);
 
	/* glfwDestroyWindow(window) // Call this function to destroy a specific window */	
	glfwTerminate(); // Destroys all remaining windows and cursors, restores modified gamma ramps, and frees resources.
 
	exit(EXIT_SUCCESS); // Function call: exit() is a C/C++ function that performs various tasks to help clean up resources.
}

Source code: C++ from... shader_configure.h

#pragma once // Instead of using include guards.
 
class Shader
{
public:
	GLuint ID; // Public Program ID.
 
	// Constructor
	// ---------------
	Shader(const charvert_pathconst charfrag_path)
	{
		char character;
 
		std::ifstream vert_stream;
		std::ifstream frag_stream;
 
		std::string vert_string;
		std::string frag_string;		
 
		// Read vertex shader text file
		// ------------------------------------
		vert_stream.open(vert_path); // I decided not to implement: Exception handling try catch method.
 
		if (vert_stream.is_open()) // Note: There are various other methods for accessing the stream, i.e., vert_stream.get() is just one option.
		{
			while (vert_stream.get(character)) // Loop getting single characters until EOF (value false) is returned. 
				vert_string += character// "The first signature returns the character read, or the end-of-file value (EOF) if no characters are available in the stream..."
 
			vert_stream.close();
			std::cout << "\n   File: " << vert_path << " opened successfully.\n\n";
		}
		else
			std::cout << "   ERROR!... File: " << vert_path << " could not be opened.\n\n";
 
		// Read fragment shader text file
		// ----------------------------------------
		frag_stream.open(frag_path);
 
		if (frag_stream.is_open())
		{
			while (frag_stream.get(character))
				frag_string += character;
 
			frag_stream.close();
			std::cout << "   File: " << frag_path << " opened successfully.\n\n";
		}
		else
			std::cout << "   ERROR!... File: " << frag_path << " could not be opened.\n\n";
 
		// std::cout << vert_string << "\n\n"; // Output the shader files to display in the console window.
		// std::cout << frag_string << "\n\n";
 
		const charvert_pointer = vert_string.c_str();
		const charfrag_pointer = frag_string.c_str();
 
		// Compile shaders
		// ----------------------
		GLuint vert_shadfrag_shad// Declare in here locally. Being attached to the public Program ID allows the shaders to be used publicly.
 
		// Create vertex shader
		// ---------------------------
		vert_shad = glCreateShader(GL_VERTEX_SHADER);
		glShaderSource(vert_shad, 1, &vert_pointerNULL);
		glCompileShader(vert_shad);
		Check_Shaders_Program(vert_shad"vert_shader");
 
		// Create fragment shader
		// -------------------------------
		frag_shad = glCreateShader(GL_FRAGMENT_SHADER);
		glShaderSource(frag_shad, 1, &frag_pointerNULL);
		glCompileShader(frag_shad);
		Check_Shaders_Program(frag_shad"frag_shader");
 
		// Create shader program
		// ------------------------------
		ID = glCreateProgram();
		glAttachShader(ID, vert_shad); // This also avoids deletion via: glDeleteShader(vert_shad) as called below.
		glAttachShader(ID, frag_shad);
		glLinkProgram(ID);
		Check_Shaders_Program(ID, "shader_program");
 
		// Note: Flagging the program object for deletion before calling "glUseProgram" would accidentally stop the program installation of the rendering state	
		// ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
		glDeleteShader(vert_shad); // Flag shader object for automatic deletion (freeing memory) when no longer attached to a program object...
		glDeleteShader(frag_shad); // ... program object is deleted (glDeleteProgram ) within: main() when the application ends.
 
		// glUseProgram(ID); // Typically this is called within: main() to select individual shaders that have been created. 
		// glDeleteProgram(ID); // Alternatively the program object can be deleted here after 1st calling:  glUseProgram(ID)
	}
 
	// Activate the shader
	// -------------------------
	void use()
	{
		glUseProgram(ID); // Function called from within main() to select an individual shader to be used.
	}
 
private:
	// Check shader compilations and program object for linking errors
	// -------------------------------------------------------------------------------------
	void Check_Shaders_Program(GLuint type, std::string name)
	{
		int success;
		int error_log_size;
		char info_log[1000]; // 1000 characters max. Typically it's less than 500 even for multiple shader errors.
 
		if (name == "vert_shader" || name == "frag_shader")
		{
			glGetShaderiv(typeGL_COMPILE_STATUS, &success);
			if (!success)
			{
				glGetShaderInfoLog(type, 1024, &error_log_sizeinfo_log);
				std::cout << "\n--- Shader Compilation Error: " << name << "\n\n" << info_log << "\n" << "Error Log Number of Characters: " << error_log_size << "\n\n";
			}
		}
		else // "shader_program"
		{
			glGetProgramiv(typeGL_LINK_STATUS, &success);
			if (!success)
			{
				glGetProgramInfoLog(type, 1024, &error_log_sizeinfo_log);
				std::cout << "\n--- Program Link Error: " << name << "\n\n" << info_log << "\n" << "Error Log Number of Characters: " << error_log_size << "\n";
			}
		}
	}
};

Source code: C++ from... text_fonts_glyphs.h

#pragma once // https://docs.microsoft.com/en-us/windows/win32/gdi/raster--vector--truetype--and-opentype-fonts
 
class Text
{
private:
	struct Alphabet_Characters
	{
		float left_bearing = 0.0f;
		float bottom_bearing = 0.0f;
		float height_plus_padding = 0.0f;
		float width_plus_padding = 0.0f;
		float glyph_advance_x = 0.0f;		
 
		char character;
 
		glm::vec2 texcoord_top_left;
		glm::vec2 texcoord_top_right;
		glm::vec2 texcoord_bottom_left;
		glm::vec2 texcoord_bottom_right;
	};
 
	struct Message_Characters
	{
		glm::vec4 bottom_left_tr1; // Triangle 1
		glm::vec4 bottom_right_tr1;
		glm::vec4 top_left_tr1;
 
		glm::vec4 top_left_tr2; // Triangle 2
		glm::vec4 top_right_tr2;
		glm::vec4 bottom_right_tr2;
	};
 
	struct Message_Parent
	{		
		unsigned VAO_message, VBO_message, VAO_alphabet, VBO_alphabet;
		unsigned alphabet_texture;
 
		bool draw_alphabet = true;
		bool dynamic_static = false;
 
		size_t allocated_memory_bytes = 0;
				
		int font_size = 10;
		int tallest_font_height = 0;
		int relative_distance = 0;
 
		int alphabet_texture_width = 0;
		int alphabet_texture_height = 0;
 
		float alphabet_start_x = -0.80f; // These are set here in OpenGL [-1, 1] coordinate range.
		float alphabet_start_y = -0.45f;
 
		float text_start_x = 0.0f;
		float text_start_y = 0.0f;		
 
		std::string message_string;
		std::vector<Alphabet_Characters> alphabet_vec;
		Message_Characters alphabet_quad;
 
		std::vector<Message_Characters> characters_quads;
		std::vector<float> start_x_current;
 
		std::string font_path;
	};
	// --------------------------------	
	std::string alphabet_string;	
 
	FT_Library& free_type;
	FT_GlyphSlot glyph; // "glyph" (FT_GlyphSlot) is simply being used as shorthand for "face" (FT_Face) ->glyph... set in: set_font_parameters()
 
	float scale_pixels_x_to_OpenGL = 0.0f; // OpenGL [-1, 1] (i.e. 2) divided by the number of screen pixels.
	float scale_pixels_y_to_OpenGL = 0.0f;
	
	int character_row_limit = 15; // Alphabet character row limit.
	int alphabet_padding = 7; // Padding is optional (it spaces out the alphabet characters, without affecting each message's character spacing)
	// Note: if character background is slightly opaque e.g. 0.1 = vec4(1, 1, 1, texture(text_Texture, texture_coordinates).r) + 0.1, then spaces, i.e. simply " " show as a: alphabet_padding * alphabet_padding square.
 
public:
	FT_Face face; // Resources are freed in main() via FT_Done_Face(...)
 
	std::vector<Message_Parent> messages;
 
	Text(FT_Libraryfree_typeint window_widthint window_height, std::string alphabet_string) : free_type(free_type)
	{
		this->alphabet_string = alphabet_string;		
		scale_pixels_x_to_OpenGL = 2.0f / window_width// Scale vertex data to render on-screen the same size as the font's set pixel-size... 
		scale_pixels_y_to_OpenGL = 2.0f / window_height// This makes the text display at the same correct pixel size, regardless of the window size.
	}
 
	void create_text_message(std::string messageint text_start_xint text_start_y, std::string font_pathint font_sizebool dynamic_static)
	{
		int alphabet_detected = -1;
		for (int i = 0; i < messages.size(); ++i)
		{
			if (messages[i].font_size == font_size && messages[i].font_path == font_path)
			{
				alphabet_detected = i;				
				break;
			}			
		}			
		Message_Parent new_message// Changed by reference during most of the below function calls.
 
		new_message.font_size = font_size;
		new_message.font_path = font_path;		
		
		if (alphabet_detected == -1) // Create new alphabet.
		{
			std::cout << "\n\n   New alphabet created (characters are listed below) --- Font path: " << font_path << " --- Font size: " << font_size;
 
			set_font_parameters(new_message);
			create_blank_texture(new_message);
			calculate_alphabet_image_size(new_message);
			format_alphabet_texture_image(new_message);
			create_alphabet_image_quad(new_message);
			set_buffer_data_alphabet(new_message);
		}
		else // Copy the existing alphabet.
		{
			new_message.draw_alphabet = false;
			new_message.alphabet_vec = messages[alphabet_detected].alphabet_vec;
			new_message.alphabet_texture = messages[alphabet_detected].alphabet_texture;
			new_message.alphabet_texture_width = messages[alphabet_detected].alphabet_texture_width;
			new_message.alphabet_texture_height = messages[alphabet_detected].alphabet_texture_height;
			new_message.tallest_font_height = messages[alphabet_detected].tallest_font_height;
			new_message.relative_distance = messages[alphabet_detected].relative_distance;
			
			std::cout << "\n\n   Existing alphabet detected (no new alphabet is required) --- Font path: " << font_path << " --- Font size: " << font_size << "\n";
		}
		new_message.message_string = message;
		process_text_compare(new_messagetext_start_xtext_start_y); // process_text_index(...) is called within this function call.
 
		new_message.dynamic_static = dynamic_static// True = dynamic.
		initialise_buffer_data_message(new_message); // Initialise the message's buffer data.
		update_buffer_data_message(new_message, 0); // Update the message's buffer data.
 
		messages.push_back(new_message); // Add the new message to the list of messages.
	}
 
	void draw_alphabets()
	{
		for (unsigned i = 0; i < messages.size(); ++i)
		{
			if (messages[i].draw_alphabet)
			{
				glBindVertexArray(messages[i].VAO_alphabet);
 
				glActiveTexture(GL_TEXTURE31);
				glBindTexture(GL_TEXTURE_2D, messages[i].alphabet_texture);
 
				glDisable(GL_DEPTH_TEST);
				glDrawArrays(GL_TRIANGLES, 0, 6);
				glEnable(GL_DEPTH_TEST);
 
				glActiveTexture(GL_TEXTURE0);
				glBindVertexArray(0);
				// std::cout << "\n   Drawing alphabet... messages index: " << i;
			}
		}
	}
 
	void draw_messages()
	{
		for (unsigned i = 0; i < messages.size(); ++i)
		{
			glBindVertexArray(messages[i].VAO_message);
			
			glActiveTexture(GL_TEXTURE31);
			glBindTexture(GL_TEXTURE_2D, messages[i].alphabet_texture);
 
			glDisable(GL_DEPTH_TEST); // Cast (unsigned) used below, silences the compiler warning (unsigned 32 bit is still over 4 billion)
			glDrawArrays(GL_TRIANGLES, 0, (unsigned)messages[i].characters_quads.size() * 6);
			glEnable(GL_DEPTH_TEST);
 
			glActiveTexture(GL_TEXTURE0);
			glBindVertexArray(0);
		}
	}
 
	void draw_messages(unsigned message_index)
	{		
		if (message_index > messages.size() - 1)
		{
			std::cout << "\n   Warning: draw_messages(...) --- 'message_index' is greater than 'messages.size() - 1'\n";
 
			int keep_console_open;			
			std::cin >> keep_console_open;
		}
		else
		{
			glBindVertexArray(messages[message_index].VAO_message);
 
			glActiveTexture(GL_TEXTURE31);
			glBindTexture(GL_TEXTURE_2D, messages[message_index].alphabet_texture);
 
			glDisable(GL_DEPTH_TEST); // Cast (unsigned) used below, silences the compiler warning (unsigned 32 bit is still over 4 billion)
			glDrawArrays(GL_TRIANGLES, 0, (unsigned)messages[message_index].characters_quads.size() * 6);
			glEnable(GL_DEPTH_TEST);
 
			glActiveTexture(GL_TEXTURE0);
			glBindVertexArray(0);
		}
	}
 
	void process_text_index(Message_Parentnew_messageunsigned indexfloat advanced_current)
	{		
		// Y-Values (by default the characters are bottom aligned) ("new_message.text_start_x & text_start_y"  are set in: process_text_compare(...))
		// ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
		float bottom_bearing = new_message.alphabet_vec[index].bottom_bearing;
		float y_pos_aligned = new_message.text_start_y - bottom_bearing;
		float height = new_message.alphabet_vec[index].height_plus_padding;
 
		float texcoord_top_left_y = new_message.alphabet_vec[index].texcoord_top_left.y;
		float texcoord_bottom_right_y = new_message.alphabet_vec[index].texcoord_bottom_right.y;
		float texcoord_top_right_y = new_message.alphabet_vec[index].texcoord_top_right.y;
		float texcoord_bottom_left_y = new_message.alphabet_vec[index].texcoord_bottom_left.y;
 
		// X-Values
		// -----------		
		float start_x_current = new_message.text_start_x + advanced_current;
		float left_bearing = new_message.alphabet_vec[index].left_bearing;
		float width = new_message.alphabet_vec[index].width_plus_padding;
 
		float texcoord_bottom_left_x = new_message.alphabet_vec[index].texcoord_bottom_left.x;
		float texcoord_bottom_right_x = new_message.alphabet_vec[index].texcoord_bottom_right.x;
		float texcoord_top_left_x = new_message.alphabet_vec[index].texcoord_top_left.x;
		float texcoord_top_right_x = new_message.alphabet_vec[index].texcoord_top_right.x;
 
		Message_Characters quad{};
 
		// Triangle 1
		// -------------
		quad.bottom_left_tr1.x = start_x_current + left_bearing;
 
		// Used for replacing characters from some index position, to the end of the full message length
		// ---------------------------------------------------------------------------------------------------------------------------
		new_message.start_x_current.push_back(start_x_current); // Record the character's start position... but excluding: left_bearing	
 
		quad.bottom_left_tr1.y = y_pos_aligned;
		quad.bottom_left_tr1.z = texcoord_bottom_left_x;
		quad.bottom_left_tr1.w = texcoord_top_left_y// Y-axis texture coordinates are reversed.
 
		quad.bottom_right_tr1.x = start_x_current + left_bearing + width;
		quad.bottom_right_tr1.y = y_pos_aligned;
		quad.bottom_right_tr1.z = texcoord_bottom_right_x;
		quad.bottom_right_tr1.w = texcoord_top_right_y;
 
		quad.top_left_tr1.x = start_x_current + left_bearing;
		quad.top_left_tr1.y = y_pos_aligned + height;
		quad.top_left_tr1.z = texcoord_top_left_x;
		quad.top_left_tr1.w = texcoord_bottom_left_y;
 
		// Triangle 2
		// -------------
		quad.top_left_tr2.x = start_x_current + left_bearing;
		quad.top_left_tr2.y = y_pos_aligned + height;
		quad.top_left_tr2.z = texcoord_top_left_x;
		quad.top_left_tr2.w = texcoord_bottom_left_y;
 
		quad.top_right_tr2.x = start_x_current + left_bearing + width;
		quad.top_right_tr2.y = y_pos_aligned + height;
		quad.top_right_tr2.z = texcoord_top_right_x;
		quad.top_right_tr2.w = texcoord_bottom_right_y;
 
		quad.bottom_right_tr2.x = start_x_current + left_bearing + width;
		quad.bottom_right_tr2.y = y_pos_aligned;
		quad.bottom_right_tr2.z = texcoord_bottom_right_x;
		quad.bottom_right_tr2.w = texcoord_top_right_y;
		// --------------------------------------------------------------
		// std::cout << "\n   CHARACTER: " << new_message.message_string.c_str()[index] << " --- start_x_current: " << start_x_current << " --- y_pos_aligned: " << y_pos_aligned << " --- width: " << width << " --- height: " << height;
		// std::cout << "\n  texcoord_top_left_x: " << texcoord_top_left_x;
		// std::cout << "\n   texcoord_top_right_x: " << texcoord_top_right_x << "\n";
 
		new_message.characters_quads.push_back(quad);		
	}	
 
	void update_buffer_data_message(Message_Parentnew_messageint characters_offset)
	{
		glBindVertexArray(new_message.VAO_message);
		glBindBuffer(GL_ARRAY_BUFFERnew_message.VBO_message);
 
		GLintptr data_offset_bytes = (GLintptr)characters_offset * 6 * 4 * sizeof(float);
		GLsizeiptr replace_size_bytes = (new_message.characters_quads.size() * 6 * 4 * sizeof(float)) - data_offset_bytes;
 
		if (data_offset_bytes + replace_size_bytes > (unsigned)new_message.allocated_memory_bytes)
		{			
			std::cout << "\n   Warning: update_buffer_data_message(...) --- 'data_offset_bytes' " << data_offset_bytes << " + 'replace_size_bytes' " << replace_size_bytes << " was too large for 'allocated_memory_bytes' "
				<< new_message.allocated_memory_bytes << " --- so 'replace_size_bytes' has been reduced to: ";
 
			replace_size_bytes -= (data_offset_bytes + replace_size_bytes) - new_message.allocated_memory_bytes;
			std::cout << replace_size_bytes;
		}
		if (data_offset_bytes + replace_size_bytes < (unsigned)new_message.allocated_memory_bytes)
		{
			std::cout << "\n   Warning: update_buffer_data_message(...) --- 'data_offset_bytes' " << data_offset_bytes << " + 'replace_size_bytes' " << replace_size_bytes << " is less than 'allocated_memory_bytes' "
				<< new_message.allocated_memory_bytes << " --- so 'characters_offset' " << characters_offset << " has been reduced to: ";
 
			characters_offset -= (int)((new_message.allocated_memory_bytes - (data_offset_bytes + replace_size_bytes)) / 6 / 4 / sizeof(float));
			std::cout << characters_offset;
		}
		int keep_console_open;
		if (characters_offset == -1)
			std::cin >> keep_console_open;
 
		glBufferSubData(GL_ARRAY_BUFFERdata_offset_bytesreplace_size_bytes, &new_message.characters_quads[characters_offset]);
		glBindVertexArray(0);
	}
 
private:
	void set_font_parameters(Message_Parent new_message)
	{	
		FT_Error error_code{};
		int keep_console_open;
		
		error_code = FT_New_Face(free_type, new_message.font_path.c_str(), 0, &face);
		if (error_code)
		{
			std::cout << "\n\n   Error code: " << error_code << " --- " << "Could not open font: " << new_message.font_path.c_str();
			std::cin >> keep_console_open;
		}
		error_code = FT_Set_Pixel_Sizes(face, 0, new_message.font_size);
		if (error_code)
		{
			std::cout << "\n\n   Error code: " << error_code << " --- " << "Could not set font pixel size : " << new_message.font_size;
			std::cin >> keep_console_open;
		}
		glyph = face->glyph; // Shorthand for "face->glyph"
}
 
	void create_blank_texture(Message_Parentnew_message)
	{
		glGenTextures(1, &new_message.alphabet_texture);
		glActiveTexture(GL_TEXTURE31);
		glBindTexture(GL_TEXTURE_2Dnew_message.alphabet_texture);
 
		glTexParameteri(GL_TEXTURE_2DGL_TEXTURE_WRAP_SGL_CLAMP_TO_EDGE);
		glTexParameteri(GL_TEXTURE_2DGL_TEXTURE_WRAP_TGL_CLAMP_TO_EDGE);
 
		glTexParameteri(GL_TEXTURE_2DGL_TEXTURE_MIN_FILTERGL_NEAREST); // GL_NEAREST... GL_LINEAR
		glTexParameteri(GL_TEXTURE_2DGL_TEXTURE_MAG_FILTERGL_NEAREST);
 
		glActiveTexture(GL_TEXTURE0);
	}
 
	void calculate_alphabet_image_size(Message_Parentnew_message)
	{
		FT_Error error_code{};
 
		int curr_row_width = 0;
		int max_row_width = 0;
		int number_of_rows = 1;
		int character_count = 0;		
		
		new_message.tallest_font_height = 0;
 
		for (unsigned i = 0; i < alphabet_string.size(); i++)
		{
			error_code = FT_Load_Char(face, alphabet_string[i]FT_LOAD_RENDER);
			if (error_code)
			{
				std::cout << "\n\n   Error code: " << error_code << " --- " << "Could not load character: " << alphabet_string[i];	
				int keep_console_open;
				std::cin >> keep_console_open;
			}
			if ((signed)glyph->bitmap.rows > new_message.tallest_font_height)
				new_message.tallest_font_height = glyph->bitmap.rows;
 
			curr_row_width += glyph->bitmap.width + alphabet_padding * 2;
 
			++character_count;
			if (character_count == character_row_limit)
			{
				character_count = 0;
				++number_of_rows;				
 
				if (curr_row_width > max_row_width)
					max_row_width = curr_row_width;
 
				curr_row_width = 0;
			}
			new_message.alphabet_texture_width = (curr_row_width > max_row_width) ? curr_row_width : max_row_width;
			// std::cout << "\n\n   alphabet_texture_width: " << alphabet_texture_width;
		}		
		new_message.alphabet_texture_height = number_of_rows * (new_message.tallest_font_height + alphabet_padding * 2);
 
		std::cout << "\n\n   alphabet_texture_width: " << new_message.alphabet_texture_width
			<< " --- alphabet_texture_height: " << new_message.alphabet_texture_height << "\n";
	}
 
	void format_alphabet_texture_image(Message_Parentnew_message)
	{
		glActiveTexture(GL_TEXTURE31);
		glBindTexture(GL_TEXTURE_2Dnew_message.alphabet_texture);
 
		glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
 
		// Initialise empty data: https://stackoverflow.com/questions/7195130/how-to-efficiently-initialize-texture-with-zeroes
		std::vector<GLubyteempty_data(new_message.alphabet_texture_width * new_message.alphabet_texture_height, 0); // GL_RED = 8 bits = 1 byte.
 
		//  https://www.khronos.org/registry/OpenGL-Refpages/gl4/html/glTexImage2D.xhtml
		// "Each element is a single red component. OpenGL converts it to floating point and assembles it to RGBA, by attaching 0 for green and blue, and 1 for alpha. Each component is clamped to the range [0, 1]"
		glTexImage2D(GL_TEXTURE_2D, 0, GL_REDnew_message.alphabet_texture_width, new_message.alphabet_texture_height, 0, GL_REDGL_UNSIGNED_BYTE, &empty_data[0]);
		
		int character_count = 0;
		int increment_x = alphabet_padding;
		int increment_y = alphabet_padding;
		
		new_message.relative_distance = new_message.tallest_font_height; // Set relative distance to initial value.
 
		for (unsigned i = 0; i < alphabet_string.size(); ++i)
		{
			FT_Load_Char(face, alphabet_string[i]FT_LOAD_RENDER); // "glyph" as used below... is shorthand for "face->glyph"
 
			int tex_coord_left = increment_x - alphabet_padding;				
				glTexSubImage2D(GL_TEXTURE_2D, 0, increment_xincrement_y, glyph->bitmap.width, glyph->bitmap.rows, GL_REDGL_UNSIGNED_BYTE, glyph->bitmap.buffer); // Apply 1 character at a time to the texture.
			int tex_coord_right = increment_x + glyph->bitmap.width + alphabet_padding;
 
			int tex_coord_bottom = increment_y - alphabet_padding;
			int tex_coord_top = increment_y + glyph->bitmap.rows + alphabet_padding;
 
			increment_x += glyph->bitmap.width + (alphabet_padding * 2);
 
			// By default the characters are bottom aligned (Note: bitmap_top = the "Remaining Distance" above that bottom alignment, after having been moved downwards by "bottom_bearing" to produce character-origin alignment)
			// ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
			if (new_message.relative_distance > new_message.tallest_font_height - glyph->bitmap_top)
				new_message.relative_distance = new_message.tallest_font_height - glyph->bitmap_top; // Record the smallest... Tallest Font - "Remaining Distance" (incidentally, the tallest font is also checked against itself by doing this)
 
			// FT_GlyphSlotRec: https://freetype.org/freetype2/docs/reference/ft2-base_interface.html#ft_glyphslotrec (Also available: https://freetype.org/freetype2/docs/reference/ft2-base_interface.html#ft_glyph_metrics)
			// ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
			Alphabet_Characters alphabet_character{};
			alphabet_character.glyph_advance_x = (glyph->advance.x / 64) * scale_pixels_x_to_OpenGL;
 
			// The values below are in pixels...  FT_Bitmap: https://freetype.org/freetype2/docs/reference/ft2-basic_types.html#ft_bitmap			
			// --------------------------------------------------------------------------------------------------------------------------------------------------------------------		
			alphabet_character.left_bearing = glyph->bitmap_left * scale_pixels_x_to_OpenGL;
			alphabet_character.width_plus_padding = (tex_coord_right - tex_coord_left) * scale_pixels_x_to_OpenGL;
			alphabet_character.bottom_bearing = ((int)glyph->bitmap.rows - (int)glyph->bitmap_top) * scale_pixels_y_to_OpenGL;
			alphabet_character.height_plus_padding = (tex_coord_top - tex_coord_bottom) * scale_pixels_y_to_OpenGL;
			alphabet_character.character = alphabet_string[i];
 
			std::cout << "\n   CHARACTER: " << alphabet_string[i] << "\n   glyph->advance.x: " << glyph->advance.x << "\n   glyph->advance.x / 64: " << glyph->advance.x / 64
				<< "\n   glyph->bitmap_left: " << glyph->bitmap_left << "\n   glyph->bitmap.width: " << glyph->bitmap.width << "\n   glyph->bitmap.rows: " << glyph->bitmap.rows
				<< "\n   bottom bearing (height - top): " << (int)glyph->bitmap.rows - (int)glyph->bitmap_top << "\n   top bearing (bitmap_top): " << glyph->bitmap_top << "\n";
 
			// Texture Coordinates Section (divide texture coordinate position values by texture size to get range [0, 1])
			// ------------------------------------------------------------------------------------------------------------------------------------------
			alphabet_character.texcoord_top_left.x = (float)tex_coord_left / (float)new_message.alphabet_texture_width;
			alphabet_character.texcoord_top_left.y = (float)(tex_coord_top) / (float)new_message.alphabet_texture_height;
 
			alphabet_character.texcoord_top_right.x = (float)(tex_coord_right) / (float)new_message.alphabet_texture_width;
			alphabet_character.texcoord_top_right.y = (float)(tex_coord_top) / (float)new_message.alphabet_texture_height;
 
			alphabet_character.texcoord_bottom_left.x = (float)(tex_coord_left) / (float)new_message.alphabet_texture_width;
			alphabet_character.texcoord_bottom_left.y = (float)(tex_coord_bottom) / (float)new_message.alphabet_texture_height;
 
			alphabet_character.texcoord_bottom_right.x = (float)(tex_coord_right) / (float)new_message.alphabet_texture_width;
			alphabet_character.texcoord_bottom_right.y = (float)(tex_coord_bottom) / (float)new_message.alphabet_texture_height;
 
			new_message.alphabet_vec.push_back(alphabet_character); // Used in: process_text_compare()
 
			++character_count;
			if (character_count == character_row_limit)
			{
				character_count = 0;				
				increment_y += new_message.tallest_font_height + (alphabet_padding * 2);
				increment_x = alphabet_padding;
			}
		}
		glActiveTexture(GL_TEXTURE0);		
	}	
 
	void create_alphabet_image_quad(Message_Parentnew_message)
	{	
		float x = new_message.alphabet_start_x;
		float y = new_message.alphabet_start_y;
 
		float width = new_message.alphabet_texture_width * scale_pixels_x_to_OpenGL;
		float height = new_message.alphabet_texture_height * scale_pixels_y_to_OpenGL;
 
		float margin = 50 * scale_pixels_x_to_OpenGL; // Display each alphabet to the right of the previous one.
		if (messages.size() > 0)
		{
			x = messages[messages.size() - 1].alphabet_start_x + (messages[messages.size() - 1].alphabet_texture_width * scale_pixels_x_to_OpenGL) + margin;
			new_message.alphabet_start_x = x;
		}
		// Triangle 1
		// -------------
		new_message.alphabet_quad.bottom_left_tr1.x = x;
		new_message.alphabet_quad.bottom_left_tr1.y = y;
		new_message.alphabet_quad.bottom_left_tr1.z = 0.0f;
		new_message.alphabet_quad.bottom_left_tr1.w = 1.0f;
 
		new_message.alphabet_quad.bottom_right_tr1.x = x + width;
		new_message.alphabet_quad.bottom_right_tr1.y = y;
		new_message.alphabet_quad.bottom_right_tr1.z = 1.0f;
		new_message.alphabet_quad.bottom_right_tr1.w = 1.0f;
 
		new_message.alphabet_quad.top_left_tr1.x = x;
		new_message.alphabet_quad.top_left_tr1.y = y + height;
		new_message.alphabet_quad.top_left_tr1.z = 0.0f;
		new_message.alphabet_quad.top_left_tr1.w = 0.0f;
 
		// Triangle 2
		// -------------
		new_message.alphabet_quad.top_left_tr2.x = x;
		new_message.alphabet_quad.top_left_tr2.y = y + height;
		new_message.alphabet_quad.top_left_tr2.z = 0.0f;
		new_message.alphabet_quad.top_left_tr2.w = 0.0f;
 
		new_message.alphabet_quad.top_right_tr2.x = x + width;
		new_message.alphabet_quad.top_right_tr2.y = y + height;
		new_message.alphabet_quad.top_right_tr2.z = 1.0f;
		new_message.alphabet_quad.top_right_tr2.w = 0.0f;
 
		new_message.alphabet_quad.bottom_right_tr2.x = x + width;
		new_message.alphabet_quad.bottom_right_tr2.y = y;
		new_message.alphabet_quad.bottom_right_tr2.z = 1.0f;
		new_message.alphabet_quad.bottom_right_tr2.w = 1.0f;
	}	
 
	void set_buffer_data_alphabet(Message_Parentnew_message)
	{
		glGenVertexArrays(1, &new_message.VAO_alphabet);
		glGenBuffers(1, &new_message.VBO_alphabet);
 
		glBindVertexArray(new_message.VAO_alphabet);
		glBindBuffer(GL_ARRAY_BUFFERnew_message.VBO_alphabet);
		
		glBufferData(GL_ARRAY_BUFFER, 6 * 4 * sizeof(float), &new_message.alphabet_quad, GL_STATIC_DRAW);
 
		glEnableVertexAttribArray(0);
		glVertexAttribPointer(0, 4, GL_FLOATGL_FALSE, 0, (void*)0);
 
		glBindVertexArray(0);
	}
 
	void process_text_compare(Message_Parentnew_messageint text_start_xint text_start_y)
	{
		float advance_to_next_character = 0.0f;
 
		// "relative_distance" and "tallest_character" are fixed values, calculated per message (used here to align the text's highest pixel to the display window's top row of pixels)
		float tallest_character = new_message.tallest_font_height * scale_pixels_y_to_OpenGL;
		float relative_distance = new_message.relative_distance * scale_pixels_y_to_OpenGL;
 
		for (unsigned i = 0; i < new_message.message_string.size(); ++i)
		{
			for (unsigned i2 = 0; i2 < new_message.alphabet_vec.size(); ++i2)
			{
				if (new_message.message_string.c_str()[i] == new_message.alphabet_vec[i2].character)
				{
					if (advance_to_next_character == 0) // Start X, Y positions need setting here, but only for the 1st character, i.e. when: advance_to_next_character = 0
					{
						// Enable these two lines for 2D window-positioned text
						// -----------------------------------------------------------------------
						new_message.text_start_x = -1.0f - new_message.alphabet_vec[i2].left_bearing + (text_start_x - alphabet_padding) * scale_pixels_x_to_OpenGL;
						new_message.text_start_y = 1.0f + relative_distance - tallest_character - (text_start_y + alphabet_padding) * scale_pixels_y_to_OpenGL;
 
						// Enable these two lines instead for 3D animated text
						// --------------------------------------------------------------------
						// new_message.text_start_x = -1.35f;
						// new_message.text_start_y = 0.0f;
					}
					process_text_index(new_messagei2advance_to_next_character);					
					advance_to_next_character += new_message.alphabet_vec[i2].glyph_advance_x;
 
					break// Stop checking the alphabet if the character is found.
				}
			}
		}		
	}
 
	void initialise_buffer_data_message(Message_Parentnew_message)
	{
		glGenVertexArrays(1, &new_message.VAO_message);
		glGenBuffers(1, &new_message.VBO_message);
 
		glBindVertexArray(new_message.VAO_message);
		glBindBuffer(GL_ARRAY_BUFFERnew_message.VBO_message);
 
		new_message.allocated_memory_bytes = new_message.characters_quads.size() * 6 * 4 * sizeof(float);
 
		if (new_message.dynamic_static)
			glBufferData(GL_ARRAY_BUFFERnew_message.allocated_memory_bytes, NULLGL_DYNAMIC_DRAW);
		else
			glBufferData(GL_ARRAY_BUFFERnew_message.allocated_memory_bytes, NULLGL_STATIC_DRAW);
 
		glEnableVertexAttribArray(0);
		glVertexAttribPointer(0, 4, GL_FLOATGL_FALSE, 0, (void*)0);
 
		glBindVertexArray(0);
	}
};

Source code: GLSL from... shader_glsl.vert (Vertex shader)

#version 420 core
 
layout (location = 0) in vec4 vertex;
 
uniform mat4 view;
uniform mat4 projection;
uniform mat4 animate;
 
out vec2 texture_coordinates;
out vec3 texcoords_anim;
 
void main(void)
{	
	texture_coordinates = vec2(vertex[2], vertex[3]); // Same as using z and w... i.e. vertex.zw
 
	// Enable this line for 2D window-positioned text
	// -------------------------------------------------------------
	gl_Position = vec4(vertex.xy, 0.0, 1.0);
 
	// Enable these two lines instead for 3D animated text
	// --------------------------------------------------------------------
	// texcoords_anim = vec3(animate * vec4(vertex.xy, 0.0, 1)); // Used simply to animate the colours.
	// gl_Position = projection * view * animate * vec4(vertex.xy, 0.0, 1.0);
}

Source code: GLSL from... shader_glsl.frag (Fragment shader)

#version 420 core
 
uniform vec3 font_colour;
uniform sampler2D alphabet_texture;
 
in vec2 texture_coordinates;
in vec3 texcoords_anim;
 
out vec4 fragment_colour;
 
void main(void)
{		
	float texture_value = texture(alphabet_texture, texture_coordinates).r;
 
	// Enable this if-statement for 2D window-positioned text
	// -------------------------------------------------------------------------
	 if (texture_value == 1) // Fully opaque character pixels.
	 {
		 fragment_colour = vec4(font_colour / 255, texture_value);
			 // fragment_colour = vec4(255 / 255, 255 / 255, 255 / 255, 1.0);
	 }
	 else if (texture_value == 0) // Fully transparent, i.e. background pixels (Note: you cannot use the "discard" method as used for 3D text further down, if colouring in the font's background)
	 {
		 fragment_colour = vec4(font_colour / 255, texture_value);
			 // fragment_colour = vec4(85.0 / 255, 160.0 / 255, 155.0 / 255, 1.0);
	 }
	 else // Anti-aliased character pixels.
	 {
		 fragment_colour = vec4(font_colour / 255, texture_value);
			 // fragment_colour = vec4(255 / 255, 255 / 255, 255 / 255, 1.0);
	 }
 
	// Enable these three lines instead for 3D animated text
	// ----------------------------------------------------------------------
	// fragment_colour = vec4((texcoords_anim * 2) + vec3(0.25, 0.25, 0.25), texture_value);
 
	// if (texture_value == 0)
		//  discard;
}