J2D: Rewrite text rendering
This patch rewrites text rendering for J2D::DrawString to now construct a texture atlas for all ASCII-range glyphs in the FT font face, instead of cosntructing a texture for every glyph. This improves text rendering performance for several reasons: 1. Binding textures is relatively expensive as the GPU is required to do a context switch for internal data like texture parameters, and also cannot optimize for accesses to the same texture across draw calls. This patch removes the need to call glBindTexture more than once per call to J2D::DrawString. 2. As a consequence of the above, all glyphs for a given string can now be rendered in a single call to glDrawArrays. This is done by storing the cached texture coordinates on CachedGlyph and constructing a full array of vertices and texture coordinates for the entire string at once, resulting in only /one/ set of client-to-device attribute uploads and only one draw call, instead of being required to upload attribute data for each glyph separately.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <vector>
|
||||
#include <glad/glad.h>
|
||||
|
||||
@@ -11,33 +12,35 @@ namespace JGL {
|
||||
|
||||
class JGL::CachedGlyph {
|
||||
private:
|
||||
GLuint texture = 0;
|
||||
char character;
|
||||
std::array<GLfloat, 12> texcoords;
|
||||
public:
|
||||
int x2offset = 0, y2offset = 0, w = 0, h = 0;
|
||||
float advanceX = 0, advanceY = 0;
|
||||
|
||||
//CachedGlyph(GLuint texture_id, char c);
|
||||
CachedGlyph(GLuint texture_id, char c, float x2o, float y2o, float w, float h, float advX, float advY);
|
||||
CachedGlyph(char c, std::array<GLfloat, 12> texcoords, float x2o, float y2o, float w, float h, float advX, float advY);
|
||||
char getCharacter();
|
||||
const GLuint* getTexture();
|
||||
const std::array<GLfloat, 12> getTexCoords() const;
|
||||
};
|
||||
|
||||
class JGL::CachedFont {
|
||||
private:
|
||||
std::vector<CachedGlyph*> glyphs{};
|
||||
GLuint texture = 0;
|
||||
GLsizei texture_width = 0, texture_height = 0;
|
||||
unsigned int font_size = 0;
|
||||
unsigned int font_index = 0;
|
||||
public:
|
||||
void appendGlyph(CachedGlyph* glyph);
|
||||
void eraseGlyph(CachedGlyph* glyph);
|
||||
void eraseGlyph(char c);
|
||||
void eraseGlyph(GLuint texture_id);
|
||||
unsigned int getFontSize();
|
||||
unsigned int getFontIndex();
|
||||
CachedGlyph* getGlyph(char c);
|
||||
std::vector<CachedGlyph*> getGlyphs();
|
||||
CachedFont(unsigned int font_size, unsigned int font_index);
|
||||
const GLuint* getTexture();
|
||||
const GLsizei getTextureWidth() const;
|
||||
const GLsizei getTextureHeight() const;
|
||||
CachedFont(GLuint texture_id, GLsizei texture_width, GLsizei texture_height, unsigned int font_size, unsigned int font_index);
|
||||
};
|
||||
|
||||
class JGL::FontCache {
|
||||
@@ -47,7 +50,7 @@ public:
|
||||
std::vector<CachedFont*> getFonts();
|
||||
CachedFont* getFont(unsigned int font_size, unsigned int font_index);
|
||||
void appendFont(CachedFont* font);
|
||||
void newFont(unsigned int font_size, unsigned int font_index);
|
||||
void newFont(GLuint texture_id, GLsizei texture_width, GLsizei texture_height, unsigned int font_size, unsigned int font_index);
|
||||
void eraseFont(CachedFont* font);
|
||||
void purgeCache();
|
||||
};
|
||||
};
|
||||
|
@@ -7,12 +7,11 @@ char CachedGlyph::getCharacter() {
|
||||
return character;
|
||||
}
|
||||
|
||||
const GLuint* CachedGlyph::getTexture() {
|
||||
return &texture;
|
||||
const std::array<GLfloat, 12> CachedGlyph::getTexCoords() const {
|
||||
return texcoords;
|
||||
}
|
||||
|
||||
CachedGlyph::CachedGlyph(GLuint texture_id, char c, float x2offset, float y2offset, float w, float h, float advanceX, float advanceY) {
|
||||
texture = texture_id;
|
||||
CachedGlyph::CachedGlyph(char c, std::array<GLfloat, 12> texcoords, float x2offset, float y2offset, float w, float h, float advanceX, float advanceY) {
|
||||
character = c;
|
||||
this->x2offset = x2offset;
|
||||
this->y2offset = y2offset;
|
||||
@@ -20,6 +19,7 @@ CachedGlyph::CachedGlyph(GLuint texture_id, char c, float x2offset, float y2offs
|
||||
this->h = h;
|
||||
this->advanceX = advanceX;
|
||||
this->advanceY = advanceY;
|
||||
this->texcoords = texcoords;
|
||||
}
|
||||
|
||||
//TODO
|
||||
@@ -44,57 +44,42 @@ CachedGlyph* JGL::CachedFont::getGlyph(char c) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
CachedFont::CachedFont(unsigned int font_size, unsigned int font_index) {
|
||||
CachedFont::CachedFont(GLuint texture_id, GLsizei texture_width, GLsizei texture_height, unsigned int font_size, unsigned int font_index) {
|
||||
this->texture = texture_id;
|
||||
this->texture_width = texture_width;
|
||||
this->texture_height = texture_height;
|
||||
this->font_size = font_size;
|
||||
this->font_index = font_index;
|
||||
}
|
||||
|
||||
void CachedFont::eraseGlyph(CachedGlyph* glyph) {
|
||||
if (glyph == nullptr)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < glyphs.size(); i++)
|
||||
if (glyphs[i] == glyph)
|
||||
glDeleteTextures(1, glyphs[i]->getTexture()),
|
||||
delete glyphs[i],
|
||||
glyphs.erase(glyphs.begin() + i);
|
||||
}
|
||||
|
||||
void CachedFont::eraseGlyph(char c) {
|
||||
for (int i = 0; i < glyphs.size(); i++)
|
||||
if (glyphs[i]->getCharacter() == c)
|
||||
glDeleteTextures(1, glyphs[i]->getTexture()),
|
||||
delete glyphs[i],
|
||||
glyphs.erase(glyphs.begin() + i);
|
||||
}
|
||||
|
||||
void CachedFont::eraseGlyph(GLuint texture_id) {
|
||||
for (int i = 0; i < glyphs.size(); i++)
|
||||
if (glyphs[i]->getTexture() == &texture_id)
|
||||
glDeleteTextures(1, glyphs[i]->getTexture()),
|
||||
delete glyphs[i],
|
||||
glyphs.erase(glyphs.begin() + i);
|
||||
}
|
||||
|
||||
std::vector<CachedGlyph*> CachedFont::getGlyphs() {
|
||||
return glyphs;
|
||||
}
|
||||
|
||||
const GLuint* CachedFont::getTexture() {
|
||||
return &texture;
|
||||
}
|
||||
|
||||
const GLsizei CachedFont::getTextureWidth() const {
|
||||
return texture_width;
|
||||
}
|
||||
|
||||
const GLsizei CachedFont::getTextureHeight() const {
|
||||
return texture_height;
|
||||
}
|
||||
|
||||
void FontCache::appendFont(CachedFont* font) {
|
||||
cachedFonts.push_back(font);
|
||||
}
|
||||
|
||||
void FontCache::newFont(unsigned int font_size, unsigned int font_index) {
|
||||
auto* font = new CachedFont(font_size, font_index);
|
||||
void FontCache::newFont(GLuint texture_id, GLsizei texture_width, GLsizei texture_height, unsigned int font_size, unsigned int font_index) {
|
||||
auto* font = new CachedFont(texture_id, texture_width, texture_height, font_size, font_index);
|
||||
cachedFonts.push_back(font);
|
||||
}
|
||||
|
||||
void FontCache::eraseFont(CachedFont* font) {
|
||||
for (int i = 0; i < cachedFonts.size(); i++) {
|
||||
if (cachedFonts[i] == font) {
|
||||
for (auto& g: cachedFonts[i]->getGlyphs())
|
||||
cachedFonts[i]->eraseGlyph(g);
|
||||
|
||||
delete cachedFonts[i];
|
||||
cachedFonts.erase(cachedFonts.begin() + i);
|
||||
}
|
||||
|
@@ -63,14 +63,8 @@ namespace JGL {
|
||||
Font font{};
|
||||
CachedFont* cachedFont = fontCache.getFont(size, font_index);
|
||||
|
||||
//If the font doesn't exist in the cache yet.
|
||||
if (!cachedFont) {
|
||||
fontCache.newFont(size, font_index);
|
||||
cachedFont = fontCache.getFont(size, font_index);
|
||||
}
|
||||
|
||||
//Set up the regular font.
|
||||
for (const auto& f : faces)
|
||||
for (const auto &f : faces)
|
||||
if (f.index == font_index)
|
||||
font = f;
|
||||
if (font.face == nullptr)
|
||||
@@ -78,64 +72,114 @@ namespace JGL {
|
||||
|
||||
FT_Set_Pixel_Sizes(font.face, 0, size);
|
||||
|
||||
glColor4f(color.r / 255.f, color.g / 255.f, color.b / 255.f, color.a / 255.f);
|
||||
//If the font doesn't exist in the cache yet.
|
||||
if (!cachedFont) {
|
||||
GLuint texture_id;
|
||||
glGenTextures(1, &texture_id);
|
||||
glBindTexture(GL_TEXTURE_2D, texture_id);
|
||||
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);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
|
||||
|
||||
std::vector<GLuint> textures(text.length());
|
||||
GLsizei width = 0;
|
||||
GLsizei max_height = 0;
|
||||
|
||||
//For each character
|
||||
for (int i = 0; i < text.length(); i++) {
|
||||
float x2, y2, w, h;
|
||||
//If the font is in the cache already.
|
||||
if (cachedFont->getGlyph(text.c_str()[i])) {
|
||||
CachedGlyph* glyph = cachedFont->getGlyph(text.c_str()[i]);
|
||||
FT_ULong charcode;
|
||||
FT_UInt gindex;
|
||||
|
||||
glBindTexture(GL_TEXTURE_2D, *glyph->getTexture());
|
||||
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);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
|
||||
//We have to loop over the available glyphs twice as we need the
|
||||
//final width and height of the texture before we can construct it
|
||||
//and subsequently upload the glyph data.
|
||||
|
||||
x2 = x + glyph->x2offset * scale;
|
||||
y2 = y - glyph->y2offset * scale; // Adjust y-coordinate
|
||||
w = glyph->w * scale;
|
||||
h = glyph->h * scale;
|
||||
x += glyph->advanceX * scale;
|
||||
y += glyph->advanceY * scale;
|
||||
|
||||
} else {
|
||||
if (FT_Load_Char(font.face, text.c_str()[i], FT_LOAD_RENDER))
|
||||
continue;
|
||||
charcode = FT_Get_First_Char(font.face, &gindex);
|
||||
//Strings are char-based so we only handle charcodes within the extended ASCII range.
|
||||
while (gindex != 0 && charcode < 255) {
|
||||
if (FT_Load_Char(font.face, charcode, FT_LOAD_RENDER))
|
||||
std::cout << "Error::FREETYPE: Failed to load charcode: " << charcode << std::endl;
|
||||
|
||||
FT_GlyphSlot g = font.face->glyph;
|
||||
glGenTextures(1, &textures.at(i));
|
||||
glBindTexture(GL_TEXTURE_2D, textures[i]);
|
||||
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);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_ALPHA, g->bitmap.width, g->bitmap.rows, 0, GL_ALPHA, GL_UNSIGNED_BYTE, g->bitmap.buffer);
|
||||
|
||||
x2 = x + g->bitmap_left * scale;
|
||||
y2 = -y - g->bitmap_top * scale; // Adjust y-coordinate
|
||||
w = g->bitmap.width * scale;
|
||||
h = g->bitmap.rows * scale;
|
||||
x += (g->advance.x >> 6) * scale;
|
||||
y += (g->advance.y >> 6) * scale;
|
||||
cachedFont->appendGlyph(new CachedGlyph(textures.at(i), text.c_str()[i], g->bitmap_left, g->bitmap_top, g->bitmap.width, g->bitmap.rows, (g->advance.x >> 6), (g->advance.y >> 6)));
|
||||
width += g->bitmap.width;
|
||||
max_height = std::max(max_height, (GLsizei)g->bitmap.rows);
|
||||
charcode = FT_Get_Next_Char(font.face, charcode, &gindex);
|
||||
}
|
||||
|
||||
GLfloat vertices[12] = {x2, y2, x2, y2 + h, x2 + w, y2 + h,x2, y2, x2 + w, y2 + h, x2 + w, y2};
|
||||
GLfloat textureCoordinates[12] = {0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0};
|
||||
fontCache.newFont(texture_id, width, max_height, size, font_index);
|
||||
cachedFont = fontCache.getFont(size, font_index);
|
||||
|
||||
glTexCoordPointer(2, GL_FLOAT, sizeof(GL_FLOAT) * 2, &textureCoordinates);
|
||||
glVertexPointer(2, GL_FLOAT, sizeof(GL_FLOAT) * 2, &vertices);
|
||||
glDrawArrays(GL_TRIANGLES, 0, 6);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_ALPHA, width, max_height, 0, GL_ALPHA, GL_UNSIGNED_BYTE, nullptr);
|
||||
|
||||
GLsizei xoffset = 0;
|
||||
|
||||
charcode = FT_Get_First_Char(font.face, &gindex);
|
||||
while (gindex != 0 && charcode < 255) {
|
||||
if (FT_Load_Char(font.face, charcode, FT_LOAD_RENDER))
|
||||
std::cout << "Error::FREETYPE: Failed to load charcode: " << charcode << std::endl;
|
||||
|
||||
FT_GlyphSlot g = font.face->glyph;
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, xoffset, 0, g->bitmap.width, g->bitmap.rows, GL_ALPHA, GL_UNSIGNED_BYTE, g->bitmap.buffer);
|
||||
|
||||
GLfloat u0 = (GLfloat)xoffset / cachedFont->getTextureWidth();
|
||||
GLfloat u1 = u0 + (GLfloat)g->bitmap.width / cachedFont->getTextureWidth();
|
||||
|
||||
GLfloat v0 = 0.0f;
|
||||
GLfloat v1 = (GLfloat)g->bitmap.rows / cachedFont->getTextureHeight();
|
||||
|
||||
std::array<GLfloat, 12> texcoords = {
|
||||
u0, v0,
|
||||
u0, v1,
|
||||
u1, v1,
|
||||
u0, v0,
|
||||
u1, v1,
|
||||
u1, v0
|
||||
};
|
||||
|
||||
cachedFont->appendGlyph(new CachedGlyph((char)charcode, texcoords, g->bitmap_left, g->bitmap_top, g->bitmap.width, g->bitmap.rows, (g->advance.x >> 6), (g->advance.y >> 6)));
|
||||
|
||||
xoffset += g->bitmap.width;
|
||||
charcode = FT_Get_Next_Char(font.face, charcode, &gindex);
|
||||
}
|
||||
}
|
||||
glBindTexture(GL_TEXTURE_2D, 0); // Unbind texture
|
||||
|
||||
glColor4f(color.r / 255.f, color.g / 255.f, color.b / 255.f, color.a / 255.f);
|
||||
|
||||
//Texture parameters are restored when the texture is bound
|
||||
glBindTexture(GL_TEXTURE_2D, *cachedFont->getTexture());
|
||||
|
||||
std::vector<GLfloat> vertices;
|
||||
std::vector<GLfloat> texcoords;
|
||||
|
||||
for (int i = 0; i < text.length(); i++) {
|
||||
float x2, y2, w, h;
|
||||
CachedGlyph *glyph = cachedFont->getGlyph(text.c_str()[i]);
|
||||
if (glyph == nullptr) continue;
|
||||
|
||||
x2 = x + glyph->x2offset * scale;
|
||||
y2 = y - glyph->y2offset * scale; // Adjust y-coordinate
|
||||
w = glyph->w * scale;
|
||||
h = glyph->h * scale;
|
||||
x += glyph->advanceX * scale;
|
||||
y += glyph->advanceY * scale;
|
||||
|
||||
std::array<GLfloat, 12> glyph_vertices = {
|
||||
x2, y2,
|
||||
x2, y2 + h,
|
||||
x2 + w, y2 + h,
|
||||
x2, y2,
|
||||
x2 + w, y2 + h,
|
||||
x2 + w, y2
|
||||
};
|
||||
auto glyph_texcoords = glyph->getTexCoords();
|
||||
|
||||
vertices.insert(vertices.end(), glyph_vertices.begin(), glyph_vertices.end());
|
||||
texcoords.insert(texcoords.end(), glyph_texcoords.begin(), glyph_texcoords.end());
|
||||
}
|
||||
|
||||
glVertexPointer(2, GL_FLOAT, sizeof(GLfloat) * 2, vertices.data());
|
||||
glTexCoordPointer(2, GL_FLOAT, sizeof(GLfloat) * 2, texcoords.data());
|
||||
glDrawArrays(GL_TRIANGLES, 0, vertices.size() / 2);
|
||||
}
|
||||
|
||||
void J2D::DrawString(const Color3& color, const std::string& text, float x, float y, float scale, u32 size, unsigned int font_index) {
|
||||
|
Reference in New Issue
Block a user