Subpixel rendering (#186)

Reviewed-on: https://git.madvoxel.net/OpenVulkano/OpenVulkano/pulls/186
Reviewed-by: Georg Hagen <georg.hagen@madvoxel.com>
Co-authored-by: ohyzha <oleksii.hyzha.ext@madvoxel.com>
Co-committed-by: ohyzha <oleksii.hyzha.ext@madvoxel.com>
This commit is contained in:
ohyzha
2025-01-13 11:05:54 +01:00
committed by Oleksii_Hyzha
parent c976d75715
commit f2b164d6e8
20 changed files with 452 additions and 112 deletions

View File

@@ -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()

View File

@@ -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)

View File

@@ -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<uint32_t> 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<uint8_t*>(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<uint8_t*>(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<UI::PerformanceInfo> m_perfInfo =
std::make_shared<UI::PerformanceInfo>();
m_ui.AddElement(m_perfInfo);

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 KiB

View File

@@ -10,6 +10,7 @@
#include <algorithm>
#include <array>
#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;

View File

@@ -8,6 +8,7 @@
#include "Math/Math.hpp"
#include "Base/PlatformEnums.hpp"
#include "Scene/SubpixelLayout.hpp"
#include <string>
#include <stdexcept>
@@ -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()
{

View File

@@ -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()

View File

@@ -6,13 +6,15 @@
#include "WindowGLFW.hpp"
#include "Base/Logger.hpp"
#include "Base/EngineConfiguration.hpp"
#include <GLFW/glfw3.h>
#if __linux__
#include <fontconfig/fontconfig.h>
#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<GLFWmonitor*> 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<int*>(&currentSize.x), reinterpret_cast<int*>(&currentSize.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);

View File

@@ -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;

View File

@@ -424,7 +424,7 @@ namespace OpenVulkano
static const std::string osName = "Windows";
return osName;
}
OsVersion SystemInfo::GetOsVersion()
{
static OsVersion osVersion = {};

View File

@@ -7,6 +7,7 @@
#include "BitmapFontAtlasGenerator.hpp"
#include "Base/Logger.hpp"
#include "Text/FontAtlas.hpp"
#include <freetype/ftlcdfil.h>
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<Shelf> shelves = Shelf::CreateShelves(atlasWidth, allGlyphs, face);
auto [allGlyphs, atlasWidth] = InitGlyphsForPacking(chset, face);
std::vector<Shelf> 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<FontAtlas>(atlasResolution, face->height * scaleFactor, FontAtlasType::BITMAP);
m_atlasData = std::make_shared<FontAtlas>(atlasResolution, face->height * scaleFactor,
static_cast<bool>(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<GlyphForPacking>& 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<char*>(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<char*>(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<char*>(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;
}
}
}
}
}

View File

@@ -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> 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<uint32_t>& charset,
const std::optional<std::string>& pngOutput = std::nullopt) override;
void GenerateAtlas(const Array<char>& fontData, const std::set<uint32_t>& charset,
@@ -42,8 +49,13 @@ namespace OpenVulkano::Scene
private:
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);
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<std::vector<GlyphForPacking>, double> InitGlyphsForPacking(const std::set<uint32_t>& chset, const FtFaceRecPtr& face);
private:
FontPixelSizeConfig m_pixelSizeConfig;
SubpixelLayout m_subpixelLayout;
};
}

View File

@@ -142,7 +142,7 @@ namespace OpenVulkano::Scene
int idx = 0;
m_atlasData = std::make_shared<FontAtlas>(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)
{

View File

@@ -24,9 +24,10 @@ namespace OpenVulkano::Scene
struct Shelf
{
inline static std::vector<Shelf> CreateShelves(uint32_t atlasWidth, std::vector<GlyphForPacking>& 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<uint32_t> AddGlyph(uint32_t glyphWidth, uint32_t glyphHeight)
uint32_t GetPrevShelvesHeight() const { return m_prevShelvesHeight; }
int GetPixelSize() const { return m_pixelSize; }
std::optional<std::pair<uint32_t, uint32_t>> 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> Shelf::CreateShelves(uint32_t atlasWidth, std::vector<GlyphForPacking>& glyphs,
const FtFaceRecPtr& face)
const FtFaceRecPtr& face, int channelsCount)
{
std::vector<Shelf> shelves;
for (GlyphForPacking& glyph : glyphs)
@@ -71,10 +83,10 @@ namespace OpenVulkano::Scene
uint32_t totalPrevShelvesHeight = 0;
for (Shelf& shelf : shelves)
{
if (std::optional<uint32_t> insertionPosX = shelf.AddGlyph(glyph.size.x, glyph.size.y))
if (std::optional<std::pair<uint32_t, uint32_t>> 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;
}

View File

@@ -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 <magic_enum.hpp>
#include <cinttypes>
#include <string_view>
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;
};
}

View File

@@ -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<char> 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<uint8_t>(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;
}
}
}

View File

@@ -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<char> 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<char> data);

View File

@@ -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 };

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}