aboutsummaryrefslogtreecommitdiff
path: root/source/30-game/Font_Base.cpp
diff options
context:
space:
mode:
authorrtk0c <[email protected]>2022-11-25 17:28:07 -0800
committerrtk0c <[email protected]>2022-11-25 17:28:07 -0800
commitf3269a49c474ffe4d382c3d60826ad1cfbb7cdc4 (patch)
treebf7854505e9dae60c84e64764589c240339c3a41 /source/30-game/Font_Base.cpp
parenta0ddfdbcbc6336685362343518770f7bdefd10fe (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.cpp448
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;
+}