diff options
author | rtk0c <[email protected]> | 2022-11-25 17:28:07 -0800 |
---|---|---|
committer | rtk0c <[email protected]> | 2022-11-25 17:28:07 -0800 |
commit | f3269a49c474ffe4d382c3d60826ad1cfbb7cdc4 (patch) | |
tree | bf7854505e9dae60c84e64764589c240339c3a41 /source/30-game/Font_Base.cpp | |
parent | a0ddfdbcbc6336685362343518770f7bdefd10fe (diff) |
Changeset: 93 Branch comment: [] Port font and UTF-8 string utilities from p6503master-ui-framework
Diffstat (limited to 'source/30-game/Font_Base.cpp')
-rw-r--r-- | source/30-game/Font_Base.cpp | 448 |
1 files changed, 448 insertions, 0 deletions
diff --git a/source/30-game/Font_Base.cpp b/source/30-game/Font_Base.cpp new file mode 100644 index 0000000..36dc4d6 --- /dev/null +++ b/source/30-game/Font_Base.cpp @@ -0,0 +1,448 @@ +// 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 <stb_rect_pack.h> +#include <stb_truetype.h> +#include <bit> +#include <cmath> +#include <cstddef> +#include <cstdint> +#include <cstring> +#include <fstream> +#include <iostream> +#include <memory> +#include <string> +#include <string_view> + +namespace ProjectBrussel_UNITY_ID { +Font::ErrorCode LoadFile(const char* path, std::unique_ptr<uint8_t[]>& data, stbtt_fontinfo& fontInfo) { + std::ifstream ifs(path, std::ios::binary | std::ios::ate); + if (ifs) { + auto size = static_cast<size_t>(ifs.tellg()); + data = std::make_unique<uint8_t[]>(size); + + ifs.seekg(0, std::ios::beg); + ifs.read(reinterpret_cast<char*>(data.get()), size); + + auto ptr = static_cast<unsigned char*>(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<GlyphVariant&>(GetVariantFor(const_cast<const GlyphInfo&>(info), type)); +} +} // namespace ProjectBrussel_UNITY_ID + +Font::InitResult Font::Init(std::span<LoadingCandidate> candidates, float fontHeight, char32_t fallbackCodepoint, int oversampleH, int oversampleV) { + using namespace ProjectBrussel_UNITY_ID; + + mFontHeight = fontHeight; + + struct SrcInfo { + std::vector<int> glyphList; // Need to use vector<int> 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<uint8_t[]> 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<SrcInfo> 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<int>(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<bool> 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<stbrp_rect> rects(glyphCount); // This type them as non-packed (zero initialization) + std::vector<stbtt_packedchar> 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<int>(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<float>(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<int>(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<stbrp_context*>(spc.pack_info); + for (auto& src : srcVec) { + int glyphCount = static_cast<int>(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<unsigned int>(atlasHeight); + auto bitmap = std::make_unique<uint8_t[]>(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<int>(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<char32_t>(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<uint32_t>(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<uint32_t>(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 <class TStringIterable> +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; +} |