From 38fca841e775926739e3fb991d7c519e871af089 Mon Sep 17 00:00:00 2001 From: Anonymous Maarten Date: Wed, 1 Aug 2018 15:15:26 +0200 Subject: [PATCH 1/7] rwengine: add font-dependent kerning --- rwengine/src/render/TextRenderer.cpp | 180 ++++++++++++++++++--------- rwengine/src/render/TextRenderer.hpp | 31 ++++- 2 files changed, 147 insertions(+), 64 deletions(-) diff --git a/rwengine/src/render/TextRenderer.cpp b/rwengine/src/render/TextRenderer.cpp index 5ecb2aa5..8e41691f 100644 --- a/rwengine/src/render/TextRenderer.cpp +++ b/rwengine/src/render/TextRenderer.cpp @@ -12,18 +12,24 @@ #include "engine/GameData.hpp" #include "render/GameRenderer.hpp" -int charToIndex(uint16_t g) { +namespace { + +unsigned charToIndex(std::uint16_t g) { // Correct for the default font maps /// @todo confirm for JA / RU font maps return g - 32; } -glm::vec4 indexToCoord(font_t font, int index) { - float x = static_cast(index % 16); - float y = static_cast(index / 16) + 0.01f; - float fontHeight = ((font == FONT_PAGER) ? 16.f : 13.f); - glm::vec2 gsize(1.f / 16.f, 1.f / fontHeight); - return glm::vec4(x, y, x + 1, y + 0.98f) * glm::vec4(gsize, gsize); +glm::vec4 indexToTexCoord(int index, const glm::u32vec2 &textureSize, const glm::u8vec2 &glyphOffset) { + constexpr unsigned TEXTURE_COLUMNS = 16; + const float x = index % TEXTURE_COLUMNS; + const float y = index / TEXTURE_COLUMNS; + // Add offset to avoid 'leakage' between adjacent glyphs + float s = (x * glyphOffset.x + 0.5f) / textureSize.x; + float t = (y * glyphOffset.y + 0.5f) / textureSize.y; + float p = ((x + 1) * glyphOffset.x - 1.5f) / textureSize.x; + float q = ((y + 1) * glyphOffset.y - 1.5f) / textureSize.y; + return glm::vec4(s, t, p, q); } const char* TextVertexShader = R"( @@ -40,9 +46,9 @@ uniform vec2 alignment; void main() { - gl_Position = proj * vec4(alignment + position, 0.0, 1.0); - TexCoord = texcoord; - Colour = colour; + gl_Position = proj * vec4(alignment + position, 0.0, 1.0); + TexCoord = texcoord; + Colour = colour; })"; const char* TextFragmentShader = R"( @@ -56,10 +62,64 @@ out vec4 outColour; void main() { - float a = texture(fontTexture, TexCoord).a; - outColour = vec4(Colour, a); + float a = texture(fontTexture, TexCoord).a; + outColour = vec4(Colour, a); })"; + +constexpr size_t GLYPHS_NB = 193; +using FontWidthLut = std::array; + +constexpr std::array fontWidthsPager = { + 3, 3, 6, 8, 6, 10, 8, 3, 5, 5, 7, 0, 3, 7, 3, 0, // 1 + 6, 4, 6, 6, 7, 6, 6, 6, 6, 6, 3, 0, 0, 0, 0, 6, // 2 + 0, 6, 6, 6, 6, 6, 6, 6, 6, 3, 6, 6, 5, 8, 7, 6, // 3 + 6, 7, 6, 6, 5, 6, 6, 8, 6, 7, 7, 0, 0, 0, 0, 0, // 4 + 0, 6, 6, 6, 6, 6, 5, 6, 6, 3, 4, 6, 3, 9, 6, 6, // 5 + 6, 6, 5, 6, 5, 6, 6, 8, 6, 6, 5, 0, 0, 0, 0, 0, // 6 + 6, 6, 6, 6, 8, 6, 6, 6, 6, 6, 5, 5, 6, 6, 6, 6, // 7 + 6, 6, 6, 6, 6, 6, 7, 6, 6, 6, 6, 9, 6, 6, 6, 6, // 8 + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7, 6, 6, // 9 + 3, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, // 10 + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, // 11 + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, // 12 + 8, +}; + +constexpr std::array fontWidthsPriceDown = { + 11, 13, 30, 27, 20, 24, 22, 12, 14, 14, 0, 26, 9, 14, 9, 26, // 1 + 20, 19, 20, 20, 22, 20, 20, 19, 20, 20, 13, 29, 24, 29, 24, 20, // 2 + 27, 20, 20, 20, 20, 20, 17, 20, 20, 10, 20, 20, 15, 30, 20, 20, // 3 + 20, 20, 20, 20, 22, 20, 22, 32, 20, 20, 19, 27, 20, 32, 23, 13, // 4 + 27, 21, 21, 21, 21, 21, 18, 22, 21, 12, 20, 22, 17, 30, 22, 21, // 5 + 21, 21, 21, 22, 21, 21, 21, 29, 19, 23, 21, 28, 25, 0, 0, 0, // 6 + 20, 20, 20, 20, 30, 20, 20, 20, 20, 20, 10, 10, 10, 10, 21, 21, // 7 + 21, 21, 20, 20, 20, 20, 21, 21, 21, 21, 21, 32, 23, 21, 21, 21, // 8 + 21, 12, 12, 12, 12, 21, 21, 21, 21, 21, 21, 21, 21, 20, 20, 20, // 9 + 13, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, // 10 + 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, // 11 + 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, // 12 + 16, +}; + +constexpr std::array fontWidthsArial = { + 27, 25, 55, 43, 47, 65, 53, 19, 29, 31, 21, 45, 23, 35, 27, 29, // 1 + 47, 33, 45, 43, 49, 47, 47, 41, 47, 45, 25, 23, 53, 43, 53, 39, // 2 + 61, 53, 51, 47, 49, 45, 43, 49, 53, 23, 41, 53, 45, 59, 53, 51, // 3 + 47, 51, 49, 49, 45, 51, 49, 59, 59, 47, 51, 31, 27, 31, 29, 27, // 4 + 19, 43, 45, 43, 43, 45, 27, 45, 43, 21, 33, 45, 23, 65, 43, 43, // 5 + 47, 45, 33, 41, 29, 43, 41, 61, 51, 43, 43, 67, 53, 67, 67, 71, // 6 + 53, 53, 53, 53, 65, 49, 45, 45, 45, 45, 23, 23, 23, 23, 51, 51, // 7 + 51, 51, 51, 51, 51, 51, 51, 43, 43, 43, 43, 65, 43, 45, 45, 45, // 8 + 45, 21, 21, 21, 21, 43, 43, 43, 43, 43, 43, 43, 43, 53, 43, 39, // 9 + 25, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, // 10 + 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 11, 19, 19, // 11 + 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, // 12 + 19, +}; + +} + struct TextVertex { glm::vec2 position; glm::vec2 texcoord; @@ -85,46 +145,43 @@ struct TextVertex { TextRenderer::TextRenderer(GameRenderer* renderer) : renderer(renderer) { textShader = renderer->getRenderer()->createShader(TextVertexShader, TextFragmentShader); - - std::fill(glyphData.begin(), glyphData.end(), GlyphInfo{.9f}); - - glyphData[charToIndex(' ')].widthFrac = 0.4f; - glyphData[charToIndex('-')].widthFrac = 0.5f; - glyphData[charToIndex('\'')].widthFrac = 0.5f; - glyphData[charToIndex('(')].widthFrac = 0.45f; - glyphData[charToIndex(')')].widthFrac = 0.45f; - glyphData[charToIndex(':')].widthFrac = 0.65f; - glyphData[charToIndex('$')].widthFrac = 0.65f; - - for (char g = '0'; g <= '9'; ++g) { - glyphData[charToIndex(g)].widthFrac = 0.65f; - } - - // Assumes contiguous a-z character encoding - for (char g = 0; g <= ('z' - 'a'); g++) { - glyphData[charToIndex('a' + g)].widthFrac = 0.7f; - glyphData[charToIndex('A' + g)].widthFrac = 0.7f; - } - // case 'i': - glyphData[charToIndex('i')].widthFrac = 0.4f; - glyphData[charToIndex('I')].widthFrac = 0.4f; - // case 'l': - glyphData[charToIndex('l')].widthFrac = 0.5f; - glyphData[charToIndex('L')].widthFrac = 0.5f; - // case 'm': - glyphData[charToIndex('m')].widthFrac = 1.0f; - glyphData[charToIndex('M')].widthFrac = 1.0f; - // case 'w': - glyphData[charToIndex('w')].widthFrac = 1.0f; - glyphData[charToIndex('W')].widthFrac = 1.0f; - // case 'accent aigu' - glyphData[0x91].widthFrac = 0.6f; } -void TextRenderer::setFontTexture(int index, const std::string& texture) { - if (index < GAME_FONTS) { - fonts[index] = texture; +void TextRenderer::setFontTexture(font_t font, const std::string& textureName) { + if (font >= FONTS_COUNT) { + RW_ERROR("Illegal font: " << font); + return; } + auto ftexture = renderer->getData()->findSlotTexture("fonts", textureName); + const glm::u32vec2 textureSize = ftexture->getSize(); + glm::u8vec2 glyphOffset{textureSize.x / 16, textureSize.x / 16}; + if (font != FONT_PAGER) { + glyphOffset.y += glyphOffset.y / 4; + } + const FontWidthLut *glyphWidths; + switch (font) { + case FONT_PAGER: + glyphWidths = &fontWidthsPager; + break; + case FONT_PRICEDOWN: + glyphWidths = &fontWidthsPriceDown; + break; + case FONT_ARIAL: + glyphWidths = &fontWidthsArial; + break; + } + std::uint8_t monoWidth = 0; + if (font == FONT_PAGER) { + monoWidth = 1 + *std::max_element(fontWidthsPager.cbegin(), + fontWidthsPager.cend()); + } + fonts[font] = FontMetaData{ + textureName, + *glyphWidths, + textureSize, + glyphOffset, + monoWidth + }; } void TextRenderer::renderText(const TextRenderer::TextInfo& ti, @@ -151,6 +208,8 @@ void TextRenderer::renderText(const TextRenderer::TextInfo& ti, auto text = ti.text; + const auto &fontMetaData = fonts[ti.font]; + for (size_t i = 0; i < text.length(); ++i) { char16_t c = text[i]; @@ -209,8 +268,8 @@ void TextRenderer::renderText(const TextRenderer::TextInfo& ti, colour = glm::vec3(ti.baseColour) * (1 / 255.f); } - int glyph = charToIndex(c); - if (glyph >= GAME_GLYPHS) { + auto glyph = charToIndex(c); + if (glyph >= fontMetaData.glyphWidths.size()) { continue; } @@ -230,11 +289,8 @@ void TextRenderer::renderText(const TextRenderer::TextInfo& ti, } } - auto& data = glyphData[glyph]; - auto tex = indexToCoord(ti.font, glyph); - - ss.x = ti.size * data.widthFrac; - tex.z = tex.x + (tex.z - tex.x) * data.widthFrac; + auto tex = indexToTexCoord(glyph, fontMetaData.textureSize, fontMetaData.glyphOffset); + ss.x = ti.size * static_cast(fontMetaData.glyphOffset.x) / fontMetaData.glyphOffset.y; // Handle special chars. if (c == '\n') { @@ -247,7 +303,15 @@ void TextRenderer::renderText(const TextRenderer::TextInfo& ti, lineLength++; glm::vec2 p = coord; - coord.x += ss.x; + float factor = ti.size / static_cast(fontMetaData.glyphOffset.y); + float glyphWidth = factor * static_cast(fontMetaData.glyphWidths[glyph]); + if (fontMetaData.monoWidth != 0) { + float monoWidth = factor * fontMetaData.monoWidth; + p.x += static_cast(monoWidth - glyphWidth) / 2; + coord.x += monoWidth; + } else { + coord.x += glyphWidth; + } maxWidth = std::max(coord.x, maxWidth); geo.emplace_back(glm::vec2{p.x, p.y + ss.y}, glm::vec2{tex.x, tex.w}, colour); @@ -287,7 +351,7 @@ void TextRenderer::renderText(const TextRenderer::TextInfo& ti, dp.start = 0; dp.blendMode = BlendMode::BLEND_ALPHA; dp.count = gb.getCount(); - auto ftexture = renderer->getData()->findSlotTexture("fonts", fonts[ti.font]); + auto ftexture = renderer->getData()->findSlotTexture("fonts", fontMetaData.textureName); dp.textures = {ftexture->getName()}; dp.depthWrite = false; diff --git a/rwengine/src/render/TextRenderer.hpp b/rwengine/src/render/TextRenderer.hpp index 0fe2a953..41f0aefe 100644 --- a/rwengine/src/render/TextRenderer.hpp +++ b/rwengine/src/render/TextRenderer.hpp @@ -14,9 +14,6 @@ #include #include -#define GAME_FONTS 3 -#define GAME_GLYPHS 192 - class GameRenderer; /** * @brief Handles rendering of bitmap font textures. @@ -62,13 +59,35 @@ public: TextRenderer(GameRenderer* renderer); ~TextRenderer() = default; - void setFontTexture(int index, const std::string& font); + void setFontTexture(font_t font, const std::string& textureName); void renderText(const TextInfo& ti, bool forceColour = false); private: - std::string fonts[GAME_FONTS]; - std::array glyphData; + class FontMetaData { + public: + FontMetaData() = default; + template + FontMetaData(const std::string &textureName, + const std::array &glyphWidths, + const glm::u32vec2 &textureSize, + const glm::u8vec2 &glyphOffset, + const std::uint8_t monoWidth) + : textureName(textureName) + , glyphWidths(glyphWidths.cbegin(), glyphWidths.cend()) + , textureSize(textureSize) + , glyphOffset(glyphOffset) + , monoWidth(monoWidth) + { + } + std::string textureName; + std::vector glyphWidths; + glm::u32vec2 textureSize; + glm::u8vec2 glyphOffset; + std::uint8_t monoWidth; + }; + + std::array fonts; GameRenderer* renderer; std::unique_ptr textShader; From 09026ae8b00b34b4bf25e29585f7767c644ea931 Mon Sep 17 00:00:00 2001 From: Anonymous Maarten Date: Thu, 2 Aug 2018 18:30:44 +0200 Subject: [PATCH 2/7] rwengine: allow newline in GameString's --- rwengine/src/render/TextRenderer.cpp | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/rwengine/src/render/TextRenderer.cpp b/rwengine/src/render/TextRenderer.cpp index 8e41691f..49a799f4 100644 --- a/rwengine/src/render/TextRenderer.cpp +++ b/rwengine/src/render/TextRenderer.cpp @@ -268,6 +268,15 @@ void TextRenderer::renderText(const TextRenderer::TextInfo& ti, colour = glm::vec3(ti.baseColour) * (1 / 255.f); } + // Handle special chars. + if (c == '\n') { + coord.x = 0.f; + coord.y += ss.y; + maxHeight = coord.y + ss.y; + lineLength = 0; + continue; + } + auto glyph = charToIndex(c); if (glyph >= fontMetaData.glyphWidths.size()) { continue; @@ -291,15 +300,6 @@ void TextRenderer::renderText(const TextRenderer::TextInfo& ti, auto tex = indexToTexCoord(glyph, fontMetaData.textureSize, fontMetaData.glyphOffset); ss.x = ti.size * static_cast(fontMetaData.glyphOffset.x) / fontMetaData.glyphOffset.y; - - // Handle special chars. - if (c == '\n') { - coord.x = 0.f; - coord.y += ss.y; - maxHeight = coord.y + ss.y; - lineLength = 0; - continue; - } lineLength++; glm::vec2 p = coord; From f0c0bd33effc06a6d7059bee4eec2fccccdb50de Mon Sep 17 00:00:00 2001 From: Anonymous Maarten Date: Tue, 7 Aug 2018 12:33:41 +0200 Subject: [PATCH 3/7] rwlib: make '~' visible in rwviewer and utf8 strings --- rwlib/source/fonts/FontMapGta3.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/rwlib/source/fonts/FontMapGta3.cpp b/rwlib/source/fonts/FontMapGta3.cpp index fb7a0c62..6d3f5ebd 100644 --- a/rwlib/source/fonts/FontMapGta3.cpp +++ b/rwlib/source/fonts/FontMapGta3.cpp @@ -76,6 +76,7 @@ static const FontMap::gschar_unicode_map_t map_gta3_font_common = { {0x78, UnicodeValue::UNICODE_SMALL_X}, {0x79, UnicodeValue::UNICODE_SMALL_Y}, {0x7a, UnicodeValue::UNICODE_SMALL_Z}, + {0x7e, UnicodeValue::UNICODE_TILDE}, {0x80, UnicodeValue::UNICODE_CAPITAL_A_GRAVE}, {0x81, UnicodeValue::UNICODE_CAPITAL_A_ACUTE}, {0x82, UnicodeValue::UNICODE_CAPITAL_A_CIRCUMFLEX}, From f85f6e3391b10bc7a697decce569f64327738108 Mon Sep 17 00:00:00 2001 From: Anonymous Maarten Date: Tue, 7 Aug 2018 12:34:29 +0200 Subject: [PATCH 4/7] rwviewer: keep unconvertible characters in the string as they are --- rwviewer/views/TextViewer.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/rwviewer/views/TextViewer.cpp b/rwviewer/views/TextViewer.cpp index 6d9b6ab5..25b505ee 100644 --- a/rwviewer/views/TextViewer.cpp +++ b/rwviewer/views/TextViewer.cpp @@ -156,10 +156,6 @@ void TextViewer::onGameStringChange(const GameString &gameString) { if (hexLineEdit->text().compare(newHexText)) { hexLineEdit->setText(newHexText); } - auto newText = QString::fromStdString(GameStringUtil::toString(gameString, currentFont)); - if (textEdit->toPlainText().compare(newText)) { - textEdit->setText(newText); - } updateRender(); } From 99dca06dbc0438970366c574e83a66bd6f04859a Mon Sep 17 00:00:00 2001 From: Anonymous Maarten Date: Tue, 7 Aug 2018 12:36:46 +0200 Subject: [PATCH 5/7] rwengine: avoid crash of rwviewer/rwgmae when rendering incomplete color string The string "~g" would crash rwviewer. --- rwengine/src/render/TextRenderer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rwengine/src/render/TextRenderer.cpp b/rwengine/src/render/TextRenderer.cpp index 49a799f4..d328e3b1 100644 --- a/rwengine/src/render/TextRenderer.cpp +++ b/rwengine/src/render/TextRenderer.cpp @@ -214,7 +214,7 @@ void TextRenderer::renderText(const TextRenderer::TextInfo& ti, char16_t c = text[i]; // Handle any markup changes. - if (c == '~' && text.length() > i + 1) { + if (c == '~' && text.length() > i + 2) { switch (text[i + 1]) { case 'b': // Blue text.erase(text.begin() + i, text.begin() + i + 3); From 1bde51a15590ff631bcbc5f0108fc7daa76a9f8c Mon Sep 17 00:00:00 2001 From: Anonymous Maarten Date: Fri, 13 Jul 2018 21:33:13 +0200 Subject: [PATCH 6/7] cmake: add optional rwtools directory --- CMakeLists.txt | 3 +++ cmake_options.cmake | 1 + rwtools/CMakeLists.txt | 0 3 files changed, 4 insertions(+) create mode 100644 rwtools/CMakeLists.txt diff --git a/CMakeLists.txt b/CMakeLists.txt index 9eefdfe7..1bf88b3f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -78,6 +78,9 @@ if(BUILD_TESTS) include(CTest) add_subdirectory(tests) endif() +if(BUILD_TOOLS) + add_subdirectory(rwtools) +endif() # Copy the license to the install directory install(FILES COPYING diff --git a/cmake_options.cmake b/cmake_options.cmake index 82d310ae..3d3fae67 100644 --- a/cmake_options.cmake +++ b/cmake_options.cmake @@ -1,5 +1,6 @@ option(RW_VERBOSE_DEBUG_MESSAGES "Print verbose debugging messages" ON) +option(BUILD_TOOLS "Build tools") option(BUILD_TESTS "Build test suite") option(BUILD_VIEWER "Build GUI data viewer") diff --git a/rwtools/CMakeLists.txt b/rwtools/CMakeLists.txt new file mode 100644 index 00000000..e69de29b From 9cfd9bb21bc0776abdaa4343896091f9592cf3e6 Mon Sep 17 00:00:00 2001 From: Anonymous Maarten Date: Fri, 3 Aug 2018 02:20:33 +0200 Subject: [PATCH 7/7] tools/rwfont: Add font texture creator --- conanfile.py | 11 +- rwtools/CMakeLists.txt | 1 + rwtools/rwfont/CMakeLists.txt | 14 ++ rwtools/rwfont/rwfontmap.cpp | 272 ++++++++++++++++++++++++++++++++++ 4 files changed, 297 insertions(+), 1 deletion(-) create mode 100644 rwtools/rwfont/CMakeLists.txt create mode 100644 rwtools/rwfont/rwfontmap.cpp diff --git a/conanfile.py b/conanfile.py index 4b8ed59e..b9dd4b58 100644 --- a/conanfile.py +++ b/conanfile.py @@ -15,11 +15,13 @@ class OpenrwConan(ConanFile): options = { 'test_data': [True, False], 'viewer': [True, False], + 'tools': [True, False], } default_options = ( 'test_data=False', 'viewer=False', + 'tools=False', 'bullet:shared=False', 'ffmpeg:iconv=False', 'libalsa:disable_python=True', # https://github.com/conan-community/community/issues/3 @@ -41,7 +43,10 @@ class OpenrwConan(ConanFile): ), 'viewer': ( 'Qt/5.11@bincrafters/stable', - ) + ), + 'tools': ( + 'freetype/2.9.0@bincrafters/stable', + ), } def requirements(self): @@ -50,6 +55,9 @@ class OpenrwConan(ConanFile): if self.options.viewer: for dep in self._rw_dependencies['viewer']: self.requires(dep) + if self.options.tools: + for dep in self._rw_dependencies['tools']: + self.requires(dep) def _configure_cmake(self): cmake = CMake(self) @@ -58,6 +66,7 @@ class OpenrwConan(ConanFile): 'CMAKE_BUILD_TYPE': self.settings.build_type, 'BUILD_TESTS': True, 'BUILD_VIEWER': self.options.viewer, + 'BUILD_TOOLS': self.options.tools, 'TESTS_NODATA': not self.options.test_data, 'USE_CONAN': True, 'BOOST_STATIC': not self.options['boost'].shared, diff --git a/rwtools/CMakeLists.txt b/rwtools/CMakeLists.txt index e69de29b..af12e113 100644 --- a/rwtools/CMakeLists.txt +++ b/rwtools/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(rwfont) diff --git a/rwtools/rwfont/CMakeLists.txt b/rwtools/rwfont/CMakeLists.txt new file mode 100644 index 00000000..57608113 --- /dev/null +++ b/rwtools/rwfont/CMakeLists.txt @@ -0,0 +1,14 @@ +# fixme: conan support +find_package(Freetype REQUIRED) +find_package(Qt5 REQUIRED COMPONENTS Gui) +add_executable(rwfontmap + rwfontmap.cpp + ) + +target_link_libraries(rwfontmap + PUBLIC + rwlib + Freetype::Freetype + Qt5::Gui + Boost::program_options + ) diff --git a/rwtools/rwfont/rwfontmap.cpp b/rwtools/rwfont/rwfontmap.cpp new file mode 100644 index 00000000..1fdf779f --- /dev/null +++ b/rwtools/rwfont/rwfontmap.cpp @@ -0,0 +1,272 @@ +#include +#include +#include + +#include +#include FT_FREETYPE_H + +#include + +#include +#include + +#include +#include +#include +#include +#include +#include + +const char *ft_error_to_string(int error) { + switch (error) { +#define FT_NOERRORDEF_(_DC, CODE, STR) case CODE: return STR; +#define FT_ERRORDEF_(_DC, CODE, STR) case CODE: return STR; +#include FT_ERROR_DEFINITIONS_H +#undef FT_ERRORDEF_ +#undef FT_NOERRORDEF_ + } + return ""; +} + +[[noreturn]] +void ft_error(int code) +{ + std::cerr << ft_error_to_string(code) << "\n"; + exit(1); +} + +class FontTextureBuffer { +public: + FontTextureBuffer(unsigned fontsize, unsigned width, unsigned height, float height_width_ratio) + : m_texture{static_cast(width), static_cast(height), QImage::Format_ARGB32} { + m_aspectratio = height_width_ratio; + m_fontsize = fontsize; + m_glyph_width = width / 16; + m_glyph_height = height_width_ratio * m_glyph_width; + + int error = FT_Init_FreeType(&m_library); + if (error) ft_error(error); + } + + ~FontTextureBuffer() { + for (auto face : m_faces) { + int error = FT_Done_Face(face); + if (error) ft_error(error); + } + int error = FT_Done_FreeType(m_library); + if (error) ft_error(error); + } + + void clear() { + m_texture.fill(QColor{0, 0, 0, 0}); + m_advances.clear(); + } + + void add_face(const char *path) { + m_faces.emplace_back(); + FT_Face &face = m_faces.back(); + int error = FT_New_Face(m_library, path, 0, &face); + if (error) ft_error(error); + error = FT_Select_Charmap(face, FT_ENCODING_UNICODE); + if (error) ft_error(error); + error = FT_Set_Pixel_Sizes(face, 0, m_fontsize); + if (error) ft_error(error); + } + + void draw_glyph_mono(int index, FT_GlyphSlot glyph) { + int x = index % 16; + int y = index / 16 - 2; + QPoint topleft = {x * static_cast(m_glyph_width), y * static_cast(m_glyph_height)}; + QPoint baselineleft = topleft + QPoint{0, static_cast(m_glyph_width)}; + + if (baselineleft.x() + glyph->metrics.horiBearingX / 64 + glyph->metrics.width / 64 >= topleft.x() + m_glyph_width) { + std::cerr << "index " << index << " crosses right border\n"; + } + if (baselineleft.y() - glyph->metrics.horiBearingY / 64 < topleft.y()) { + std::cerr << "index " << index << " crosses top border\n"; + } + if (baselineleft.y() - glyph->metrics.horiBearingY / 64 + glyph->metrics.height / 64 >= topleft.y() + m_glyph_height) { + std::cerr << "index " << index << " crosses bottom border\n"; + } + + for (unsigned row = 0; row < glyph->bitmap.rows; ++row) { + const unsigned char *buffer = glyph->bitmap.buffer + row * glyph->bitmap.pitch; + for (unsigned i = 0; i < glyph->bitmap.width; ++i) { + bool pixel = (buffer[i/8] << (i % 8)) & 0x80; + QPoint point = baselineleft + QPoint{static_cast(i), static_cast(row - (glyph->metrics.horiBearingY / 64))}; + QColor color{255, 255, 255, pixel ? 255 : 0}; + m_texture.setPixelColor(point, color); + } + } + } + + void draw_glyph_normal(int index, FT_GlyphSlot glyph) { + int x = index % 16; + int y = index / 16 - 2; + QPoint topleft = {x * static_cast(m_glyph_width), y * static_cast(m_glyph_height)}; + QPoint baselineleft = topleft + QPoint{0, 4 * static_cast(m_glyph_height) / 5}; + + if (baselineleft.x() + glyph->metrics.horiBearingX / 64 + glyph->metrics.width / 64 >= topleft.x() + m_glyph_width) { + std::cerr << "index " << index << " crosses right border\n"; + } + if (baselineleft.y() - glyph->metrics.horiBearingY / 64 < topleft.y()) { + std::cerr << "index " << index << " crosses top border\n"; + } + if (baselineleft.y() - glyph->metrics.horiBearingY / 64 + glyph->metrics.height / 64 >= topleft.y() + m_glyph_height) { + std::cerr << "index " << index << " crosses bottom border\n"; + } + + for (unsigned row = 0; row < glyph->bitmap.rows; ++row) { + const unsigned char *buffer = glyph->bitmap.buffer + row * glyph->bitmap.pitch; + for (unsigned i = 0; i < glyph->bitmap.width; ++i) { + QPoint point = baselineleft + QPoint{static_cast(i), static_cast(row - (glyph->metrics.horiBearingY / 64))}; + QColor color{255, 255, 255, buffer[i]}; + m_texture.setPixelColor(point, color); + } + } + } + + enum RenderMode { + MONO, + NORMAL + }; + + void create_font_map(const FontMap &fontmap, RenderMode mode) { + GameStringChar next = 0x20; + FT_Render_Mode render_mode; + switch (mode) { + case RenderMode::MONO: + render_mode = FT_RENDER_MODE_MONO; + break; + default: + case RenderMode::NORMAL: + render_mode = FT_RENDER_MODE_NORMAL; + break; + } + for (auto it = fontmap.to_unicode_begin(); it != fontmap.to_unicode_end(); ++it) { + while (it->first != next) { + m_advances.push_back(0); + ++next; + } + for (auto face : m_faces) { + auto glyph_index = FT_Get_Char_Index(face, it->second); + if (glyph_index == 0) { + continue; + } + int error = FT_Load_Glyph(face, glyph_index, FT_LOAD_DEFAULT); + if (error) ft_error(error); + error = FT_Render_Glyph(face->glyph, render_mode); + if (error) ft_error(error); + + switch (mode) { + case RenderMode::MONO: + draw_glyph_mono(it->first, face->glyph); + break; + case RenderMode::NORMAL: + draw_glyph_normal(it->first, face->glyph); + break; + } + + int advance = face->glyph->metrics.horiAdvance / 64;// - face->glyph->metrics.horiBearingX / 64; + if (advance < 0 || advance > 255) { + std::cerr << "advance out of range\n"; + } + m_advances.push_back(advance); + ++next; + break; + } + if (it->first + 1 != next) { + std::cerr << "unknown character.\n"; exit(1); + + } + } + } + + void write(const rwfs::path &bitmap_path, const rwfs::path &advance_path) { + QImageWriter writer(bitmap_path.c_str()); + writer.write(m_texture); + std::ofstream ofs(advance_path.c_str(), std::ios_base::out); + ofs << m_aspectratio << '\n'; + for (auto adv : m_advances) { + ofs << int(adv) << '\n'; + } +// ofs.write(reinterpret_cast(m_advances.data()), m_advances.size()); + } + +private: + QImage m_texture; + + float m_aspectratio; + unsigned m_fontsize; + unsigned m_glyph_width; + unsigned m_glyph_height; + FT_Library m_library; + + std::vector m_faces; + + std::vector m_advances; +}; + +int main(int argc, const char *argv[]) +{ + namespace po = boost::program_options; + po::options_description desc("Options"); + desc.add_options() + ("help", "Show this help message") + ("fontsize,s", po::value()->value_name("FONTSIZE")->required(), "Fontsize") + ("width,w", po::value()->value_name("WIDTH")->required(), "Width of the texture") + ("height,h", po::value()->value_name("HEIGHT")->required(), "Height of the texture") + ("ratio,r", po::value()->value_name("ASPECTRATIO"), "Aspect ratio") + ("map,m", po::value()->value_name("MAP")->required(), "Font map to use") + ("font,f", po::value>()->value_name("PATH")->required(), "Path to fonts") + ("texture,t", po::value()->value_name("PATH")->required(), "Output texture") + ("advance,a", po::value()->value_name("PATH")->required(), "Output advances") + ; + + po::variables_map vm; + try { + po::store(po::parse_command_line(argc, argv, desc), vm); + if (vm.count("help")) { + std::cout << desc; + return EXIT_SUCCESS; + } + po::notify(vm); + } catch (po::error &ex) { + std::cerr << "Error parsing arguments: " << ex.what() << std::endl; + std::cerr << desc; + return EXIT_FAILURE; + } + + const auto fontmap_index = vm["map"].as(); + const auto width = vm["width"].as(); + const auto height = vm["height"].as(); + const auto fontsize = vm["fontsize"].as(); + + float aspect; + if (vm.count("aspect")) { + aspect = vm["aspect"].as(); + } else { + if (fontmap_index == 0) { + aspect = 1.0f; + } else { + aspect = 1.25f; + } + } + + if (fontmap_index >= fontmaps_gta3_font.size()) { + std::cerr << "Illegal map: range: [0, " << fontmaps_gta3_font.size() << ")\n"; + std::cerr << desc; + return EXIT_FAILURE; + } + + FontTextureBuffer texBuffer{fontsize, width, height, aspect}; + + for (const auto &fontpath : vm["font"].as>()) { + texBuffer.add_face(fontpath.c_str()); + } + + texBuffer.create_font_map(fontmaps_gta3_font[fontmap_index], FontTextureBuffer::RenderMode::NORMAL); + + texBuffer.write(vm["texture"].as(), vm["advance"].as()); + return 0; +}