Working on signed distance fields text rendering

This commit is contained in:
Brad Davis 2015-01-31 21:42:54 -08:00
parent 71b677ef5c
commit 60317b3526
9 changed files with 74189 additions and 317 deletions

View file

@ -913,7 +913,8 @@ void Avatar::setAttachmentData(const QVector<AttachmentData>& attachmentData) {
void Avatar::setDisplayName(const QString& displayName) {
AvatarData::setDisplayName(displayName);
_displayNameBoundingRect = textRenderer(DISPLAYNAME)->metrics().tightBoundingRect(displayName);
// FIXME need to find an alternate mechanism for this.
//_displayNameBoundingRect = textRenderer(DISPLAYNAME)->metrics().tightBoundingRect(displayName);
}
void Avatar::setBillboard(const QByteArray& billboard) {

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,7 @@
#include "MatrixStack.h"
QMatrix4x4 fromGlm(const glm::mat4 & m) {
return QMatrix4x4(&m[0][0]).transposed();
return QMatrix4x4(&m[0][0]);
}

View file

@ -0,0 +1,205 @@
/************************************************************************************
Authors : Bradley Austin Davis <bdavis@saintandreas.org>
Copyright : Copyright Brad Davis. All Rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
************************************************************************************/
#pragma once
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <glm/gtc/noise.hpp>
#include <glm/gtc/epsilon.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <stack>
#include <QMatrix4x4>
class MatrixStack : public std::stack<glm::mat4> {
public:
MatrixStack() {
push(glm::mat4());
}
explicit MatrixStack(const MatrixStack & other) {
*((std::stack<glm::mat4>*)this) = *((std::stack<glm::mat4>*)&other);
}
operator const glm::mat4 & () const {
return top();
}
MatrixStack & pop() {
std::stack<glm::mat4>::pop();
assert(!empty());
return *this;
}
MatrixStack & push() {
emplace(top());
return *this;
}
MatrixStack & identity() {
top() = glm::mat4();
return *this;
}
MatrixStack & push(const glm::mat4 & mat) {
std::stack<glm::mat4>::push(mat);
return *this;
}
MatrixStack & rotate(const glm::mat3 & rotation) {
return postMultiply(glm::mat4(rotation));
}
MatrixStack & rotate(const glm::quat & rotation) {
return postMultiply(glm::mat4_cast(rotation));
}
MatrixStack & rotate(float theta, const glm::vec3 & axis) {
return postMultiply(glm::rotate(glm::mat4(), theta, axis));
}
MatrixStack & translate(float translation) {
return translate(glm::vec3(translation, 0, 0));
}
MatrixStack & translate(const glm::vec2 & translation) {
return translate(glm::vec3(translation, 0));
}
MatrixStack & translate(const glm::vec3 & translation) {
return postMultiply(glm::translate(glm::mat4(), translation));
}
MatrixStack & preTranslate(const glm::vec3 & translation) {
return preMultiply(glm::translate(glm::mat4(), translation));
}
MatrixStack & scale(float factor) {
return scale(glm::vec3(factor));
}
MatrixStack & scale(const glm::vec3 & scale) {
return postMultiply(glm::scale(glm::mat4(), scale));
}
MatrixStack & transform(const glm::mat4 & xfm) {
return postMultiply(xfm);
}
MatrixStack & preMultiply(const glm::mat4 & xfm) {
top() = xfm * top();
return *this;
}
MatrixStack & postMultiply(const glm::mat4 & xfm) {
top() *= xfm;
return *this;
}
// Remove the rotation component of a matrix. useful for billboarding
MatrixStack & unrotate() {
glm::quat inverse = glm::inverse(glm::quat_cast(top()));
top() = top() * glm::mat4_cast(inverse);
return *this;
}
// Remove the translation component of a matrix. useful for skyboxing
MatrixStack & untranslate() {
top()[3] = glm::vec4(0, 0, 0, 1);
return *this;
}
template <typename Function>
void withPush(Function f) {
size_t startingDepth = size();
push();
f();
pop();
assert(startingDepth = size());
}
template <typename Function>
void withIdentity(Function f) {
withPush([&] {
identity();
f();
});
}
static MatrixStack & projection() {
static MatrixStack projection;
return projection;
}
static MatrixStack & modelview() {
static MatrixStack modelview;
return modelview;
}
template <typename Function>
static void withPushAll(Function f) {
withPush(projection(), modelview(), f);
}
template <typename Function>
static void withIdentityAll(Function f) {
withPush(projection(), modelview(), [=] {
projection().identity();
modelview().identity();
f();
});
}
template <typename Function>
static void withPush(MatrixStack & stack, Function f) {
stack.withPush(f);
}
template <typename Function>
static void withPush(MatrixStack & stack1, MatrixStack & stack2, Function f) {
stack1.withPush([&]{
stack2.withPush(f);
});
}
template <typename Function>
static void withGlMatrices(Function f) {
// Push the current stack, and then copy the values out of OpenGL
withPushAll([&] {
// Fetch the current matrices out of GL stack
// FIXME, eliminate the usage of deprecated GL
MatrixStack & mv = MatrixStack::modelview();
MatrixStack & pr = MatrixStack::projection();
glm::mat4 & mvm = mv.top();
glGetFloatv(GL_MODELVIEW_MATRIX, &(mvm[0][0]));
glm::mat4 & prm = pr.top();
glGetFloatv(GL_PROJECTION_MATRIX, &(prm[0][0]));
f();
});
}
};
QMatrix4x4 fromGlm(const glm::mat4 & m);

View file

@ -9,32 +9,93 @@
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include <gpu/GPUConfig.h>
#include "TextRenderer.h"
#include "MatrixStack.h"
#include <gpu/GPUConfig.h>
#include <QApplication>
#include <QDesktopWidget>
#include <QFont>
#include <QPaintEngine>
#include <QtDebug>
#include <QString>
#include <QStringList>
#include <QWindow>
#include <QBuffer>
#include <QFile>
#include "TextRenderer.h"
#include "glm/glm.hpp"
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include "gpu/GLBackend.h"
#include "gpu/Stream.h"
#include "FontInconsolataMedium.h"
#include "FontRoboto.h"
#include "FontJackInput.h"
#include "FontTimeless.h"
namespace Shaders {
// Normally we could use 'enum class' to avoid namespace pollution,
// but we want easy conversion to GLuint
namespace Attributes {
enum {
Position = 0,
TexCoord = 1,
};
};
}
// the width/height of the cached glyph textures
const int IMAGE_SIZE = 512;
const char TextRenderer::SHADER_TEXT_VS[] = R"XXXX(#version 330
uniform mat4 Projection = mat4(1);
uniform mat4 ModelView = mat4(1);
layout(location = 0) in vec3 Position;
layout(location = 1) in vec2 TexCoord;
out vec2 vTexCoord;
void main() {
vTexCoord = TexCoord;
gl_Position = Projection * ModelView * vec4(Position, 1);
})XXXX";
const char TextRenderer::SHADER_TEXT_FS[] = R"XXXX(#version 330
uniform sampler2D Font;
uniform vec4 Color;
in vec2 vTexCoord;
out vec4 FragColor;
const float gamma = 2.6;
const float smoothing = 100.0;
void main() {
FragColor = vec4(1);
// retrieve signed distance
float sdf = texture(Font, vTexCoord).r;
// perform adaptive anti-aliasing of the edges
// The larger we're rendering, the less anti-aliasing we need
float s = smoothing * length(fwidth(vTexCoord));
float w = clamp( s, 0.0, 0.5);
float a = smoothstep(0.5 - w, 0.5 + w, sdf);
// gamma correction for linear attenuation
a = pow(a, 1.0 / gamma);
if (a < 0.001) {
discard;
}
// final color
FragColor = vec4(Color.rgb, a);
})XXXX";
const float TextRenderer::DTP_TO_METERS = 0.003528f;
const float TextRenderer::METERS_TO_DTP = 1.0f / TextRenderer::DTP_TO_METERS;
static uint qHash(const TextRenderer::Properties& key, uint seed = 0) {
// can be switched to qHash(key.font, seed) when we require Qt 5.3+
return qHash(key.font.family(), qHash(key.font.pointSize(), seed));
return qHash(key.font);
}
static bool operator==(const TextRenderer::Properties& p1, const TextRenderer::Properties& p2) {
@ -43,7 +104,7 @@ static bool operator==(const TextRenderer::Properties& p1, const TextRenderer::P
TextRenderer* TextRenderer::getInstance(const char* family, int pointSize, int weight, bool italic,
EffectType effect, int effectThickness, const QColor& color) {
Properties properties = { QFont(family, pointSize, weight, italic), effect, effectThickness, color };
Properties properties = { QString(family), effect, effectThickness, color };
TextRenderer*& instance = _instances[properties];
if (!instance) {
instance = new TextRenderer(properties);
@ -51,18 +112,43 @@ TextRenderer* TextRenderer::getInstance(const char* family, int pointSize, int w
return instance;
}
TextRenderer::TextRenderer(const Properties& properties) :
_effectType(properties.effect),
_effectThickness(properties.effectThickness),
_rowHeight(0),
_color(properties.color) {
QBuffer buffer;
if (properties.font == MONO_FONT_FAMILY) {
buffer.setData((const char*)SDFF_JACKINPUT, sizeof(SDFF_JACKINPUT));
} else if (properties.font == INCONSOLATA_FONT_FAMILY) {
buffer.setData((const char*)SDFF_INCONSOLATA_MEDIUM, sizeof(SDFF_INCONSOLATA_MEDIUM));
} else if (properties.font == SANS_FONT_FAMILY) {
buffer.setData((const char*)SDFF_ROBOTO, sizeof(SDFF_ROBOTO));
} else {
buffer.setData((const char*)SDFF_TIMELESS, sizeof(SDFF_TIMELESS));
}
buffer.open(QIODevice::ReadOnly);
read(buffer);
}
TextRenderer::~TextRenderer() {
glDeleteTextures(_allTextureIDs.size(), _allTextureIDs.constData());
}
int TextRenderer::calculateHeight(const char* str) {
const TextRenderer::Glyph & TextRenderer::getGlyph(const QChar & c) const {
if (!_glyphs.contains(c)) {
return _glyphs[QChar('?')];
}
return _glyphs[c];
};
int TextRenderer::calculateHeight(const char* str) const {
int maxHeight = 0;
for (const char* ch = str; *ch != 0; ch++) {
const Glyph& glyph = getGlyph(*ch);
if (glyph.textureID() == 0) {
continue;
}
if (glyph.bounds().height() > maxHeight) {
maxHeight = glyph.bounds().height();
}
@ -70,258 +156,371 @@ int TextRenderer::calculateHeight(const char* str) {
return maxHeight;
}
int TextRenderer::draw(int x, int y, const char* str, const glm::vec4& color) {
int compactColor = ((int(color.x * 255.0f) & 0xFF)) |
((int(color.y * 255.0f) & 0xFF) << 8) |
((int(color.z * 255.0f) & 0xFF) << 16) |
((int(color.w * 255.0f) & 0xFF) << 24);
int maxHeight = 0;
for (const char* ch = str; *ch != 0; ch++) {
const Glyph& glyph = getGlyph(*ch);
if (glyph.textureID() == 0) {
x += glyph.width();
continue;
}
if (glyph.bounds().height() > maxHeight) {
maxHeight = glyph.bounds().height();
}
//glBindTexture(GL_TEXTURE_2D, glyph.textureID());
int left = x + glyph.bounds().x();
int right = x + glyph.bounds().x() + glyph.bounds().width();
int bottom = y + glyph.bounds().y();
int top = y + glyph.bounds().y() + glyph.bounds().height();
glm::vec2 leftBottom = glm::vec2(float(left), float(bottom));
glm::vec2 rightTop = glm::vec2(float(right), float(top));
float scale = QApplication::desktop()->windowHandle()->devicePixelRatio() / IMAGE_SIZE;
float ls = glyph.location().x() * scale;
float rs = (glyph.location().x() + glyph.bounds().width()) * scale;
float bt = glyph.location().y() * scale;
float tt = (glyph.location().y() + glyph.bounds().height()) * scale;
const int NUM_COORDS_SCALARS_PER_GLYPH = 16;
float vertexBuffer[NUM_COORDS_SCALARS_PER_GLYPH] = { leftBottom.x, leftBottom.y, ls, bt,
rightTop.x, leftBottom.y, rs, bt,
rightTop.x, rightTop.y, rs, tt,
leftBottom.x, rightTop.y, ls, tt, };
const int NUM_COLOR_SCALARS_PER_GLYPH = 4;
int colorBuffer[NUM_COLOR_SCALARS_PER_GLYPH] = { compactColor, compactColor, compactColor, compactColor };
gpu::Buffer::Size offset = sizeof(vertexBuffer) * _numGlyphsBatched;
gpu::Buffer::Size colorOffset = sizeof(colorBuffer) * _numGlyphsBatched;
if ((offset + sizeof(vertexBuffer)) > _glyphsBuffer->getSize()) {
_glyphsBuffer->append(sizeof(vertexBuffer), (gpu::Buffer::Byte*) vertexBuffer);
_glyphsColorBuffer->append(sizeof(colorBuffer), (gpu::Buffer::Byte*) colorBuffer);
} else {
_glyphsBuffer->setSubData(offset, sizeof(vertexBuffer), (gpu::Buffer::Byte*) vertexBuffer);
_glyphsColorBuffer->setSubData(colorOffset, sizeof(colorBuffer), (gpu::Buffer::Byte*) colorBuffer);
}
_numGlyphsBatched++;
x += glyph.width();
}
// TODO: remove these calls once we move to a full batched rendering of the text, for now, one draw call per draw() function call
drawBatch();
clearBatch();
return maxHeight;
template <class T>
void readStream(QIODevice & in, T & t) {
in.read((char*)&t, sizeof(t));
}
int TextRenderer::computeWidth(char ch)
template <typename T, size_t N>
void readStream(T(&t)[N]) {
in.read((char*)t, N);
}
void TextRenderer::read(const QString & path) {
QFile file(path);
file.open(QFile::ReadOnly);
read(file);
}
void TextRenderer::Glyph::read(QIODevice & in) {
uint16_t charcode;
readStream(in, charcode);
c = charcode;
readStream(in, ul);
readStream(in, size);
readStream(in, offset);
readStream(in, d);
lr = ul + size;
}
void TextRenderer::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 metrics data
_glyphs.clear();
uint16_t count;
readStream(in, count);
for (int i = 0; i < count; ++i) {
Glyph g;
g.read(in);
_glyphs[g.c] = g;
}
// read image data
if (!_image.loadFromData(in.readAll(), "PNG")) {
qFatal("Failed to read SDFF image");
}
setupGL();
}
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 glmToRect(const glm::vec2 & pos, const glm::vec2 & size) {
QRectF result(pos.x, pos.y, size.x, size.y);
return result;
}
glm::uvec2 toGlm(const QSize & size) {
return glm::uvec2(size.width(), size.height());
}
QRectF TextRenderer::Glyph::bounds() const {
return glmToRect(offset, size);
}
QRectF TextRenderer::Glyph::textureBounds(const glm::vec2 & textureSize) const {
glm::vec2 pos = ul;
glm::vec2 size = lr - ul;
pos /= textureSize;
size /= textureSize;
return glmToRect(pos, size);
}
void TextRenderer::setupGL() {
_texture = TexturePtr(new QOpenGLTexture(_image, QOpenGLTexture::DontGenerateMipMaps));
_program = ProgramPtr(new QOpenGLShaderProgram());
if (!_program->create()) {
qFatal("Could not create text shader");
}
if (
!_program->addShaderFromSourceCode(QOpenGLShader::Vertex, SHADER_TEXT_VS) ||
!_program->addShaderFromSourceCode(QOpenGLShader::Fragment, SHADER_TEXT_FS) ||
!_program->link()) {
qFatal(_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 VertexArray());
_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);
glEnableVertexAttribArray(Shaders::Attributes::Position);
glVertexAttribPointer(Shaders::Attributes::Position, 3, GL_FLOAT, false, stride, nullptr);
glEnableVertexAttribArray(Shaders::Attributes::TexCoord);
glVertexAttribPointer(Shaders::Attributes::TexCoord, 2, GL_FLOAT, false, stride, offset);
_vao->release();
}
int TextRenderer::computeWidth(const QChar & ch) const
{
return getGlyph(ch).width();
}
int TextRenderer::computeWidth(const char* str)
int TextRenderer::computeWidth(const QString & str) const
{
int width = 0;
for (const char* ch = str; *ch != 0; ch++) {
width += computeWidth(*ch);
foreach(QChar c, str) {
width += computeWidth(c);
}
return width;
}
TextRenderer::TextRenderer(const Properties& properties) :
_font(properties.font),
_metrics(_font),
_effectType(properties.effect),
_effectThickness(properties.effectThickness),
_x(IMAGE_SIZE),
_y(IMAGE_SIZE),
_rowHeight(0),
_color(properties.color),
_glyphsBuffer(new gpu::Buffer()),
_glyphsColorBuffer(new gpu::Buffer()),
_glyphsStreamFormat(new gpu::Stream::Format()),
_glyphsStream(new gpu::BufferStream()),
_numGlyphsBatched(0)
{
_glyphsStreamFormat->setAttribute(gpu::Stream::POSITION, 0, gpu::Element(gpu::VEC2, gpu::FLOAT, gpu::XYZ), 0);
const int NUM_POS_COORDS = 2;
const int VERTEX_TEXCOORD_OFFSET = NUM_POS_COORDS * sizeof(float);
_glyphsStreamFormat->setAttribute(gpu::Stream::TEXCOORD, 0, gpu::Element(gpu::VEC2, gpu::FLOAT, gpu::UV), VERTEX_TEXCOORD_OFFSET);
_glyphsStreamFormat->setAttribute(gpu::Stream::COLOR, 1, gpu::Element(gpu::VEC4, gpu::UINT8, gpu::RGBA));
_glyphsStream->addBuffer(_glyphsBuffer, 0, _glyphsStreamFormat->getChannels().at(0)._stride);
_glyphsStream->addBuffer(_glyphsColorBuffer, 0, _glyphsStreamFormat->getChannels().at(1)._stride);
_font.setKerning(false);
int TextRenderer::computeWidth(const char * str) const {
int width = 0;
while (*str) {
width += computeWidth(*str++);
}
return width;
}
const Glyph& TextRenderer::getGlyph(char c) {
Glyph& glyph = _glyphs[c];
if (glyph.isValid()) {
return glyph;
}
// we use 'J' as a representative size for the solid block character
QChar ch = (c == SOLID_BLOCK_CHAR) ? QChar('J') : QChar(c);
QRect baseBounds = _metrics.boundingRect(ch);
if (baseBounds.isEmpty()) {
glyph = Glyph(0, QPoint(), QRect(), _metrics.width(ch));
return glyph;
}
// grow the bounds to account for effect, if any
if (_effectType == SHADOW_EFFECT) {
baseBounds.adjust(-_effectThickness, 0, 0, _effectThickness);
} else if (_effectType == OUTLINE_EFFECT) {
baseBounds.adjust(-_effectThickness, -_effectThickness, _effectThickness, _effectThickness);
}
// grow the bounds to account for antialiasing
baseBounds.adjust(-1, -1, 1, 1);
// adjust bounds for device pixel scaling
float ratio = QApplication::desktop()->windowHandle()->devicePixelRatio();
QRect bounds(baseBounds.x() * ratio, baseBounds.y() * ratio, baseBounds.width() * ratio, baseBounds.height() * ratio);
if (_x + bounds.width() > IMAGE_SIZE) {
// we can't fit it on the current row; move to next
_y += _rowHeight;
_x = _rowHeight = 0;
}
if (_y + bounds.height() > IMAGE_SIZE) {
// can't fit it on current texture; make a new one
glGenTextures(1, &_currentTextureID);
_x = _y = _rowHeight = 0;
glBindTexture(GL_TEXTURE_2D, _currentTextureID);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, IMAGE_SIZE, IMAGE_SIZE, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
_allTextureIDs.append(_currentTextureID);
} else {
glBindTexture(GL_TEXTURE_2D, _currentTextureID);
}
// render the glyph into an image and copy it into the texture
QImage image(bounds.width(), bounds.height(), QImage::Format_ARGB32);
if (c == SOLID_BLOCK_CHAR) {
image.fill(_color);
} else {
image.fill(0);
QPainter painter(&image);
QFont font = _font;
if (ratio == 1.0f) {
painter.setFont(_font);
} else {
QFont enlargedFont = _font;
enlargedFont.setPointSize(_font.pointSize() * ratio);
painter.setFont(enlargedFont);
}
if (_effectType == SHADOW_EFFECT) {
for (int i = 0; i < _effectThickness * ratio; i++) {
painter.drawText(-bounds.x() - 1 - i, -bounds.y() + 1 + i, ch);
}
} else if (_effectType == OUTLINE_EFFECT) {
QPainterPath path;
QFont font = _font;
font.setStyleStrategy(QFont::ForceOutline);
path.addText(-bounds.x() - 0.5, -bounds.y() + 0.5, font, ch);
QPen pen;
pen.setWidth(_effectThickness * ratio);
pen.setJoinStyle(Qt::RoundJoin);
pen.setCapStyle(Qt::RoundCap);
painter.setPen(pen);
painter.setRenderHint(QPainter::Antialiasing);
painter.drawPath(path);
}
painter.setPen(_color);
painter.drawText(-bounds.x(), -bounds.y(), ch);
}
glTexSubImage2D(GL_TEXTURE_2D, 0, _x, _y, bounds.width(), bounds.height(), GL_RGBA, GL_UNSIGNED_BYTE, image.constBits());
glyph = Glyph(_currentTextureID, QPoint(_x / ratio, _y / ratio), baseBounds, _metrics.width(ch));
_x += bounds.width();
_rowHeight = qMax(_rowHeight, bounds.height());
glBindTexture(GL_TEXTURE_2D, 0);
return glyph;
}
void TextRenderer::drawBatch() {
if (_numGlyphsBatched <= 0) {
return;
}
// TODO: Right now the drawBatch is called while calling the draw() function but in the future we'll need to apply the correct transform stack
/*
GLint matrixMode;
glGetIntegerv(GL_MATRIX_MODE, &matrixMode);
glMatrixMode(GL_MODELVIEW);
glPushMatrix();
glLoadIdentity();
glMatrixMode(GL_PROJECTION);
glPushMatrix();
glLoadIdentity();
*/
gpu::Batch batch;
glEnable(GL_TEXTURE_2D);
// TODO: Apply the correct font atlas texture, for now only one texture per TextRenderer so it should be good
glBindTexture(GL_TEXTURE_2D, _currentTextureID);
batch.setInputFormat(_glyphsStreamFormat);
batch.setInputStream(0, *_glyphsStream);
batch.draw(gpu::QUADS, _numGlyphsBatched * 4, 0);
gpu::GLBackend::renderBatch(batch);
glDisableClientState(GL_VERTEX_ARRAY);
glDisableClientState(GL_TEXTURE_COORD_ARRAY);
glDisableClientState(GL_COLOR_ARRAY);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindTexture(GL_TEXTURE_2D, 0);
glDisable(GL_TEXTURE_2D);
// TODO: Right now the drawBatch is called while calling the draw() function but in the future we'll need to apply the correct transform stack
/*
glPopMatrix();
glMatrixMode(GL_MODELVIEW);
glPopMatrix();
glMatrixMode(matrixMode);
*/
}
void TextRenderer::clearBatch() {
_numGlyphsBatched = 0;
int TextRenderer::computeWidth(char ch) const {
return computeWidth(QChar(ch));
}
QHash<TextRenderer::Properties, TextRenderer*> TextRenderer::_instances;
Glyph::Glyph(int textureID, const QPoint& location, const QRect& bounds, int width) :
_textureID(textureID), _location(location), _bounds(bounds), _width(width) {
float TextRenderer::drawString(
float x, float y,
const QString & str,
const glm::vec4& color,
float fontSize,
float maxWidth) {
// This is a hand made scale intended to match the previous scale of text in the application
float scale = 0.25f; // DTP_TO_METERS;
if (fontSize > 0.0) {
scale *= fontSize / _fontSize;
}
//bool wrap = false; // (maxWidth == maxWidth);
//if (wrap) {
// maxWidth /= scale;
//}
// Stores how far we've moved from the start of the string, in DTP units
static const float SPACE_ADVANCE = getGlyph(' ').d;
glm::vec2 advance;
MatrixStack::withGlMatrices([&] {
// Fetch the matrices out of GL
_program->bind();
_program->setUniformValue("Color", color.r, color.g, color.b, color.a);
_program->setUniformValue("Projection", fromGlm(MatrixStack::projection().top()));
_texture->bind();
_vao->bind();
MatrixStack & mv = MatrixStack::modelview();
MatrixStack & pr = MatrixStack::projection();
// scale the modelview into font units
mv.translate(glm::vec2(x, y)).translate(glm::vec2(0, scale * -_ascent)).scale(glm::vec3(scale, -scale, scale));
foreach(QString token, str.split(" ")) {
// float tokenWidth = measureWidth(token, fontSize);
// if (wrap && 0 != advance.x && (advance.x + tokenWidth) > maxWidth) {
// advance.x = 0;
// advance.y -= (_ascent + _descent);
// }
foreach(QChar c, token) {
if (QChar('\n') == c) {
advance.x = 0;
advance.y -= (_ascent + _descent);
return;
}
if (!_glyphs.contains(c)) {
c = QChar('?');
}
// get metrics for this character to speed up measurements
const Glyph & m = _glyphs[c];
//if (wrap && ((advance.x + m.d) > maxWidth)) {
// advance.x = 0;
// advance.y -= (_ascent + _descent);
//}
// 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);
_program->setUniformValue("ModelView", fromGlm(mv.top()));
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, (void*)(m.indexOffset));
});
advance.x += m.d;//+ m.offset.x;// font->getAdvance(m, mFontSize);
}
advance.x += SPACE_ADVANCE;
}
_vao->release();
_program->release();
});
return advance.x * scale;
}
#if 0
int TextRenderer::draw(int x, int y, const char* str, const glm::vec4& color) {
int compactColor = ((int(color.x * 255.0f) & 0xFF)) |
((int(color.y * 255.0f) & 0xFF) << 8) |
((int(color.z * 255.0f) & 0xFF) << 16) |
((int(color.w * 255.0f) & 0xFF) << 24);
int maxHeight = 0;
for (const char* ch = str; *ch != 0; ch++) {
const Glyph& glyph = getGlyph(*ch);
if (glyph.textureID() == 0) {
x += glyph.width();
continue;
}
if (glyph.bounds().height() > maxHeight) {
maxHeight = glyph.bounds().height();
}
//glBindTexture(GL_TEXTURE_2D, glyph.textureID());
int left = x + glyph.bounds().x();
int right = x + glyph.bounds().x() + glyph.bounds().width();
int bottom = y + glyph.bounds().y();
int top = y + glyph.bounds().y() + glyph.bounds().height();
glm::vec2 leftBottom = glm::vec2(float(left), float(bottom));
glm::vec2 rightTop = glm::vec2(float(right), float(top));
float scale = QApplication::desktop()->windowHandle()->devicePixelRatio() / IMAGE_SIZE;
float ls = glyph.location().x() * scale;
float rs = (glyph.location().x() + glyph.bounds().width()) * scale;
float bt = glyph.location().y() * scale;
float tt = (glyph.location().y() + glyph.bounds().height()) * scale;
const int NUM_COORDS_SCALARS_PER_GLYPH = 16;
float vertexBuffer[NUM_COORDS_SCALARS_PER_GLYPH] = { leftBottom.x, leftBottom.y, ls, bt,
rightTop.x, leftBottom.y, rs, bt,
rightTop.x, rightTop.y, rs, tt,
leftBottom.x, rightTop.y, ls, tt, };
const int NUM_COLOR_SCALARS_PER_GLYPH = 4;
int colorBuffer[NUM_COLOR_SCALARS_PER_GLYPH] = { compactColor, compactColor, compactColor, compactColor };
gpu::Buffer::Size offset = sizeof(vertexBuffer) * _numGlyphsBatched;
gpu::Buffer::Size colorOffset = sizeof(colorBuffer) * _numGlyphsBatched;
if ((offset + sizeof(vertexBuffer)) > _glyphsBuffer->getSize()) {
_glyphsBuffer->append(sizeof(vertexBuffer), (gpu::Buffer::Byte*) vertexBuffer);
_glyphsColorBuffer->append(sizeof(colorBuffer), (gpu::Buffer::Byte*) colorBuffer);
} else {
_glyphsBuffer->setSubData(offset, sizeof(vertexBuffer), (gpu::Buffer::Byte*) vertexBuffer);
_glyphsColorBuffer->setSubData(colorOffset, sizeof(colorBuffer), (gpu::Buffer::Byte*) colorBuffer);
}
_numGlyphsBatched++;
x += glyph.width();
}
// TODO: remove these calls once we move to a full batched rendering of the text, for now, one draw call per draw() function call
drawBatch();
clearBatch();
return maxHeight;
}
//_glyphsStreamFormat->setAttribute(gpu::Stream::POSITION, 0, gpu::Element(gpu::VEC2, gpu::FLOAT, gpu::XYZ), 0);
//const int NUM_POS_COORDS = 2;
//const int VERTEX_TEXCOORD_OFFSET = NUM_POS_COORDS * sizeof(float);
//_glyphsStreamFormat->setAttribute(gpu::Stream::TEXCOORD, 0, gpu::Element(gpu::VEC2, gpu::FLOAT, gpu::UV), VERTEX_TEXCOORD_OFFSET);
//_glyphsStreamFormat->setAttribute(gpu::Stream::COLOR, 1, gpu::Element(gpu::VEC4, gpu::UINT8, gpu::RGBA));
//_glyphsStream->addBuffer(_glyphsBuffer, 0, _glyphsStreamFormat->getChannels().at(0)._stride);
//_glyphsStream->addBuffer(_glyphsColorBuffer, 0, _glyphsStreamFormat->getChannels().at(1)._stride);
//_font.setKerning(false);
#endif

View file

@ -14,7 +14,7 @@
#include <gpu/GPUConfig.h>
#include <glm/glm.hpp>
#include <unordered_map>
#include <QColor>
#include <QFont>
#include <QFontMetrics>
@ -22,11 +22,15 @@
#include <QImage>
#include <QVector>
// FIXME, decouple from the GL headers
#include <QOpenGLShaderProgram>
#include <QOpenGLTexture>
#include <QOpenGLVertexArrayObject>
#include <QOpenGLBuffer>
#include <gpu/Resource.h>
#include <gpu/Stream.h>
// a special "character" that renders as a solid block
const char SOLID_BLOCK_CHAR = 127;
@ -45,19 +49,17 @@ const char SOLID_BLOCK_CHAR = 127;
#define INCONSOLATA_FONT_WEIGHT QFont::Bold
#endif
class Glyph;
class TextRenderer {
public:
enum EffectType { NO_EFFECT, SHADOW_EFFECT, OUTLINE_EFFECT };
class Properties {
public:
QFont font;
EffectType effect;
int effectThickness;
QColor color;
QString font;
EffectType effect;
int effectThickness;
QColor color;
};
static TextRenderer* getInstance(const char* family, int pointSize = -1, int weight = -1, bool italic = false,
@ -65,30 +67,72 @@ public:
~TextRenderer();
const QFontMetrics& metrics() const { return _metrics; }
// returns the height of the tallest character
int calculateHeight(const char* str);
int calculateHeight(const char * str) const;
int calculateHeight(const QString & str) const;
int computeWidth(char ch) const;
int computeWidth(const QChar & ch) const;
int computeWidth(const QString & str) const;
int computeWidth(const char * str) const;
// also returns the height of the tallest character
int draw(int x, int y, const char* str, const glm::vec4& color);
int computeWidth(char ch);
int computeWidth(const char* str);
inline int draw(int x, int y, const char* str,
const glm::vec4& color = glm::vec4(-1),
float fontSize = -1,
float maxWidth = -1
) {
return drawString(x, y, QString(str), color, fontSize, maxWidth);
}
float drawString(
float x, float y,
const QString & str,
const glm::vec4& color = glm::vec4(-1),
float fontSize = -1,
float maxWidth = -1);
void drawBatch();
void clearBatch();
private:
static QHash<Properties, TextRenderer*> _instances;
// Allow fonts to scale appropriately when rendered in a
// scene where units are meters
static const float DTP_TO_METERS; // = 0.003528f;
static const float METERS_TO_DTP; // = 1.0 / DTP_TO_METERS;
static const char SHADER_TEXT_FS[];
static const char SHADER_TEXT_VS[];
TextRenderer(const Properties& properties);
const Glyph& getGlyph(char c);
// stores the font metrics for a single character
struct Glyph {
QChar c;
glm::vec2 ul;
glm::vec2 lr;
glm::vec2 size;
glm::vec2 offset;
float d; // xadvance - adjusts character positioning
size_t indexOffset;
// the font to render
QFont _font;
// the font metrics
QFontMetrics _metrics;
int width() const {
return size.x;
}
QRectF bounds() const;
QRectF textureBounds(const glm::vec2 & textureSize) const;
void read(QIODevice & in);
};
using TexturePtr = QSharedPointer < QOpenGLTexture >;
using VertexArrayPtr = QSharedPointer< QOpenGLVertexArrayObject >;
using ProgramPtr = QSharedPointer < QOpenGLShaderProgram >;
using BufferPtr = QSharedPointer < QOpenGLBuffer >;
const Glyph& getGlyph(const QChar & c) const;
// the type of effect to apply
EffectType _effectType;
@ -97,58 +141,40 @@ private:
int _effectThickness;
// maps characters to cached glyph info
QHash<char, Glyph> _glyphs;
QHash<QChar, Glyph> _glyphs;
// the id of the glyph texture to which we're currently writing
GLuint _currentTextureID;
// the position within the current glyph texture
int _x, _y;
int _pointSize;
// the height of the current row of characters
int _rowHeight;
// the list of all texture ids for which we're responsible
QVector<GLuint> _allTextureIDs;
// text color
QColor _color;
// Graphics Buffer containing the current accumulated glyphs to render
gpu::BufferPointer _glyphsBuffer;
gpu::BufferPointer _glyphsColorBuffer;
gpu::Stream::FormatPointer _glyphsStreamFormat;
gpu::BufferStreamPointer _glyphsStream;
int _numGlyphsBatched;
static QHash<Properties, TextRenderer*> _instances;
QString _family;
float _fontSize{ 12 };
float _leading{ 0 };
float _ascent{ 0 };
float _descent{ 0 };
float _spaceWidth{ 0 };
BufferPtr _vertices;
BufferPtr _indices;
TexturePtr _texture;
VertexArrayPtr _vao;
QImage _image;
ProgramPtr _program;
// Parse the signed distance field font file
void read(const QString & path);
void read(QIODevice & path);
// Initialize the OpenGL structures
void setupGL();
};
class Glyph {
public:
Glyph(int textureID = 0, const QPoint& location = QPoint(), const QRect& bounds = QRect(), int width = 0);
GLuint textureID() const { return _textureID; }
const QPoint& location () const { return _location; }
const QRect& bounds() const { return _bounds; }
int width () const { return _width; }
bool isValid() { return _width != 0; }
private:
// the id of the OpenGL texture containing the glyph
GLuint _textureID;
// the location of the character within the texture
QPoint _location;
// the bounds of the character
QRect _bounds;
// the width of the character (distance to next, as opposed to bounds width)
int _width;
};
#endif // hifi_TextRenderer_h