// // 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 #include #include #include #include #include #include #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 device template void readStream(QIODevice& in, T& t) { in.read((char*) &t, sizeof(t)); } template void readStream(QIODevice& in, T (&t)[N]) { in.read((char*) t, N); } template void fillBuffer(QBuffer& buffer, T (&t)[N]) { buffer.setData((const char*) t, N); } // stores the font metrics for a single character struct 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 _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 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' member const 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 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); } }