// Font loading code adapted from https://github.com/ocornut/imgui #include "Font.hpp" #include "CommonVertexIndex.hpp" #include "Image.hpp" #include "Macros.hpp" #include "String.hpp" #include "Texture.hpp" #include "Utils.hpp" #include "VertexIndex.hpp" #include #include #include #include #include #include #include #include #include #include #include #include namespace ProjectBrussel_UNITY_ID { Font::ErrorCode LoadFile(const char* path, std::unique_ptr& data, stbtt_fontinfo& fontInfo) { std::ifstream ifs(path, std::ios::binary | std::ios::ate); if (ifs) { auto size = static_cast(ifs.tellg()); data = std::make_unique(size); ifs.seekg(0, std::ios::beg); ifs.read(reinterpret_cast(data.get()), size); auto ptr = static_cast(data.get()); if (!stbtt_InitFont(&fontInfo, ptr, stbtt_GetFontOffsetForIndex(ptr, 0))) { return Font::EC_FileIOFailed; } return Font::EC_Success; } else { return Font::EC_FontLoadingFailed; } } const GlyphVariant& GetVariantFor(const GlyphInfo& info, FontType type) { switch (type) { case FontType::Regular: return info.regular; case FontType::Italic: return info.italic; case FontType::Bold: return info.bold; case FontType::BoldItalic: return info.boldItalic; default: UNREACHABLE; } } GlyphVariant& GetVariantFor(GlyphInfo& info, FontType type) { return const_cast(GetVariantFor(const_cast(info), type)); } } // namespace ProjectBrussel_UNITY_ID Font::InitResult Font::Init(std::span candidates, float fontHeight, char32_t fallbackCodepoint, int oversampleH, int oversampleV) { using namespace ProjectBrussel_UNITY_ID; mFontHeight = fontHeight; struct SrcInfo { std::vector glyphList; // Need to use vector because stb_true_type use int* stbtt_fontinfo fontInfo; stbtt_pack_range packRange; stbrp_rect* rects; // glyphList.size() long stbtt_packedchar* packedChars; // glyphList.size() long const char32_t* glyphRange; std::unique_ptr data; size_t dataSize; int glyphHighest = 0; // Highest codepoint in this glyph range FontType type; }; // 1. Initialize font loading structure, load ttf from disk std::vector srcVec(candidates.size()); int glyphHighest = 0; // Highest codepoint across all given glyph ranges for (size_t i = 0; i < candidates.size(); ++i) { auto& cand = candidates[i]; auto& src = srcVec[i]; src.glyphRange = cand.glyphRange; src.type = cand.type; auto result = LoadFile(cand.ttfPath, src.data, src.fontInfo); if (result != EC_Success) { return InitResult{ .errorCode = result, .failedItemIdx = (int)i, }; } for (auto range = src.glyphRange; range[0] && range[1]; range += 2) { src.glyphHighest = std::max(src.glyphHighest, static_cast(range[1])); } glyphHighest = std::max(glyphHighest, src.glyphHighest); } // 2. For every requested codepoint, check for their presence in the font data, and handle redundancy or overlaps between source fonts to avoid unused glyphs std::vector glyphSet(glyphHighest + 1); int glyphCount = 0; for (auto& src : srcVec) { if (src.type != FontType::Regular) { continue; } for (auto range = src.glyphRange; range[0] && range[1]; range += 2) { for (char32_t codepoint = range[0]; codepoint <= range[1]; ++codepoint) { // Remove duplicates if (glyphSet[codepoint]) { continue; } // Font file doesn't have the requested codepoint if (!stbtt_FindGlyphIndex(&src.fontInfo, codepoint)) { continue; } src.glyphList.push_back(codepoint); glyphSet[codepoint] = true; glyphCount++; } } } for (auto& src : srcVec) { if (src.type == FontType::Regular) { continue; } for (auto range = src.glyphRange; range[0] && range[1]; range += 2) { for (char32_t codepoint = range[0]; codepoint <= range[1]; ++codepoint) { // Ignore any formatted (italic, bold, bolditalic) glyph that doesn't have a regular variant if (!glyphSet[codepoint]) { continue; } // Font file doesn't have the requested codepoint if (!stbtt_FindGlyphIndex(&src.fontInfo, codepoint)) { continue; } src.glyphList.push_back(codepoint); glyphCount++; } } } glyphSet.clear(); // 3. Reserve memory for packing mGlyphs.reserve(glyphCount); std::vector rects(glyphCount); // This type them as non-packed (zero initialization) std::vector packedChars(glyphCount); // 4. Gather glyphs sizes so we can pack them in our virtual canvas constexpr int kTexGlyphPadding = 1; // Prevent texture sampler from accidentally getting neighbor glyph's pixel data float totalSurface = 0; int rectCount = 0; int packedCharCount = 0; for (auto& src : srcVec) { int glyphCount = static_cast(src.glyphList.size()); src.rects = &rects[rectCount]; src.packedChars = &packedChars[packedCharCount]; rectCount += glyphCount; packedCharCount += glyphCount; src.packRange.font_size = mFontHeight; src.packRange.first_unicode_codepoint_in_range = 0; src.packRange.array_of_unicode_codepoints = src.glyphList.data(); src.packRange.num_chars = glyphCount; src.packRange.chardata_for_range = src.packedChars; src.packRange.h_oversample = oversampleH; src.packRange.v_oversample = oversampleV; float scale = stbtt_ScaleForPixelHeight(&src.fontInfo, mFontHeight); for (int i = 0; i < glyphCount; ++i) { int glyphIndex = stbtt_FindGlyphIndex(&src.fontInfo, src.glyphList[i]); int x0, y0, x1, y1; stbtt_GetGlyphBitmapBoxSubpixel(&src.fontInfo, glyphIndex, scale * oversampleH, scale * oversampleV, 0, 0, &x0, &y0, &x1, &y1); src.rects[i].w = (stbrp_coord)(x1 - x0 + kTexGlyphPadding + oversampleH - 1); src.rects[i].h = (stbrp_coord)(y1 - y0 + kTexGlyphPadding + oversampleV - 1); totalSurface += static_cast(src.rects[i].w * src.rects[i].h); } } // We need a width for the skyline algorithm, any width // The exact width doesn't really matter much, but some API/GPU have texture size limitations and increasing width can decrease height auto surfaceSqrt = static_cast(std::sqrt(totalSurface)) + 1; int atlasWidth = (surfaceSqrt >= 4096 * 0.7f) ? 4096 : (surfaceSqrt >= 2048 * 0.7f) ? 2048 : (surfaceSqrt >= 1024 * 0.7f) ? 1024 : 512; int atlasHeight = 0; // Calculate later in 6 // 5. Start packing constexpr int kMaxAtlasHeight = 1024 * 32; stbtt_pack_context spc; stbtt_PackBegin(&spc, nullptr, atlasWidth, kMaxAtlasHeight, 0, kTexGlyphPadding, nullptr); // 6. Pack each source font. No rendering yet, we are working with rectangles in an infinitely tall texture at this point auto context = reinterpret_cast(spc.pack_info); for (auto& src : srcVec) { int glyphCount = static_cast(src.glyphList.size()); stbrp_pack_rects(context, src.rects, glyphCount); for (int i = 0; i < glyphCount; i++) { if (src.rects[i].was_packed) { atlasHeight = std::max(atlasHeight, src.rects[i].y + src.rects[i].h); } } } // 7. Allocate bitmap atlasHeight = std::bit_ceil(atlasHeight); auto bitmap = std::make_unique(atlasWidth * atlasHeight); std::memset(bitmap.get(), 0, atlasWidth * atlasHeight * sizeof(uint8_t)); spc.pixels = bitmap.get(); spc.height = atlasHeight; // 8. Render/rasterize font characters into the texture for (auto& src : srcVec) { stbtt_PackFontRangesRenderIntoRects(&spc, &src.fontInfo, &src.packRange, 1, src.rects); src.rects = nullptr; } // 9. End packing stbtt_PackEnd(&spc); rects.clear(); // Lambda for code reuse for step 10 and 12 auto SetupGlyphInfoVariant = [&](SrcInfo& src) -> void { float scale = stbtt_ScaleForPixelHeight(&src.fontInfo, mFontHeight); int unscaledAscent, unscaledDescent, unscaledLineGap; stbtt_GetFontVMetrics(&src.fontInfo, &unscaledAscent, &unscaledDescent, &unscaledLineGap); float ascent = std::floor(unscaledAscent * scale + ((unscaledAscent > 0.0f) ? +1 : -1)); float descent = std::floor(unscaledDescent * scale + ((unscaledDescent > 0.0f) ? +1 : -1)); float fontOffsetX = 0; float fontOffsetY = std::round(ascent); UNUSED(descent); int glyphCount = static_cast(src.glyphList.size()); for (int i = 0; i < glyphCount; ++i) { int codepoint = src.glyphList[i]; auto& packedChar = src.packedChars[i]; float x = 0; float y = 0; UNUSED(x); UNUSED(y); stbtt_aligned_quad quad; stbtt_GetPackedQuad(src.packedChars, atlasWidth, atlasHeight, i, &x, &y, &quad, 0); GlyphInfo* info; if (src.type == FontType::Regular) { mGlyphs.emplace_back(); info = &mGlyphs.back(); info->codepoint = codepoint; } else { info = &mGlyphs[mGlyphLookup[codepoint]]; } auto& variant = ProjectBrussel_UNITY_ID::GetVariantFor(*info, src.type); variant.x0 = quad.x0 + fontOffsetX; variant.y0 = quad.y0 + fontOffsetY; variant.x1 = quad.x1 + fontOffsetX; variant.y1 = quad.y1 + fontOffsetY; variant.u0 = quad.s0; variant.v0 = quad.t0; variant.u1 = quad.s1; variant.v1 = quad.t1; variant.horizontalAdvance = std::lrint(packedChar.xadvance); // For step 12 to override // If some glyph doesn't have a particular variant, it will remain as Regular in the final product if (src.type == FontType::Regular) { info->italic = info->regular; info->bold = info->regular; info->boldItalic = info->regular; } } }; // 10. Setup glyph infos for (auto& src : srcVec) { if (src.type == FontType::Regular) { SetupGlyphInfoVariant(src); } } // 11. Setup glyph lookup table mGlyphLookup.resize(glyphHighest + 1, -1); for (int i = 0, size = (int)mGlyphs.size(); i < size; ++i) { auto& info = mGlyphs[i]; mGlyphLookup[info.codepoint] = i; } // Provided fallback codepoint doesn't exist, use the first glyph present if (fallbackCodepoint >= static_cast(glyphHighest) || mGlyphLookup[fallbackCodepoint] == -1) { mFallbackGlyphIdx = 0; } else { mFallbackGlyphIdx = mGlyphLookup[fallbackCodepoint]; } for (size_t i = 0; i < mGlyphLookup.size(); ++i) { if (mGlyphLookup[i] == -1) { mGlyphLookup[i] = mFallbackGlyphIdx; } } // 12. Setup glyph info for non-regular variants // This step won't cause any new glyph infos to be added, so it's fine (and we need the lookup table here) to do it after building lookup table for (auto& src : srcVec) { if (src.type != FontType::Regular) { SetupGlyphInfoVariant(src); } } // 13. Upload our rasterized bitmap into a Texture Image image; image.InitFromPixels(std::move(bitmap), glm::ivec2(atlasWidth, atlasHeight), 1); // Leave the texture upside down, we flip the UV in the text shader mAtlas.Attach(new Texture()); mAtlas->InitFromImage( image, TextureProperties{ // When down sampling, 'linear' help reduce flicker when scale changes .minifyingFilter = Tags::TF_Linear, // When up sampling, 'nearest' make the text look sharp (though pixelated) .magnifyingFilter = Tags::TF_Nearest, }); return InitResult{ .errorCode = EC_Success, .failedItemIdx = 0, }; } float Font::GetFontHeight() const { return mFontHeight; } int Font::HorizontalAdvance(char32_t c, FontType type) const { auto info = FindGlyphFallback(c); return ProjectBrussel_UNITY_ID::GetVariantFor(info, type).horizontalAdvance; } int Font::HorizontalAdvance(std::string_view str, FontType type) const { int width = 0; for (auto c : Utf8IterableString(str)) { width += HorizontalAdvance(c, type); } return width; } int Font::GetGlyphCount() const { return mGlyphs.size(); } const GlyphInfo& Font::GetFallbackGlyph() const { return mGlyphs[mFallbackGlyphIdx]; } const GlyphInfo& Font::FindGlyphFallback(char32_t codepoint) const { auto codepointInt = static_cast(codepoint); if (codepointInt < mGlyphLookup.size()) { return mGlyphs[mGlyphLookup[codepointInt]]; } else { return mGlyphs[mFallbackGlyphIdx]; } } const GlyphInfo* Font::FindGlyph(char32_t codepoint) const { auto codepointInt = static_cast(codepoint); if (codepointInt < mGlyphLookup.size()) { return &mGlyphs[mGlyphLookup[codepointInt]]; } else { return nullptr; } } namespace ProjectBrussel_UNITY_ID { void GenQuad(Vertex_PTC* vertices, glm::vec3 pos, const GlyphVariant& variant) { // Top left vertices[0].x = pos.x + variant.x0; vertices[0].y = pos.y + variant.y0; vertices[0].u = variant.u0; vertices[0].v = variant.v0; // Top right vertices[1].x = pos.x + variant.x1; vertices[1].y = pos.y + variant.y0; vertices[1].u = variant.u1; vertices[1].v = variant.v0; // Bottom right vertices[2].x = pos.x + variant.x1; vertices[2].y = pos.y + variant.y1; vertices[2].u = variant.u1; vertices[2].v = variant.v1; // Bottom left vertices[3].x = pos.x + variant.x0; vertices[3].y = pos.y + variant.y1; vertices[3].u = variant.u0; vertices[3].v = variant.v1; } template void DrawString(const Font& font, TStringIterable text, Font::DrawTargetPointer& t) { auto pos = t.pos; auto vertices = t.vertices; auto indices = t.indices; auto initialVertexIdx = t.initialVertexIdx; for (char32_t codepoint : text) { auto& info = font.FindGlyphFallback(codepoint); auto& variant = ProjectBrussel_UNITY_ID::GetVariantFor(info, t.type); GenQuad(vertices, pos, variant); Vertex_PTC::Assign(vertices, pos.z); Vertex_PTC::Assign(vertices, t.color); Index_U32::Assign(indices, initialVertexIdx); pos.x += variant.horizontalAdvance; t.glyphsRendered++; t.horizontalAdvance += variant.horizontalAdvance; vertices += 4; indices += 6; initialVertexIdx += 4; } } } // namespace ProjectBrussel_UNITY_ID void Font::DrawTo(std::string_view text, DrawTargetPointer& t) const { ProjectBrussel_UNITY_ID::DrawString(*this, Utf8IterableString(text), t); } void Font::DrawTo(std::u32string_view text, DrawTargetPointer& t) const { ProjectBrussel_UNITY_ID::DrawString(*this, text, t); } const Texture& Font::GetGlyphAtlas() const { return *mAtlas; }