/* * * Copyright (C) 2024 Brett Terpstra * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include #include "blt/gfx/window.h" #include #include #include // TODO: signed distance fonts //#include #include namespace blt::gfx { font_texture_atlas::font_texture_atlas(blt::i32 dimensions, blt::u32 padding): texture_gl2D(dimensions, dimensions, GL_R8), padding(padding) { bind(); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0); } font_texture_atlas::added_font_results_t font_texture_atlas::add_font(const font::font_file_t& file, float size) { return add_font(file, file.character_min_index, size); } font_texture_atlas::added_font_results_t font_texture_atlas::add_font(const font::font_file_t& file, blt::u64 start, float size) { blt::u32 max_height = 0; blt::u64 min_index_used = file.character_max_index; blt::u64 max_index_used = start; file.font.set_pixel_sizes(0, static_cast(size)); for (auto i = start; i < file.character_max_index; i++) { // returns an empty optional if there is no error code if (file.font.load_char(i)) continue; auto& face = *file.font.get(); auto width = face->glyph->bitmap.width; auto height = face->glyph->bitmap.rows; max_height = std::max(max_height, height); // if adding this width overflows, we need to move to the next row. if (used_width + width > static_cast(getWidth())) { used_height += max_height + padding; max_height = 0; used_width = 0; } // if we can't fit the height of this character we should move to the next texture atlas. if (used_height + height > static_cast(getHeight())) return {i, min_index_used, max_index_used}; min_index_used = std::min(min_index_used, i); max_index_used = std::max(max_index_used, i); auto begin_width = used_width; auto begin_height = used_height; auto end_width = used_width + width; auto end_height = used_height + height; used_width += width + padding; // BLT_TRACE("%d %d %d %d", begin_width, begin_height, width, height); // upload the texture upload(face->glyph->bitmap.buffer, GL_RED, 0, static_cast(begin_width), static_cast(begin_height), static_cast(width), static_cast(height), GL_UNSIGNED_BYTE); glyphs[file.font.get_name()].insert(std::pair{i, bounded_t{ font::glyph_t{ {width, height}, {face->glyph->bitmap_left, face->glyph->bitmap_top}, face->glyph->advance.x}, {begin_width, begin_height}, {end_width, end_height}} }); } bind(); generateMipmaps(); return {-1ul, min_index_used, max_index_used}; } font_generator_t::font_generator_t(blt::i32 dimensions, blt::u32 padding, float gen_size): dimensions(dimensions), padding(padding), gen_size(gen_size) {} void font_generator_t::add_font(const font::font_file_t& file) { glPixelStorei(GL_UNPACK_ALIGNMENT, 1); auto& vec = atlases; if (atlases.empty()) atlases.emplace_back(std::make_unique(dimensions, padding)); auto& font_vec = font_associations[file.font.get_name()]; blt::u64 start_index = file.character_min_index; while (start_index != -1ul) { // add font and associate results with a font file auto results = atlases.back()->add_font(file, start_index, gen_size); font_vec.emplace_back(atlases.back().get(), results.min_index_used, results.max_index_used); // didn't fit, need to push new atlas if (results.last_inserted_index != -1ul) vec.emplace_back(std::make_unique(dimensions, padding)); start_index = results.last_inserted_index; } glPixelStorei(GL_UNPACK_ALIGNMENT, 4); } font_renderer_t::font_renderer_t() = default; void font_renderer_t::create(float gen_size, blt::i32 dimensions) { font_shader = shader_t::make_unique(shader_2d_font_vert, shader_2d_font_frag); font_shader->bind(); font_shader->bindAttribute(0, "vertex"); font::create(); if (dimensions == 0) { int size; glGetIntegerv(GL_MAX_TEXTURE_SIZE, &size); dimensions = std::min(size, 2048); BLT_INFO("Max texture size %d, font renderer will use size %d", size, dimensions); } generator = std::make_unique(dimensions, 2, gen_size); } void font_renderer_t::create_default(float gen_size, blt::i32 dimensions) { create(gen_size, dimensions); // add_default_font(reinterpret_cast(font::default_font_compressed_data), font::default_font_compressed_size, true); add_default_font(reinterpret_cast(ImGui::Spectrum::SourceSansProRegular_compressed_data), ImGui::Spectrum::SourceSansProRegular_compressed_size, true); } void font_renderer_t::cleanup() { for (auto& n : added_texts) n = nullptr; for (auto& c : cached_deallocated_text_objects) c = nullptr; added_texts.clear(); cached_deallocated_text_objects.clear(); text_ptr_to_index.clear(); font_shader = nullptr; generator = nullptr; blt::gfx::font::cleanup(); } font_renderer_t::compiled_text_t* font_renderer_t::create_text(std::string_view str, float size, std::string_view font) { std::unique_ptr ptr = allocate_text(); ptr->setText(str, size, font); auto index = added_texts.size(); added_texts.push_back(std::move(ptr)); text_ptr_to_index[added_texts.back().get()] = index; return added_texts.back().get(); } void font_renderer_t::destroy_text(font_renderer_t::compiled_text_t* text) { auto index = text_ptr_to_index[text]; text_ptr_to_index.erase(text); cached_deallocated_text_objects.push_back(std::move(added_texts[index])); added_texts[index] = nullptr; added_texts.erase(added_texts.begin() + static_cast(index)); } void font_renderer_t::render() { #ifndef __EMSCRIPTEN__ glDisable(GL_MULTISAMPLE); #endif glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); font_shader->bind(); glActiveTexture(GL_TEXTURE0); for (auto [text, index] : text_ptr_to_index) { // stupid hack if (text_ptr_to_text_index.find(text) != text_ptr_to_text_index.end()) continue; text->bind(); blt::mat4x4 transform; transform.translate(text->position); transform.scale(text->scale); transform.rotateZ(blt::toRadians(text->rotation)); font_shader->setMatrix("transform", transform); font_shader->setFloat("z_index", text->z_index); font_shader->setVec4("color", text->color); text->draw(); } for (auto& [text, list] : render_list) { text->bind(); render_context_t before{text->current_size}; text->save_to(before); for (const auto& context : list) { text->restore_from(context); blt::mat4x4 transform; transform.translate(text->position); transform.scale(text->scale); transform.rotateZ(blt::toRadians(text->rotation)); font_shader->setMatrix("transform", transform); font_shader->setFloat("z_index", text->z_index); font_shader->setVec4("color", text->color); text->draw(); } text->restore_from(before); if (last_rendered_strings.find(text) != last_rendered_strings.end()) last_rendered_strings.erase(text); } render_list.clear(); for (auto& text : last_rendered_strings) { rendered_strings.erase(text_ptr_to_text_index[text]); text_ptr_to_text_index.erase(text); cached_deallocated_text_objects.emplace_back(std::move(added_texts[text_ptr_to_index[text]])); text_ptr_to_index.erase(text); } last_rendered_strings.clear(); for (auto& text : currently_rendered_strings) last_rendered_strings.insert(text); currently_rendered_strings.clear(); #ifndef __EMSCRIPTEN__ glEnable(GL_MULTISAMPLE); #endif } void font_renderer_t::add_default_font(std::string_view path, std::string_view name) { font::font_face_t default_font_face{path, name}; font::font_file_t default_font{default_font_face, 0, 256}; add_font(default_font); } void font_renderer_t::add_default_font(const blt::u8* data, blt::size_t size, bool compressed, std::string_view name) { font::font_face_t default_font_face{name, data, size, compressed}; font::font_file_t default_font{default_font_face, 0, 256}; add_font(default_font); } std::unique_ptr font_renderer_t::allocate_text() { std::unique_ptr ptr; if (!cached_deallocated_text_objects.empty()) { ptr = std::move(*cached_deallocated_text_objects.begin()); cached_deallocated_text_objects.erase(cached_deallocated_text_objects.begin()); } else { ptr = std::unique_ptr(new compiled_text_t{*generator}); } return ptr; } font_renderer_t::render_context_t& font_renderer_t::render_text(std::string_view str, float size, std::string_view font) { auto str_iter = rendered_strings.find(text_index_t{std::string(str), std::string(font)}); compiled_text_t* ptr; if (str_iter != rendered_strings.end()) { ptr = str_iter->second; currently_rendered_strings.insert(ptr); ptr->setSize(size); } else { auto index = added_texts.size(); added_texts.emplace_back(allocate_text()); ptr = added_texts.back().get(); ptr->setText(str, size); text_ptr_to_text_index[ptr] = text_index_t{std::string(str), std::string(font)}; rendered_strings[text_index_t{std::string(str), std::string(font)}] = ptr; text_ptr_to_index[ptr] = index; currently_rendered_strings.insert(ptr); } auto& vec = render_list[ptr]; vec.emplace_back(size); return vec.back(); } font_renderer_t::compiled_text_t& font_renderer_t::compiled_text_t::setText(std::string_view str, float size, std::string_view f) { if (str == contents) return *this; contents = str; font = f; setSize(size); recompile(); return *this; } font_renderer_t::compiled_text_t::compiled_text_t(font_generator_t& generator): generator(generator) { auto vbo = vertex_array_t::make_vbo({}); vbo->vbo.create(); vbo->vbo.bind(); vbo->vbo.allocate(sizeof(float) * 6 * 4, GL_DYNAMIC_DRAW); vao.bind(); vao.bindVBO(vbo, 0, 4, GL_FLOAT, 4 * sizeof(float), 0); } void font_renderer_t::compiled_text_t::draw() { font_texture_atlas* last_texture = nullptr; for (const auto& render : renders) { if (last_texture != render.texture) { last_texture = render.texture; render.texture->bind(); } glDrawArrays(GL_TRIANGLES, static_cast(render.render_start), static_cast(render.render_count)); } } font_renderer_t::compiled_text_t& font_renderer_t::compiled_text_t::recompile() { static std::vector vertices; vertices.clear(); renders.clear(); font_texture_atlas* last_texture = nullptr; blt::size_t draw_start = 0; blt::size_t draw_count = 0; const float sc = generator.get_generated_size() / current_size; float global_x = 0; float global_y = 0; // TODO: parker UTF8 for (const auto& c : contents) { if (bounds.y() > 0 && std::abs(global_y) >= bounds.y() * sc) continue; if (c == '\n') { global_x = 0; global_y -= generator.get_generated_size(); continue; } auto& texture = generator.get_texture(std::string(font), c); auto& ch = texture.get_glyph(c, font); if (std::isblank(c)) { if (bounds.x() > 0 && global_x > bounds.x() * sc) { global_x = 0; global_y -= generator.get_generated_size(); continue; } } if (last_texture == nullptr) last_texture = &texture; else if (last_texture != &texture) { renders.emplace_back(last_texture, draw_start, draw_count); draw_start += draw_count; draw_count = 0; } auto dims = static_cast(generator.get_dimensions()); float x_pos = global_x + static_cast(ch.glyph.bearing.x()); float y_pos = global_y - static_cast(ch.glyph.size.y() - ch.glyph.bearing.y()); auto w = static_cast(ch.glyph.size.x()); auto h = static_cast(ch.glyph.size.y()); auto texture_min_x = static_cast(ch.min.x()) / dims; auto texture_min_y = static_cast(ch.min.y()) / dims; auto texture_max_x = static_cast(ch.max.x()) / dims; auto texture_max_y = static_cast(ch.max.y()) / dims; // BLT_TRACE("%c: %f | xy[%f %f] wh[%f %f] | min[%f %f] max[%f %f] |", c, x, x_pos, y_pos, w, h, texture_min_x, texture_min_y, texture_max_x, texture_max_y); std::array local_vert = { x_pos, y_pos + h, texture_min_x, texture_min_y, x_pos, y_pos, texture_min_x, texture_max_y, x_pos + w, y_pos, texture_max_x, texture_max_y, x_pos, y_pos + h, texture_min_x, texture_min_y, x_pos + w, y_pos, texture_max_x, texture_max_y, x_pos + w, y_pos + h, texture_max_x, texture_min_y }; vertices.insert(vertices.end(), local_vert.begin(), local_vert.end()); draw_count += 6; global_x += static_cast(ch.glyph.advance >> 6); } // BLT_TRACE("size: %ld %ld %ld", draw_count, vertices.size(), vertices.size() / 4); renders.emplace_back(last_texture, draw_start, draw_count); vao.getBuffer(0).update(static_cast(vertices.size() * sizeof(float)), vertices.data()); return *this; } bool font_renderer_t::text_index_t::operator==(const font_renderer_t::text_index_t& rhs) const { return text == rhs.text && font == rhs.font; } bool font_renderer_t::text_index_t::operator!=(const font_renderer_t::text_index_t& rhs) const { return !(rhs == *this); } }