Hey great video, I wanted to say that I recommend using Sign Distance Fields as the advantages outweigh the cons in my opinion. With TTF it sounds like your either planning on creating a "master atlas" pre-runtime (recommend) at some scale that looks nice, or computing the texture for a given scale on demand during runtime. With SDFs you should create the "master atlas" (like with the TTF) upscaled to what ever looks the best for your case, and then create Glyphs that represent your Text Quads. You don't have to use 0.5 as your fixed cut off point (in fact i strongly recommend passing it as a variable Uniform to your shader). With SDFs you get anti-aliasing in to form of MSAA for "free" just use GL_LINEAR in your GL_TEXTURE_MIN_FILTER and GL_TEXTURE_MAG_FILTER in your glTexParameteri calls at the initial glGenTextures (or you can use GL_LINEAR_MIPMAP_LINEAR / GL_LINEAR_MIPMAP_NEAREST if you plan on using Mipmaps for 3D effects). Also I recommend that you DON'T modify your VertexBuffer data for every text string you use, for static strings yeah sure it's efficient to pack the VBO and save it by itself for later. But for dynamic text (say like in a text editor, or FPS readout, or anything that is a throwaway string), instead you can use a "master Vertex Buffer Object" and store each character's index offset in their Glyph, and use a Transformation Matrix to modify the string's position/rotation/scale, rather than having to touch the GPU's memory directly. Here's a link to a tool you can use to make SFD files (parse them to taste): libgdx.badlogicgames.com/tools.html It's at the bottom called "Hiero, A bitmap font packing tool" And here's a RUclips link on how to implement Hiero in OpenGL: ruclips.net/video/d8cfgcJR9Tk/видео.html (Edit: Oops here's the link on how to read the Font Files from Hiero: ruclips.net/video/mnIQEQoHHCU/видео.html ) TL;DR Sign Distance Fields are fast, look good, allow for "fancy" effects, have fast anti-aliasing, and are easy to implement. I use SDFs myself and they look great. Hope this helps :)
Hey @Tony Stark, I completely agree with you that the signed distance field is the preferable way to do this, I just didn't want to give advice on something that I haven't implemented (yet). I'm going to implement this in my own code base in hopefully the next week or so, and then I'll give an updated video on how to use the SDF rendering or more advanced rendering techniques. Do you have an example of some code I could take a look at for how you handle dynamic and/or static text? I would love to see some extra code just so that I can try get a better picture of what you're talking about with the glyph index and everything :)
@@GamesWithGabe Sure, I code in C++ and take more of a Shotgun approach so I can give you some pseudo code that's some what legible :) PART 1/2 Edit: RUclips wouldn't let me post this in one comment :( Sorry in advance for any spelling mistakes Note: This code is not real and will not compile, but my actual code is sooooo intertwined that well... I just don't want to have write it all out :D I tried to clean it up from my actual code and yes I know this isn't optimized, it's better and "pointer safe" in the engine but this is nicer on the eyes. If anything was badly written, or if you have any questions let me know. struct Font { struct Glyph { // This struct is a member of Font unsigned int id; // This can be whatever type is needed (char/wchar_t/etc.) GLint index; // Vertex Offset for glDrawArrays(...) Vector2ui STmin, STmax; // Pixel width and height in Texture Space (can be used for explicit sizing) Vector2f UVmin, UVmax; Vector2f size; // Note: This is in HALF width/height and is in relative float values, while STmin, and STmax are full length and are in absolute values Vector2i offset; // Used for if the character is like 'q' or '`' that are below, above normal characters Vector2f forward_spacing; // Array modifiers; // Not used in this example but are for contraction of Glyphs like s̶t̶r̶i̶k̶e̶t̶h̶r̶o̶u̶g̶h̶ ̶t̶e̶x̶t, Diacritics (Á), etc. }; Vector2f scalar_coefficient; // Used to "Normalize" Fonts of Different Scales Vector2i line_margin; unsigned int null_glyph; // This is just the location (almost always zero) for a zero-width or unique character in case of failure GLsizei vertex_attrib_stride; RenderTexture_OpenGL atlas; // This just holds the GLuint id for the texture plus some other stuff like image format and quality RenderFormat_OpenGL format; // This is where the VAO for the VBO is held. RenderBuffer_OpenGL buffer; // This is where the VBO is held. The VBO data is the Quads for each character/glyph RenderProgram_OpenGL program; Olist glyphs; // This is an Ordered Array for faster Glyph finding. is just the type value of Glyph::id // Note: This Olist is only allowed to be intialized but never modified after that, as that would change the position of the Glyph locations for pre-existing Dynaimc Strings bool FindGlyph(unsigned int character_as_uint, unsigned int& resulting_location) { unsigned int olist_element; if (!this->glyphs.QuickSortFind(character_as_uint, olist_element)) { // Just gets the Glyph with the id that matches 'character_as_uint' resulting_location = this->null_glyph; // In this example I discard a failed string, but you may want to use the null glyph rather than discarding the whole string sometimes return false; } resulting_location = olist_element; return true; } bool Initalize() { // Just loads the data in from the Font File into this->glyphs, and sets up the atlas, format, buffer, and program. Then uploads the Font to the Engine } }; struct StaticTextString_OpenGL { unsigned int font_link; // Each Font has a unique ID in the Engine when they're added to it, and this just links to it (I use array style access later on to show the process) Color4f color; float width, edge_width; Matrix4f transformation; // A 4x4 Matrix for the Translation/Rotation/Scale/3D Projection/etc. for this particular Text String RenderBuffer_OpenGL buffer; // The VBO that holds the Quads of the Text String GLsizei count; // The number of characters in the string }; struct DynaimcTextString_OpenGL { unsigned int font_link; Color4f color; float width, edge_width; Matrix4f transformation; Array glyphs; // An Array of the locations of the Glyphs in the Font that make this Text String's characters (not the literal string itself) };
PART 2/2 void RenderStaticTextString(StaticTextString_OpenGL& SomeText) { // Okay so, in my Engine the Static Text Strings are Added to a Render Hierarchy for efficiency, but that's WAY too big for this comment RenderProgram_OpenGL* Program = &Engine.Fonts[SomeText.font_link].program; // This is a Pointer, just so I don't have to write the same thing every time here for each PseudoHierarchy->RenderBatch { glUseProgram(Program->binding_location); glEnable(GL_BLEND); glBlendEquation(GL_MULT); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, Engine.Fonts[SomeText.font_link].atlas.binding_location); glUniform1i(Program->uniform_texture_binding_location, 0); //glBindVertexArray(... This is handled by the hierarchy directly ...); glBindBuffer(GL_ARRAY_BUFFER, SomeText.buffer.binding_location); //glVertexAttribPointer(... This is handled by the hierarchy directly ...); glUniform4f(Program->color_uniform_binding_location, SomeText.color.r, SomeText.color.g, SomeText.color.b, SomeText.color.a); glUniform1f(Program->width_uniform_binding_location, SomeText.width); glUniform1f(Program->edge_width_uniform_binding_location, SomeText.edge_width); glUniformMatrix4fv( Program->transformation_uniform_binding_location, 1, GL_FALSE, (Program->view_projection_matrix * SomeText.transformation).GetGLfloatPointer() // Just Returns the 4x4 Matrix's Vectors as a single GLfloat* ); // didn't fit on one line in the RUclips comment ... Any Other Uniforms ... glDrawArrays(GL_QUADS, 0, SomeText.count * 4); } ... Unbind and Clean Up Here ... } void RenderDynamicTextString(DynaimcTextString_OpenGL& SomeText) { // A Dynaimc String can be added to the Render Hierarchy, or rendered as a stand alone object RenderProgram_OpenGL* Program = &Engine.Fonts[SomeText.font_link].program; glUseProgram(Program->program.binding_location); glEnable(GL_BLEND); glBlendEquation(GL_MULT); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, Engine.Fonts[SomeText.font_link].atlas.binding_location); glUniform1i(Program->uniform_texture_binding_location, 0); glBindVertexArray(Engine.Fonts[SomeText.font_link].format.binding_location); EDIT: oops I noticed s typeo here... SomeText.buffer.Activate(); // This Handles Things like glVertexAttribPointer(), glBindBuffer(), glVertexAttribFormat(), and glVertexAttribBinding() Should acually be... Engine.Fonts[SomeText.font_link].buffer.Activate(); // Because you're binding to the "master Vertex Buffer Object" aka unmodifed/shared VBO (also Dynamic Strings don't have a "buffer" member, oops lol) glUniform4f(Program->color_uniform_binding_location, SomeText.color.r, SomeText.color.g, SomeText.color.b, SomeText.color.a); glUniform1f(Program->width_uniform_binding_location, SomeText.width); glUniform1f(Program->edge_width_uniform_binding_location, SomeText.edge_width); ... Any Other Uniforms ... Matrix4f ModelSpace; Vector2f GlyphPosition = Vector2f(0.0f, 0.0f); for (unsigned int x = 0; x < SomeText.glyphs.GetSize(); ++x) { ModelSpace.SetIdenity(); ModelSpace.Translate(Vector3f(GlyphPosition, 0.0f)); ModelSpace.Scale(Vector3f(Engine.Fonts[SomeText.font_link].scalar_coefficient, 0.0f)); ModelSpace = Program->view_projection_matrix * SomeText.transformation * ModelSpace; glUniformMatrix4fv(Program->transformation_uniform_binding_location, 1, GL_FALSE, ModelSpace.GetGLfloatPointer()); GlyphPosition += Engine.Fonts[SomeText.font_link].glyphs[SomeText.glyphs[x]].forward_spacing; // Note: OpenGL uses Vertex Attrib Stride as offsets and not bytes // Also, Dynamic String uses more calls to glDrawArrays(...) but never uses modified VBO data (glDrawElements(...) can be used instead but this is just a simple example) glDrawArrays(GL_QUADS, Engine.Fonts[SomeText.font_link].glyphs[SomeText.glyphs[x]].index, 4); // ***** Glyph.index is the nᵗʰ vertex in the VBO ***** } ... Unbind and Clean Up Here ... } // Note: In C++ is a literal string when used in this context (your_text_here = "Hello World") // Also, this is a simplified example and does use SomeFont->line_margin or bounding area constraints. But shows that a Dynamic String is much faster to create and delete bool CreateDynamicString(char* your_text_here, Font* SomeFont, DynaimcTextString_OpenGL& result) { unsigned int location; for (unsigned int x = 0; your_text_here[x] != '\0'; ++x) { // '\0' is the "Null Terminator" in C++ (it ends a literal string) if (!SomeFont->FindGlyph(static_cast(your_text_here[x]), location)) { return false; } // cast to whatever type is needed result.glyphs.Push(location); } return true; } // Again simplified, but shows how Glyph Data can be laid out and written to the VBO through RenderBuffer_OpenGL // Note: Because the VBO data is being modified, if you wanted to do fancy String shapes like arcs and swigles you can. Rather than stright lines bool CreateStaticString(char* your_text_here, Font* SomeFont, StaticTextString_OpenGL& result) { result.buffer.Initalize(); result.count = 0; unsigned int location; unsigned int SubIndex = 0; Vector2f GlyphPosition = Vector2f(0.0f, 0.0f); GLfloat GlyphData[SomeFont->vertex_attrib_stride]; // You can't use variable expressions to declare the size of a stack array, but you get the point for (unsigned int x = 0; your_text_here[x] != '\0'; ++x) { if (!SomeFont->FindGlyph(static_cast(your_text_here[x]), location)) { return false; } result.buffer.Add(SomeFont->vertex_attrib_stride * 4); // The VBO Data for the four corners of the Quad in Model Space GlyphData = { GlyphPosition - SomeFont->glyphs[location].size.x + SomeFont->glyphs[location].offset, GlyphPosition - SomeFont->glyphs[location].size.y + SomeFont->glyphs[location].offset, SomeFont->glyphs[location].UVmin.x, SomeFont->glyphs[location].UVmin.y } // ***** This is Just Like how Glyph.index is Defined During Font Initialization ***** result.buffer.SubWrite(SubIndex, GlyphData); // vertex_attrib_stride; GlyphData = { GlyphPosition + SomeFont->glyphs[location].size.x + SomeFont->glyphs[location].offset, GlyphPosition - SomeFont->glyphs[location].size.y + SomeFont->glyphs[location].offset, SomeFont->glyphs[location].UVmax.x, SomeFont->glyphs[location].UVmin.y } result.buffer.SubWrite(SubIndex, GlyphData); SubIndex += SomeFont->vertex_attrib_stride; GlyphData = { GlyphPosition + SomeFont->glyphs[location].size.x + SomeFont->glyphs[location].offset, GlyphPosition + SomeFont->glyphs[location].size.y + SomeFont->glyphs[location].offset, SomeFont->glyphs[location].UVmax.x, SomeFont->glyphs[location].UVmax.y } result.buffer.SubWrite(SubIndex, GlyphData); SubIndex += SomeFont->vertex_attrib_stride; GlyphData = { GlyphPosition - SomeFont->glyphs[location].size.x + SomeFont->glyphs[location].offset, GlyphPosition + SomeFont->glyphs[location].size.y + SomeFont->glyphs[location].offset, SomeFont->glyphs[location].UVmin.x, SomeFont->glyphs[location].UVmax.y } result.buffer.SubWrite(SubIndex, GlyphData); SubIndex += SomeFont->vertex_attrib_stride; result.count += 1; GlyphPosition += SomeFont->glyphs[location].forward_spacing; } return true; }
This is fantastic! Was worried more about the conceptuals of font rendering, specifically dealing with bitmaps and storing the information of each glyph. I'm working with the raylib library in C, which does provide for font handling, but this more direct and foundational method of building something from the ground up is really exciting! Thanks for the great video!
Holy cow! This is great, I’ve been writing a little framework in C and I’ve done so in the past with rust but I feel like I actually struggle more with the concepts than the actual code
Thanks Louis! I'm definitely the same way, where sometimes its better for me to just see how something is done, and not necessarily how its coded. I hope your framework goes well!
This really helped for me! I was new to all this font rendering and stuff. A follow-up with OpenGL/Vulkan calls would be the next step. Or in Dear imgui. I'm a programmer and have knowledge quite some topics, of course I know ttf's. But this really gives a better inside in how Word processors are working as well.
Glad this helped you out Melroy! I'm planning on doing a video on how to implement it using OpenGL pretty soon, I just have to find the time to develop a slightly better system then record the tutorial :)
Thanks Luan! And I would love to give a video on the file format specs, but first I have to learn the specs haha. I may look into the format and write a parser as a fun side-project in the future though, and if I do I'll release a video on my findings :)
Hi. I am just starting off opengl and graphics. This channel is literally a hidden gem! Could you explain what draw calls are and and what you mean by render it in "1 draw call"? I understand this is a long topic but I am not able to find good videos on it. If you know any excellent resources for this and any other opengl/graphics concepts, could u suggest those too? For example, what you used to learn about these topics
Hi Ayush thanks for the comment! When I say draw calls I'm referring to any time you call a glDraw* function. So calling glDrawArrays, or glDrawElements, is typically one draw call. This isn't always true because there are certain draw calls in OpenGL like glDrawMulti* which can do multiple "draw calls" at once. If you want to dive deeper into how OpenGL works, I would highly recommend the OpenGL Superbible. I'm reading through the 7th edition which talks about some of the newest OpenGL features, but I believe there is a newer version and that may be even better :)
There are, I had some trouble trying to get it to work right the first time, so I figured I would give the Java 2D AWT library as an alternative just in case :)
A video of this quality deserves wayyy more views! I've never seen such a good video with only 200 views
Game Engine concepts?!!! This was uncalled for already. And Font Rendering? :O
Hey great video, I wanted to say that I recommend using Sign Distance Fields as the advantages outweigh the cons in my opinion. With TTF it sounds like your either planning on creating a "master atlas" pre-runtime (recommend) at some scale that looks nice, or computing the texture for a given scale on demand during runtime. With SDFs you should create the "master atlas" (like with the TTF) upscaled to what ever looks the best for your case, and then create Glyphs that represent your Text Quads.
You don't have to use 0.5 as your fixed cut off point (in fact i strongly recommend passing it as a variable Uniform to your shader). With SDFs you get anti-aliasing in to form of MSAA for "free" just use GL_LINEAR in your GL_TEXTURE_MIN_FILTER and GL_TEXTURE_MAG_FILTER in your glTexParameteri calls at the initial glGenTextures (or you can use GL_LINEAR_MIPMAP_LINEAR / GL_LINEAR_MIPMAP_NEAREST if you plan on using Mipmaps for 3D effects).
Also I recommend that you DON'T modify your VertexBuffer data for every text string you use, for static strings yeah sure it's efficient to pack the VBO and save it by itself for later. But for dynamic text (say like in a text editor, or FPS readout, or anything that is a throwaway string), instead you can use a "master Vertex Buffer Object" and store each character's index offset in their Glyph, and use a Transformation Matrix to modify the string's position/rotation/scale, rather than having to touch the GPU's memory directly.
Here's a link to a tool you can use to make SFD files (parse them to taste): libgdx.badlogicgames.com/tools.html It's at the bottom called "Hiero, A bitmap font packing tool"
And here's a RUclips link on how to implement Hiero in OpenGL: ruclips.net/video/d8cfgcJR9Tk/видео.html
(Edit: Oops here's the link on how to read the Font Files from Hiero: ruclips.net/video/mnIQEQoHHCU/видео.html )
TL;DR Sign Distance Fields are fast, look good, allow for "fancy" effects, have fast anti-aliasing, and are easy to implement.
I use SDFs myself and they look great. Hope this helps :)
Thinmatrix has a tutorial on both right?
@@amrutakelkar7529 He does have videos on both :)
Hey @Tony Stark, I completely agree with you that the signed distance field is the preferable way to do this, I just didn't want to give advice on something that I haven't implemented (yet). I'm going to implement this in my own code base in hopefully the next week or so, and then I'll give an updated video on how to use the SDF rendering or more advanced rendering techniques. Do you have an example of some code I could take a look at for how you handle dynamic and/or static text? I would love to see some extra code just so that I can try get a better picture of what you're talking about with the glyph index and everything :)
@@GamesWithGabe Sure, I code in C++ and take more of a Shotgun approach so I can give you some pseudo code that's some what legible :)
PART 1/2
Edit: RUclips wouldn't let me post this in one comment :(
Sorry in advance for any spelling mistakes
Note: This code is not real and will not compile, but my actual code is sooooo intertwined that well... I just don't want to have write it all out :D
I tried to clean it up from my actual code and yes I know this isn't optimized, it's better and "pointer safe" in the engine but this is nicer on the eyes.
If anything was badly written, or if you have any questions let me know.
struct Font {
struct Glyph { // This struct is a member of Font
unsigned int id; // This can be whatever type is needed (char/wchar_t/etc.)
GLint index; // Vertex Offset for glDrawArrays(...)
Vector2ui STmin, STmax; // Pixel width and height in Texture Space (can be used for explicit sizing)
Vector2f UVmin, UVmax;
Vector2f size; // Note: This is in HALF width/height and is in relative float values, while STmin, and STmax are full length and are in absolute values
Vector2i offset; // Used for if the character is like 'q' or '`' that are below, above normal characters
Vector2f forward_spacing;
// Array modifiers; // Not used in this example but are for contraction of Glyphs like s̶t̶r̶i̶k̶e̶t̶h̶r̶o̶u̶g̶h̶ ̶t̶e̶x̶t, Diacritics (Á), etc.
};
Vector2f scalar_coefficient; // Used to "Normalize" Fonts of Different Scales
Vector2i line_margin;
unsigned int null_glyph; // This is just the location (almost always zero) for a zero-width or unique character in case of failure
GLsizei vertex_attrib_stride;
RenderTexture_OpenGL atlas; // This just holds the GLuint id for the texture plus some other stuff like image format and quality
RenderFormat_OpenGL format; // This is where the VAO for the VBO is held.
RenderBuffer_OpenGL buffer; // This is where the VBO is held. The VBO data is the Quads for each character/glyph
RenderProgram_OpenGL program;
Olist glyphs; // This is an Ordered Array for faster Glyph finding. is just the type value of Glyph::id
// Note: This Olist is only allowed to be intialized but never modified after that, as that would change the position of the Glyph locations for pre-existing Dynaimc Strings
bool FindGlyph(unsigned int character_as_uint, unsigned int& resulting_location) {
unsigned int olist_element;
if (!this->glyphs.QuickSortFind(character_as_uint, olist_element)) { // Just gets the Glyph with the id that matches 'character_as_uint'
resulting_location = this->null_glyph; // In this example I discard a failed string, but you may want to use the null glyph rather than discarding the whole string sometimes
return false;
}
resulting_location = olist_element;
return true;
}
bool Initalize() {
// Just loads the data in from the Font File into this->glyphs, and sets up the atlas, format, buffer, and program. Then uploads the Font to the Engine
}
};
struct StaticTextString_OpenGL {
unsigned int font_link; // Each Font has a unique ID in the Engine when they're added to it, and this just links to it (I use array style access later on to show the process)
Color4f color;
float width, edge_width;
Matrix4f transformation; // A 4x4 Matrix for the Translation/Rotation/Scale/3D Projection/etc. for this particular Text String
RenderBuffer_OpenGL buffer; // The VBO that holds the Quads of the Text String
GLsizei count; // The number of characters in the string
};
struct DynaimcTextString_OpenGL {
unsigned int font_link;
Color4f color;
float width, edge_width;
Matrix4f transformation;
Array glyphs; // An Array of the locations of the Glyphs in the Font that make this Text String's characters (not the literal string itself)
};
PART 2/2
void RenderStaticTextString(StaticTextString_OpenGL& SomeText) {
// Okay so, in my Engine the Static Text Strings are Added to a Render Hierarchy for efficiency, but that's WAY too big for this comment
RenderProgram_OpenGL* Program = &Engine.Fonts[SomeText.font_link].program; // This is a Pointer, just so I don't have to write the same thing every time here
for each PseudoHierarchy->RenderBatch {
glUseProgram(Program->binding_location);
glEnable(GL_BLEND);
glBlendEquation(GL_MULT);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, Engine.Fonts[SomeText.font_link].atlas.binding_location);
glUniform1i(Program->uniform_texture_binding_location, 0);
//glBindVertexArray(... This is handled by the hierarchy directly ...);
glBindBuffer(GL_ARRAY_BUFFER, SomeText.buffer.binding_location);
//glVertexAttribPointer(... This is handled by the hierarchy directly ...);
glUniform4f(Program->color_uniform_binding_location, SomeText.color.r, SomeText.color.g, SomeText.color.b, SomeText.color.a);
glUniform1f(Program->width_uniform_binding_location, SomeText.width);
glUniform1f(Program->edge_width_uniform_binding_location, SomeText.edge_width);
glUniformMatrix4fv(
Program->transformation_uniform_binding_location,
1,
GL_FALSE,
(Program->view_projection_matrix * SomeText.transformation).GetGLfloatPointer() // Just Returns the 4x4 Matrix's Vectors as a single GLfloat*
); // didn't fit on one line in the RUclips comment
... Any Other Uniforms ...
glDrawArrays(GL_QUADS, 0, SomeText.count * 4);
}
... Unbind and Clean Up Here ...
}
void RenderDynamicTextString(DynaimcTextString_OpenGL& SomeText) {
// A Dynaimc String can be added to the Render Hierarchy, or rendered as a stand alone object
RenderProgram_OpenGL* Program = &Engine.Fonts[SomeText.font_link].program;
glUseProgram(Program->program.binding_location);
glEnable(GL_BLEND);
glBlendEquation(GL_MULT);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, Engine.Fonts[SomeText.font_link].atlas.binding_location);
glUniform1i(Program->uniform_texture_binding_location, 0);
glBindVertexArray(Engine.Fonts[SomeText.font_link].format.binding_location);
EDIT: oops I noticed s typeo here...
SomeText.buffer.Activate(); // This Handles Things like glVertexAttribPointer(), glBindBuffer(), glVertexAttribFormat(), and glVertexAttribBinding()
Should acually be...
Engine.Fonts[SomeText.font_link].buffer.Activate(); // Because you're binding to the "master Vertex Buffer Object" aka unmodifed/shared VBO (also Dynamic Strings don't have a "buffer" member, oops lol)
glUniform4f(Program->color_uniform_binding_location, SomeText.color.r, SomeText.color.g, SomeText.color.b, SomeText.color.a);
glUniform1f(Program->width_uniform_binding_location, SomeText.width);
glUniform1f(Program->edge_width_uniform_binding_location, SomeText.edge_width);
... Any Other Uniforms ...
Matrix4f ModelSpace;
Vector2f GlyphPosition = Vector2f(0.0f, 0.0f);
for (unsigned int x = 0; x < SomeText.glyphs.GetSize(); ++x) {
ModelSpace.SetIdenity();
ModelSpace.Translate(Vector3f(GlyphPosition, 0.0f));
ModelSpace.Scale(Vector3f(Engine.Fonts[SomeText.font_link].scalar_coefficient, 0.0f));
ModelSpace = Program->view_projection_matrix * SomeText.transformation * ModelSpace;
glUniformMatrix4fv(Program->transformation_uniform_binding_location, 1, GL_FALSE, ModelSpace.GetGLfloatPointer());
GlyphPosition += Engine.Fonts[SomeText.font_link].glyphs[SomeText.glyphs[x]].forward_spacing;
// Note: OpenGL uses Vertex Attrib Stride as offsets and not bytes
// Also, Dynamic String uses more calls to glDrawArrays(...) but never uses modified VBO data (glDrawElements(...) can be used instead but this is just a simple example)
glDrawArrays(GL_QUADS, Engine.Fonts[SomeText.font_link].glyphs[SomeText.glyphs[x]].index, 4); // ***** Glyph.index is the nᵗʰ vertex in the VBO *****
}
... Unbind and Clean Up Here ...
}
// Note: In C++ is a literal string when used in this context (your_text_here = "Hello World")
// Also, this is a simplified example and does use SomeFont->line_margin or bounding area constraints. But shows that a Dynamic String is much faster to create and delete
bool CreateDynamicString(char* your_text_here, Font* SomeFont, DynaimcTextString_OpenGL& result) {
unsigned int location;
for (unsigned int x = 0; your_text_here[x] != '\0'; ++x) { // '\0' is the "Null Terminator" in C++ (it ends a literal string)
if (!SomeFont->FindGlyph(static_cast(your_text_here[x]), location)) { return false; } // cast to whatever type is needed
result.glyphs.Push(location);
}
return true;
}
// Again simplified, but shows how Glyph Data can be laid out and written to the VBO through RenderBuffer_OpenGL
// Note: Because the VBO data is being modified, if you wanted to do fancy String shapes like arcs and swigles you can. Rather than stright lines
bool CreateStaticString(char* your_text_here, Font* SomeFont, StaticTextString_OpenGL& result) {
result.buffer.Initalize();
result.count = 0;
unsigned int location;
unsigned int SubIndex = 0;
Vector2f GlyphPosition = Vector2f(0.0f, 0.0f);
GLfloat GlyphData[SomeFont->vertex_attrib_stride]; // You can't use variable expressions to declare the size of a stack array, but you get the point
for (unsigned int x = 0; your_text_here[x] != '\0'; ++x) {
if (!SomeFont->FindGlyph(static_cast(your_text_here[x]), location)) { return false; }
result.buffer.Add(SomeFont->vertex_attrib_stride * 4);
// The VBO Data for the four corners of the Quad in Model Space
GlyphData = {
GlyphPosition - SomeFont->glyphs[location].size.x + SomeFont->glyphs[location].offset,
GlyphPosition - SomeFont->glyphs[location].size.y + SomeFont->glyphs[location].offset,
SomeFont->glyphs[location].UVmin.x,
SomeFont->glyphs[location].UVmin.y
}
// ***** This is Just Like how Glyph.index is Defined During Font Initialization *****
result.buffer.SubWrite(SubIndex, GlyphData); // vertex_attrib_stride;
GlyphData = {
GlyphPosition + SomeFont->glyphs[location].size.x + SomeFont->glyphs[location].offset,
GlyphPosition - SomeFont->glyphs[location].size.y + SomeFont->glyphs[location].offset,
SomeFont->glyphs[location].UVmax.x,
SomeFont->glyphs[location].UVmin.y
}
result.buffer.SubWrite(SubIndex, GlyphData);
SubIndex += SomeFont->vertex_attrib_stride;
GlyphData = {
GlyphPosition + SomeFont->glyphs[location].size.x + SomeFont->glyphs[location].offset,
GlyphPosition + SomeFont->glyphs[location].size.y + SomeFont->glyphs[location].offset,
SomeFont->glyphs[location].UVmax.x,
SomeFont->glyphs[location].UVmax.y
}
result.buffer.SubWrite(SubIndex, GlyphData);
SubIndex += SomeFont->vertex_attrib_stride;
GlyphData = {
GlyphPosition - SomeFont->glyphs[location].size.x + SomeFont->glyphs[location].offset,
GlyphPosition + SomeFont->glyphs[location].size.y + SomeFont->glyphs[location].offset,
SomeFont->glyphs[location].UVmin.x,
SomeFont->glyphs[location].UVmax.y
}
result.buffer.SubWrite(SubIndex, GlyphData);
SubIndex += SomeFont->vertex_attrib_stride;
result.count += 1;
GlyphPosition += SomeFont->glyphs[location].forward_spacing;
}
return true;
}
dude thank you, almost no one does tutorials like this one, pls make more!
Thanks for this tip, that header file was fantastically easy to use!
And if you are fine with using a pixelated non anti-aliased font the sizing resolution doesn't matter! Great video!
This is fantastic! Was worried more about the conceptuals of font rendering, specifically dealing with bitmaps and storing the information of each glyph. I'm working with the raylib library in C, which does provide for font handling, but this more direct and foundational method of building something from the ground up is really exciting! Thanks for the great video!
Holy cow! This is great, I’ve been writing a little framework in C and I’ve done so in the past with rust but I feel like I actually struggle more with the concepts than the actual code
Thanks Louis! I'm definitely the same way, where sometimes its better for me to just see how something is done, and not necessarily how its coded. I hope your framework goes well!
Yet more excellent content from Gabe!
This really helped for me! I was new to all this font rendering and stuff. A follow-up with OpenGL/Vulkan calls would be the next step. Or in Dear imgui.
I'm a programmer and have knowledge quite some topics, of course I know ttf's. But this really gives a better inside in how Word processors are working as well.
Glad this helped you out Melroy! I'm planning on doing a video on how to implement it using OpenGL pretty soon, I just have to find the time to develop a slightly better system then record the tutorial :)
WOW! Very nice video! Probably the best on font rendering you can get on youtube. Simpel and no code just how I like it :)
Thanks Lotwig! I'll keep that in mind and try and make more concept with no code videos :D
👏🏻👏🏻👏🏻👏🏻 Amazing video, explain about TTF File Format Struct
Thanks Luan! And I would love to give a video on the file format specs, but first I have to learn the specs haha. I may look into the format and write a parser as a fun side-project in the future though, and if I do I'll release a video on my findings :)
this is great!
more non game related gpu stuff please
Thanks miguel! And I would love too, I just haven't been able to think of any other good ideas. Do you have any suggestions?
Very nice guide. I'll stick with Imgui however haha
Hi. I am just starting off opengl and graphics. This channel is literally a hidden gem! Could you explain what draw calls are and and what you mean by render it in "1 draw call"? I understand this is a long topic but I am not able to find good videos on it. If you know any excellent resources for this and any other opengl/graphics concepts, could u suggest those too? For example, what you used to learn about these topics
Hi Ayush thanks for the comment! When I say draw calls I'm referring to any time you call a glDraw* function. So calling glDrawArrays, or glDrawElements, is typically one draw call. This isn't always true because there are certain draw calls in OpenGL like glDrawMulti* which can do multiple "draw calls" at once. If you want to dive deeper into how OpenGL works, I would highly recommend the OpenGL Superbible. I'm reading through the 7th edition which talks about some of the newest OpenGL features, but I believe there is a newer version and that may be even better :)
this is very helpful, thanks!
No problem @Asher, I want to release a video on how to code it pretty soon also :)
Thank you.
2d game engine series man love it
Uhmm. UTF8 is not a character set.
2:47 But you *will* end up loading thousands upon thousands of characters if you want to load *all* characters for Mandarin.
there is stb bindings in lwjgl
There are, I had some trouble trying to get it to work right the first time, so I figured I would give the Java 2D AWT library as an alternative just in case :)
BEZ - E- A not BEZ -E-ERR