implement tight atlas packing

This commit is contained in:
ohyzha
2025-01-02 17:40:52 +02:00
parent 4f2db7891c
commit cfb613d6bb
2 changed files with 125 additions and 55 deletions

View File

@@ -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<uint32_t> 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<GlyphForPacking>& glyphs, const Scene::FtFaceRecPtr& face)
{
std::vector<Shelf> 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<uint32_t> 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<uint32_t>& charset,
@@ -33,28 +115,43 @@ namespace OpenVulkano::Scene
m_atlasData = std::make_shared<AtlasData>();
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<FT_F26Dot6>(m_pixelSizeConfig.size) * 64,
static_cast<FT_UInt>(m_pixelSizeConfig.dpi), static_cast<FT_UInt>(m_pixelSizeConfig.dpi));
FT_Set_Pixel_Sizes(face.get(), 0, pixelSize);
}
const double sq = std::sqrt(chset.size());
const size_t glyphsPerRow = (static_cast<size_t>(sq)) + (sq - static_cast<size_t>(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<GlyphForPacking> 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<Shelf> 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<double>(slot->metrics.width * toPixelScaler),
static_cast<double>(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);
}
}