diff --git a/examples/ExampleApps/TextExampleApp.cpp b/examples/ExampleApps/TextExampleApp.cpp index 3436a83..f6d15c4 100644 --- a/examples/ExampleApps/TextExampleApp.cpp +++ b/examples/ExampleApps/TextExampleApp.cpp @@ -25,6 +25,7 @@ #include "Image/ImageLoaderPng.hpp" #include "Scene/FontAtlasGenerator.hpp" #include "Scene/IFontAtlasGenerator.hpp" +#include "Scene/BitmapFontAtlasGenerator.hpp" #include #ifdef _WIN32 @@ -39,6 +40,7 @@ namespace OpenVulkano namespace fs = std::filesystem; //#define CREATE_NEW_ATLAS 1 + #define CREATE_BITMAP_ATLAS 0 class TextExampleAppImpl final : public TextExampleApp { @@ -66,21 +68,34 @@ namespace OpenVulkano const int N = texts.size(); auto& resourceLoader = ResourceLoader::GetInstance(); const std::string fontPath = resourceLoader.GetResourcePath("Roboto-Regular.ttf"); - m_nodesPool.resize(N * 2); - m_drawablesPool.resize(N * 2); - + m_nodesPool.resize(N * 3); + m_drawablesPool.resize(N * 3); + +#if CREATE_BITMAP_ATLAS + std::set s = BitmapFontAtlasGenerator::LoadAllGlyphs(fontPath); + BitmapFontAtlasGenerator generator; + generator.GenerateAtlas(fontPath, s); + generator.SaveAtlasMetadataInfo("bitmap_atlas"); +#endif + #if defined(MSDFGEN_AVAILABLE) && defined(CREATE_NEW_ATLAS) - msdf_atlas::Charset charset = SdfFontAtlasGenerator::LoadAllGlyphs(fontPath); - m_atlasGenerator.GenerateAtlas(fontPath, charset); - m_msdfAtlasGenerator.GenerateAtlas(fontPath, charset); - m_atlasGenerator.SaveAtlasMetadataInfo("sdf_atlas.png"); - m_msdfAtlasGenerator.SaveAtlasMetadataInfo("msdf_atlas"); + std::set s = SdfFontAtlasGenerator::LoadAllGlyphs(fontPath); + msdf_atlas::Charset charset; + for (uint32_t c : s) + { + charset.add(c); + } + m_atlasGenerator.GenerateAtlas(fontPath, charset); + m_msdfAtlasGenerator.GenerateAtlas(fontPath, charset); + m_atlasGenerator.SaveAtlasMetadataInfo("sdf_atlas.png"); + m_msdfAtlasGenerator.SaveAtlasMetadataInfo("msdf_atlas"); #else auto sdfMetadataInfo = resourceLoader.GetResource("sdf_atlas_packed.png"); auto msdfMetadataInfo = resourceLoader.GetResource("msdf_atlas_packed.png"); + auto bitmapMetadataInfo = resourceLoader.GetResource("bitmap_atlas_packed.png"); #endif - for (int i = 0; i < texts.size() * 2; i++) + for (int i = 0; i < texts.size() * 3; i++) { int textIdx = i % texts.size(); TextDrawable* t = nullptr; @@ -96,15 +111,24 @@ namespace OpenVulkano t->SetShader(&TextDrawable::GetMsdfDefaultShader()); } #else - if (i < texts.size()) + int xOffset = 0; + if (i < N) { t = new TextDrawable(sdfMetadataInfo, texts[textIdx].second); t->SetShader(&TextDrawable::GetSdfDefaultShader()); + xOffset = -5; } - else + else if (i >= N && i < N * 2) { t = new TextDrawable(msdfMetadataInfo, texts[textIdx].second); t->SetShader(&TextDrawable::GetMsdfDefaultShader()); + xOffset = 15; + } + else + { + t = new TextDrawable(bitmapMetadataInfo, texts[textIdx].second); + t->SetShader(&TextDrawable::GetBitmapDefaultShader()); + xOffset = 35; } // OR use separate texture + metadata file //auto metadataInfo = resourceLoader.GetResource("atlas_metadata"); @@ -121,7 +145,7 @@ namespace OpenVulkano t->GenerateText(texts[textIdx].first); m_drawablesPool[i] = t; m_nodesPool[i].Init(); - m_nodesPool[i].SetMatrix(Math::Utils::translate(glm::mat4x4(1.f), Vector3f((i < texts.size() ? -5 : 15), 2 - textIdx * 2, 0))); + m_nodesPool[i].SetMatrix(Math::Utils::translate(glm::mat4x4(1.f), Vector3f(xOffset, 2 - textIdx * 2, 0))); m_nodesPool[i].AddDrawable(m_drawablesPool[i]); m_scene.GetRoot()->AddChild(&m_nodesPool[i]); } diff --git a/examples/ExampleSources/bitmap_atlas_packed.png b/examples/ExampleSources/bitmap_atlas_packed.png new file mode 100644 index 0000000..d8071d6 Binary files /dev/null and b/examples/ExampleSources/bitmap_atlas_packed.png differ diff --git a/openVulkanoCpp/Scene/AtlasData.hpp b/openVulkanoCpp/Scene/AtlasData.hpp index 08dc6a6..ed75309 100644 --- a/openVulkanoCpp/Scene/AtlasData.hpp +++ b/openVulkanoCpp/Scene/AtlasData.hpp @@ -31,6 +31,7 @@ namespace OpenVulkano::Scene { SDF = 0, MSDF, + BITMAP, UNKNOWN }; static constexpr std::string_view DEFAULT_FG_SHADERS[] = { "Shader/text", "Shader/msdfText" }; diff --git a/openVulkanoCpp/Scene/BitmapFontAtlasGenerator.cpp b/openVulkanoCpp/Scene/BitmapFontAtlasGenerator.cpp new file mode 100644 index 0000000..533dd80 --- /dev/null +++ b/openVulkanoCpp/Scene/BitmapFontAtlasGenerator.cpp @@ -0,0 +1,117 @@ +/* + * 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" + +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; + } + + m_atlasData.reset(new AtlasData); + const std::string sourceName = (std::holds_alternative(source) ? std::get<0>(source) : "Binary array"); + const auto& [lib, face] = FontAtlasGeneratorBase::InitFreetype(source); + FT_FaceRec* pFace = face.get(); + + // TODO: add flexibility to set your own size + const Math::Vector2ui cellSize = { 24, 24 }; + // 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(pFace, 0, cellSize.y - cellSize.y / 3); + 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 }; + + // 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. / pFace->units_per_EM); + SetupAtlasData(atlasResolution, pFace->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) + { + error = FT_Load_Char(pFace, codepoint, FT_LOAD_RENDER); + if (error) + { + Logger::APP->error("FT_Load_Char for codepoint {} failed while reading from source {}", codepoint, sourceName); + 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); + } + + 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]; + } + } + + 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)); + 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); + + } +} diff --git a/openVulkanoCpp/Scene/BitmapFontAtlasGenerator.hpp b/openVulkanoCpp/Scene/BitmapFontAtlasGenerator.hpp new file mode 100644 index 0000000..949b323 --- /dev/null +++ b/openVulkanoCpp/Scene/BitmapFontAtlasGenerator.hpp @@ -0,0 +1,25 @@ +/* + * 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 "FontAtlasGeneratorBase.hpp" + +namespace OpenVulkano::Scene +{ + + class BitmapFontAtlasGenerator : public FontAtlasGeneratorBase + { + public: + BitmapFontAtlasGenerator() : FontAtlasGeneratorBase(1) {} + void GenerateAtlas(const std::string& fontFile, const std::set& charset, + const std::optional& pngOutput = std::nullopt) override; + void GenerateAtlas(const Array& fontData, const std::set& charset, + const std::optional& pngOutput = std::nullopt) override; + private: + void Generate(const std::variant>& source, const std::set& chset, const std::optional& pngOutput); + }; +} diff --git a/openVulkanoCpp/Scene/FontAtlasGenerator.cpp b/openVulkanoCpp/Scene/FontAtlasGenerator.cpp index dd0bdb9..179bfcf 100644 --- a/openVulkanoCpp/Scene/FontAtlasGenerator.cpp +++ b/openVulkanoCpp/Scene/FontAtlasGenerator.cpp @@ -8,17 +8,9 @@ #include "FontAtlasGenerator.hpp" #include "Base/Logger.hpp" -#include "Scene/AtlasData.hpp" #include #include #include -#define STBI_MSC_SECURE_CRT -#define STB_IMAGE_WRITE_IMPLEMENTATION -#include -#include -#include FT_FREETYPE_H -#include -#include namespace OpenVulkano::Scene { @@ -28,43 +20,9 @@ namespace OpenVulkano::Scene FontAtlasGeneratorConfig FontAtlasGeneratorConfig::sdfDefaultConfig = { 42, 1.0, 5 }; FontAtlasGeneratorConfig FontAtlasGeneratorConfig::msdfDefaultConfig = { 32, 1.0, 3 }; - template - Charset FontAtlasGenerator::LoadAllGlyphs(const std::variant>& data) - { - FT_Library library; - auto error = FT_Init_FreeType(&library); - if (error) { throw std::runtime_error("Could not initalize freetype library\n"); } - FT_Face face; - if (std::holds_alternative(data)) - { - error = FT_New_Face(library, std::get<0>(data).c_str(), 0, &face); - } - else - { - auto& arr = std::get<1>(data); - error = FT_New_Memory_Face(library, (const FT_Byte*)(arr.Data()), arr.Size(), 0, &face); - } - if (error == FT_Err_Unknown_File_Format) { throw std::runtime_error("Unknown font file format\n"); } - else if (error) { throw std::runtime_error("Font file could not be opened or read or it's corrupted\n"); } - - // some fancy font without unicode charmap - if (face->charmap == nullptr) { throw std::runtime_error("Selected font doesn't contain unicode charmap"); } - Charset s; - FT_UInt glyphIndex; - FT_ULong unicode = FT_Get_First_Char(face, &glyphIndex); - while (glyphIndex != 0) - { - s.add(unicode); - unicode = FT_Get_Next_Char(face, unicode, &glyphIndex); - } - FT_Done_Face(face); - FT_Done_FreeType(library); - return s; - } - template void FontAtlasGenerator::GenerateAtlas(const std::string& fontFile, const std::set& charset, - const std::optional& pngOutput) + const std::optional& pngOutput) { FreetypeHandle* ft; FontHandle* font; @@ -74,21 +32,19 @@ namespace OpenVulkano::Scene Generate(ft, font, s, pngOutput); } - template - FontAtlasGenerator::FontAtlasGenerator() + template FontAtlasGenerator::FontAtlasGenerator() : FontAtlasGeneratorBase(Channels) { if constexpr (Channels == 1) m_config = FontAtlasGeneratorConfig::sdfDefaultConfig; else m_config = FontAtlasGeneratorConfig::msdfDefaultConfig; } template - void FontAtlasGenerator::GenerateAtlas(const Array& fontData, int length, - const std::set& charset, - const std::optional& pngOutput) + void FontAtlasGenerator::GenerateAtlas(const Array& fontData, const std::set& charset, + const std::optional& pngOutput) { FreetypeHandle* ft; FontHandle* font; - InitFreetypeFromBuffer(ft, font, (const msdfgen::byte*)(fontData.Data()), length); + InitFreetypeFromBuffer(ft, font, (const msdfgen::byte*)(fontData.Data()), fontData.Size()); Charset s; std::for_each(s.begin(), s.end(), [&](uint32_t unicode) { s.add(unicode); }); Generate(ft, font, s, pngOutput); @@ -116,37 +72,6 @@ namespace OpenVulkano::Scene Generate(ft, font, charset, pngOutput); } - template - void FontAtlasGenerator::SaveAtlasMetadataInfo(const std::string& outputFile, - bool packIntoSingleFile) const - { - if (m_atlasData->glyphs.empty()) - { - Logger::DATA->info("No glyphs loaded. Nothing to save."); - return; - } - std::string fileName = outputFile; - uint32_t packedFlag = packIntoSingleFile; - if (packIntoSingleFile) - { - std::filesystem::path fPath(fileName); - fileName = (fPath.parent_path() / fPath.stem()).string() + "_packed.png"; - SavePng(fileName); - } - std::fstream fs(fileName.c_str(), std::ios_base::out | std::ios_base::binary | (packedFlag ? std::ios_base::app : std::ios_base::trunc)); - fs.write(reinterpret_cast(&m_atlasData->meta), sizeof(AtlasMetadata)); - uint64_t metadataBytes = sizeof(AtlasMetadata); - for (const auto& [key, val]: m_atlasData->glyphs) - { - fs.write(reinterpret_cast(&key), sizeof(uint32_t)); - fs.write(reinterpret_cast(&val), sizeof(GlyphInfo)); - metadataBytes += sizeof(uint32_t); - metadataBytes += sizeof(GlyphInfo); - } - fs.write(reinterpret_cast(&metadataBytes), sizeof(uint64_t)); - fs.write(reinterpret_cast(&packedFlag), sizeof(uint32_t)); - } - template void FontAtlasGenerator::InitFreetypeFromFile(FreetypeHandle*& ft, FontHandle*& font, const std::string& fontFile) @@ -217,13 +142,12 @@ namespace OpenVulkano::Scene generator.generate(glyphsGeometry.data(), glyphsGeometry.size()); int idx = 0; + SetupAtlasData(Math::Vector3ui(width, height, 1), fontGeometry.getMetrics().lineHeight, + channelsCount == 1 ? FontAtlasType::SDF : FontAtlasType::MSDF); + if constexpr (Channels == 3) { // store RGB as RGBA - m_atlasData->img = std::make_unique(); - m_atlasData->img->data = Array(width * height * 4); - m_atlasData->img->resolution = Math::Vector3ui(width, height, 1); - m_atlasData->img->dataFormat = OpenVulkano::DataFormat::R8G8B8A8_UNORM; const BitmapConstRef storage = generator.atlasStorage(); msdfgen::byte* data = static_cast(m_atlasData->img->data.Data()); for (size_t srcPos = 0, dstPos = 0; srcPos < width * height * 3; srcPos += 3, dstPos += 4) @@ -236,21 +160,10 @@ namespace OpenVulkano::Scene } else { - m_atlasData->img = std::make_unique(); - m_atlasData->img->data = Array(width * height); - m_atlasData->img->resolution = Math::Vector3ui(width, height, 1); - m_atlasData->img->dataFormat = OpenVulkano::DataFormat::R8_UNORM; const msdfgen::BitmapConstRef& storage = generator.atlasStorage(); memcpy(m_atlasData->img->data.Data(), storage.pixels, width * height); } - m_atlasData->texture.resolution = m_atlasData->img->resolution; - m_atlasData->texture.textureBuffer = m_atlasData->img->data.Data(); - m_atlasData->texture.format = m_atlasData->img->dataFormat; - m_atlasData->texture.size = width * height * channelsCount; - m_atlasData->meta.lineHeight = fontGeometry.getMetrics().lineHeight; - m_atlasData->meta.atlasType = channelsCount == 1 ? FontAtlasType::SDF : FontAtlasType::MSDF; - struct Bbox { double l = 0, r = 0, t = 0, b = 0; @@ -274,31 +187,12 @@ namespace OpenVulkano::Scene double t = glyphAtlasBbox.t; double b = glyphAtlasBbox.b; - info.xyz[0].x = bearingX; - info.xyz[0].y = h - bearingY; - info.xyz[0].z = 0; - info.uv[0].x = l / m_atlasData->texture.resolution.x; - info.uv[0].y = b / m_atlasData->texture.resolution.y; - - info.xyz[1].x = bearingX + w; - info.xyz[1].y = h - bearingY; - info.xyz[1].z = 0; - info.uv[1].x = r / m_atlasData->texture.resolution.x; - info.uv[1].y = b / m_atlasData->texture.resolution.y; - - info.xyz[2].x = bearingX + w; - info.xyz[2].y = bearingY; //h - bearingY + h; - info.xyz[2].z = 0; - info.uv[2].x = r / m_atlasData->texture.resolution.x; - info.uv[2].y = t / m_atlasData->texture.resolution.y; - - info.xyz[3].x = bearingX; - info.xyz[3].y = bearingY; - info.xyz[3].z = 0; - info.uv[3].x = l / m_atlasData->texture.resolution.x; - info.uv[3].y = t / m_atlasData->texture.resolution.y; - - info.advance = glyphBox.advance; + Math::AABB glyphAtlasAABB; + glyphAtlasAABB.min.x = l; + glyphAtlasAABB.min.y = b; + glyphAtlasAABB.max.x = r; + glyphAtlasAABB.max.y = t; + SetGlyphData(info, { bearingX, bearingY }, { w, h }, glyphAtlasAABB, glyphBox.advance); } if (pngOutput && !pngOutput->empty()) { SavePng(pngOutput.value()); } @@ -306,21 +200,6 @@ namespace OpenVulkano::Scene deinitializeFreetype(ft); } - template - void FontAtlasGenerator::SavePng(const std::string& output) const - { - stbi_flip_vertically_on_write(1); - if (std::filesystem::path(output).extension() == ".png") - { - stbi_write_png(output.c_str(), m_atlasData->img->resolution.x, m_atlasData->img->resolution.y, - channelsCount, m_atlasData->img->data.Data(), channelsCount * m_atlasData->img->resolution.x); - } - else - { - stbi_write_png((output + ".png").c_str(), m_atlasData->img->resolution.x, m_atlasData->img->resolution.y, - channelsCount, m_atlasData->img->data.Data(), channelsCount * m_atlasData->img->resolution.x); - } - } template class FontAtlasGenerator<1>; template class FontAtlasGenerator<3>; } diff --git a/openVulkanoCpp/Scene/FontAtlasGenerator.hpp b/openVulkanoCpp/Scene/FontAtlasGenerator.hpp index bac326c..1ae7863 100644 --- a/openVulkanoCpp/Scene/FontAtlasGenerator.hpp +++ b/openVulkanoCpp/Scene/FontAtlasGenerator.hpp @@ -8,9 +8,7 @@ #if __has_include("msdfgen.h") -#include "Scene/AtlasData.hpp" -#include "IFontAtlasGenerator.hpp" -#include "Scene/Texture.hpp" +#include "FontAtlasGeneratorBase.hpp" #include #include #include @@ -32,7 +30,7 @@ namespace OpenVulkano::Scene }; template - class FontAtlasGenerator : public IFontAtlasGenerator + class FontAtlasGenerator : public FontAtlasGeneratorBase { private: using SdfGenerator = msdf_atlas::ImmediateAtlasGenerator::type; using Config = FontAtlasGeneratorConfig; static constexpr int channelsCount = (Channels == 1 ? 1 : 4); - static msdf_atlas::Charset LoadAllGlyphs(const std::variant>& data); FontAtlasGenerator(); void GenerateAtlas(const std::string& fontFile, const std::set& charset, const std::optional& pngOutput = std::nullopt) override; - void GenerateAtlas(const Array& fontData, int length, const std::set& charset, + void GenerateAtlas(const Array& fontData, const std::set& charset, const std::optional& pngOutput = std::nullopt) override; void GenerateAtlas(const std::string& fontFile, const msdf_atlas::Charset& charset = msdf_atlas::Charset::ASCII, const std::optional& pngOutput = std::nullopt); void GenerateAtlas(const msdfgen::byte* fontData, int length, const msdf_atlas::Charset& charset = msdf_atlas::Charset::ASCII, const std::optional& pngOutput = std::nullopt); - void SaveAtlasMetadataInfo(const std::string& outputFile, bool packIntoSingleFile = true) const override; void SetGeneratorConfig(const Config& config) { m_config = config; } - std::shared_ptr GetAtlasData() const { return m_atlasData; } Config& GetGeneratorConfig() { return m_config; } private: void InitFreetypeFromFile(msdfgen::FreetypeHandle*& ft, msdfgen::FontHandle*& font, const std::string& file); @@ -64,11 +59,8 @@ namespace OpenVulkano::Scene const msdfgen::byte* fontData, int length); void Generate(msdfgen::FreetypeHandle* ft, msdfgen::FontHandle* font, const msdf_atlas::Charset& chset, const std::optional& pngOutput); - void SavePng(const std::string& output) const; - private: Config m_config; - std::shared_ptr m_atlasData; }; using SdfFontAtlasGenerator = FontAtlasGenerator<1>; using MsdfFontAtlasGenerator = FontAtlasGenerator<3>; diff --git a/openVulkanoCpp/Scene/FontAtlasGeneratorBase.cpp b/openVulkanoCpp/Scene/FontAtlasGeneratorBase.cpp new file mode 100644 index 0000000..e9528fd --- /dev/null +++ b/openVulkanoCpp/Scene/FontAtlasGeneratorBase.cpp @@ -0,0 +1,180 @@ +/* + * 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 "FontAtlasGeneratorBase.hpp" +#include "Base/Logger.hpp" +#define STBI_MSC_SECURE_CRT +#define STB_IMAGE_WRITE_IMPLEMENTATION +#include +#include +#include +#include + +namespace OpenVulkano::Scene +{ + std::pair + FontAtlasGeneratorBase::InitFreetype(const std::variant>& source) + { + FT_Library library; + auto error = FT_Init_FreeType(&library); + if (error) + { + throw std::runtime_error("Could not initalize freetype library\n"); + } + FT_Face face; + if (std::holds_alternative(source)) + { + error = FT_New_Face(library, std::get<0>(source).c_str(), 0, &face); + } + else + { + auto& arr = std::get<1>(source); + error = FT_New_Memory_Face(library, (const FT_Byte*) (arr.Data()), arr.Size(), 0, &face); + } + if (error == FT_Err_Unknown_File_Format) + { + throw std::runtime_error("Unknown font file format\n"); + } + else if (error) + { + throw std::runtime_error("Font file could not be opened or read or it's corrupted\n"); + } + + // some fancy font without unicode charmap + if (face->charmap == nullptr) + { + throw std::runtime_error("Selected font doesn't contain unicode charmap"); + } + return std::make_pair(FT_LIB_REC(library), FT_FACE_REC(face)); + } + + + void FontAtlasGeneratorBase::SaveAtlasMetadataInfo(const std::string& outputFile, bool packIntoSingleFile) const + { + if (m_atlasData->glyphs.empty()) + { + Logger::DATA->info("No glyphs loaded. Nothing to save."); + return; + } + std::string fileName = outputFile; + uint32_t packedFlag = packIntoSingleFile; + if (packIntoSingleFile) + { + std::filesystem::path fPath(fileName); + fileName = (fPath.parent_path() / fPath.stem()).string() + "_packed.png"; + SavePng(fileName); + } + std::fstream fs(fileName.c_str(), std::ios_base::out | std::ios_base::binary | (packedFlag ? std::ios_base::app : std::ios_base::trunc)); + fs.write(reinterpret_cast(&m_atlasData->meta), sizeof(AtlasMetadata)); + uint64_t metadataBytes = sizeof(AtlasMetadata); + for (const auto& [key, val] : m_atlasData->glyphs) + { + fs.write(reinterpret_cast(&key), sizeof(uint32_t)); + fs.write(reinterpret_cast(&val), sizeof(GlyphInfo)); + metadataBytes += sizeof(uint32_t); + metadataBytes += sizeof(GlyphInfo); + } + fs.write(reinterpret_cast(&metadataBytes), sizeof(uint64_t)); + fs.write(reinterpret_cast(&packedFlag), sizeof(uint32_t)); + } + + void FontAtlasGeneratorBase::SavePng(const std::string& output) const + { + stbi_flip_vertically_on_write(1); + if (std::filesystem::path(output).extension() == ".png") + { + stbi_write_png(output.c_str(), m_atlasData->img->resolution.x, m_atlasData->img->resolution.y, m_channelsCount, m_atlasData->img->data.Data(), + m_channelsCount * m_atlasData->img->resolution.x); + } + else + { + stbi_write_png((output + ".png").c_str(), m_atlasData->img->resolution.x, m_atlasData->img->resolution.y, + m_channelsCount, m_atlasData->img->data.Data(), m_channelsCount * m_atlasData->img->resolution.x); + } + } + + void FontAtlasGeneratorBase::SetupAtlasData(Math::Vector2ui textureResolution, double lineHeight, + FontAtlasType::Type atlasType) + { + // generate texture + if (m_channelsCount == 1) + { + m_atlasData->img = std::make_unique(); + m_atlasData->img->data = Array(textureResolution.x * textureResolution.y); + m_atlasData->img->resolution = Math::Vector3ui(textureResolution, 1); + m_atlasData->img->dataFormat = OpenVulkano::DataFormat::R8_UNORM; + } + else + { + m_atlasData->img = std::make_unique(); + // RGBA + m_atlasData->img->data = Array(textureResolution.x * textureResolution.y * 4); + m_atlasData->img->resolution = Math::Vector3ui(textureResolution, 1); + m_atlasData->img->dataFormat = OpenVulkano::DataFormat::R8G8B8A8_UNORM; + } + m_atlasData->texture.resolution = m_atlasData->img->resolution; + m_atlasData->texture.textureBuffer = m_atlasData->img->data.Data(); + m_atlasData->texture.format = m_atlasData->img->dataFormat; + m_atlasData->texture.size = m_atlasData->img->data.Size(); + m_atlasData->texture.m_samplerConfig = &SamplerConfig::NEAREST; + m_atlasData->meta.atlasType = atlasType; + m_atlasData->meta.lineHeight = lineHeight; + } + + void FontAtlasGeneratorBase::SetGlyphData(GlyphInfo& info, Math::Vector2d bearing, Math::Vector2d size, + const Math::AABB& aabb, double advance) + { + double bearingX = bearing.x; + double bearingY = bearing.y; + double w = size.x; + double h = size.y; + double l = aabb.min.x; + double r = aabb.max.x; + double t = aabb.max.y; + double b = aabb.min.y; + + info.xyz[0].x = bearingX; + info.xyz[0].y = h - bearingY; + info.xyz[0].z = 0; + info.uv[0].x = l / m_atlasData->texture.resolution.x; + info.uv[0].y = b / m_atlasData->texture.resolution.y; + + info.xyz[1].x = bearingX + w; + info.xyz[1].y = h - bearingY; + info.xyz[1].z = 0; + info.uv[1].x = r / m_atlasData->texture.resolution.x; + info.uv[1].y = b / m_atlasData->texture.resolution.y; + + info.xyz[2].x = bearingX + w; + info.xyz[2].y = bearingY; //h - bearingY + h; + info.xyz[2].z = 0; + info.uv[2].x = r / m_atlasData->texture.resolution.x; + info.uv[2].y = t / m_atlasData->texture.resolution.y; + + info.xyz[3].x = bearingX; + info.xyz[3].y = bearingY; + info.xyz[3].z = 0; + info.uv[3].x = l / m_atlasData->texture.resolution.x; + info.uv[3].y = t / m_atlasData->texture.resolution.y; + + info.advance = advance; + } + + std::set FontAtlasGeneratorBase::LoadAllGlyphs(const std::variant>& data) + { + const auto& [lib, face] = InitFreetype(data); + std::set s; + FT_UInt glyphIndex; + FT_ULong unicode = FT_Get_First_Char(face.get(), &glyphIndex); + while (glyphIndex != 0) + { + s.insert(unicode); + unicode = FT_Get_Next_Char(face.get(), unicode, &glyphIndex); + } + return s; + } + +} diff --git a/openVulkanoCpp/Scene/FontAtlasGeneratorBase.hpp b/openVulkanoCpp/Scene/FontAtlasGeneratorBase.hpp new file mode 100644 index 0000000..e1e1ea7 --- /dev/null +++ b/openVulkanoCpp/Scene/FontAtlasGeneratorBase.hpp @@ -0,0 +1,56 @@ +/* + * 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 "IFontAtlasGenerator.hpp" +#include "Math/AABB.hpp" +#include +#include FT_FREETYPE_H +#include +#include + +namespace OpenVulkano::Scene +{ + class FontAtlasGeneratorBase : public IFontAtlasGenerator + { + struct LibDeleter + { + void operator()(FT_Library lib) + { + FT_Done_FreeType(lib); + } + }; + + struct FaceDeleter + { + void operator()(FT_Face face) + { + FT_Done_Face(face); + } + }; + + public: + using FT_LIB_REC = std::unique_ptr; + using FT_FACE_REC = std::unique_ptr; + + FontAtlasGeneratorBase(int channelsCount) : m_channelsCount(channelsCount) {} + void SaveAtlasMetadataInfo(const std::string& outputFile, bool packIntoSingleFile = true) const override; + std::shared_ptr GetAtlasData() const { return m_atlasData; } + int GetAtlasChannelsCount() const { return m_channelsCount; } + + static std::set LoadAllGlyphs(const std::variant>& data); + + protected: + void SavePng(const std::string& output) const; + void SetupAtlasData(Math::Vector2ui textureResolution, double lineHeight, FontAtlasType::Type atlasType); + void SetGlyphData(GlyphInfo& info, Math::Vector2d bearing, Math::Vector2d size, const Math::AABB& aabb, double advance); + static std::pair InitFreetype(const std::variant>& source); + protected: + int m_channelsCount; + std::shared_ptr m_atlasData; + }; +} \ No newline at end of file diff --git a/openVulkanoCpp/Scene/IFontAtlasGenerator.hpp b/openVulkanoCpp/Scene/IFontAtlasGenerator.hpp index bd641c1..cdb4094 100644 --- a/openVulkanoCpp/Scene/IFontAtlasGenerator.hpp +++ b/openVulkanoCpp/Scene/IFontAtlasGenerator.hpp @@ -21,7 +21,7 @@ namespace OpenVulkano::Scene public: virtual void GenerateAtlas(const std::string& fontFile, const std::set& charset, const std::optional& pngOutput = std::nullopt) = 0; - virtual void GenerateAtlas(const Array& fontData, int length, const std::set& charset, + virtual void GenerateAtlas(const Array& fontData, const std::set& charset, const std::optional& pngOutput = std::nullopt) = 0; virtual void SaveAtlasMetadataInfo(const std::string& outputFile, bool packIntoSingleFile = true) const = 0; virtual std::shared_ptr GetAtlasData() const = 0; diff --git a/openVulkanoCpp/Scene/TextDrawable.cpp b/openVulkanoCpp/Scene/TextDrawable.cpp index be09db9..2f083aa 100644 --- a/openVulkanoCpp/Scene/TextDrawable.cpp +++ b/openVulkanoCpp/Scene/TextDrawable.cpp @@ -26,8 +26,8 @@ namespace OpenVulkano::Scene static Shader sdfDefaultShader; if (once) { - sdfDefaultShader.AddShaderProgram(OpenVulkano::ShaderProgramType::VERTEX, "Shader/text"); - sdfDefaultShader.AddShaderProgram(OpenVulkano::ShaderProgramType::FRAGMENT, "Shader/text"); + sdfDefaultShader.AddShaderProgram(OpenVulkano::ShaderProgramType::VERTEX, "Shader/sdfText"); + sdfDefaultShader.AddShaderProgram(OpenVulkano::ShaderProgramType::FRAGMENT, "Shader/sdfText"); sdfDefaultShader.AddVertexInputDescription(OpenVulkano::Vertex::GetVertexInputDescription()); sdfDefaultShader.AddDescriptorSetLayoutBinding(Texture::DESCRIPTOR_SET_LAYOUT_BINDING); DescriptorSetLayoutBinding desc = UniformBuffer::DESCRIPTOR_SET_LAYOUT_BINDING; @@ -46,7 +46,7 @@ namespace OpenVulkano::Scene static Shader msdfDefaultShader; if (once) { - msdfDefaultShader.AddShaderProgram(OpenVulkano::ShaderProgramType::VERTEX, "Shader/text"); + msdfDefaultShader.AddShaderProgram(OpenVulkano::ShaderProgramType::VERTEX, "Shader/sdfText"); msdfDefaultShader.AddShaderProgram(OpenVulkano::ShaderProgramType::FRAGMENT, "Shader/msdfText"); msdfDefaultShader.AddVertexInputDescription(OpenVulkano::Vertex::GetVertexInputDescription()); msdfDefaultShader.AddDescriptorSetLayoutBinding(Texture::DESCRIPTOR_SET_LAYOUT_BINDING); @@ -60,6 +60,26 @@ namespace OpenVulkano::Scene return msdfDefaultShader; } + Shader& TextDrawable::GetBitmapDefaultShader() + { + static bool once = true; + static Shader bitmapDefaultShader; + if (once) + { + bitmapDefaultShader.AddShaderProgram(OpenVulkano::ShaderProgramType::VERTEX, "Shader/text"); + bitmapDefaultShader.AddShaderProgram(OpenVulkano::ShaderProgramType::FRAGMENT, "Shader/text"); + bitmapDefaultShader.AddVertexInputDescription(OpenVulkano::Vertex::GetVertexInputDescription()); + bitmapDefaultShader.AddDescriptorSetLayoutBinding(Texture::DESCRIPTOR_SET_LAYOUT_BINDING); + DescriptorSetLayoutBinding desc = UniformBuffer::DESCRIPTOR_SET_LAYOUT_BINDING; + desc.stageFlags = ShaderProgramType::FRAGMENT; + bitmapDefaultShader.AddDescriptorSetLayoutBinding(desc); + bitmapDefaultShader.alphaBlend = true; + bitmapDefaultShader.cullMode = CullMode::NONE; + once = false; + } + return bitmapDefaultShader; + } + TextDrawable::TextDrawable(const TextConfig& config) { m_cfg = config; @@ -248,6 +268,10 @@ namespace OpenVulkano::Scene ++i; } m_bbox.Init(bmin, bmax); + if (m_atlasData->meta.atlasType == FontAtlasType::BITMAP) + { + m_material.texture->m_samplerConfig = &SamplerConfig::NEAREST; + } SimpleDrawable::Init(m_shader, &m_geometry, &m_material, &m_uniBuffer); } diff --git a/openVulkanoCpp/Scene/TextDrawable.hpp b/openVulkanoCpp/Scene/TextDrawable.hpp index 3896651..f8d6e3f 100644 --- a/openVulkanoCpp/Scene/TextDrawable.hpp +++ b/openVulkanoCpp/Scene/TextDrawable.hpp @@ -38,6 +38,7 @@ namespace OpenVulkano::Scene public: static Shader& GetSdfDefaultShader(); static Shader& GetMsdfDefaultShader(); + static Shader& GetBitmapDefaultShader(); TextDrawable(const TextConfig& config = TextConfig()); TextDrawable(const Array& atlasMetadata, const TextConfig& config = TextConfig()); TextDrawable(const std::string& atlasMetadataFile, const TextConfig& config = TextConfig()); diff --git a/openVulkanoCpp/Shader/sdfText.frag b/openVulkanoCpp/Shader/sdfText.frag new file mode 100644 index 0000000..579d9c7 --- /dev/null +++ b/openVulkanoCpp/Shader/sdfText.frag @@ -0,0 +1,39 @@ +#version 450 + +layout(location = 1) in vec2 texCoord; + +layout(location = 0) out vec4 outColor; + +layout(set = 2, binding = 0) uniform sampler2D texSampler; + +layout(set = 3, binding = 0) uniform TextConfig +{ + vec4 textColor; + vec4 borderColor; + vec4 backgroundColor; + float threshold; + float borderSize; + float smoothing; + bool applyBorder; +} textConfig; + +void main() +{ + float distance = texture(texSampler, texCoord).r; + float alpha = smoothstep(textConfig.threshold - textConfig.smoothing, textConfig.threshold + textConfig.smoothing, distance); + if (textConfig.applyBorder) + { + float border = smoothstep(textConfig.threshold + textConfig.borderSize - textConfig.smoothing, + textConfig.threshold + textConfig.borderSize + textConfig.smoothing, distance); + outColor = mix(textConfig.borderColor, textConfig.textColor, border) * alpha; + } + else + { + outColor = vec4(textConfig.textColor) * alpha; + } + + if (textConfig.backgroundColor.a != 0) + { + outColor = mix(textConfig.backgroundColor, outColor, alpha); + } +} diff --git a/openVulkanoCpp/Shader/sdfText.vert b/openVulkanoCpp/Shader/sdfText.vert new file mode 100644 index 0000000..c259c65 --- /dev/null +++ b/openVulkanoCpp/Shader/sdfText.vert @@ -0,0 +1,26 @@ +#version 450 +layout(location = 0) in vec3 position; +layout(location = 1) in vec3 normal; +layout(location = 2) in vec3 tangent; +layout(location = 3) in vec3 biTangent; +layout(location = 4) in vec3 textureCoordinates; +layout(location = 5) in vec4 color; +layout(location = 1) out vec2 fragTextureCoordinates; + +layout(set = 0, binding = 0) uniform NodeData +{ + mat4 world; +} node; + +layout(set = 1, binding = 0) uniform CameraData +{ + mat4 viewProjection; + mat4 view; + mat4 projection; + vec4 camPos; +} cam; + +void main() { + gl_Position = cam.viewProjection * node.world * vec4(position, 1.0); + fragTextureCoordinates.xy = textureCoordinates.xy; +} diff --git a/openVulkanoCpp/Shader/text.frag b/openVulkanoCpp/Shader/text.frag index 5333c6f..c960875 100644 --- a/openVulkanoCpp/Shader/text.frag +++ b/openVulkanoCpp/Shader/text.frag @@ -1,5 +1,6 @@ #version 450 +layout(location = 0) in vec4 color; layout(location = 1) in vec2 texCoord; layout(location = 0) out vec4 outColor; @@ -12,28 +13,19 @@ layout(set = 3, binding = 0) uniform TextConfig vec4 borderColor; vec4 backgroundColor; float threshold; - float borderSize; - float smoothing; - bool applyBorder; + float borderSize; + float smoothing; + bool applyBorder; } textConfig; void main() { - float distance = texture(texSampler, texCoord).r; - float alpha = smoothstep(textConfig.threshold - textConfig.smoothing, textConfig.threshold + textConfig.smoothing, distance); - if (textConfig.applyBorder) - { - float border = smoothstep(textConfig.threshold + textConfig.borderSize - textConfig.smoothing, - textConfig.threshold + textConfig.borderSize + textConfig.smoothing, distance); - outColor = mix(textConfig.borderColor, textConfig.textColor, border) * alpha; - } - else - { - outColor = vec4(textConfig.textColor) * alpha; - } - - if (textConfig.backgroundColor.a != 0) - { - outColor = mix(textConfig.backgroundColor, outColor, alpha); - } + // interesting results + //float distance = texture(texSampler, texCoord).r; + //float alpha = smoothstep(textConfig.threshold - textConfig.smoothing, textConfig.threshold + textConfig.smoothing, distance); + //outColor = vec4(textConfig.textColor) * alpha; + + + vec4 sampled = vec4(1.0, 1.0, 1.0, texture(texSampler, texCoord).r); + outColor = vec4(textConfig.textColor) * sampled; } diff --git a/openVulkanoCpp/Shader/text.vert b/openVulkanoCpp/Shader/text.vert index 1e9ef24..47f4b61 100644 --- a/openVulkanoCpp/Shader/text.vert +++ b/openVulkanoCpp/Shader/text.vert @@ -5,6 +5,8 @@ layout(location = 2) in vec3 tangent; layout(location = 3) in vec3 biTangent; layout(location = 4) in vec3 textureCoordinates; layout(location = 5) in vec4 color; + +layout(location = 0) out vec4 outColor; layout(location = 1) out vec2 fragTextureCoordinates; layout(set = 0, binding = 0) uniform NodeData @@ -23,4 +25,5 @@ layout(set = 1, binding = 0) uniform CameraData void main() { gl_Position = cam.viewProjection * node.world * vec4(position, 1.0); fragTextureCoordinates.xy = textureCoordinates.xy; + outColor = color; }