655 lines · 28.1 KB · C++
RawDownload
////  Copyright 2013-2019 High Fidelity, Inc.//  Copyright 2020-2021 Vircadia contributors//  Copyright 2022-2023 Overte e.V.////  Distributed under the Apache License, Version 2.0.//  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html//  SPDX-License-Identifier: Apache-2.0// #include "Font.h" #include <QFile>#include <QImage>#include <QNetworkReply>#include <QThreadStorage> #include "artery-font/artery-font.h"#include "artery-font/std-artery-font.h" #include <ColorUtils.h> #include <StreamHelpers.h>#include <shaders/Shaders.h> #include "../render-utils/ShaderConstants.h"#include "../RenderUtilsLogging.h"#include "FontFamilies.h"#include "../StencilMaskPass.h" #include "NetworkAccessManager.h"#include "NetworkingConstants.h" static std::mutex fontMutex; // transparent, unlit, forward, mirror, fadestd::map<std::tuple<bool, bool, bool, bool, bool>, gpu::PipelinePointer> Font::_pipelines;gpu::Stream::FormatPointer Font::_format; struct TextureVertex {    glm::vec2 pos;    glm::vec2 tex;    glm::vec4 bounds;    float isTofu;    TextureVertex() {}    TextureVertex(const glm::vec2& pos, const glm::vec2& tex, const glm::vec4& bounds, bool isTofu) : pos(pos), tex(tex), bounds(bounds), isTofu(isTofu ? 1.0f : 0.0f) {}}; static const int NUMBER_OF_INDICES_PER_QUAD = 6;  // 1 quad = 2 trianglesstatic const int VERTICES_PER_QUAD = 4;           // 1 quad = 4 vertices (must match value in sdf_text3D.slv)const float DOUBLE_MAX_OFFSET_PIXELS = 20.0f;     // must match value in sdf_text3D.slh struct QuadBuilder {    TextureVertex vertices[VERTICES_PER_QUAD];     QuadBuilder(const Glyph& glyph, const glm::vec2& offset, float scale, bool enlargeForShadows) {        glm::vec2 min = offset + glm::vec2(glyph.offset.x, glyph.offset.y - glyph.size.y);        glm::vec2 size = glyph.size;        glm::vec2 texMin = glyph.texOffset;        glm::vec2 texSize = glyph.texSize;         // We need the pre-adjustment bounds for clamping        glm::vec4 bounds = glm::vec4(texMin, texSize);        if (enlargeForShadows) {            glm::vec2 imageSize = glyph.size / glyph.texSize;            glm::vec2 sizeDelta = 0.5f * DOUBLE_MAX_OFFSET_PIXELS * scale * imageSize;            glm::vec2 oldSize = size;            size += sizeDelta;            min.y -= sizeDelta.y;             texSize = texSize * (size / oldSize);        }         // min = bottomLeft        vertices[0] = TextureVertex(min,                                    texMin + glm::vec2(0.0f, texSize.y), bounds, glyph.isTofu);        vertices[1] = TextureVertex(min + glm::vec2(size.x, 0.0f),                                    texMin + texSize, bounds, glyph.isTofu);        vertices[2] = TextureVertex(min + glm::vec2(0.0f, size.y),                                    texMin, bounds, glyph.isTofu);        vertices[3] = TextureVertex(min + size,                                    texMin + glm::vec2(texSize.x, 0.0f), bounds, glyph.isTofu);    }}; Font::Pointer Font::load(const QString& family, QIODevice& fontFile) {    Pointer font = std::make_shared<Font>(family);    font->read(fontFile);    return font;} void Font::handleFontNetworkReply() {    auto requestReply = qobject_cast<QNetworkReply*>(sender());    Q_ASSERT(requestReply != nullptr);     if (requestReply->error() == QNetworkReply::NoError) {        read(*requestReply);    } else {        qDebug() << "Error downloading " << requestReply->url() << " - " << requestReply->errorString();    }} QThreadStorage<size_t> _readOffset;QThreadStorage<size_t> _readMax;int readHelper(void* dst, int length, void* data) {    if (!dst) {        qWarning() << "readHelper called with NULL pointer as first argument: dst = " << dst << "; length = " << length << "; data =" << data;        return 0;    }     if (_readOffset.localData() + length > _readMax.localData()) {        return -1;    }    memcpy(dst, (char *)data + _readOffset.localData(), length);    _readOffset.setLocalData(_readOffset.localData() + length);    return length;}; void Font::read(QIODevice& in) {    _loaded = false;     QByteArray data = in.readAll();    _readOffset.setLocalData(0);    _readMax.setLocalData(data.length());    artery_font::StdArteryFont<float> arteryFont;    bool success = artery_font::decode<&readHelper, float, artery_font::StdList, artery_font::StdByteArray, artery_font::StdString>(arteryFont, (void *)data.data());     if (!success) {        qDebug() << "Font" << _family << "failed to decode.";        return;    }     if (arteryFont.variants.length() == 0) {        qDebug() << "Font" << _family << "has 0 variants.";        return;    }     _distanceRange = glm::vec2(arteryFont.variants[0].metrics.distanceRange);    const float ascent = arteryFont.variants[0].metrics.ascender;    _fontHeight = ascent + fabs(arteryFont.variants[0].metrics.descender);    _leading = arteryFont.variants[0].metrics.lineHeight;    _spaceWidth = 0.5f * arteryFont.variants[0].metrics.emSize; // We use half the emSize as a first guess for _spaceWidth     if (arteryFont.variants[0].glyphs.length() == 0) {        qDebug() << "Font" << _family << "has 0 glyphs.";        return;    }     QVector<Glyph> glyphs;    glyphs.reserve(arteryFont.variants[0].glyphs.length());    for (int i = 0; i < arteryFont.variants[0].glyphs.length(); i++) {        auto& g = arteryFont.variants[0].glyphs[i];         Glyph glyph;        glyph.c = g.codepoint;        glyph.texOffset = glm::vec2(g.imageBounds.l, g.imageBounds.b);        glyph.texSize = glm::vec2(g.imageBounds.r, g.imageBounds.t) - glyph.texOffset;        glyph.offset = glm::vec2(g.planeBounds.l, g.planeBounds.b);        glyph.size = glm::vec2(g.planeBounds.r, g.planeBounds.t) - glyph.offset;        glyph.d = g.advance.h;        glyphs.push_back(glyph);         // If we find the space character, we save its size in _spaceWidth for later        if (glyph.c == ' ') {            _spaceWidth = glyph.d;        }    }     if (arteryFont.images.length() == 0) {        qDebug() << "Font" << _family << "has 0 images.";        return;    }     if (arteryFont.images[0].imageType != artery_font::ImageType::IMAGE_MTSDF) {        qDebug() << "Font" << _family << "has the wrong image type.  Expected MTSDF (7), got" << arteryFont.images[0].imageType;        return;    }     if (arteryFont.images[0].encoding != artery_font::ImageEncoding::IMAGE_PNG) {        qDebug() << "Font" << _family << "has the wrong encoding.  Expected PNG (8), got" << arteryFont.images[0].encoding;        return;    }     if (arteryFont.images[0].pixelFormat != artery_font::PixelFormat::PIXEL_UNSIGNED8) {        qDebug() << "Font" << _family << "has the wrong pixel format.  Expected unsigned char (8), got" << arteryFont.images[0].pixelFormat;        return;    }     if (arteryFont.images[0].width == 0 || arteryFont.images[0].height == 0) {        qDebug() << "Font" << _family << "has image with width or height of 0.  Width:" << arteryFont.images[0].width << ", height:"<< arteryFont.images[0].height;        return;    }     // read image data    QImage image;    if (!image.loadFromData((const unsigned char*)arteryFont.images[0].data, arteryFont.images[0].data.length(), "PNG")) {        qDebug() << "Failed to read image for font" << _family;        return;    }     _glyphs.clear();    glm::vec2 imageSize = toGlm(image.size());    _distanceRange /= imageSize;    bool hasTofu = false;    foreach(Glyph g, glyphs) {        // Adjust the pixel texture coordinates into UV coordinates,        g.texSize /= imageSize;        g.texOffset /= imageSize;        // Y flip        g.texOffset.y = 1.0f - (g.texOffset.y + g.texSize.y);        g.offset.y = -(1.0f - (g.offset.y + g.size.y));        // store in the character to glyph hash        _glyphs[g.c] = g;        if (g.c == '?') {            _tofuGlyph = g;            hasTofu = true;        }    };     _tofuGlyph.texSize = glm::vec2(1.0f);    _tofuGlyph.texOffset = glm::vec2(0.0f);    if (!hasTofu) {        _tofuGlyph.size = glm::vec2(2.0f * _spaceWidth, ascent);        _tofuGlyph.offset = glm::vec2(0.0f, -(1.0f - ascent));        _tofuGlyph.d = 2.0f * _spaceWidth;    }    _tofuGlyph.isTofu = true;     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);    }    // FIXME: We're forcing this to use only one mip, and then manually doing anisotropic filtering in the shader,    // and also calling textureLod.  Shouldn't this just use anisotropic filtering and auto-generate mips?    // We should also use smoothstep for anti-aliasing, as explained here: https://github.com/libgdx/libgdx/wiki/Distance-field-fonts    _texture = gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Texture::SINGLE_MIP,                                      Sampler(Sampler::FILTER_MIN_POINT_MAG_LINEAR));    _texture->setStoredMipFormat(formatMip);    _texture->assignStoredMip(0, image.sizeInBytes(), image.constBits());    _texture->setImportant(true);     _loaded = true;    _needsParamsUpdate = true;} static QHash<QString, Font::Pointer> LOADED_FONTS; Font::Pointer Font::load(const QString& family) {    std::lock_guard<std::mutex> lock(fontMutex);    if (!LOADED_FONTS.contains(family)) {        QString loadFilename;         if (family == ROBOTO_FONT_FAMILY) {            loadFilename = ":/Roboto.arfont";        } else if (family == INCONSOLATA_FONT_FAMILY) {            loadFilename = ":/InconsolataMedium.arfont";        } else if (family == COURIER_FONT_FAMILY) {            loadFilename = ":/CourierPrime.arfont";        } else if (family == TIMELESS_FONT_FAMILY) {            loadFilename = ":/Timeless.arfont";        } else if (family.startsWith("http")) {            auto loadingFont = std::make_shared<Font>(family);            LOADED_FONTS[family] = loadingFont;             auto& networkAccessManager = NetworkAccessManager::getInstance();             QNetworkRequest networkRequest;            networkRequest.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);            networkRequest.setHeader(QNetworkRequest::UserAgentHeader, NetworkingConstants::OVERTE_USER_AGENT);            networkRequest.setUrl(family);             auto networkReply = networkAccessManager.get(networkRequest);            connect(networkReply, &QNetworkReply::finished, loadingFont.get(), &Font::handleFontNetworkReply);        } else if (!LOADED_FONTS.contains(ROBOTO_FONT_FAMILY)) {            // Unrecognized font and we haven't loaded Roboto yet            loadFilename = ":/Roboto.arfont";        } else {            // Unrecognized font but we've already loaded Roboto            LOADED_FONTS[family] = LOADED_FONTS[ROBOTO_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(family, fontFile);        }    }    return LOADED_FONTS[family];} Font::Font(const QString& family) : _family(family) {    static std::once_flag once;    std::call_once(once, []{        Q_INIT_RESOURCE(fonts);    });} // 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 _tofuGlyph;    }    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;} float Font::computeTokenWidth(const QString& token) const {    float advance = 0.0f;    foreach(QChar c, token) {        Q_ASSERT(c != '\n');        advance += (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) {            float tokenWidth = computeTokenWidth(line);            extent.x = std::max(tokenWidth, extent.x);        }        extent.y = lines.count() * _fontHeight;    }    return extent;} void Font::setupGPU() {    if (_pipelines.empty()) {        using namespace shader::render_utils::program;         // transparent, unlit, forward, mirror, fade        static const std::vector<std::tuple<bool, bool, bool, bool, bool, uint32_t>> keys = {            std::make_tuple(false, false, false, false, false, sdf_text3D),            std::make_tuple(true, false, false, false, false, sdf_text3D_translucent),            std::make_tuple(false, true, false, false, false, sdf_text3D_unlit),            std::make_tuple(true, true, false, false, false, sdf_text3D_translucent_unlit),            std::make_tuple(false, false, true, false, false, sdf_text3D_forward),            std::make_tuple(true, false, true, false, false, sdf_text3D_forward /*sdf_text3D_translucent_forward*/),            std::make_tuple(false, true, true, false, false, sdf_text3D_translucent_unlit /*sdf_text3D_unlit_forward*/),            std::make_tuple(true, true, true,  false, false, sdf_text3D_translucent_unlit/*sdf_text3D_translucent_unlit_forward*/),            std::make_tuple(false, false, false, true, false, sdf_text3D_mirror),            std::make_tuple(true, false, false, true, false, sdf_text3D_translucent_mirror),            std::make_tuple(false, true, false, true, false, sdf_text3D_unlit_mirror),            std::make_tuple(true, true, false, true, false, sdf_text3D_translucent_unlit_mirror),            std::make_tuple(false, false, true, true, false, sdf_text3D_forward_mirror),            std::make_tuple(true, false, true, true, false, sdf_text3D_forward_mirror /*sdf_text3D_translucent_forward_mirror*/),            std::make_tuple(false, true, true, true, false, sdf_text3D_translucent_unlit_mirror /*sdf_text3D_unlit_forward_mirror*/),            std::make_tuple(true, true, true,  true, false, sdf_text3D_translucent_unlit_mirror/*sdf_text3D_translucent_unlit_forward_mirror*/),            std::make_tuple(false, false, false, false, true, sdf_text3D_fade),            std::make_tuple(true, false, false, false, true, sdf_text3D_translucent_fade),            std::make_tuple(false, true, false, false, true, sdf_text3D_unlit_fade),            std::make_tuple(true, true, false, false, true, sdf_text3D_translucent_unlit_fade),            std::make_tuple(false, false, false, true, true, sdf_text3D_mirror_fade),            std::make_tuple(true, false, false, true, true, sdf_text3D_translucent_mirror_fade),            std::make_tuple(false, true, false, true, true, sdf_text3D_unlit_mirror_fade),            std::make_tuple(true, true, false, true, true, sdf_text3D_translucent_unlit_mirror_fade),            // no such thing as forward + fade        };        for (auto& key : keys) {            bool transparent = std::get<0>(key);            bool unlit = std::get<1>(key);            bool forward = std::get<2>(key);            bool mirror = std::get<3>(key);            bool fade = std::get<4>(key);             auto state = std::make_shared<gpu::State>();            state->setCullMode(gpu::State::CULL_BACK);            state->setDepthTest(true, !transparent, ComparisonFunction::LESS_EQUAL);            state->setBlendFunction(transparent,                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);            if (transparent) {                PrepareStencil::testMask(*state);            } else {                PrepareStencil::testMaskDrawShape(*state);            }             // TODO: Revisit this once we support specifying depth bias values in ShapeKey            // assume the text payload is in front of an already-biased background (bias of -1),            // so push the text a little further out so it doesn't z-fight            state->setDepthBias(-2.0f);            state->setDepthBiasSlopeScale(-2.0f);             _pipelines[std::make_tuple(transparent, unlit, forward, mirror, fade)] = gpu::Pipeline::create(gpu::Shader::createProgram(std::get<5>(key)), state);        }         // Sanity checks        static const int TEX_COORD_OFFSET = offsetof(TextureVertex, tex);        static const int TEX_BOUNDS_OFFSET = offsetof(TextureVertex, bounds);        static const int TOFU_OFFSET = offsetof(TextureVertex, isTofu);        assert(TEX_COORD_OFFSET == sizeof(glm::vec2));        assert(sizeof(TextureVertex) == 2 * sizeof(glm::vec2) + sizeof(glm::vec4) + sizeof(float));        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), TEX_COORD_OFFSET);        _format->setAttribute(gpu::Stream::TEXCOORD1, 0, gpu::Element(gpu::VEC4, gpu::FLOAT, gpu::XYZW), TEX_BOUNDS_OFFSET);        _format->setAttribute(gpu::Stream::TANGENT, 0, gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::RED), TOFU_OFFSET);    }} inline QuadBuilder adjustedQuadBuilderForAlignmentMode(const Glyph& glyph, glm::vec2 advance, float scale, float enlargeForShadows,                                                TextAlignment alignment, float rightSpacing, TextVerticalAlignment verticalAlignment, float bottomSpacing) {    if (alignment == TextAlignment::RIGHT) {        advance.x += rightSpacing;    } else if (alignment == TextAlignment::CENTER) {        advance.x += 0.5f * rightSpacing;    }    if (verticalAlignment == TextVerticalAlignment::BOTTOM) {        advance.y += bottomSpacing;    } else if (verticalAlignment == TextVerticalAlignment::CENTER) {        advance.y += 0.5f * bottomSpacing;    }    return QuadBuilder(glyph, advance, scale, enlargeForShadows);} void Font::buildVertices(Font::DrawInfo& drawInfo, const QString& str, const glm::vec2& origin, const glm::vec2& bounds, float scale, bool enlargeForShadows,                         TextAlignment alignment, TextVerticalAlignment verticalAlignment) {    drawInfo.verticesBuffer = std::make_shared<gpu::Buffer>(gpu::Buffer::VertexBuffer);    drawInfo.indicesBuffer = std::make_shared<gpu::Buffer>(gpu::Buffer::IndexBuffer);    drawInfo.indexCount = 0;    int numVertices = 0;     drawInfo.string = str;    drawInfo.bounds = bounds;    drawInfo.origin = origin;     float rightEdge = origin.x + bounds.x;    float bottomEdge = origin.y - bounds.y;     // Top left of text    bool firstTokenOfLine = true;    glm::vec2 advance = origin;    std::vector<std::pair<Glyph, vec2>> glyphsAndCorners;    const QStringList tokens = tokenizeForWrapping(str);    for (int i = 0; i < tokens.length(); i++) {        const QString& token = tokens[i];         if ((bounds.y != -1) && (advance.y < bottomEdge)) {            // We are out of the y bound, stop drawing            break;        }         bool isNewLine = (token == QString('\n'));        bool forceNewLine = false;         // Handle wrapping        if (!isNewLine && (bounds.x != -1) && (advance.x + computeExtent(token).x > rightEdge)) {            // We are out of the x bound, force new line            forceNewLine = true;        }         if (isNewLine || (forceNewLine && !firstTokenOfLine)) {            if (forceNewLine && !firstTokenOfLine) {                // We want to try this token again on the new line                i--;            }             // Character return, move the advance to a new line            advance = glm::vec2(origin.x, advance.y - _leading);            firstTokenOfLine = true;            // No need to draw anything, go directly to next token            continue;        }         // Draw the token        for (const QChar& c : token) {            if (bounds.x != -1 && advance.x > rightEdge) {                break;            }            const Glyph& glyph = getGlyph(c);             glyphsAndCorners.emplace_back(glyph, advance); 

Lines 1–500 of 655.