From cfb613d6bb5990dc1ef1071a98fe8d0c56eda747 Mon Sep 17 00:00:00 2001 From: ohyzha Date: Thu, 2 Jan 2025 17:40:52 +0200 Subject: [PATCH] implement tight atlas packing --- .../Scene/BitmapFontAtlasGenerator.cpp | 178 ++++++++++++------ .../Scene/BitmapFontAtlasGenerator.hpp | 2 +- 2 files changed, 125 insertions(+), 55 deletions(-) diff --git a/openVulkanoCpp/Scene/BitmapFontAtlasGenerator.cpp b/openVulkanoCpp/Scene/BitmapFontAtlasGenerator.cpp index a0ff547..4be3489 100644 --- a/openVulkanoCpp/Scene/BitmapFontAtlasGenerator.cpp +++ b/openVulkanoCpp/Scene/BitmapFontAtlasGenerator.cpp @@ -7,6 +7,88 @@ #include "BitmapFontAtlasGenerator.hpp" #include "Base/Logger.hpp" +namespace +{ + using namespace OpenVulkano; + + struct GlyphForPacking + { + uint32_t code; + size_t firstGlyphByteInAtlas; + Math::Vector2d atlasPos; + Math::Vector2i wh; + }; + + struct Shelf + { + Shelf(uint32_t width, uint32_t height) : m_width(width), m_height(height), m_remainingWidth(width) {} + bool HasSpaceForGlyph(uint32_t glyphWidth, uint32_t glyphHeight) const + { + return m_remainingWidth >= glyphWidth && m_height >= glyphHeight; + } + uint32_t GetWidth() const { return m_width; } + uint32_t GetHeight() const { return m_height; } + uint32_t GetNextGlyphPos() const { return m_nextGlyphPos; }; + uint32_t GetOccupiedSize() const { return ((m_width - m_remainingWidth) * m_height); } + std::optional AddGlyph(uint32_t glyphWidth, uint32_t glyphHeight) + { + if (!HasSpaceForGlyph(glyphWidth, glyphHeight)) + { + return {}; + } + uint32_t insertionPos = m_nextGlyphPos; + m_nextGlyphPos += glyphWidth; + m_remainingWidth -= glyphWidth; + return insertionPos; + } + + private: + uint32_t m_width; + uint32_t m_height; + uint32_t m_remainingWidth; + uint32_t m_nextGlyphPos = 0; + }; + + std::vector CreateShelves(uint32_t atlasWidth, std::vector& glyphs, const Scene::FtFaceRecPtr& face) + { + std::vector shelves; + for (GlyphForPacking& glyph : glyphs) + { + FT_Error error = FT_Load_Char(face.get(), glyph.code, FT_LOAD_RENDER); + if (error) + { + continue; + } + + FT_GlyphSlot slot = face->glyph; + bool needNewShelf = true; + uint32_t totalPrevShelvesHeight = 0; + for (Shelf& shelf : shelves) + { + if (std::optional insertionPosX = shelf.AddGlyph(glyph.wh.x, glyph.wh.y)) + { + glyph.firstGlyphByteInAtlas = *insertionPosX + (totalPrevShelvesHeight * atlasWidth); + glyph.atlasPos.x = *insertionPosX; + glyph.atlasPos.y = totalPrevShelvesHeight; + needNewShelf = false; + break; + } + totalPrevShelvesHeight += shelf.GetHeight(); + } + + if (needNewShelf) + { + shelves.emplace_back(atlasWidth, glyph.wh.y); + shelves.back().AddGlyph(glyph.wh.x, glyph.wh.y); + glyph.firstGlyphByteInAtlas = totalPrevShelvesHeight * atlasWidth; + glyph.atlasPos.x = 0; + glyph.atlasPos.y = totalPrevShelvesHeight; + } + } + return shelves; + } +} + namespace OpenVulkano::Scene { void BitmapFontAtlasGenerator::GenerateAtlas(const std::string& fontFile, const std::set& charset, @@ -33,28 +115,43 @@ namespace OpenVulkano::Scene m_atlasData = std::make_shared(); const auto& [lib, face] = FontAtlasGeneratorBase::InitFreetype(source); - - Math::Vector2ui cellSize; if (m_pixelSizeConfig.isPixelSize) { - cellSize = { m_pixelSizeConfig.size, m_pixelSizeConfig.size }; - // set pixel width/height lower than glyph size above, otherwise some glyphs will be cropped or some overlapping will be present - FT_Set_Pixel_Sizes(face.get(), 0, cellSize.y - cellSize.y / 3); + FT_Set_Pixel_Sizes(face.get(), 0, m_pixelSizeConfig.size); } else { const float pixelSize = (m_pixelSizeConfig.size * m_pixelSizeConfig.dpi) / 72.0f; - //int fontHeight = round((face->bbox.yMax - face->bbox.yMin) * pixelSize / face->units_per_EM); - //int fontWidth = round((face->bbox.xMax - face->bbox.xMin) * pixelSize / face->units_per_EM); - cellSize = { pixelSize, pixelSize }; - FT_Set_Char_Size(face.get(), 0, static_cast(m_pixelSizeConfig.size) * 64, - static_cast(m_pixelSizeConfig.dpi), static_cast(m_pixelSizeConfig.dpi)); + FT_Set_Pixel_Sizes(face.get(), 0, pixelSize); } - - const double sq = std::sqrt(chset.size()); - const size_t glyphsPerRow = (static_cast(sq)) + (sq - static_cast(sq) != 0); - const size_t rows = (chset.size() / glyphsPerRow) + (chset.size() % glyphsPerRow != 0); - const Math::Vector2ui atlasResolution = { glyphsPerRow * cellSize.x, rows * cellSize.y }; + + FT_Error error = 0; + double area = 0; + std::vector allGlyphs; + allGlyphs.reserve(chset.size()); + for (uint32_t codepoint : chset) + { + error = FT_Load_Char(face.get(), codepoint, FT_LOAD_RENDER); + if (error) + { + Logger::APP->error("FT_Load_Char for codepoint {} has failed. {}", codepoint, GetFreetypeErrorDescription(error)); + continue; + } + FT_GlyphSlot slot = face->glyph; + unsigned int h = slot->bitmap.rows; + unsigned int w = slot->bitmap.width; + GlyphForPacking& glyph = allGlyphs.emplace_back(); + glyph.code = codepoint; + glyph.wh = { slot->bitmap.width, slot->bitmap.rows }; + area += h * w; + } + + const double sq = ceil(sqrt(area)); + std::sort(allGlyphs.begin(), allGlyphs.end(), [](const GlyphForPacking& a, const GlyphForPacking& b) { return a.wh.y > b.wh.y; }); + std::vector shelves = ::CreateShelves(sq, allGlyphs, face); + uint32_t atlasHeight = 0; + std::for_each(shelves.begin(), shelves.end(), [&](const Shelf& shelf) { atlasHeight += shelf.GetHeight(); }); + const Math::Vector2ui atlasResolution = { sq, atlasHeight }; // same as in msdfgen lib by default. see import-font.h for reference // TODO: probably also support keeping coordinates as the integer values native to the font file @@ -64,60 +161,34 @@ namespace OpenVulkano::Scene SetupAtlasData(atlasResolution, face->height * scaleFactor, FontAtlasType::BITMAP); size_t loadedGlyphs = 0; - FT_Error error = 0; - int currentPosX = 0; - int currentPosY = 0; - Math::Vector2ui gridPos = { 0, 0 }; - for (uint32_t codepoint : chset) + for (const GlyphForPacking& glyph : allGlyphs) { - error = FT_Load_Char(face.get(), codepoint, FT_LOAD_RENDER); + error = FT_Load_Char(face.get(), glyph.code, FT_LOAD_RENDER); if (error) { - Logger::APP->error("FT_Load_Char for codepoint {} has failed. {}", codepoint, GetFreetypeErrorDescription(error)); + Logger::APP->error("FT_Load_Char for codepoint {} has failed. {}", glyph.code, + GetFreetypeErrorDescription(error)); continue; } FT_GlyphSlot slot = face->glyph; - if (slot->bitmap.width > cellSize.x || slot->bitmap.rows > cellSize.y) - { - Logger::APP->warn("Glyph size exceeds grid cell size: {}x{} exceeds {}x{}", slot->bitmap.width, slot->bitmap.rows, cellSize.x, cellSize.y); - // skip such glyph for now to avoid crash - continue; - } - - const size_t firstGlyphByte = (gridPos.y * cellSize.x + gridPos.x * atlasResolution.x * cellSize.y); for (int row = 0; row < slot->bitmap.rows; row++) { for (int col = 0; col < slot->bitmap.width; col++) { - m_atlasData->img->data[firstGlyphByte + row * atlasResolution.x + col] = slot->bitmap.buffer[(slot->bitmap.rows - 1 - row) * slot->bitmap.pitch + col]; + m_atlasData->img->data[glyph.firstGlyphByteInAtlas + row * atlasResolution.x + col] = + slot->bitmap.buffer[(slot->bitmap.rows - 1 - row) * slot->bitmap.pitch + col]; } } - GlyphInfo& glyphInfo = m_atlasData->glyphs[codepoint]; - const Math::Vector2d glyphMetrics = { slot->metrics.width * scaleFactor, slot->metrics.height * scaleFactor }; - const Math::Vector2d glyphBearing = { slot->metrics.horiBearingX * scaleFactor, slot->metrics.horiBearingY * scaleFactor }; - // metrics are 1/64 of a pixel - constexpr double toPixelScaler = 1. / 64; - const Math::Vector2d whPixel = { static_cast(slot->metrics.width * toPixelScaler), - static_cast(slot->metrics.height * toPixelScaler) }; - Math::AABB glyphAtlasAABB(Math::Vector3f(currentPosX, currentPosY, 0), Math::Vector3f(currentPosX + whPixel.x, currentPosY + whPixel.y, 0)); + GlyphInfo& glyphInfo = m_atlasData->glyphs[glyph.code]; + const Math::Vector2d glyphMetrics = { slot->metrics.width * scaleFactor, + slot->metrics.height * scaleFactor }; + const Math::Vector2d glyphBearing = { slot->metrics.horiBearingX * scaleFactor, + slot->metrics.horiBearingY * scaleFactor }; + Math::AABB glyphAtlasAABB(Math::Vector3f(glyph.atlasPos.x, glyph.atlasPos.y, 0), Math::Vector3f(glyph.atlasPos.x + slot->bitmap.width, glyph.atlasPos.y + slot->bitmap.rows, 0)); SetGlyphData(glyphInfo, glyphBearing, glyphMetrics, glyphAtlasAABB, slot->advance.x * scaleFactor); - - currentPosX += cellSize.x; loadedGlyphs++; - - if (currentPosX + cellSize.x > atlasResolution.x) - { - currentPosX = 0; - currentPosY += cellSize.y; - gridPos.y = 0; - gridPos.x++; - } - else - { - gridPos.y++; - } } if (pngOutput) @@ -125,6 +196,5 @@ namespace OpenVulkano::Scene SavePng(*pngOutput); } Logger::APP->debug("Created atlas with {} glyphs, {} glyphs could not be loaded", loadedGlyphs, chset.size() - loadedGlyphs); - } } diff --git a/openVulkanoCpp/Scene/BitmapFontAtlasGenerator.hpp b/openVulkanoCpp/Scene/BitmapFontAtlasGenerator.hpp index 7ec1fad..a4db4b5 100644 --- a/openVulkanoCpp/Scene/BitmapFontAtlasGenerator.hpp +++ b/openVulkanoCpp/Scene/BitmapFontAtlasGenerator.hpp @@ -12,7 +12,7 @@ namespace OpenVulkano::Scene { struct FontPixelSizeConfig { - float size = 16.f; + float size = 24.f; float dpi = 72.f; bool isPixelSize = true; };