diff --git a/examples/ExampleSources/bitmap_atlas_packed.png b/examples/ExampleSources/bitmap_atlas_packed.png index d8071d6..69ffc87 100644 Binary files a/examples/ExampleSources/bitmap_atlas_packed.png and b/examples/ExampleSources/bitmap_atlas_packed.png differ diff --git a/openVulkanoCpp/Image/ImageLoader.cpp b/openVulkanoCpp/Image/ImageLoader.cpp index 14c51e5..c5d6d17 100644 --- a/openVulkanoCpp/Image/ImageLoader.cpp +++ b/openVulkanoCpp/Image/ImageLoader.cpp @@ -16,9 +16,9 @@ namespace OpenVulkano::Image std::unique_ptr IImageLoader::loadData(const uint8_t* data, int size, int desiredChannels) { Image result; - int rows, cols, channels; + int width, height, channels; stbi_set_flip_vertically_on_load(true); - uint8_t* pixelData = stbi_load_from_memory(data, static_cast(size), &rows, &cols, &channels, desiredChannels); + uint8_t* pixelData = stbi_load_from_memory(data, static_cast(size), &width, &height, &channels, desiredChannels); if (desiredChannels != 0 && channels < desiredChannels) { Logger::INPUT->warn( @@ -38,13 +38,13 @@ namespace OpenVulkano::Image result.dataFormat = OpenVulkano::DataFormat::R8G8B8A8_UNORM; break; } - result.resolution.x = cols; - result.resolution.y = rows; + result.resolution.x = width; + result.resolution.y = height; result.resolution.z = 1; if (channels == 3) { - result.data = OpenVulkano::Array(cols * rows * 4); - for (size_t srcPos = 0, dstPos = 0; srcPos < cols * rows * 3; srcPos += 3, dstPos += 4) + result.data = OpenVulkano::Array(width * height * 4); + for (size_t srcPos = 0, dstPos = 0; dstPos < result.data.Size(); srcPos += 3, dstPos += 4) { result.data[dstPos] = pixelData[srcPos]; result.data[dstPos + 1] = pixelData[srcPos + 1]; @@ -54,7 +54,7 @@ namespace OpenVulkano::Image } else { - result.data = OpenVulkano::Array(cols * rows * channels); + result.data = OpenVulkano::Array(width * height * channels); std::memcpy(result.data.Data(), pixelData, result.data.Size()); } stbi_image_free(pixelData); diff --git a/openVulkanoCpp/Scene/BitmapFontAtlasGenerator.cpp b/openVulkanoCpp/Scene/BitmapFontAtlasGenerator.cpp index a0ff547..119bd7b 100644 --- a/openVulkanoCpp/Scene/BitmapFontAtlasGenerator.cpp +++ b/openVulkanoCpp/Scene/BitmapFontAtlasGenerator.cpp @@ -33,28 +33,14 @@ namespace OpenVulkano::Scene m_atlasData = std::make_shared(); const auto& [lib, face] = FontAtlasGeneratorBase::InitFreetype(source); + FT_Set_Pixel_Sizes(face.get(), 0, m_pixelSizeConfig.CalculatePixelSize()); - 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); - } - 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)); - } - - 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 }; + auto [allGlyphs, area] = InitGlyphsForPacking(chset, face); + const double atlasWidth = ceil(sqrt(area)); + std::vector shelves = Shelf::CreateShelves(atlasWidth, allGlyphs, face); + uint32_t atlasHeight = 0; + std::for_each(shelves.begin(), shelves.end(), [&](const Shelf& shelf) { atlasHeight += shelf.GetHeight(); }); + const Math::Vector2ui atlasResolution = { atlasWidth, 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 @@ -62,69 +48,68 @@ namespace OpenVulkano::Scene // The coordinates will be normalized to the em size, i.e. 1 = 1 em const double scaleFactor = (1. / face->units_per_EM); SetupAtlasData(atlasResolution, face->height * scaleFactor, FontAtlasType::BITMAP); + FillGlyphsInfo(allGlyphs, face, scaleFactor); + if (pngOutput) + { + SavePng(*pngOutput); + } + } - size_t loadedGlyphs = 0; + std::pair, double> + BitmapFontAtlasGenerator::InitGlyphsForPacking(const std::set& chset, const FtFaceRecPtr& face) + { FT_Error error = 0; - int currentPosX = 0; - int currentPosY = 0; - Math::Vector2ui gridPos = { 0, 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)); + Logger::SCENE->error("FT_Load_Char for codepoint {} has failed. {}", codepoint, + GetFreetypeErrorDescription(error)); + continue; + } + FT_GlyphSlot slot = face->glyph; + GlyphForPacking& glyph = allGlyphs.emplace_back(codepoint, Math::Vector2ui(slot->bitmap.width, slot->bitmap.rows)); + area += slot->bitmap.rows * slot->bitmap.width; + } + std::sort(allGlyphs.begin(), allGlyphs.end(), + [](const GlyphForPacking& a, const GlyphForPacking& b) { return a.size.y > b.size.y; }); + return { allGlyphs, area }; + } + + void BitmapFontAtlasGenerator::FillGlyphsInfo(const std::vector& allGlyphs, const FtFaceRecPtr& face, double scaleFactor) + { + size_t loadedGlyphs = 0; + for (const GlyphForPacking& glyph : allGlyphs) + { + FT_Error error = FT_Load_Char(face.get(), glyph.code, FT_LOAD_RENDER); + if (error) + { + Logger::SCENE->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]; - } + std::memcpy(&m_atlasData->img->data[glyph.firstGlyphByteInAtlas + row * m_atlasData->img->resolution.x], + &slot->bitmap.buffer[(slot->bitmap.rows - 1 - row) * slot->bitmap.pitch], + slot->bitmap.width); } - 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) - { - SavePng(*pngOutput); - } - Logger::APP->debug("Created atlas with {} glyphs, {} glyphs could not be loaded", loadedGlyphs, chset.size() - loadedGlyphs); - + Logger::SCENE->debug("Created atlas with {} glyphs, {} glyphs could not be loaded", loadedGlyphs, allGlyphs.size() - loadedGlyphs); } } diff --git a/openVulkanoCpp/Scene/BitmapFontAtlasGenerator.hpp b/openVulkanoCpp/Scene/BitmapFontAtlasGenerator.hpp index 7ec1fad..7b8c59f 100644 --- a/openVulkanoCpp/Scene/BitmapFontAtlasGenerator.hpp +++ b/openVulkanoCpp/Scene/BitmapFontAtlasGenerator.hpp @@ -7,14 +7,28 @@ #pragma once #include "FontAtlasGeneratorBase.hpp" +#include "Shelf.hpp" namespace OpenVulkano::Scene { - struct FontPixelSizeConfig + class FontPixelSizeConfig { - float size = 16.f; - float dpi = 72.f; - bool isPixelSize = true; + public: + FontPixelSizeConfig(float size = 24.f, float dpi = 72.f, bool isPixelSize = true) + : m_size(size), m_dpi(dpi), m_isPixelSize(isPixelSize) + { + } + void SetSize(float size) { m_size = size; } + void SetDpi(float dpi) { m_dpi = dpi; } + void SetIsPixelSize(bool isPixelSize) { m_isPixelSize = isPixelSize; } + [[nodiscard]] float GetSize() const { return m_size; } + [[nodiscard]] float GetDpi() const { return m_dpi; } + [[nodiscard]] bool GetIsPixelSize() const { return m_isPixelSize; } + [[nodiscard]] unsigned CalculatePixelSize() const { return m_isPixelSize ? m_size : (m_size * m_dpi) / 72.0f; } + private: + float m_size; + float m_dpi; + bool m_isPixelSize; }; class BitmapFontAtlasGenerator : public FontAtlasGeneratorBase @@ -27,6 +41,8 @@ namespace OpenVulkano::Scene const std::optional& pngOutput = std::nullopt) override; private: void Generate(const std::variant>& source, const std::set& chset, const std::optional& pngOutput); + void FillGlyphsInfo(const std::vector& allGlyphs, const FtFaceRecPtr& face, double scaleFactor); + std::pair, double> InitGlyphsForPacking(const std::set& chset, const FtFaceRecPtr& face); private: FontPixelSizeConfig m_pixelSizeConfig; }; diff --git a/openVulkanoCpp/Scene/Shelf.hpp b/openVulkanoCpp/Scene/Shelf.hpp new file mode 100644 index 0000000..b0522b7 --- /dev/null +++ b/openVulkanoCpp/Scene/Shelf.hpp @@ -0,0 +1,96 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +#pragma once + +#include "Math/Math.hpp" +#include "Scene/FreetypeHelper.hpp" +#include +#include + +namespace OpenVulkano::Scene +{ + struct GlyphForPacking + { + uint32_t code; + Math::Vector2i size; + Math::Vector2d atlasPos; + size_t firstGlyphByteInAtlas; + }; + + struct Shelf + { + inline static std::vector CreateShelves(uint32_t atlasWidth, std::vector& glyphs, + const FtFaceRecPtr& face); + + 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 Shelf::CreateShelves(uint32_t atlasWidth, std::vector& glyphs, + const 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.size.x, glyph.size.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.size.y); + shelves.back().AddGlyph(glyph.size.x, glyph.size.y); + glyph.firstGlyphByteInAtlas = totalPrevShelvesHeight * atlasWidth; + glyph.atlasPos.x = 0; + glyph.atlasPos.y = totalPrevShelvesHeight; + } + } + return shelves; + } +}