diff --git a/specter/.gitmodules b/specter/.gitmodules index 700d91aa6..3cddf9b87 100644 --- a/specter/.gitmodules +++ b/specter/.gitmodules @@ -1,3 +1,6 @@ [submodule "freetype2"] path = freetype2 url = https://github.com/AxioDL/freetype2 +[submodule "MathLib"] + path = MathLib + url = https://github.com/AxioDL/MathLib.git diff --git a/specter/CMakeLists.txt b/specter/CMakeLists.txt index daa565bc3..303c6e070 100644 --- a/specter/CMakeLists.txt +++ b/specter/CMakeLists.txt @@ -1,4 +1,6 @@ add_subdirectory(freetype2) +add_subdirectory(MathLib) +set(MATHLIB_INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/MathLib/include) if(NOT DEFINED HECL_INCLUDE_DIR) message(FATAL_ERROR "Specter may only be built as a sub-project containing hecl with @@ -24,7 +26,8 @@ add_subdirectory(resources/fonts) include_directories(include ${HECL_INCLUDE_DIR} ${BOO_INCLUDE_DIR} ${LOG_VISOR_INCLUDE_DIR} ${ATHENA_INCLUDE_DIR} - ${CMAKE_CURRENT_SOURCE_DIR}/freetype2/include) + ${CMAKE_CURRENT_SOURCE_DIR}/freetype2/include + ${MATHLIB_INCLUDE_DIR}) list(APPEND SPECTER_HEADERS include/Specter/Specter.hpp diff --git a/specter/MathLib b/specter/MathLib new file mode 160000 index 000000000..8d54a3c7b --- /dev/null +++ b/specter/MathLib @@ -0,0 +1 @@ +Subproject commit 8d54a3c7b5d76e32d9fb3ad57619a680b32909b7 diff --git a/specter/include/Specter/FontCache.hpp b/specter/include/Specter/FontCache.hpp index 883a6d0e2..4ac7fd5bb 100644 --- a/specter/include/Specter/FontCache.hpp +++ b/specter/include/Specter/FontCache.hpp @@ -42,6 +42,8 @@ class FreeTypeGZipMemFace FT_Face m_face = nullptr; public: FreeTypeGZipMemFace(FT_Library lib, const uint8_t* data, size_t sz); + FreeTypeGZipMemFace(const FreeTypeGZipMemFace& other) = delete; + FreeTypeGZipMemFace& operator=(const FreeTypeGZipMemFace& other) = delete; ~FreeTypeGZipMemFace() {close();} void open(); void close(); @@ -53,7 +55,9 @@ class FontAtlas friend class FontCache; FT_Face m_face; boo::ITextureS* m_tex; + uint32_t m_dpi; +public: struct Glyph { atUint32 m_unicodePoint; @@ -65,14 +69,30 @@ class FontAtlas atUint8 m_width; atUint8 m_height; atInt8 m_verticalOffset; - atUint16 m_kernIdx; + atInt16 m_kernIdx = -1; }; + + struct KernAdj + { + atUint32 a; + atUint32 b; + atInt8 adj; + }; + +private: std::vector m_glyphs; - std::map m_glyphLookup; + std::vector m_kernAdjs; + std::unordered_map m_glyphLookup; public: - FontAtlas(boo::IGraphicsDataFactory* gf, FT_Face face, bool subpixel, Athena::io::FileWriter& writer); - FontAtlas(boo::IGraphicsDataFactory* gf, FT_Face face, bool subpixel, Athena::io::FileReader& reader); + FontAtlas(boo::IGraphicsDataFactory* gf, FT_Face face, uint32_t dpi, + bool subpixel, Athena::io::FileWriter& writer); + FontAtlas(boo::IGraphicsDataFactory* gf, FT_Face face, uint32_t dpi, + bool subpixel, Athena::io::FileReader& reader); + FontAtlas(const FontAtlas& other) = delete; + FontAtlas& operator=(const FontAtlas& other) = delete; + + uint32_t dpi() const {return m_dpi;} }; class FontCache @@ -92,6 +112,8 @@ class FontCache std::unordered_map> m_cachedAtlases; public: FontCache(const HECL::Runtime::FileStoreManager& fileMgr); + FontCache(const FontCache& other) = delete; + FontCache& operator=(const FontCache& other) = delete; FontTag prepCustomFont(boo::IGraphicsDataFactory* gf, const std::string& name, FT_Face face, bool subpixel=false, @@ -106,6 +128,8 @@ public: {return prepCustomFont(gf, "bmonofont", m_monoFace, subpixel, points, dpi);} void closeBuiltinFonts() {m_regFace.close(); m_monoFace.close();} + + const FontAtlas& lookupAtlas(FontTag tag) const; }; } diff --git a/specter/include/Specter/TextView.hpp b/specter/include/Specter/TextView.hpp index c1dd2d35a..9398dccf2 100644 --- a/specter/include/Specter/TextView.hpp +++ b/specter/include/Specter/TextView.hpp @@ -2,14 +2,68 @@ #define SPECTER_TEXTVIEW_HPP #include "View.hpp" +#include +#include +#include + +#include "FontCache.hpp" namespace Specter { class TextView : public View { + boo::IGraphicsBufferD* m_glyphBuf; + int m_validDynamicSlots = 0; + size_t m_curGlyphCapacity; + const FontAtlas& m_fontAtlas; + boo::IVertexFormat* m_vtxFmt = nullptr; /* OpenGL only */ +public: + class System + { + friend class TextView; + boo::IGraphicsDataFactory* m_factory; + FontCache& m_fcache; + boo::IShaderPipeline* m_regular; + boo::IShaderPipeline* m_subpixel; + boo::IGraphicsBufferS* m_quadVBO; + boo::IVertexFormat* m_vtxFmt = nullptr; /* Not OpenGL */ + System(boo::IGraphicsDataFactory* factory, FontCache& fcache) + : m_factory(factory), m_fcache(fcache) {} + }; + static System BuildTextSystem(boo::GLDataFactory* factory, FontCache& fcache); +#if _WIN32 + static Shaders BuildTextSystem(boo::ID3DDataFactory* factory, FontCache& fcache); +#elif BOO_HAS_METAL + static Shaders BuildTextSystem(boo::MetalDataFactory* factory, FontCache& fcache); +#endif + + TextView(System& system, FontTag font, size_t initGlyphCapacity=256); + + struct RenderGlyph + { + Zeus::CMatrix4f m_mvp; + Zeus::CVector3f m_uv[4]; + Zeus::CColor m_color; + }; + std::vector& accessGlyphs() {return m_glyphs;} + void updateGlyphs() {m_validDynamicSlots = 0;} + + void typesetGlyphs(const std::string& str, + Zeus::CColor defaultColor=Zeus::CColor::skWhite); + void typesetGlyphs(const std::wstring& str, + Zeus::CColor defaultColor=Zeus::CColor::skWhite); + + void colorGlyphs(Zeus::CColor newColor); + void colorGlyphsTypeOn(Zeus::CColor newColor, float startInterval=0.2, float fadeTime=0.5); + void think(); + + void draw(boo::IGraphicsCommandQueue* gfxQ); + +private: + std::vector m_glyphs; }; } diff --git a/specter/include/Specter/View.hpp b/specter/include/Specter/View.hpp index 9fb5a7579..800d9cc70 100644 --- a/specter/include/Specter/View.hpp +++ b/specter/include/Specter/View.hpp @@ -2,18 +2,20 @@ #define SPECTER_VIEW_HPP #include +#include "CVector3f.hpp" +#include "CMatrix4f.hpp" +#include "CTransform.hpp" +#include "CColor.hpp" namespace Specter { class View { - boo::SWindowRect m_rect; +protected: + boo::SWindowRect m_viewport; + void bindViewport(boo::IGraphicsCommandQueue* gfxQ) {gfxQ->setViewport(m_viewport);} public: - void bindViewport(boo::IGraphicsCommandQueue* gfxQ) - { - gfxQ->setViewport(m_rect); - } virtual void draw(boo::IGraphicsCommandQueue* gfxQ)=0; }; diff --git a/specter/lib/FontCache.cpp b/specter/lib/FontCache.cpp index c370b74f4..2dbfb0b88 100644 --- a/specter/lib/FontCache.cpp +++ b/specter/lib/FontCache.cpp @@ -121,8 +121,9 @@ static inline void GridFitGlyph(FT_GlyphSlot slot, FT_UInt& width, FT_UInt& heig height = slot->metrics.height >> 6; } -FontAtlas::FontAtlas(boo::IGraphicsDataFactory* gf, FT_Face face, +FontAtlas::FontAtlas(boo::IGraphicsDataFactory* gf, FT_Face face, uint32_t dpi, bool subpixel, Athena::io::FileWriter& writer) +: m_dpi(dpi) { FT_Int32 baseFlags = FT_LOAD_NO_BITMAP; if (subpixel) @@ -163,6 +164,7 @@ FontAtlas::FontAtlas(boo::IGraphicsDataFactory* gf, FT_Face face, charcode = FT_Get_Next_Char(face, charcode, &gindex); } m_glyphs.reserve(glyphCount); + m_glyphLookup.reserve(glyphCount); totalHeight = RoundUpPow2(totalHeight); unsigned finalHeight = fullTexmapLayers ? TEXMAP_DIM : totalHeight; @@ -200,6 +202,7 @@ FontAtlas::FontAtlas(boo::IGraphicsDataFactory* gf, FT_Face face, while (gindex != 0) { FT_Load_Glyph(face, gindex, FT_LOAD_RENDER | baseFlags); + m_glyphLookup[charcode] = m_glyphs.size(); m_glyphs.emplace_back(); Glyph& g = m_glyphs.back(); g.m_unicodePoint = charcode; @@ -270,6 +273,7 @@ FontAtlas::FontAtlas(boo::IGraphicsDataFactory* gf, FT_Face face, while (gindex != 0) { FT_Load_Glyph(face, gindex, FT_LOAD_RENDER | baseFlags); + m_glyphLookup[charcode] = m_glyphs.size(); m_glyphs.emplace_back(); Glyph& g = m_glyphs.back(); g.m_unicodePoint = charcode; @@ -312,8 +316,174 @@ FontAtlas::FontAtlas(boo::IGraphicsDataFactory* gf, FT_Face face, } } -FontAtlas::FontAtlas(boo::IGraphicsDataFactory* gf, FT_Face face, bool subpixel, Athena::io::FileReader& reader) +FontAtlas::FontAtlas(boo::IGraphicsDataFactory* gf, FT_Face face, uint32_t dpi, + bool subpixel, Athena::io::FileReader& reader) +: m_dpi(dpi) { + FT_Int32 baseFlags = FT_LOAD_NO_BITMAP; + if (subpixel) + baseFlags |= FT_LOAD_TARGET_LCD; + else + baseFlags |= FT_LOAD_TARGET_NORMAL; + + /* First count glyphs exposed by unicode charmap */ + size_t glyphCount = 0; + FT_UInt gindex; + FT_ULong charcode = FT_Get_First_Char(face, &gindex); + while (gindex != 0) + { + ++glyphCount; + charcode = FT_Get_Next_Char(face, charcode, &gindex); + } + m_glyphs.reserve(glyphCount); + m_glyphLookup.reserve(glyphCount); + + unsigned fullTexmapLayers = reader.readUint32Big() - 1; + reader.readUint32Big(); + unsigned finalHeight = reader.readUint32Big(); + + if (subpixel) + { + /* Allocate texmap */ + std::unique_ptr texmap; + size_t bufSz; + if (fullTexmapLayers) + { + //printf("ALLOC: %u\n", fullTexmapLayers + 1); + size_t count = TEXMAP_DIM * TEXMAP_DIM * (fullTexmapLayers + 1); + texmap.reset(new RgbaPixel[count]); + bufSz = count * sizeof(RgbaPixel); + memset(texmap.get(), 0, bufSz); + } + else + { + size_t count = TEXMAP_DIM * finalHeight; + texmap.reset(new RgbaPixel[TEXMAP_DIM * finalHeight]); + bufSz = count * sizeof(RgbaPixel); + memset(texmap.get(), 0, bufSz); + } + + /* Assemble glyph texmaps and internal data structures */ + charcode = FT_Get_First_Char(face, &gindex); + unsigned curLineWidth = 1; + unsigned curLineHeight = 0; + unsigned totalHeight = 1; + fullTexmapLayers = 0; + while (gindex != 0) + { + FT_Load_Glyph(face, gindex, baseFlags); + FT_UInt width, height; + GridFitGlyph(face->glyph, width, height); + m_glyphs.emplace_back(); + Glyph& g = m_glyphs.back(); + g.m_unicodePoint = charcode; + g.m_layerIdx = fullTexmapLayers; + g.m_width = width; + g.m_height = height; + g.m_uv[0] = curLineWidth / float(TEXMAP_DIM); + g.m_uv[1] = totalHeight / float(finalHeight); + g.m_uv[2] = g.m_uv[0] + g.m_width / float(TEXMAP_DIM); + g.m_uv[3] = g.m_uv[1] + g.m_height / float(finalHeight); + g.m_leftPadding = 0; + g.m_advance = face->glyph->advance.x; + g.m_rightPadding = 0; + g.m_verticalOffset = face->glyph->metrics.horiBearingY / 64; + g.m_kernIdx = 0; + if (curLineWidth + g.m_width + 1 > TEXMAP_DIM) + { + totalHeight += curLineHeight + 1; + curLineHeight = 0; + curLineWidth = 1; + } + curLineHeight = std::max(curLineHeight, height); + if (totalHeight + curLineHeight + 1 > TEXMAP_DIM) + { + totalHeight = 1; + ++fullTexmapLayers; + //printf("RealB: %u\n", gindex); + curLineHeight = 0; + curLineWidth = 1; + } + curLineWidth += g.m_width + 1; + charcode = FT_Get_Next_Char(face, charcode, &gindex); + } + + reader.readUBytesToBuf((atUint8*)texmap.get(), bufSz); + m_tex = + gf->newStaticArrayTexture(TEXMAP_DIM, finalHeight, fullTexmapLayers + 1, + boo::TextureFormat::RGBA8, texmap.get(), bufSz); + } + else + { + /* Allocate texmap */ + std::unique_ptr texmap; + size_t bufSz; + if (fullTexmapLayers) + { + //printf("ALLOC: %u\n", fullTexmapLayers + 1); + size_t count = TEXMAP_DIM * TEXMAP_DIM * (fullTexmapLayers + 1); + texmap.reset(new GreyPixel[count]); + bufSz = count * sizeof(GreyPixel); + memset(texmap.get(), 0, bufSz); + } + else + { + size_t count = TEXMAP_DIM * totalHeight; + texmap.reset(new GreyPixel[TEXMAP_DIM * totalHeight]); + bufSz = count * sizeof(GreyPixel); + memset(texmap.get(), 0, bufSz); + } + + /* Assemble glyph texmaps and internal data structures */ + charcode = FT_Get_First_Char(face, &gindex); + unsigned curLineWidth = 1; + unsigned curLineHeight = 0; + unsigned totalHeight = 1; + fullTexmapLayers = 0; + while (gindex != 0) + { + FT_Load_Glyph(face, gindex, baseFlags); + FT_UInt width, height; + GridFitGlyph(face->glyph, width, height); + m_glyphs.emplace_back(); + Glyph& g = m_glyphs.back(); + g.m_unicodePoint = charcode; + g.m_layerIdx = fullTexmapLayers; + g.m_width = width; + g.m_height = height; + g.m_uv[0] = curLineWidth / float(TEXMAP_DIM); + g.m_uv[1] = totalHeight / float(finalHeight); + g.m_uv[2] = g.m_uv[0] + g.m_width / float(TEXMAP_DIM); + g.m_uv[3] = g.m_uv[1] + g.m_height / float(finalHeight); + g.m_leftPadding = 0; + g.m_advance = face->glyph->advance.x; + g.m_rightPadding = 0; + g.m_verticalOffset = face->glyph->metrics.horiBearingY >> 6; + g.m_kernIdx = 0; + if (curLineWidth + g.m_width + 1 > TEXMAP_DIM) + { + totalHeight += curLineHeight + 1; + curLineHeight = 0; + curLineWidth = 1; + } + curLineHeight = std::max(curLineHeight, height); + if (totalHeight + curLineHeight + 1 > TEXMAP_DIM) + { + totalHeight = 1; + ++fullTexmapLayers; + //printf("RealB: %u\n", gindex); + curLineHeight = 0; + curLineWidth = 1; + } + curLineWidth += g.m_width + 1; + charcode = FT_Get_Next_Char(face, charcode, &gindex); + } + + reader.readUBytesToBuf((atUint8*)texmap.get(), bufSz); + m_tex = + gf->newStaticArrayTexture(TEXMAP_DIM, finalHeight, fullTexmapLayers + 1, + boo::TextureFormat::I8, texmap.get(), bufSz); + } } FontCache::Library::Library() @@ -367,7 +537,7 @@ FontTag FontCache::prepCustomFont(boo::IGraphicsDataFactory* gf, atUint32 magic = r.readUint32Big(); if (r.position() == 4 && magic == 'FONT') { - m_cachedAtlases.emplace(tag, std::make_unique(gf, face, subpixel, r)); + m_cachedAtlases.emplace(tag, std::make_unique(gf, face, dpi, subpixel, r)); return tag; } } @@ -379,8 +549,16 @@ FontTag FontCache::prepCustomFont(boo::IGraphicsDataFactory* gf, if (w.hasError()) Log.report(LogVisor::FatalError, "unable to open '%s' for writing", cachePath.c_str()); w.writeUint32Big('FONT'); - m_cachedAtlases.emplace(tag, std::make_unique(gf, face, subpixel, w)); + m_cachedAtlases.emplace(tag, std::make_unique(gf, face, dpi, subpixel, w)); return tag; } +const FontAtlas& FontCache::lookupAtlas(FontTag tag) const +{ + auto search = m_cachedAtlases.find(tag); + if (search == m_cachedAtlases.cend()) + Log.report(LogVisor::FatalError, "invalid font"); + return *search->second.get(); +} + } diff --git a/specter/lib/TextView.cpp b/specter/lib/TextView.cpp index 50ccdd956..a547dac9a 100644 --- a/specter/lib/TextView.cpp +++ b/specter/lib/TextView.cpp @@ -1,6 +1,164 @@ #include "Specter/TextView.hpp" +#include "utf8proc.h" namespace Specter { +static LogVisor::LogModule Log("Specter::TextView"); + +TextView::System TextView::BuildTextSystem(boo::GLDataFactory* factory, FontCache& fcache) +{ + static const char* VS = + "#version 330\n" + "layout(location=0) in vec3 posIn;\n" + "layout(location=1) in mat4 mvMtx;\n" + "layout(location=5) in vec3 uvIn[4];\n" + "layout(location=9) in vec4 colorIn;\n" + "struct VertToFrag\n" + "{\n" + " vec3 uv;\n" + " vec4 color;\n" + "};\n" + "out VertToFrag vtf;\n" + "void main()\n" + "{\n" + " vtf.uv = uvIn[gl_VertexID];\n" + " vtf.color = colorIn;\n" + " gl_Position = mvMtx * vec4(posIn, 1.0);\n" + "}\n"; + + static const char* FSReg = + "#version 330\n" + "uniform sampler2DArray fontTex;\n" + "struct VertToFrag\n" + "{\n" + " vec3 uv;\n" + " vec4 color;\n" + "};\n" + "in VertToFrag vtf;\n" + "layout(location=0) out vec4 colorOut;\n" + "void main()\n" + "{\n" + " colorOut = vtf.color;\n" + " colorOut.a = texture(fontTex, vtf.uv).r;\n" + "}\n"; + + static const char* FSSubpixel = + "#version 330\n" + "uniform sampler2DArray fontTex;\n" + "struct VertToFrag\n" + "{\n" + " vec3 uv;\n" + " vec4 color;\n" + "};\n" + "in VertToFrag vtf;\n" + "layout(location=0, index=0) out vec4 colorOut;\n" + "layout(location=0, index=1) out vec4 blendOut;\n" + "void main()\n" + "{\n" + " colorOut = vtf.color;\n" + " blendOut = texture(fontTex, vtf.uv);\n" + "}\n"; + + static float Quad[] = + { + 0.0, 0.0, 0.0, + 0.0, 1.0, 0.0, + 1.0, 1.0, 0.0, + 1.0, 0.0, 0.0, + }; + + System ret(factory, fcache); + + ret.m_regular = + factory->newShaderPipeline(VS, FSReg, 1, "fontTex", 0, nullptr, + boo::BlendFactor::SrcAlpha, boo::BlendFactor::InvSrcAlpha, + true, true, false); + + ret.m_subpixel = + factory->newShaderPipeline(VS, FSSubpixel, 1, "fontTex", 0, nullptr, + boo::BlendFactor::SrcColor1, boo::BlendFactor::InvSrcColor1, + true, true, false); + + ret.m_quadVBO = + factory->newStaticBuffer(boo::BufferUse::Vertex, Quad, sizeof(Quad), 1); + + return ret; +} + +#if _WIN32 +TextView::Shaders TextView::BuildTextSystem(boo::ID3DDataFactory* factory, FontCache& fcache) +{ +} + +#elif BOO_HAS_METAL +TextView::Shaders TextView::BuildTextSystem(boo::MetalDataFactory* factory, FontCache& fcache) +{ +} +#endif + +TextView::TextView(System& system, FontTag font, size_t initGlyphCapacity) +: m_curGlyphCapacity(initGlyphCapacity), + m_fontAtlas(system.m_fcache.lookupAtlas(font)) +{ + m_glyphBuf = + system.m_factory->newDynamicBuffer(boo::BufferUse::Vertex, + sizeof(RenderGlyph), initGlyphCapacity); + + if (!system.m_vtxFmt) + { + boo::VertexElementDescriptor vdescs[] = + { + {system.m_quadVBO, nullptr, boo::VertexSemantic::Position}, + {m_glyphBuf, nullptr, boo::VertexSemantic::ModelView | boo::VertexSemantic::Instanced, 0}, + {m_glyphBuf, nullptr, boo::VertexSemantic::ModelView | boo::VertexSemantic::Instanced, 1}, + {m_glyphBuf, nullptr, boo::VertexSemantic::ModelView | boo::VertexSemantic::Instanced, 2}, + {m_glyphBuf, nullptr, boo::VertexSemantic::ModelView | boo::VertexSemantic::Instanced, 3}, + {m_glyphBuf, nullptr, boo::VertexSemantic::UV4 | boo::VertexSemantic::Instanced, 0}, + {m_glyphBuf, nullptr, boo::VertexSemantic::UV4 | boo::VertexSemantic::Instanced, 1}, + {m_glyphBuf, nullptr, boo::VertexSemantic::UV4 | boo::VertexSemantic::Instanced, 2}, + {m_glyphBuf, nullptr, boo::VertexSemantic::UV4 | boo::VertexSemantic::Instanced, 3}, + {m_glyphBuf, nullptr, boo::VertexSemantic::Color | boo::VertexSemantic::Instanced} + }; + m_vtxFmt = system.m_factory->newVertexFormat(10, vdescs); + } + + m_glyphs.reserve(initGlyphCapacity); +} + +void TextView::typesetGlyphs(const std::string& str, Zeus::CColor defaultColor) +{ + size_t rem = str.size(); + const utf8proc_uint8_t* it = str.data(); + while (rem) + { + utf8proc_int32_t ch; + utf8proc_ssize_t sz = utf8proc_iterate(it, -1, &ch); + if (sz < 0) + Log.report(LogVisor::FatalError, "invalid UTF-8 char"); + + m_fontAtlas + + rem -= sz; + it += sz; + } +} +void TextView::typesetGlyphs(const std::wstring& str, Zeus::CColor defaultColor) +{ +} + +void TextView::colorGlyphs(Zeus::CColor newColor) +{ +} +void TextView::colorGlyphsTypeOn(Zeus::CColor newColor, float startInterval, float fadeTime) +{ +} +void TextView::think() +{ +} + +void TextView::draw(boo::IGraphicsCommandQueue* gfxQ) +{ +} + }