483 lines · 15.5 KB · C++
RawDownload
////  TextRenderer3D.cpp//  interface/src/ui////  Created by Andrzej Kapolka on 4/24/13.//  Copyright 2013 High Fidelity, Inc.////  Distributed under the Apache License, Version 2.0.//  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html// #include "TextRenderer3D.h" #include <gpu/GPUConfig.h>#include <gpu/GLBackend.h>#include <gpu/Shader.h> #include <QBuffer>#include <QImage>#include <QStringList>#include <QFile> #include "GLMHelpers.h"#include "MatrixStack.h"#include "RenderUtilsLogging.h" #include "sdf_text3D_vert.h"#include "sdf_text3D_frag.h" #include "GeometryCache.h"#include "DeferredLightingEffect.h" // Helper functions for reading binary data from an IO devicetemplate<class T>void readStream(QIODevice& in, T& t) {    in.read((char*) &t, sizeof(t));} template<typename T, size_t N>void readStream(QIODevice& in, T (&t)[N]) {    in.read((char*) t, N);} template<class T, size_t N>void fillBuffer(QBuffer& buffer, T (&t)[N]) {    buffer.setData((const char*) t, N);} // stores the font metrics for a single characterstruct Glyph3D {    QChar c;    glm::vec2 texOffset;    glm::vec2 texSize;    glm::vec2 size;    glm::vec2 offset;    float d;  // xadvance - adjusts character positioning    size_t indexOffset;     // We adjust bounds because offset is the bottom left corner of the font but the top left corner of a QRect    QRectF bounds() const { return glmToRect(offset, size).translated(0.0f, -size.y); }    QRectF textureBounds() const { return glmToRect(texOffset, texSize); }     void read(QIODevice& in);}; void Glyph3D::read(QIODevice& in) {    uint16_t charcode;    readStream(in, charcode);    c = charcode;    readStream(in, texOffset);    readStream(in, size);    readStream(in, offset);    readStream(in, d);    texSize = size;} struct TextureVertex {    glm::vec2 pos;    glm::vec2 tex;    TextureVertex() {}    TextureVertex(const glm::vec2& pos, const glm::vec2& tex) : pos(pos), tex(tex) {}}; struct QuadBuilder {    TextureVertex vertices[4];    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 + size,                                    texMin + glm::vec2(texSize.x, 0.0f));        vertices[3] = TextureVertex(min + glm::vec2(0.0f, size.y),                                    texMin);    }    QuadBuilder(const Glyph3D& glyph, const glm::vec2& offset) :    QuadBuilder(offset + glm::vec2(glyph.offset.x, glyph.offset.y - glyph.size.y), glyph.size,                    glyph.texOffset, glyph.texSize) {}    }; class Font3D {public:    Font3D();        void read(QIODevice& path);     glm::vec2 computeExtent(const QString& str) const;    float getFontSize() const { return _fontSize; }        // Render string to batch    void drawString(gpu::Batch& batch, float x, float y, const QString& str,            const glm::vec4* color, TextRenderer3D::EffectType effectType,            const glm::vec2& bound); private:    QStringList tokenizeForWrapping(const QString& str) const;    QStringList splitLines(const QString& str) const;    glm::vec2 computeTokenExtent(const QString& str) const;        const Glyph3D& getGlyph(const QChar& c) const;        void setupGPU();        // maps characters to cached glyph info    // HACK... the operator[] const for QHash returns a    // copy of the value, not a const value reference, so    // we declare the hash as mutable in order to avoid such    // copies    mutable QHash<QChar, Glyph3D> _glyphs;        // Font characteristics    QString _family;    float _fontSize = 0.0f;    float _leading = 0.0f;    float _ascent = 0.0f;    float _descent = 0.0f;    float _spaceWidth = 0.0f;        bool _initialized = false;        // gpu structures    gpu::PipelinePointer _pipeline;    gpu::TexturePointer _texture;    gpu::Stream::FormatPointer _format;    gpu::BufferPointer _verticesBuffer;    gpu::BufferStreamPointer _stream;    unsigned int _numVertices = 0;        int _fontLoc = -1;    int _outlineLoc = -1;    int _colorLoc = -1;        // last string render characteristics    QString _lastStringRendered;    glm::vec2 _lastBounds;}; static QHash<QString, Font3D*> LOADED_FONTS; Font3D* loadFont3D(QIODevice& fontFile) {    Font3D* result = new Font3D();    result->read(fontFile);    return result;} Font3D* loadFont3D(const QString& family) {    if (!LOADED_FONTS.contains(family)) {                const QString SDFF_COURIER_PRIME_FILENAME = ":/CourierPrime.sdff";        const QString SDFF_INCONSOLATA_MEDIUM_FILENAME = ":/InconsolataMedium.sdff";        const QString SDFF_ROBOTO_FILENAME = ":/Roboto.sdff";        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] = loadFont3D(fontFile);        }    }    return LOADED_FONTS[family];} Font3D::Font3D() {    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 Glyph3D& Font3D::getGlyph(const QChar& c) const {    if (!_glyphs.contains(c)) {        return _glyphs[QChar('?')];    }    return _glyphs[c];} QStringList Font3D::splitLines(const QString& str) const {    return str.split('\n');} QStringList Font3D::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 Font3D::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 Font3D::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 Font3D::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<Glyph3D> glyphs(count);    // std::for_each instead of Qt foreach because we need non-const references    std::for_each(glyphs.begin(), glyphs.end(), [&](Glyph3D& 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(Glyph3D 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::UINT8, gpu::RGB);    gpu::Element formatMip = gpu::Element(gpu::VEC3, gpu::UINT8, gpu::RGB);    if (image.hasAlphaChannel()) {        formatGPU = gpu::Element(gpu::VEC4, gpu::UINT8, gpu::RGBA);        formatMip = gpu::Element(gpu::VEC4, gpu::UINT8, 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 Font3D::setupGPU() {    if (!_initialized) {        _initialized = true;                // Setup render pipeline        auto vertexShader = gpu::ShaderPointer(gpu::Shader::createVertex(std::string(sdf_text3D_vert)));        auto pixelShader = gpu::ShaderPointer(gpu::Shader::createPixel(std::string(sdf_text3D_frag)));        gpu::ShaderPointer program = gpu::ShaderPointer(gpu::Shader::createProgram(vertexShader, pixelShader));                gpu::Shader::BindingSet slotBindings;        gpu::Shader::makeProgram(*program, slotBindings);                _fontLoc = program->getTextures().findLocation("Font");        _outlineLoc = program->getUniforms().findLocation("Outline");        _colorLoc = program->getUniforms().findLocation("Color");                gpu::StatePointer state = gpu::StatePointer(new gpu::State());        state->setCullMode(gpu::State::CULL_BACK);        state->setDepthTest(true, true, gpu::LESS_EQUAL);        state->setBlendFunction(false,                                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::PipelinePointer(gpu::Pipeline::create(program, state));                // 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.reset(new 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 Font3D::drawString(gpu::Batch& batch, float x, float y, const QString& str, const glm::vec4* color,                      TextRenderer3D::EffectType effectType, const glm::vec2& bounds) {    if (str == "") {        return;    }        if (str != _lastStringRendered || bounds != _lastBounds) {        _verticesBuffer.reset(new gpu::Buffer());        _numVertices = 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];                                        QuadBuilder qd(glyph, advance - glm::vec2(0.0f, _ascent));                    _verticesBuffer->append(sizeof(QuadBuilder), (const gpu::Byte*)&qd);                    _numVertices += 4;                                        // Advance by glyph size                    advance.x += glyph.d;                }                                // Add space after all non return tokens                advance.x += _spaceWidth;            }        }    }        setupGPU();    batch.setPipeline(_pipeline);    batch.setUniformTexture(_fontLoc, _texture);    batch._glUniform1i(_outlineLoc, (effectType == TextRenderer3D::OUTLINE_EFFECT));    batch._glUniform4fv(_colorLoc, 1, (const GLfloat*)color);        batch.setInputFormat(_format);    batch.setInputBuffer(0, _verticesBuffer, 0, _format->getChannels().at(0)._stride);    batch.draw(gpu::QUADS, _numVertices, 0);} TextRenderer3D* TextRenderer3D::getInstance(const char* family,        int weight, bool italic, EffectType effect, int effectThickness) {    return new TextRenderer3D(family, weight, italic, effect, effectThickness);} TextRenderer3D::TextRenderer3D(const char* family, int weight, bool italic,                           EffectType effect, int effectThickness) :    _effectType(effect),    _effectThickness(effectThickness),    _font(loadFont3D(family)) {    if (!_font) {        qWarning() << "Unable to load font with family " << family;        _font = loadFont3D("Courier");    }    if (1 != _effectThickness) {        qWarning() << "Effect thickness not currently supported";    }    if (NO_EFFECT != _effectType && OUTLINE_EFFECT != _effectType) {        qWarning() << "Effect type not currently supported";    }} TextRenderer3D::~TextRenderer3D() {} glm::vec2 TextRenderer3D::computeExtent(const QString& str) const {    if (_font) {        return _font->computeExtent(str);    }    return glm::vec2(0.0f, 0.0f);} float TextRenderer3D::getFontSize() const {    if (_font) {        return _font->getFontSize();    }    return 0.0f;} void TextRenderer3D::draw(gpu::Batch& batch, float x, float y, const QString& str, const glm::vec4& color,                         const glm::vec2& bounds) {    // The font does all the OpenGL work    if (_font) {        // Cache color so that the pointer stays valid.        _color = color;        _font->drawString(batch, x, y, str, &_color, _effectType, bounds);    }}