GeenzAnd another.bf31fc710 years agoHistory
373 lines · 12.9 KB · C++
RawDownload
#include "Font.h" #include <QFile>#include <QImage> #include <ColorUtils.h> #include <StreamHelpers.h> #include "sdf_text3D_vert.h"#include "sdf_text3D_frag.h"#include "sdf_text3D_overlay_frag.h" #include "../RenderUtilsLogging.h"#include "FontFamilies.h" struct TextureVertex {    glm::vec2 pos;    glm::vec2 tex;    TextureVertex() {}    TextureVertex(const glm::vec2& pos, const glm::vec2& tex) : pos(pos), tex(tex) {}}; static const int NUMBER_OF_INDICES_PER_QUAD = 6; // 1 quad = 2 trianglesstatic const int VERTICES_PER_QUAD = 4; // 1 quad = 4 vertices struct QuadBuilder {    TextureVertex vertices[VERTICES_PER_QUAD];     QuadBuilder(const glm::vec2& min, const glm::vec2& size,                const glm::vec2& texMin, const glm::vec2& texSize) {        // min = bottomLeft        vertices[0] = TextureVertex(min,                                    texMin + glm::vec2(0.0f, texSize.y));        vertices[1] = TextureVertex(min + glm::vec2(size.x, 0.0f),                                    texMin + texSize);        vertices[2] = TextureVertex(min + glm::vec2(0.0f, size.y),                                    texMin);        vertices[3] = TextureVertex(min + size,                                    texMin + glm::vec2(texSize.x, 0.0f));    }    QuadBuilder(const Glyph& glyph, const glm::vec2& offset) :    QuadBuilder(offset + glm::vec2(glyph.offset.x, glyph.offset.y - glyph.size.y), glyph.size,                    glyph.texOffset, glyph.texSize) {} };   static QHash<QString, Font::Pointer> LOADED_FONTS; Font::Pointer Font::load(QIODevice& fontFile) {    Pointer font = std::make_shared<Font>();    font->read(fontFile);    return font;} Font::Pointer Font::load(const QString& family) {    if (!LOADED_FONTS.contains(family)) {         static const QString SDFF_COURIER_PRIME_FILENAME{ ":/CourierPrime.sdff" };        static const QString SDFF_INCONSOLATA_MEDIUM_FILENAME{ ":/InconsolataMedium.sdff" };        static const QString SDFF_ROBOTO_FILENAME{ ":/Roboto.sdff" };        static const QString SDFF_TIMELESS_FILENAME{ ":/Timeless.sdff" };         QString loadFilename;         if (family == MONO_FONT_FAMILY) {            loadFilename = SDFF_COURIER_PRIME_FILENAME;        } else if (family == INCONSOLATA_FONT_FAMILY) {            loadFilename = SDFF_INCONSOLATA_MEDIUM_FILENAME;        } else if (family == SANS_FONT_FAMILY) {            loadFilename = SDFF_ROBOTO_FILENAME;        } else {            if (!LOADED_FONTS.contains(SERIF_FONT_FAMILY)) {                loadFilename = SDFF_TIMELESS_FILENAME;            } else {                LOADED_FONTS[family] = LOADED_FONTS[SERIF_FONT_FAMILY];            }        }         if (!loadFilename.isEmpty()) {            QFile fontFile(loadFilename);            fontFile.open(QIODevice::ReadOnly);             qCDebug(renderutils) << "Loaded font" << loadFilename << "from Qt Resource System.";             LOADED_FONTS[family] = load(fontFile);        }    }    return LOADED_FONTS[family];} Font::Font() {    static bool fontResourceInitComplete = false;    if (!fontResourceInitComplete) {        Q_INIT_RESOURCE(fonts);        fontResourceInitComplete = true;    }} // NERD RAGE: why doesn't QHash have a 'const T & operator[] const' memberconst Glyph& Font::getGlyph(const QChar& c) const {    if (!_glyphs.contains(c)) {        return _glyphs[QChar('?')];    }    return _glyphs[c];} QStringList Font::splitLines(const QString& str) const {    return str.split('\n');} QStringList Font::tokenizeForWrapping(const QString& str) const {    QStringList tokens;    for(auto line : splitLines(str)) {        if (!tokens.empty()) {            tokens << QString('\n');        }        tokens << line.split(' ');    }    return tokens;} glm::vec2 Font::computeTokenExtent(const QString& token) const {    glm::vec2 advance(0, _fontSize);    foreach(QChar c, token) {        Q_ASSERT(c != '\n');        advance.x += (c == ' ') ? _spaceWidth : getGlyph(c).d;    }    return advance;} glm::vec2 Font::computeExtent(const QString& str) const {    glm::vec2 extent = glm::vec2(0.0f, 0.0f);     QStringList lines{ splitLines(str) };    if (!lines.empty()) {        for(const auto& line : lines) {            glm::vec2 tokenExtent = computeTokenExtent(line);            extent.x = std::max(tokenExtent.x, extent.x);        }        extent.y = lines.count() * _fontSize;    }    return extent;} void Font::read(QIODevice& in) {    uint8_t header[4];    readStream(in, header);    if (memcmp(header, "SDFF", 4)) {        qFatal("Bad SDFF file");    }     uint16_t version;    readStream(in, version);     // read font name    _family = "";    if (version > 0x0001) {        char c;        readStream(in, c);        while (c) {            _family += c;            readStream(in, c);        }    }     // read font data    readStream(in, _leading);    readStream(in, _ascent);    readStream(in, _descent);    readStream(in, _spaceWidth);    _fontSize = _ascent + _descent;     // Read character count    uint16_t count;    readStream(in, count);    // read metrics data for each character    QVector<Glyph> glyphs(count);    // std::for_each instead of Qt foreach because we need non-const references    std::for_each(glyphs.begin(), glyphs.end(), [&](Glyph& g) {        g.read(in);    });     // read image data    QImage image;    if (!image.loadFromData(in.readAll(), "PNG")) {        qFatal("Failed to read SDFF image");    }     _glyphs.clear();    glm::vec2 imageSize = toGlm(image.size());    foreach(Glyph g, glyphs) {        // Adjust the pixel texture coordinates into UV coordinates,        g.texSize /= imageSize;        g.texOffset /= imageSize;        // store in the character to glyph hash        _glyphs[g.c] = g;    };     image = image.convertToFormat(QImage::Format_RGBA8888);     gpu::Element formatGPU = gpu::Element(gpu::VEC3, gpu::NUINT8, gpu::RGB);    gpu::Element formatMip = gpu::Element(gpu::VEC3, gpu::NUINT8, gpu::RGB);    if (image.hasAlphaChannel()) {        formatGPU = gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA);        formatMip = gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::BGRA);    }    _texture = gpu::TexturePointer(gpu::Texture::create2D(formatGPU, image.width(), image.height(),                                   gpu::Sampler(gpu::Sampler::FILTER_MIN_POINT_MAG_LINEAR)));    _texture->assignStoredMip(0, formatMip, image.byteCount(), image.constBits());    _texture->autoGenerateMips(-1);} void Font::setupGPU() {    if (!_initialized) {        _initialized = true;         // Setup render pipeline        {            auto vertexShader = gpu::Shader::createVertex(std::string(sdf_text3D_vert));            auto pixelShader = gpu::Shader::createPixel(std::string(sdf_text3D_frag));            auto pixelShaderOverlay = gpu::Shader::createPixel(std::string(sdf_text3D_overlay_frag));            gpu::ShaderPointer program = gpu::Shader::createProgram(vertexShader, pixelShader);            gpu::ShaderPointer programOverlay = gpu::Shader::createProgram(vertexShader, pixelShaderOverlay);             gpu::Shader::BindingSet slotBindings;            gpu::Shader::makeProgram(*program, slotBindings);            gpu::Shader::makeProgram(*programOverlay, slotBindings);             _fontLoc = program->getTextures().findLocation("Font");            _outlineLoc = program->getUniforms().findLocation("Outline");            _colorLoc = program->getUniforms().findLocation("Color");             auto state = std::make_shared<gpu::State>();            state->setCullMode(gpu::State::CULL_BACK);            state->setDepthTest(true, true, gpu::LESS_EQUAL);            state->setBlendFunction(true,                gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA,                gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE);            _pipeline = gpu::Pipeline::create(program, state);             auto layeredState = std::make_shared<gpu::State>();            layeredState->setCullMode(gpu::State::CULL_BACK);            layeredState->setDepthTest(true, true, gpu::LESS_EQUAL);            _layeredPipeline = gpu::Pipeline::create(programOverlay, layeredState);        }         // Sanity checks        static const int OFFSET = offsetof(TextureVertex, tex);        assert(OFFSET == sizeof(glm::vec2));        assert(sizeof(glm::vec2) == 2 * sizeof(float));        assert(sizeof(TextureVertex) == 2 * sizeof(glm::vec2));        assert(sizeof(QuadBuilder) == 4 * sizeof(TextureVertex));         // Setup rendering structures        _format = std::make_shared<gpu::Stream::Format>();        _format->setAttribute(gpu::Stream::POSITION, 0, gpu::Element(gpu::VEC2, gpu::FLOAT, gpu::XYZ), 0);        _format->setAttribute(gpu::Stream::TEXCOORD, 0, gpu::Element(gpu::VEC2, gpu::FLOAT, gpu::UV), OFFSET);    }} void Font::rebuildVertices(float x, float y, const QString& str, const glm::vec2& bounds) {    _verticesBuffer = std::make_shared<gpu::Buffer>();    _numVertices = 0;    _indicesBuffer = std::make_shared<gpu::Buffer>();    _numIndices = 0;     _lastStringRendered = str;    _lastBounds = bounds;     // Top left of text    glm::vec2 advance = glm::vec2(x, y);    foreach(const QString& token, tokenizeForWrapping(str)) {        bool isNewLine = (token == QString('\n'));        bool forceNewLine = false;         // Handle wrapping        if (!isNewLine && (bounds.x != -1) && (advance.x + computeExtent(token).x > x + bounds.x)) {            // We are out of the x bound, force new line            forceNewLine = true;        }        if (isNewLine || forceNewLine) {            // Character return, move the advance to a new line            advance = glm::vec2(x, advance.y - _leading);             if (isNewLine) {                // No need to draw anything, go directly to next token                continue;            } else if (computeExtent(token).x > bounds.x) {                // token will never fit, stop drawing                break;            }        }        if ((bounds.y != -1) && (advance.y - _fontSize < -y - bounds.y)) {            // We are out of the y bound, stop drawing            break;        }         // Draw the token        if (!isNewLine) {            for (auto c : token) {                auto glyph = _glyphs[c];                quint16 verticesOffset = _numVertices;                 QuadBuilder qd(glyph, advance - glm::vec2(0.0f, _ascent));                _verticesBuffer->append(sizeof(QuadBuilder), (const gpu::Byte*)&qd);                _numVertices += 4;                                // Sam's recommended triangle slices                // Triangle tri1 = { v0, v1, v3 };                // Triangle tri2 = { v1, v2, v3 };                // NOTE: Random guy on the internet's recommended triangle slices                // Triangle tri1 = { v0, v1, v2 };                // Triangle tri2 = { v2, v3, v0 };                 // The problem here being that the 4 vertices are { ll, lr, ul, ur }, a Z pattern                // Additionally, you want to ensure that the shared side vertices are used sequentially                // to improve cache locality                //                //  2 -- 3                //  |    |                //  |    |                //  0 -- 1                //                //  { 0, 1, 2 } -> { 2, 1, 3 }                quint16 indices[NUMBER_OF_INDICES_PER_QUAD];                indices[0] = verticesOffset + 0;                indices[1] = verticesOffset + 1;                indices[2] = verticesOffset + 2;                indices[3] = verticesOffset + 2;                indices[4] = verticesOffset + 1;                indices[5] = verticesOffset + 3;                _indicesBuffer->append(sizeof(indices), (const gpu::Byte*)indices);                _numIndices += NUMBER_OF_INDICES_PER_QUAD;                                 // Advance by glyph size                advance.x += glyph.d;            }             // Add space after all non return tokens            advance.x += _spaceWidth;        }    }} void Font::drawString(gpu::Batch& batch, float x, float y, const QString& str, const glm::vec4* color,                      EffectType effectType, const glm::vec2& bounds, bool layered) {    if (str == "") {        return;    }     if (str != _lastStringRendered || bounds != _lastBounds) {        rebuildVertices(x, y, str, bounds);    }     setupGPU();     batch.setPipeline(layered ? _layeredPipeline : _pipeline);    batch.setResourceTexture(_fontLoc, _texture);    batch._glUniform1i(_outlineLoc, (effectType == OUTLINE_EFFECT));        // need the gamma corrected color here    glm::vec4 lrgba = ColorUtils::sRGBToLinearVec4(*color);    batch._glUniform4fv(_colorLoc, 1, (const float*)&lrgba);     batch.setInputFormat(_format);    batch.setInputBuffer(0, _verticesBuffer, 0, _format->getChannels().at(0)._stride);    batch.setIndexBuffer(gpu::UINT16, _indicesBuffer, 0);    batch.drawIndexed(gpu::TRIANGLES, _numIndices, 0);}