Merge branch 'master' into pfm_pnm

This commit is contained in:
Georg Hagen
2025-01-04 16:54:16 +01:00
37 changed files with 996 additions and 495 deletions

View File

@@ -70,7 +70,7 @@ namespace OpenVulkano
if (!file.is_open())
{
if (emptyOnMissing) return {};
if constexpr (std::is_same_v<std::remove_reference_t<std::remove_cv_t<T>>, std::filesystem::path>)
if constexpr (std::is_same_v<std::decay_t<T>, std::filesystem::path>)
{
throw std::runtime_error("Failed to open file '" + filePath.string() + "'!");
}

View File

@@ -190,13 +190,20 @@ namespace OpenVulkano
static Array<char> ReadFile(const T& filePath, bool emptyOnMissing = false,
bool nullTerminateString = false);
template<size_t N>
static Array<char> ReadFile(const char (&filePath)[N], bool emptyOnMissing = false,
bool nullTerminateString = false)
{
return ReadFile(std::string(filePath), emptyOnMissing, nullTerminateString);
}
template<class T>
static int GetUniqueTypeId()
{
static const int id = uniqueTypeID++;
return id;
}
static std::string DemangleTypeName(const char* name);
};
}

View File

@@ -35,15 +35,15 @@ endif ()
file(GLOB SHADER_SRC_FILES ${CMAKE_CURRENT_SOURCE_DIR}/Shader/*)
list(FILTER SHADER_SRC_FILES EXCLUDE REGEX ".*\\.(hpp|cpp)$")
add_library(openVulkanoCpp ${MAIN_FILE} ${SHADER_SRC_FILES} ${GENERATED_SHADER_SOURCES})
FilterPlatformPaths(sources)
add_library(openVulkanoCpp ${MAIN_FILE} ${SHADER_SRC_FILES} ${GENERATED_SHADER_SOURCES} ${sources})
set_property(TARGET openVulkanoCpp PROPERTY CXX_STANDARD 20)
source_group("Shaders" FILES ${SHADER_SRC_FILES})
source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}" FILES ${sources})
FilterPlatformPaths(sources)
SetWarningSettings(openVulkanoCpp)
SetGlmDefines(openVulkanoCpp)
target_sources(openVulkanoCpp PRIVATE ${sources})
target_include_directories(openVulkanoCpp PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} ${SHADER_OUTPUT_DEST})
if(APPLE)
@@ -67,7 +67,10 @@ if (NOT ANDROID AND NOT IOS)
endif()
endif()
target_link_libraries(openVulkanoCpp PUBLIC magic_enum yaml-cpp fmt spdlog glm pugixml stb eigen utf8cpp imgui_internal TracyClient stud-uuid ryml unordered_dense concurrentqueue units ktx dds_image)
target_link_libraries(openVulkanoCpp PUBLIC magic_enum yaml-cpp fmt spdlog glm pugixml stb eigen utf8cpp imgui_internal TracyClient stud-uuid ryml unordered_dense concurrentqueue units dds_image)
if (ENABLE_KTX)
target_link_libraries(openVulkanoCpp PUBLIC ktx)
endif ()
LinkAssimp(openVulkanoCpp)
LinkLibArchive(openVulkanoCpp)
LinkLibJpegTurbo(openVulkanoCpp)

View File

@@ -0,0 +1,33 @@
/*
* 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 <zlib.h>
#include <cstdlib>
namespace Extensions
{
static unsigned char* STBZlibCompressor(unsigned char* data, int data_len, int* out_len, int quality)
{
uLong maxCompressedSize = compressBound(data_len);
void* outData = malloc(maxCompressedSize);
if (!outData)
{
*out_len = 0;
return nullptr;
}
int result = compress2(static_cast<Bytef*>(outData), &maxCompressedSize, data, data_len, Z_BEST_COMPRESSION);
if (result != Z_OK)
{
free(outData);
*out_len = 0;
return nullptr;
}
*out_len = static_cast<int>(maxCompressedSize);
return static_cast<unsigned char*>(outData);
}
}

View File

@@ -30,6 +30,7 @@ using namespace OpenVulkano;
- (id)initWithWindow:(MetalViewWindow*)win
{
window = win;
window->SetContentScale(UIScreen.mainScreen.nativeScale);
return self;
}

View File

@@ -5,7 +5,9 @@
*/
#include "ImageLoaderKtx.hpp"
#include "Base/Logger.hpp"
#if __has_include("ktx.h")
#include <ktx.h>
#include <memory>
@@ -13,22 +15,72 @@
#include <iostream>
#include <stdexcept>
namespace
{
void KtxDestroy(ktxTexture* tex)
{
ktxTexture_Destroy(tex);
}
}
namespace OpenVulkano::Image
{
namespace
{
struct KTXTextureDeleter {
void operator()(ktxTexture* texture) const {
if (texture) {
ktxTexture_Destroy(texture);
}
}
};
std::unique_ptr<Image> ExtractImage(ktxTexture* texture)
{
if (!texture->pData)
{
throw std::runtime_error("No image data found in KTX texture.");
}
auto width = static_cast<int>(texture->baseWidth);
auto height = static_cast<int>(texture->baseHeight);
auto image = std::make_unique<Image>();
image->resolution.x = width;
image->resolution.y = height;
image->resolution.z = 1;
if (texture->classId == ktxTexture1_c)
{
ktxTexture1* tx = reinterpret_cast<ktxTexture1*>(texture);
DataFormat format = DataFormat::Format::UNDEFINED;
switch (tx->glInternalformat)
{
case 0x8051: /* GL_RGB8_EXT */ format = DataFormat::Format::R8G8B8_UNORM; break;
case 0x8054: /* GL_RGB16_EXT */ format = DataFormat::Format::R16G16B16_UNORM; break;
case 0x8057: /* GL_RGB5_A1_EXT */ format = DataFormat::Format::R5G5B5A1_UNORM_PACK16; break;
case 0x8058: /* GL_RGBA8_EXT */ format = DataFormat::Format::R8G8B8A8_UNORM; break;
case 0x8059: /* GL_RGB10_A2_EXT */ format = DataFormat::Format::A2R10G10B10_UNORM_PACK32; break;
case 0x805B: /* GL_RGBA16_EXT */ format = DataFormat::Format::R16G16B16A16_UNORM; break;
default: throw std::runtime_error("Unhandled texture1 format: " + tx->glInternalformat);
}
image->dataFormat = format;
}
else if (texture->classId == ktxTexture2_c)
{
ktxTexture2* tx = reinterpret_cast<ktxTexture2*>(texture);
image->dataFormat = reinterpret_cast<DataFormat::Format&>(tx->vkFormat);
}
else [[unlikely]]
{
throw std::runtime_error("ktxTexture has unhandled classId!");
}
image->data = Array<uint8_t>(texture->dataSize);
memcpy(image->data.Data(), texture->pData, image->data.Size());
return image;
}
}
std::unique_ptr<Image> ImageLoaderKtx::loadFromFile(const std::string& filePath)
{
ktxTexture* tmp = nullptr;
KTX_error_code error_code =
ktxTexture_CreateFromNamedFile(filePath.c_str(), KTX_TEXTURE_CREATE_LOAD_IMAGE_DATA_BIT, &tmp);
std::unique_ptr<ktxTexture, decltype(&KtxDestroy)> texture(tmp, &KtxDestroy);
std::unique_ptr<ktxTexture, KTXTextureDeleter> texture(tmp);
if (error_code != KTX_SUCCESS)
{
@@ -44,7 +96,7 @@ namespace OpenVulkano::Image
ktxTexture* tmp = nullptr;
KTX_error_code error_code =
ktxTexture_CreateFromMemory(buffer.data(), buffer.size(), KTX_TEXTURE_CREATE_LOAD_IMAGE_DATA_BIT, &tmp);
std::unique_ptr<ktxTexture, decltype(&KtxDestroy)> texture(tmp, &KtxDestroy);
std::unique_ptr<ktxTexture, KTXTextureDeleter> texture(tmp);
if (error_code != KTX_SUCCESS)
{
@@ -60,7 +112,7 @@ namespace OpenVulkano::Image
ktxTexture* tmp = nullptr;
KTX_error_code result =
ktxTexture_CreateFromNamedFile(filename.c_str(), KTX_TEXTURE_CREATE_LOAD_IMAGE_DATA_BIT, &tmp);
std::unique_ptr<ktxTexture, decltype(&KtxDestroy)> texture(tmp, &KtxDestroy);
std::unique_ptr<ktxTexture, KTXTextureDeleter> texture(tmp);
if (result != KTX_SUCCESS)
{
@@ -71,51 +123,26 @@ namespace OpenVulkano::Image
Math::Vector2i dimensions(texture->baseWidth, texture->baseHeight);
return dimensions;
}
std::unique_ptr<Image> ImageLoaderKtx::ExtractImage(ktxTexture* texture)
}
#else
namespace OpenVulkano::Image
{
std::unique_ptr<Image> ImageLoaderKtx::loadFromFile(const std::string& filePath)
{
if (!texture->pData)
{
throw std::runtime_error("No image data found in KTX texture.");
}
auto width = static_cast<int>(texture->baseWidth);
auto height = static_cast<int>(texture->baseHeight);
auto image = std::make_unique<Image>();
image->resolution.x = width;
image->resolution.y = height;
image->resolution.z = 1;
if (texture->classId == ktxTexture1_c)
{
ktxTexture1* tx = reinterpret_cast<ktxTexture1*>(texture);
DataFormat format = DataFormat::Format::UNDEFINED;
switch (tx->glInternalformat)
{
case 0x8051: /* GL_RGB8_EXT */ format = DataFormat::Format::R8G8B8_UNORM; break;
case 0x8054: /* GL_RGB16_EXT */ format = DataFormat::Format::R16G16B16_UNORM; break;
case 0x8057: /* GL_RGB5_A1_EXT */ format = DataFormat::Format::R5G5B5A1_UNORM_PACK16; break;
case 0x8058: /* GL_RGBA8_EXT */ format = DataFormat::Format::R8G8B8A8_UNORM; break;
case 0x8059: /* GL_RGB10_A2_EXT */ format = DataFormat::Format::A2R10G10B10_UNORM_PACK32; break;
case 0x805B: /* GL_RGBA16_EXT */ format = DataFormat::Format::R16G16B16A16_UNORM; break;
default: throw std::runtime_error("Unhandled texture1 format: " + tx->glInternalformat);
}
image->dataFormat = format;
}
else if (texture->classId == ktxTexture2_c)
{
ktxTexture2* tx = reinterpret_cast<ktxTexture2*>(texture);
image->dataFormat = reinterpret_cast<DataFormat::Format&>(tx->vkFormat);
}
else [[unlikely]]
{
throw std::runtime_error("ktxTexture has unhandled classId!");
}
image->data = Array<uint8_t>(texture->dataSize);
memcpy(image->data.Data(), texture->pData, image->data.Size());
return image;
Logger::DATA->error("KTX loading not available.");
return nullptr;
}
}
std::unique_ptr<Image> ImageLoaderKtx::loadFromMemory(const std::vector<uint8_t>& buffer)
{
Logger::DATA->error("KTX loading not available.");
return nullptr;
}
Math::Vector2i ImageLoaderKtx::GetImageDimensions(const std::string& filename)
{
Logger::DATA->error("KTX loading not available.");
return {0,0};
}
}
#endif

View File

@@ -8,8 +8,6 @@
#include "ImageLoader.hpp"
struct ktxTexture;
namespace OpenVulkano::Image
{
class ImageLoaderKtx : public IImageLoader
@@ -18,8 +16,5 @@ namespace OpenVulkano::Image
std::unique_ptr<Image> loadFromFile(const std::string& filePath) override;
std::unique_ptr<Image> loadFromMemory(const std::vector<uint8_t>& buffer) override;
Math::Vector2i GetImageDimensions(const std::string& filename) override;
protected:
std::unique_ptr<Image> ExtractImage(ktxTexture* texture);
};
}

View File

@@ -250,16 +250,21 @@ namespace OpenVulkano::Input
enum Button: int16_t
{
BUTTON_TAP = 0,
BUTTON_TWO_FINGER_TAP
BUTTON_TWO_FINGER_TAP,
BUTTON_LAST = BUTTON_TWO_FINGER_TAP
};
enum Axis : int16_t
{
AXIS_TAP_X = 0,
AXIS_TAP_Y,
AXIS_TAP_X_ABS,
AXIS_TAP_Y_ABS,
AXIS_TAP_DURATION,
AXIS_TAP_TWO_FINGERS_X,
AXIS_TAP_TWO_FINGERS_Y,
AXIS_TAP_TWO_FINGERS_X_ABS,
AXIS_TAP_TWO_FINGERS_Y_ABS,
AXIS_TAP_TWO_FINGER_DURATION,
AXIS_PAN_X,
AXIS_PAN_Y,

View File

@@ -194,6 +194,8 @@ namespace OpenVulkano::Input
auto diff = m_nextTap - m_lastTap;
m_axes[InputKey::Touch::Axis::AXIS_TAP_X] = diff.x;
m_axes[InputKey::Touch::Axis::AXIS_TAP_Y] = diff.y;
m_axes[InputKey::Touch::Axis::AXIS_TAP_X_ABS] = m_nextTap.x;
m_axes[InputKey::Touch::Axis::AXIS_TAP_Y_ABS] = m_nextTap.y;
m_lastTap = m_nextTap;
// Pan

View File

@@ -31,6 +31,7 @@ namespace OpenVulkano::Scene
{
SDF = 0,
MSDF,
BITMAP,
UNKNOWN
};
static constexpr std::string_view DEFAULT_FG_SHADERS[] = { "Shader/text", "Shader/msdfText" };

View File

@@ -0,0 +1,130 @@
/*
* 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<uint32_t>& charset,
const std::optional<std::string>& pngOutput)
{
Generate(fontFile, charset, pngOutput);
}
void BitmapFontAtlasGenerator::GenerateAtlas(const Array<char>& fontData, const std::set<uint32_t>& charset,
const std::optional<std::string>& pngOutput)
{
Generate(fontData, charset, pngOutput);
}
void BitmapFontAtlasGenerator::Generate(const std::variant<std::string, Array<char>>& source,
const std::set<uint32_t>& chset,
const std::optional<std::string>& pngOutput)
{
if (chset.empty())
{
Logger::APP->info("Charset is empty. Nothing to generate.");
return;
}
m_atlasData = std::make_shared<AtlasData>();
const auto& [lib, face] = FontAtlasGeneratorBase::InitFreetype(source);
Math::Vector2ui cellSize;
if (m_pixelSizeConfig.isPixelSize)
{
cellSize = { m_pixelSizeConfig.size, m_pixelSizeConfig.size };
// 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(face.get(), 0, cellSize.y - cellSize.y / 3);
}
else
{
const float pixelSize = (m_pixelSizeConfig.size * m_pixelSizeConfig.dpi) / 72.0f;
//int fontHeight = round((face->bbox.yMax - face->bbox.yMin) * pixelSize / face->units_per_EM);
//int fontWidth = round((face->bbox.xMax - face->bbox.xMin) * pixelSize / face->units_per_EM);
cellSize = { pixelSize, pixelSize };
FT_Set_Char_Size(face.get(), 0, static_cast<FT_F26Dot6>(m_pixelSizeConfig.size) * 64,
static_cast<FT_UInt>(m_pixelSizeConfig.dpi), static_cast<FT_UInt>(m_pixelSizeConfig.dpi));
}
const double sq = std::sqrt(chset.size());
const size_t glyphsPerRow = (static_cast<size_t>(sq)) + (sq - static_cast<size_t>(sq) != 0);
const size_t rows = (chset.size() / glyphsPerRow) + (chset.size() % glyphsPerRow != 0);
const Math::Vector2ui atlasResolution = { glyphsPerRow * cellSize.x, rows * cellSize.y };
// same as in msdfgen lib by default. see import-font.h for reference
// TODO: probably also support keeping coordinates as the integer values native to the font file
// but since some algorithms have already been implemented for EM_NORMALIZED mode, currently there is no support for default font metrics (ints)
// The coordinates will be normalized to the em size, i.e. 1 = 1 em
const double scaleFactor = (1. / face->units_per_EM);
SetupAtlasData(atlasResolution, face->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(face.get(), codepoint, FT_LOAD_RENDER);
if (error)
{
Logger::APP->error("FT_Load_Char for codepoint {} has failed. {}", codepoint, GetFreetypeErrorDescription(error));
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);
// skip such glyph for now to avoid crash
continue;
}
const size_t firstGlyphByte = (gridPos.y * cellSize.x + gridPos.x * atlasResolution.x * cellSize.y);
for (int row = 0; row < slot->bitmap.rows; row++)
{
for (int 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<double>(slot->metrics.width * toPixelScaler),
static_cast<double>(slot->metrics.height * toPixelScaler) };
Math::AABB glyphAtlasAABB(Math::Vector3f(currentPosX, currentPosY, 0), Math::Vector3f(currentPosX + whPixel.x, currentPosY + whPixel.y, 0));
SetGlyphData(glyphInfo, glyphBearing, glyphMetrics, glyphAtlasAABB, slot->advance.x * scaleFactor);
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);
}
}

View File

@@ -0,0 +1,33 @@
/*
* 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
{
struct FontPixelSizeConfig
{
float size = 16.f;
float dpi = 72.f;
bool isPixelSize = true;
};
class BitmapFontAtlasGenerator : public FontAtlasGeneratorBase
{
public:
BitmapFontAtlasGenerator(FontPixelSizeConfig config = FontPixelSizeConfig()) : FontAtlasGeneratorBase(1), m_pixelSizeConfig(config) {}
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,
const std::optional<std::string>& pngOutput = std::nullopt) override;
private:
void Generate(const std::variant<std::string, Array<char>>& source, const std::set<uint32_t>& chset, const std::optional<std::string>& pngOutput);
private:
FontPixelSizeConfig m_pixelSizeConfig;
};
}

View File

@@ -1,327 +0,0 @@
/*
* 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/.
*/
#if __has_include("msdfgen.h")
#include "FontAtlasGenerator.hpp"
#include "Base/Logger.hpp"
#include "Scene/AtlasData.hpp"
#include <msdfgen.h>
#include <msdfgen-ext.h>
#include <msdf-atlas-gen/msdf-atlas-gen.h>
#define STBI_MSC_SECURE_CRT
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include <stb_image_write.h>
#include <ft2build.h>
#include FT_FREETYPE_H
#include <fstream>
#include <filesystem>
namespace OpenVulkano::Scene
{
using namespace msdfgen;
using namespace msdf_atlas;
FontAtlasGeneratorConfig FontAtlasGeneratorConfig::sdfDefaultConfig = { 42, 1.0, 5 };
FontAtlasGeneratorConfig FontAtlasGeneratorConfig::msdfDefaultConfig = { 32, 1.0, 3 };
template<int Channels>
Charset FontAtlasGenerator<Channels>::LoadAllGlyphs(const std::variant<std::string, Array<char>>& 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<std::string>(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<int Channels>
void FontAtlasGenerator<Channels>::GenerateAtlas(const std::string& fontFile, const std::set<uint32_t>& charset,
const std::optional<std::string>& pngOutput)
{
FreetypeHandle* ft;
FontHandle* font;
InitFreetypeFromFile(ft, font, fontFile);
Charset s;
std::for_each(s.begin(), s.end(), [&](uint32_t unicode) { s.add(unicode); });
Generate(ft, font, s, pngOutput);
}
template<int Channels>
FontAtlasGenerator<Channels>::FontAtlasGenerator()
{
if constexpr (Channels == 1) m_config = FontAtlasGeneratorConfig::sdfDefaultConfig;
else m_config = FontAtlasGeneratorConfig::msdfDefaultConfig;
}
template<int Channels>
void FontAtlasGenerator<Channels>::GenerateAtlas(const Array<char>& fontData, int length,
const std::set<uint32_t>& charset,
const std::optional<std::string>& pngOutput)
{
FreetypeHandle* ft;
FontHandle* font;
InitFreetypeFromBuffer(ft, font, (const msdfgen::byte*)(fontData.Data()), length);
Charset s;
std::for_each(s.begin(), s.end(), [&](uint32_t unicode) { s.add(unicode); });
Generate(ft, font, s, pngOutput);
}
template<int Channels>
void FontAtlasGenerator<Channels>::GenerateAtlas(const std::string& fontFile, const Charset& charset,
const std::optional<std::string>& pngOutput)
{
// TODO: dynamic atlas and add only those symbols which are not present yet in current atlas
FreetypeHandle* ft;
FontHandle* font;
InitFreetypeFromFile(ft, font, fontFile);
Generate(ft, font, charset, pngOutput);
}
template<int Channels>
void FontAtlasGenerator<Channels>::GenerateAtlas(const msdfgen::byte* fontData, int length,
const Charset& charset,
const std::optional<std::string>& pngOutput)
{
FreetypeHandle* ft;
FontHandle* font;
InitFreetypeFromBuffer(ft, font, fontData, length);
Generate(ft, font, charset, pngOutput);
}
template<int Channels>
void FontAtlasGenerator<Channels>::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<const char*>(&m_atlasData->meta), sizeof(AtlasMetadata));
uint64_t metadataBytes = sizeof(AtlasMetadata);
for (const auto& [key, val]: m_atlasData->glyphs)
{
fs.write(reinterpret_cast<const char*>(&key), sizeof(uint32_t));
fs.write(reinterpret_cast<const char*>(&val), sizeof(GlyphInfo));
metadataBytes += sizeof(uint32_t);
metadataBytes += sizeof(GlyphInfo);
}
fs.write(reinterpret_cast<const char*>(&metadataBytes), sizeof(uint64_t));
fs.write(reinterpret_cast<const char*>(&packedFlag), sizeof(uint32_t));
}
template<int Channels>
void FontAtlasGenerator<Channels>::InitFreetypeFromFile(FreetypeHandle*& ft, FontHandle*& font,
const std::string& fontFile)
{
ft = initializeFreetype();
if (!ft) { throw std::runtime_error("Failed to initialize freetype"); }
font = loadFont(ft, fontFile.data());
if (!font)
{
deinitializeFreetype(ft);
ft = nullptr;
throw std::runtime_error(fmt::format("Failed to load font from file {0}", fontFile.data()));
}
}
template<int Channels>
void FontAtlasGenerator<Channels>::InitFreetypeFromBuffer(FreetypeHandle*& ft, FontHandle*& font,
const msdfgen::byte* fontData, int length)
{
ft = initializeFreetype();
if (!ft) { throw std::runtime_error("Failed to initialize freetype"); }
font = loadFontData(ft, fontData, length);
if (!font)
{
deinitializeFreetype(ft);
ft = nullptr;
throw std::runtime_error("Failed to load font data from given buffer");
}
}
template<int Channels>
void FontAtlasGenerator<Channels>::Generate(FreetypeHandle* ft, FontHandle* font, const Charset& chset,
const std::optional<std::string>& pngOutput)
{
m_atlasData.reset(new AtlasData);
std::vector<GlyphGeometry> glyphsGeometry;
// FontGeometry is a helper class that loads a set of glyphs from a single font.
FontGeometry fontGeometry(&glyphsGeometry);
fontGeometry.loadCharset(font, 1, chset);
if constexpr (Channels == 3)
{
const double maxCornerAngle = 3.0;
for (GlyphGeometry& glyph: glyphsGeometry)
glyph.edgeColoring(&msdfgen::edgeColoringByDistance, maxCornerAngle, 0);
}
TightAtlasPacker packer;
packer.setDimensionsConstraint(DimensionsConstraint::SQUARE);
int width, height;
const int glyphsPerRow = std::sqrt(glyphsGeometry.size());
const int glyphSize = m_config.glyphSize;
const int rowWidth = glyphSize * glyphsPerRow;
packer.setDimensions(rowWidth, rowWidth);
// something to play with. should not be too high.
// more value - more sdf impact
packer.setPixelRange(m_config.pixelRange);
packer.setMiterLimit(m_config.miterLimit);
packer.pack(glyphsGeometry.data(), glyphsGeometry.size());
packer.getDimensions(width, height);
Generator generator;
generator.resize(width, height);
GeneratorAttributes attributes;
generator.setAttributes(attributes);
generator.setThreadCount(4);
generator.generate(glyphsGeometry.data(), glyphsGeometry.size());
int idx = 0;
if constexpr (Channels == 3)
{
// store RGB as RGBA
m_atlasData->img = std::make_unique<Image::Image>();
m_atlasData->img->data = Array<uint8_t>(width * height * 4);
m_atlasData->img->resolution = Math::Vector3ui(width, height, 1);
m_atlasData->img->dataFormat = OpenVulkano::DataFormat::R8G8B8A8_UNORM;
const BitmapConstRef<msdfgen::byte, 3> storage = generator.atlasStorage();
msdfgen::byte* data = static_cast<msdfgen::byte*>(m_atlasData->img->data.Data());
for (size_t srcPos = 0, dstPos = 0; srcPos < width * height * 3; srcPos += 3, dstPos += 4)
{
data[dstPos] = storage.pixels[srcPos];
data[dstPos + 1] = storage.pixels[srcPos + 1];
data[dstPos + 2] = storage.pixels[srcPos + 2];
data[dstPos + 3] = 255;
}
}
else
{
m_atlasData->img = std::make_unique<Image::Image>();
m_atlasData->img->data = Array<uint8_t>(width * height);
m_atlasData->img->resolution = Math::Vector3ui(width, height, 1);
m_atlasData->img->dataFormat = OpenVulkano::DataFormat::R8_UNORM;
const msdfgen::BitmapConstRef<msdfgen::byte, 1>& 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;
};
for (const auto& glyph: glyphsGeometry)
{
GlyphInfo& info = m_atlasData->glyphs[glyph.getCodepoint()];
const GlyphBox& glyphBox = generator.getLayout()[idx++];
Bbox glyphBaselineBbox, glyphAtlasBbox;
glyph.getQuadPlaneBounds(glyphBaselineBbox.l, glyphBaselineBbox.b, glyphBaselineBbox.r,
glyphBaselineBbox.t);
glyph.getQuadAtlasBounds(glyphAtlasBbox.l, glyphAtlasBbox.b, glyphAtlasBbox.r, glyphAtlasBbox.t);
double bearingX = glyphBox.bounds.l;
double bearingY = glyphBox.bounds.t;
double w = glyphBaselineBbox.r - glyphBaselineBbox.l;
double h = glyphBaselineBbox.t - glyphBaselineBbox.b;
double l = glyphAtlasBbox.l;
double r = glyphAtlasBbox.r;
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;
}
if (pngOutput && !pngOutput->empty()) { SavePng(pngOutput.value()); }
destroyFont(font);
deinitializeFreetype(ft);
}
template<int Channels>
void FontAtlasGenerator<Channels>::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>;
}
#endif

View File

@@ -0,0 +1,174 @@
/*
* 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"
#include "Extensions/STBZlibCompressor.hpp"
#include <filesystem>
#include <fstream>
#define STBI_MSC_SECURE_CRT
#define STB_IMAGE_WRITE_IMPLEMENTATION
#define STBIW_ZLIB_COMPRESS Extensions::STBZlibCompressor
#include <stb_image_write.h>
namespace OpenVulkano::Scene
{
std::pair<FtLibraryRecPtr, FtFaceRecPtr>
FontAtlasGeneratorBase::InitFreetype(const std::variant<std::string, Array<char>>& source)
{
FT_Library library;
auto error = FT_Init_FreeType(&library);
if (error)
{
throw std::runtime_error(fmt::format("Could not initalize freetype library. {}", GetFreetypeErrorDescription(error)));
}
FT_Face face;
if (std::holds_alternative<std::string>(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(fmt::format("Font file could not be opened or read or it's corrupted. {}", GetFreetypeErrorDescription(error)));
}
// some fancy font without unicode charmap
if (face->charmap == nullptr)
{
throw std::runtime_error("Selected font doesn't contain unicode charmap");
}
return { FtLibraryRecPtr(library), FtFaceRecPtr(face) };
}
std::string FontAtlasGeneratorBase::GetFreetypeErrorDescription(FT_Error error)
{
if (const char* s = FT_Error_String(error))
{
return s;
}
return fmt::format("Error code is {}", error);
}
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<const char*>(&m_atlasData->meta), sizeof(AtlasMetadata));
uint64_t metadataBytes = sizeof(AtlasMetadata);
for (const auto& [key, val] : m_atlasData->glyphs)
{
fs.write(reinterpret_cast<const char*>(&key), sizeof(uint32_t));
fs.write(reinterpret_cast<const char*>(&val), sizeof(GlyphInfo));
metadataBytes += sizeof(uint32_t);
metadataBytes += sizeof(GlyphInfo);
}
fs.write(reinterpret_cast<const char*>(&metadataBytes), sizeof(uint64_t));
fs.write(reinterpret_cast<const char*>(&packedFlag), sizeof(uint32_t));
fs.close();
}
void FontAtlasGeneratorBase::SavePng(std::string output) const
{
stbi_flip_vertically_on_write(1);
if (std::filesystem::path(output).extension() != ".png")
{
output += ".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);
}
void FontAtlasGeneratorBase::SetupAtlasData(Math::Vector2ui textureResolution, double lineHeight,
FontAtlasType::Type atlasType)
{
// generate texture
m_atlasData->img = std::make_unique<Image::Image>();
m_atlasData->img->resolution = Math::Vector3ui(textureResolution, 1);
m_atlasData->img->dataFormat = m_channelsCount == 1 ? DataFormat::R8_UNORM : DataFormat::R8G8B8A8_UNORM;
m_atlasData->img->data = Array<uint8_t>(m_atlasData->img->dataFormat.CalculatedSize(textureResolution.x, textureResolution.y));
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->meta.atlasType = atlasType;
m_atlasData->meta.lineHeight = lineHeight;
if (atlasType == FontAtlasType::BITMAP)
{
m_atlasData->texture.m_samplerConfig = &SamplerConfig::NEAREST;
}
}
void FontAtlasGeneratorBase::SetGlyphData(GlyphInfo& info, Math::Vector2d bearing, Math::Vector2d size,
const Math::AABB& aabb, double advance)
{
const double bearingX = bearing.x;
const double bearingY = bearing.y;
const double w = size.x;
const double h = size.y;
const double l = aabb.min.x;
const double r = aabb.max.x;
const double t = aabb.max.y;
const double b = aabb.min.y;
info.xyz[0].x = bearingX;
info.xyz[0].y = h - bearingY;
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.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.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.uv[3].x = l / m_atlasData->texture.resolution.x;
info.uv[3].y = t / m_atlasData->texture.resolution.y;
info.advance = advance;
}
std::set<uint32_t> FontAtlasGeneratorBase::LoadAllGlyphs(const std::variant<std::string, Array<char>>& data)
{
const auto& [lib, face] = InitFreetype(data);
std::set<uint32_t> 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;
}
}

View File

@@ -0,0 +1,36 @@
/*
* 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 "FreetypeHelper.hpp"
#include <variant>
#include <set>
namespace OpenVulkano::Scene
{
class FontAtlasGeneratorBase : public IFontAtlasGenerator
{
public:
FontAtlasGeneratorBase(int channelsCount) : m_channelsCount(channelsCount) {}
void SaveAtlasMetadataInfo(const std::string& outputFile, bool packIntoSingleFile = true) const override;
std::shared_ptr<AtlasData> GetAtlasData() const { return m_atlasData; }
int GetAtlasChannelsCount() const { return m_channelsCount; }
static std::set<uint32_t> LoadAllGlyphs(const std::variant<std::string, Array<char>>& data);
protected:
void SavePng(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::string GetFreetypeErrorDescription(FT_Error error);
static std::pair<FtLibraryRecPtr, FtFaceRecPtr> InitFreetype(const std::variant<std::string, Array<char>>& source);
protected:
int m_channelsCount;
std::shared_ptr<AtlasData> m_atlasData;
};
}

View File

@@ -0,0 +1,28 @@
/*
* 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 <ft2build.h>
#include FT_FREETYPE_H
#include <memory>
namespace OpenVulkano::Scene
{
struct LibDeleter
{
void operator()(FT_Library lib) { FT_Done_FreeType(lib); }
};
struct FaceDeleter
{
void operator()(FT_Face face) { FT_Done_Face(face); }
};
using FtLibraryRecPtr = std::unique_ptr<FT_LibraryRec_, LibDeleter>;
using FtFaceRecPtr = std::unique_ptr<FT_FaceRec_, FaceDeleter>;
}

View File

@@ -21,7 +21,7 @@ namespace OpenVulkano::Scene
public:
virtual void GenerateAtlas(const std::string& fontFile, const std::set<uint32_t>& charset,
const std::optional<std::string>& pngOutput = std::nullopt) = 0;
virtual void GenerateAtlas(const Array<char>& fontData, int length, const std::set<uint32_t>& charset,
virtual void GenerateAtlas(const Array<char>& fontData, const std::set<uint32_t>& charset,
const std::optional<std::string>& pngOutput = std::nullopt) = 0;
virtual void SaveAtlasMetadataInfo(const std::string& outputFile, bool packIntoSingleFile = true) const = 0;
virtual std::shared_ptr<AtlasData> GetAtlasData() const = 0;

View File

@@ -59,10 +59,10 @@ namespace OpenVulkano::Scene
m_actionClick->BindKey(Input::InputKey::Touch::BUTTON_TAP);
m_actionClickX = input->GetAction("ClickIntersectionPosX");
m_actionClickX->BindKey(Input::InputKey::Touch::Axis::AXIS_TAP_X);
m_actionClickX->BindKey(Input::InputKey::Touch::Axis::AXIS_TAP_X_ABS);
m_actionClickX->BindKey(Input::InputKey::Mouse::Axis::AXIS_X_ABS);
m_actionClickY = input->GetAction("ClickIntersectionPosY");
m_actionClickY->BindKey(Input::InputKey::Touch::Axis::AXIS_TAP_Y);
m_actionClickY->BindKey(Input::InputKey::Touch::Axis::AXIS_TAP_Y_ABS);
m_actionClickY->BindKey(Input::InputKey::Mouse::Axis::AXIS_Y_ABS);
}
}
}

View File

@@ -0,0 +1,206 @@
/*
* 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/.
*/
#if __has_include("msdfgen.h")
#include "SdfFontAtlasGenerator.hpp"
#include "Base/Logger.hpp"
#include <msdfgen.h>
#include <msdfgen-ext.h>
#include <msdf-atlas-gen/msdf-atlas-gen.h>
namespace OpenVulkano::Scene
{
using namespace msdfgen;
using namespace msdf_atlas;
SdfFontAtlasGeneratorConfig SdfFontAtlasGeneratorConfig::sdfDefaultConfig = { 42, 1.0, 5 };
SdfFontAtlasGeneratorConfig SdfFontAtlasGeneratorConfig::msdfDefaultConfig = { 32, 1.0, 3 };
template<int Channels>
void SdfFontAtlasGeneratorGeneric<Channels>::GenerateAtlas(const std::string& fontFile, const std::set<uint32_t>& charset,
const std::optional<std::string>& pngOutput)
{
FreetypeHandle* ft;
FontHandle* font;
InitFreetypeFromFile(ft, font, fontFile);
Charset s;
std::for_each(s.begin(), s.end(), [&](uint32_t unicode) { s.add(unicode); });
Generate(ft, font, s, pngOutput);
}
template<int Channels> SdfFontAtlasGeneratorGeneric<Channels>::SdfFontAtlasGeneratorGeneric() : FontAtlasGeneratorBase(Channels)
{
if constexpr (Channels == 1) m_config = SdfFontAtlasGeneratorConfig::sdfDefaultConfig;
else m_config = SdfFontAtlasGeneratorConfig::msdfDefaultConfig;
}
template<int Channels>
void SdfFontAtlasGeneratorGeneric<Channels>::GenerateAtlas(const Array<char>& fontData, const std::set<uint32_t>& charset,
const std::optional<std::string>& pngOutput)
{
FreetypeHandle* ft;
FontHandle* font;
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);
}
template<int Channels>
void SdfFontAtlasGeneratorGeneric<Channels>::GenerateAtlas(const std::string& fontFile, const Charset& charset,
const std::optional<std::string>& pngOutput)
{
// TODO: dynamic atlas and add only those symbols which are not present yet in current atlas
FreetypeHandle* ft;
FontHandle* font;
InitFreetypeFromFile(ft, font, fontFile);
Generate(ft, font, charset, pngOutput);
}
template<int Channels>
void SdfFontAtlasGeneratorGeneric<Channels>::GenerateAtlas(const msdfgen::byte* fontData, int length,
const Charset& charset,
const std::optional<std::string>& pngOutput)
{
FreetypeHandle* ft;
FontHandle* font;
InitFreetypeFromBuffer(ft, font, fontData, length);
Generate(ft, font, charset, pngOutput);
}
template<int Channels>
void SdfFontAtlasGeneratorGeneric<Channels>::InitFreetypeFromFile(FreetypeHandle*& ft, FontHandle*& font,
const std::string& fontFile)
{
ft = initializeFreetype();
if (!ft) { throw std::runtime_error("Failed to initialize freetype"); }
font = loadFont(ft, fontFile.data());
if (!font)
{
deinitializeFreetype(ft);
ft = nullptr;
throw std::runtime_error(fmt::format("Failed to load font from file {0}", fontFile.data()));
}
}
template<int Channels>
void SdfFontAtlasGeneratorGeneric<Channels>::InitFreetypeFromBuffer(FreetypeHandle*& ft, FontHandle*& font,
const msdfgen::byte* fontData, int length)
{
ft = initializeFreetype();
if (!ft) { throw std::runtime_error("Failed to initialize freetype"); }
font = loadFontData(ft, fontData, length);
if (!font)
{
deinitializeFreetype(ft);
ft = nullptr;
throw std::runtime_error("Failed to load font data from given buffer");
}
}
template<int Channels>
void SdfFontAtlasGeneratorGeneric<Channels>::Generate(FreetypeHandle* ft, FontHandle* font, const Charset& chset,
const std::optional<std::string>& pngOutput)
{
m_atlasData.reset(new AtlasData);
std::vector<GlyphGeometry> glyphsGeometry;
// FontGeometry is a helper class that loads a set of glyphs from a single font.
FontGeometry fontGeometry(&glyphsGeometry);
fontGeometry.loadCharset(font, 1, chset);
if constexpr (Channels == 3)
{
const double maxCornerAngle = 3.0;
for (GlyphGeometry& glyph: glyphsGeometry)
glyph.edgeColoring(&msdfgen::edgeColoringByDistance, maxCornerAngle, 0);
}
TightAtlasPacker packer;
packer.setDimensionsConstraint(DimensionsConstraint::SQUARE);
int width, height;
const int glyphsPerRow = std::sqrt(glyphsGeometry.size());
const int glyphSize = m_config.glyphSize;
const int rowWidth = glyphSize * glyphsPerRow;
packer.setDimensions(rowWidth, rowWidth);
// something to play with. should not be too high.
// more value - more sdf impact
packer.setPixelRange(m_config.pixelRange);
packer.setMiterLimit(m_config.miterLimit);
packer.pack(glyphsGeometry.data(), glyphsGeometry.size());
packer.getDimensions(width, height);
Generator generator;
generator.resize(width, height);
GeneratorAttributes attributes;
generator.setAttributes(attributes);
generator.setThreadCount(4);
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
const BitmapConstRef<msdfgen::byte, 3> storage = generator.atlasStorage();
msdfgen::byte* data = static_cast<msdfgen::byte*>(m_atlasData->img->data.Data());
for (size_t srcPos = 0, dstPos = 0; srcPos < width * height * 3; srcPos += 3, dstPos += 4)
{
data[dstPos] = storage.pixels[srcPos];
data[dstPos + 1] = storage.pixels[srcPos + 1];
data[dstPos + 2] = storage.pixels[srcPos + 2];
data[dstPos + 3] = 255;
}
}
else
{
const msdfgen::BitmapConstRef<msdfgen::byte, 1>& storage = generator.atlasStorage();
memcpy(m_atlasData->img->data.Data(), storage.pixels, width * height);
}
struct Bbox
{
double l = 0, r = 0, t = 0, b = 0;
};
for (const auto& glyph: glyphsGeometry)
{
GlyphInfo& info = m_atlasData->glyphs[glyph.getCodepoint()];
const GlyphBox& glyphBox = generator.getLayout()[idx++];
Bbox glyphBaselineBbox, glyphAtlasBbox;
glyph.getQuadPlaneBounds(glyphBaselineBbox.l, glyphBaselineBbox.b, glyphBaselineBbox.r,
glyphBaselineBbox.t);
glyph.getQuadAtlasBounds(glyphAtlasBbox.l, glyphAtlasBbox.b, glyphAtlasBbox.r, glyphAtlasBbox.t);
double bearingX = glyphBox.bounds.l;
double bearingY = glyphBox.bounds.t;
double w = glyphBaselineBbox.r - glyphBaselineBbox.l;
double h = glyphBaselineBbox.t - glyphBaselineBbox.b;
double l = glyphAtlasBbox.l;
double r = glyphAtlasBbox.r;
double t = glyphAtlasBbox.t;
double b = glyphAtlasBbox.b;
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()); }
destroyFont(font);
deinitializeFreetype(ft);
}
template class SdfFontAtlasGeneratorGeneric<1>;
template class SdfFontAtlasGeneratorGeneric<3>;
}
#endif

View File

@@ -8,9 +8,7 @@
#if __has_include("msdfgen.h")
#include "Scene/AtlasData.hpp"
#include "IFontAtlasGenerator.hpp"
#include "Scene/Texture.hpp"
#include "FontAtlasGeneratorBase.hpp"
#include <msdfgen.h>
#include <msdf-atlas-gen/msdf-atlas-gen.h>
#include <string>
@@ -22,17 +20,17 @@
namespace OpenVulkano::Scene
{
struct FontAtlasGeneratorConfig
struct SdfFontAtlasGeneratorConfig
{
static FontAtlasGeneratorConfig sdfDefaultConfig;
static FontAtlasGeneratorConfig msdfDefaultConfig;
static SdfFontAtlasGeneratorConfig sdfDefaultConfig;
static SdfFontAtlasGeneratorConfig msdfDefaultConfig;
int glyphSize;
double miterLimit;
msdfgen::Range pixelRange;
};
template<int Channels>
class FontAtlasGenerator : public IFontAtlasGenerator
class SdfFontAtlasGeneratorGeneric final : public FontAtlasGeneratorBase
{
private:
using SdfGenerator = msdf_atlas::ImmediateAtlasGenerator<float, 1, msdf_atlas::sdfGenerator,
@@ -41,22 +39,19 @@ namespace OpenVulkano::Scene
msdf_atlas::BitmapAtlasStorage<msdfgen::byte, 3>>;
public:
using Generator = std::conditional<Channels == 1, SdfGenerator, MsdfGenerator>::type;
using Config = FontAtlasGeneratorConfig;
using Config = SdfFontAtlasGeneratorConfig;
static constexpr int channelsCount = (Channels == 1 ? 1 : 4);
static msdf_atlas::Charset LoadAllGlyphs(const std::variant<std::string, Array<char>>& data);
FontAtlasGenerator();
SdfFontAtlasGeneratorGeneric();
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, int length, const std::set<uint32_t>& charset,
void GenerateAtlas(const Array<char>& fontData, const std::set<uint32_t>& charset,
const std::optional<std::string>& pngOutput = std::nullopt) override;
void GenerateAtlas(const std::string& fontFile, const msdf_atlas::Charset& charset = msdf_atlas::Charset::ASCII,
const std::optional<std::string>& pngOutput = std::nullopt);
void GenerateAtlas(const msdfgen::byte* fontData, int length,
const msdf_atlas::Charset& charset = msdf_atlas::Charset::ASCII,
const std::optional<std::string>& 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<AtlasData> GetAtlasData() const { return m_atlasData; }
Config& GetGeneratorConfig() { return m_config; }
private:
void InitFreetypeFromFile(msdfgen::FreetypeHandle*& ft, msdfgen::FontHandle*& font, const std::string& file);
@@ -64,13 +59,10 @@ 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<std::string>& pngOutput);
void SavePng(const std::string& output) const;
private:
Config m_config;
std::shared_ptr<AtlasData> m_atlasData;
};
using SdfFontAtlasGenerator = FontAtlasGenerator<1>;
using MsdfFontAtlasGenerator = FontAtlasGenerator<3>;
using SdfFontAtlasGenerator = SdfFontAtlasGeneratorGeneric<1>;
using MsdfFontAtlasGenerator = SdfFontAtlasGeneratorGeneric<3>;
}
#endif

View File

@@ -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;
@@ -129,6 +149,10 @@ namespace OpenVulkano::Scene
m_cfg = config;
m_uniBuffer.Init(sizeof(TextConfig), &m_cfg, 3);
m_uniBuffer.binding.stageFlags = ShaderProgramType::FRAGMENT;
if (m_atlasData->meta.atlasType == FontAtlasType::BITMAP)
{
m_material.texture->m_samplerConfig = &SamplerConfig::NEAREST;
}
}
TextDrawable::TextDrawable(const std::shared_ptr<AtlasData>& atlasData, const TextConfig& config)

View File

@@ -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<char>& atlasMetadata, const TextConfig& config = TextConfig());
TextDrawable(const std::string& atlasMetadataFile, const TextConfig& config = TextConfig());

View File

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

View File

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

View File

@@ -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,13 @@ 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);
}
vec4 sampled = vec4(1.0, 1.0, 1.0, texture(texSampler, texCoord).r);
outColor = vec4(textConfig.textColor) * sampled;
}

View File

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

View File

@@ -12,11 +12,19 @@
namespace OpenVulkano::Vulkan
{
namespace
{
constexpr uint32_t SAMPLE_SIZE_WIDTH = 5;
constexpr uint32_t SAMPLE_SIZE_HEIGHT = 5;
constexpr uint32_t SAMPLE_SIZE = SAMPLE_SIZE_WIDTH * SAMPLE_SIZE_HEIGHT;
constexpr uint32_t SAMPLE_CENTER = SAMPLE_SIZE / 2;
}
void DepthBufferQuery::Init()
{
auto device = renderer.GetContext().device->device;
vk::BufferCreateInfo bufferInfo = { {}, 25 * sizeof(float), vk::BufferUsageFlagBits::eTransferDst };
vk::BufferCreateInfo bufferInfo = { {}, SAMPLE_SIZE * sizeof(float), vk::BufferUsageFlagBits::eTransferDst };
bufferDepth = device.createBuffer(bufferInfo);
const vk::MemoryRequirements memRequirements = device.getBufferMemoryRequirements(bufferDepth);
size_t size = memRequirements.size;
@@ -45,32 +53,27 @@ namespace OpenVulkano::Vulkan
float DepthBufferQuery::GetQueriedValue() const
{
if (cpuDepthBuffer[0] == -2) return -2;
if (cpuDepthBuffer[12] > 0 && cpuDepthBuffer[12] < 1) return cpuDepthBuffer[12];
double val = 0;
int validCount = 0;
for (int i = 0; i < 25; i++)
if (cpuDepthBuffer[SAMPLE_CENTER] > 0 && cpuDepthBuffer[SAMPLE_CENTER] < 1) return cpuDepthBuffer[SAMPLE_CENTER];
std::sort(cpuDepthBuffer, cpuDepthBuffer + SAMPLE_SIZE);
uint32_t start = UINT32_MAX, end = UINT32_MAX;
for (uint32_t i = 0; i < SAMPLE_SIZE; i++)
{
float f = cpuDepthBuffer[i];
if (f > 0 && f < 1)
{
val += f;
validCount++;
}
if (f > 0 && start == UINT32_MAX) start = i;
if (f < 1 && end == UINT32_MAX) end = i;
else if (f >= 1) break;
}
if (validCount == 0) return 1; // Prevent divide by 0
return val / validCount;
if (start == UINT32_MAX || end == UINT32_MAX) return cpuDepthBuffer[SAMPLE_CENTER];
return cpuDepthBuffer[(start + end) / 2];
}
vk::Offset3D DepthBufferQuery::GetCopyOffset() const
{
vk::Extent3D depthExtent = renderer.GetContext().swapChain.GetCurrentDepthBuffer().extent;
int32_t x = static_cast<int32_t>(depthQueryCoordinates.x * depthExtent.width);
x = std::min<int32_t>(depthExtent.width - 5, std::max(0, x));
x = std::min<int32_t>(depthExtent.width - SAMPLE_SIZE_WIDTH, std::max(0, x));
int32_t y = static_cast<int32_t>(depthQueryCoordinates.y * depthExtent.height);
y = std::min<int32_t>(depthExtent.height - 5, std::max(0, y));
y = std::min<int32_t>(depthExtent.height - SAMPLE_SIZE_HEIGHT, std::max(0, y));
return { x, y, 0 };
}
@@ -78,13 +81,13 @@ namespace OpenVulkano::Vulkan
{
if (!copyDepthBuffer) return;
copyDepthBuffer = false;
std::fill(cpuDepthBuffer, cpuDepthBuffer + 25, -2.0f); // Invalidate data in buffer to allow detecting if copy is done
std::fill(cpuDepthBuffer, cpuDepthBuffer + SAMPLE_SIZE, -2.0f); // Invalidate data in buffer to allow detecting if copy is done
const vk::ImageAspectFlags aspectMask = vk::ImageAspectFlagBits::eDepth | vk::ImageAspectFlagBits::eStencil;
const Image& depthBufferImage = renderer.GetContext().swapChain.GetCurrentDepthBuffer();
constexpr vk::Extent3D copySize = { 5, 5, 1 };
constexpr vk::Extent3D copySize = { SAMPLE_SIZE_WIDTH, SAMPLE_SIZE_HEIGHT, 1 };
const vk::ImageSubresourceLayers layout = { vk::ImageAspectFlagBits::eDepth, 0, 0, 1 };
vk::BufferImageCopy imgCopy = { 0, 5, 5, layout, GetCopyOffset(), copySize };
vk::BufferImageCopy imgCopy = { 0, 0, 0, layout, GetCopyOffset(), copySize };
const vk::ImageMemoryBarrier imgMemBarrier({}, vk::AccessFlagBits::eTransferRead, vk::ImageLayout::eDepthStencilAttachmentOptimal, vk::ImageLayout::eTransferSrcOptimal, 0, 0, depthBufferImage.image, vk::ImageSubresourceRange(aspectMask, 0, 1, 0, 1));
commandBuffer.pipelineBarrier(vk::PipelineStageFlagBits::eTopOfPipe, vk::PipelineStageFlagBits::eTransfer, {}, nullptr, nullptr, imgMemBarrier);