#include "pch.h" #include "log.h" #include "font.h" #include "shader.h" #include "asset.h" #include "util.h" #include "app.h" #include "renderer_gl/opengl_capabilities.h" #include #include #include namespace { [[nodiscard]] GLint font_atlas_internal_format() noexcept { return static_cast(pp::renderer::gl::texture_format_for_channel_count(1U).internal_format); } [[nodiscard]] GLint font_atlas_pixel_format() noexcept { return static_cast(pp::renderer::gl::texture_format_for_channel_count(1U).pixel_format); } [[nodiscard]] GLenum texture_unit(std::uint32_t unit_index) noexcept { return static_cast(pp::renderer::gl::active_texture_unit(unit_index)); } void gen_buffers_adapter(std::uint32_t count, std::uint32_t* ids) noexcept { glGenBuffers(static_cast(count), ids); } void bind_buffer_adapter(std::uint32_t target, std::uint32_t buffer) noexcept { glBindBuffer(static_cast(target), static_cast(buffer)); } void buffer_data_adapter( std::uint32_t target, std::intptr_t byte_count, const void* data, std::uint32_t usage) noexcept { glBufferData(static_cast(target), static_cast(byte_count), data, static_cast(usage)); } void gen_vertex_arrays_adapter(std::uint32_t count, std::uint32_t* ids) noexcept { glGenVertexArrays(static_cast(count), ids); } void bind_vertex_array_adapter(std::uint32_t vertex_array) noexcept { glBindVertexArray(static_cast(vertex_array)); } void enable_vertex_attrib_array_adapter(std::uint32_t index) noexcept { glEnableVertexAttribArray(static_cast(index)); } void vertex_attrib_pointer_adapter( std::uint32_t index, std::int32_t component_count, std::uint32_t component_type, std::uint8_t normalized, std::int32_t stride, const void* offset) noexcept { glVertexAttribPointer( static_cast(index), static_cast(component_count), static_cast(component_type), static_cast(normalized), static_cast(stride), offset); } void draw_elements_adapter( std::uint32_t mode, std::int32_t count, std::uint32_t index_type, const void* index_offset) noexcept { glDrawElements( static_cast(mode), static_cast(count), static_cast(index_type), index_offset); } void draw_arrays_adapter(std::uint32_t mode, std::int32_t first, std::int32_t count) noexcept { glDrawArrays(static_cast(mode), static_cast(first), static_cast(count)); } [[nodiscard]] std::span text_mesh_vertex_attributes() noexcept { static const std::array attributes { pp::renderer::gl::OpenGlVertexAttribute { .index = 0U, .component_count = 2, .component_type = pp::renderer::gl::vertex_attribute_float_component_type(), .normalized = static_cast(pp::renderer::gl::vertex_attribute_not_normalized()), .stride = static_cast(sizeof(glm::vec4)), .offset = 0U, }, pp::renderer::gl::OpenGlVertexAttribute { .index = 1U, .component_count = 2, .component_type = pp::renderer::gl::vertex_attribute_float_component_type(), .normalized = static_cast(pp::renderer::gl::vertex_attribute_not_normalized()), .stride = static_cast(sizeof(glm::vec4)), .offset = static_cast(sizeof(float) * 2), }, }; return attributes; } [[nodiscard]] pp::renderer::gl::OpenGlMeshCreateDispatch text_mesh_create_dispatch() noexcept { return pp::renderer::gl::OpenGlMeshCreateDispatch { .gen_buffers = gen_buffers_adapter, .bind_buffer = bind_buffer_adapter, .buffer_data = buffer_data_adapter, .gen_vertex_arrays = gen_vertex_arrays_adapter, .bind_vertex_array = bind_vertex_array_adapter, .enable_vertex_attrib_array = enable_vertex_attrib_array_adapter, .vertex_attrib_pointer = vertex_attrib_pointer_adapter, }; } [[nodiscard]] pp::renderer::gl::OpenGlBufferUploadDispatch text_buffer_upload_dispatch() noexcept { return pp::renderer::gl::OpenGlBufferUploadDispatch { .bind_buffer = bind_buffer_adapter, .buffer_data = buffer_data_adapter, }; } [[nodiscard]] pp::renderer::gl::OpenGlMeshDrawDispatch text_mesh_draw_dispatch() noexcept { return pp::renderer::gl::OpenGlMeshDrawDispatch { .bind_vertex_array = bind_vertex_array_adapter, .draw_elements = draw_elements_adapter, .draw_arrays = draw_arrays_adapter, }; } } std::map FontManager::m_fonts; Sampler FontManager::m_sampler; Font::Font(Font&& other) noexcept { w = other.w; h = other.h; bounds = other.bounds; size = other.size; scale = other.size; font_tex = std::move(other.font_tex); path = std::move(other.path); id = std::move(other.id); chars = std::move(other.chars); } void Font::operator=(Font&& other) noexcept { w = other.w; h = other.h; bounds = other.bounds; size = other.size; scale = other.size; font_tex = std::move(other.font_tex); path = std::move(other.path); id = std::move(other.id); chars = std::move(other.chars); } bool Font::load(const std::string& ttf, int font_size, float font_scale) { Asset file; LOG("Font::load %s", ttf.c_str()); if (file.open(ttf.c_str()) && file.read_all()) { w = h = 512 * (int)ceilf(font_scale); path = ttf; scale = font_scale; LOG("Font::load loaded"); auto bitmap = std::make_unique(w*h); chars.resize(num_chars); //int offset = stbtt_FindMatchingFont(file.m_data, "Arial Bold", STBTT_MACSTYLE_DONTCARE); //if (offset < 0) // offset = 0; stbtt_BakeFontBitmap(file.m_data, 0, (float)font_size*scale, bitmap.get(), w, h, start_char, num_chars, chars.data()); calc_bounds(); font_tex.create(w, h, font_atlas_internal_format(), font_atlas_pixel_format(), bitmap.get()); file.close(); size = font_size; return true; } return false; } void Font::change_scale(float scale) { load(path, size, scale); } void Font::calc_bounds() { glm::vec2 bbmin(FLT_MAX); glm::vec2 bbmax(-FLT_MAX); for (int i = 0; i < num_chars; i++) { stbtt_aligned_quad q; float x = 0, y = 0; stbtt_GetBakedQuad(chars.data(), w, h, i, &x, &y, &q, true); bbmin = glm::min(bbmin, { q.x0 / scale, q.y0 / scale }); bbmax = glm::max(bbmax, { q.x1 / scale, q.y1 / scale }); } bounds = { glm::vec4(bbmin, bbmax) }; } void FontManager::init() { m_sampler.create(); } bool FontManager::load(const std::string& id, const std::string& ttf, int sz, float scale) { return m_fonts[id].load(ttf, sz, scale); } const Font& FontManager::get(const std::string& name, int size, const std::string& weight, bool italic) { auto id = fmt::format("{}-{}{}-{}", name, weight, italic ? "-italic" : "", size); auto it = m_fonts.find(id); if (it == m_fonts.end()) { LOG("load font %s", id.c_str()); auto path = fmt::format("data/fonts/{}-{}{}.ttf", name, weight, italic ? "-italic" : ""); m_fonts[id].load(path, size, App::I->display_density * App::I->zoom); return m_fonts[id]; } return it->second; } void FontManager::change_scale(float scale) { for (auto& f : m_fonts) f.second.change_scale(scale); } std::vector TextMesh::tokenize(const std::string& s, const Font& f) const noexcept { std::vector parts; std::array delims = { ' ', '\n', ',', '.', ':', ';', '?', '!', '\\', '/' }; std::string tmp; bool wrap = false; for (char c : s) { bool is_delim = std::find(delims.begin(), delims.end(), c) != delims.end(); wrap |= is_delim; // set wrap to notify a delimiter has been reached // when a new non-delim char is detected, start a new token if (wrap && !is_delim && !tmp.empty()) { parts.push_back(tmp); tmp.clear(); wrap = false; } tmp.push_back(c); } // insert last partial token if (!tmp.empty()) parts.push_back(tmp); // measure each token length std::vector ret; for (auto p : parts) { float x = 0; float y = 0; for (char c : p) { if ((uint8_t)c >= 32 && (uint8_t)c < 256) // visible ascii character { stbtt_aligned_quad q; stbtt_GetBakedQuad((stbtt_bakedchar*)f.chars.data(), f.w, f.h, (uint8_t)c - f.start_char, &x, &y, &q, true); } } ret.emplace_back(p, x); } return ret; } bool TextMesh::create() { App::I->render_task([this] { const auto mesh = pp::renderer::gl::create_opengl_mesh_objects( pp::renderer::gl::OpenGlMeshUpload { .vertex_data = nullptr, .vertex_byte_count = 0, .index_data = nullptr, .index_byte_count = 0, .indexed = true, .vertex_array_count = 1U, .attributes = text_mesh_vertex_attributes(), }, text_mesh_create_dispatch()); if (mesh.ok()) { font_buffers[0] = static_cast(mesh.value().vertex_buffer); font_buffers[1] = static_cast(mesh.value().index_buffer); font_array = static_cast(mesh.value().vertex_arrays[0]); } }); return true; } void TextMesh::update(const std::string& text, const std::string& font, int size, const std::string& weight, bool italic) { this->font = font; this->size = size; this->weight = weight; this->italic = italic; auto& f = FontManager::get(font, size, weight, italic); float spacing = (f.bounds.w - f.bounds.y); float avg_width = f.bounds.z - f.bounds.x; cur_box = glm::vec4(0, f.bounds.y, 5, spacing); glm::vec2 bbmin(FLT_MAX); glm::vec2 bbmax(-FLT_MAX); if (text.empty()) { bbmin = { 0, -spacing * 0.5f }; bbmax = { 1, +spacing * 0.5f }; } if (f.chars.size()) { float x = 0; float y = 0; std::vector v; std::vector idx; std::vector parts = max_width > 0 ? tokenize(text, f) : std::vector{ Token(text, 0.f) }; for (size_t i = 0; i < parts.size(); i++) { const auto& p = parts[i]; if (i > 0 && max_width > 0 && (x + p.w) > max_width * f.scale) { x = 0; y += spacing * f.scale; } for (char c : p.s) { if (c == '\n') { x = 0; y += spacing * f.scale; cur_box = glm::vec4(x, y / f.scale + f.bounds.y, 5, spacing); continue; } stbtt_aligned_quad q; float wrap_test_x = x; stbtt_GetBakedQuad((stbtt_bakedchar*)f.chars.data(), f.w, f.h, (uint8_t)c - f.start_char, &wrap_test_x, &y, &q, true); if (max_width > 0 && q.x1 > max_width * f.scale) { x = 0; y += spacing * f.scale; } stbtt_GetBakedQuad((stbtt_bakedchar*)f.chars.data(), f.w, f.h, (uint8_t)c - f.start_char, &x, &y, &q, true); auto n = (int)v.size(); v.emplace_back(q.x0 / f.scale, q.y1 / f.scale, q.s0, q.t1); v.emplace_back(q.x0 / f.scale, q.y0 / f.scale, q.s0, q.t0); v.emplace_back(q.x1 / f.scale, q.y0 / f.scale, q.s1, q.t0); v.emplace_back(q.x1 / f.scale, q.y1 / f.scale, q.s1, q.t1); idx.push_back(n + 0); idx.push_back(n + 1); idx.push_back(n + 2); idx.push_back(n + 0); idx.push_back(n + 2); idx.push_back(n + 3); bbmin = glm::min(bbmin, glm::floor(xy(f.bounds))); bbmax = glm::max(bbmax, glm::ceil(glm::vec2(q.x1 / f.scale, y / f.scale + f.bounds.w))); if (max_width > 0 && q.x1 / f.scale + 5 > max_width * f.scale) cur_box = glm::vec4(0, (y + spacing * f.scale) / f.scale + f.bounds.y, 5, spacing); else cur_box = glm::vec4(q.x1 / f.scale + 2, y / f.scale + f.bounds.y, 5, spacing); } } for (auto& vi : v) vi -= glm::vec4(xy(f.bounds), 0, 0); cur_box -= glm::vec4(xy(f.bounds), 0, 0); bb = bbmax - bbmin; font_array_count = (int)idx.size(); App::I->render_task([&] { (void)pp::renderer::gl::upload_opengl_buffer_data( pp::renderer::gl::OpenGlBufferUpload { .target = pp::renderer::gl::element_array_buffer_target(), .buffer_id = font_buffers[1], .data = idx.data(), .byte_count = static_cast(idx.size() * sizeof(GLushort)), .usage = pp::renderer::gl::static_draw_buffer_usage(), }, text_buffer_upload_dispatch()); (void)pp::renderer::gl::upload_opengl_buffer_data( pp::renderer::gl::OpenGlBufferUpload { .target = pp::renderer::gl::array_buffer_target(), .buffer_id = font_buffers[0], .data = v.data(), .byte_count = static_cast(v.size() * sizeof(glm::vec4)), .usage = pp::renderer::gl::static_draw_buffer_usage(), }, text_buffer_upload_dispatch()); }); } } void TextMesh::draw() { auto& f = FontManager::get(font, size, weight, italic); if (f.font_tex.ready()) { glActiveTexture(texture_unit(0U)); f.font_tex.bind(); FontManager::m_sampler.bind(0); (void)pp::renderer::gl::draw_opengl_mesh( pp::renderer::gl::OpenGlMeshDraw { .vertex_array = font_array, .mode = pp::renderer::gl::primitive_mode_for_fill_count(3U), .count = font_array_count, .indexed = true, .index_type = pp::renderer::gl::index_type_for_index_size(sizeof(GLushort)), .index_offset = nullptr, }, text_mesh_draw_dispatch()); f.font_tex.unbind(); FontManager::m_sampler.unbind(); } }