aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--source/10-common/String.cpp350
-rw-r--r--source/10-common/String.hpp79
-rw-r--r--source/30-game/Font.hpp131
-rw-r--r--source/30-game/FontManager.cpp94
-rw-r--r--source/30-game/FontManager.hpp21
-rw-r--r--source/30-game/Font_Base.cpp448
-rw-r--r--source/30-game/Font_GlyphRanges.cpp253
-rw-r--r--source/30-game/Texture.cpp34
-rw-r--r--source/30-game/Texture.hpp19
9 files changed, 1407 insertions, 22 deletions
diff --git a/source/10-common/String.cpp b/source/10-common/String.cpp
new file mode 100644
index 0000000..9e61893
--- /dev/null
+++ b/source/10-common/String.cpp
@@ -0,0 +1,350 @@
+#include "String.hpp"
+
+/*
+#include <doctest/doctest.h>
+*/
+
+Utf8Iterator::Utf8Iterator(std::string_view::iterator it)
+ : mIter{ std::move(it) } {
+}
+
+constexpr unsigned char kFirstBitMask = 0b10000000;
+constexpr unsigned char kSecondBitMask = 0b01000000;
+constexpr unsigned char kThirdBitMask = 0b00100000;
+constexpr unsigned char kFourthBitMask = 0b00010000;
+constexpr unsigned char kFifthBitMask = 0b00001000;
+
+Utf8Iterator& Utf8Iterator::operator++() {
+ char firstByte = *mIter;
+ std::string::difference_type offset = 1;
+
+ // This means the first byte has a value greater than 127, and so is beyond the ASCII range.
+ if (firstByte & kFirstBitMask) {
+ // This means that the first byte has a value greater than 224, and so it must be at least a three-octet code point.
+ if (firstByte & kThirdBitMask) {
+ // This means that the first byte has a value greater than 240, and so it must be a four-octet code point.
+ if (firstByte & kFourthBitMask) {
+ offset = 4;
+ } else {
+ offset = 3;
+ }
+ } else {
+ offset = 2;
+ }
+ }
+
+ mIter += offset;
+ mDirty = true;
+ return *this;
+}
+
+Utf8Iterator Utf8Iterator::operator++(int) {
+ Utf8Iterator temp = *this;
+ ++(*this);
+ return temp;
+}
+
+Utf8Iterator& Utf8Iterator::operator--() {
+ --mIter;
+
+ // This means that the previous byte is not an ASCII character.
+ if (*mIter & kFirstBitMask) {
+ --mIter;
+ if ((*mIter & kSecondBitMask) == 0) {
+ --mIter;
+ if ((*mIter & kSecondBitMask) == 0) {
+ --mIter;
+ }
+ }
+ }
+
+ mDirty = true;
+ return *this;
+}
+
+Utf8Iterator Utf8Iterator::operator--(int) {
+ Utf8Iterator temp = *this;
+ --(*this);
+ return temp;
+}
+
+char32_t Utf8Iterator::operator*() const {
+ UpdateCurrentValue();
+ return mCurrentCodePoint;
+}
+
+std::string_view::iterator Utf8Iterator::AsInternal() const {
+ // updateCurrentValue();
+ return mIter;
+}
+
+bool operator==(const Utf8Iterator& lhs, const Utf8Iterator& rhs) {
+ return lhs.mIter == rhs.mIter;
+}
+
+bool operator!=(const Utf8Iterator& lhs, const Utf8Iterator& rhs) {
+ return lhs.mIter != rhs.mIter;
+}
+
+bool operator==(const Utf8Iterator& lhs, std::string_view::iterator rhs) {
+ return lhs.mIter == rhs;
+}
+
+bool operator!=(const Utf8Iterator& lhs, std::string_view::iterator rhs) {
+ return lhs.mIter != rhs;
+}
+
+void Utf8Iterator::UpdateCurrentValue() const {
+ if (!mDirty) {
+ return;
+ }
+
+ mCurrentCodePoint = 0;
+ char firstByte = *mIter;
+
+ // This means the first byte has a value greater than 127, and so is beyond the ASCII range.
+ if (firstByte & kFirstBitMask) {
+ // This means that the first byte has a value greater than 191, and so it must be at least a three-octet code point.
+ if (firstByte & kThirdBitMask) {
+ // This means that the first byte has a value greater than 224, and so it must be a four-octet code point.
+ if (firstByte & kFourthBitMask) {
+ mCurrentCodePoint = (firstByte & 0x07) << 18;
+ char secondByte = *(mIter + 1);
+ mCurrentCodePoint += (secondByte & 0x3f) << 12;
+ char thirdByte = *(mIter + 2);
+ mCurrentCodePoint += (thirdByte & 0x3f) << 6;
+
+ char fourthByte = *(mIter + 3);
+ mCurrentCodePoint += (fourthByte & 0x3f);
+ } else {
+ mCurrentCodePoint = (firstByte & 0x0f) << 12;
+ char secondByte = *(mIter + 1);
+ mCurrentCodePoint += (secondByte & 0x3f) << 6;
+ char thirdByte = *(mIter + 2);
+ mCurrentCodePoint += (thirdByte & 0x3f);
+ }
+ } else {
+ mCurrentCodePoint = (firstByte & 0x1f) << 6;
+ char secondByte = *(mIter + 1);
+ mCurrentCodePoint += (secondByte & 0x3f);
+ }
+ } else {
+ mCurrentCodePoint = firstByte;
+ }
+
+ mDirty = true;
+}
+
+Utf8IterableString::Utf8IterableString(std::string_view str)
+ : mStr{ str } {
+}
+
+Utf8Iterator Utf8IterableString::begin() const {
+ return Utf8Iterator(mStr.begin());
+}
+
+Utf8Iterator Utf8IterableString::end() const {
+ return Utf8Iterator(mStr.end());
+}
+
+/*
+TEST_CASE("Iterating ASCII string") {
+ std::string ascii("This is an ASCII string");
+ std::u32string output;
+ output.reserve(ascii.length());
+
+ for (char32_t c : Utf8IterableString(ascii)) {
+ output += c;
+ }
+
+ CHECK(output == U"This is an ASCII string");
+}
+
+// BMP: Basic Multilingual Plane
+TEST_CASE("Iterating BMP string") {
+ std::string unicode("Unicode test \u8FD9\u662F\u4E00\u4E2A\u6D4B\u8BD5\u7528\u5B57\u7B26\u4E32");
+ std::u32string output;
+ output.reserve(10);
+
+ for (char32_t c : Utf8IterableString(unicode)) {
+ output += c;
+ }
+
+ CHECK(output == U"Unicode test \u8FD9\u662F\u4E00\u4E2A\u6D4B\u8BD5\u7528\u5B57\u7B26\u4E32");
+}
+*/
+
+std::u32string Utils::ConvertUtf8To32(std::string_view in) {
+ std::u32string str;
+ // Actual size cannot be smaller than this
+ str.reserve(in.size());
+ for (char32_t codepoint : Utf8IterableString(in)) {
+ str += codepoint;
+ }
+ return str;
+}
+
+std::string Utils::ConvertUtf32To8(std::u32string_view in) {
+ std::string str;
+ for (char32_t codepoint : in) {
+ if (codepoint <= 0x7F) {
+ str += codepoint;
+ } else if (codepoint <= 0x7FF) {
+ str += 0xC0 | (codepoint >> 6); // 110xxxxx
+ str += 0x80 | (codepoint & 0x3F); // 10xxxxxx
+ } else if (codepoint <= 0xFFFF) {
+ str += 0xE0 | (codepoint >> 12); // 1110xxxx
+ str += 0x80 | ((codepoint >> 6) & 0x3F); // 10xxxxxx
+ str += 0x80 | (codepoint & 0x3F); // 10xxxxxx
+ } else if (codepoint <= 0x10FFFF) {
+ str += 0xF0 | (codepoint >> 18); // 11110xxx
+ str += 0x80 | ((codepoint >> 12) & 0x3F); // 10xxxxxx
+ str += 0x80 | ((codepoint >> 6) & 0x3F); // 10xxxxxx
+ str += 0x80 | (codepoint & 0x3F); // 10xxxxxx
+ }
+ }
+ return str;
+}
+
+/*
+TEST_CASE("Utils::ConvertUtf32To8() with ASCII") {
+ auto output = Utils::ConvertUtf32To8(U"This is an ASCII string");
+ CHECK(output == "This is an ASCII string");
+}
+
+TEST_CASE("Utils::ConvertUtf32To8() with BMP codepoints") {
+ auto output = Utils::ConvertUtf32To8(U"Unicode test \u8FD9\u662F\u4E00\u4E2A\u6D4B\u8BD5\u7528\u5B57\u7B26\u4E32");
+ CHECK(output == "Unicode test \u8FD9\u662F\u4E00\u4E2A\u6D4B\u8BD5\u7528\u5B57\u7B26\u4E32");
+}
+ */
+
+std::string_view Utils::SliceUtf8(std::string_view str, size_t begin, size_t end) {
+ const char* resBegin;
+ size_t resLength = 0;
+
+ Utf8Iterator it{ str.begin() };
+ size_t i = 0; // Nth codepoint on the string
+
+ // Skip until `it` points to the `begin`-th codepoint in the string
+ while (i < begin) {
+ i++;
+ it++;
+ } // Postcondition: i == begin
+ resBegin = &*it.AsInternal();
+
+ while (i < end) {
+ auto prev = it;
+ i++;
+ it++;
+
+ resLength += std::distance(prev.AsInternal(), it.AsInternal());
+ } // Postcondition: i == end
+
+ return { resBegin, resLength };
+}
+
+/*
+TEST_CASE("Utils::CreateRange() with ASCII") {
+ auto a = Utils::CreateRange("This is an ASCII string", 1, 1 + 5);
+ std::string range(a);
+ CHECK(range == "his i");
+}
+
+TEST_CASE("Utils::CreateRange() with BMP codepoints") {
+ std::string range(Utils::SliceUtf8("Unicode test \u8FD9\u662F\u4E00\u4E2A\u6D4B\u8BD5\u7528\u5B57\u7B26\u4E32", 11, 11 + 5));
+ CHECK(range == "t \u8FD9\u662F\u4E00");
+}
+*/
+
+size_t Utils::CountUtf8Codepoints(std::string_view str) {
+ size_t result = 0;
+ for (char32_t _ : Utf8IterableString(str)) {
+ result++;
+ }
+ return result;
+}
+
+/*
+TEST_CASE("Utils::GetLength() test") {
+ CHECK(Utils::GetLength("This is an ASCII string") == 23);
+ CHECK(Utils::CountUtf8Codepoints("Unicode test \u8FD9\u662F\u4E00\u4E2A\u6D4B\u8BD5\u7528\u5B57\u7B26\u4E32") == 23);
+}
+
+Utils::CodepointInfo Utils::FindLastCodepoint(std::string_view str) {
+ Utf8Iterator it{ str.begin() };
+ Utf8Iterator prev{ it };
+ size_t codepoints = 0;
+
+ Utf8Iterator end{ str.end() };
+ while (it != end) {
+ codepoints++;
+
+ prev = it;
+ it++;
+ }
+ // it == end
+ // prev == <last codepoint in str>
+
+ return {
+ codepoints - 1,
+ (size_t)std::distance(str.begin(), prev.AsInternal()),
+ };
+}
+
+TEST_CASE("Utils::FindLastCodepoint() ASCII test") {
+ auto [index, byteOffset] = Utils::FindLastCodepoint("This is an ASCII string");
+ CHECK(index == 22);
+ CHECK(index == 22);
+}
+
+TEST_CASE("Utils::FindLastCodepoint() BMP test") {
+ auto [index, byteOffset] = Utils::FindLastCodepoint("Unicode test \u8FD9\u662F\u4E00\u4E2A\u6D4B\u8BD5\u7528\u5B57\u7B26\u4E32");
+ CHECK(index == 22);
+ CHECK(byteOffset == 40);
+}
+
+Utils::CodepointInfo Utils::FindCodepoint(std::string_view str, size_t codepointIdx) {
+ Utf8Iterator it{ str.begin() };
+ Utf8Iterator prev{ it };
+ size_t codepoint = 0;
+
+ Utf8Iterator end{ str.end() };
+ while (true) {
+ if (codepoint == codepointIdx) {
+ return { codepoint, (size_t)std::distance(str.begin(), it.AsInternal()) };
+ }
+ if (it == end) {
+ return { codepoint - 1, (size_t)std::distance(str.begin(), prev.AsInternal()) };
+ }
+
+ codepoint++;
+
+ prev = it;
+ it++;
+ }
+}
+
+TEST_CASE("Utils::FindCodepoint() ASCII test") {
+ auto [codepointOffset, byteOffset] = Utils::FindCodepoint("This is an ASCII string", 6);
+ CHECK(codepointOffset == 6);
+ CHECK(byteOffset == 6);
+}
+
+TEST_CASE("Utils::FindCodepoint() ASCII past-the-end test") {
+ auto [codepointOffset, byteOffset] = Utils::FindCodepoint("This is an ASCII string", 100);
+ CHECK(codepointOffset == 22);
+ CHECK(byteOffset == 22);
+}
+
+TEST_CASE("Utils::FindCodepoint() BMP test") {
+ auto [codepointOffset, byteOffset] = Utils::FindCodepoint("Unicode test \u8FD9\u662F\u4E00\u4E2A\u6D4B\u8BD5\u7528\u5B57\u7B26\u4E32", 14);
+ CHECK(codepointOffset == 14);
+ CHECK(byteOffset == 16);
+}
+
+TEST_CASE("Utils::FindCodepoint() BMP past-the-end test") {
+ auto [codepointOffset, byteOffset] = Utils::FindCodepoint("Unicode test \u8FD9\u662F\u4E00\u4E2A\u6D4B\u8BD5\u7528\u5B57\u7B26\u4E32", 100);
+ CHECK(codepointOffset == 22);
+ CHECK(byteOffset == 40);
+}
+*/
diff --git a/source/10-common/String.hpp b/source/10-common/String.hpp
new file mode 100644
index 0000000..8d54bad
--- /dev/null
+++ b/source/10-common/String.hpp
@@ -0,0 +1,79 @@
+#pragma once
+
+#include <cstddef>
+#include <string>
+#include <string_view>
+
+class Utf8Iterator {
+public:
+ using iterator_category = std::bidirectional_iterator_tag;
+ using value_type = char32_t;
+ using difference_type = std::string_view::difference_type;
+ using pointer = const char32_t*;
+ using reference = const char32_t&;
+
+private:
+ std::string_view::iterator mIter;
+ mutable char32_t mCurrentCodePoint = 0;
+ mutable bool mDirty = true;
+
+public:
+ Utf8Iterator(std::string_view::iterator it);
+ ~Utf8Iterator() = default;
+
+ Utf8Iterator(const Utf8Iterator& that) = default;
+ Utf8Iterator& operator=(const Utf8Iterator& that) = default;
+ Utf8Iterator(Utf8Iterator&& that) = default;
+ Utf8Iterator& operator=(Utf8Iterator&& that) = default;
+
+ Utf8Iterator& operator++();
+ Utf8Iterator operator++(int);
+ Utf8Iterator& operator--();
+ Utf8Iterator operator--(int);
+
+ char32_t operator*() const;
+ std::string_view::iterator AsInternal() const;
+
+ friend bool operator==(const Utf8Iterator& lhs, const Utf8Iterator& rhs);
+ friend bool operator!=(const Utf8Iterator& lhs, const Utf8Iterator& rhs);
+ friend bool operator==(const Utf8Iterator& lhs, std::string_view::iterator rhs);
+ friend bool operator!=(const Utf8Iterator& lhs, std::string_view::iterator rhs);
+
+private:
+ void UpdateCurrentValue() const;
+};
+
+class Utf8IterableString {
+private:
+ std::string_view mStr;
+
+public:
+ Utf8IterableString(std::string_view str);
+ Utf8Iterator begin() const;
+ Utf8Iterator end() const;
+};
+
+namespace Utils {
+
+std::u32string ConvertUtf8To32(std::string_view str);
+std::string ConvertUtf32To8(std::u32string_view str);
+
+/// Slice the given UTF-8 string into the given range, in codepoints.
+std::string_view SliceUtf8(std::string_view str, size_t begin, size_t end);
+
+/// Calculate the given UTF-8 string's number of codepoints.
+size_t CountUtf8Codepoints(std::string_view str);
+
+struct CodepointInfo {
+ size_t index;
+ size_t byteOffset;
+};
+
+/// Find info about the last codepoint in the given UTF-8 string.
+/// \param str A non-empty UTF-8 encoded string.
+CodepointInfo FindLastCodepoint(std::string_view str);
+/// Find info about the nth codepoint in the given UTF-8 string. If codepointIdx is larger than the length, info for the last codepoint will be returned.
+/// \param str A non-empty UTF-8 encoded string.
+CodepointInfo FindCodepoint(std::string_view str, size_t codepointIdx);
+
+} // namespace Utils
diff --git a/source/30-game/Font.hpp b/source/30-game/Font.hpp
new file mode 100644
index 0000000..cc64c0c
--- /dev/null
+++ b/source/30-game/Font.hpp
@@ -0,0 +1,131 @@
+#pragma once
+
+#include "Color.hpp"
+#include "CommonVertexIndex.hpp"
+#include "RcPtr.hpp"
+
+#include <glad/glad.h>
+#include <cstddef>
+#include <cstdint>
+#include <glm/glm.hpp>
+#include <memory>
+#include <span>
+#include <string_view>
+#include <vector>
+
+enum class FontType {
+ Regular,
+ Italic,
+ Bold,
+ BoldItalic,
+};
+
+struct GlyphVariant {
+ int horizontalAdvance;
+ float x0, y0;
+ float x1, y1;
+ float u0, v0;
+ float u1, v1;
+};
+
+// TODO optimize for memory when most glyphs don't have a non-regular variant
+// such as CJK characters rarely use bold or italics
+struct GlyphInfo {
+ char32_t codepoint;
+ GlyphVariant regular;
+ GlyphVariant italic;
+ GlyphVariant bold;
+ GlyphVariant boldItalic;
+};
+
+class Font : public RefCounted {
+public:
+ static const char32_t* GetGlyphRangesDefault();
+ static const char32_t* GetGlyphRangesKorean();
+ static const char32_t* GetGlyphRangesJapanese();
+ static const char32_t* GetGlyphRangesChineseFull();
+ static const char32_t* GetGlyphRangesChineseSimplifiedCommon();
+ static const char32_t* GetGlyphRangesCyrillic();
+ static const char32_t* GetGlyphRangesThai();
+ static const char32_t* GetGlyphRangesVietnamese();
+
+private:
+ std::vector<GlyphInfo> mGlyphs;
+ std::vector<int> mGlyphLookup;
+ RcPtr<Texture> mAtlas;
+ float mFontHeight;
+ int mFallbackGlyphIdx;
+
+public:
+ struct LoadingCandidate {
+ // No std::string_view here because iostreams only support null termianted string
+ const char* ttfPath;
+ const char32_t* glyphRange;
+ FontType type = FontType::Regular;
+ };
+
+ Font() = default;
+ Font(const Font&) = delete;
+ Font& operator=(const Font&) = delete;
+ Font(Font&&) = default;
+ Font& operator=(Font&&) = default;
+
+ enum ErrorCode {
+ EC_Success,
+ EC_FileIOFailed,
+ EC_FontLoadingFailed,
+ };
+ struct InitResult {
+ ErrorCode errorCode;
+ int failedItemIdx;
+ };
+
+ /// Initialize this font object from a list of ttf files. Each ttf covers a range of unicode characters,
+ /// and a specific font type, which are combined together into an atlas texture.
+ ///
+ /// \see FontType
+ /// \see Font::LoadingCandidate
+ InitResult Init(std::span<LoadingCandidate> candidates, float fontHeight, char32_t fallbackCodepoint = '?', int oversampleH = 1, int oversampleV = 1);
+
+ float GetFontHeight() const;
+ int HorizontalAdvance(char32_t c, FontType type = FontType::Regular) const;
+ int HorizontalAdvance(std::string_view str, FontType type = FontType::Regular) const;
+
+ int GetGlyphCount() const;
+ const GlyphInfo& GetFallbackGlyph() const;
+
+ /// Find the glyph corresponding to the given codepoint. If none is present, return the fallback glyph
+ /// specified in Init().
+ const GlyphInfo& FindGlyphFallback(char32_t codepoint) const;
+ /// Find the glyph corresponding to the given codepoint. If none is present, `nullptr` is returned.
+ const GlyphInfo* FindGlyph(char32_t codepoint) const;
+
+ struct DrawTargetPointer {
+ // Draw target info
+ Vertex_PTC* vertices;
+ uint32_t* indices;
+ uint32_t initialVertexIdx;
+
+ // Text info
+ glm::vec3 pos;
+ FontType type = FontType::Regular;
+ RgbaColor color = RgbaColor{};
+
+ // Output info, written to by DrawTo()
+ int glyphsRendered;
+ float horizontalAdvance;
+ };
+
+ /// Populate the target with quads, with first character's origin at `pos` and every vertex colored `color`.
+ /// User must allocate enough space for all characers beforehand (one way is to reserve the # of bytes).
+ /// Suitable for raw array allocations or something like PodVector<T> that doesn't do zero-initialization on
+ /// resize.
+ ///
+ /// \see Font::DrawTargetPointer
+ void DrawTo(std::string_view text, DrawTargetPointer& t) const;
+ void DrawTo(std::u32string_view text, DrawTargetPointer& t) const;
+
+ /// OpenGL texture containing the font atlas. Format is in GL_RED because we don't need the other channels, due to the
+ /// font atlas being black and white (in other words, opacity only).
+ const Texture& GetGlyphAtlas() const;
+};
diff --git a/source/30-game/FontManager.cpp b/source/30-game/FontManager.cpp
new file mode 100644
index 0000000..11a3a5b
--- /dev/null
+++ b/source/30-game/FontManager.cpp
@@ -0,0 +1,94 @@
+#include "FontManager.hpp"
+
+RcPtr<Font> FontManager::sans{ new Font() };
+RcPtr<Font> FontManager::serif{ new Font() };
+RcPtr<Font> FontManager::monospace{ new Font() };
+
+const RcPtr<Font>& FontManager::GetDefaultFont() {
+ return sans;
+}
+
+const Font* FontManager::ResolveFallback(const Font* font) {
+ if (font == nullptr) {
+ return FontManager::sans.Get();
+ } else {
+ return font;
+ }
+}
+
+const RcPtr<Font>& FontManager::ResolveFallback(const RcPtr<Font>& font) {
+ if (font == nullptr) {
+ return FontManager::sans;
+ } else {
+ return font;
+ }
+}
+
+void FontManager::Init() {
+ Font::LoadingCandidate sansFiles[] = {
+ {
+ .ttfPath = "fonts/NotoSans-Regular.ttf",
+ .glyphRange = Font::GetGlyphRangesDefault(),
+ .type = FontType::Regular,
+ },
+ {
+ .ttfPath = "fonts/NotoSans-Italic.ttf",
+ .glyphRange = Font::GetGlyphRangesDefault(),
+ .type = FontType::Italic,
+ },
+ {
+ .ttfPath = "fonts/NotoSans-Bold.ttf",
+ .glyphRange = Font::GetGlyphRangesDefault(),
+ .type = FontType::Bold,
+ },
+ {
+ .ttfPath = "fonts/NotoSans-BoldItalic.ttf",
+ .glyphRange = Font::GetGlyphRangesDefault(),
+ .type = FontType::BoldItalic,
+ },
+ {
+ .ttfPath = "fonts/NotoSansSC-Regular.otf",
+ .glyphRange = Font::GetGlyphRangesChineseSimplifiedCommon(),
+ .type = FontType::Regular,
+ },
+ };
+
+ sans->Init(sansFiles, 18.0f);
+
+ Font::LoadingCandidate serifFiles[] = {
+ {
+ .ttfPath = "fonts/NotoSerif-Regular.ttf",
+ .glyphRange = Font::GetGlyphRangesDefault(),
+ .type = FontType::Regular,
+ },
+ {
+ .ttfPath = "fonts/NotoSerif-Italic.ttf",
+ .glyphRange = Font::GetGlyphRangesDefault(),
+ .type = FontType::Italic,
+ },
+ {
+ .ttfPath = "fonts/NotoSerif-Bold.ttf",
+ .glyphRange = Font::GetGlyphRangesDefault(),
+ .type = FontType::Bold,
+ },
+ {
+ .ttfPath = "fonts/NotoSerif-BoldItalic.ttf",
+ .glyphRange = Font::GetGlyphRangesDefault(),
+ .type = FontType::BoldItalic,
+ },
+ };
+
+ serif->Init(sansFiles, 18.0f);
+
+ Font::LoadingCandidate monospacedFiles[] = {
+ {
+ .ttfPath = "fonts/NotoMono-Regular.ttf",
+ .glyphRange = Font::GetGlyphRangesDefault(),
+ .type = FontType::Regular,
+ },
+ };
+ monospace->Init(monospacedFiles, 18.0f);
+}
+
+void FontManager::Shutdown() {
+}
diff --git a/source/30-game/FontManager.hpp b/source/30-game/FontManager.hpp
new file mode 100644
index 0000000..ef87f0c
--- /dev/null
+++ b/source/30-game/FontManager.hpp
@@ -0,0 +1,21 @@
+#pragma once
+
+#include "Font.hpp"
+#include "RcPtr.hpp"
+
+class FontManager {
+public:
+ // Pointers are valid staring from program entry, but object will not be initialized until FontManager::Init() is called.
+ static RcPtr<Font> sans;
+ static RcPtr<Font> serif;
+ static RcPtr<Font> monospace;
+
+public:
+ static const RcPtr<Font>& GetDefaultFont();
+
+ static const Font* ResolveFallback(const Font* font);
+ static const RcPtr<Font>& ResolveFallback(const RcPtr<Font>& font);
+
+ static void Init();
+ static void Shutdown();
+};
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;
+}
diff --git a/source/30-game/Font_GlyphRanges.cpp b/source/30-game/Font_GlyphRanges.cpp
new file mode 100644
index 0000000..dd48594
--- /dev/null
+++ b/source/30-game/Font_GlyphRanges.cpp
@@ -0,0 +1,253 @@
+// Adapted from https://github.com/ocornut/imgui
+// specifically imgui_draw.cpp as of time of writing
+
+#include "Font.hpp"
+
+#include <cstddef>
+#include <cstring>
+
+static void UnpackAccumulativeOffsetsIntoRanges(int baseCodepoint, const short* accumulativeOffsets, size_t accumulativeOffsetsCount, char32_t* outRanges) {
+ for (int n = 0; n < accumulativeOffsetsCount; n++, outRanges += 2) {
+ outRanges[0] = outRanges[1] = (char32_t)(baseCodepoint + accumulativeOffsets[n]);
+ baseCodepoint += accumulativeOffsets[n];
+ }
+ outRanges[0] = 0;
+}
+
+// clang-format off
+static const char32_t kDefaultRanges[] = {
+ 0x0020, 0x00FF, // Basic Latin + Latin Supplement
+ 0,
+};
+// clang-format on
+const char32_t* Font::GetGlyphRangesDefault() {
+ return &kDefaultRanges[0];
+}
+
+// clang-format off
+static const char32_t kKoreanRanges[] = {
+ 0x0020, 0x00FF, // Basic Latin + Latin Supplement
+ 0x3131, 0x3163, // Korean alphabets
+ 0xAC00, 0xD7A3, // Korean characters
+ 0,
+};
+// clang-format on
+const char32_t* Font::GetGlyphRangesKorean() {
+ return &kKoreanRanges[0];
+}
+
+// 2999 ideograms code points for Japanese
+// - 2136 Joyo (meaning "for regular use" or "for common use") Kanji code points
+// - 863 Jinmeiyo (meaning "for personal name") Kanji code points
+// - Sourced from the character information database of the Information-technology Promotion Agency, Japan
+// - https://mojikiban.ipa.go.jp/mji/
+// - Available under the terms of the Creative Commons Attribution-ShareAlike 2.1 Japan (CC BY-SA 2.1 JP).
+// - https://creativecommons.org/licenses/by-sa/2.1/jp/deed.en
+// - https://creativecommons.org/licenses/by-sa/2.1/jp/legalcode
+// - You can generate this code by the script at:
+// - https://github.com/vaiorabbit/everyday_use_kanji
+// - References:
+// - List of Joyo Kanji
+// - (Official list by the Agency for Cultural Affairs) https://www.bunka.go.jp/kokugo_nihongo/sisaku/joho/joho/kakuki/14/tosin02/index.html
+// - (Wikipedia) https://en.wikipedia.org/wiki/List_of_j%C5%8Dy%C5%8D_kanji
+// - List of Jinmeiyo Kanji
+// - (Official list by the Ministry of Justice) http://www.moj.go.jp/MINJI/minji86.html
+// - (Wikipedia) https://en.wikipedia.org/wiki/Jinmeiy%C5%8D_kanji
+// - Missing 1 Joyo Kanji: U+20B9F (Kun'yomi: Shikaru, On'yomi: Shitsu,shichi), see https://github.com/ocornut/imgUI/pull/3627 for details.
+// (Stored as accumulative offsets from the initial unicode codepoint 0x4E00. This encoding is designed to helps us compact the source code size.)
+// clang-format off
+static const short kJapaneseAccumulativeOffsetsFrom0x4E00[] =
+{
+ 0,1,2,4,1,1,1,1,2,1,3,3,2,2,1,5,3,5,7,5,6,1,2,1,7,2,6,3,1,8,1,1,4,1,1,18,2,11,2,6,2,1,2,1,5,1,2,1,3,1,2,1,2,3,3,1,1,2,3,1,1,1,12,7,9,1,4,5,1,
+ 1,2,1,10,1,1,9,2,2,4,5,6,9,3,1,1,1,1,9,3,18,5,2,2,2,2,1,6,3,7,1,1,1,1,2,2,4,2,1,23,2,10,4,3,5,2,4,10,2,4,13,1,6,1,9,3,1,1,6,6,7,6,3,1,2,11,3,
+ 2,2,3,2,15,2,2,5,4,3,6,4,1,2,5,2,12,16,6,13,9,13,2,1,1,7,16,4,7,1,19,1,5,1,2,2,7,7,8,2,6,5,4,9,18,7,4,5,9,13,11,8,15,2,1,1,1,2,1,2,2,1,2,2,8,
+ 2,9,3,3,1,1,4,4,1,1,1,4,9,1,4,3,5,5,2,7,5,3,4,8,2,1,13,2,3,3,1,14,1,1,4,5,1,3,6,1,5,2,1,1,3,3,3,3,1,1,2,7,6,6,7,1,4,7,6,1,1,1,1,1,12,3,3,9,5,
+ 2,6,1,5,6,1,2,3,18,2,4,14,4,1,3,6,1,1,6,3,5,5,3,2,2,2,2,12,3,1,4,2,3,2,3,11,1,7,4,1,2,1,3,17,1,9,1,24,1,1,4,2,2,4,1,2,7,1,1,1,3,1,2,2,4,15,1,
+ 1,2,1,1,2,1,5,2,5,20,2,5,9,1,10,8,7,6,1,1,1,1,1,1,6,2,1,2,8,1,1,1,1,5,1,1,3,1,1,1,1,3,1,1,12,4,1,3,1,1,1,1,1,10,3,1,7,5,13,1,2,3,4,6,1,1,30,
+ 2,9,9,1,15,38,11,3,1,8,24,7,1,9,8,10,2,1,9,31,2,13,6,2,9,4,49,5,2,15,2,1,10,2,1,1,1,2,2,6,15,30,35,3,14,18,8,1,16,10,28,12,19,45,38,1,3,2,3,
+ 13,2,1,7,3,6,5,3,4,3,1,5,7,8,1,5,3,18,5,3,6,1,21,4,24,9,24,40,3,14,3,21,3,2,1,2,4,2,3,1,15,15,6,5,1,1,3,1,5,6,1,9,7,3,3,2,1,4,3,8,21,5,16,4,
+ 5,2,10,11,11,3,6,3,2,9,3,6,13,1,2,1,1,1,1,11,12,6,6,1,4,2,6,5,2,1,1,3,3,6,13,3,1,1,5,1,2,3,3,14,2,1,2,2,2,5,1,9,5,1,1,6,12,3,12,3,4,13,2,14,
+ 2,8,1,17,5,1,16,4,2,2,21,8,9,6,23,20,12,25,19,9,38,8,3,21,40,25,33,13,4,3,1,4,1,2,4,1,2,5,26,2,1,1,2,1,3,6,2,1,1,1,1,1,1,2,3,1,1,1,9,2,3,1,1,
+ 1,3,6,3,2,1,1,6,6,1,8,2,2,2,1,4,1,2,3,2,7,3,2,4,1,2,1,2,2,1,1,1,1,1,3,1,2,5,4,10,9,4,9,1,1,1,1,1,1,5,3,2,1,6,4,9,6,1,10,2,31,17,8,3,7,5,40,1,
+ 7,7,1,6,5,2,10,7,8,4,15,39,25,6,28,47,18,10,7,1,3,1,1,2,1,1,1,3,3,3,1,1,1,3,4,2,1,4,1,3,6,10,7,8,6,2,2,1,3,3,2,5,8,7,9,12,2,15,1,1,4,1,2,1,1,
+ 1,3,2,1,3,3,5,6,2,3,2,10,1,4,2,8,1,1,1,11,6,1,21,4,16,3,1,3,1,4,2,3,6,5,1,3,1,1,3,3,4,6,1,1,10,4,2,7,10,4,7,4,2,9,4,3,1,1,1,4,1,8,3,4,1,3,1,
+ 6,1,4,2,1,4,7,2,1,8,1,4,5,1,1,2,2,4,6,2,7,1,10,1,1,3,4,11,10,8,21,4,6,1,3,5,2,1,2,28,5,5,2,3,13,1,2,3,1,4,2,1,5,20,3,8,11,1,3,3,3,1,8,10,9,2,
+ 10,9,2,3,1,1,2,4,1,8,3,6,1,7,8,6,11,1,4,29,8,4,3,1,2,7,13,1,4,1,6,2,6,12,12,2,20,3,2,3,6,4,8,9,2,7,34,5,1,18,6,1,1,4,4,5,7,9,1,2,2,4,3,4,1,7,
+ 2,2,2,6,2,3,25,5,3,6,1,4,6,7,4,2,1,4,2,13,6,4,4,3,1,5,3,4,4,3,2,1,1,4,1,2,1,1,3,1,11,1,6,3,1,7,3,6,2,8,8,6,9,3,4,11,3,2,10,12,2,5,11,1,6,4,5,
+ 3,1,8,5,4,6,6,3,5,1,1,3,2,1,2,2,6,17,12,1,10,1,6,12,1,6,6,19,9,6,16,1,13,4,4,15,7,17,6,11,9,15,12,6,7,2,1,2,2,15,9,3,21,4,6,49,18,7,3,2,3,1,
+ 6,8,2,2,6,2,9,1,3,6,4,4,1,2,16,2,5,2,1,6,2,3,5,3,1,2,5,1,2,1,9,3,1,8,6,4,8,11,3,1,1,1,1,3,1,13,8,4,1,3,2,2,1,4,1,11,1,5,2,1,5,2,5,8,6,1,1,7,
+ 4,3,8,3,2,7,2,1,5,1,5,2,4,7,6,2,8,5,1,11,4,5,3,6,18,1,2,13,3,3,1,21,1,1,4,1,4,1,1,1,8,1,2,2,7,1,2,4,2,2,9,2,1,1,1,4,3,6,3,12,5,1,1,1,5,6,3,2,
+ 4,8,2,2,4,2,7,1,8,9,5,2,3,2,1,3,2,13,7,14,6,5,1,1,2,1,4,2,23,2,1,1,6,3,1,4,1,15,3,1,7,3,9,14,1,3,1,4,1,1,5,8,1,3,8,3,8,15,11,4,14,4,4,2,5,5,
+ 1,7,1,6,14,7,7,8,5,15,4,8,6,5,6,2,1,13,1,20,15,11,9,2,5,6,2,11,2,6,2,5,1,5,8,4,13,19,25,4,1,1,11,1,34,2,5,9,14,6,2,2,6,1,1,14,1,3,14,13,1,6,
+ 12,21,14,14,6,32,17,8,32,9,28,1,2,4,11,8,3,1,14,2,5,15,1,1,1,1,3,6,4,1,3,4,11,3,1,1,11,30,1,5,1,4,1,5,8,1,1,3,2,4,3,17,35,2,6,12,17,3,1,6,2,
+ 1,1,12,2,7,3,3,2,1,16,2,8,3,6,5,4,7,3,3,8,1,9,8,5,1,2,1,3,2,8,1,2,9,12,1,1,2,3,8,3,24,12,4,3,7,5,8,3,3,3,3,3,3,1,23,10,3,1,2,2,6,3,1,16,1,16,
+ 22,3,10,4,11,6,9,7,7,3,6,2,2,2,4,10,2,1,1,2,8,7,1,6,4,1,3,3,3,5,10,12,12,2,3,12,8,15,1,1,16,6,6,1,5,9,11,4,11,4,2,6,12,1,17,5,13,1,4,9,5,1,11,
+ 2,1,8,1,5,7,28,8,3,5,10,2,17,3,38,22,1,2,18,12,10,4,38,18,1,4,44,19,4,1,8,4,1,12,1,4,31,12,1,14,7,75,7,5,10,6,6,13,3,2,11,11,3,2,5,28,15,6,18,
+ 18,5,6,4,3,16,1,7,18,7,36,3,5,3,1,7,1,9,1,10,7,2,4,2,6,2,9,7,4,3,32,12,3,7,10,2,23,16,3,1,12,3,31,4,11,1,3,8,9,5,1,30,15,6,12,3,2,2,11,19,9,
+ 14,2,6,2,3,19,13,17,5,3,3,25,3,14,1,1,1,36,1,3,2,19,3,13,36,9,13,31,6,4,16,34,2,5,4,2,3,3,5,1,1,1,4,3,1,17,3,2,3,5,3,1,3,2,3,5,6,3,12,11,1,3,
+ 1,2,26,7,12,7,2,14,3,3,7,7,11,25,25,28,16,4,36,1,2,1,6,2,1,9,3,27,17,4,3,4,13,4,1,3,2,2,1,10,4,2,4,6,3,8,2,1,18,1,1,24,2,2,4,33,2,3,63,7,1,6,
+ 40,7,3,4,4,2,4,15,18,1,16,1,1,11,2,41,14,1,3,18,13,3,2,4,16,2,17,7,15,24,7,18,13,44,2,2,3,6,1,1,7,5,1,7,1,4,3,3,5,10,8,2,3,1,8,1,1,27,4,2,1,
+ 12,1,2,1,10,6,1,6,7,5,2,3,7,11,5,11,3,6,6,2,3,15,4,9,1,1,2,1,2,11,2,8,12,8,5,4,2,3,1,5,2,2,1,14,1,12,11,4,1,11,17,17,4,3,2,5,5,7,3,1,5,9,9,8,
+ 2,5,6,6,13,13,2,1,2,6,1,2,2,49,4,9,1,2,10,16,7,8,4,3,2,23,4,58,3,29,1,14,19,19,11,11,2,7,5,1,3,4,6,2,18,5,12,12,17,17,3,3,2,4,1,6,2,3,4,3,1,
+ 1,1,1,5,1,1,9,1,3,1,3,6,1,8,1,1,2,6,4,14,3,1,4,11,4,1,3,32,1,2,4,13,4,1,2,4,2,1,3,1,11,1,4,2,1,4,4,6,3,5,1,6,5,7,6,3,23,3,5,3,5,3,3,13,3,9,10,
+ 1,12,10,2,3,18,13,7,160,52,4,2,2,3,2,14,5,4,12,4,6,4,1,20,4,11,6,2,12,27,1,4,1,2,2,7,4,5,2,28,3,7,25,8,3,19,3,6,10,2,2,1,10,2,5,4,1,3,4,1,5,
+ 3,2,6,9,3,6,2,16,3,3,16,4,5,5,3,2,1,2,16,15,8,2,6,21,2,4,1,22,5,8,1,1,21,11,2,1,11,11,19,13,12,4,2,3,2,3,6,1,8,11,1,4,2,9,5,2,1,11,2,9,1,1,2,
+ 14,31,9,3,4,21,14,4,8,1,7,2,2,2,5,1,4,20,3,3,4,10,1,11,9,8,2,1,4,5,14,12,14,2,17,9,6,31,4,14,1,20,13,26,5,2,7,3,6,13,2,4,2,19,6,2,2,18,9,3,5,
+ 12,12,14,4,6,2,3,6,9,5,22,4,5,25,6,4,8,5,2,6,27,2,35,2,16,3,7,8,8,6,6,5,9,17,2,20,6,19,2,13,3,1,1,1,4,17,12,2,14,7,1,4,18,12,38,33,2,10,1,1,
+ 2,13,14,17,11,50,6,33,20,26,74,16,23,45,50,13,38,33,6,6,7,4,4,2,1,3,2,5,8,7,8,9,3,11,21,9,13,1,3,10,6,7,1,2,2,18,5,5,1,9,9,2,68,9,19,13,2,5,
+ 1,4,4,7,4,13,3,9,10,21,17,3,26,2,1,5,2,4,5,4,1,7,4,7,3,4,2,1,6,1,1,20,4,1,9,2,2,1,3,3,2,3,2,1,1,1,20,2,3,1,6,2,3,6,2,4,8,1,3,2,10,3,5,3,4,4,
+ 3,4,16,1,6,1,10,2,4,2,1,1,2,10,11,2,2,3,1,24,31,4,10,10,2,5,12,16,164,15,4,16,7,9,15,19,17,1,2,1,1,5,1,1,1,1,1,3,1,4,3,1,3,1,3,1,2,1,1,3,3,7,
+ 2,8,1,2,2,2,1,3,4,3,7,8,12,92,2,10,3,1,3,14,5,25,16,42,4,7,7,4,2,21,5,27,26,27,21,25,30,31,2,1,5,13,3,22,5,6,6,11,9,12,1,5,9,7,5,5,22,60,3,5,
+ 13,1,1,8,1,1,3,3,2,1,9,3,3,18,4,1,2,3,7,6,3,1,2,3,9,1,3,1,3,2,1,3,1,1,1,2,1,11,3,1,6,9,1,3,2,3,1,2,1,5,1,1,4,3,4,1,2,2,4,4,1,7,2,1,2,2,3,5,13,
+ 18,3,4,14,9,9,4,16,3,7,5,8,2,6,48,28,3,1,1,4,2,14,8,2,9,2,1,15,2,4,3,2,10,16,12,8,7,1,1,3,1,1,1,2,7,4,1,6,4,38,39,16,23,7,15,15,3,2,12,7,21,
+ 37,27,6,5,4,8,2,10,8,8,6,5,1,2,1,3,24,1,16,17,9,23,10,17,6,1,51,55,44,13,294,9,3,6,2,4,2,2,15,1,1,1,13,21,17,68,14,8,9,4,1,4,9,3,11,7,1,1,1,
+ 5,6,3,2,1,1,1,2,3,8,1,2,2,4,1,5,5,2,1,4,3,7,13,4,1,4,1,3,1,1,1,5,5,10,1,6,1,5,2,1,5,2,4,1,4,5,7,3,18,2,9,11,32,4,3,3,2,4,7,11,16,9,11,8,13,38,
+ 32,8,4,2,1,1,2,1,2,4,4,1,1,1,4,1,21,3,11,1,16,1,1,6,1,3,2,4,9,8,57,7,44,1,3,3,13,3,10,1,1,7,5,2,7,21,47,63,3,15,4,7,1,16,1,1,2,8,2,3,42,15,4,
+ 1,29,7,22,10,3,78,16,12,20,18,4,67,11,5,1,3,15,6,21,31,32,27,18,13,71,35,5,142,4,10,1,2,50,19,33,16,35,37,16,19,27,7,1,133,19,1,4,8,7,20,1,4,
+ 4,1,10,3,1,6,1,2,51,5,40,15,24,43,22928,11,1,13,154,70,3,1,1,7,4,10,1,2,1,1,2,1,2,1,2,2,1,1,2,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,2,1,1,1,
+ 3,2,1,1,1,1,2,1,1,
+};
+// clang-format on
+
+// clang-format off
+static char32_t japaneseBaseRanges[] = {
+ // not zero-terminated
+ 0x0020, 0x00FF, // Basic Latin + Latin Supplement
+ 0x3000, 0x30FF, // CJK Symbols and Punctuations, Hiragana, Katakana
+ 0x31F0, 0x31FF, // Katakana Phonetic Extensions
+ 0xFF00, 0xFFEF // Half-width characters
+};
+// clang-format on
+static char32_t japaneseFullRanges[std::size(japaneseBaseRanges) + std::size(kJapaneseAccumulativeOffsetsFrom0x4E00) * 2 + 1] = { 0 };
+const char32_t* Font::GetGlyphRangesJapanese() {
+ if (!japaneseFullRanges[0]) {
+ memcpy(japaneseFullRanges, japaneseBaseRanges, sizeof(japaneseBaseRanges));
+ UnpackAccumulativeOffsetsIntoRanges(0x4E00, kJapaneseAccumulativeOffsetsFrom0x4E00, std::size(kJapaneseAccumulativeOffsetsFrom0x4E00), japaneseFullRanges + std::size(japaneseBaseRanges));
+ }
+ return &japaneseFullRanges[0];
+}
+
+// clang-format off
+static const char32_t kChineseFullRanges[] = {
+ 0x0020, 0x00FF, // Basic Latin + Latin Supplement
+ 0x2000, 0x206F, // General Punctuation
+ 0x3000, 0x30FF, // CJK Symbols and Punctuations, Hiragana, Katakana
+ 0x31F0, 0x31FF, // Katakana Phonetic Extensions
+ 0xFF00, 0xFFEF, // Half-width characters
+ 0x4e00, 0x9FAF, // CJK Ideograms
+ 0,
+};
+// clang-format on
+const char32_t* Font::GetGlyphRangesChineseFull() {
+ return &kChineseFullRanges[0];
+}
+
+// Store 2500 regularly used characters for Simplified Chinese.
+// Sourced from https://zh.wiktionary.org/wiki/%E9%99%84%E5%BD%95:%E7%8E%B0%E4%BB%A3%E6%B1%89%E8%AF%AD%E5%B8%B8%E7%94%A8%E5%AD%97%E8%A1%A8
+// This table covers 97.97% of all characters used during the month in July, 1987.
+// (Stored as accumulative offsets from the initial unicode codepoint 0x4E00. This encoding is designed to helps us compact the source code size.)
+// clang-format off
+static const short kChineseAccumulativeOffsetsFrom0x4E00[] = {
+ 0,1,2,4,1,1,1,1,2,1,3,2,1,2,2,1,1,1,1,1,5,2,1,2,3,3,3,2,2,4,1,1,1,2,1,5,2,3,1,2,1,2,1,1,2,1,1,2,2,1,4,1,1,1,1,5,10,1,2,19,2,1,2,1,2,1,2,1,2,
+ 1,5,1,6,3,2,1,2,2,1,1,1,4,8,5,1,1,4,1,1,3,1,2,1,5,1,2,1,1,1,10,1,1,5,2,4,6,1,4,2,2,2,12,2,1,1,6,1,1,1,4,1,1,4,6,5,1,4,2,2,4,10,7,1,1,4,2,4,
+ 2,1,4,3,6,10,12,5,7,2,14,2,9,1,1,6,7,10,4,7,13,1,5,4,8,4,1,1,2,28,5,6,1,1,5,2,5,20,2,2,9,8,11,2,9,17,1,8,6,8,27,4,6,9,20,11,27,6,68,2,2,1,1,
+ 1,2,1,2,2,7,6,11,3,3,1,1,3,1,2,1,1,1,1,1,3,1,1,8,3,4,1,5,7,2,1,4,4,8,4,2,1,2,1,1,4,5,6,3,6,2,12,3,1,3,9,2,4,3,4,1,5,3,3,1,3,7,1,5,1,1,1,1,2,
+ 3,4,5,2,3,2,6,1,1,2,1,7,1,7,3,4,5,15,2,2,1,5,3,22,19,2,1,1,1,1,2,5,1,1,1,6,1,1,12,8,2,9,18,22,4,1,1,5,1,16,1,2,7,10,15,1,1,6,2,4,1,2,4,1,6,
+ 1,1,3,2,4,1,6,4,5,1,2,1,1,2,1,10,3,1,3,2,1,9,3,2,5,7,2,19,4,3,6,1,1,1,1,1,4,3,2,1,1,1,2,5,3,1,1,1,2,2,1,1,2,1,1,2,1,3,1,1,1,3,7,1,4,1,1,2,1,
+ 1,2,1,2,4,4,3,8,1,1,1,2,1,3,5,1,3,1,3,4,6,2,2,14,4,6,6,11,9,1,15,3,1,28,5,2,5,5,3,1,3,4,5,4,6,14,3,2,3,5,21,2,7,20,10,1,2,19,2,4,28,28,2,3,
+ 2,1,14,4,1,26,28,42,12,40,3,52,79,5,14,17,3,2,2,11,3,4,6,3,1,8,2,23,4,5,8,10,4,2,7,3,5,1,1,6,3,1,2,2,2,5,28,1,1,7,7,20,5,3,29,3,17,26,1,8,4,
+ 27,3,6,11,23,5,3,4,6,13,24,16,6,5,10,25,35,7,3,2,3,3,14,3,6,2,6,1,4,2,3,8,2,1,1,3,3,3,4,1,1,13,2,2,4,5,2,1,14,14,1,2,2,1,4,5,2,3,1,14,3,12,
+ 3,17,2,16,5,1,2,1,8,9,3,19,4,2,2,4,17,25,21,20,28,75,1,10,29,103,4,1,2,1,1,4,2,4,1,2,3,24,2,2,2,1,1,2,1,3,8,1,1,1,2,1,1,3,1,1,1,6,1,5,3,1,1,
+ 1,3,4,1,1,5,2,1,5,6,13,9,16,1,1,1,1,3,2,3,2,4,5,2,5,2,2,3,7,13,7,2,2,1,1,1,1,2,3,3,2,1,6,4,9,2,1,14,2,14,2,1,18,3,4,14,4,11,41,15,23,15,23,
+ 176,1,3,4,1,1,1,1,5,3,1,2,3,7,3,1,1,2,1,2,4,4,6,2,4,1,9,7,1,10,5,8,16,29,1,1,2,2,3,1,3,5,2,4,5,4,1,1,2,2,3,3,7,1,6,10,1,17,1,44,4,6,2,1,1,6,
+ 5,4,2,10,1,6,9,2,8,1,24,1,2,13,7,8,8,2,1,4,1,3,1,3,3,5,2,5,10,9,4,9,12,2,1,6,1,10,1,1,7,7,4,10,8,3,1,13,4,3,1,6,1,3,5,2,1,2,17,16,5,2,16,6,
+ 1,4,2,1,3,3,6,8,5,11,11,1,3,3,2,4,6,10,9,5,7,4,7,4,7,1,1,4,2,1,3,6,8,7,1,6,11,5,5,3,24,9,4,2,7,13,5,1,8,82,16,61,1,1,1,4,2,2,16,10,3,8,1,1,
+ 6,4,2,1,3,1,1,1,4,3,8,4,2,2,1,1,1,1,1,6,3,5,1,1,4,6,9,2,1,1,1,2,1,7,2,1,6,1,5,4,4,3,1,8,1,3,3,1,3,2,2,2,2,3,1,6,1,2,1,2,1,3,7,1,8,2,1,2,1,5,
+ 2,5,3,5,10,1,2,1,1,3,2,5,11,3,9,3,5,1,1,5,9,1,2,1,5,7,9,9,8,1,3,3,3,6,8,2,3,2,1,1,32,6,1,2,15,9,3,7,13,1,3,10,13,2,14,1,13,10,2,1,3,10,4,15,
+ 2,15,15,10,1,3,9,6,9,32,25,26,47,7,3,2,3,1,6,3,4,3,2,8,5,4,1,9,4,2,2,19,10,6,2,3,8,1,2,2,4,2,1,9,4,4,4,6,4,8,9,2,3,1,1,1,1,3,5,5,1,3,8,4,6,
+ 2,1,4,12,1,5,3,7,13,2,5,8,1,6,1,2,5,14,6,1,5,2,4,8,15,5,1,23,6,62,2,10,1,1,8,1,2,2,10,4,2,2,9,2,1,1,3,2,3,1,5,3,3,2,1,3,8,1,1,1,11,3,1,1,4,
+ 3,7,1,14,1,2,3,12,5,2,5,1,6,7,5,7,14,11,1,3,1,8,9,12,2,1,11,8,4,4,2,6,10,9,13,1,1,3,1,5,1,3,2,4,4,1,18,2,3,14,11,4,29,4,2,7,1,3,13,9,2,2,5,
+ 3,5,20,7,16,8,5,72,34,6,4,22,12,12,28,45,36,9,7,39,9,191,1,1,1,4,11,8,4,9,2,3,22,1,1,1,1,4,17,1,7,7,1,11,31,10,2,4,8,2,3,2,1,4,2,16,4,32,2,
+ 3,19,13,4,9,1,5,2,14,8,1,1,3,6,19,6,5,1,16,6,2,10,8,5,1,2,3,1,5,5,1,11,6,6,1,3,3,2,6,3,8,1,1,4,10,7,5,7,7,5,8,9,2,1,3,4,1,1,3,1,3,3,2,6,16,
+ 1,4,6,3,1,10,6,1,3,15,2,9,2,10,25,13,9,16,6,2,2,10,11,4,3,9,1,2,6,6,5,4,30,40,1,10,7,12,14,33,6,3,6,7,3,1,3,1,11,14,4,9,5,12,11,49,18,51,31,
+ 140,31,2,2,1,5,1,8,1,10,1,4,4,3,24,1,10,1,3,6,6,16,3,4,5,2,1,4,2,57,10,6,22,2,22,3,7,22,6,10,11,36,18,16,33,36,2,5,5,1,1,1,4,10,1,4,13,2,7,
+ 5,2,9,3,4,1,7,43,3,7,3,9,14,7,9,1,11,1,1,3,7,4,18,13,1,14,1,3,6,10,73,2,2,30,6,1,11,18,19,13,22,3,46,42,37,89,7,3,16,34,2,2,3,9,1,7,1,1,1,2,
+ 2,4,10,7,3,10,3,9,5,28,9,2,6,13,7,3,1,3,10,2,7,2,11,3,6,21,54,85,2,1,4,2,2,1,39,3,21,2,2,5,1,1,1,4,1,1,3,4,15,1,3,2,4,4,2,3,8,2,20,1,8,7,13,
+ 4,1,26,6,2,9,34,4,21,52,10,4,4,1,5,12,2,11,1,7,2,30,12,44,2,30,1,1,3,6,16,9,17,39,82,2,2,24,7,1,7,3,16,9,14,44,2,1,2,1,2,3,5,2,4,1,6,7,5,3,
+ 2,6,1,11,5,11,2,1,18,19,8,1,3,24,29,2,1,3,5,2,2,1,13,6,5,1,46,11,3,5,1,1,5,8,2,10,6,12,6,3,7,11,2,4,16,13,2,5,1,1,2,2,5,2,28,5,2,23,10,8,4,
+ 4,22,39,95,38,8,14,9,5,1,13,5,4,3,13,12,11,1,9,1,27,37,2,5,4,4,63,211,95,2,2,2,1,3,5,2,1,1,2,2,1,1,1,3,2,4,1,2,1,1,5,2,2,1,1,2,3,1,3,1,1,1,
+ 3,1,4,2,1,3,6,1,1,3,7,15,5,3,2,5,3,9,11,4,2,22,1,6,3,8,7,1,4,28,4,16,3,3,25,4,4,27,27,1,4,1,2,2,7,1,3,5,2,28,8,2,14,1,8,6,16,25,3,3,3,14,3,
+ 3,1,1,2,1,4,6,3,8,4,1,1,1,2,3,6,10,6,2,3,18,3,2,5,5,4,3,1,5,2,5,4,23,7,6,12,6,4,17,11,9,5,1,1,10,5,12,1,1,11,26,33,7,3,6,1,17,7,1,5,12,1,11,
+ 2,4,1,8,14,17,23,1,2,1,7,8,16,11,9,6,5,2,6,4,16,2,8,14,1,11,8,9,1,1,1,9,25,4,11,19,7,2,15,2,12,8,52,7,5,19,2,16,4,36,8,1,16,8,24,26,4,6,2,9,
+ 5,4,36,3,28,12,25,15,37,27,17,12,59,38,5,32,127,1,2,9,17,14,4,1,2,1,1,8,11,50,4,14,2,19,16,4,17,5,4,5,26,12,45,2,23,45,104,30,12,8,3,10,2,2,
+ 3,3,1,4,20,7,2,9,6,15,2,20,1,3,16,4,11,15,6,134,2,5,59,1,2,2,2,1,9,17,3,26,137,10,211,59,1,2,4,1,4,1,1,1,2,6,2,3,1,1,2,3,2,3,1,3,4,4,2,3,3,
+ 1,4,3,1,7,2,2,3,1,2,1,3,3,3,2,2,3,2,1,3,14,6,1,3,2,9,6,15,27,9,34,145,1,1,2,1,1,1,1,2,1,1,1,1,2,2,2,3,1,2,1,1,1,2,3,5,8,3,5,2,4,1,3,2,2,2,12,
+ 4,1,1,1,10,4,5,1,20,4,16,1,15,9,5,12,2,9,2,5,4,2,26,19,7,1,26,4,30,12,15,42,1,6,8,172,1,1,4,2,1,1,11,2,2,4,2,1,2,1,10,8,1,2,1,4,5,1,2,5,1,8,
+ 4,1,3,4,2,1,6,2,1,3,4,1,2,1,1,1,1,12,5,7,2,4,3,1,1,1,3,3,6,1,2,2,3,3,3,2,1,2,12,14,11,6,6,4,12,2,8,1,7,10,1,35,7,4,13,15,4,3,23,21,28,52,5,
+ 26,5,6,1,7,10,2,7,53,3,2,1,1,1,2,163,532,1,10,11,1,3,3,4,8,2,8,6,2,2,23,22,4,2,2,4,2,1,3,1,3,3,5,9,8,2,1,2,8,1,10,2,12,21,20,15,105,2,3,1,1,
+ 3,2,3,1,1,2,5,1,4,15,11,19,1,1,1,1,5,4,5,1,1,2,5,3,5,12,1,2,5,1,11,1,1,15,9,1,4,5,3,26,8,2,1,3,1,1,15,19,2,12,1,2,5,2,7,2,19,2,20,6,26,7,5,
+ 2,2,7,34,21,13,70,2,128,1,1,2,1,1,2,1,1,3,2,2,2,15,1,4,1,3,4,42,10,6,1,49,85,8,1,2,1,1,4,4,2,3,6,1,5,7,4,3,211,4,1,2,1,2,5,1,2,4,2,2,6,5,6,
+ 10,3,4,48,100,6,2,16,296,5,27,387,2,2,3,7,16,8,5,38,15,39,21,9,10,3,7,59,13,27,21,47,5,21,6
+};
+// clang-format on
+
+// clang-format off
+static char32_t chineseCommonBaseRanges[] = {
+ // not zero-terminated
+ 0x0020, 0x00FF, // Basic Latin + Latin Supplement
+ 0x2000, 0x206F, // General Punctuation
+ 0x3000, 0x30FF, // CJK Symbols and Punctuations, Hiragana, Katakana
+ 0x31F0, 0x31FF, // Katakana Phonetic Extensions
+ 0xFF00, 0xFFEF // Half-width characters
+};
+// clang-format on
+static char32_t chineseCommonFullRanges[std::size(chineseCommonBaseRanges) + std::size(kChineseAccumulativeOffsetsFrom0x4E00) * 2 + 1] = { 0 };
+const char32_t* Font::GetGlyphRangesChineseSimplifiedCommon() {
+ if (!chineseCommonFullRanges[0]) {
+ memcpy(chineseCommonFullRanges, chineseCommonBaseRanges, sizeof(chineseCommonBaseRanges));
+ UnpackAccumulativeOffsetsIntoRanges(0x4E00, kChineseAccumulativeOffsetsFrom0x4E00, std::size(kChineseAccumulativeOffsetsFrom0x4E00), chineseCommonFullRanges + std::size(chineseCommonBaseRanges));
+ }
+ return &chineseCommonFullRanges[0];
+}
+
+// clang-format off
+static const char32_t kCryllicRanges[] = {
+ 0x0020, 0x00FF, // Basic Latin + Latin Supplement
+ 0x0400, 0x052F, // Cyrillic + Cyrillic Supplement
+ 0x2DE0, 0x2DFF, // Cyrillic Extended-A
+ 0xA640, 0xA69F, // Cyrillic Extended-B
+ 0,
+};
+// clang-format on
+const char32_t* Font::GetGlyphRangesCyrillic() {
+ return &kCryllicRanges[0];
+}
+
+// clang-format off
+static const char32_t kThaiRanges[] = {
+ 0x0020, 0x00FF, // Basic Latin
+ 0x2010, 0x205E, // Punctuations
+ 0x0E00, 0x0E7F, // Thai
+ 0,
+};
+// clang-format on
+const char32_t* Font::GetGlyphRangesThai() {
+ return &kThaiRanges[0];
+}
+
+// clang-format off
+static const char32_t kVietnameseRanges[] = {
+ 0x0020, 0x00FF, // Basic Latin
+ 0x0102, 0x0103,
+ 0x0110, 0x0111,
+ 0x0128, 0x0129,
+ 0x0168, 0x0169,
+ 0x01A0, 0x01A1,
+ 0x01AF, 0x01B0,
+ 0x1EA0, 0x1EF9,
+ 0,
+};
+// clang-format on
+const char32_t* Font::GetGlyphRangesVietnamese() {
+ return &kVietnameseRanges[0];
+}
diff --git a/source/30-game/Texture.cpp b/source/30-game/Texture.cpp
index 6fa7c8a..e5c9705 100644
--- a/source/30-game/Texture.cpp
+++ b/source/30-game/Texture.cpp
@@ -14,7 +14,8 @@ Texture::~Texture() {
glDeleteTextures(1, &mHandle);
}
-static GLenum MapTextureFilteringToGL(Tags::TexFilter option) {
+namespace ProjectBrussel_UNITY_ID {
+GLint MapTextureFilteringToGL(Tags::TexFilter option) {
using namespace Tags;
switch (option) {
case TF_Linear: return GL_LINEAR;
@@ -22,8 +23,11 @@ static GLenum MapTextureFilteringToGL(Tags::TexFilter option) {
}
return 0;
}
+} // namespace ProjectBrussel_UNITY_ID
+
+Texture::ErrorCode Texture::InitFromFile(const char* filePath, const TextureProperties& props) {
+ using namespace ProjectBrussel_UNITY_ID;
-Texture::ErrorCode Texture::InitFromFile(const char* filePath) {
if (IsValid()) {
return EC_AlreadyInitialized;
}
@@ -40,21 +44,23 @@ Texture::ErrorCode Texture::InitFromFile(const char* filePath) {
glGenTextures(1, &mHandle);
glBindTexture(GL_TEXTURE_2D, mHandle);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, MapTextureFilteringToGL(props.minifyingFilter));
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, MapTextureFilteringToGL(props.magnifyingFilter));
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, result);
- mInfo.size = { width, height };
+ mProps.size = { width, height };
return EC_Success;
}
-Texture::ErrorCode Texture::InitFromImage(const Image& image) {
+Texture::ErrorCode Texture::InitFromImage(const Image& image, const TextureProperties& props) {
+ using namespace ProjectBrussel_UNITY_ID;
+
if (IsValid()) {
return EC_AlreadyInitialized;
}
- GLenum sourceFormat;
+ GLint sourceFormat;
switch (image.GetChannels()) {
case 1: sourceFormat = GL_RED; break;
case 2: sourceFormat = GL_RG; break;
@@ -69,11 +75,13 @@ Texture::ErrorCode Texture::InitFromImage(const Image& image) {
glGenTextures(1, &mHandle);
glBindTexture(GL_TEXTURE_2D, mHandle);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, MapTextureFilteringToGL(props.minifyingFilter));
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, MapTextureFilteringToGL(props.magnifyingFilter));
glTexImage2D(GL_TEXTURE_2D, 0, sourceFormat, size.x, size.y, 0, sourceFormat, GL_UNSIGNED_BYTE, dataPtr);
- mInfo.size = size;
+ // Copy all [In] properties
+ mProps = props;
+ mProps.size = size;
return EC_Success;
}
@@ -192,7 +200,7 @@ Texture::ErrorCode Texture::InitAtlas(const AtlasInput& in, AtlasOutput* out) {
// 5. Generate atlas texture info
mHandle = atlasTexture;
- mInfo.size = { atlasWidth, atlasHeight };
+ mProps.size = { atlasWidth, atlasHeight };
// 6. Generate output information
if (out) {
@@ -216,8 +224,8 @@ Texture::ErrorCode Texture::InitAtlas(const AtlasInput& in, AtlasOutput* out) {
return EC_Success;
}
-const TextureInfo& Texture::GetInfo() const {
- return mInfo;
+const TextureProperties& Texture::GetInfo() const {
+ return mProps;
}
GLuint Texture::GetHandle() const {
diff --git a/source/30-game/Texture.hpp b/source/30-game/Texture.hpp
index 108dfa7..2931724 100644
--- a/source/30-game/Texture.hpp
+++ b/source/30-game/Texture.hpp
@@ -19,17 +19,19 @@ struct Subregion {
float v1 = 0.0f;
};
-struct TextureInfo {
- glm::ivec2 size;
- Tags::TexFilter minifyingFilter = Tags::TF_Linear;
- Tags::TexFilter magnifyingFilter = Tags::TF_Linear;
+struct TextureProperties {
+ // All [In] fields should be set by the user before initializing the texture
+ // All [Out] fields will be set by the initialization process, and any previously user-set value will be ignored
+ /* [In] */ Tags::TexFilter minifyingFilter = Tags::TF_Linear;
+ /* [In] */ Tags::TexFilter magnifyingFilter = Tags::TF_Linear;
+ /* [Out] */ glm::ivec2 size;
};
class Texture : public RefCounted {
friend class TextureStitcher;
private:
- TextureInfo mInfo;
+ TextureProperties mProps;
GLuint mHandle = 0;
public:
@@ -47,9 +49,8 @@ public:
EC_FileIoFailed,
EC_InvalidImage,
};
-
- ErrorCode InitFromFile(const char* filePath);
- ErrorCode InitFromImage(const Image& image);
+ ErrorCode InitFromFile(const char* filePath, const TextureProperties& props = {});
+ ErrorCode InitFromImage(const Image& image, const TextureProperties& props = {});
struct AtlasSource {
std::string name;
@@ -77,7 +78,7 @@ public:
};
ErrorCode InitAtlas(const AtlasInput& in, AtlasOutput* out = nullptr);
- const TextureInfo& GetInfo() const;
+ const TextureProperties& GetInfo() const;
GLuint GetHandle() const;
bool IsValid() const;