318 lines
10 KiB
C++
318 lines
10 KiB
C++
#include "uui/opengl/font_manager.hpp"
|
|
|
|
#include <cmath>
|
|
#include <cstring>
|
|
|
|
#include <algorithm>
|
|
#include <iostream>
|
|
|
|
#include "uui/config.hpp"
|
|
#include "uui/opengl/application.hpp"
|
|
#include "uui/opengl/gl_utils.hpp"
|
|
#include "uui/shader.hpp"
|
|
|
|
uui::GlFontManager::GlFontManager()
|
|
{
|
|
initialized = false;
|
|
}
|
|
|
|
uui::GlFontManager::~GlFontManager()
|
|
{
|
|
if(initialized)
|
|
deinit();
|
|
}
|
|
|
|
void uui::GlFontManager::init()
|
|
{
|
|
if(initialized)
|
|
throw std::runtime_error("GlFontManager error : calling init while initialized");
|
|
|
|
if constexpr(uui::GlApplication::debug_mode)
|
|
std::cout << "Init GlFontManager" << std::endl;
|
|
|
|
if(FT_Init_FreeType(&ft))
|
|
throw std::runtime_error("ERROR::FREETYPE: Could not init FreeType Library");
|
|
|
|
gl_program = glCreateProgram();
|
|
// Vertex shader compilation
|
|
GLuint vertex_shader = glCreateShader(GL_VERTEX_SHADER);
|
|
{
|
|
auto vertex_src = readFile("bin/uui/opengl/shaders/text.vert");
|
|
auto vertex_str = std::string(vertex_src.begin(), vertex_src.end());
|
|
auto vertex_code = vertex_str.c_str();
|
|
glShaderSource(vertex_shader, 1, &vertex_code, NULL);
|
|
glCompileShader(vertex_shader);
|
|
if constexpr(uui::GlApplication::debug_mode)
|
|
{
|
|
int success;
|
|
char info[512];
|
|
glGetShaderiv(vertex_shader, GL_COMPILE_STATUS, &success);
|
|
if(!success)
|
|
{
|
|
glGetShaderInfoLog(vertex_shader, 512, NULL, info);
|
|
std::cout << "GlBox vertex shader creation error : " << info << std::endl;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fragment shader compilation
|
|
GLuint fragment_shader = glCreateShader(GL_FRAGMENT_SHADER);
|
|
{
|
|
auto fragment_src = readFile("bin/uui/opengl/shaders/text.frag");
|
|
auto fragment_str = std::string(fragment_src.begin(), fragment_src.end());
|
|
auto fragment_code = fragment_str.c_str();
|
|
glShaderSource(fragment_shader, 1, &fragment_code, NULL);
|
|
glCompileShader(fragment_shader);
|
|
if constexpr(uui::GlApplication::debug_mode)
|
|
{
|
|
int success;
|
|
char info[512];
|
|
glGetShaderiv(fragment_shader, GL_COMPILE_STATUS, &success);
|
|
if(!success)
|
|
{
|
|
glGetShaderInfoLog(fragment_shader, 512, NULL, info);
|
|
std::cout << "GlBox fragment shader creation error : " << info << std::endl;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Linking shader program
|
|
glAttachShader(gl_program, vertex_shader);
|
|
if constexpr(uui::GlApplication::debug_mode)
|
|
_glCheckError();
|
|
glAttachShader(gl_program, fragment_shader);
|
|
if constexpr(uui::GlApplication::debug_mode)
|
|
_glCheckError();
|
|
glLinkProgram(gl_program);
|
|
if constexpr(uui::GlApplication::debug_mode)
|
|
{
|
|
int success;
|
|
char info[512];
|
|
glGetProgramiv(gl_program, GL_LINK_STATUS, &success);
|
|
if(!success)
|
|
{
|
|
glGetProgramInfoLog(gl_program, 512, NULL, info);
|
|
std::cout << "GlBox error : " << info << std::endl;
|
|
}
|
|
}
|
|
glDeleteShader(vertex_shader);
|
|
glDeleteShader(fragment_shader);
|
|
|
|
// init_font("/usr/share/fonts/noto/NotoSans-Regular.ttf", 32);
|
|
init_font("FreeSans.ttf", 32);
|
|
|
|
// Initialize VAO/VBO for rendering
|
|
glGenVertexArrays(1, &gl_vao);
|
|
glGenBuffers(1, &gl_vbo);
|
|
glBindVertexArray(gl_vao);
|
|
glBindBuffer(GL_ARRAY_BUFFER, gl_vbo);
|
|
glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 6 * 4, NULL, GL_DYNAMIC_DRAW);
|
|
glEnableVertexAttribArray(0);
|
|
glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(float), 0);
|
|
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
|
glBindVertexArray(0);
|
|
|
|
initialized = true;
|
|
}
|
|
|
|
void uui::GlFontManager::deinit()
|
|
{
|
|
if(!initialized)
|
|
throw std::runtime_error("GlFontManager error : calling deinit while not initialized");
|
|
|
|
if constexpr(uui::GlApplication::debug_mode)
|
|
std::cout << "Deinit GlFontManager" << std::endl;
|
|
|
|
for(const auto& font: fonts) glDeleteTextures(1, &font.atlas_texture);
|
|
glDeleteBuffers(1, &gl_vbo);
|
|
glDeleteBuffers(1, &gl_vao);
|
|
glDeleteProgram(gl_program);
|
|
FT_Done_FreeType(ft);
|
|
initialized = false;
|
|
}
|
|
|
|
const uui::GlFontManager::Font& uui::GlFontManager::init_font(const std::string& font_path, std::size_t font_size)
|
|
{
|
|
fonts.emplace_back();
|
|
auto& font = fonts.back();
|
|
|
|
FT_Face face;
|
|
if(FT_New_Face(ft, font_path.c_str(), 0, &face))
|
|
throw std::runtime_error("ERROR::FREETYPE: Failed to load font");
|
|
FT_Set_Pixel_Sizes(face, 0, font_size);
|
|
|
|
FT_GlyphSlot glyph = face->glyph;
|
|
|
|
unsigned int row_width = 0;
|
|
unsigned int row_height = 0;
|
|
|
|
memset(font.char_infos, 0, sizeof(font.char_infos));
|
|
|
|
// Find minimum size for a texture holding all visible ASCII characters
|
|
for(int i = 32; i < 256; i++)
|
|
{
|
|
if(FT_Load_Char(face, i, FT_LOAD_RENDER))
|
|
{
|
|
std::cout << "GlFontManager warning : loading character " << i << " failed!" << std::endl;
|
|
continue;
|
|
}
|
|
if(row_width + glyph->bitmap.width + 1 >= ATLAS_MAX_WIDTH)
|
|
{
|
|
font.atlas_width = std::max(font.atlas_width, row_width);
|
|
font.atlas_height += row_height;
|
|
row_width = 0;
|
|
row_height = 0;
|
|
}
|
|
row_width += glyph->bitmap.width + 1;
|
|
row_height = std::max(row_height, glyph->bitmap.rows);
|
|
}
|
|
|
|
font.atlas_width = std::max(font.atlas_width, row_width);
|
|
font.atlas_height += row_height;
|
|
|
|
glLinkProgram(gl_program);
|
|
|
|
// Create a texture that will be used to hold all ASCII glyphs
|
|
glActiveTexture(Config::FONT_TEXTURE_UNIT);
|
|
glGenTextures(1, &font.atlas_texture);
|
|
glBindTexture(GL_TEXTURE_2D, font.atlas_texture);
|
|
|
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, font.atlas_width, font.atlas_height, 0, GL_RED, GL_UNSIGNED_BYTE, 0);
|
|
|
|
// We require 1 byte alignment when uploading texture data
|
|
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
|
|
|
// Clamping to edges is important to prevent artifacts when scaling
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
|
|
|
// Linear filtering usually looks best for text
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
|
|
|
// Paste all glyph bitmaps into the texture, remembering the offset
|
|
int offset_x = 0;
|
|
int offset_y = 0;
|
|
|
|
row_height = 0;
|
|
|
|
for(int i = 32; i < 256; i++)
|
|
{
|
|
if(FT_Load_Char(face, i, FT_LOAD_RENDER))
|
|
{
|
|
std::cout << "GlFontManager warning : loading character " << i << " failed!" << std::endl;
|
|
continue;
|
|
}
|
|
|
|
if(offset_x + glyph->bitmap.width + 1 >= ATLAS_MAX_WIDTH)
|
|
{
|
|
offset_y += row_height;
|
|
row_height = 0;
|
|
offset_x = 0;
|
|
}
|
|
|
|
glTexSubImage2D(
|
|
GL_TEXTURE_2D, 0, offset_x, offset_y, glyph->bitmap.width, glyph->bitmap.rows, GL_RED, GL_UNSIGNED_BYTE,
|
|
glyph->bitmap.buffer);
|
|
font.char_infos[i].advance_x = glyph->advance.x >> 6;
|
|
font.char_infos[i].advance_y = glyph->advance.y >> 6;
|
|
|
|
font.char_infos[i].bitmap_width = glyph->bitmap.width;
|
|
font.char_infos[i].bitmap_height = glyph->bitmap.rows;
|
|
|
|
font.char_infos[i].bitmap_left = glyph->bitmap_left;
|
|
font.char_infos[i].bitmap_top = glyph->bitmap_top;
|
|
|
|
font.char_infos[i].texture_x = offset_x / (float)font.atlas_width;
|
|
font.char_infos[i].texture_y = offset_y / (float)font.atlas_height;
|
|
|
|
row_height = std::max(row_height, glyph->bitmap.rows);
|
|
offset_x += glyph->bitmap.width + 1;
|
|
}
|
|
FT_Done_Face(face);
|
|
|
|
return font;
|
|
}
|
|
|
|
std::tuple<int, int> uui::GlFontManager::get_text_size(
|
|
const std::string& text, uui::GlFontManager::Font& font, float scale_x, float scale_y)
|
|
{
|
|
float width = 0.0f;
|
|
float height = 0.0f;
|
|
for(auto current_char: text)
|
|
{
|
|
const auto& char_info = font.char_infos[static_cast<std::size_t>(current_char)];
|
|
width += char_info.advance_x * scale_x;
|
|
// height = std::max(height, char_info.bitmap_top + char_info.bitmap_height * scale_y);
|
|
height = std::max(height, roundf(char_info.bitmap_height * scale_y));
|
|
}
|
|
return {static_cast<int>(width), static_cast<int>(height)};
|
|
}
|
|
|
|
void uui::GlFontManager::render_text(
|
|
const std::string& text, uui::GlFontManager::Font& font, float x, float y, float scale_x, float scale_y,
|
|
const std::tuple<float, float, float, float>& text_color)
|
|
{
|
|
GLuint gl_prev_program;
|
|
glGetIntegerv(GL_CURRENT_PROGRAM, (GLint*)(&gl_prev_program));
|
|
glUseProgram(gl_program);
|
|
|
|
glActiveTexture(Config::FONT_TEXTURE_UNIT);
|
|
auto [color_r, color_g, color_b, color_a] = text_color;
|
|
glUniform4f(glGetUniformLocation(gl_program, "textColor"), color_r, color_g, color_b, color_a);
|
|
glBindVertexArray(gl_vao);
|
|
|
|
// Use the texture containing the atlas
|
|
glBindTexture(GL_TEXTURE_2D, font.atlas_texture);
|
|
|
|
std::vector<float> vertices;
|
|
vertices.resize(24 * text.size());
|
|
// vertices.resize(text.size());
|
|
int char_count = 0;
|
|
|
|
// Loop through all characters
|
|
float out_base_x = x;
|
|
float out_base_y = y;
|
|
for(auto current_char: text)
|
|
{
|
|
const auto& char_info = font.char_infos[static_cast<std::size_t>(current_char)];
|
|
// Calculate the vertex and texture coordinates
|
|
float char_x = out_base_x + char_info.bitmap_left * scale_x;
|
|
float char_y = out_base_y - char_info.bitmap_top * scale_y;
|
|
float w = char_info.bitmap_width * scale_x;
|
|
float h = char_info.bitmap_height * scale_y;
|
|
|
|
// Advance the cursor to the start of the next character
|
|
out_base_x += char_info.advance_x * scale_x;
|
|
out_base_y += char_info.advance_y * scale_y;
|
|
|
|
// Skip glyphs that have no pixels
|
|
if(!w || !h)
|
|
continue;
|
|
|
|
// clang-format off
|
|
float current_vertex[24] =
|
|
{
|
|
char_x, char_y, char_info.texture_x, char_info.texture_y, // top_left
|
|
char_x + w, char_y, char_info.texture_x + char_info.bitmap_width / font.atlas_width, char_info.texture_y, // top-right
|
|
char_x, char_y + h, char_info.texture_x, char_info.texture_y + char_info.bitmap_height / font.atlas_height, // bottom-right
|
|
char_x + w, char_y, char_info.texture_x + char_info.bitmap_width / font.atlas_width, char_info.texture_y, // top-right
|
|
char_x, char_y + h, char_info.texture_x, char_info.texture_y + char_info.bitmap_height / font.atlas_height, // bottom-left
|
|
char_x + w, char_y + h, char_info.texture_x + char_info.bitmap_width / font.atlas_width,
|
|
char_info.texture_y + char_info.bitmap_height / font.atlas_height // bottom-right
|
|
};
|
|
std::memcpy(vertices.data() + (char_count * 24), current_vertex, 24 * sizeof(float));
|
|
// clang-format on
|
|
char_count += 1;
|
|
}
|
|
|
|
// Draw all the character on the screen in one go
|
|
glBindBuffer(GL_ARRAY_BUFFER, gl_vbo);
|
|
glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(float), vertices.data(), GL_DYNAMIC_DRAW);
|
|
glDrawArrays(GL_TRIANGLES, 0, vertices.size());
|
|
|
|
// glDisableVertexAttribArray(0);
|
|
glBindTexture(GL_TEXTURE_2D, 0);
|
|
glBindVertexArray(0);
|
|
glUseProgram(gl_prev_program);
|
|
}
|