//// 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. Load more
// // 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, fade std::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 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, 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' member const 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);