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
This commit is contained in:
Jan Laupetin
2026-07-08 23:35:58 +02:00
committed by GitHub
parent c385f50a0e
commit 0874f0c36f
21 changed files with 724 additions and 30 deletions
+3
View File
@@ -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
+2
View File
@@ -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
+2 -2
View File
@@ -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;
+7 -14
View File
@@ -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)
+62 -2
View File
@@ -15,9 +15,11 @@
#include "Json/JsonExtension.h"
#include "Font/FontCommon.h"
#include <cassert>
#include <nlohmann/json.hpp>
#include <optional>
#include <string>
#include <variant>
#include <vector>
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<unsigned> fontSize;
std::optional<int> yOffset;
#ifdef FEATURE_T6
std::optional<bool> isScalingAllowed;
#endif
std::optional<std::string> generatedMaterialName;
std::optional<std::string> generatedGlowMaterialName;
};
NLOHMANN_DEFINE_TYPE_EXTENSION(
JsonFileFont,
file,
fontSize,
yOffset,
#ifdef FEATURE_T6
isScalingAllowed,
#endif
generatedMaterialName,
generatedGlowMaterialName
);
class JsonFont
{
public:
std::variant<JsonCompiledFont, JsonFileFont> font;
};
NLOHMANN_TO_JSON_METHOD(JsonFont)
{
if (std::holds_alternative<JsonCompiledFont>(nlohmann_json_t.font))
{
to_json(nlohmann_json_j, std::get<JsonCompiledFont>(nlohmann_json_t.font));
}
else
{
assert(std::holds_alternative<JsonFileFont>(nlohmann_json_t.font));
to_json(nlohmann_json_j, std::get<JsonFileFont>(nlohmann_json_t.font));
}
}
NLOHMANN_FROM_JSON_METHOD(JsonFont)
{
if (nlohmann_json_j.contains("file"))
{
nlohmann_json_t.font.emplace<JsonFileFont>();
from_json(nlohmann_json_j, std::get<JsonFileFont>(nlohmann_json_t.font));
}
else
{
nlohmann_json_t.font.emplace<JsonCompiledFont>();
from_json(nlohmann_json_j, std::get<JsonCompiledFont>(nlohmann_json_t.font));
}
}
} // namespace GAME
+2
View File
@@ -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
@@ -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 <format>
#include <filesystem>
#include <nlohmann/json.hpp>
#include <stb_truetype.h>
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<Glyph>& 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<unsigned>(x1 - x0);
const auto glyphHeight = static_cast<unsigned>(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<unsigned>(bottomY + 1u, 16u);
}
std::unique_ptr<image::Texture> BakeAlphaTexture(const unsigned char* fontData, const unsigned fontSize, const int yOffset, std::vector<Glyph>& glyphs)
{
stbtt_fontinfo f;
if (!stbtt_InitFont(&f, fontData, 0))
return nullptr;
const auto scale = stbtt_ScaleForPixelHeight(&f, static_cast<float>(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<unsigned>(x1 - x0);
const auto glyphHeight = static_cast<unsigned>(y1 - y0);
if (x + glyphWidth + 1 >= textureWidth)
{
// advance to next row
x = 1;
y = bottomY;
}
glyph.x0 = static_cast<decltype(Glyph::x0)>(x0);
glyph.y0 = static_cast<decltype(Glyph::y0)>(y0 + yOffset);
glyph.dx = static_cast<decltype(Glyph::dx)>(std::round(scale * static_cast<float>(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<int>(glyphWidth), static_cast<int>(glyphHeight), textureWidth, scale, scale, fontGlyphIndex);
glyph.pixelWidth = static_cast<decltype(Glyph::pixelWidth)>(glyphWidth);
glyph.pixelHeight = static_cast<decltype(Glyph::pixelHeight)>(glyphHeight);
glyph.s0 = static_cast<float>(x) / static_cast<float>(textureWidth);
glyph.t0 = static_cast<float>(y) / static_cast<float>(textureHeight);
glyph.s1 = static_cast<float>(x + glyphWidth) / static_cast<float>(textureWidth);
glyph.t1 = static_cast<float>(y + glyphHeight) / static_cast<float>(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<float>(textureWidth);
glyph.t0 = 1.0f / static_cast<float>(textureHeight);
glyph.s1 = glyph.s0;
glyph.t1 = glyph.t0;
}
}
return texture;
}
XAssetInfo<GfxImage>* CreateFontImage(const std::string& name, const image::Texture& texture, MemoryManager& memory, AssetCreationContext& context)
{
auto* image = memory.Alloc<GfxImage>();
image->name = memory.Dup(name.c_str());
image->width = static_cast<decltype(GfxImage::width)>(texture.GetWidth());
image->height = static_cast<decltype(GfxImage::height)>(texture.GetHeight());
image->depth = static_cast<decltype(GfxImage::depth)>(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<GfxImageLoadDef*>(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<decltype(GfxImageLoadDef::resourceSize)>(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<AssetImage>(name, image);
}
XAssetInfo<Material>* CreateFontMaterialShared(Material* material, AssetRegistration<AssetMaterial>& registration, XAssetInfo<GfxImage>* 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<GfxStateBits>(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<MaterialTextureDef>(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<AssetMaterial>(std::move(registration));
}
XAssetInfo<Material>* CreateFontMaterial(const std::string& name, XAssetInfo<GfxImage>* image, MemoryManager& memory, AssetCreationContext& context)
{
auto* material = memory.Alloc<Material>();
material->info.name = memory.Dup(name.c_str());
AssetRegistration<AssetMaterial> 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<AssetTechniqueSet>(
#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<AssetMaterial>(std::move(registration));
}
XAssetInfo<Material>* CreateFontGlowMaterial(const std::string& name, XAssetInfo<GfxImage>* image, MemoryManager& memory, AssetCreationContext& context)
{
auto* material = memory.Alloc<Material>();
material->info.name = memory.Dup(name.c_str());
AssetRegistration<AssetMaterial> 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<AssetTechniqueSet>(
#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<AssetMaterial>(std::move(registration));
}
class FontCompiler final : public AssetCreator<AssetFont>
{
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_s>();
font->fontName = m_memory.Dup(assetName.c_str());
AssetRegistration<AssetFont> 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<JsonFont>();
if (!std::holds_alternative<JsonFileFont>(jFont.font))
return AssetCreationResult::NoAction();
if (CreateFontFromJson(std::get<JsonFileFont>(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<AssetFont>& 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<decltype(Font_s::pixelHeight)>(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<unsigned char> fontData(static_cast<size_t>(fontFile.m_length));
fontFile.m_stream->read(reinterpret_cast<char*>(fontData.data()), fontFile.m_length);
std::vector<Glyph> 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<decltype(Glyph::letter)>(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<decltype(Glyph::letter)>(glyphs.size());
font.glyphs = m_memory.Alloc<Glyph>(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<KerningPairs>(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<AssetCreator<AssetFont>> CREATE_COMPILER_METHOD(MemoryManager& memory, ISearchPath& searchPath)
{
return std::make_unique<FontCompiler>(memory, searchPath);
}
} // namespace font
@@ -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 <memory>
namespace font
{
#set CREATE_COMPILER_METHOD "CreateCompiler" + GAME
std::unique_ptr<AssetCreator<GAME::AssetFont>> CREATE_COMPILER_METHOD(MemoryManager& memory, ISearchPath& searchPath);
} // namespace font
@@ -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));
@@ -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));
}
@@ -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));
}
+7 -1
View File
@@ -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,
@@ -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));
@@ -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));
+20 -7
View File
@@ -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<Texture> TextureConverter::Convert()
{
CreateOutputTexture();
+9
View File
@@ -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<Texture> Convert();
private:
@@ -25,6 +27,13 @@ namespace image
std::function<uint64_t(const void* offset, unsigned bitCount)> m_read_pixel_func;
std::function<void(void* offset, uint64_t pixel, unsigned bitCount)> 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<Texture> m_output_texture;
const ImageFormat* m_input_format;
+5 -2
View File
@@ -238,7 +238,10 @@ namespace
}
const auto jFont = jRoot.get<JsonFont>();
if (CreateFontFromJson(jFont, *font, registration, context))
if (!std::holds_alternative<JsonCompiledFont>(jFont.font))
return AssetCreationResult::NoAction();
if (CreateFontFromJson(std::get<JsonCompiledFont>(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<AssetFont>& registration, AssetCreationContext& context) const
bool CreateFontFromJson(const JsonCompiledFont& jFont, Font_s& font, AssetRegistration<AssetFont>& registration, AssetCreationContext& context) const
{
font.pixelHeight = static_cast<decltype(Font_s::pixelHeight)>(jFont.pixelHeight);
+3 -2
View File
@@ -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<JsonCompiledFont>();
CreateCompiledJsonFont(std::get<JsonCompiledFont>(jFont.font), font);
ordered_json jRoot;
jRoot["$schema"] = "http://openassettools.dev/schema/font.v1.json";
Vendored Submodule
+1
Submodule thirdparty/stb added at 31c1ad3745
+42
View File
@@ -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
+2
View File
@@ -0,0 +1,2 @@
#define STB_TRUETYPE_IMPLEMENTATION
#include "stb_truetype.h"