diff --git a/3rdParty/glfw/CMakeLists.txt b/3rdParty/glfw/CMakeLists.txt index 030c155..632e74e 100644 --- a/3rdParty/glfw/CMakeLists.txt +++ b/3rdParty/glfw/CMakeLists.txt @@ -13,6 +13,7 @@ FetchContent_Declare( ) set(GLFW_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) set(GLFW_BUILD_TESTS OFF CACHE BOOL "" FORCE) +set(GLFW_BUILD_DOCS OFF CACHE BOOL "" FORCE) if(APPLE) set(GLFW_VULKAN_STATIC ON CACHE BOOL "" FORCE) endif() diff --git a/3rdParty/msdf/CMakeLists.txt b/3rdParty/msdf/CMakeLists.txt index e7f7849..15e8db4 100644 --- a/3rdParty/msdf/CMakeLists.txt +++ b/3rdParty/msdf/CMakeLists.txt @@ -36,6 +36,12 @@ if (ENABLE_MSDF) set(FT_SRC_DIR "${CMAKE_BINARY_DIR}/_deps/freetype-src") set(FT_BUILD_DIR "${FT_SRC_DIR}/build") + + set(FREETYPE_CONFIG_FILE "${FT_SRC_DIR}/include/freetype/config/ftoption.h") + file(READ "${FREETYPE_CONFIG_FILE}" FILE_CONTENTS) + string(REPLACE "/* #define FT_CONFIG_OPTION_SUBPIXEL_RENDERING */" "#define FT_CONFIG_OPTION_SUBPIXEL_RENDERING" FILE_CONTENTS "${FILE_CONTENTS}") + file(WRITE "${FREETYPE_CONFIG_FILE}" "${FILE_CONTENTS}") + file(MAKE_DIRECTORY ${FT_BUILD_DIR}) if (IOS) set(PLATFORM_CFG -DCMAKE_TOOLCHAIN_FILE=${CMAKE_CURRENT_SOURCE_DIR}/patched_freetype_iOS_toolchain.cmake) diff --git a/examples/ExampleApps/TextExampleApp.cpp b/examples/ExampleApps/TextExampleApp.cpp index 871b57c..e286b87 100644 --- a/examples/ExampleApps/TextExampleApp.cpp +++ b/examples/ExampleApps/TextExampleApp.cpp @@ -56,17 +56,18 @@ namespace OpenVulkano texts.push_back(std::make_pair("This is first line\nSecond gg line\nThird G line", TextConfig())); texts[1].second.backgroundColor.a = 255; - const int N = texts.size(); + constexpr int atlasesCount = 4; + const int textsCount = texts.size(); auto& resourceLoader = ResourceLoader::GetInstance(); const std::string fontPath = resourceLoader.GetResourcePath("Roboto-Regular.ttf"); - m_nodesPool.resize(N * 3); - m_drawablesPool.resize(N * 3); + m_nodesPool.resize(textsCount * atlasesCount); + m_drawablesPool.resize(textsCount * atlasesCount); if constexpr (CREATE_BITMAP_ATLAS) { // ReSharper disable once CppDFAUnreachableCode std::set s = BitmapFontAtlasGenerator::LoadAllGlyphs(fontPath); - BitmapFontAtlasGenerator generator; + BitmapFontAtlasGenerator generator(FontPixelSizeConfig(), SubpixelLayout::RGB); generator.GenerateAtlas(fontPath, s); generator.GetAtlas()->Save("bitmap_atlas_packed.png"); } @@ -81,63 +82,73 @@ namespace OpenVulkano auto sdfMetadataInfo = resourceLoader.GetResource("sdf_atlas_packed.png"); auto msdfMetadataInfo = resourceLoader.GetResource("msdf_atlas_packed.png"); auto bitmapMetadataInfo = resourceLoader.GetResource("bitmap_atlas_packed.png"); + auto bitmapSubpixelRenderingMetadataInfo = resourceLoader.GetResource("bitmap_subpixel_atlas_packed.png"); #endif - for (int i = 0; i < texts.size() * 3; i++) + for (int i = 0, xOffset = -5; i < atlasesCount; i++, xOffset += 20) { - int textIdx = i % texts.size(); - TextDrawable* t = nullptr; + for (int j = 0; j < texts.size(); j++) + { + TextDrawable* t = nullptr; #if defined(MSDFGEN_AVAILABLE) && CREATE_NEW_ATLAS - if (i < texts.size()) - { - t = new TextDrawable(m_atlasGenerator.GetAtlasData(), texts[textIdx].second); - } - else - { - t = new TextDrawable(m_msdfAtlasGenerator.GetAtlasData(), texts[textIdx].second); - } + if (i < texts.size()) + { + t = new TextDrawable(m_atlasGenerator.GetAtlasData(), texts[j].second); + t->SetShader(&TextDrawable::GetSdfDefaultShader()); + } + else + { + t = new TextDrawable(m_msdfAtlasGenerator.GetAtlasData(), texts[j].second); + t->SetShader(&TextDrawable::GetMsdfDefaultShader()); + } #else - int xOffset = 0; - if (i < N) - { - t = new TextDrawable(sdfMetadataInfo, texts[textIdx].second); - xOffset = -5; + if (i == 0) + { + t = new TextDrawable(sdfMetadataInfo, texts[j].second); + } + else if (i == 1) + { + t = new TextDrawable(msdfMetadataInfo, texts[j].second); + } + else if (i == 2) + { + // bitmap + t = new TextDrawable(bitmapMetadataInfo, texts[j].second); + } + else if (i == 3) + { + // bitmap subpixel rendering + t = new TextDrawable(bitmapSubpixelRenderingMetadataInfo, texts[j].second); + } + // OR use separate texture + metadata file + //auto metadataInfo = resourceLoader.GetResource("atlas_metadata"); + //auto data = resourceLoader.GetResource("roboto-regular-atlas.png"); + //Image::ImageLoaderPng loader; + //static auto image = loader.loadData(reinterpret_cast(data.Data()), data.Size()); + //static Texture tex; + //tex.resolution = image->resolution; + //tex.textureBuffer = image->data.Data(); + //tex.format = image->dataFormat; + //tex.size = image->data.Size(); // 1 channel + //TextDrawable* t = new TextDrawable(metadataInfo, &tex, texts[i].second); +#endif // MSDFGEN_AVAILABLE + const int nodeIdx = i * texts.size() + j; + t->GenerateText(texts[j].first); + m_drawablesPool[nodeIdx].reset(t); + m_nodesPool[nodeIdx].Init(); + m_nodesPool[nodeIdx].SetMatrix( + Math::Utils::translate(glm::mat4x4(1.f), Vector3f(xOffset, 2 - j * 2, 0))); + m_nodesPool[nodeIdx].AddDrawable(m_drawablesPool[nodeIdx].get()); + m_scene.GetRoot()->AddChild(&m_nodesPool[nodeIdx]); } - else if (i >= N && i < N * 2) - { - t = new TextDrawable(msdfMetadataInfo, texts[textIdx].second); - xOffset = 15; - } - else - { - t = new TextDrawable(bitmapMetadataInfo, texts[textIdx].second); - xOffset = 35; - } - // OR use separate texture + metadata file - //auto metadataInfo = resourceLoader.GetResource("atlas_metadata"); - //auto data = resourceLoader.GetResource("roboto-regular-atlas.png"); - //Image::ImageLoaderPng loader; - //static auto image = loader.loadData(reinterpret_cast(data.Data()), data.Size()); - //static Texture tex; - //tex.resolution = image->resolution; - //tex.textureBuffer = image->data.Data(); - //tex.format = image->dataFormat; - //tex.size = image->data.Size(); // 1 channel - //TextDrawable* t = new TextDrawable(metadataInfo, &tex, texts[i].second); -#endif // MSDFGEN_AVAILABLE - t->GenerateText(texts[textIdx].first); - m_drawablesPool[i].reset(t); - m_nodesPool[i].Init(); - m_nodesPool[i].SetMatrix(Math::Utils::translate(glm::mat4x4(1.f), Vector3f(xOffset, 2 - textIdx * 2, 0))); - m_nodesPool[i].AddDrawable(m_drawablesPool[i].get()); - m_scene.GetRoot()->AddChild(&m_nodesPool[i]); } + GetGraphicsAppManager()->GetRenderer()->SetScene(&m_scene); m_camController.Init(&m_cam); m_camController.SetDefaultKeybindings(); m_camController.SetPosition({ 10, 0, 15 }); m_camController.SetBoostFactor(5); - + std::shared_ptr m_perfInfo = std::make_shared(); m_ui.AddElement(m_perfInfo); diff --git a/examples/ExampleSources/bitmap_subpixel_atlas_packed.png b/examples/ExampleSources/bitmap_subpixel_atlas_packed.png new file mode 100644 index 0000000..344c8b6 Binary files /dev/null and b/examples/ExampleSources/bitmap_subpixel_atlas_packed.png differ diff --git a/openVulkanoCpp/Base/EngineConfiguration.hpp b/openVulkanoCpp/Base/EngineConfiguration.hpp index 61ef440..ffe7053 100644 --- a/openVulkanoCpp/Base/EngineConfiguration.hpp +++ b/openVulkanoCpp/Base/EngineConfiguration.hpp @@ -10,6 +10,7 @@ #include #include #include "Base/Event.hpp" +#include "Scene/SubpixelLayout.hpp" #undef max namespace OpenVulkano @@ -34,6 +35,9 @@ namespace OpenVulkano [[nodiscard]] bool GetVSync() const { return m_vSync; } void SetVSync(bool vSync) { m_vSync = vSync; } + [[nodiscard]] SubpixelLayout GetSubpixelLayout() const { return m_subpixelLayout; } + void SetSubpixelLayout(SubpixelLayout subpixelLayout) { m_subpixelLayout = subpixelLayout; } + [[nodiscard]] int32_t GetFpsCap() const { return m_fpsCap; } void SetFpsCap(int32_t fpsCap) { @@ -58,6 +62,7 @@ namespace OpenVulkano bool m_preferFramebufferFormatSRGB = true; bool m_lazyRendering = false; bool m_vSync = false; + SubpixelLayout m_subpixelLayout = SubpixelLayout::AUTO; int32_t m_fpsCap = -1; // -1 = no fps cap. 0 = fps cap if vsync, no cap otherwise. > 0 = set fps cap #ifdef __APPLE__ uint32_t m_preferredImageCount = 3; diff --git a/openVulkanoCpp/Base/UI/IWindow.hpp b/openVulkanoCpp/Base/UI/IWindow.hpp index 4b7c868..4499ccc 100644 --- a/openVulkanoCpp/Base/UI/IWindow.hpp +++ b/openVulkanoCpp/Base/UI/IWindow.hpp @@ -8,6 +8,7 @@ #include "Math/Math.hpp" #include "Base/PlatformEnums.hpp" +#include "Scene/SubpixelLayout.hpp" #include #include @@ -89,6 +90,7 @@ namespace OpenVulkano virtual float GetContentScale() const { return 1; } virtual float GetInterfaceOrientation() const { return 0; } + virtual SubpixelLayout GetSubpixelLayout() const { return SubpixelLayout::UNKNOWN; } protected: static uint32_t CreateWindowId() { diff --git a/openVulkanoCpp/CMakeLists.txt b/openVulkanoCpp/CMakeLists.txt index 87fd36b..d2295be 100644 --- a/openVulkanoCpp/CMakeLists.txt +++ b/openVulkanoCpp/CMakeLists.txt @@ -60,8 +60,10 @@ SetShaderDependency(openVulkanoCpp ${SHADER_OUTPUT_DEST}) if (NOT ANDROID AND NOT IOS) - target_link_libraries(openVulkanoCpp PUBLIC glfw pugixml) - target_link_libraries(openVulkanoCpp PUBLIC ftxui::screen ftxui::dom ftxui::component) + if (LINUX) + target_link_libraries(openVulkanoCpp PUBLIC fontconfig) + endif() + target_link_libraries(openVulkanoCpp PUBLIC glfw pugixml ftxui::screen ftxui::dom ftxui::component) if (ENABLE_CURL) LinkCurl(openVulkanoCpp) endif() diff --git a/openVulkanoCpp/Host/GLFW/WindowGLFW.cpp b/openVulkanoCpp/Host/GLFW/WindowGLFW.cpp index b669695..303c876 100644 --- a/openVulkanoCpp/Host/GLFW/WindowGLFW.cpp +++ b/openVulkanoCpp/Host/GLFW/WindowGLFW.cpp @@ -6,13 +6,15 @@ #include "WindowGLFW.hpp" #include "Base/Logger.hpp" +#include "Base/EngineConfiguration.hpp" #include +#if __linux__ + #include +#endif namespace OpenVulkano::GLFW { - WindowGLFW::WindowGLFW(OpenVulkano::GLFW::InputProviderGLFW& inputProvider) - : inputProvider(inputProvider) - {} + WindowGLFW::WindowGLFW(OpenVulkano::GLFW::InputProviderGLFW& inputProvider) : inputProvider(inputProvider) {} WindowGLFW::~WindowGLFW() noexcept { @@ -31,8 +33,8 @@ namespace OpenVulkano::GLFW { int posX, posY, sizeX, sizeY; glfwGetMonitorWorkarea(monitors[i], &posX, &posY, &sizeX, &sizeY); - if (windowConfig.position.x >= posX && windowConfig.position.x < posX + sizeX && - windowConfig.position.y >= posY && windowConfig.position.y < posY + sizeY) + if (windowConfig.position.x >= posX && windowConfig.position.x < posX + sizeX + && windowConfig.position.y >= posY && windowConfig.position.y < posY + sizeY) { return monitors[i]; } @@ -56,8 +58,12 @@ namespace OpenVulkano::GLFW glfwWindowHint(GLFW_DECORATED, (~windowConfig.windowMode) & 1); glfwWindowHint(GLFW_TRANSPARENT_FRAMEBUFFER, windowConfig.transparentFrameBuffer); //TODO handle full screen resolutions - window = glfwCreateWindow(windowConfig.size.x, windowConfig.size.y, windowConfig.title.c_str(), GetTargetMonitor(), nullptr); - if (!window) return; + window = glfwCreateWindow(windowConfig.size.x, windowConfig.size.y, windowConfig.title.c_str(), + GetTargetMonitor(), nullptr); + if (!window) + { + return; + } float scaleX, scaleY; glfwGetWindowContentScale(window, &scaleX, &scaleY); contentScale = std::max(scaleX, scaleY); @@ -89,13 +95,9 @@ namespace OpenVulkano::GLFW { glfwSetInputMode(window, GLFW_RAW_MOUSE_MOTION, hideMouse); glfwSetInputMode(window, GLFW_CURSOR, hideMouse ? GLFW_CURSOR_DISABLED : GLFW_CURSOR_NORMAL); - } - GLFWmonitor* WindowGLFW::GetPrimaryMonitor() - { - return glfwGetPrimaryMonitor(); - } + GLFWmonitor* WindowGLFW::GetPrimaryMonitor() { return glfwGetPrimaryMonitor(); } std::vector WindowGLFW::GetMonitors() { @@ -112,13 +114,19 @@ namespace OpenVulkano::GLFW void WindowGLFW::Init(RenderAPI::RenderApi renderApi) { - if (renderApi == RenderAPI::Vulkan) glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); + if (renderApi == RenderAPI::Vulkan) + { + glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); + } Create(); if (!window) { throw WindowInitFailedException("Failed to initialize window"); } - if (renderApi != RenderAPI::Vulkan) MakeCurrentThread(); + if (renderApi != RenderAPI::Vulkan) + { + MakeCurrentThread(); + } Logger::WINDOW->info("GLFW Window created (id: {0}) with scale {1}", GetWindowId(), contentScale); } @@ -129,20 +137,11 @@ namespace OpenVulkano::GLFW Logger::WINDOW->info("GLFW Window destroyed (id: {0})", GetWindowId()); } - void WindowGLFW::Present() const - { - glfwSwapBuffers(window); - } + void WindowGLFW::Present() const { glfwSwapBuffers(window); } - void WindowGLFW::Show() - { - glfwShowWindow(window); - } + void WindowGLFW::Show() { glfwShowWindow(window); } - void WindowGLFW::Hide() - { - glfwHideWindow(window); - } + void WindowGLFW::Hide() { glfwHideWindow(window); } void WindowGLFW::SetTitle(const std::string& title) { @@ -175,7 +174,9 @@ namespace OpenVulkano::GLFW Math::Vector2ui WindowGLFW::GetSize() { if (currentSize.x == 0 || currentSize.y == 0) + { glfwGetWindowSize(window, reinterpret_cast(¤tSize.x), reinterpret_cast(¤tSize.y)); + } return currentSize; } @@ -195,6 +196,67 @@ namespace OpenVulkano::GLFW glfwSetWindowSizeLimits(window, minWidth, minHeight, maxWidth, maxHeight); } + SubpixelLayout WindowGLFW::GetSubpixelLayout() const + { + SubpixelLayout engineLayout = EngineConfiguration::GetEngineConfiguration()->GetSubpixelLayout(); + if (engineLayout != SubpixelLayout::UNKNOWN) + { + return engineLayout; + } +#if _WIN32 + BOOL val; + // check if font smoothing is enabled + SystemParametersInfoA(SPI_GETFONTSMOOTHING, 0, &val, 0); + if (!val) + { + return SubpixelLayout::UNKNOWN; + } + + UINT mode; + SystemParametersInfoA(SPI_GETFONTSMOOTHINGORIENTATION, 0, &mode, 0); + if (mode == FE_FONTSMOOTHINGORIENTATIONBGR) + { + return SubpixelLayout::BGR; + } + else if (mode == FE_FONTSMOOTHINGORIENTATIONRGB) + { + return SubpixelLayout::RGB; + } + return SubpixelLayout::UNKNOWN; +#elif __linux__ + FcInit(); + FcPattern* pattern = FcPatternCreate(); + FcConfigSubstitute(nullptr, pattern, FcMatchPattern); + FcDefaultSubstitute(pattern); + + FcResult result; + FcPattern* match = FcFontMatch(nullptr, pattern, &result); + if (!match) + { + Logger::WINDOW->error("Failed to match font pattern."); + FcPatternDestroy(pattern); + return SubpixelLayout::UNKNOWN; + } + + int subpixelOrder = -1; + if (FcPatternGetInteger(match, FC_RGBA, 0, &subpixelOrder) == FcResultMatch) { + switch (subpixelOrder) { + case FC_RGBA_RGB: return SubpixelLayout::RGB; + case FC_RGBA_BGR: return SubpixelLayout::BGR; + case FC_RGBA_VRGB: return SubpixelLayout::RGBV; + case FC_RGBA_VBGR: return SubpixelLayout::BGRV; + case FC_RGBA_NONE: + default: return SubpixelLayout::UNKNOWN; + } + } + FcPatternDestroy(match); + FcPatternDestroy(pattern); + return SubpixelLayout::UNKNOWN; +#else + return SubpixelLayout::UNKNOWN; +#endif + } + void WindowGLFW::MakeCurrentThread() { glfwMakeContextCurrent(window); diff --git a/openVulkanoCpp/Host/GLFW/WindowGLFW.hpp b/openVulkanoCpp/Host/GLFW/WindowGLFW.hpp index f7876ed..a14e86a 100644 --- a/openVulkanoCpp/Host/GLFW/WindowGLFW.hpp +++ b/openVulkanoCpp/Host/GLFW/WindowGLFW.hpp @@ -64,6 +64,8 @@ namespace OpenVulkano::GLFW void SetSizeLimits(int minWidth, int minHeight, int maxWidth, int maxHeight) override; + SubpixelLayout GetSubpixelLayout() const override; + [[nodiscard]] float GetContentScale() const override { return contentScale; } void MakeCurrentThread() override; diff --git a/openVulkanoCpp/Host/Windows/SystemInfo.cpp b/openVulkanoCpp/Host/Windows/SystemInfo.cpp index a7a9270..bea3c6b 100644 --- a/openVulkanoCpp/Host/Windows/SystemInfo.cpp +++ b/openVulkanoCpp/Host/Windows/SystemInfo.cpp @@ -424,7 +424,7 @@ namespace OpenVulkano static const std::string osName = "Windows"; return osName; } - + OsVersion SystemInfo::GetOsVersion() { static OsVersion osVersion = {}; diff --git a/openVulkanoCpp/Scene/BitmapFontAtlasGenerator.cpp b/openVulkanoCpp/Scene/BitmapFontAtlasGenerator.cpp index b75cf4e..d94d884 100644 --- a/openVulkanoCpp/Scene/BitmapFontAtlasGenerator.cpp +++ b/openVulkanoCpp/Scene/BitmapFontAtlasGenerator.cpp @@ -7,6 +7,7 @@ #include "BitmapFontAtlasGenerator.hpp" #include "Base/Logger.hpp" #include "Text/FontAtlas.hpp" +#include namespace OpenVulkano::Scene { @@ -34,10 +35,19 @@ namespace OpenVulkano::Scene 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, area] = InitGlyphsForPacking(chset, face); - const double atlasWidth = ceil(sqrt(area)); - std::vector shelves = Shelf::CreateShelves(atlasWidth, allGlyphs, face); + 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 }; @@ -47,7 +57,10 @@ namespace OpenVulkano::Scene // 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, FontAtlasType::BITMAP); + 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); } @@ -61,20 +74,67 @@ namespace OpenVulkano::Scene allGlyphs.reserve(chset.size()); for (uint32_t codepoint : chset) { - error = FT_Load_Char(face.get(), codepoint, FT_LOAD_RENDER); + 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, Math::Vector2ui(slot->bitmap.width, slot->bitmap.rows)); + 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; }); - return { allGlyphs, area }; + // 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) @@ -82,7 +142,7 @@ namespace OpenVulkano::Scene size_t loadedGlyphs = 0; for (const GlyphForPacking& glyph : allGlyphs) { - FT_Error error = FT_Load_Char(face.get(), glyph.code, FT_LOAD_RENDER); + 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, @@ -91,12 +151,20 @@ namespace OpenVulkano::Scene } FT_GlyphSlot slot = face->glyph; - char* baseAddress = static_cast(m_atlasData->GetTexture()->textureBuffer) + glyph.firstGlyphByteInAtlas; - for (int row = 0; row < slot->bitmap.rows; row++) + if (m_channelsCount == 1) { - std::memcpy(baseAddress + row * m_atlasData->GetTexture()->resolution.x, - &slot->bitmap.buffer[(slot->bitmap.rows - 1 - row) * slot->bitmap.pitch], - slot->bitmap.width); + 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]; @@ -104,10 +172,56 @@ namespace OpenVulkano::Scene 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)); + + 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; + } + } + } + } } + diff --git a/openVulkanoCpp/Scene/BitmapFontAtlasGenerator.hpp b/openVulkanoCpp/Scene/BitmapFontAtlasGenerator.hpp index 7b8c59f..c7fbfcb 100644 --- a/openVulkanoCpp/Scene/BitmapFontAtlasGenerator.hpp +++ b/openVulkanoCpp/Scene/BitmapFontAtlasGenerator.hpp @@ -7,6 +7,7 @@ #pragma once #include "FontAtlasGeneratorBase.hpp" +#include "Scene/SubpixelLayout.hpp" #include "Shelf.hpp" namespace OpenVulkano::Scene @@ -34,7 +35,13 @@ namespace OpenVulkano::Scene class BitmapFontAtlasGenerator : public FontAtlasGeneratorBase { public: - BitmapFontAtlasGenerator(FontPixelSizeConfig config = FontPixelSizeConfig()) : FontAtlasGeneratorBase(1), m_pixelSizeConfig(config) {} + BitmapFontAtlasGenerator(FontPixelSizeConfig config = FontPixelSizeConfig(), + std::optional subpixelLayout = std::nullopt) + : FontAtlasGeneratorBase(subpixelLayout.has_value() && *subpixelLayout < SubpixelLayout::UNKNOWN ? 4 : 1) + , m_pixelSizeConfig(config) + , m_subpixelLayout(subpixelLayout.value_or(SubpixelLayout::UNKNOWN)) + { + } 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, @@ -42,8 +49,13 @@ namespace OpenVulkano::Scene 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); + void FillSubpixelData(const FT_Bitmap& bitmap, const GlyphForPacking& glyph); + FT_Int32 GetGlyphRenderMode() const; + // tmp function + Math::Vector2ui ScaleGlyphSize(unsigned int w, unsigned int h) const; std::pair, double> InitGlyphsForPacking(const std::set& chset, const FtFaceRecPtr& face); private: FontPixelSizeConfig m_pixelSizeConfig; + SubpixelLayout m_subpixelLayout; }; } diff --git a/openVulkanoCpp/Scene/SdfFontAtlasGenerator.cpp b/openVulkanoCpp/Scene/SdfFontAtlasGenerator.cpp index d72a1a5..cd1c049 100644 --- a/openVulkanoCpp/Scene/SdfFontAtlasGenerator.cpp +++ b/openVulkanoCpp/Scene/SdfFontAtlasGenerator.cpp @@ -142,7 +142,7 @@ namespace OpenVulkano::Scene int idx = 0; m_atlasData = std::make_shared(Math::Vector2ui{ width, height }, fontGeometry.getMetrics().lineHeight, - channelsCount == 1 ? FontAtlasType::SDF : FontAtlasType::MSDF); + channelsCount == 1 ? FontAtlasType::SDF : FontAtlasType::MSDF, m_channelsCount == 1 ? DataFormat::R8_UNORM : DataFormat::R8G8B8A8_UNORM); if constexpr (Channels == 3) { diff --git a/openVulkanoCpp/Scene/Shelf.hpp b/openVulkanoCpp/Scene/Shelf.hpp index 922bd65..6b633b7 100644 --- a/openVulkanoCpp/Scene/Shelf.hpp +++ b/openVulkanoCpp/Scene/Shelf.hpp @@ -24,9 +24,10 @@ namespace OpenVulkano::Scene struct Shelf { inline static std::vector CreateShelves(uint32_t atlasWidth, std::vector& glyphs, - const FtFaceRecPtr& face); + const FtFaceRecPtr& face, int channelsCount); - Shelf(uint32_t width, uint32_t height) : m_width(width), m_height(height), m_remainingWidth(width) {} + Shelf(uint32_t width, uint32_t height, int pixelSize, uint32_t prevShelvesHeight) + : m_width(width), m_height(height), m_remainingWidth(width), m_pixelSize(pixelSize), m_prevShelvesHeight(prevShelvesHeight) {} bool HasSpaceForGlyph(uint32_t glyphWidth, uint32_t glyphHeight) const { return m_remainingWidth >= glyphWidth && m_height >= glyphHeight; @@ -35,7 +36,10 @@ namespace OpenVulkano::Scene 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) + uint32_t GetPrevShelvesHeight() const { return m_prevShelvesHeight; } + int GetPixelSize() const { return m_pixelSize; } + + std::optional> AddGlyph(uint32_t glyphWidth, uint32_t glyphHeight) { if (!HasSpaceForGlyph(glyphWidth, glyphHeight)) { @@ -44,7 +48,13 @@ namespace OpenVulkano::Scene uint32_t insertionPos = m_nextGlyphPos; m_nextGlyphPos += glyphWidth; m_remainingWidth -= glyphWidth; - return insertionPos; + + uint32_t hOffset = m_height - (m_height - glyphHeight); + uint32_t glyphFirstByte = (insertionPos * m_pixelSize) + + ((hOffset * m_width * m_pixelSize) - (m_width * m_pixelSize)) + + (m_prevShelvesHeight * m_width * m_pixelSize); + + return std::make_pair(insertionPos, glyphFirstByte); } private: @@ -52,10 +62,12 @@ namespace OpenVulkano::Scene uint32_t m_height; uint32_t m_remainingWidth; uint32_t m_nextGlyphPos = 0; + uint32_t m_prevShelvesHeight; + int m_pixelSize; }; std::vector Shelf::CreateShelves(uint32_t atlasWidth, std::vector& glyphs, - const FtFaceRecPtr& face) + const FtFaceRecPtr& face, int channelsCount) { std::vector shelves; for (GlyphForPacking& glyph : glyphs) @@ -71,10 +83,10 @@ namespace OpenVulkano::Scene uint32_t totalPrevShelvesHeight = 0; for (Shelf& shelf : shelves) { - if (std::optional insertionPosX = shelf.AddGlyph(glyph.size.x, glyph.size.y)) + if (std::optional> opt = shelf.AddGlyph(glyph.size.x, glyph.size.y)) { - glyph.firstGlyphByteInAtlas = *insertionPosX + (totalPrevShelvesHeight * atlasWidth); - glyph.atlasPos.x = *insertionPosX; + glyph.firstGlyphByteInAtlas = opt->second; + glyph.atlasPos.x = opt->first; glyph.atlasPos.y = totalPrevShelvesHeight; needNewShelf = false; break; @@ -84,9 +96,10 @@ namespace OpenVulkano::Scene if (needNewShelf) { - shelves.emplace_back(atlasWidth, glyph.size.y); - shelves.back().AddGlyph(glyph.size.x, glyph.size.y); - glyph.firstGlyphByteInAtlas = totalPrevShelvesHeight * atlasWidth; + shelves.emplace_back(atlasWidth, glyph.size.y, channelsCount, totalPrevShelvesHeight); + Shelf& shelf = shelves.back(); + uint32_t firstByte = (*shelf.AddGlyph(glyph.size.x, glyph.size.y)).second; + glyph.firstGlyphByteInAtlas = firstByte; glyph.atlasPos.x = 0; glyph.atlasPos.y = totalPrevShelvesHeight; } diff --git a/openVulkanoCpp/Scene/SubpixelLayout.hpp b/openVulkanoCpp/Scene/SubpixelLayout.hpp new file mode 100644 index 0000000..394e9b5 --- /dev/null +++ b/openVulkanoCpp/Scene/SubpixelLayout.hpp @@ -0,0 +1,75 @@ +/* + * 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 "Scene/DataFormat.hpp" +#include +#include +#include + +namespace OpenVulkano +{ + class SubpixelLayout + { + public: + enum Layout : uint32_t + { + RGB, + BGR, + RGBV, + BGRV, + // unknown and auto must be last two + UNKNOWN, + NONE = UNKNOWN, + AUTO = UNKNOWN + }; + + constexpr SubpixelLayout() = default; + + constexpr SubpixelLayout(Layout layout) : m_layout(layout) {} + + [[nodiscard]] DataFormat GetTextureDataFormat() const + { + if (m_layout >= SubpixelLayout::UNKNOWN) + { + return DataFormat::R8_UINT; + } + if (m_layout == SubpixelLayout::BGR || m_layout == SubpixelLayout::BGRV) + { + return DataFormat::B8G8R8A8_UINT; + } + return DataFormat::R8G8B8A8_UINT; + } + + [[nodiscard]] std::string_view GetName() const { return magic_enum::enum_name(m_layout); } + + [[nodiscard]] constexpr bool operator==(Layout rhs) const + { + return m_layout == rhs; + } + + [[nodiscard]] constexpr bool operator!=(Layout rhs) const + { + return m_layout != rhs; + } + + [[nodiscard]] constexpr operator uint32_t() const + { + return m_layout; + } + + [[nodiscard]] bool IsHorizontalSubpixelLayout() const + { + return m_layout == SubpixelLayout::RGB || m_layout == SubpixelLayout::BGR; + } + + explicit operator bool() const { return m_layout < Layout::UNKNOWN; } + + private: + Layout m_layout = Layout::UNKNOWN; + }; +} diff --git a/openVulkanoCpp/Scene/Text/FontAtlas.cpp b/openVulkanoCpp/Scene/Text/FontAtlas.cpp index 0ec4eba..5fe41aa 100644 --- a/openVulkanoCpp/Scene/Text/FontAtlas.cpp +++ b/openVulkanoCpp/Scene/Text/FontAtlas.cpp @@ -120,7 +120,10 @@ namespace OpenVulkano::Scene const std::span metadataSpan(metadata, eofHeader.metadataSize); if (eofHeader.flags.version == 0) LoadLegacy(metadataSpan); else LoadNew(metadataSpan); - if (GetAtlasType() >= FontAtlasType::BITMAP) m_texture.m_samplerConfig = &SamplerConfig::NEAREST; + if (GetAtlasType() >= FontAtlasType::BITMAP && GetAtlasType() != FontAtlasType::UNKNOWN) + { + m_texture.m_samplerConfig = &SamplerConfig::NEAREST; + } } void FontAtlas::LoadLegacy(const std::span data) @@ -171,14 +174,18 @@ namespace OpenVulkano::Scene m_texture.textureBuffer = m_imgData.Data(); } - void FontAtlas::Init(const Math::Vector2ui textureResolution, const double lineHeight, const FontAtlasType atlasType) + void FontAtlas::Init(const Math::Vector2ui textureResolution, const double lineHeight, + const FontAtlasType atlasType, DataFormat dataFormat) { m_metadata = { lineHeight, atlasType }; - m_texture.format = atlasType.GetChannelCount() == 1 ? DataFormat::R8_UNORM : DataFormat::R8G8B8A8_UNORM; + m_texture.format = dataFormat; m_texture.resolution = { textureResolution, 1 }; m_imgData = Array(m_texture.format.CalculatedSize(m_texture.resolution.x, m_texture.resolution.y)); m_texture.textureBuffer = m_imgData.Data(); m_texture.size = m_imgData.Size(); - if (atlasType >= FontAtlasType::BITMAP) m_texture.m_samplerConfig = &SamplerConfig::NEAREST; + if (atlasType >= FontAtlasType::BITMAP && atlasType != FontAtlasType::UNKNOWN) + { + m_texture.m_samplerConfig = &SamplerConfig::NEAREST; + } } } \ No newline at end of file diff --git a/openVulkanoCpp/Scene/Text/FontAtlas.hpp b/openVulkanoCpp/Scene/Text/FontAtlas.hpp index 9249eb7..9f04be8 100644 --- a/openVulkanoCpp/Scene/Text/FontAtlas.hpp +++ b/openVulkanoCpp/Scene/Text/FontAtlas.hpp @@ -46,11 +46,15 @@ namespace OpenVulkano::Scene public: FontAtlas() = default; - FontAtlas(const Math::Vector2ui textureResolution, const double lineHeight, const FontAtlasType atlasType) { Init(textureResolution, lineHeight, atlasType); } + FontAtlas(const Math::Vector2ui textureResolution, const double lineHeight, const FontAtlasType atlasType, + DataFormat dataFormat) + { + Init(textureResolution, lineHeight, atlasType, dataFormat); + } FontAtlas(const std::filesystem::path& path); FontAtlas(const std::span data) { Load(data); } - void Init(Math::Vector2ui textureResolution, double lineHeight, FontAtlasType atlasType); + void Init(Math::Vector2ui textureResolution, double lineHeight, FontAtlasType atlasType, DataFormat dataFormat); void Save(const std::filesystem::path& path) const; void Load(std::span data); diff --git a/openVulkanoCpp/Scene/Text/FontAtlasType.hpp b/openVulkanoCpp/Scene/Text/FontAtlasType.hpp index ba6e31d..dc95833 100644 --- a/openVulkanoCpp/Scene/Text/FontAtlasType.hpp +++ b/openVulkanoCpp/Scene/Text/FontAtlasType.hpp @@ -18,10 +18,12 @@ namespace OpenVulkano::Scene SDF = 0, MSDF, BITMAP, + BITMAP_SUBPIXEL, UNKNOWN }; - static constexpr std::string_view DEFAULT_FG_SHADERS[] = { "Shader/sdfText", "Shader/msdfText", "Shader/text" }; + static constexpr std::string_view DEFAULT_FG_SHADERS[] = { "Shader/sdfText", "Shader/msdfText", "Shader/text", + "Shader/subpixelText" }; static constexpr uint32_t CHANNEL_COUNT[] = { 1, 4, 1, 4, 0 }; diff --git a/openVulkanoCpp/Scene/TextDrawable.cpp b/openVulkanoCpp/Scene/TextDrawable.cpp index 113f3e0..5971385 100644 --- a/openVulkanoCpp/Scene/TextDrawable.cpp +++ b/openVulkanoCpp/Scene/TextDrawable.cpp @@ -21,6 +21,7 @@ namespace OpenVulkano::Scene constexpr uint32_t MISSING_GLYPH_SYMBOL = '?'; Shader DEFAULT_SHADER_BITMAP = TextDrawable::MakeDefaultShader(FontAtlasType::BITMAP); + Shader DEFAULT_SHADER_BITMAP_SUBPIXEL = TextDrawable::MakeDefaultShader(FontAtlasType::BITMAP_SUBPIXEL); Shader DEFAULT_SHADER_SDF = TextDrawable::MakeDefaultShader(FontAtlasType::SDF); Shader DEFAULT_SHADER_MSDF = TextDrawable::MakeDefaultShader(FontAtlasType::MSDF); } @@ -156,6 +157,7 @@ namespace OpenVulkano::Scene case FontAtlasType::MSDF: return &DEFAULT_SHADER_MSDF; default: Logger::RENDER->warn("No default shader for atlas type: {}", type.GetName()); case FontAtlasType::BITMAP: return &DEFAULT_SHADER_BITMAP; + case FontAtlasType::BITMAP_SUBPIXEL: return &DEFAULT_SHADER_BITMAP_SUBPIXEL; } } } \ No newline at end of file diff --git a/openVulkanoCpp/Shader/subpixelText.frag b/openVulkanoCpp/Shader/subpixelText.frag new file mode 100644 index 0000000..098c5a8 --- /dev/null +++ b/openVulkanoCpp/Shader/subpixelText.frag @@ -0,0 +1,20 @@ +#version 450 + +layout(location = 0) in vec4 color; +layout(location = 1) in vec4 bgColor; +layout(location = 2) in vec2 texCoord; + +layout(location = 0) out vec4 outColor; + +layout(set = 2, binding = 0) uniform sampler2D texSampler; + +void main() +{ + vec4 sampled = texture(texSampler, texCoord); + float alpha = max(sampled.r, max(sampled.g, sampled.b)); + outColor = vec4(color) * vec4(sampled.rgb, alpha); + if (bgColor.a != 0) + { + outColor = mix(bgColor, outColor, alpha); + } +}