Files
OpenAssetTools/src/ObjCompiling/Font/FontCompiler.cpp.template
T
Jan LaupetinandGitHub 0874f0c36f 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
2026-07-08 23:35:58 +02:00

525 lines
20 KiB
Plaintext

#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