From 0874f0c36f202722eb74cefac7bc1362d3f825b7 Mon Sep 17 00:00:00 2001 From: Jan Laupetin Date: Wed, 8 Jul 2026 23:35:58 +0200 Subject: [PATCH] feat: font generation from ttf (#882) * chore: add file variant of json fonts * chore: add stb dependency * feat: generate fonts from ttf files * chore: add option to add yOffset to file font * chore: dynamically adjust generated font bitmap size * fix: printable characters should respect ISO-8859-1 control characters * chore: do not write missing glyphs into the bitmap * chore: remove test texture conversion * chore: adjust font compiler for iw3,iw4,iw5,t4,t5 * chore: add possibility to fill color channels when converting * fix: games other than t6 use rgba for fonts * fix: make sure no mipmaps are set on loaddef * fix: t4 and iw3 use dimensions in image loaddef * chore: also include optional glyphs in fonts --- .gitmodules | 3 + premake5.lua | 2 + src/Common/Game/T6/T6_Assets.h | 4 +- src/ObjCommon/Font/FontCommon.cpp | 21 +- src/ObjCommon/Font/JsonFont.h.template | 64 ++- src/ObjCompiling.lua | 2 + .../Font/FontCompiler.cpp.template | 524 ++++++++++++++++++ src/ObjCompiling/Font/FontCompiler.h.template | 23 + src/ObjCompiling/Game/IW3/ObjCompilerIW3.cpp | 2 + src/ObjCompiling/Game/IW4/ObjCompilerIW4.cpp | 2 + src/ObjCompiling/Game/IW5/ObjCompilerIW5.cpp | 2 + src/ObjCompiling/Game/T4/ObjCompilerT4.cpp | 8 +- src/ObjCompiling/Game/T5/ObjCompilerT5.cpp | 2 + src/ObjCompiling/Game/T6/ObjCompilerT6.cpp | 2 + src/ObjImage/Image/TextureConverter.cpp | 27 +- src/ObjImage/Image/TextureConverter.h | 9 + src/ObjLoading/Font/FontLoader.cpp.template | 7 +- src/ObjWriting/Font/FontDumper.cpp.template | 5 +- thirdparty/stb | 1 + thirdparty/stb.lua | 42 ++ thirdparty/stb_impl/stb_truetype.cpp | 2 + 21 files changed, 724 insertions(+), 30 deletions(-) create mode 100644 src/ObjCompiling/Font/FontCompiler.cpp.template create mode 100644 src/ObjCompiling/Font/FontCompiler.h.template create mode 160000 thirdparty/stb create mode 100644 thirdparty/stb.lua create mode 100644 thirdparty/stb_impl/stb_truetype.cpp diff --git a/.gitmodules b/.gitmodules index c3a4e5b0..974c68f7 100644 --- a/.gitmodules +++ b/.gitmodules @@ -22,3 +22,6 @@ [submodule "thirdparty/webwindowed"] path = thirdparty/webwindowed url = https://github.com/Laupetin/webwindowed.git +[submodule "thirdparty/stb"] + path = thirdparty/stb + url = https://github.com/nothings/stb.git diff --git a/premake5.lua b/premake5.lua index 517387fa..0cf82dae 100644 --- a/premake5.lua +++ b/premake5.lua @@ -100,6 +100,7 @@ include "thirdparty/json.lua" include "thirdparty/minilzo.lua" include "thirdparty/minizip.lua" include "thirdparty/salsa20.lua" +include "thirdparty/stb.lua" include "thirdparty/webwindowed.lua" include "thirdparty/zlib.lua" @@ -115,6 +116,7 @@ group "ThirdParty" minilzo:project() minizip:project() salsa20:project() + stb:project() zlib:project() if _OPTIONS["modman"] then diff --git a/src/Common/Game/T6/T6_Assets.h b/src/Common/Game/T6/T6_Assets.h index bf4fab97..bad6882a 100644 --- a/src/Common/Game/T6/T6_Assets.h +++ b/src/Common/Game/T6/T6_Assets.h @@ -838,7 +838,7 @@ namespace T6 uint32_t valid : 1; }; - enum MapType + enum MapType : unsigned char { MAPTYPE_NONE = 0x0, MAPTYPE_INVALID1 = 0x1, @@ -900,7 +900,7 @@ namespace T6 struct GfxImage { GfxTexture texture; - char mapType; + MapType mapType; char semantic; char category; bool delayLoadPixels; diff --git a/src/ObjCommon/Font/FontCommon.cpp b/src/ObjCommon/Font/FontCommon.cpp index e7aab9ad..fc9759b1 100644 --- a/src/ObjCommon/Font/FontCommon.cpp +++ b/src/ObjCommon/Font/FontCommon.cpp @@ -13,6 +13,7 @@ namespace font return path.string(); } + // ISO-8859-1 bool IsPrintableLetter(const unsigned letter) { // Control characters @@ -23,25 +24,17 @@ namespace font if (letter < 0x7F) return true; + // 0xA0 is not a control character but NBSP that just looks like a space + if (letter < 0xA1) + return false; + // There are characters after this point that are printable as well // But they don't seem to play a role in cod fonts if (letter > 0xFF) return false; - switch (letter) - { - case 0x7F: - case 0x81: - case 0x8D: - case 0x8F: - case 0x90: - case 0x9D: - case 0xA0: - case 0xAD: - return false; - default: - return true; - } + // 0xAD is soft hyphen + return letter != 0xAD; } std::string LetterToString(const unsigned letter) diff --git a/src/ObjCommon/Font/JsonFont.h.template b/src/ObjCommon/Font/JsonFont.h.template index a7248cb1..ce974fec 100644 --- a/src/ObjCommon/Font/JsonFont.h.template +++ b/src/ObjCommon/Font/JsonFont.h.template @@ -15,9 +15,11 @@ #include "Json/JsonExtension.h" #include "Font/FontCommon.h" +#include #include #include #include +#include #include namespace GAME @@ -100,7 +102,7 @@ namespace GAME } #endif - class JsonFont + class JsonCompiledFont { public: unsigned pixelHeight; @@ -116,7 +118,7 @@ namespace GAME }; NLOHMANN_DEFINE_TYPE_EXTENSION( - JsonFont, + JsonCompiledFont, pixelHeight, #ifdef FEATURE_T6 isScalingAllowed, @@ -130,4 +132,62 @@ namespace GAME glyphs #endif ); + + class JsonFileFont + { + public: + std::string file; + std::optional fontSize; + std::optional yOffset; +#ifdef FEATURE_T6 + std::optional isScalingAllowed; +#endif + std::optional generatedMaterialName; + std::optional generatedGlowMaterialName; + }; + + NLOHMANN_DEFINE_TYPE_EXTENSION( + JsonFileFont, + file, + fontSize, + yOffset, +#ifdef FEATURE_T6 + isScalingAllowed, +#endif + generatedMaterialName, + generatedGlowMaterialName + ); + + class JsonFont + { + public: + std::variant font; + }; + + NLOHMANN_TO_JSON_METHOD(JsonFont) + { + if (std::holds_alternative(nlohmann_json_t.font)) + { + to_json(nlohmann_json_j, std::get(nlohmann_json_t.font)); + } + else + { + assert(std::holds_alternative(nlohmann_json_t.font)); + to_json(nlohmann_json_j, std::get(nlohmann_json_t.font)); + } + } + + NLOHMANN_FROM_JSON_METHOD(JsonFont) + { + if (nlohmann_json_j.contains("file")) + { + nlohmann_json_t.font.emplace(); + from_json(nlohmann_json_j, std::get(nlohmann_json_t.font)); + } + else + { + nlohmann_json_t.font.emplace(); + from_json(nlohmann_json_j, std::get(nlohmann_json_t.font)); + } + } } // namespace GAME diff --git a/src/ObjCompiling.lua b/src/ObjCompiling.lua index 8070274d..3fff19fd 100644 --- a/src/ObjCompiling.lua +++ b/src/ObjCompiling.lua @@ -15,6 +15,7 @@ end function ObjCompiling:link(links) links:add(self:name()) links:linkto(minilzo) + links:linkto(stb) links:linkto(Utils) links:linkto(ObjCommon) links:linkto(ObjLoading) @@ -58,4 +59,5 @@ function ObjCompiling:project() minilzo:include(includes) Utils:include(includes) json:include(includes) + stb:include(includes) end diff --git a/src/ObjCompiling/Font/FontCompiler.cpp.template b/src/ObjCompiling/Font/FontCompiler.cpp.template new file mode 100644 index 00000000..b31e044e --- /dev/null +++ b/src/ObjCompiling/Font/FontCompiler.cpp.template @@ -0,0 +1,524 @@ +#options GAME (IW3, IW4, IW5, T4, T5, T6) + +#filename "Game/" + GAME + "/Font/FontCompiler" + GAME + ".cpp" + +#if GAME == "IW3" +#define FEATURE_IW3 +#define IWI_NS iwi6 +#define GAME_LOWER "iw3" +#elif GAME == "IW4" +#define FEATURE_IW4 +#define IWI_NS iwi8 +#define GAME_LOWER "iw4" +#elif GAME == "IW5" +#define FEATURE_IW5 +#define IWI_NS iwi8 +#define GAME_LOWER "iw5" +#elif GAME == "T4" +#define FEATURE_T4 +#define IWI_NS iwi6 +#define GAME_LOWER "t4" +#elif GAME == "T5" +#define FEATURE_T5 +#define IWI_NS iwi13 +#define GAME_LOWER "t5" +#elif GAME == "T6" +#define FEATURE_T6 +#define IWI_NS iwi27 +#define GAME_LOWER "t6" +#endif + +// This file was templated. +// See FontCompiler.cpp.template. +// Do not modify, changes will be lost. + +#set COMPILER_HEADER "\"FontCompiler" + GAME + ".h\"" +#include COMPILER_HEADER + +#set COMMON_HEADER "\"Game/" + GAME + "/Common" + GAME + ".h\"" +#include COMMON_HEADER +#include "Font/FontCommon.h" +#set JSON_HEADER "\"Game/" + GAME + "/Font/JsonFont" + GAME + ".h\"" +#include JSON_HEADER +#include "Image/IwiTypes.h" +#include "Image/Texture.h" +#include "Image/TextureConverter.h" +#include "Utils/Alignment.h" +#include "Utils/Logging/Log.h" +#include "Utils/StringUtils.h" + +#include +#include +#include +#include + +using namespace nlohmann; +using namespace GAME; +namespace fs = std::filesystem; + +namespace +{ + constexpr auto REQUIRED_GLYPH_START = 0x20; + constexpr auto OPTIONAL_GLYPH_END_INCLUSIVE = 0xFF; + constexpr unsigned REQUIRED_PLUS_OPTIONAL_GLYPH_COUNT = OPTIONAL_GLYPH_END_INCLUSIVE + 1 - REQUIRED_GLYPH_START; + + constexpr unsigned DEFAULT_FONT_SIZE = 20; + constexpr unsigned DEFAULT_Y_OFFSET = 0; + constexpr unsigned DEFAULT_TEXTURE_WIDTH = 256; + + void PrintError(const Font_s& font, const std::string& message) + { + con::error("Cannot compile font \"{}\": {}", font.fontName, message); + } + + unsigned GetBakedTextureHeight(const stbtt_fontinfo& f, const float scale, const std::vector& glyphs, const unsigned textureWidth) + { + auto x = 1u; + auto y = 1u; + auto bottomY = 1u; + + for (const auto& glyph : glyphs) + { + const auto fontGlyphIndex = stbtt_FindGlyphIndex(&f, glyph.letter); + + int advance, lsb; + stbtt_GetGlyphHMetrics(&f, fontGlyphIndex, &advance, &lsb); + + int x0, y0, x1, y1; + stbtt_GetGlyphBitmapBox(&f, fontGlyphIndex, scale, scale, &x0, &y0, &x1, &y1); + + const auto glyphWidth = static_cast(x1 - x0); + const auto glyphHeight = static_cast(y1 - y0); + if (x + glyphWidth + 1u >= textureWidth) + { + // advance to next row + x = 1u; + y = bottomY; + } + + assert(x + glyphWidth < textureWidth); + + if (fontGlyphIndex > 0) + { + x = x + glyphWidth + 1u; + bottomY = std::max(y + glyphHeight + 1u, bottomY); + } + } + + return utils::Align(bottomY + 1u, 16u); + } + + std::unique_ptr BakeAlphaTexture(const unsigned char* fontData, const unsigned fontSize, const int yOffset, std::vector& glyphs) + { + stbtt_fontinfo f; + if (!stbtt_InitFont(&f, fontData, 0)) + return nullptr; + + const auto scale = stbtt_ScaleForPixelHeight(&f, static_cast(fontSize)); + + constexpr auto textureWidth = DEFAULT_TEXTURE_WIDTH; + const auto textureHeight = GetBakedTextureHeight(f, scale, glyphs, textureWidth); + + auto texture = image::Texture::CreateForType(image::TextureType::T_2D, &image::format::A8, textureWidth, textureHeight, 1, false); + texture->Allocate(); + auto* buf = texture->GetBufferForMipLevel(0); + + f.userdata = nullptr; + memset(buf, 0, textureWidth * textureHeight); + + auto x = 1u; + auto y = 1u; + auto bottomY = 1u; + for (auto& glyph : glyphs) + { + const auto fontGlyphIndex = stbtt_FindGlyphIndex(&f, glyph.letter); + + int advance, lsb; + stbtt_GetGlyphHMetrics(&f, fontGlyphIndex, &advance, &lsb); + + int x0, y0, x1, y1; + stbtt_GetGlyphBitmapBox(&f, fontGlyphIndex, scale, scale, &x0, &y0, &x1, &y1); + + const auto glyphWidth = static_cast(x1 - x0); + const auto glyphHeight = static_cast(y1 - y0); + if (x + glyphWidth + 1 >= textureWidth) + { + // advance to next row + x = 1; + y = bottomY; + } + + glyph.x0 = static_cast(x0); + glyph.y0 = static_cast(y0 + yOffset); + glyph.dx = static_cast(std::round(scale * static_cast(advance))); + + // Glyph index 0 means no glyph found in font + if (fontGlyphIndex > 0) + { + // check if it fits vertically AFTER potentially moving to next row + assert(y + glyphHeight + 1u < textureHeight); + if (y + glyphHeight + 1u >= textureHeight) + return nullptr; + + assert(x + glyphWidth < textureWidth); + assert(y + glyphHeight < textureHeight); + + stbtt_MakeGlyphBitmap( + &f, buf + x + y * textureWidth, static_cast(glyphWidth), static_cast(glyphHeight), textureWidth, scale, scale, fontGlyphIndex); + + glyph.pixelWidth = static_cast(glyphWidth); + glyph.pixelHeight = static_cast(glyphHeight); + glyph.s0 = static_cast(x) / static_cast(textureWidth); + glyph.t0 = static_cast(y) / static_cast(textureHeight); + glyph.s1 = static_cast(x + glyphWidth) / static_cast(textureWidth); + glyph.t1 = static_cast(y + glyphHeight) / static_cast(textureHeight); + + x = x + glyphWidth + 1u; + bottomY = std::max(y + glyphHeight + 1u, bottomY); + } + else + { + // If the font does not have the glyph we set it to empty space + glyph.pixelWidth = 0; + glyph.pixelHeight = 0; + glyph.s0 = 1.0f / static_cast(textureWidth); + glyph.t0 = 1.0f / static_cast(textureHeight); + glyph.s1 = glyph.s0; + glyph.t1 = glyph.t0; + } + } + + return texture; + } + + XAssetInfo* CreateFontImage(const std::string& name, const image::Texture& texture, MemoryManager& memory, AssetCreationContext& context) + { + auto* image = memory.Alloc(); + image->name = memory.Dup(name.c_str()); + image->width = static_cast(texture.GetWidth()); + image->height = static_cast(texture.GetHeight()); + image->depth = static_cast(texture.GetDepth()); + image->mapType = MAPTYPE_2D; + image->semantic = TS_2D; + image->noPicmip = true; + + const auto textureSize = texture.GetFormat()->GetSizeOfMipLevel(0, image->width, image->height, image->depth); + auto* loadDef = static_cast(memory.AllocRaw(offsetof(GfxImageLoadDef, data) + textureSize)); + image->texture.loadDef = loadDef; + +#if defined(FEATURE_IW3) || defined(FEATURE_T4) + loadDef->dimensions[0] = image->width; + loadDef->dimensions[1] = image->height; + loadDef->dimensions[2] = image->depth; +#endif + + loadDef->resourceSize = static_cast(textureSize); +#ifdef FEATURE_T6 + loadDef->format = texture.GetFormat()->GetDxgiFormat(); +#else + loadDef->format = texture.GetFormat()->GetD3DFormat(); +#endif + loadDef->levelCount = 1; + std::memcpy(loadDef->data, texture.GetBufferForMipLevel(0), textureSize); + + loadDef->flags |= image::IWI_NS::IMG_FLAG_NOMIPMAPS; + + return context.AddAsset(name, image); + } + + XAssetInfo* CreateFontMaterialShared(Material* material, AssetRegistration& registration, XAssetInfo* image, MemoryManager& memory, AssetCreationContext& context) + { + material->cameraRegion = CAMERA_REGION_NONE; + +#ifdef defined(FEATURE_IW4) || defined(FEATURE_IW5) + material->stateFlags = 3; +#endif +#ifdef FEATURE_T6 + material->info.contents = 1; +#endif +#if defined(FEATURE_T5) || defined(FEATURE_T6) + material->info.layeredSurfaceTypes = 0x20000000; +#endif + material->info.textureAtlasColumnCount = 1; + material->info.textureAtlasRowCount = 1; + + for (auto& entry : material->stateBitsEntry) + entry = -1; + material->stateBitsEntry[TECHNIQUE_UNLIT] = 0; + + material->stateBitsCount = 1; + material->stateBitsTable = memory.Alloc(1); + auto& stateBits = material->stateBitsTable[0].loadBits.structured; + stateBits.srcBlendRgb = GFXS_BLEND_SRCALPHA; + stateBits.dstBlendRgb = GFXS_BLEND_INVSRCALPHA; + stateBits.blendOpRgb = GFXS_BLENDOP_ADD; + stateBits.alphaTest = GFXS_ALPHA_TEST_GT_0; + stateBits.cullFace = GFXS_CULL_BACK; + stateBits.srcBlendAlpha = GFXS_BLEND_INVDESTALPHA; + stateBits.dstBlendAlpha = GFXS_BLEND_ONE; + stateBits.blendOpAlpha = GFXS_BLENDOP_ADD; + stateBits.colorWriteRgb = 1; + stateBits.colorWriteAlpha = 1; + stateBits.polymodeLine = 0; + stateBits.depthWrite = 0; + stateBits.depthTestDisabled = 1; + stateBits.polygonOffset = GFXS_POLYGON_OFFSET_0; + + material->textureCount = 1; + material->textureTable = memory.Alloc(1); + auto& textureDef = material->textureTable[0]; + +#if defined(FEATURE_T6) + // Yes, there is a typo. That's what the game does. + textureDef.nameHash = Common::R_HashString("FontTextutre"); + textureDef.nameStart = 'F'; + textureDef.nameEnd = 'e'; +#else + textureDef.nameHash = Common::R_HashString("colorMap"); + textureDef.nameStart = 'c'; + textureDef.nameEnd = 'p'; +#endif + + textureDef.samplerState.filter = TEXTURE_FILTER_LINEAR; + textureDef.samplerState.mipMap = SAMPLER_MIPMAP_ENUM_DISABLED; + textureDef.semantic = TS_2D; +#ifdef FEATURE_T6 + textureDef.image = image->Asset(); +#else + textureDef.u.image = image->Asset(); +#endif + registration.AddDependency(image); + + return context.AddAsset(std::move(registration)); + } + + XAssetInfo* CreateFontMaterial(const std::string& name, XAssetInfo* image, MemoryManager& memory, AssetCreationContext& context) + { + auto* material = memory.Alloc(); + material->info.name = memory.Dup(name.c_str()); + AssetRegistration registration(name, material); + + CreateFontMaterialShared(material, registration, image, memory, context); + +#if defined(FEATURE_IW4) + material->info.sortKey = 47; +#elif defined(FEATURE_IW5) + material->info.sortKey = 54; +#elif defined(FEATURE_IW3) || defined(FEATURE_T4) || defined(FEATURE_T5) + material->info.sortKey = 43; +#elif defined(FEATURE_T6) + material->info.sortKey = 40; +#endif + + auto* techsetAsset = context.LoadDependency( +#if defined(FEATURE_T6) + "sw4_2d_alphafont_3q76z816" +#else + "2d" +#endif + ); + if (!techsetAsset) + return nullptr; + material->techniqueSet = techsetAsset->Asset(); + registration.AddDependency(techsetAsset); + + auto& stateBits = material->stateBitsTable[0].loadBits.structured; + stateBits.dstBlendRgb = GFXS_BLEND_INVSRCALPHA; +#if defined(FEATURE_IW4) || defined(FEATURE_IW5) + stateBits.dstBlendAlpha = GFXS_BLEND_ZERO; +#endif + + return context.AddAsset(std::move(registration)); + } + + XAssetInfo* CreateFontGlowMaterial(const std::string& name, XAssetInfo* image, MemoryManager& memory, AssetCreationContext& context) + { + auto* material = memory.Alloc(); + material->info.name = memory.Dup(name.c_str()); + AssetRegistration registration(name, material); + + CreateFontMaterialShared(material, registration, image, memory, context); + +#if defined(FEATURE_IW4) + material->info.sortKey = 34; +#elif defined(FEATURE_IW5) + material->info.sortKey = 54; +#elif defined(FEATURE_IW3) || defined(FEATURE_T4) || defined(FEATURE_T5) || defined(FEATURE_T6) + material->info.sortKey = 4; +#endif + + auto* techsetAsset = context.LoadDependency( +#if defined(FEATURE_T6) + "sw4_2d_alphafont_3340wf8q" +#else + "2d" +#endif + ); + if (!techsetAsset) + return nullptr; + material->techniqueSet = techsetAsset->Asset(); + registration.AddDependency(techsetAsset); + + auto& stateBits = material->stateBitsTable[0].loadBits.structured; + stateBits.dstBlendRgb = GFXS_BLEND_ONE; + + return context.AddAsset(std::move(registration)); + } + + class FontCompiler final : public AssetCreator + { + public: + FontCompiler(MemoryManager& memory, ISearchPath& searchPath) + : m_memory(memory), + m_search_path(searchPath) + { + } + + AssetCreationResult CreateAsset(const std::string& assetName, AssetCreationContext& context) override + { + const auto file = m_search_path.Open(font::GetJsonFileNameForAssetName(assetName)); + if (!file.IsOpen()) + return AssetCreationResult::NoAction(); + + auto* font = m_memory.Alloc(); + font->fontName = m_memory.Dup(assetName.c_str()); + AssetRegistration registration(assetName, font); + + try + { + const auto jRoot = json::parse(*file.m_stream); + std::string type; + unsigned version; + + jRoot.at("_type").get_to(type); + jRoot.at("_version").get_to(version); + + if (type != "font" || version != 1u) + { + con::error(R"(Tried to load font "{}" but did not find expected type font of version 1)", assetName); + return AssetCreationResult::Failure(); + } + + std::string game; + jRoot.at("_game").get_to(game); + if (game != GAME_LOWER) + { + con::error(R"(Tried to load font "{}" but "_game" did not have expected value {})", assetName, GAME_LOWER); + return AssetCreationResult::Failure(); + } + + const auto jFont = jRoot.get(); + if (!std::holds_alternative(jFont.font)) + return AssetCreationResult::NoAction(); + + if (CreateFontFromJson(std::get(jFont.font), *font, registration, context)) + return AssetCreationResult::Success(context.AddAsset(std::move(registration))); + } + catch (const json::exception& e) + { + con::error("Failed to parse json of font: {}", e.what()); + } + + return AssetCreationResult::Failure(); + } + + private: + bool CreateFontFromJson(const JsonFileFont& jFont, Font_s& font, AssetRegistration& registration, AssetCreationContext& context) const + { + const auto fontSize = jFont.fontSize.value_or(DEFAULT_FONT_SIZE); + const auto yOffset = jFont.yOffset.value_or(DEFAULT_Y_OFFSET); + font.pixelHeight = static_cast(fontSize); + +#ifdef FEATURE_T6 + const auto scalingAllowed = jFont.isScalingAllowed.value_or(false); + font.isScalingAllowed = scalingAllowed ? 1 : 0; +#endif + + const auto extension = fs::path(jFont.file).extension().string(); + if (!utils::StringEqualsIgnoreCase(extension, ".ttf")) + { + PrintError(font, std::format("Only ttf files are supported: {}", jFont.file)); + return false; + } + + const auto fontFile = m_search_path.Open(jFont.file); + if (!fontFile.IsOpen()) + { + PrintError(font, std::format("Failed to open font file: {}", jFont.file)); + return false; + } + + std::vector fontData(static_cast(fontFile.m_length)); + fontFile.m_stream->read(reinterpret_cast(fontData.data()), fontFile.m_length); + + std::vector glyphs; + glyphs.reserve(REQUIRED_PLUS_OPTIONAL_GLYPH_COUNT); + for (auto letter = REQUIRED_GLYPH_START; letter <= OPTIONAL_GLYPH_END_INCLUSIVE; letter++) + { + Glyph glyph{}; + glyph.letter = static_cast(letter); + glyphs.emplace_back(glyph); + } + + auto texture = BakeAlphaTexture(fontData.data(), fontSize, yOffset, glyphs); + if (!texture) + { + PrintError(font, std::format("Failed to bake font alpha texture: {}", jFont.file)); + return false; + } + +#ifndef FEATURE_T6 + { + image::TextureConverter converter(texture.get(), &image::format::B8_G8_R8_A8); + converter.SetColorFill(true, true, true); + texture = converter.Convert(); + } +#endif + + font.glyphCount = static_cast(glyphs.size()); + font.glyphs = m_memory.Alloc(font.glyphCount); + std::memcpy(font.glyphs, glyphs.data(), font.glyphCount * sizeof(Glyph)); + +#ifdef FEATURE_T6 + // The game is bugged here and accesses the kerning pairs even when kerningPairsCount is set to 0. + // It always checks the first entry so we add a nonsense entry here that never matches + // to ensure that no kerning is applied on randomly matching bytes. + font.kerningPairsCount = 1; + font.kerningPairs = m_memory.Alloc(1); + font.kerningPairs[0].wFirst = 0; + font.kerningPairs[0].wSecond = 0; + font.kerningPairs[0].iKernAmount = 0; +#endif + + auto materialName = jFont.generatedMaterialName.value_or(font.fontName); + auto* image = CreateFontImage(materialName, *texture, m_memory, context); + + auto fontMaterial = CreateFontMaterial(materialName, image, m_memory, context); + if (!fontMaterial) + return false; + font.material = fontMaterial->Asset(); + registration.AddDependency(fontMaterial); + + auto glowMaterialName = jFont.generatedGlowMaterialName.value_or(std::format("{}_glow", materialName)); + auto fontGlowMaterial = CreateFontGlowMaterial(glowMaterialName, image, m_memory, context); + if (!fontGlowMaterial) + return false; + font.glowMaterial = fontGlowMaterial->Asset(); + registration.AddDependency(fontGlowMaterial); + + return true; + } + + MemoryManager& m_memory; + ISearchPath& m_search_path; + }; +} // namespace + +namespace font +{ +#set CREATE_COMPILER_METHOD "CreateCompiler" + GAME + std::unique_ptr> CREATE_COMPILER_METHOD(MemoryManager& memory, ISearchPath& searchPath) + { + return std::make_unique(memory, searchPath); + } +} // namespace font diff --git a/src/ObjCompiling/Font/FontCompiler.h.template b/src/ObjCompiling/Font/FontCompiler.h.template new file mode 100644 index 00000000..86eeed20 --- /dev/null +++ b/src/ObjCompiling/Font/FontCompiler.h.template @@ -0,0 +1,23 @@ +#options GAME (IW3, IW4, IW5, T4, T5, T6) + +#filename "Game/" + GAME + "/Font/FontCompiler" + GAME + ".h" + +// This file was templated. +// See FontCompiler.h.template. +// Do not modify, changes will be lost. + +#pragma once + +#include "Asset/IAssetCreator.h" +#set GAME_HEADER "\"Game/" + GAME + "/" + GAME + ".h\"" +#include GAME_HEADER +#include "SearchPath/ISearchPath.h" +#include "Utils/MemoryManager.h" + +#include + +namespace font +{ +#set CREATE_COMPILER_METHOD "CreateCompiler" + GAME + std::unique_ptr> CREATE_COMPILER_METHOD(MemoryManager& memory, ISearchPath& searchPath); +} // namespace font diff --git a/src/ObjCompiling/Game/IW3/ObjCompilerIW3.cpp b/src/ObjCompiling/Game/IW3/ObjCompilerIW3.cpp index ad084e43..5a1fbaad 100644 --- a/src/ObjCompiling/Game/IW3/ObjCompilerIW3.cpp +++ b/src/ObjCompiling/Game/IW3/ObjCompilerIW3.cpp @@ -1,5 +1,6 @@ #include "ObjCompilerIW3.h" +#include "Game/IW3/Font/FontCompilerIW3.h" #include "Game/IW3/IW3.h" #include "Game/IW3/Techset/TechniqueCompilerIW3.h" #include "Game/IW3/Techset/TechsetCompilerIW3.h" @@ -17,6 +18,7 @@ namespace auto& memory = zone.Memory(); collection.AddAssetCreator(techset::CreateTechsetCompilerIW3(memory, searchPath)); + collection.AddAssetCreator(font::CreateCompilerIW3(memory, searchPath)); collection.AddSubAssetCreator(techset::CreateTechniqueCompilerIW3(memory, zone, searchPath)); collection.AddSubAssetCreator(techset::CreateVertexDeclCompilerIW3(memory)); diff --git a/src/ObjCompiling/Game/IW4/ObjCompilerIW4.cpp b/src/ObjCompiling/Game/IW4/ObjCompilerIW4.cpp index 88457744..293a2eab 100644 --- a/src/ObjCompiling/Game/IW4/ObjCompilerIW4.cpp +++ b/src/ObjCompiling/Game/IW4/ObjCompilerIW4.cpp @@ -1,5 +1,6 @@ #include "ObjCompilerIW4.h" +#include "Game/IW4/Font/FontCompilerIW4.h" #include "Game/IW4/IW4.h" #include "Game/IW4/Techset/TechniqueCompilerIW4.h" #include "Game/IW4/Techset/TechsetCompilerIW4.h" @@ -22,6 +23,7 @@ namespace #endif collection.AddAssetCreator(techset::CreateVertexDeclCompilerIW4(memory)); collection.AddAssetCreator(techset::CreateTechsetCompilerIW4(memory, searchPath)); + collection.AddAssetCreator(font::CreateCompilerIW4(memory, searchPath)); collection.AddSubAssetCreator(techset::CreateTechniqueCompilerIW4(memory, zone, searchPath)); } diff --git a/src/ObjCompiling/Game/IW5/ObjCompilerIW5.cpp b/src/ObjCompiling/Game/IW5/ObjCompilerIW5.cpp index 5a6ba6d3..2c6f2f3b 100644 --- a/src/ObjCompiling/Game/IW5/ObjCompilerIW5.cpp +++ b/src/ObjCompiling/Game/IW5/ObjCompilerIW5.cpp @@ -1,5 +1,6 @@ #include "ObjCompilerIW5.h" +#include "Game/IW5/Font/FontCompilerIW5.h" #include "Game/IW5/IW5.h" #include "Game/IW5/Techset/TechniqueCompilerIW5.h" #include "Game/IW5/Techset/TechsetCompilerIW5.h" @@ -18,6 +19,7 @@ namespace collection.AddAssetCreator(techset::CreateVertexDeclCompilerIW5(memory)); collection.AddAssetCreator(techset::CreateTechsetCompilerIW5(memory, searchPath)); + collection.AddAssetCreator(font::CreateCompilerIW5(memory, searchPath)); collection.AddSubAssetCreator(techset::CreateTechniqueCompilerIW5(memory, zone, searchPath)); } diff --git a/src/ObjCompiling/Game/T4/ObjCompilerT4.cpp b/src/ObjCompiling/Game/T4/ObjCompilerT4.cpp index aaae5282..59df2745 100644 --- a/src/ObjCompiling/Game/T4/ObjCompilerT4.cpp +++ b/src/ObjCompiling/Game/T4/ObjCompilerT4.cpp @@ -1,5 +1,6 @@ #include "ObjCompilerT4.h" +#include "Game/T4/Font/FontCompilerT4.h" #include "Game/T4/T4.h" #include "Image/ImageIwdPostProcessor.h" @@ -9,7 +10,12 @@ using namespace T4; namespace { - void ConfigureCompilers(AssetCreatorCollection& collection, Zone& zone, ISearchPath& searchPath) {} + void ConfigureCompilers(AssetCreatorCollection& collection, Zone& zone, ISearchPath& searchPath) + { + auto& memory = zone.Memory(); + + collection.AddAssetCreator(font::CreateCompilerT4(memory, searchPath)); + } void ConfigurePostProcessors(AssetCreatorCollection& collection, Zone& zone, diff --git a/src/ObjCompiling/Game/T5/ObjCompilerT5.cpp b/src/ObjCompiling/Game/T5/ObjCompilerT5.cpp index 12a63556..f3ec7b1c 100644 --- a/src/ObjCompiling/Game/T5/ObjCompilerT5.cpp +++ b/src/ObjCompiling/Game/T5/ObjCompilerT5.cpp @@ -1,5 +1,6 @@ #include "ObjCompilerT5.h" +#include "Game/T5/Font/FontCompilerT5.h" #include "Game/T5/T5.h" #include "Game/T5/Techset/TechniqueCompilerT5.h" #include "Game/T5/Techset/TechsetCompilerT5.h" @@ -17,6 +18,7 @@ namespace auto& memory = zone.Memory(); collection.AddAssetCreator(techset::CreateTechsetCompilerT5(memory, searchPath)); + collection.AddAssetCreator(font::CreateCompilerT5(memory, searchPath)); collection.AddSubAssetCreator(techset::CreateTechniqueCompilerT5(memory, zone, searchPath)); collection.AddSubAssetCreator(techset::CreateVertexDeclCompilerT5(memory)); diff --git a/src/ObjCompiling/Game/T6/ObjCompilerT6.cpp b/src/ObjCompiling/Game/T6/ObjCompilerT6.cpp index 37ac9529..158fcbea 100644 --- a/src/ObjCompiling/Game/T6/ObjCompilerT6.cpp +++ b/src/ObjCompiling/Game/T6/ObjCompilerT6.cpp @@ -1,5 +1,6 @@ #include "ObjCompilerT6.h" +#include "Game/T6/Font/FontCompilerT6.h" #include "Game/T6/T6.h" #include "Game/T6/Techset/TechniqueCompilerT6.h" #include "Game/T6/Techset/TechsetCompilerT6.h" @@ -24,6 +25,7 @@ namespace collection.AddAssetCreator(key_value_pairs::CreateCompilerT6(memory, zone, zoneDefinition.m_zone_definition, zoneStates)); collection.AddAssetCreator(techset::CreateTechsetCompilerT6(memory, searchPath)); + collection.AddAssetCreator(font::CreateCompilerT6(memory, searchPath)); collection.AddSubAssetCreator(techset::CreateTechniqueCompilerT6(memory, zone, searchPath)); collection.AddSubAssetCreator(techset::CreateVertexDeclCompilerT6(memory)); diff --git a/src/ObjImage/Image/TextureConverter.cpp b/src/ObjImage/Image/TextureConverter.cpp index a3ccf56c..144e321d 100644 --- a/src/ObjImage/Image/TextureConverter.cpp +++ b/src/ObjImage/Image/TextureConverter.cpp @@ -115,7 +115,10 @@ namespace image } TextureConverter::TextureConverter(const Texture* inputTexture, const ImageFormat* targetFormat) - : m_input_texture(inputTexture), + : m_fill_r(false), + m_fill_g(false), + m_fill_b(false), + m_input_texture(inputTexture), m_output_texture(nullptr), m_input_format(inputTexture->GetFormat()), m_output_format(targetFormat) @@ -158,10 +161,13 @@ namespace image const auto gInputMask = inputFormat->HasG() ? Mask1(inputFormat->m_g_size) << inputFormat->m_g_offset : 0; const auto bInputMask = inputFormat->HasB() ? Mask1(inputFormat->m_b_size) << inputFormat->m_b_offset : 0; const auto aInputMask = inputFormat->HasA() ? Mask1(inputFormat->m_a_size) << inputFormat->m_a_offset : 0; + const auto rFill = m_fill_r ? Mask1(outputFormat->m_r_size) << outputFormat->m_r_offset : 0; + const auto gFill = m_fill_g ? Mask1(outputFormat->m_g_size) << outputFormat->m_g_offset : 0; + const auto bFill = m_fill_b ? Mask1(outputFormat->m_b_size) << outputFormat->m_b_offset : 0; const auto aOutputMask = outputFormat->HasA() ? Mask1(outputFormat->m_a_size) << outputFormat->m_a_offset : 0; - const bool rConvert = rInputMask != 0 && outputFormat->m_r_size > 0; - const bool gConvert = gInputMask != 0 && outputFormat->m_g_size > 0; - const bool bConvert = bInputMask != 0 && outputFormat->m_b_size > 0; + const bool rConvert = (rInputMask != 0 || rFill) && outputFormat->m_r_size > 0; + const bool gConvert = (gInputMask != 0 || gFill) && outputFormat->m_g_size > 0; + const bool bConvert = (bInputMask != 0 || bFill) && outputFormat->m_b_size > 0; // alpha has a default of 1 so we need to convert even input has no alpha const bool aConvert = outputFormat->m_a_size > 0; @@ -182,11 +188,11 @@ namespace image const auto inPixel = m_read_pixel_func(&inputBuffer[inputOffset], inputFormat->m_bits_per_pixel); if (rConvert) - outPixel |= (inPixel & rInputMask) >> inputFormat->m_r_offset << outputFormat->m_r_offset; + outPixel |= ((inPixel & rInputMask) >> inputFormat->m_r_offset << outputFormat->m_r_offset) | rFill; if (gConvert) - outPixel |= (inPixel & gInputMask) >> inputFormat->m_g_offset << outputFormat->m_g_offset; + outPixel |= ((inPixel & gInputMask) >> inputFormat->m_g_offset << outputFormat->m_g_offset) | gFill; if (bConvert) - outPixel |= (inPixel & bInputMask) >> inputFormat->m_b_offset << outputFormat->m_b_offset; + outPixel |= ((inPixel & bInputMask) >> inputFormat->m_b_offset << outputFormat->m_b_offset) | bFill; if (aConvert) { @@ -222,6 +228,13 @@ namespace image } } + void TextureConverter::SetColorFill(const bool fillR, const bool fillG, const bool fillB) + { + m_fill_r = fillR; + m_fill_g = fillG; + m_fill_b = fillB; + } + std::unique_ptr TextureConverter::Convert() { CreateOutputTexture(); diff --git a/src/ObjImage/Image/TextureConverter.h b/src/ObjImage/Image/TextureConverter.h index bdec57aa..ae08f749 100644 --- a/src/ObjImage/Image/TextureConverter.h +++ b/src/ObjImage/Image/TextureConverter.h @@ -12,6 +12,8 @@ namespace image public: TextureConverter(const Texture* inputTexture, const ImageFormat* targetFormat); + void SetColorFill(bool fillR, bool fillG, bool fillB); + std::unique_ptr Convert(); private: @@ -25,6 +27,13 @@ namespace image std::function m_read_pixel_func; std::function m_write_pixel_func; + struct + { + bool m_fill_r : 1; + bool m_fill_g : 1; + bool m_fill_b : 1; + }; + const Texture* m_input_texture; std::unique_ptr m_output_texture; const ImageFormat* m_input_format; diff --git a/src/ObjLoading/Font/FontLoader.cpp.template b/src/ObjLoading/Font/FontLoader.cpp.template index 02543156..8446ca89 100644 --- a/src/ObjLoading/Font/FontLoader.cpp.template +++ b/src/ObjLoading/Font/FontLoader.cpp.template @@ -238,7 +238,10 @@ namespace } const auto jFont = jRoot.get(); - if (CreateFontFromJson(jFont, *font, registration, context)) + if (!std::holds_alternative(jFont.font)) + return AssetCreationResult::NoAction(); + + if (CreateFontFromJson(std::get(jFont.font), *font, registration, context)) return AssetCreationResult::Success(context.AddAsset(std::move(registration))); } catch (const json::exception& e) @@ -250,7 +253,7 @@ namespace } private: - bool CreateFontFromJson(const JsonFont& jFont, Font_s& font, AssetRegistration& registration, AssetCreationContext& context) const + bool CreateFontFromJson(const JsonCompiledFont& jFont, Font_s& font, AssetRegistration& registration, AssetCreationContext& context) const { font.pixelHeight = static_cast(jFont.pixelHeight); diff --git a/src/ObjWriting/Font/FontDumper.cpp.template b/src/ObjWriting/Font/FontDumper.cpp.template index 4dc6720f..427d3f99 100644 --- a/src/ObjWriting/Font/FontDumper.cpp.template +++ b/src/ObjWriting/Font/FontDumper.cpp.template @@ -83,7 +83,7 @@ namespace } #endif - void CreateJsonFont(JsonFont& jFont, const Font_s& font) + void CreateCompiledJsonFont(JsonCompiledFont& jFont, const Font_s& font) { jFont.pixelHeight = font.pixelHeight; @@ -125,7 +125,8 @@ namespace void Dump(const Font_s& font) const { JsonFont jFont; - CreateJsonFont(jFont, font); + jFont.font.emplace(); + CreateCompiledJsonFont(std::get(jFont.font), font); ordered_json jRoot; jRoot["$schema"] = "http://openassettools.dev/schema/font.v1.json"; diff --git a/thirdparty/stb b/thirdparty/stb new file mode 160000 index 00000000..31c1ad37 --- /dev/null +++ b/thirdparty/stb @@ -0,0 +1 @@ +Subproject commit 31c1ad37456438565541f4919958214b6e762fb4 diff --git a/thirdparty/stb.lua b/thirdparty/stb.lua new file mode 100644 index 00000000..b3fa0bef --- /dev/null +++ b/thirdparty/stb.lua @@ -0,0 +1,42 @@ +stb = {} + +function stb:include(includes) + if includes:handle(self:name()) then + includedirs { + path.join(ThirdPartyFolder(), "stb") + } + end +end + +function stb:link(links) + links:add(self:name()) +end + +function stb:use() + +end + +function stb:name() + return "stb" +end + +function stb:project() + local folder = ThirdPartyFolder() + local includes = Includes:create() + + project(self:name()) + targetdir(TargetDirectoryLib) + location "%{wks.location}/thirdparty/%{prj.name}" + kind "StaticLib" + language "C++" + + files { + path.join(folder, "stb/*.h"), + path.join(folder, "stb_impl/*.cpp") + } + + self:include(includes) + + -- Disable warnings. They do not have any value to us since it is not our code. + warnings "off" +end diff --git a/thirdparty/stb_impl/stb_truetype.cpp b/thirdparty/stb_impl/stb_truetype.cpp new file mode 100644 index 00000000..dc22d882 --- /dev/null +++ b/thirdparty/stb_impl/stb_truetype.cpp @@ -0,0 +1,2 @@ +#define STB_TRUETYPE_IMPLEMENTATION +#include "stb_truetype.h"