Merge pull request 'Tight atlas packing' (#183) from misc into master
Reviewed-on: https://git.madvoxel.net/OpenVulkano/OpenVulkano/pulls/183 Reviewed-by: Georg Hagen <georg.hagen@madvoxel.com>
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 161 KiB After Width: | Height: | Size: 174 KiB |
@@ -16,9 +16,9 @@ namespace OpenVulkano::Image
|
|||||||
std::unique_ptr<Image> IImageLoader::loadData(const uint8_t* data, int size, int desiredChannels)
|
std::unique_ptr<Image> IImageLoader::loadData(const uint8_t* data, int size, int desiredChannels)
|
||||||
{
|
{
|
||||||
Image result;
|
Image result;
|
||||||
int rows, cols, channels;
|
int width, height, channels;
|
||||||
stbi_set_flip_vertically_on_load(true);
|
stbi_set_flip_vertically_on_load(true);
|
||||||
uint8_t* pixelData = stbi_load_from_memory(data, static_cast<int>(size), &rows, &cols, &channels, desiredChannels);
|
uint8_t* pixelData = stbi_load_from_memory(data, static_cast<int>(size), &width, &height, &channels, desiredChannels);
|
||||||
if (desiredChannels != 0 && channels < desiredChannels)
|
if (desiredChannels != 0 && channels < desiredChannels)
|
||||||
{
|
{
|
||||||
Logger::INPUT->warn(
|
Logger::INPUT->warn(
|
||||||
@@ -38,13 +38,13 @@ namespace OpenVulkano::Image
|
|||||||
result.dataFormat = OpenVulkano::DataFormat::R8G8B8A8_UNORM;
|
result.dataFormat = OpenVulkano::DataFormat::R8G8B8A8_UNORM;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
result.resolution.x = cols;
|
result.resolution.x = width;
|
||||||
result.resolution.y = rows;
|
result.resolution.y = height;
|
||||||
result.resolution.z = 1;
|
result.resolution.z = 1;
|
||||||
if (channels == 3)
|
if (channels == 3)
|
||||||
{
|
{
|
||||||
result.data = OpenVulkano::Array<uint8_t>(cols * rows * 4);
|
result.data = OpenVulkano::Array<uint8_t>(width * height * 4);
|
||||||
for (size_t srcPos = 0, dstPos = 0; srcPos < cols * rows * 3; srcPos += 3, dstPos += 4)
|
for (size_t srcPos = 0, dstPos = 0; dstPos < result.data.Size(); srcPos += 3, dstPos += 4)
|
||||||
{
|
{
|
||||||
result.data[dstPos] = pixelData[srcPos];
|
result.data[dstPos] = pixelData[srcPos];
|
||||||
result.data[dstPos + 1] = pixelData[srcPos + 1];
|
result.data[dstPos + 1] = pixelData[srcPos + 1];
|
||||||
@@ -54,7 +54,7 @@ namespace OpenVulkano::Image
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
result.data = OpenVulkano::Array<uint8_t>(cols * rows * channels);
|
result.data = OpenVulkano::Array<uint8_t>(width * height * channels);
|
||||||
std::memcpy(result.data.Data(), pixelData, result.data.Size());
|
std::memcpy(result.data.Data(), pixelData, result.data.Size());
|
||||||
}
|
}
|
||||||
stbi_image_free(pixelData);
|
stbi_image_free(pixelData);
|
||||||
|
|||||||
@@ -33,28 +33,14 @@ namespace OpenVulkano::Scene
|
|||||||
|
|
||||||
m_atlasData = std::make_shared<AtlasData>();
|
m_atlasData = std::make_shared<AtlasData>();
|
||||||
const auto& [lib, face] = FontAtlasGeneratorBase::InitFreetype(source);
|
const auto& [lib, face] = FontAtlasGeneratorBase::InitFreetype(source);
|
||||||
|
FT_Set_Pixel_Sizes(face.get(), 0, m_pixelSizeConfig.CalculatePixelSize());
|
||||||
|
|
||||||
Math::Vector2ui cellSize;
|
auto [allGlyphs, area] = InitGlyphsForPacking(chset, face);
|
||||||
if (m_pixelSizeConfig.isPixelSize)
|
const double atlasWidth = ceil(sqrt(area));
|
||||||
{
|
std::vector<Shelf> shelves = Shelf::CreateShelves(atlasWidth, allGlyphs, face);
|
||||||
cellSize = { m_pixelSizeConfig.size, m_pixelSizeConfig.size };
|
uint32_t atlasHeight = 0;
|
||||||
// set pixel width/height lower than glyph size above, otherwise some glyphs will be cropped or some overlapping will be present
|
std::for_each(shelves.begin(), shelves.end(), [&](const Shelf& shelf) { atlasHeight += shelf.GetHeight(); });
|
||||||
FT_Set_Pixel_Sizes(face.get(), 0, cellSize.y - cellSize.y / 3);
|
const Math::Vector2ui atlasResolution = { atlasWidth, atlasHeight };
|
||||||
}
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
|
|
||||||
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 };
|
|
||||||
|
|
||||||
// same as in msdfgen lib by default. see import-font.h for reference
|
// 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
|
// 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
|
// The coordinates will be normalized to the em size, i.e. 1 = 1 em
|
||||||
const double scaleFactor = (1. / face->units_per_EM);
|
const double scaleFactor = (1. / face->units_per_EM);
|
||||||
SetupAtlasData(atlasResolution, face->height * scaleFactor, FontAtlasType::BITMAP);
|
SetupAtlasData(atlasResolution, face->height * scaleFactor, FontAtlasType::BITMAP);
|
||||||
|
FillGlyphsInfo(allGlyphs, face, scaleFactor);
|
||||||
|
if (pngOutput)
|
||||||
|
{
|
||||||
|
SavePng(*pngOutput);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
size_t loadedGlyphs = 0;
|
std::pair<std::vector<GlyphForPacking>, double>
|
||||||
|
BitmapFontAtlasGenerator::InitGlyphsForPacking(const std::set<uint32_t>& chset, const FtFaceRecPtr& face)
|
||||||
|
{
|
||||||
FT_Error error = 0;
|
FT_Error error = 0;
|
||||||
int currentPosX = 0;
|
double area = 0;
|
||||||
int currentPosY = 0;
|
std::vector<GlyphForPacking> allGlyphs;
|
||||||
Math::Vector2ui gridPos = { 0, 0 };
|
allGlyphs.reserve(chset.size());
|
||||||
for (uint32_t codepoint : chset)
|
for (uint32_t codepoint : chset)
|
||||||
{
|
{
|
||||||
error = FT_Load_Char(face.get(), codepoint, FT_LOAD_RENDER);
|
error = FT_Load_Char(face.get(), codepoint, FT_LOAD_RENDER);
|
||||||
if (error)
|
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<GlyphForPacking>& 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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
FT_GlyphSlot slot = face->glyph;
|
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 row = 0; row < slot->bitmap.rows; row++)
|
||||||
{
|
{
|
||||||
for (int col = 0; col < slot->bitmap.width; 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],
|
||||||
m_atlasData->img->data[firstGlyphByte + row * atlasResolution.x + col] = slot->bitmap.buffer[(slot->bitmap.rows - 1 - row) * slot->bitmap.pitch + col];
|
slot->bitmap.width);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
GlyphInfo& glyphInfo = m_atlasData->glyphs[codepoint];
|
GlyphInfo& glyphInfo = m_atlasData->glyphs[glyph.code];
|
||||||
const Math::Vector2d glyphMetrics = { slot->metrics.width * scaleFactor, slot->metrics.height * scaleFactor };
|
const Math::Vector2d glyphMetrics = { slot->metrics.width * scaleFactor,
|
||||||
const Math::Vector2d glyphBearing = { slot->metrics.horiBearingX * scaleFactor, slot->metrics.horiBearingY * scaleFactor };
|
slot->metrics.height * scaleFactor };
|
||||||
// metrics are 1/64 of a pixel
|
const Math::Vector2d glyphBearing = { slot->metrics.horiBearingX * scaleFactor,
|
||||||
constexpr double toPixelScaler = 1. / 64;
|
slot->metrics.horiBearingY * scaleFactor };
|
||||||
const Math::Vector2d whPixel = { static_cast<double>(slot->metrics.width * toPixelScaler),
|
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));
|
||||||
static_cast<double>(slot->metrics.height * toPixelScaler) };
|
|
||||||
Math::AABB glyphAtlasAABB(Math::Vector3f(currentPosX, currentPosY, 0), Math::Vector3f(currentPosX + whPixel.x, currentPosY + whPixel.y, 0));
|
|
||||||
SetGlyphData(glyphInfo, glyphBearing, glyphMetrics, glyphAtlasAABB, slot->advance.x * scaleFactor);
|
SetGlyphData(glyphInfo, glyphBearing, glyphMetrics, glyphAtlasAABB, slot->advance.x * scaleFactor);
|
||||||
|
|
||||||
currentPosX += cellSize.x;
|
|
||||||
loadedGlyphs++;
|
loadedGlyphs++;
|
||||||
|
|
||||||
if (currentPosX + cellSize.x > atlasResolution.x)
|
|
||||||
{
|
|
||||||
currentPosX = 0;
|
|
||||||
currentPosY += cellSize.y;
|
|
||||||
gridPos.y = 0;
|
|
||||||
gridPos.x++;
|
|
||||||
}
|
}
|
||||||
else
|
Logger::SCENE->debug("Created atlas with {} glyphs, {} glyphs could not be loaded", loadedGlyphs, allGlyphs.size() - loadedGlyphs);
|
||||||
{
|
|
||||||
gridPos.y++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pngOutput)
|
|
||||||
{
|
|
||||||
SavePng(*pngOutput);
|
|
||||||
}
|
|
||||||
Logger::APP->debug("Created atlas with {} glyphs, {} glyphs could not be loaded", loadedGlyphs, chset.size() - loadedGlyphs);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,14 +7,28 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "FontAtlasGeneratorBase.hpp"
|
#include "FontAtlasGeneratorBase.hpp"
|
||||||
|
#include "Shelf.hpp"
|
||||||
|
|
||||||
namespace OpenVulkano::Scene
|
namespace OpenVulkano::Scene
|
||||||
{
|
{
|
||||||
struct FontPixelSizeConfig
|
class FontPixelSizeConfig
|
||||||
{
|
{
|
||||||
float size = 16.f;
|
public:
|
||||||
float dpi = 72.f;
|
FontPixelSizeConfig(float size = 24.f, float dpi = 72.f, bool isPixelSize = true)
|
||||||
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
|
class BitmapFontAtlasGenerator : public FontAtlasGeneratorBase
|
||||||
@@ -27,6 +41,8 @@ namespace OpenVulkano::Scene
|
|||||||
const std::optional<std::string>& pngOutput = std::nullopt) override;
|
const std::optional<std::string>& pngOutput = std::nullopt) override;
|
||||||
private:
|
private:
|
||||||
void Generate(const std::variant<std::string, Array<char>>& source, const std::set<uint32_t>& chset, const std::optional<std::string>& pngOutput);
|
void Generate(const std::variant<std::string, Array<char>>& source, const std::set<uint32_t>& chset, const std::optional<std::string>& pngOutput);
|
||||||
|
void FillGlyphsInfo(const std::vector<GlyphForPacking>& allGlyphs, const FtFaceRecPtr& face, double scaleFactor);
|
||||||
|
std::pair<std::vector<GlyphForPacking>, double> InitGlyphsForPacking(const std::set<uint32_t>& chset, const FtFaceRecPtr& face);
|
||||||
private:
|
private:
|
||||||
FontPixelSizeConfig m_pixelSizeConfig;
|
FontPixelSizeConfig m_pixelSizeConfig;
|
||||||
};
|
};
|
||||||
|
|||||||
96
openVulkanoCpp/Scene/Shelf.hpp
Normal file
96
openVulkanoCpp/Scene/Shelf.hpp
Normal file
@@ -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 <vector>
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
namespace OpenVulkano::Scene
|
||||||
|
{
|
||||||
|
struct GlyphForPacking
|
||||||
|
{
|
||||||
|
uint32_t code;
|
||||||
|
Math::Vector2i size;
|
||||||
|
Math::Vector2d atlasPos;
|
||||||
|
size_t firstGlyphByteInAtlas;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Shelf
|
||||||
|
{
|
||||||
|
inline static std::vector<Shelf> CreateShelves(uint32_t atlasWidth, std::vector<GlyphForPacking>& 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<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> Shelf::CreateShelves(uint32_t atlasWidth, std::vector<GlyphForPacking>& glyphs,
|
||||||
|
const 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.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user