From 32522d06cf7da80fc10a5376a0e7e8a3a3d301c1 Mon Sep 17 00:00:00 2001 From: WillisMedwell Date: Mon, 11 Mar 2024 14:27:02 +1100 Subject: [PATCH] Improved: FontAtlas generation --- code/Demos/src/Main.cpp | 21 +- code/Engine/include/Media/Font.hpp | 129 ------- code/Engine/include/Media/FontAtlas.hpp | 46 +++ code/Engine/include/Media/Image.hpp | 6 + code/Engine/include/Media/Media.hpp | 2 +- code/Engine/src/Media/Font.cpp | 486 ------------------------ code/Engine/src/Media/FontAtlas.cpp | 218 +++++++++++ code/Engine/src/Media/Image.cpp | 59 +-- 8 files changed, 312 insertions(+), 655 deletions(-) delete mode 100644 code/Engine/include/Media/Font.hpp create mode 100644 code/Engine/include/Media/FontAtlas.hpp delete mode 100644 code/Engine/src/Media/Font.cpp create mode 100644 code/Engine/src/Media/FontAtlas.cpp diff --git a/code/Demos/src/Main.cpp b/code/Demos/src/Main.cpp index d63d2d0..c512210 100644 --- a/code/Demos/src/Main.cpp +++ b/code/Demos/src/Main.cpp @@ -418,13 +418,8 @@ struct IsoData { struct IsoLogic { void init(AppRenderer& renderer, Core::AudioManager& audio, IsoData& data) { - std::cout << std::thread::hardware_concurrency() << '\n'; - - /*Media::Sound sound {};*/ - Media::Sound sound = Media::Sound::create("assets/background_sound.wav").on_error_panic().value_move(); - auto res = audio.load_sound_into_buffer(sound).on_error(print_then_quit); data.sound_buffer = res.value(); @@ -433,25 +428,17 @@ struct IsoLogic { data.spinning.angle = 0; data.spinning.rotations_per_second = 1; - auto model_data = Utily::FileReader::load_entire_file("assets/teapot.obj") - .on_error_panic() - .value_move(); - auto model = Model::decode_as_static_model(model_data, ".obj") - .on_error_panic() - .value_move(); - auto image = Media::Image::create("assets/texture.png") - .on_error_panic() - .value_move(); + auto model_data = Utily::FileReader::load_entire_file("assets/teapot.obj").on_error_panic().value_move(); + auto model = Model::decode_as_static_model(model_data, ".obj").on_error_panic().value_move(); + auto image = Media::Image::create("assets/texture.png").on_error_panic().value_move(); data.font_batch_renderer.emplace(Renderer::FontBatchRenderer::create(data.resource_manager, "assets/RobotoMono.ttf") .on_error_panic() .value_move()); - auto font_atlas = Media::FontAtlas::create("assets/RobotoMono.ttf", 500).on_error_panic().value_move(); - font_atlas.atlas_image().save_to_disk("RobotoMonoAtlas.png"); + Media::FontAtlas::create("assets/RobotoMono.ttf", 500).on_error_panic().value().atlas_image().save_to_disk("RobotoMonoAtlas.png"); data.instance_renderer.init(data.resource_manager, model, image); - data.source_handle = audio.play_sound(data.sound_buffer, { 5, 0, 0 }).on_error(print_then_quit).value(); data.start_time = std::chrono::high_resolution_clock::now(); } diff --git a/code/Engine/include/Media/Font.hpp b/code/Engine/include/Media/Font.hpp deleted file mode 100644 index e95e0a5..0000000 --- a/code/Engine/include/Media/Font.hpp +++ /dev/null @@ -1,129 +0,0 @@ -#pragma once - -#include - -#include "Media/Image.hpp" -#include "Model/Static.hpp" -#include -#include -#include -#include -#include - -#if 0 -namespace Media { - - namespace FontAtlasConstants { - consteval static auto gen_drawable_chars() { - constexpr char first_printable = char(32); - constexpr char last_printable = char(127); - constexpr size_t n = last_printable - first_printable; - std::array chars {}; - std::ranges::copy(std::views::iota(first_printable, last_printable), chars.begin()); - return chars; - } - constexpr static auto DRAWABLE_CHARS = gen_drawable_chars(); - - consteval static auto gen_is_char_drawable_table() -> std::array { - auto table = std::array { false }; - std::ptrdiff_t i = 0; - for (auto iter = table.begin(); iter != table.end(); ++iter, ++i) { - *iter = std::ranges::find(DRAWABLE_CHARS, static_cast(i)) != DRAWABLE_CHARS.end(); - } - return table; - } - constexpr static auto IS_CHAR_DRAWABLE = gen_is_char_drawable_table(); - } - - class Font; - - class FontAtlas - { - public: - struct UvCoord { - float min_x; - float max_x; - float min_y; - float max_y; - }; - - Media::Image image = {}; - int32_t columns { 0 }, rows { 0 }; - int32_t glyph_width { 0 }, glyph_height { 0 }; - - constexpr FontAtlas() = default; - - // Allow move operations - FontAtlas(FontAtlas&& other) = default; - auto operator=(FontAtlas&& other) noexcept -> FontAtlas&; - - // Refuse copy operations. - FontAtlas(const FontAtlas&) = delete; - FontAtlas& operator=(const FontAtlas&) = delete; - - auto init(Media::Font& font, uint32_t char_height_px) -> Utily::Result; - - auto uv_coord_of_char(char a) const -> UvCoord; - }; - - class Font - { - public: - Font() = default; - Font(const Font&) = delete; - Font(Font&& other); - - [[nodiscard]] auto init(std::vector& encoded_ttf) noexcept -> Utily::Result; - [[nodiscard]] auto gen_image_atlas(uint32_t char_height_px) -> Utily::Result; - - void stop() noexcept; - - ~Font(); - - private: - void* _font_face = nullptr; - }; - - namespace FontMeshGenerator { - auto generate_static_mesh(std::string_view str, const float char_height, const glm::vec2 bottom_left_pos, const FontAtlas& atlas) -> std::tuple, std::array>; - } -} - -#else - -namespace Media { - - class FontAtlas - { - public: - /// @brief Load .ttf font from disk. Generate a font-atlas image. Can fail. - [[nodiscard]] static auto create(std::filesystem::path path, uint32_t char_height_px) noexcept -> Utily::Result; - - FontAtlas(FontAtlas&& other) - : _m(std::move(other._m)) { } - - struct UvCoord { - float min_x; - float max_x; - float min_y; - float max_y; - }; - [[nodiscard]] auto uv_for(char a) const noexcept -> FontAtlas::UvCoord; - - [[nodiscard]] auto atlas_image() const noexcept -> const Media::Image& { return _m.atlas_image; } - [[nodiscard]] auto atlas_layout() const noexcept { return _m.atlas_layout; } - [[nodiscard]] auto glyph_dimensions() const noexcept { return _m.glyph_dimensions; } - - private: - struct M { - Media::Image atlas_image; - glm::vec2 atlas_layout; - glm::vec2 glyph_dimensions; - } _m; - - explicit FontAtlas(M&& m) - : _m(std::move(m)) { } - }; -} - -#endif \ No newline at end of file diff --git a/code/Engine/include/Media/FontAtlas.hpp b/code/Engine/include/Media/FontAtlas.hpp new file mode 100644 index 0000000..eaf17b9 --- /dev/null +++ b/code/Engine/include/Media/FontAtlas.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include + +#include "Media/Image.hpp" +#include "Model/Static.hpp" +#include +#include +#include +#include +#include + +namespace Media { + + class FontAtlas + { + public: + /// @brief Load .ttf font from disk. Generate a font-atlas image. Can fail. + [[nodiscard]] static auto create(std::filesystem::path path, uint32_t char_height_px) noexcept -> Utily::Result; + + FontAtlas(FontAtlas&& other) + : _m(std::move(other._m)) { } + + struct UvCoord { + float min_x; + float max_x; + float min_y; + float max_y; + }; + [[nodiscard]] auto uv_for(char a) const noexcept -> FontAtlas::UvCoord; + + [[nodiscard]] auto atlas_image() const noexcept -> const Media::Image& { return _m.atlas_image; } + [[nodiscard]] auto atlas_layout() const noexcept { return _m.atlas_layout; } + [[nodiscard]] auto glyph_dimensions() const noexcept { return _m.glyph_dimensions; } + + private: + struct M { + Media::Image atlas_image; + glm::vec2 atlas_layout; + glm::vec2 glyph_dimensions; + } _m; + + explicit FontAtlas(M&& m) + : _m(std::move(m)) { } + }; +} diff --git a/code/Engine/include/Media/Image.hpp b/code/Engine/include/Media/Image.hpp index 165f4bb..3451335 100644 --- a/code/Engine/include/Media/Image.hpp +++ b/code/Engine/include/Media/Image.hpp @@ -77,10 +77,15 @@ namespace Media { /// @brief Load png image from disk and decode it. Can fail. [[nodiscard]] static auto create(std::filesystem::path path) -> Utily::Result; + /// @brief Take decoded-raw image data and copy it. Can fail. [[nodiscard]] static auto create(std::span raw_bytes, glm::uvec2 dimensions, InternalFormat format) -> Utily::Result; + /// @brief Take ownership of decoded-raw image data. Can fail. + [[nodiscard]] static auto create(std::unique_ptr&& data, size_t data_size_bytes, glm::uvec2 dimensions, InternalFormat format) + -> Utily::Result; + [[nodiscard]] inline auto raw_bytes() const noexcept { return std::span { _m.data.get(), _m.data_size_bytes }; } [[nodsicard]] inline auto dimensions() const noexcept { return _m.dimensions; } [[nodiscard]] auto format() const { return _m.format; } @@ -90,6 +95,7 @@ namespace Media { Image(const Image&) = delete; auto save_to_disk(std::filesystem::path path) const noexcept -> Utily::Result; + private: struct M { std::unique_ptr data = {}; diff --git a/code/Engine/include/Media/Media.hpp b/code/Engine/include/Media/Media.hpp index 99f13c5..39ec241 100644 --- a/code/Engine/include/Media/Media.hpp +++ b/code/Engine/include/Media/Media.hpp @@ -1,5 +1,5 @@ #pragma once #include "Media/Image.hpp" -#include "Media/Font.hpp" +#include "Media/FontAtlas.hpp" #include "Media/Sound.hpp" diff --git a/code/Engine/src/Media/Font.cpp b/code/Engine/src/Media/Font.cpp deleted file mode 100644 index 14920a2..0000000 --- a/code/Engine/src/Media/Font.cpp +++ /dev/null @@ -1,486 +0,0 @@ -#include "Media/Font.hpp" - -#include -#include -#include -#include -#include - -#include "Profiler/Profiler.hpp" - -// #include - -#include -#include FT_FREETYPE_H - -struct FreeType { - FT_Library library = nullptr; - - FreeType() { - Profiler::Timer timer("Font::FreeType() *library init*", { "freetype", "font", "init" }); - if (auto error = FT_Init_FreeType(&library); error) { - const auto ft_err_msg = std::string_view { FT_Error_String(error) }; - std::cerr - << "Failed to initialise FreeType library: \n" - << ft_err_msg - << std::endl; - } - } - ~FreeType() { - if (library != nullptr) { - FT_Done_FreeType(library); - } - library = nullptr; - } -}; - -static FreeType free_type {}; - -#if 0 -namespace Media { - auto FontAtlas::operator=(FontAtlas&& other) noexcept -> FontAtlas& { - this->image = std::move(other.image); - this->columns = other.columns; - this->rows = other.rows; - this->glyph_width = other.glyph_width; - this->glyph_height = other.glyph_height; - return *this; - } - - auto FontAtlas::init(Media::Font& font, uint32_t char_height_px) -> Utily::Result { - auto res = font.gen_image_atlas(char_height_px); - if (res.has_error()) { - return res.error(); - } - *this = std::move(res.value()); - - return {}; - } - - auto FontAtlas::uv_coord_of_char(char a) const -> FontAtlas::UvCoord { - constexpr auto drawble_chars = FontAtlasConstants::DRAWABLE_CHARS; - - assert(drawble_chars.front() <= a && a <= drawble_chars.back() && "Must be a printable character"); - - const auto i = static_cast(a - drawble_chars.front()); - - const float r = static_cast(i / columns); - const float c = static_cast(i % columns); - - return UvCoord { - .min_x = c / static_cast(columns), - .max_x = (c + 1) / static_cast(columns), - .min_y = 1 - (r + 1) / static_cast(rows), - .max_y = 1 - r / static_cast(rows), - }; - } - - auto Font::init(std::vector& encoded_ttf) noexcept -> Utily::Result { - FT_Face ff = nullptr; - if (auto error = FT_New_Memory_Face(free_type.library, encoded_ttf.data(), encoded_ttf.size(), 0, &ff); error) { - return Utily::Error { FT_Error_String(error) }; - } - _font_face = ff; - return {}; - } - - struct FreeTypeGlyph { - std::vector buffer = {}; - std::ptrdiff_t width { 0 }, height { 0 }, spanline { 0 }, left_padding { 0 }; - - constexpr FreeTypeGlyph() = default; - - constexpr FreeTypeGlyph(const FT_GlyphSlot& slot) - : width(slot->bitmap.width) - , height(slot->bitmap.rows) - , spanline(slot->bitmap_top) - , left_padding(slot->bitmap_left) { - auto buffer_data = std::span(slot->bitmap.buffer, slot->bitmap.width * slot->bitmap.rows); - buffer = std::vector { buffer_data.begin(), buffer_data.end() }; - } - }; - - struct GlyphInfo { - std::ptrdiff_t width = 0, height = 0, spanline = 0, left_padding = 0; - - GlyphInfo() = default; - - GlyphInfo(const FT_GlyphSlot& slot) - : width(slot->bitmap.width) - , height(slot->bitmap.rows + 1) - , spanline(slot->bitmap_top) - , left_padding(slot->bitmap_left) { } - - static auto take_max_values(const GlyphInfo& lhs, const GlyphInfo& rhs) -> GlyphInfo { - GlyphInfo gi; - gi.width = std::max(lhs.width, rhs.width); - gi.height = std::max(lhs.height, rhs.height); - gi.spanline = std::max(lhs.spanline, rhs.spanline); - gi.left_padding = std::max(lhs.left_padding, rhs.left_padding); - return gi; - } - }; - - auto Font::gen_image_atlas(uint32_t char_height_px) -> Utily::Result { - Profiler::Timer timer("Font::gen_image_atlas()", { "font", "loading" }); - - constexpr auto& drawable_chars = FontAtlasConstants::DRAWABLE_CHARS; - - // Validate and Scale FreeType Face. - if (_font_face == nullptr) { - return Utily::Error { "Font not initialised." }; - } - FT_Face ff = reinterpret_cast(_font_face); - if (auto error = FT_Set_Pixel_Sizes(ff, char_height_px, char_height_px); error) { - return Utily::Error { FT_Error_String(error) }; - } - - auto generate_and_cache_ft_glyph = [&](char c, FreeTypeGlyph& ftg) -> GlyphInfo { - auto glyph_index = FT_Get_Char_Index(ff, static_cast(c)); - - if (auto error = FT_Load_Glyph(ff, glyph_index, FT_LOAD_DEFAULT); error) [[unlikely]] { - throw std::runtime_error(FT_Error_String(error)); - } - if (auto error = FT_Render_Glyph(ff->glyph, FT_Render_Mode::FT_RENDER_MODE_NORMAL); error) [[unlikely]] { - throw std::runtime_error(FT_Error_String(error)); - } - ftg = FreeTypeGlyph(ff->glyph); - return GlyphInfo(ff->glyph); - }; - - auto cached_ft_bitmaps = std::array {}; - GlyphInfo atlas_info = {}; - { - Profiler::Timer timer("FT_Render_glyphs()"); - // 1. Transform -> Generates and caches the freetype bitmaps & returns its glyph layout - // 2. Reduce -> Calculate the maximum bounding glyph layout, so all characters have enough space in atlas. - atlas_info = std::transform_reduce( - drawable_chars.begin(), - drawable_chars.end(), - cached_ft_bitmaps.begin(), - GlyphInfo {}, - &GlyphInfo::take_max_values, - generate_and_cache_ft_glyph); - } - Profiler::Timer blit_timer("blit_glyphs_into_altas()"); - - const auto [atlas_glyphs_per_row, atlas_num_rows] = [](float glyph_width, float glyph_height, float num_glyphs_in_atlas) { - -#if 1 // generate most square shape by brute force. - auto [min_diff, min_x, min_y] = std::tuple { std::numeric_limits::max(), 0, num_glyphs_in_atlas }; - for (float i = 1; i < num_glyphs_in_atlas; ++i) { - auto x = i; - auto y = num_glyphs_in_atlas / i; - x = std::floor(x); - y = std::ceil(y); - auto diff = std::abs((x * glyph_width) - (y * glyph_height)); - if (diff < min_diff && num_glyphs_in_atlas <= x * y) { - min_diff = diff; - min_x = x; - min_y = y; - } - } -#else - float expected_dimensions = std::sqrtf(num_glyphs_in_atlas); - float ratio = glyph_height / glyph_width; - float min_x = std::floor(expected_dimensions * ratio); - float min_y = std::ceil(expected_dimensions / ratio); - assert(num_glyphs_in_atlas < min_x * min_y); -#endif - return std::tuple { min_x, min_y }; - }(atlas_info.width, atlas_info.height, drawable_chars.size()); - - const int atlas_img_height = atlas_info.height * atlas_num_rows; - const int atlas_img_width = atlas_info.width * atlas_glyphs_per_row; - - std::vector atlas_buffer; - - atlas_buffer.resize(atlas_img_height * atlas_img_width, (uint8_t)0); - - for (std::ptrdiff_t i = 0; i < static_cast(drawable_chars.size()); ++i) { - const auto& bitmap_ft = cached_ft_bitmaps[i]; - const auto atlas_coords = glm::ivec2 { i % atlas_glyphs_per_row, i / atlas_glyphs_per_row }; - const auto adjusted_offset = glm::ivec2 { atlas_coords.x * atlas_info.width, atlas_coords.y * atlas_info.height }; - - auto get_atlas_buffer_dest = [&](int x, int y) -> uint8_t& { - const auto px_coords = glm::ivec2(adjusted_offset.x + x, adjusted_offset.y + y); - return atlas_buffer[px_coords.y * atlas_img_width + px_coords.x + bitmap_ft.left_padding]; - }; - for (std::ptrdiff_t y = 0; y < bitmap_ft.height; ++y) { - for (std::ptrdiff_t x = 0; x < bitmap_ft.width; ++x) { - // align to relative bitmap - const std::ptrdiff_t relative_y = (y + atlas_info.spanline - bitmap_ft.spanline); - const std::ptrdiff_t ft_offset = y * bitmap_ft.width + x; - get_atlas_buffer_dest(x, relative_y) = bitmap_ft.buffer[ft_offset]; - } - } - } - - FontAtlas font_atlas; - font_atlas.columns = atlas_glyphs_per_row; - font_atlas.rows = atlas_num_rows; - font_atlas.glyph_width = static_cast(atlas_info.width); - font_atlas.glyph_height = static_cast(atlas_info.height); - font_atlas.image.init_raw(std::move(atlas_buffer), atlas_img_width, atlas_img_height, ColourFormat::greyscale); - return font_atlas; - } - - void Font::stop() noexcept { - if (_font_face != nullptr) { - FT_Done_Face(reinterpret_cast(_font_face)); - } - _font_face = nullptr; - } - - Font::~Font() { - stop(); - } - - auto FontMeshGenerator::generate_static_mesh(std::string_view str, const float char_height, const glm::vec2 bottom_left_pos, const FontAtlas& atlas) -> std::tuple, std::array> { - Profiler::Timer timer("FontMeshGenerate::generate_static_mesh()"); - - auto is_not_printable = [](char c) { - return !(FontAtlasConstants::DRAWABLE_CHARS.front() <= c && c <= FontAtlasConstants::DRAWABLE_CHARS.front()); - }; - assert(std::any_of(str.begin(), str.end(), is_not_printable)); - - constexpr int max_chars = 100; - if (str.size() > max_chars) { - throw std::length_error("Exceeded maximum char capacity"); - } - - static std::array vertices = {}; - static std::array indices = {}; - - const size_t vert_size = str.size() * 4; - const size_t indi_size = str.size() * 6; - - Model::Vertex2D* v_ptr[] = { - vertices.data() + 0, - vertices.data() + 1, - vertices.data() + 2, - vertices.data() + 3 - }; - Model::Index* i_ptr[] = { - indices.data() + 0, - indices.data() + 1, - indices.data() + 2, - indices.data() + 3, - indices.data() + 4, - indices.data() + 5, - }; - - const float char_width = static_cast(atlas.glyph_width) / static_cast(atlas.glyph_height) * char_height; - const float y_min = bottom_left_pos.y; - const float y_max = y_min + char_height; - -#if 1 - for (int i = 0; i < str.size(); ++i) { - char c = str[i]; -#else - for (auto [i, c] : str | std::views::enumerate) { -#endif - const auto uv = atlas.uv_coord_of_char(c); - - const int v_offset = i * 4; - const int i_offset = i * 6; - - const float x_min = char_width * i + bottom_left_pos.x; - const float x_max = x_min + char_width; - - std::construct_at(v_ptr[0] + v_offset, glm::vec2(x_min, y_min), glm::vec2(uv.min_x, uv.min_y)); - std::construct_at(v_ptr[1] + v_offset, glm::vec2(x_max, y_min), glm::vec2(uv.max_x, uv.min_y)); - std::construct_at(v_ptr[2] + v_offset, glm::vec2(x_max, y_max), glm::vec2(uv.max_x, uv.max_y)); - std::construct_at(v_ptr[3] + v_offset, glm::vec2(x_min, y_max), glm::vec2(uv.min_x, uv.max_y)); - - *(i_ptr[0] + i_offset) = v_offset + 0; - *(i_ptr[1] + i_offset) = v_offset + 1; - *(i_ptr[2] + i_offset) = v_offset + 2; - *(i_ptr[3] + i_offset) = v_offset + 2; - *(i_ptr[4] + i_offset) = v_offset + 3; - *(i_ptr[5] + i_offset) = v_offset + 0; - } - return std::tuple { vertices, indices }; - } -} -#else - -namespace Media { - constexpr static auto PRINTABLE_CHARS = []() { - constexpr char first_printable = char(32); - constexpr char last_printable = char(127); - constexpr size_t n = last_printable - first_printable; - std::array chars {}; - std::ranges::copy(std::views::iota(first_printable, last_printable), chars.begin()); - return chars; - }(); - - auto FontAtlas::create(std::filesystem::path path, uint32_t char_height_px) noexcept -> Utily::Result { - // 1. Load ttf file from disk. - // 2. Initalise the font face - // 3. Generate and cache the bitmap for each glyph. - // 4. Determine the most compact atlas dimensions. - // 5. Allocate raw image data. - // 6. Blit each cached glyph bitmap onto the atlas, ensuring the same spanline. - // 7. Create Image and font atlas. - - // 1. - auto file_load_result = Utily::FileReader::load_entire_file(path); - if (file_load_result.has_error()) { - return file_load_result.error(); - } - const auto& encoded_ttf = file_load_result.value(); - - // 2. - FT_Face ft_face = nullptr; - if (auto error = FT_New_Memory_Face(free_type.library, encoded_ttf.data(), encoded_ttf.size(), 0, &ft_face); error) { - return Utily::Error { FT_Error_String(error) }; - } - if (auto error = FT_Set_Pixel_Sizes(ft_face, 0, char_height_px); error) { - return Utily::Error { FT_Error_String(error) }; - } - - // 3. - struct GlyphDimensions { - glm::uvec2 bitmap_dimensions = { 0, 0 }; - uint32_t spanline = 0; - uint32_t left_padding = 0; - }; - struct CachedGlyph { - char c; - std::vector bitmap; - GlyphDimensions dimensions; - }; - auto create_cached_glyph = [&ft_face](char c) -> CachedGlyph { - auto glyph_index = FT_Get_Char_Index(ft_face, static_cast(c)); - FT_Load_Glyph(ft_face, glyph_index, FT_LOAD_DEFAULT); - FT_Render_Glyph(ft_face->glyph, FT_Render_Mode::FT_RENDER_MODE_NORMAL); - auto ft_bitmap = std::span { - ft_face->glyph->bitmap.buffer, - ft_face->glyph->bitmap.width * ft_face->glyph->bitmap.rows - }; - auto bitmap = std::vector(ft_bitmap.size()); - std::ranges::copy(ft_bitmap, bitmap.begin()); - - auto a = std::span { - ft_face->glyph, static_cast(ft_face->num_glyphs) - }; - - return CachedGlyph { - .c = c, - .bitmap = std::move(bitmap), - .dimensions = { - .bitmap_dimensions = { ft_face->glyph->bitmap.width, ft_face->glyph->bitmap.rows }, - .spanline = static_cast(ft_face->glyph->bitmap_top), - .left_padding = static_cast(ft_face->glyph->bitmap_left) } - }; - }; - std::array cached_glyphs; - std::transform(PRINTABLE_CHARS.begin(), PRINTABLE_CHARS.end(), cached_glyphs.begin(), create_cached_glyph); - - // 4. - auto take_max_dimensions = [&](GlyphDimensions&& agg, const CachedGlyph& cg) { - return GlyphDimensions { - .bitmap_dimensions = { - std::max(agg.bitmap_dimensions.x, cg.dimensions.bitmap_dimensions.x), - std::max(agg.bitmap_dimensions.y, cg.dimensions.bitmap_dimensions.y), - }, - .spanline = std::max(agg.spanline, cg.dimensions.spanline), - .left_padding = std::max(agg.left_padding, cg.dimensions.left_padding), - }; - }; - auto atlas_info = std::reduce(cached_glyphs.begin(), cached_glyphs.end(), GlyphDimensions {}, take_max_dimensions); - auto take_max_height = [&](uint32_t&& agg, const CachedGlyph& cd) { - return std::max(cd.dimensions.bitmap_dimensions.y + atlas_info.spanline - cd.dimensions.spanline, agg); - }; - atlas_info.bitmap_dimensions.y = std::reduce(cached_glyphs.begin(), cached_glyphs.end(), atlas_info.bitmap_dimensions.y, take_max_height); - - // 5. - const auto [atlas_glyphs_per_row, atlas_num_rows] = [](float glyph_width, float glyph_height, float num_glyphs_in_atlas) { - auto [min_diff, min_x, min_y] = std::tuple { std::numeric_limits::max(), 0, num_glyphs_in_atlas }; - for (float i = 1; i < num_glyphs_in_atlas; ++i) { - auto x = i; - auto y = num_glyphs_in_atlas / i; - x = std::floor(x); - y = std::ceil(y); - auto diff = std::abs((x * glyph_width) - (y * glyph_height)); - if (diff < min_diff && num_glyphs_in_atlas <= x * y) { - min_diff = diff; - min_x = x; - min_y = y; - } - } - return std::tuple { min_x, min_y }; - }(atlas_info.bitmap_dimensions.x, atlas_info.bitmap_dimensions.y, PRINTABLE_CHARS.size()); - - const int atlas_img_height = atlas_info.bitmap_dimensions.y * atlas_num_rows; - const int atlas_img_width = atlas_info.bitmap_dimensions.x * atlas_glyphs_per_row; - - std::vector atlas_buffer; - atlas_buffer.resize(atlas_img_height * atlas_img_width, (uint8_t)0); - - std::cout << atlas_info.bitmap_dimensions.x << ','; - std::cout << atlas_info.bitmap_dimensions.y << '\n'; - - std::cout << atlas_img_width << ','; - std::cout << atlas_img_height << '\n'; - - // 6. - for (std::ptrdiff_t i = 0; i < static_cast(PRINTABLE_CHARS.size()); ++i) { - const auto& bitmap_ft = cached_glyphs[i]; - const auto atlas_coords = glm::ivec2 { i % atlas_glyphs_per_row, i / atlas_glyphs_per_row }; - const auto adjusted_offset = glm::ivec2 { atlas_coords.x * atlas_info.bitmap_dimensions.x, atlas_coords.y * atlas_info.bitmap_dimensions.y }; - - auto get_atlas_buffer_dest = [&](int x, int y) -> uint8_t& { - static int max_y = 0; - if (y > max_y) { - max_y = y; - std::cout << "MAX Y -> " << y << '\n'; - } - const auto px_coords = glm::ivec2(adjusted_offset.x + x, adjusted_offset.y + y); - return atlas_buffer[px_coords.y * atlas_img_width + px_coords.x + bitmap_ft.dimensions.left_padding]; - }; - for (std::ptrdiff_t y = 0; y < bitmap_ft.dimensions.bitmap_dimensions.y; ++y) { - for (std::ptrdiff_t x = 0; x < bitmap_ft.dimensions.bitmap_dimensions.x; ++x) { - // align to relative bitmap - const std::ptrdiff_t relative_y = (y + atlas_info.spanline - bitmap_ft.dimensions.spanline); - const std::ptrdiff_t ft_offset = y * bitmap_ft.dimensions.bitmap_dimensions.x + x; - get_atlas_buffer_dest(x, relative_y) = bitmap_ft.bitmap[ft_offset]; - } - } - } - - // 7. - auto image_result = Media::Image::create( - std::span(atlas_buffer.cbegin(), atlas_buffer.cend()), - glm::uvec2(atlas_img_width, atlas_img_height), - Media::Image::InternalFormat::greyscale); - - if (image_result.has_error()) { - return image_result.error(); - } - - return FontAtlas(M { - .atlas_image = std::move(image_result.value()), - .atlas_layout = { atlas_glyphs_per_row, atlas_num_rows }, - .glyph_dimensions = { - atlas_info.bitmap_dimensions.x, - atlas_info.bitmap_dimensions.y } }); - } - auto FontAtlas::uv_for(char a) const noexcept -> FontAtlas::UvCoord { - assert(PRINTABLE_CHARS.front() <= a && a <= PRINTABLE_CHARS.back() && "Must be a printable character"); - - const auto i = static_cast(a - PRINTABLE_CHARS.front()); - const float r = static_cast(i / _m.atlas_layout.x); - const float c = static_cast(i % static_cast(_m.atlas_layout.x)); - - return { - .min_x = c / static_cast(_m.atlas_layout.x), - .max_x = (c + 1) / static_cast(_m.atlas_layout.x), - .min_y = 1 - (r + 1) / static_cast(_m.atlas_layout.y), - .max_y = 1 - r / static_cast(_m.atlas_layout.y), - }; - } -} - -#endif \ No newline at end of file diff --git a/code/Engine/src/Media/FontAtlas.cpp b/code/Engine/src/Media/FontAtlas.cpp new file mode 100644 index 0000000..12e408c --- /dev/null +++ b/code/Engine/src/Media/FontAtlas.cpp @@ -0,0 +1,218 @@ +#include "Media/FontAtlas.hpp" + +#include +#include +#include +#include +#include + +#include "Profiler/Profiler.hpp" + +// #include + +#include +#include FT_FREETYPE_H + +struct FreeType { + FT_Library library = nullptr; + + FreeType() { + Profiler::Timer timer("Font::FreeType() *library init*", { "freetype", "font", "init" }); + if (auto error = FT_Init_FreeType(&library); error) { + const auto ft_err_msg = std::string_view { FT_Error_String(error) }; + std::cerr + << "Failed to initialise FreeType library: \n" + << ft_err_msg + << std::endl; + } + } + ~FreeType() { + if (library != nullptr) { + FT_Done_FreeType(library); + } + library = nullptr; + } +}; + +thread_local FreeType free_type {}; + +namespace Media { + constexpr static auto PRINTABLE_CHARS = []() { + constexpr char first_printable = char(32); + constexpr char last_printable = char(127); + constexpr size_t n = last_printable - first_printable; + std::array chars {}; + std::ranges::copy(std::views::iota(first_printable, last_printable), chars.begin()); + return chars; + }(); + + auto FontAtlas::create(std::filesystem::path path, uint32_t char_height_px) noexcept -> Utily::Result { + Profiler::Timer timer("Media::FontAtlas::create()"); + + // 1. Load ttf file from disk. + // 2. Initalise the font face + // 3. Generate and cache the bitmap for each glyph. + // 4. Determine the most compact atlas dimensions. + // 5. Allocate raw image data. + // 6. Blit each cached glyph bitmap onto the atlas, ensuring the same spanline. + // 7. Create Image and font atlas. + + // 1. + auto file_load_result = Utily::FileReader::load_entire_file(path); + if (file_load_result.has_error()) { + return file_load_result.error(); + } + const auto& encoded_ttf = file_load_result.value(); + + // 2. + FT_Face ft_face = nullptr; + if (auto error = FT_New_Memory_Face(free_type.library, encoded_ttf.data(), encoded_ttf.size(), 0, &ft_face); error) { + return Utily::Error { FT_Error_String(error) }; + } + if (auto error = FT_Set_Pixel_Sizes(ft_face, 0, char_height_px); error) { + return Utily::Error { FT_Error_String(error) }; + } + + // 3. + struct GlyphDimensions { + glm::uvec2 bitmap_dimensions = { 0, 0 }; + uint32_t spanline = 0; + uint32_t left_padding = 0; + }; + struct CachedGlyph { + char c; + std::vector bitmap; + GlyphDimensions dimensions; + }; + auto create_cached_glyph = [&ft_face](char c) -> CachedGlyph { + auto glyph_index = FT_Get_Char_Index(ft_face, static_cast(c)); + FT_Load_Glyph(ft_face, glyph_index, FT_LOAD_DEFAULT); + FT_Render_Glyph(ft_face->glyph, FT_Render_Mode::FT_RENDER_MODE_NORMAL); + auto ft_bitmap = std::span { + ft_face->glyph->bitmap.buffer, + ft_face->glyph->bitmap.width * ft_face->glyph->bitmap.rows + }; + auto bitmap = std::vector(ft_bitmap.size()); + std::ranges::copy(ft_bitmap, bitmap.begin()); + + auto a = std::span { + ft_face->glyph, static_cast(ft_face->num_glyphs) + }; + + return CachedGlyph { + .c = c, + .bitmap = std::move(bitmap), + .dimensions = { + .bitmap_dimensions = { ft_face->glyph->bitmap.width, ft_face->glyph->bitmap.rows }, + .spanline = static_cast(ft_face->glyph->bitmap_top), + .left_padding = static_cast(ft_face->glyph->bitmap_left) } + }; + }; + std::array cached_glyphs; + std::transform(PRINTABLE_CHARS.begin(), PRINTABLE_CHARS.end(), cached_glyphs.begin(), create_cached_glyph); + + // 4. + auto take_max_dimensions = [&](GlyphDimensions&& agg, const CachedGlyph& cg) { + return GlyphDimensions { + .bitmap_dimensions = { + std::max(agg.bitmap_dimensions.x, cg.dimensions.bitmap_dimensions.x), + std::max(agg.bitmap_dimensions.y, cg.dimensions.bitmap_dimensions.y), + }, + .spanline = std::max(agg.spanline, cg.dimensions.spanline), + .left_padding = std::max(agg.left_padding, cg.dimensions.left_padding), + }; + }; + auto atlas_info = std::reduce(cached_glyphs.begin(), cached_glyphs.end(), GlyphDimensions {}, take_max_dimensions); + auto take_max_height = [&](uint32_t&& agg, const CachedGlyph& cd) { + return std::max(cd.dimensions.bitmap_dimensions.y + atlas_info.spanline - cd.dimensions.spanline, agg); + }; + atlas_info.bitmap_dimensions.y = std::reduce(cached_glyphs.begin(), cached_glyphs.end(), atlas_info.bitmap_dimensions.y, take_max_height); + + // 5. + const auto [atlas_glyphs_per_row, atlas_num_rows] = [](float glyph_width, float glyph_height, float num_glyphs_in_atlas) { + auto [min_diff, min_x, min_y] = std::tuple { std::numeric_limits::max(), 0, num_glyphs_in_atlas }; + for (float i = 1; i < num_glyphs_in_atlas; ++i) { + auto x = i; + auto y = num_glyphs_in_atlas / i; + x = std::floor(x); + y = std::ceil(y); + auto diff = std::abs((x * glyph_width) - (y * glyph_height)); + if (diff < min_diff && num_glyphs_in_atlas <= x * y) { + min_diff = diff; + min_x = x; + min_y = y; + } + } + return std::tuple { min_x, min_y }; + }(atlas_info.bitmap_dimensions.x, atlas_info.bitmap_dimensions.y, PRINTABLE_CHARS.size()); + + const int atlas_img_height = atlas_info.bitmap_dimensions.y * atlas_num_rows; + const int atlas_img_width = atlas_info.bitmap_dimensions.x * atlas_glyphs_per_row; + + auto atlas_buffer_size = static_cast(atlas_img_height * atlas_img_width); + auto atlas_buffer = std::make_unique(atlas_buffer_size); + auto atlas_buffer_begin = atlas_buffer.get(); + auto atlas_buffer_end = atlas_buffer_begin + atlas_img_height * atlas_img_width; + + std::fill(atlas_buffer_begin, atlas_buffer_end, (uint8_t)0); + + // 6. + { + Profiler::Timer timer2("blit_glyphs_to_atlas()"); + for (std::ptrdiff_t i = 0; i < static_cast(PRINTABLE_CHARS.size()); ++i) { + const auto& bitmap_ft = cached_glyphs[i]; + const auto atlas_coords = glm::ivec2 { i % atlas_glyphs_per_row, i / atlas_glyphs_per_row }; + const auto adjusted_offset = glm::ivec2 { atlas_coords.x * atlas_info.bitmap_dimensions.x, atlas_coords.y * atlas_info.bitmap_dimensions.y }; + + auto get_atlas_buffer_dest = [&](int x, int y) { + const auto px_coords = glm::ivec2(adjusted_offset.x + x, adjusted_offset.y + y); + return atlas_buffer_begin + (px_coords.y * atlas_img_width + px_coords.x + bitmap_ft.dimensions.left_padding); + }; + for (std::ptrdiff_t y = 0; y < bitmap_ft.dimensions.bitmap_dimensions.y; ++y) { + // align to relative bitmap + const std::ptrdiff_t relative_y = y + atlas_info.spanline - bitmap_ft.dimensions.spanline; + const std::ptrdiff_t ft_offset = y * bitmap_ft.dimensions.bitmap_dimensions.x; + + auto src = std::span { + bitmap_ft.bitmap.data() + ft_offset, + static_cast(bitmap_ft.dimensions.bitmap_dimensions.x) + }; + std::ranges::copy(src, get_atlas_buffer_dest(0, relative_y)); + } + } + } + + // 7. + auto image_result = Media::Image::create( + std::move(atlas_buffer), + atlas_buffer_size, + glm::uvec2(atlas_img_width, atlas_img_height), + Media::Image::InternalFormat::greyscale); + + if (image_result.has_error()) { + return image_result.error(); + } + + return FontAtlas(M { + .atlas_image = std::move(image_result.value()), + .atlas_layout = { atlas_glyphs_per_row, atlas_num_rows }, + .glyph_dimensions = { + atlas_info.bitmap_dimensions.x, + atlas_info.bitmap_dimensions.y }, + }); + } + auto FontAtlas::uv_for(char a) const noexcept -> FontAtlas::UvCoord { + assert(PRINTABLE_CHARS.front() <= a && a <= PRINTABLE_CHARS.back() && "Must be a printable character"); + + const auto i = static_cast(a - PRINTABLE_CHARS.front()); + const float r = static_cast(i / _m.atlas_layout.x); + const float c = static_cast(i % static_cast(_m.atlas_layout.x)); + + return { + .min_x = c / static_cast(_m.atlas_layout.x), + .max_x = (c + 1) / static_cast(_m.atlas_layout.x), + .min_y = 1 - (r + 1) / static_cast(_m.atlas_layout.y), + .max_y = 1 - r / static_cast(_m.atlas_layout.y), + }; + } +} diff --git a/code/Engine/src/Media/Image.cpp b/code/Engine/src/Media/Image.cpp index dcdc589..8f1f98f 100644 --- a/code/Engine/src/Media/Image.cpp +++ b/code/Engine/src/Media/Image.cpp @@ -201,6 +201,7 @@ namespace Media { namespace Media { auto Image::create(std::filesystem::path path) -> Utily::Result { + Profiler::Timer timer("Media::Image::create()"); // 1. Load the file contents into memory. // 2. Decode the file contents via libspng. // 3. Construct a valid Image instance. @@ -216,34 +217,40 @@ namespace Media { const auto& encoded_png = load_file_result.value(); // 2. - spng_ctx* ctx = spng_ctx_new(0); - int spng_error = 0; - spng_error = spng_set_png_buffer(ctx, encoded_png.data(), encoded_png.size()); - if (spng_error) { - return Utily::Error(std::string(spng_strerror(spng_error))); - } + std::unique_ptr data; size_t data_size_bytes = 0; - spng_error = spng_decoded_image_size(ctx, SPNG_FMT_RGBA8, &data_size_bytes); - if (spng_error) { - return Utily::Error(std::string(spng_strerror(spng_error))); - } - auto data = std::make_unique_for_overwrite(data_size_bytes); - spng_error = spng_decode_image(ctx, data.get(), data_size_bytes, SPNG_FMT_RGBA8, 0); - if (spng_error) { - return Utily::Error(std::string(spng_strerror(spng_error))); - } - spng_ihdr idhr; - spng_error = spng_get_ihdr(ctx, &idhr); - if (spng_error) { - return Utily::Error(std::string(spng_strerror(spng_error))); + glm::uvec2 dimensions = { 0, 0 }; + { + Profiler::Timer timer2("libspng_decode_image()"); + spng_ctx* ctx = spng_ctx_new(0); + int spng_error = 0; + spng_error = spng_set_png_buffer(ctx, encoded_png.data(), encoded_png.size()); + if (spng_error) { + return Utily::Error(std::string(spng_strerror(spng_error))); + } + spng_error = spng_decoded_image_size(ctx, SPNG_FMT_RGBA8, &data_size_bytes); + if (spng_error) { + return Utily::Error(std::string(spng_strerror(spng_error))); + } + data = std::make_unique_for_overwrite(data_size_bytes); + spng_error = spng_decode_image(ctx, data.get(), data_size_bytes, SPNG_FMT_RGBA8, 0); + if (spng_error) { + return Utily::Error(std::string(spng_strerror(spng_error))); + } + spng_ihdr idhr; + spng_error = spng_get_ihdr(ctx, &idhr); + if (spng_error) { + return Utily::Error(std::string(spng_strerror(spng_error))); + } + dimensions = { idhr.width, idhr.height }; + spng_ctx_free(ctx); } - spng_ctx_free(ctx); // 3. return Image(M { .data = std::move(data), .data_size_bytes = data_size_bytes, - .dimensions = { idhr.width, idhr.height }, + .dimensions = dimensions, .format = InternalFormat::rgba }); } auto Image::create(std::span raw_bytes, glm::uvec2 dimensions, InternalFormat format) -> Utily::Result { @@ -266,7 +273,7 @@ namespace Media { auto data = std::make_unique_for_overwrite(raw_bytes.size()); auto data_size_bytes = raw_bytes.size(); // 3. - std::uninitialized_copy(raw_bytes.cbegin(), raw_bytes.cend(), data.get()); + std::uninitialized_copy(raw_bytes.begin(), raw_bytes.end(), data.get()); // 4. return Image(M { .data = std::move(data), @@ -275,6 +282,14 @@ namespace Media { .format = format }); } + auto Image::create(std::unique_ptr&& data, size_t data_size_bytes, glm::uvec2 dimensions, InternalFormat format) -> Utily::Result { + return Image(M { + .data = std::move(data), + .data_size_bytes = data_size_bytes, + .dimensions = dimensions, + .format = format }); + } + auto Image::opengl_format() const -> uint32_t { switch (_m.format) { case InternalFormat::greyscale: