mirror of
https://github.com/JulianGro/overte.git
synced 2025-04-25 20:55:10 +02:00
548 lines
17 KiB
C++
548 lines
17 KiB
C++
//
|
|
// TextRenderer.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 <gpu/GPUConfig.h>
|
|
#include <QApplication>
|
|
#include <QtDebug>
|
|
#include <QString>
|
|
#include <QStringList>
|
|
#include <QBuffer>
|
|
#include <QFile>
|
|
|
|
// FIXME, decouple from the GL headers
|
|
#include <QOpenGLShaderProgram>
|
|
#include <QOpenGLTexture>
|
|
#include <QOpenGLVertexArrayObject>
|
|
#include <QOpenGLBuffer>
|
|
|
|
#include <glm/glm.hpp>
|
|
#include <glm/gtc/matrix_transform.hpp>
|
|
|
|
#include "gpu/GLBackend.h"
|
|
#include "gpu/Stream.h"
|
|
|
|
#include "GLMHelpers.h"
|
|
#include "MatrixStack.h"
|
|
#include "TextRenderer.h"
|
|
|
|
#include "sdf_text_vert.h"
|
|
#include "sdf_text_frag.h"
|
|
|
|
// Helper functions for reading binary data from an IO device
|
|
template<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);
|
|
}
|
|
|
|
// FIXME support the shadow effect, or remove it from the API
|
|
// FIXME figure out how to improve the anti-aliasing on the
|
|
// interior of the outline fonts
|
|
|
|
// stores the font metrics for a single character
|
|
struct Glyph {
|
|
QChar c;
|
|
glm::vec2 texOffset;
|
|
glm::vec2 texSize;
|
|
glm::vec2 size;
|
|
glm::vec2 offset;
|
|
float d; // xadvance - adjusts character positioning
|
|
size_t indexOffset;
|
|
|
|
QRectF bounds() const;
|
|
QRectF textureBounds(const glm::vec2 & textureSize) const;
|
|
|
|
void read(QIODevice & in);
|
|
};
|
|
|
|
void Glyph::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;
|
|
}
|
|
|
|
const float DEFAULT_POINT_SIZE = 12;
|
|
|
|
class Font {
|
|
public:
|
|
|
|
Font();
|
|
|
|
using TexturePtr = QSharedPointer < QOpenGLTexture >;
|
|
using VertexArrayPtr = QSharedPointer< QOpenGLVertexArrayObject >;
|
|
using ProgramPtr = QSharedPointer < QOpenGLShaderProgram >;
|
|
using BufferPtr = QSharedPointer < QOpenGLBuffer >;
|
|
|
|
// 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, Glyph> _glyphs;
|
|
|
|
// the id of the glyph texture to which we're currently writing
|
|
GLuint _currentTextureID;
|
|
|
|
int _pointSize;
|
|
|
|
// the height of the current row of characters
|
|
int _rowHeight;
|
|
|
|
QString _family;
|
|
float _fontSize { 0 };
|
|
float _leading { 0 };
|
|
float _ascent { 0 };
|
|
float _descent { 0 };
|
|
float _spaceWidth { 0 };
|
|
|
|
BufferPtr _vertices;
|
|
BufferPtr _indices;
|
|
TexturePtr _texture;
|
|
VertexArrayPtr _vao;
|
|
QImage _image;
|
|
ProgramPtr _program;
|
|
|
|
const Glyph & getGlyph(const QChar & c) const;
|
|
void read(QIODevice& path);
|
|
// Initialize the OpenGL structures
|
|
void setupGL();
|
|
|
|
glm::vec2 computeExtent(const QString & str) const;
|
|
|
|
glm::vec2 computeTokenExtent(const QString & str) const;
|
|
|
|
glm::vec2 drawString(float x, float y, const QString & str,
|
|
const glm::vec4& color, TextRenderer::EffectType effectType,
|
|
const glm::vec2& bound);
|
|
|
|
private:
|
|
QStringList tokenizeForWrapping(const QString & str) const;
|
|
|
|
bool _initialized;
|
|
};
|
|
|
|
static QHash<QString, Font*> LOADED_FONTS;
|
|
|
|
Font* loadFont(QIODevice& fontFile) {
|
|
Font* result = new Font();
|
|
result->read(fontFile);
|
|
return result;
|
|
}
|
|
|
|
Font* loadFont(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);
|
|
|
|
qDebug() << "Loaded font" << loadFilename << "from Qt Resource System.";
|
|
|
|
LOADED_FONTS[family] = loadFont(fontFile);
|
|
}
|
|
}
|
|
return LOADED_FONTS[family];
|
|
}
|
|
|
|
Font::Font() : _initialized(false) {
|
|
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 Glyph & Font::getGlyph(const QChar & c) const {
|
|
if (!_glyphs.contains(c)) {
|
|
return _glyphs[QChar('?')];
|
|
}
|
|
return _glyphs[c];
|
|
}
|
|
|
|
void Font::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;
|
|
_rowHeight = _fontSize + _descent;
|
|
|
|
// Read character count
|
|
uint16_t count;
|
|
readStream(in, count);
|
|
// read metrics data for each character
|
|
QVector<Glyph> 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
|
|
if (!_image.loadFromData(in.readAll(), "PNG")) {
|
|
qFatal("Failed to read SDFF image");
|
|
}
|
|
|
|
_glyphs.clear();
|
|
foreach(Glyph g, glyphs) {
|
|
// Adjust the pixel texture coordinates into UV coordinates,
|
|
glm::vec2 imageSize = toGlm(_image.size());
|
|
g.texSize /= imageSize;
|
|
g.texOffset /= imageSize;
|
|
// store in the character to glyph hash
|
|
_glyphs[g.c] = g;
|
|
};
|
|
}
|
|
|
|
struct TextureVertex {
|
|
glm::vec2 pos;
|
|
glm::vec2 tex;
|
|
TextureVertex() {
|
|
}
|
|
TextureVertex(const glm::vec2 & pos, const glm::vec2 & tex) :
|
|
pos(pos), tex(tex) {
|
|
}
|
|
TextureVertex(const QPointF & pos, const QPointF & tex) :
|
|
pos(pos.x(), pos.y()), tex(tex.x(), tex.y()) {
|
|
}
|
|
};
|
|
|
|
struct QuadBuilder {
|
|
TextureVertex vertices[4];
|
|
QuadBuilder(const QRectF & r, const QRectF & tr) {
|
|
vertices[0] = TextureVertex(r.bottomLeft(), tr.topLeft());
|
|
vertices[1] = TextureVertex(r.bottomRight(), tr.topRight());
|
|
vertices[2] = TextureVertex(r.topRight(), tr.bottomRight());
|
|
vertices[3] = TextureVertex(r.topLeft(), tr.bottomLeft());
|
|
}
|
|
};
|
|
|
|
QRectF Glyph::bounds() const {
|
|
return glmToRect(offset, size);
|
|
}
|
|
|
|
QRectF Glyph::textureBounds(const glm::vec2 & textureSize) const {
|
|
return glmToRect(texOffset, texSize);
|
|
}
|
|
|
|
void Font::setupGL() {
|
|
if (_initialized) {
|
|
return;
|
|
}
|
|
_initialized = true;
|
|
|
|
_texture = TexturePtr(
|
|
new QOpenGLTexture(_image, QOpenGLTexture::GenerateMipMaps));
|
|
_program = ProgramPtr(new QOpenGLShaderProgram());
|
|
if (!_program->create()) {
|
|
qFatal("Could not create text shader");
|
|
}
|
|
if (!_program->addShaderFromSourceCode(QOpenGLShader::Vertex, sdf_text_vert) || //
|
|
!_program->addShaderFromSourceCode(QOpenGLShader::Fragment, sdf_text_frag) || //
|
|
!_program->link()) {
|
|
qFatal("%s", _program->log().toLocal8Bit().constData());
|
|
}
|
|
|
|
std::vector<TextureVertex> vertexData;
|
|
std::vector<GLuint> indexData;
|
|
vertexData.reserve(_glyphs.size() * 4);
|
|
std::for_each(_glyphs.begin(), _glyphs.end(), [&](Glyph & m) {
|
|
GLuint index = (GLuint)vertexData.size();
|
|
|
|
QRectF bounds = m.bounds();
|
|
QRectF texBounds = m.textureBounds(toGlm(_image.size()));
|
|
QuadBuilder qb(bounds, texBounds);
|
|
for (int i = 0; i < 4; ++i) {
|
|
vertexData.push_back(qb.vertices[i]);
|
|
}
|
|
|
|
m.indexOffset = indexData.size() * sizeof(GLuint);
|
|
// FIXME use triangle strips + primitive restart index
|
|
indexData.push_back(index + 0);
|
|
indexData.push_back(index + 1);
|
|
indexData.push_back(index + 2);
|
|
indexData.push_back(index + 0);
|
|
indexData.push_back(index + 2);
|
|
indexData.push_back(index + 3);
|
|
});
|
|
|
|
_vao = VertexArrayPtr(new QOpenGLVertexArrayObject());
|
|
_vao->create();
|
|
_vao->bind();
|
|
|
|
_vertices = BufferPtr(new QOpenGLBuffer(QOpenGLBuffer::VertexBuffer));
|
|
_vertices->create();
|
|
_vertices->bind();
|
|
_vertices->allocate(&vertexData[0],
|
|
sizeof(TextureVertex) * vertexData.size());
|
|
_indices = BufferPtr(new QOpenGLBuffer(QOpenGLBuffer::IndexBuffer));
|
|
_indices->create();
|
|
_indices->bind();
|
|
_indices->allocate(&indexData[0], sizeof(GLuint) * indexData.size());
|
|
|
|
GLsizei stride = (GLsizei) sizeof(TextureVertex);
|
|
void* offset = (void*) offsetof(TextureVertex, tex);
|
|
int posLoc = _program->attributeLocation("Position");
|
|
int texLoc = _program->attributeLocation("TexCoord");
|
|
glEnableVertexAttribArray(posLoc);
|
|
glVertexAttribPointer(posLoc, 2, GL_FLOAT, false, stride, nullptr);
|
|
glEnableVertexAttribArray(texLoc);
|
|
glVertexAttribPointer(texLoc, 2, GL_FLOAT, false, stride, offset);
|
|
_vao->release();
|
|
}
|
|
|
|
// FIXME there has to be a cleaner way of doing this
|
|
QStringList Font::tokenizeForWrapping(const QString & str) const {
|
|
QStringList result;
|
|
foreach(const QString & token1, str.split(" ", QString::SkipEmptyParts)) {
|
|
bool lineFeed = false;
|
|
foreach(const QString & token2, token1.split("\n")) {
|
|
if (lineFeed) {
|
|
result << "\n";
|
|
}
|
|
if (token2.size()) {
|
|
result << token2;
|
|
}
|
|
lineFeed = true;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
|
|
glm::vec2 Font::computeTokenExtent(const QString & token) const {
|
|
glm::vec2 advance(0, _rowHeight - _descent);
|
|
foreach(QChar c, token) {
|
|
assert(c != ' ' && c != '\n');
|
|
const Glyph & m = getGlyph(c);
|
|
advance.x += m.d;
|
|
}
|
|
return advance;
|
|
}
|
|
|
|
|
|
glm::vec2 Font::computeExtent(const QString & str) const {
|
|
glm::vec2 extent(0, _rowHeight - _descent);
|
|
// FIXME, come up with a better method of splitting text
|
|
// that will allow wrapping but will preserve things like
|
|
// tabs or consecutive spaces
|
|
bool firstTokenOnLine = true;
|
|
float lineWidth = 0.0f;
|
|
QStringList tokens = tokenizeForWrapping(str);
|
|
foreach(const QString & token, tokens) {
|
|
if (token == "\n") {
|
|
extent.x = std::max(lineWidth, extent.x);
|
|
lineWidth = 0.0f;
|
|
extent.y += _rowHeight;
|
|
firstTokenOnLine = true;
|
|
continue;
|
|
}
|
|
if (!firstTokenOnLine) {
|
|
lineWidth += _spaceWidth;
|
|
}
|
|
lineWidth += computeTokenExtent(token).x;
|
|
firstTokenOnLine = false;
|
|
}
|
|
extent.x = std::max(lineWidth, extent.x);
|
|
return extent;
|
|
}
|
|
|
|
// FIXME support the maxWidth parameter and allow the text to automatically wrap
|
|
// even without explicit line feeds.
|
|
glm::vec2 Font::drawString(float x, float y, const QString & str,
|
|
const glm::vec4& color, TextRenderer::EffectType effectType,
|
|
const glm::vec2& bounds) {
|
|
|
|
setupGL();
|
|
|
|
// Stores how far we've moved from the start of the string, in DTP units
|
|
glm::vec2 advance(0, -_rowHeight - _descent);
|
|
|
|
_program->bind();
|
|
_program->setUniformValue("Color", color.r, color.g, color.b, color.a);
|
|
_program->setUniformValue("Projection",
|
|
fromGlm(MatrixStack::projection().top()));
|
|
if (effectType == TextRenderer::OUTLINE_EFFECT) {
|
|
_program->setUniformValue("Outline", true);
|
|
}
|
|
// Needed?
|
|
glEnable(GL_TEXTURE_2D);
|
|
_texture->bind();
|
|
_vao->bind();
|
|
|
|
MatrixStack & mv = MatrixStack::modelview();
|
|
// scale the modelview into font units
|
|
mv.translate(glm::vec3(0, _ascent, 0));
|
|
foreach(const QString & token, tokenizeForWrapping(str)) {
|
|
if (token == "\n") {
|
|
advance.x = 0.0f;
|
|
advance.y -= _rowHeight;
|
|
// If we've wrapped right out of the bounds, then we're
|
|
// done with rendering the tokens
|
|
if (bounds.y > 0 && abs(advance.y) > bounds.y) {
|
|
break;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
glm::vec2 tokenExtent = computeTokenExtent(token);
|
|
if (bounds.x > 0 && advance.x > 0) {
|
|
// We check if we'll be out of bounds
|
|
if (advance.x + tokenExtent.x >= bounds.x) {
|
|
// We're out of bounds, so wrap to the next line
|
|
advance.x = 0.0f;
|
|
advance.y -= _rowHeight;
|
|
// If we've wrapped right out of the bounds, then we're
|
|
// done with rendering the tokens
|
|
if (bounds.y > 0 && abs(advance.y) > bounds.y) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach(const QChar & c, token) {
|
|
// get metrics for this character to speed up measurements
|
|
const Glyph & m = getGlyph(c);
|
|
// We create an offset vec2 to hold the local offset of this character
|
|
// This includes compensating for the inverted Y axis of the font
|
|
// coordinates
|
|
glm::vec2 offset(advance);
|
|
offset.y -= m.size.y;
|
|
// Bind the new position
|
|
mv.withPush([&] {
|
|
mv.translate(offset);
|
|
// FIXME find a better (and GL ES 3.1 compatible) way of rendering the text
|
|
// that doesn't involve a single GL call per character.
|
|
// Most likely an 'indirect' call or an 'instanced' call.
|
|
_program->setUniformValue("ModelView", fromGlm(mv.top()));
|
|
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, (void*)(m.indexOffset));
|
|
});
|
|
advance.x += m.d;
|
|
}
|
|
advance.x += _spaceWidth;
|
|
}
|
|
|
|
_vao->release();
|
|
_program->release();
|
|
// FIXME, needed?
|
|
// glDisable(GL_TEXTURE_2D);
|
|
return advance;
|
|
}
|
|
|
|
TextRenderer* TextRenderer::getInstance(const char* family, float pointSize,
|
|
int weight, bool italic, EffectType effect, int effectThickness,
|
|
const QColor& color) {
|
|
if (pointSize < 0) {
|
|
pointSize = DEFAULT_POINT_SIZE;
|
|
}
|
|
return new TextRenderer(family, pointSize, weight, italic, effect,
|
|
effectThickness, color);
|
|
}
|
|
|
|
TextRenderer::TextRenderer(const char* family, float pointSize, int weight,
|
|
bool italic, EffectType effect, int effectThickness,
|
|
const QColor& color) :
|
|
_effectType(effect), _effectThickness(effectThickness), _pointSize(pointSize), _color(color), _font(loadFont(family)) {
|
|
if (1 != _effectThickness) {
|
|
qWarning() << "Effect thickness not current supported";
|
|
}
|
|
if (NO_EFFECT != _effectType && OUTLINE_EFFECT != _effectType) {
|
|
qWarning() << "Effect thickness not current supported";
|
|
}
|
|
}
|
|
|
|
TextRenderer::~TextRenderer() {
|
|
}
|
|
|
|
glm::vec2 TextRenderer::computeExtent(const QString & str) const {
|
|
float scale = (_pointSize / DEFAULT_POINT_SIZE) * 0.25f;
|
|
return _font->computeExtent(str) * scale;
|
|
}
|
|
|
|
float TextRenderer::draw(float x, float y, const QString & str,
|
|
const glm::vec4& color, const glm::vec2 & bounds) {
|
|
glm::vec4 actualColor(color);
|
|
if (actualColor.r < 0) {
|
|
actualColor = toGlm(_color);
|
|
}
|
|
|
|
float scale = (_pointSize / DEFAULT_POINT_SIZE) * 0.25f;
|
|
glm::vec2 result;
|
|
MatrixStack::withGlMatrices([&] {
|
|
MatrixStack & mv = MatrixStack::modelview();
|
|
// scale the modelview into font units
|
|
// FIXME migrate the constant scale factor into the geometry of the
|
|
// fonts so we don't have to flip the Y axis here and don't have to
|
|
// scale at all.
|
|
mv.translate(glm::vec2(x, y)).scale(glm::vec3(scale, -scale, scale));
|
|
// The font does all the OpenGL work
|
|
result = _font->drawString(x, y, str, actualColor, _effectType, bounds / scale);
|
|
});
|
|
return result.x;
|
|
}
|
|
|