// // 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 #include #include #include #include #include #include "../render-utils/ShaderConstants.h" #include "../RenderUtilsLogging.h" #include "FontFamilies.h" #include "../StencilMaskPass.h" #include "NetworkAccessManager.h" #include "NetworkingConstants.h" static std::mutex fontMutex; std::map, gpu::PipelinePointer> Font::_pipelines; gpu::Stream::FormatPointer Font::_format; struct TextureVertex { glm::vec2 pos; glm::vec2 tex; glm::vec4 bounds; TextureVertex() {} TextureVertex(const glm::vec2& pos, const glm::vec2& tex, const glm::vec4& bounds) : pos(pos), tex(tex), bounds(bounds) {} }; static const int NUMBER_OF_INDICES_PER_QUAD = 6; // 1 quad = 2 triangles static 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); vertices[1] = TextureVertex(min + glm::vec2(size.x, 0.0f), texMin + texSize, bounds); vertices[2] = TextureVertex(min + glm::vec2(0.0f, size.y), texMin, bounds); vertices[3] = TextureVertex(min + size, texMin + glm::vec2(texSize.x, 0.0f), bounds); } }; Font::Pointer Font::load(QIODevice& fontFile) { Pointer font = std::make_shared(); font->read(fontFile); return font; } static QHash LOADED_FONTS; Font::Pointer Font::load(const QString& family) { std::lock_guard lock(fontMutex); if (!LOADED_FONTS.contains(family)) { QString loadFilename; if (family == ROBOTO_FONT_FAMILY) { loadFilename = ":/Roboto.sdff"; } else if (family == INCONSOLATA_FONT_FAMILY) { loadFilename = ":/InconsolataMedium.sdff"; } else if (family == COURIER_FONT_FAMILY) { loadFilename = ":/CourierPrime.sdff"; } else if (family == TIMELESS_FONT_FAMILY) { loadFilename = ":/Timeless.sdff"; } else if (family.startsWith("http")) { auto loadingFont = std::make_shared(); loadingFont->setLoaded(false); LOADED_FONTS[family] = loadingFont; auto& networkAccessManager = NetworkAccessManager::getInstance(); QNetworkRequest networkRequest; networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); 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.sdff"; } 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(fontFile); } } return LOADED_FONTS[family]; } void Font::handleFontNetworkReply() { auto requestReply = qobject_cast(sender()); Q_ASSERT(requestReply != nullptr); if (requestReply->error() == QNetworkReply::NoError) { setLoaded(true); read(*requestReply); } else { qDebug() << "Error downloading " << requestReply->url() << " - " << requestReply->errorString(); } } Font::Font() { 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' member const 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)) { qDebug() << "Bad SDFF file"; _loaded = false; return; } 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 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")) { qDebug() << "Failed to read SDFF image"; _loaded = false; return; } _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); } // 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, gpu::Sampler(gpu::Sampler::FILTER_MIN_POINT_MAG_LINEAR)); _texture->setStoredMipFormat(formatMip); _texture->assignStoredMip(0, image.sizeInBytes(), image.constBits()); _texture->setImportant(true); } void Font::setupGPU() { if (_pipelines.empty()) { using namespace shader::render_utils::program; static const std::vector> keys = { std::make_tuple(false, false, false, sdf_text3D), std::make_tuple(true, false, false, sdf_text3D_translucent), std::make_tuple(false, true, false, sdf_text3D_unlit), std::make_tuple(true, true, false, sdf_text3D_translucent_unlit), std::make_tuple(false, false, true, sdf_text3D_forward), std::make_tuple(true, false, true, sdf_text3D_forward/*sdf_text3D_translucent_forward*/), std::make_tuple(false, true, true, sdf_text3D_translucent_unlit/*sdf_text3D_unlit_forward*/), std::make_tuple(true, true, true, sdf_text3D_translucent_unlit/*sdf_text3D_translucent_unlit_forward*/) }; for (auto& key : keys) { auto state = std::make_shared(); state->setCullMode(gpu::State::CULL_BACK); state->setDepthTest(true, !std::get<0>(key), gpu::LESS_EQUAL); state->setBlendFunction(std::get<0>(key), 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 (std::get<0>(key)) { PrepareStencil::testMask(*state); } else { PrepareStencil::testMaskDrawShape(*state); } _pipelines[std::make_tuple(std::get<0>(key), std::get<1>(key), std::get<2>(key))] = gpu::Pipeline::create(gpu::Shader::createProgram(std::get<3>(key)), state); } // Sanity checks static const int TEX_COORD_OFFSET = offsetof(TextureVertex, tex); static const int TEX_BOUNDS_OFFSET = offsetof(TextureVertex, bounds); assert(TEX_COORD_OFFSET == sizeof(glm::vec2)); assert(sizeof(TextureVertex) == 2 * sizeof(glm::vec2) + sizeof(glm::vec4)); assert(sizeof(QuadBuilder) == 4 * sizeof(TextureVertex)); // Setup rendering structures _format = std::make_shared(); _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); } } inline QuadBuilder adjustedQuadBuilderForAlignmentMode(const Glyph& glyph, glm::vec2 advance, float scale, float enlargeForShadows, TextAlignment alignment, float rightSpacing) { if (alignment == TextAlignment::RIGHT) { advance.x += rightSpacing; } else if (alignment == TextAlignment::CENTER) { advance.x += 0.5f * rightSpacing; } 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) { drawInfo.verticesBuffer = std::make_shared(); drawInfo.indicesBuffer = std::make_shared(); drawInfo.indexCount = 0; int numVertices = 0; drawInfo.string = str; drawInfo.bounds = bounds; drawInfo.origin = origin; float enlargedBoundsX = bounds.x - 0.5f * DOUBLE_MAX_OFFSET_PIXELS * float(enlargeForShadows); float rightEdge = origin.x + enlargedBoundsX; // Top left of text glm::vec2 advance = origin; std::vector> glyphsAndCorners; 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 > rightEdge)) { // 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(origin.x, advance.y - _leading); if (isNewLine) { // No need to draw anything, go directly to next token continue; } else if (computeExtent(token).x > enlargedBoundsX) { // token will never fit, stop drawing break; } } if ((bounds.y != -1) && (advance.y - _fontSize < origin.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]; glyphsAndCorners.emplace_back(glyph, advance - glm::vec2(0.0f, _ascent)); // Advance by glyph size advance.x += glyph.d; } // Add space after all non return tokens advance.x += _spaceWidth; } } std::vector quadBuilders; quadBuilders.reserve(glyphsAndCorners.size()); { int i = glyphsAndCorners.size() - 1; while (i >= 0) { auto nextGlyphAndCorner = glyphsAndCorners[i]; float rightSpacing = rightEdge - (nextGlyphAndCorner.second.x + nextGlyphAndCorner.first.d); quadBuilders.push_back(adjustedQuadBuilderForAlignmentMode(nextGlyphAndCorner.first, nextGlyphAndCorner.second, scale, enlargeForShadows, alignment, rightSpacing)); i--; while (i >= 0) { auto prevGlyphAndCorner = glyphsAndCorners[i]; // We're to the right of the last character we checked, which means we're on a previous line, so we need to // recalculate the spacing, so we exit this loop if (prevGlyphAndCorner.second.x >= nextGlyphAndCorner.second.x) { break; } quadBuilders.push_back(adjustedQuadBuilderForAlignmentMode(prevGlyphAndCorner.first, prevGlyphAndCorner.second, scale, enlargeForShadows, alignment, rightSpacing)); nextGlyphAndCorner = prevGlyphAndCorner; i--; } } } // The quadBuilders is backwards now because we looped over the glyphs backwards to adjust their alignment for (int i = quadBuilders.size() - 1; i >= 0; i--) { quint16 verticesOffset = numVertices; drawInfo.verticesBuffer->append(quadBuilders[i]); numVertices += VERTICES_PER_QUAD; // 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; drawInfo.indicesBuffer->append(sizeof(indices), (const gpu::Byte*)indices); drawInfo.indexCount += NUMBER_OF_INDICES_PER_QUAD; } } void Font::drawString(gpu::Batch& batch, Font::DrawInfo& drawInfo, const QString& str, const glm::vec4& color, const glm::vec3& effectColor, float effectThickness, TextEffect effect, TextAlignment alignment, const glm::vec2& origin, const glm::vec2& bounds, float scale, bool unlit, bool forward) { if (!_loaded || str == "") { return; } int textEffect = (int)effect; const int SHADOW_EFFECT = (int)TextEffect::SHADOW_EFFECT; // If we're switching to or from shadow effect mode, we need to rebuild the vertices if (str != drawInfo.string || bounds != drawInfo.bounds || origin != drawInfo.origin || alignment != _alignment || (drawInfo.params.effect != textEffect && (textEffect == SHADOW_EFFECT || drawInfo.params.effect == SHADOW_EFFECT)) || (textEffect == SHADOW_EFFECT && scale != _scale)) { _scale = scale; _alignment = alignment; buildVertices(drawInfo, str, origin, bounds, scale, textEffect == SHADOW_EFFECT, alignment); } setupGPU(); if (!drawInfo.paramsBuffer || drawInfo.params.color != color || drawInfo.params.effectColor != effectColor || drawInfo.params.effectThickness != effectThickness || drawInfo.params.effect != textEffect) { drawInfo.params.color = color; drawInfo.params.effectColor = effectColor; drawInfo.params.effectThickness = effectThickness; drawInfo.params.effect = textEffect; // need the gamma corrected color here DrawParams gpuDrawParams; gpuDrawParams.color = ColorUtils::sRGBToLinearVec4(drawInfo.params.color); gpuDrawParams.effectColor = ColorUtils::sRGBToLinearVec3(drawInfo.params.effectColor); gpuDrawParams.effectThickness = drawInfo.params.effectThickness; gpuDrawParams.effect = drawInfo.params.effect; if (!drawInfo.paramsBuffer) { drawInfo.paramsBuffer = std::make_shared(sizeof(DrawParams), nullptr); } drawInfo.paramsBuffer->setSubData(0, sizeof(DrawParams), (const gpu::Byte*)&gpuDrawParams); } batch.setPipeline(_pipelines[std::make_tuple(color.a < 1.0f, unlit, forward)]); batch.setInputFormat(_format); batch.setInputBuffer(0, drawInfo.verticesBuffer, 0, _format->getChannels().at(0)._stride); batch.setResourceTexture(render_utils::slot::texture::TextFont, _texture); batch.setUniformBuffer(0, drawInfo.paramsBuffer, 0, sizeof(DrawParams)); batch.setIndexBuffer(gpu::UINT16, drawInfo.indicesBuffer, 0); batch.drawIndexed(gpu::TRIANGLES, drawInfo.indexCount, 0); }