From 711506e05507d4633843f0689e8068fcac5b18df Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Sun, 1 Feb 2015 14:15:34 -0800 Subject: [PATCH] Moving implementation details out of header --- libraries/render-utils/src/TextRenderer.cpp | 452 ++++++++++---------- libraries/render-utils/src/TextRenderer.h | 97 +---- 2 files changed, 251 insertions(+), 298 deletions(-) diff --git a/libraries/render-utils/src/TextRenderer.cpp b/libraries/render-utils/src/TextRenderer.cpp index 85951197ff..8b6a3e2964 100644 --- a/libraries/render-utils/src/TextRenderer.cpp +++ b/libraries/render-utils/src/TextRenderer.cpp @@ -20,6 +20,12 @@ #include #include +// FIXME, decouple from the GL headers +#include +#include +#include +#include + #include #include @@ -30,6 +36,7 @@ #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 @@ -41,7 +48,22 @@ namespace Shaders { }; } -const char TextRenderer::SHADER_TEXT_VS[] = R"XXXX(#version 330 +// 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(T(&t)[N]) { + in.read((char*)t, N); +} + +glm::uvec2 toGlm(const QSize & size) { + return glm::uvec2(size.width(), size.height()); +} + +const char SHADER_TEXT_VS[] = R"XXXX(#version 330 uniform mat4 Projection = mat4(1); uniform mat4 ModelView = mat4(1); @@ -56,10 +78,11 @@ void main() { gl_Position = Projection * ModelView * vec4(Position, 1); })XXXX"; -const char TextRenderer::SHADER_TEXT_FS[] = R"XXXX(#version 330 +const char SHADER_TEXT_FS[] = R"XXXX(#version 330 uniform sampler2D Font; -uniform vec4 Color; +uniform vec4 Color = vec4(1); +uniform bool Outline = false; in vec2 vTexCoord; out vec4 FragColor; @@ -72,7 +95,9 @@ void main() { // retrieve signed distance float sdf = texture(Font, vTexCoord).r; - + if (Outline && (sdf > 0.6)) { + sdf = 1.0 - sdf; + } // perform adaptive anti-aliasing of the edges // The larger we're rendering, the less anti-aliasing we need float s = smoothing * length(fwidth(vTexCoord)); @@ -82,7 +107,7 @@ void main() { // gamma correction for linear attenuation a = pow(a, 1.0 / gamma); - if (a < 0.001) { + if (a < 0.01) { discard; } @@ -90,101 +115,96 @@ void main() { 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); -} +// 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; -static bool operator==(const TextRenderer::Properties& p1, const TextRenderer::Properties& p2) { - return p1.font == p2.font && p1.effect == p2.effect && p1.effectThickness == p2.effectThickness && p1.color == p2.color; -} - -TextRenderer* TextRenderer::getInstance(const char* family, int pointSize, int weight, bool italic, - EffectType effect, int effectThickness, const QColor& color) { - Properties properties = { QString(family), effect, effectThickness, color }; - TextRenderer*& instance = _instances[properties]; - if (!instance) { - instance = new TextRenderer(properties); - } - 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)); + int width() const { + return size.x; } - buffer.open(QIODevice::ReadOnly); - read(buffer); + 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; -TextRenderer::~TextRenderer() { -} +class Font { +public: + using TexturePtr = QSharedPointer < QOpenGLTexture >; + using VertexArrayPtr = QSharedPointer< QOpenGLVertexArrayObject >; + using ProgramPtr = QSharedPointer < QOpenGLShaderProgram >; + using BufferPtr = QSharedPointer < QOpenGLBuffer >; -const TextRenderer::Glyph & TextRenderer::getGlyph(const QChar & c) const { + // maps characters to cached glyph info + QHash _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 & Font::getGlyph(const QChar & c) const; + void read(QIODevice & path); + // Initialize the OpenGL structures + void setupGL(); + + glm::vec2 drawString( + float x, float y, + const QString & str, + const glm::vec4& color, + float scale, + TextRenderer::EffectType effectType, + float maxWidth); +}; + + +const Glyph & Font::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.bounds().height() > maxHeight) { - maxHeight = glyph.bounds().height(); - } - } - return maxHeight; -} - - -template -void readStream(QIODevice & in, T & t) { - in.read((char*)&t, sizeof(t)); -} - -template -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) { +void Font::read(QIODevice & in) { uint8_t header[4]; readStream(in, header); if (memcmp(header, "SDFF", 4)) { @@ -211,23 +231,33 @@ void TextRenderer::read(QIODevice & in) { readStream(in, _descent); readStream(in, _spaceWidth); _fontSize = _ascent + _descent; + _rowHeight = _fontSize + _descent / 2; - // read metrics data - _glyphs.clear(); - + // Read character count uint16_t count; readStream(in, count); - - for (int i = 0; i < count; ++i) { - Glyph g; + // read metrics data for each character + QVector glyphs(count); + // std::for_each instead of Qt foreach because we need non-const references + std::for_each(glyphs.begin(), glyphs.end(), [&](Glyph & g){ g.read(in); - _glyphs[g.c] = g; - } + }); // 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; + }; + setupGL(); } @@ -259,23 +289,16 @@ QRectF glmToRect(const glm::vec2 & pos, const glm::vec2 & size) { return result; } -glm::uvec2 toGlm(const QSize & size) { - return glm::uvec2(size.width(), size.height()); -} -QRectF TextRenderer::Glyph::bounds() const { +QRectF 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); +QRectF Glyph::textureBounds(const glm::vec2 & textureSize) const { + return glmToRect(texOffset, texSize); } -void TextRenderer::setupGL() { +void Font::setupGL() { _texture = TexturePtr(new QOpenGLTexture(_image, QOpenGLTexture::DontGenerateMipMaps)); _program = ProgramPtr(new QOpenGLShaderProgram()); if (!_program->create()) { @@ -337,46 +360,19 @@ void TextRenderer::setupGL() { _vao->release(); } -int TextRenderer::computeWidth(const QChar & ch) const -{ - return getGlyph(ch).width(); -} - -int TextRenderer::computeWidth(const QString & str) const -{ - int width = 0; - foreach(QChar c, str) { - width += computeWidth(c); - } - return width; -} - -int TextRenderer::computeWidth(const char * str) const { - int width = 0; - while (*str) { - width += computeWidth(*str++); - } - return width; -} - -int TextRenderer::computeWidth(char ch) const { - return computeWidth(QChar(ch)); -} - -QHash TextRenderer::_instances; - -float TextRenderer::drawString( +glm::vec2 Font::drawString( float x, float y, const QString & str, const glm::vec4& color, - float fontSize, + float scale, + TextRenderer::EffectType effectType, 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; - } + //float scale = 0.25f; // DTP_TO_METERS; + //if (fontSize > 0.0) { + // scale *= fontSize / _fontSize; + //} //bool wrap = false; // (maxWidth == maxWidth); //if (wrap) { @@ -385,32 +381,35 @@ float TextRenderer::drawString( // 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; + glm::vec2 advance(0, -_ascent); 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())); + if (effectType == TextRenderer::OUTLINE_EFFECT) { + _program->setUniformValue("Outline", true); + } _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)); + mv.translate(glm::vec2(x, y)).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); + // advance.y -= _rowHeight; // } foreach(QChar c, token) { if (QChar('\n') == c) { advance.x = 0; - advance.y -= (_ascent + _descent); - return; + advance.y -= _rowHeight; + continue; } if (!_glyphs.contains(c)) { @@ -421,7 +420,7 @@ float TextRenderer::drawString( const Glyph & m = _glyphs[c]; //if (wrap && ((advance.x + m.d) > maxWidth)) { // advance.x = 0; - // advance.y -= (_ascent + _descent); + // advance.y -= _rowHeight; //} // We create an offset vec2 to hold the local offset of this character @@ -444,83 +443,106 @@ float TextRenderer::drawString( _program->release(); }); - return advance.x * scale; + 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(Properties{ QString(family), pointSize, effect, effectThickness, color }); +} + +template +Font * loadFont(T (&t)[N]) { + Font * result = new Font(); + QBuffer buffer; + buffer.setData((const char*)t, N); + buffer.open(QBuffer::ReadOnly); + result->read(buffer); + return result; +} + +static QHash LOADED_FONTS; + +Font * loadFont(const QString & family) { + if (!LOADED_FONTS.contains(family)) { + if (family == MONO_FONT_FAMILY) { + LOADED_FONTS[family] = loadFont(SDFF_JACKINPUT); + } else if (family == INCONSOLATA_FONT_FAMILY) { + LOADED_FONTS[family] = loadFont(SDFF_INCONSOLATA_MEDIUM); + } else if (family == SANS_FONT_FAMILY) { + LOADED_FONTS[family] = loadFont(SDFF_ROBOTO); + } else { + if (!LOADED_FONTS.contains(SERIF_FONT_FAMILY)) { + LOADED_FONTS[SERIF_FONT_FAMILY] = loadFont(SDFF_TIMELESS); + } + LOADED_FONTS[family] = LOADED_FONTS[SERIF_FONT_FAMILY]; + } + } + return LOADED_FONTS[family]; +} + +TextRenderer::TextRenderer(const Properties& properties) : + _effectType(properties.effect), + _pointSize(properties.pointSize), + _effectThickness(properties.effectThickness), + _color(properties.color), + _font(loadFont(properties.font)) +{ + +} + +TextRenderer::~TextRenderer() { } -#if 0 +int TextRenderer::computeWidth(const QChar & ch) const { + //return getGlyph(ch).width(); + return 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(); +int TextRenderer::computeWidth(const QString & str) const { + int width = 0; + foreach(QChar c, str) { + width += computeWidth(c); } + return 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(); +int TextRenderer::computeWidth(const char * str) const { + int width = 0; + while (*str) { + width += computeWidth(*str++); + } + return width; +} +int TextRenderer::computeWidth(char ch) const { + return computeWidth(QChar(ch)); +} + +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.bounds().height() > maxHeight) { + // maxHeight = glyph.bounds().height(); + // } + //} 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)); +float TextRenderer::drawString( + float x, float y, + const QString & str, + const glm::vec4& color, + float maxWidth) { + glm::vec4 actualColor(color); + if (actualColor.r < 0) { + actualColor = glm::vec4(_color.redF(), _color.greenF(), _color.blueF(), 1.0); + } + return _font->drawString(x, y, str, actualColor, (_pointSize / DEFAULT_POINT_SIZE) * 0.25f, _effectType, maxWidth).x; +} -//_glyphsStream->addBuffer(_glyphsBuffer, 0, _glyphsStreamFormat->getChannels().at(0)._stride); -//_glyphsStream->addBuffer(_glyphsColorBuffer, 0, _glyphsStreamFormat->getChannels().at(1)._stride); - -//_font.setKerning(false); -#endif diff --git a/libraries/render-utils/src/TextRenderer.h b/libraries/render-utils/src/TextRenderer.h index cf6602f339..e06520d4e7 100644 --- a/libraries/render-utils/src/TextRenderer.h +++ b/libraries/render-utils/src/TextRenderer.h @@ -22,12 +22,6 @@ #include #include -// FIXME, decouple from the GL headers -#include -#include -#include -#include - #include #include @@ -37,6 +31,9 @@ const char SOLID_BLOCK_CHAR = 127; // the standard sans serif font family #define SANS_FONT_FAMILY "Helvetica" +// the standard sans serif font family +#define SERIF_FONT_FAMILY "Timeless" + // the standard mono font family #define MONO_FONT_FAMILY "Courier" @@ -57,12 +54,13 @@ public: class Properties { public: QString font; + float pointSize; EffectType effect; int effectThickness; QColor color; }; - static TextRenderer* getInstance(const char* family, int pointSize = -1, int weight = -1, bool italic = false, + static TextRenderer* getInstance(const char* family, float pointSize = -1, int weight = -1, bool italic = false, EffectType effect = NO_EFFECT, int effectThickness = 1, const QColor& color = QColor(255, 255, 255)); ~TextRenderer(); @@ -80,100 +78,33 @@ public: // also returns the height of the tallest character 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); + return drawString(x, y, QString(str), color, 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 _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); - // 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; - - 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; + const EffectType _effectType; // the thickness of the effect - int _effectThickness; - - // maps characters to cached glyph info - QHash _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; - + const int _effectThickness; + + const float _pointSize; + // text color - QColor _color; + const QColor _color; - 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(); + friend class Font; + Font * _font; };