mirror of
https://github.com/overte-org/overte.git
synced 2025-08-09 23:14:34 +02:00
Merge pull request #5349 from jherico/marge
Working on text rendering overlays
This commit is contained in:
commit
3ea348b73b
24 changed files with 738 additions and 1297 deletions
23
interface/resources/qml/TextOverlayElement.qml
Normal file
23
interface/resources/qml/TextOverlayElement.qml
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import Hifi 1.0
|
||||||
|
import QtQuick 2.3
|
||||||
|
import QtQuick.Controls 1.2
|
||||||
|
|
||||||
|
TextOverlayElement {
|
||||||
|
id: root
|
||||||
|
Rectangle {
|
||||||
|
color: root.backgroundColor
|
||||||
|
anchors.fill: parent
|
||||||
|
Text {
|
||||||
|
x: root.leftMargin
|
||||||
|
y: root.topMargin
|
||||||
|
id: text
|
||||||
|
objectName: "textElement"
|
||||||
|
text: root.text
|
||||||
|
color: root.textColor
|
||||||
|
font.family: root.fontFamily
|
||||||
|
font.pixelSize: root.fontSize
|
||||||
|
lineHeightMode: Text.FixedHeight
|
||||||
|
lineHeight: root.lineHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -92,7 +92,6 @@
|
||||||
#include <SettingHandle.h>
|
#include <SettingHandle.h>
|
||||||
#include <SimpleAverage.h>
|
#include <SimpleAverage.h>
|
||||||
#include <SoundCache.h>
|
#include <SoundCache.h>
|
||||||
#include <TextRenderer.h>
|
|
||||||
#include <Tooltip.h>
|
#include <Tooltip.h>
|
||||||
#include <UserActivityLogger.h>
|
#include <UserActivityLogger.h>
|
||||||
#include <UUID.h>
|
#include <UUID.h>
|
||||||
|
|
|
@ -23,7 +23,6 @@
|
||||||
|
|
||||||
#include <ByteCountCoding.h>
|
#include <ByteCountCoding.h>
|
||||||
#include <SharedUtil.h>
|
#include <SharedUtil.h>
|
||||||
#include <TextRenderer.h>
|
|
||||||
|
|
||||||
#include "InterfaceConfig.h"
|
#include "InterfaceConfig.h"
|
||||||
#include "world.h"
|
#include "world.h"
|
||||||
|
@ -79,27 +78,6 @@ const glm::vec3 randVector() {
|
||||||
return glm::vec3(randFloat() - 0.5f, randFloat() - 0.5f, randFloat() - 0.5f) * 2.0f;
|
return glm::vec3(randFloat() - 0.5f, randFloat() - 0.5f, randFloat() - 0.5f) * 2.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
static TextRenderer* textRenderer(int mono) {
|
|
||||||
static TextRenderer* monoRenderer = TextRenderer::getInstance(MONO_FONT_FAMILY);
|
|
||||||
static TextRenderer* proportionalRenderer = TextRenderer::getInstance(SANS_FONT_FAMILY,
|
|
||||||
-1, -1, false, TextRenderer::SHADOW_EFFECT);
|
|
||||||
static TextRenderer* inconsolataRenderer = TextRenderer::getInstance(INCONSOLATA_FONT_FAMILY, -1, INCONSOLATA_FONT_WEIGHT,
|
|
||||||
false);
|
|
||||||
switch (mono) {
|
|
||||||
case 1:
|
|
||||||
return monoRenderer;
|
|
||||||
case 2:
|
|
||||||
return inconsolataRenderer;
|
|
||||||
case 0:
|
|
||||||
default:
|
|
||||||
return proportionalRenderer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int widthText(float scale, int mono, char const* string) {
|
|
||||||
return textRenderer(mono)->computeExtent(string).x; // computeWidth(string) * (scale / 0.10);
|
|
||||||
}
|
|
||||||
|
|
||||||
void renderCollisionOverlay(int width, int height, float magnitude, float red, float blue, float green) {
|
void renderCollisionOverlay(int width, int height, float magnitude, float red, float blue, float green) {
|
||||||
const float MIN_VISIBLE_COLLISION = 0.01f;
|
const float MIN_VISIBLE_COLLISION = 0.01f;
|
||||||
if (magnitude > MIN_VISIBLE_COLLISION) {
|
if (magnitude > MIN_VISIBLE_COLLISION) {
|
||||||
|
|
|
@ -22,7 +22,6 @@ float randFloat();
|
||||||
const glm::vec3 randVector();
|
const glm::vec3 randVector();
|
||||||
|
|
||||||
void renderWorldBox(gpu::Batch& batch);
|
void renderWorldBox(gpu::Batch& batch);
|
||||||
int widthText(float scale, int mono, char const* string);
|
|
||||||
|
|
||||||
void renderCollisionOverlay(int width, int height, float magnitude, float red = 0, float blue = 0, float green = 0);
|
void renderCollisionOverlay(int width, int height, float magnitude, float red = 0, float blue = 0, float green = 0);
|
||||||
|
|
||||||
|
|
|
@ -281,7 +281,7 @@ enum TextRendererType {
|
||||||
|
|
||||||
static TextRenderer3D* textRenderer(TextRendererType type) {
|
static TextRenderer3D* textRenderer(TextRendererType type) {
|
||||||
static TextRenderer3D* chatRenderer = TextRenderer3D::getInstance(SANS_FONT_FAMILY, -1,
|
static TextRenderer3D* chatRenderer = TextRenderer3D::getInstance(SANS_FONT_FAMILY, -1,
|
||||||
false, TextRenderer3D::SHADOW_EFFECT);
|
false, SHADOW_EFFECT);
|
||||||
static TextRenderer3D* displayNameRenderer = TextRenderer3D::getInstance(SANS_FONT_FAMILY);
|
static TextRenderer3D* displayNameRenderer = TextRenderer3D::getInstance(SANS_FONT_FAMILY);
|
||||||
|
|
||||||
switch(type) {
|
switch(type) {
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
|
|
||||||
#include <QtCore/QTimer>
|
#include <QtCore/QTimer>
|
||||||
|
|
||||||
|
#include <gpu/GPUConfig.h>
|
||||||
#include <AccountManager.h>
|
#include <AccountManager.h>
|
||||||
#include <AddressManager.h>
|
#include <AddressManager.h>
|
||||||
#include <AnimationHandle.h>
|
#include <AnimationHandle.h>
|
||||||
|
@ -31,7 +32,7 @@
|
||||||
#include <PerfStat.h>
|
#include <PerfStat.h>
|
||||||
#include <ShapeCollider.h>
|
#include <ShapeCollider.h>
|
||||||
#include <SharedUtil.h>
|
#include <SharedUtil.h>
|
||||||
#include <TextRenderer.h>
|
#include <TextRenderer3D.h>
|
||||||
#include <UserActivityLogger.h>
|
#include <UserActivityLogger.h>
|
||||||
|
|
||||||
#include "devices/Faceshift.h"
|
#include "devices/Faceshift.h"
|
||||||
|
|
|
@ -55,15 +55,15 @@ void ImageOverlay::render(RenderArgs* args) {
|
||||||
_isLoaded = true;
|
_isLoaded = true;
|
||||||
_texture = DependencyManager::get<TextureCache>()->getTexture(_imageURL);
|
_texture = DependencyManager::get<TextureCache>()->getTexture(_imageURL);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we are not visible or loaded, return. If we are trying to render an
|
// If we are not visible or loaded, return. If we are trying to render an
|
||||||
// image but the texture hasn't loaded, return.
|
// image but the texture hasn't loaded, return.
|
||||||
if (!_visible || !_isLoaded || (_renderImage && !_texture->isLoaded())) {
|
if (!_visible || !_isLoaded || (_renderImage && !_texture->isLoaded())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto geometryCache = DependencyManager::get<GeometryCache>();
|
||||||
gpu::Batch& batch = *args->_batch;
|
gpu::Batch& batch = *args->_batch;
|
||||||
|
geometryCache->useSimpleDrawPipeline(batch);
|
||||||
if (_renderImage) {
|
if (_renderImage) {
|
||||||
batch.setResourceTexture(0, _texture->getGPUTexture());
|
batch.setResourceTexture(0, _texture->getGPUTexture());
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -10,15 +10,72 @@
|
||||||
|
|
||||||
// include this before QGLWidget, which includes an earlier version of OpenGL
|
// include this before QGLWidget, which includes an earlier version of OpenGL
|
||||||
#include "InterfaceConfig.h"
|
#include "InterfaceConfig.h"
|
||||||
|
#include "Application.h"
|
||||||
#include "TextOverlay.h"
|
#include "TextOverlay.h"
|
||||||
|
#include "OffscreenUi.h"
|
||||||
|
#include "text/FontFamilies.h"
|
||||||
|
#include <gpu/GLBackend.h>
|
||||||
#include <DependencyManager.h>
|
#include <DependencyManager.h>
|
||||||
#include <GeometryCache.h>
|
#include <GeometryCache.h>
|
||||||
|
#include <TextureCache.h>
|
||||||
|
#include <GLMHelpers.h>
|
||||||
#include <RegisteredMetaTypes.h>
|
#include <RegisteredMetaTypes.h>
|
||||||
#include <SharedUtil.h>
|
#include <SharedUtil.h>
|
||||||
#include <TextRenderer.h>
|
#include <ViewFrustum.h>
|
||||||
|
#include <QQuickItem>
|
||||||
|
|
||||||
|
#define TEXT_OVERLAY_PROPERTY(type, name, initialValue) \
|
||||||
|
Q_PROPERTY(type name READ name WRITE set##name NOTIFY name##Changed) \
|
||||||
|
public: \
|
||||||
|
type name() { return _##name; }; \
|
||||||
|
void set##name(const type& name) { \
|
||||||
|
if (name != _##name) { \
|
||||||
|
_##name = name; \
|
||||||
|
emit name##Changed(); \
|
||||||
|
} \
|
||||||
|
} \
|
||||||
|
private: \
|
||||||
|
type _##name{ initialValue };
|
||||||
|
|
||||||
|
|
||||||
|
class TextOverlayElement : public QQuickItem {
|
||||||
|
Q_OBJECT
|
||||||
|
HIFI_QML_DECL
|
||||||
|
private:
|
||||||
|
TEXT_OVERLAY_PROPERTY(QString, text, "")
|
||||||
|
TEXT_OVERLAY_PROPERTY(QString, fontFamily, SANS_FONT_FAMILY)
|
||||||
|
TEXT_OVERLAY_PROPERTY(QString, textColor, "#ffffffff")
|
||||||
|
TEXT_OVERLAY_PROPERTY(QString, backgroundColor, "#B2000000")
|
||||||
|
TEXT_OVERLAY_PROPERTY(qreal, fontSize, 18)
|
||||||
|
TEXT_OVERLAY_PROPERTY(qreal, lineHeight, 18)
|
||||||
|
TEXT_OVERLAY_PROPERTY(qreal, leftMargin, 0)
|
||||||
|
TEXT_OVERLAY_PROPERTY(qreal, topMargin, 0)
|
||||||
|
|
||||||
|
public:
|
||||||
|
TextOverlayElement(QQuickItem* parent = nullptr) : QQuickItem(parent) {
|
||||||
|
}
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void textChanged();
|
||||||
|
void fontFamilyChanged();
|
||||||
|
void fontSizeChanged();
|
||||||
|
void lineHeightChanged();
|
||||||
|
void leftMarginChanged();
|
||||||
|
void topMarginChanged();
|
||||||
|
void textColorChanged();
|
||||||
|
void backgroundColorChanged();
|
||||||
|
};
|
||||||
|
|
||||||
|
HIFI_QML_DEF(TextOverlayElement)
|
||||||
|
|
||||||
|
QString toQmlColor(const glm::vec4& v) {
|
||||||
|
QString templat("#%1%2%3%4");
|
||||||
|
return templat.
|
||||||
|
arg((int)(v.a * 255), 2, 16, QChar('0')).
|
||||||
|
arg((int)(v.r * 255), 2, 16, QChar('0')).
|
||||||
|
arg((int)(v.g * 255), 2, 16, QChar('0')).
|
||||||
|
arg((int)(v.b * 255), 2, 16, QChar('0'));
|
||||||
|
}
|
||||||
|
|
||||||
TextOverlay::TextOverlay() :
|
TextOverlay::TextOverlay() :
|
||||||
_backgroundColor(DEFAULT_BACKGROUND_COLOR),
|
_backgroundColor(DEFAULT_BACKGROUND_COLOR),
|
||||||
|
@ -27,7 +84,20 @@ TextOverlay::TextOverlay() :
|
||||||
_topMargin(DEFAULT_MARGIN),
|
_topMargin(DEFAULT_MARGIN),
|
||||||
_fontSize(DEFAULT_FONTSIZE)
|
_fontSize(DEFAULT_FONTSIZE)
|
||||||
{
|
{
|
||||||
_textRenderer = TextRenderer::getInstance(SANS_FONT_FAMILY, _fontSize, DEFAULT_FONT_WEIGHT);
|
|
||||||
|
qApp->postLambdaEvent([=] {
|
||||||
|
static std::once_flag once;
|
||||||
|
std::call_once(once, [] {
|
||||||
|
TextOverlayElement::registerType();
|
||||||
|
});
|
||||||
|
auto offscreenUi = DependencyManager::get<OffscreenUi>();
|
||||||
|
TextOverlayElement::show([=](QQmlContext* context, QObject* object) {
|
||||||
|
_qmlElement = static_cast<TextOverlayElement*>(object);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
while (!_qmlElement) {
|
||||||
|
QThread::msleep(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TextOverlay::TextOverlay(const TextOverlay* textOverlay) :
|
TextOverlay::TextOverlay(const TextOverlay* textOverlay) :
|
||||||
|
@ -39,11 +109,21 @@ TextOverlay::TextOverlay(const TextOverlay* textOverlay) :
|
||||||
_topMargin(textOverlay->_topMargin),
|
_topMargin(textOverlay->_topMargin),
|
||||||
_fontSize(textOverlay->_fontSize)
|
_fontSize(textOverlay->_fontSize)
|
||||||
{
|
{
|
||||||
_textRenderer = TextRenderer::getInstance(SANS_FONT_FAMILY, _fontSize, DEFAULT_FONT_WEIGHT);
|
qApp->postLambdaEvent([=] {
|
||||||
|
auto offscreenUi = DependencyManager::get<OffscreenUi>();
|
||||||
|
TextOverlayElement::show([this](QQmlContext* context, QObject* object) {
|
||||||
|
_qmlElement = static_cast<TextOverlayElement*>(object);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
while (!_qmlElement) {
|
||||||
|
QThread::sleep(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TextOverlay::~TextOverlay() {
|
TextOverlay::~TextOverlay() {
|
||||||
delete _textRenderer;
|
if (_qmlElement) {
|
||||||
|
_qmlElement->deleteLater();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
xColor TextOverlay::getBackgroundColor() {
|
xColor TextOverlay::getBackgroundColor() {
|
||||||
|
@ -65,45 +145,34 @@ xColor TextOverlay::getBackgroundColor() {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void TextOverlay::render(RenderArgs* args) {
|
void TextOverlay::render(RenderArgs* args) {
|
||||||
if (!_visible) {
|
if (_visible != _qmlElement->isVisible()) {
|
||||||
return; // do nothing if we're not visible
|
_qmlElement->setVisible(_visible);
|
||||||
}
|
}
|
||||||
|
float pulseLevel = updatePulse();
|
||||||
|
static float _oldPulseLevel = 0.0f;
|
||||||
|
if (pulseLevel != _oldPulseLevel) {
|
||||||
|
|
||||||
const float MAX_COLOR = 255.0f;
|
}
|
||||||
xColor backgroundColor = getBackgroundColor();
|
|
||||||
glm::vec4 quadColor(backgroundColor.red / MAX_COLOR, backgroundColor.green / MAX_COLOR, backgroundColor.blue / MAX_COLOR,
|
|
||||||
getBackgroundAlpha());
|
|
||||||
|
|
||||||
int left = _bounds.left();
|
|
||||||
int right = _bounds.right() + 1;
|
|
||||||
int top = _bounds.top();
|
|
||||||
int bottom = _bounds.bottom() + 1;
|
|
||||||
|
|
||||||
glm::vec2 topLeft(left, top);
|
|
||||||
glm::vec2 bottomRight(right, bottom);
|
|
||||||
glBindTexture(GL_TEXTURE_2D, 0);
|
|
||||||
DependencyManager::get<GeometryCache>()->renderQuad(topLeft, bottomRight, quadColor);
|
|
||||||
|
|
||||||
const int leftAdjust = -1; // required to make text render relative to left edge of bounds
|
|
||||||
const int topAdjust = -2; // required to make text render relative to top edge of bounds
|
|
||||||
int x = _bounds.left() + _leftMargin + leftAdjust;
|
|
||||||
int y = _bounds.top() + _topMargin + topAdjust;
|
|
||||||
|
|
||||||
float alpha = getAlpha();
|
|
||||||
glm::vec4 textColor = {_color.red / MAX_COLOR, _color.green / MAX_COLOR, _color.blue / MAX_COLOR, alpha };
|
|
||||||
_textRenderer->draw(x, y, _text, textColor);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void TextOverlay::setProperties(const QScriptValue& properties) {
|
void TextOverlay::setProperties(const QScriptValue& properties) {
|
||||||
Overlay2D::setProperties(properties);
|
Overlay2D::setProperties(properties);
|
||||||
|
_qmlElement->setX(_bounds.left());
|
||||||
|
_qmlElement->setY(_bounds.top());
|
||||||
|
_qmlElement->setWidth(_bounds.width());
|
||||||
|
_qmlElement->setHeight(_bounds.height());
|
||||||
|
_qmlElement->settextColor(toQmlColor(vec4(toGlm(_color), _alpha)));
|
||||||
QScriptValue font = properties.property("font");
|
QScriptValue font = properties.property("font");
|
||||||
if (font.isObject()) {
|
if (font.isObject()) {
|
||||||
if (font.property("size").isValid()) {
|
if (font.property("size").isValid()) {
|
||||||
setFontSize(font.property("size").toInt32());
|
setFontSize(font.property("size").toInt32());
|
||||||
}
|
}
|
||||||
|
QFont font(_qmlElement->fontFamily());
|
||||||
|
font.setPixelSize(_qmlElement->fontSize());
|
||||||
|
QFontMetrics fm(font);
|
||||||
|
_qmlElement->setlineHeight(fm.lineSpacing() * 1.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
QScriptValue text = properties.property("text");
|
QScriptValue text = properties.property("text");
|
||||||
|
@ -126,6 +195,7 @@ void TextOverlay::setProperties(const QScriptValue& properties) {
|
||||||
if (properties.property("backgroundAlpha").isValid()) {
|
if (properties.property("backgroundAlpha").isValid()) {
|
||||||
_backgroundAlpha = properties.property("backgroundAlpha").toVariant().toFloat();
|
_backgroundAlpha = properties.property("backgroundAlpha").toVariant().toFloat();
|
||||||
}
|
}
|
||||||
|
_qmlElement->setbackgroundColor(toQmlColor(vec4(toGlm(_backgroundColor), _backgroundAlpha)));
|
||||||
|
|
||||||
if (properties.property("leftMargin").isValid()) {
|
if (properties.property("leftMargin").isValid()) {
|
||||||
setLeftMargin(properties.property("leftMargin").toVariant().toInt());
|
setLeftMargin(properties.property("leftMargin").toVariant().toInt());
|
||||||
|
@ -166,15 +236,37 @@ QScriptValue TextOverlay::getProperty(const QString& property) {
|
||||||
}
|
}
|
||||||
|
|
||||||
QSizeF TextOverlay::textSize(const QString& text) const {
|
QSizeF TextOverlay::textSize(const QString& text) const {
|
||||||
auto extents = _textRenderer->computeExtent(text);
|
int lines = 1;
|
||||||
|
foreach(QChar c, text) {
|
||||||
return QSizeF(extents.x, extents.y);
|
if (c == QChar('\n')) {
|
||||||
|
++lines;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QFont font(_qmlElement->fontFamily());
|
||||||
|
font.setPixelSize(_qmlElement->fontSize());
|
||||||
|
QFontMetrics fm(font);
|
||||||
|
QSizeF result = QSizeF(fm.width(text), _qmlElement->lineHeight() * lines);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
void TextOverlay::setFontSize(int fontSize) {
|
void TextOverlay::setFontSize(int fontSize) {
|
||||||
_fontSize = fontSize;
|
_fontSize = fontSize;
|
||||||
|
_qmlElement->setfontSize(fontSize);
|
||||||
auto oldTextRenderer = _textRenderer;
|
|
||||||
_textRenderer = TextRenderer::getInstance(SANS_FONT_FAMILY, _fontSize, DEFAULT_FONT_WEIGHT);
|
|
||||||
delete oldTextRenderer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TextOverlay::setText(const QString& text) {
|
||||||
|
_text = text;
|
||||||
|
_qmlElement->settext(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TextOverlay::setLeftMargin(int margin) {
|
||||||
|
_leftMargin = margin;
|
||||||
|
_qmlElement->setleftMargin(margin);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TextOverlay::setTopMargin(int margin) {
|
||||||
|
_topMargin = margin;
|
||||||
|
_qmlElement->settopMargin(margin);
|
||||||
|
}
|
||||||
|
|
||||||
|
#include "TextOverlay.moc"
|
||||||
|
|
|
@ -26,7 +26,7 @@ const int DEFAULT_MARGIN = 10;
|
||||||
const int DEFAULT_FONTSIZE = 12;
|
const int DEFAULT_FONTSIZE = 12;
|
||||||
const int DEFAULT_FONT_WEIGHT = 50;
|
const int DEFAULT_FONT_WEIGHT = 50;
|
||||||
|
|
||||||
class TextRenderer;
|
class TextOverlayElement;
|
||||||
|
|
||||||
class TextOverlay : public Overlay2D {
|
class TextOverlay : public Overlay2D {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
@ -45,9 +45,9 @@ public:
|
||||||
float getBackgroundAlpha() const { return _backgroundAlpha; }
|
float getBackgroundAlpha() const { return _backgroundAlpha; }
|
||||||
|
|
||||||
// setters
|
// setters
|
||||||
void setText(const QString& text) { _text = text; }
|
void setText(const QString& text);
|
||||||
void setLeftMargin(int margin) { _leftMargin = margin; }
|
void setLeftMargin(int margin);
|
||||||
void setTopMargin(int margin) { _topMargin = margin; }
|
void setTopMargin(int margin);
|
||||||
void setFontSize(int fontSize);
|
void setFontSize(int fontSize);
|
||||||
|
|
||||||
virtual void setProperties(const QScriptValue& properties);
|
virtual void setProperties(const QScriptValue& properties);
|
||||||
|
@ -57,9 +57,7 @@ public:
|
||||||
QSizeF textSize(const QString& text) const; // Pixels
|
QSizeF textSize(const QString& text) const; // Pixels
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
TextOverlayElement* _qmlElement{ nullptr };
|
||||||
TextRenderer* _textRenderer = nullptr;
|
|
||||||
|
|
||||||
QString _text;
|
QString _text;
|
||||||
xColor _backgroundColor;
|
xColor _backgroundColor;
|
||||||
float _backgroundAlpha;
|
float _backgroundAlpha;
|
||||||
|
|
|
@ -17,7 +17,6 @@
|
||||||
#include <DeferredLightingEffect.h>
|
#include <DeferredLightingEffect.h>
|
||||||
#include <GeometryCache.h>
|
#include <GeometryCache.h>
|
||||||
#include <PerfStat.h>
|
#include <PerfStat.h>
|
||||||
#include <TextRenderer.h>
|
|
||||||
#include <OffscreenQmlSurface.h>
|
#include <OffscreenQmlSurface.h>
|
||||||
#include <AbstractViewStateInterface.h>
|
#include <AbstractViewStateInterface.h>
|
||||||
#include <GLMHelpers.h>
|
#include <GLMHelpers.h>
|
||||||
|
|
|
@ -1,576 +0,0 @@
|
||||||
//
|
|
||||||
// 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>
|
|
||||||
|
|
||||||
#ifdef __GNUC__
|
|
||||||
#pragma GCC diagnostic push
|
|
||||||
#pragma GCC diagnostic ignored "-Wdouble-promotion"
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// FIXME, decouple from the GL headers
|
|
||||||
#include <QOpenGLShaderProgram>
|
|
||||||
#include <QOpenGLTexture>
|
|
||||||
#include <QOpenGLVertexArrayObject>
|
|
||||||
#include <QOpenGLBuffer>
|
|
||||||
|
|
||||||
#ifdef __GNUC__
|
|
||||||
#pragma GCC diagnostic pop
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#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 "RenderUtilsLogging.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);
|
|
||||||
|
|
||||||
qCDebug(renderutils) << "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(" ")) {
|
|
||||||
bool lineFeed = false;
|
|
||||||
if (token1.isEmpty()) {
|
|
||||||
result << token1;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
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 && std::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 && std::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();
|
|
||||||
_texture->release(); // TODO: Brad & Sam, let's discuss this. Without this non-textured quads get their colors borked.
|
|
||||||
_program->release();
|
|
||||||
|
|
||||||
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 (!_font) {
|
|
||||||
qWarning() << "Unable to load font with family " << family;
|
|
||||||
_font = loadFont("Courier");
|
|
||||||
}
|
|
||||||
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;
|
|
||||||
if (_font) {
|
|
||||||
return _font->computeExtent(str) * scale;
|
|
||||||
}
|
|
||||||
return glm::vec2(0.1f,0.1f);
|
|
||||||
}
|
|
||||||
|
|
||||||
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::withPushAll([&] {
|
|
||||||
MatrixStack & mv = MatrixStack::modelview();
|
|
||||||
MatrixStack & pr = MatrixStack::projection();
|
|
||||||
gpu::GLBackend::fetchMatrix(GL_MODELVIEW_MATRIX, mv.top());
|
|
||||||
gpu::GLBackend::fetchMatrix(GL_PROJECTION_MATRIX, pr.top());
|
|
||||||
|
|
||||||
// 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
|
|
||||||
if (_font) {
|
|
||||||
result = _font->drawString(x, y, str, actualColor, _effectType, bounds / scale);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return result.x;
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,89 +0,0 @@
|
||||||
//
|
|
||||||
// TextRenderer.h
|
|
||||||
// interface/src/ui
|
|
||||||
//
|
|
||||||
// Created by Andrzej Kapolka on 4/26/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
|
|
||||||
//
|
|
||||||
|
|
||||||
#ifndef hifi_TextRenderer_h
|
|
||||||
#define hifi_TextRenderer_h
|
|
||||||
|
|
||||||
#include <gpu/GPUConfig.h>
|
|
||||||
#include <glm/glm.hpp>
|
|
||||||
#include <unordered_map>
|
|
||||||
#include <QColor>
|
|
||||||
#include <QFont>
|
|
||||||
#include <QFontMetrics>
|
|
||||||
#include <QHash>
|
|
||||||
#include <QImage>
|
|
||||||
#include <QVector>
|
|
||||||
|
|
||||||
#include <gpu/Resource.h>
|
|
||||||
#include <gpu/Stream.h>
|
|
||||||
|
|
||||||
// a special "character" that renders as a solid block
|
|
||||||
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"
|
|
||||||
|
|
||||||
// the Inconsolata font family
|
|
||||||
#ifdef Q_OS_WIN
|
|
||||||
#define INCONSOLATA_FONT_FAMILY "Fixedsys"
|
|
||||||
#define INCONSOLATA_FONT_WEIGHT QFont::Normal
|
|
||||||
#else
|
|
||||||
#define INCONSOLATA_FONT_FAMILY "Inconsolata"
|
|
||||||
#define INCONSOLATA_FONT_WEIGHT QFont::Bold
|
|
||||||
#endif
|
|
||||||
|
|
||||||
class Font;
|
|
||||||
|
|
||||||
// TextRenderer is actually a fairly thin wrapper around a Font class
|
|
||||||
// defined in the cpp file.
|
|
||||||
class TextRenderer {
|
|
||||||
public:
|
|
||||||
enum EffectType { NO_EFFECT, SHADOW_EFFECT, OUTLINE_EFFECT };
|
|
||||||
|
|
||||||
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();
|
|
||||||
|
|
||||||
glm::vec2 computeExtent(const QString & str) const;
|
|
||||||
|
|
||||||
float draw(
|
|
||||||
float x, float y,
|
|
||||||
const QString & str,
|
|
||||||
const glm::vec4& color = glm::vec4(-1.0f),
|
|
||||||
const glm::vec2& bounds = glm::vec2(-1.0f));
|
|
||||||
|
|
||||||
private:
|
|
||||||
TextRenderer(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));
|
|
||||||
|
|
||||||
// the type of effect to apply
|
|
||||||
const EffectType _effectType;
|
|
||||||
|
|
||||||
// the thickness of the effect
|
|
||||||
const int _effectThickness;
|
|
||||||
|
|
||||||
const float _pointSize;
|
|
||||||
|
|
||||||
// text color
|
|
||||||
const QColor _color;
|
|
||||||
|
|
||||||
Font * _font;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
#endif // hifi_TextRenderer_h
|
|
|
@ -10,6 +10,7 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
#include "TextRenderer3D.h"
|
#include "TextRenderer3D.h"
|
||||||
|
#include <StreamHelpers.h>
|
||||||
|
|
||||||
#include <gpu/GPUConfig.h>
|
#include <gpu/GPUConfig.h>
|
||||||
#include <gpu/GLBackend.h>
|
#include <gpu/GLBackend.h>
|
||||||
|
@ -20,6 +21,8 @@
|
||||||
#include <QStringList>
|
#include <QStringList>
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
|
|
||||||
|
#include "text/Font.h"
|
||||||
|
|
||||||
#include "GLMHelpers.h"
|
#include "GLMHelpers.h"
|
||||||
#include "MatrixStack.h"
|
#include "MatrixStack.h"
|
||||||
#include "RenderUtilsLogging.h"
|
#include "RenderUtilsLogging.h"
|
||||||
|
@ -30,421 +33,22 @@
|
||||||
#include "GeometryCache.h"
|
#include "GeometryCache.h"
|
||||||
#include "DeferredLightingEffect.h"
|
#include "DeferredLightingEffect.h"
|
||||||
|
|
||||||
// Helper functions for reading binary data from an IO device
|
const float TextRenderer3D::DEFAULT_POINT_SIZE = 12;
|
||||||
template<class T>
|
|
||||||
void readStream(QIODevice& in, T& t) {
|
TextRenderer3D* TextRenderer3D::getInstance(const char* family, float pointSize,
|
||||||
in.read((char*) &t, sizeof(t));
|
bool bold, bool italic, EffectType effect, int effectThickness) {
|
||||||
|
return new TextRenderer3D(family, pointSize, false, italic, effect, effectThickness);
|
||||||
}
|
}
|
||||||
|
|
||||||
template<typename T, size_t N>
|
TextRenderer3D::TextRenderer3D(const char* family, float pointSize, int weight, bool italic,
|
||||||
void readStream(QIODevice& in, T (&t)[N]) {
|
EffectType effect, int effectThickness) :
|
||||||
in.read((char*) t, N);
|
_pointSize(pointSize),
|
||||||
}
|
|
||||||
|
|
||||||
template<class T, size_t N>
|
|
||||||
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<QChar, Glyph3D> _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<QString, Font3D*> 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<Glyph3D> 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(true,
|
|
||||||
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.setResourceTexture(_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),
|
_effectType(effect),
|
||||||
_effectThickness(effectThickness),
|
_effectThickness(effectThickness),
|
||||||
_font(loadFont3D(family)) {
|
_font(Font::load(family)) {
|
||||||
if (!_font) {
|
if (!_font) {
|
||||||
qWarning() << "Unable to load font with family " << family;
|
qWarning() << "Unable to load font with family " << family;
|
||||||
_font = loadFont3D("Courier");
|
_font = Font::load("Courier");
|
||||||
}
|
}
|
||||||
if (1 != _effectThickness) {
|
if (1 != _effectThickness) {
|
||||||
qWarning() << "Effect thickness not currently supported";
|
qWarning() << "Effect thickness not currently supported";
|
||||||
|
|
|
@ -15,37 +15,25 @@
|
||||||
#include <glm/glm.hpp>
|
#include <glm/glm.hpp>
|
||||||
#include <QColor>
|
#include <QColor>
|
||||||
|
|
||||||
// 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"
|
|
||||||
|
|
||||||
// the Inconsolata font family
|
|
||||||
#ifdef Q_OS_WIN
|
|
||||||
#define INCONSOLATA_FONT_FAMILY "Fixedsys"
|
|
||||||
#define INCONSOLATA_FONT_WEIGHT QFont::Normal
|
|
||||||
#else
|
|
||||||
#define INCONSOLATA_FONT_FAMILY "Inconsolata"
|
|
||||||
#define INCONSOLATA_FONT_WEIGHT QFont::Bold
|
|
||||||
#endif
|
|
||||||
|
|
||||||
namespace gpu {
|
namespace gpu {
|
||||||
class Batch;
|
class Batch;
|
||||||
}
|
}
|
||||||
class Font3D;
|
class Font;
|
||||||
|
|
||||||
|
#include "text/EffectType.h"
|
||||||
|
#include "text/FontFamilies.h"
|
||||||
|
|
||||||
// TextRenderer3D is actually a fairly thin wrapper around a Font class
|
// TextRenderer3D is actually a fairly thin wrapper around a Font class
|
||||||
// defined in the cpp file.
|
// defined in the cpp file.
|
||||||
class TextRenderer3D {
|
class TextRenderer3D {
|
||||||
public:
|
public:
|
||||||
enum EffectType { NO_EFFECT, SHADOW_EFFECT, OUTLINE_EFFECT };
|
static const float DEFAULT_POINT_SIZE;
|
||||||
|
|
||||||
|
static TextRenderer3D* getInstance(const char* family, float pointSize = DEFAULT_POINT_SIZE,
|
||||||
|
bool bold = false, bool italic = false, EffectType effect = NO_EFFECT, int effectThickness = 1);
|
||||||
|
|
||||||
static TextRenderer3D* getInstance(const char* family, int weight = -1, bool italic = false,
|
|
||||||
EffectType effect = NO_EFFECT, int effectThickness = 1);
|
|
||||||
~TextRenderer3D();
|
~TextRenderer3D();
|
||||||
|
|
||||||
glm::vec2 computeExtent(const QString& str) const;
|
glm::vec2 computeExtent(const QString& str) const;
|
||||||
|
@ -55,7 +43,7 @@ public:
|
||||||
const glm::vec2& bounds = glm::vec2(-1.0f));
|
const glm::vec2& bounds = glm::vec2(-1.0f));
|
||||||
|
|
||||||
private:
|
private:
|
||||||
TextRenderer3D(const char* family, int weight = -1, bool italic = false,
|
TextRenderer3D(const char* family, float pointSize, int weight = -1, bool italic = false,
|
||||||
EffectType effect = NO_EFFECT, int effectThickness = 1);
|
EffectType effect = NO_EFFECT, int effectThickness = 1);
|
||||||
|
|
||||||
// the type of effect to apply
|
// the type of effect to apply
|
||||||
|
@ -66,8 +54,9 @@ private:
|
||||||
|
|
||||||
// text color
|
// text color
|
||||||
glm::vec4 _color;
|
glm::vec4 _color;
|
||||||
|
float _pointSize{ DEFAULT_POINT_SIZE };
|
||||||
|
|
||||||
Font3D* _font;
|
Font* _font;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,49 +0,0 @@
|
||||||
<@include gpu/Config.slh@>
|
|
||||||
<$VERSION_HEADER$>
|
|
||||||
// Generated on <$_SCRIBE_DATE$>
|
|
||||||
// sdf_text.frag
|
|
||||||
// fragment shader
|
|
||||||
//
|
|
||||||
// Created by Bradley Austin Davis on 2015-02-04
|
|
||||||
// Based on fragment shader code from
|
|
||||||
// https://github.com/paulhoux/Cinder-Samples/blob/master/TextRendering/include/text/Text.cpp
|
|
||||||
// Distributed under the Apache License, Version 2.0.
|
|
||||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
|
||||||
|
|
||||||
uniform sampler2D Font;
|
|
||||||
uniform vec4 Color;
|
|
||||||
uniform bool Outline;
|
|
||||||
|
|
||||||
varying vec2 vTexCoord;
|
|
||||||
|
|
||||||
const float gamma = 2.6;
|
|
||||||
const float smoothing = 100.0;
|
|
||||||
const float interiorCutoff = 0.8;
|
|
||||||
const float outlineExpansion = 0.2;
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
// retrieve signed distance
|
|
||||||
float sdf = texture2D(Font, vTexCoord).r;
|
|
||||||
if (Outline) {
|
|
||||||
if (sdf > interiorCutoff) {
|
|
||||||
sdf = 1.0 - sdf;
|
|
||||||
} else {
|
|
||||||
sdf += outlineExpansion;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 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.01) {
|
|
||||||
discard;
|
|
||||||
}
|
|
||||||
|
|
||||||
// final color
|
|
||||||
gl_FragColor = vec4(Color.rgb, a);
|
|
||||||
}
|
|
|
@ -1,24 +0,0 @@
|
||||||
<@include gpu/Config.slh@>
|
|
||||||
<$VERSION_HEADER$>
|
|
||||||
// Generated on <$_SCRIBE_DATE$>
|
|
||||||
// sdf_text.vert
|
|
||||||
// vertex shader
|
|
||||||
//
|
|
||||||
// Created by Brad Davis on 10/14/13.
|
|
||||||
//
|
|
||||||
// Distributed under the Apache License, Version 2.0.
|
|
||||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
|
||||||
//
|
|
||||||
|
|
||||||
uniform mat4 Projection;
|
|
||||||
uniform mat4 ModelView;
|
|
||||||
|
|
||||||
attribute vec2 Position;
|
|
||||||
attribute vec2 TexCoord;
|
|
||||||
|
|
||||||
varying vec2 vTexCoord;
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
vTexCoord = TexCoord;
|
|
||||||
gl_Position = Projection * ModelView * vec4(Position, 0.0, 1.0);
|
|
||||||
}
|
|
15
libraries/render-utils/src/text/EffectType.h
Normal file
15
libraries/render-utils/src/text/EffectType.h
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
//
|
||||||
|
// Created by Bradley Austin Davis on 2015/07/16
|
||||||
|
// 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
|
||||||
|
//
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#ifndef hifi_EffectType_h
|
||||||
|
#define hifi_EffectType_h
|
||||||
|
|
||||||
|
enum EffectType { NO_EFFECT, SHADOW_EFFECT, OUTLINE_EFFECT };
|
||||||
|
|
||||||
|
#endif
|
322
libraries/render-utils/src/text/Font.cpp
Normal file
322
libraries/render-utils/src/text/Font.cpp
Normal file
|
@ -0,0 +1,322 @@
|
||||||
|
#include "Font.h"
|
||||||
|
|
||||||
|
#include <QFile>
|
||||||
|
#include <QImage>
|
||||||
|
|
||||||
|
#include <StreamHelpers.h>
|
||||||
|
|
||||||
|
#include "sdf_text3D_vert.h"
|
||||||
|
#include "sdf_text3D_frag.h"
|
||||||
|
|
||||||
|
#include "../RenderUtilsLogging.h"
|
||||||
|
#include "FontFamilies.h"
|
||||||
|
|
||||||
|
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 Glyph& glyph, const glm::vec2& offset) :
|
||||||
|
QuadBuilder(offset + glm::vec2(glyph.offset.x, glyph.offset.y - glyph.size.y), glyph.size,
|
||||||
|
glyph.texOffset, glyph.texSize) {}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
static QHash<QString, Font*> LOADED_FONTS;
|
||||||
|
|
||||||
|
Font* Font::load(QIODevice& fontFile) {
|
||||||
|
Font* result = new Font();
|
||||||
|
result->read(fontFile);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Font* Font::load(const QString& family) {
|
||||||
|
if (!LOADED_FONTS.contains(family)) {
|
||||||
|
|
||||||
|
static const QString SDFF_COURIER_PRIME_FILENAME{ ":/CourierPrime.sdff" };
|
||||||
|
static const QString SDFF_INCONSOLATA_MEDIUM_FILENAME{ ":/InconsolataMedium.sdff" };
|
||||||
|
static const QString SDFF_ROBOTO_FILENAME{ ":/Roboto.sdff" };
|
||||||
|
static 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] = load(fontFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return LOADED_FONTS[family];
|
||||||
|
}
|
||||||
|
|
||||||
|
Font::Font() {
|
||||||
|
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];
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
glm::vec2 Font::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 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) {
|
||||||
|
glm::vec2 tokenExtent = computeTokenExtent(line);
|
||||||
|
extent.x = std::max(tokenExtent.x, extent.x);
|
||||||
|
}
|
||||||
|
extent.y = lines.count() * _fontSize;
|
||||||
|
}
|
||||||
|
return extent;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
// 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
|
||||||
|
QImage image;
|
||||||
|
if (!image.loadFromData(in.readAll(), "PNG")) {
|
||||||
|
qFatal("Failed to read SDFF image");
|
||||||
|
}
|
||||||
|
|
||||||
|
_glyphs.clear();
|
||||||
|
glm::vec2 imageSize = toGlm(image.size());
|
||||||
|
foreach(Glyph 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 Font::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(true,
|
||||||
|
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 Font::rebuildVertices(float x, float y, const QString& str, const glm::vec2& bounds) {
|
||||||
|
_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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Font::drawString(gpu::Batch& batch, float x, float y, const QString& str, const glm::vec4* color,
|
||||||
|
EffectType effectType, const glm::vec2& bounds) {
|
||||||
|
if (str == "") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str != _lastStringRendered || bounds != _lastBounds) {
|
||||||
|
rebuildVertices(x, y, str, bounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
setupGPU();
|
||||||
|
|
||||||
|
batch.setPipeline(_pipeline);
|
||||||
|
batch.setResourceTexture(_fontLoc, _texture);
|
||||||
|
batch._glUniform1i(_outlineLoc, (effectType == 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);
|
||||||
|
}
|
80
libraries/render-utils/src/text/Font.h
Normal file
80
libraries/render-utils/src/text/Font.h
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
//
|
||||||
|
// Created by Bradley Austin Davis on 2015/07/16
|
||||||
|
// 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
|
||||||
|
//
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#ifndef hifi_Font_h
|
||||||
|
#define hifi_Font_h
|
||||||
|
|
||||||
|
#include "Glyph.h"
|
||||||
|
#include "EffectType.h"
|
||||||
|
#include <gpu/Batch.h>
|
||||||
|
#include <gpu/Pipeline.h>
|
||||||
|
|
||||||
|
class Font {
|
||||||
|
public:
|
||||||
|
Font();
|
||||||
|
|
||||||
|
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, EffectType effectType,
|
||||||
|
const glm::vec2& bound);
|
||||||
|
|
||||||
|
static Font* load(QIODevice& fontFile);
|
||||||
|
static Font* load(const QString& family);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QStringList tokenizeForWrapping(const QString& str) const;
|
||||||
|
QStringList splitLines(const QString& str) const;
|
||||||
|
glm::vec2 computeTokenExtent(const QString& str) const;
|
||||||
|
|
||||||
|
const Glyph& getGlyph(const QChar& c) const;
|
||||||
|
void rebuildVertices(float x, float y, const QString& str, const glm::vec2& bounds);
|
||||||
|
|
||||||
|
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<QChar, Glyph> _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;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
31
libraries/render-utils/src/text/FontFamilies.h
Normal file
31
libraries/render-utils/src/text/FontFamilies.h
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
//
|
||||||
|
// Created by Bradley Austin Davis on 2015/07/16
|
||||||
|
// 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
|
||||||
|
//
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#ifndef hifi_FontFamilies_h
|
||||||
|
#define hifi_FontFamilies_h
|
||||||
|
|
||||||
|
// 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"
|
||||||
|
|
||||||
|
// the Inconsolata font family
|
||||||
|
#ifdef Q_OS_WIN
|
||||||
|
#define INCONSOLATA_FONT_FAMILY "Fixedsys"
|
||||||
|
#define INCONSOLATA_FONT_WEIGHT QFont::Normal
|
||||||
|
#else
|
||||||
|
#define INCONSOLATA_FONT_FAMILY "Inconsolata"
|
||||||
|
#define INCONSOLATA_FONT_WEIGHT QFont::Bold
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif
|
22
libraries/render-utils/src/text/Glyph.cpp
Normal file
22
libraries/render-utils/src/text/Glyph.cpp
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
#include "Glyph.h"
|
||||||
|
#include <StreamHelpers.h>
|
||||||
|
|
||||||
|
// We adjust bounds because offset is the bottom left corner of the font but the top left corner of a QRect
|
||||||
|
QRectF Glyph::bounds() const {
|
||||||
|
return glmToRect(offset, size).translated(0.0f, -size.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
QRectF Glyph::textureBounds() const {
|
||||||
|
return glmToRect(texOffset, texSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
38
libraries/render-utils/src/text/Glyph.h
Normal file
38
libraries/render-utils/src/text/Glyph.h
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
//
|
||||||
|
// Created by Bradley Austin Davis on 2015/07/16
|
||||||
|
// 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
|
||||||
|
//
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#ifndef hifi_Glyph_h
|
||||||
|
#define hifi_Glyph_h
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include <QChar>
|
||||||
|
#include <QRect>
|
||||||
|
#include <QIODevice>
|
||||||
|
|
||||||
|
#include <GLMHelpers.h>
|
||||||
|
|
||||||
|
// stores the font metrics for a single character
|
||||||
|
struct Glyph {
|
||||||
|
QChar c;
|
||||||
|
vec2 texOffset;
|
||||||
|
vec2 texSize;
|
||||||
|
vec2 size;
|
||||||
|
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;
|
||||||
|
QRectF textureBounds() const;
|
||||||
|
|
||||||
|
void read(QIODevice& in);
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
32
libraries/shared/src/StreamHelpers.h
Normal file
32
libraries/shared/src/StreamHelpers.h
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
//
|
||||||
|
// Created by Bradley Austin Davis 2015/07/16
|
||||||
|
// Copyright 2014 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
|
||||||
|
//
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#ifndef hifi_StreamHelpers_h
|
||||||
|
#define hifi_StreamHelpers_h
|
||||||
|
|
||||||
|
#include <QIODevice>
|
||||||
|
#include <QBuffer>
|
||||||
|
|
||||||
|
// Helper functions for reading binary data from an IO device
|
||||||
|
template<class T>
|
||||||
|
inline void readStream(QIODevice& in, T& t) {
|
||||||
|
in.read((char*) &t, sizeof(t));
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename T, size_t N>
|
||||||
|
inline void readStream(QIODevice& in, T (&t)[N]) {
|
||||||
|
in.read((char*) t, N);
|
||||||
|
}
|
||||||
|
|
||||||
|
template<class T, size_t N>
|
||||||
|
inline void fillBuffer(QBuffer& buffer, T (&t)[N]) {
|
||||||
|
buffer.setData((const char*) t, N);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
|
@ -8,7 +8,7 @@
|
||||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||||
//
|
//
|
||||||
|
|
||||||
#include "TextRenderer.h"
|
#include <gpu/GPUConfig.h>
|
||||||
|
|
||||||
#include <QWindow>
|
#include <QWindow>
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
|
@ -29,6 +29,7 @@
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <glm/glm.hpp>
|
#include <glm/glm.hpp>
|
||||||
|
|
||||||
#include <PathUtils.h>
|
#include <PathUtils.h>
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
|
|
||||||
|
@ -86,7 +87,7 @@ class QTestWindow : public QWindow {
|
||||||
|
|
||||||
QOpenGLContext* _context{ nullptr };
|
QOpenGLContext* _context{ nullptr };
|
||||||
QSize _size;
|
QSize _size;
|
||||||
TextRenderer* _textRenderer[4];
|
//TextRenderer* _textRenderer[4];
|
||||||
RateCounter fps;
|
RateCounter fps;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
@ -146,12 +147,12 @@ public:
|
||||||
glGetError();
|
glGetError();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
_textRenderer[0] = TextRenderer::getInstance(SANS_FONT_FAMILY, 12, false);
|
//_textRenderer[0] = TextRenderer::getInstance(SANS_FONT_FAMILY, 12, false);
|
||||||
_textRenderer[1] = TextRenderer::getInstance(SERIF_FONT_FAMILY, 12, false,
|
//_textRenderer[1] = TextRenderer::getInstance(SERIF_FONT_FAMILY, 12, false,
|
||||||
TextRenderer::SHADOW_EFFECT);
|
// TextRenderer::SHADOW_EFFECT);
|
||||||
_textRenderer[2] = TextRenderer::getInstance(MONO_FONT_FAMILY, 48, -1,
|
//_textRenderer[2] = TextRenderer::getInstance(MONO_FONT_FAMILY, 48, -1,
|
||||||
false, TextRenderer::OUTLINE_EFFECT);
|
// false, TextRenderer::OUTLINE_EFFECT);
|
||||||
_textRenderer[3] = TextRenderer::getInstance(INCONSOLATA_FONT_FAMILY, 24);
|
//_textRenderer[3] = TextRenderer::getInstance(INCONSOLATA_FONT_FAMILY, 24);
|
||||||
|
|
||||||
glEnable(GL_BLEND);
|
glEnable(GL_BLEND);
|
||||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||||
|
@ -190,48 +191,6 @@ static const glm::uvec2 QUAD_OFFSET(10, 10);
|
||||||
static const glm::vec3 COLORS[4] = { { 1.0, 1.0, 1.0 }, { 0.5, 1.0, 0.5 }, {
|
static const glm::vec3 COLORS[4] = { { 1.0, 1.0, 1.0 }, { 0.5, 1.0, 0.5 }, {
|
||||||
1.0, 0.5, 0.5 }, { 0.5, 0.5, 1.0 } };
|
1.0, 0.5, 0.5 }, { 0.5, 0.5, 1.0 } };
|
||||||
|
|
||||||
void QTestWindow::renderText() {
|
|
||||||
glMatrixMode(GL_PROJECTION);
|
|
||||||
glLoadIdentity();
|
|
||||||
glOrtho(0, _size.width(), _size.height(), 0, 1, -1);
|
|
||||||
glMatrixMode(GL_MODELVIEW);
|
|
||||||
glLoadIdentity();
|
|
||||||
|
|
||||||
const glm::uvec2 size = glm::uvec2(_size.width() / 2, _size.height() / 2);
|
|
||||||
|
|
||||||
const glm::uvec2 offsets[4] = {
|
|
||||||
{ QUAD_OFFSET.x, QUAD_OFFSET.y },
|
|
||||||
{ size.x + QUAD_OFFSET.x, QUAD_OFFSET.y },
|
|
||||||
{ size.x + QUAD_OFFSET.x, size.y + QUAD_OFFSET.y },
|
|
||||||
{ QUAD_OFFSET.x, size.y + QUAD_OFFSET.y },
|
|
||||||
};
|
|
||||||
|
|
||||||
QString str = QString::fromWCharArray(EXAMPLE_TEXT);
|
|
||||||
for (int i = 0; i < 4; ++i) {
|
|
||||||
glm::vec2 bounds = _textRenderer[i]->computeExtent(str);
|
|
||||||
glPushMatrix();
|
|
||||||
{
|
|
||||||
glTranslatef(offsets[i].x, offsets[i].y, 0);
|
|
||||||
glColor3f(0, 0, 0);
|
|
||||||
glBegin(GL_QUADS);
|
|
||||||
{
|
|
||||||
glVertex2f(0, 0);
|
|
||||||
glVertex2f(0, bounds.y);
|
|
||||||
glVertex2f(bounds.x, bounds.y);
|
|
||||||
glVertex2f(bounds.x, 0);
|
|
||||||
}
|
|
||||||
glEnd();
|
|
||||||
}
|
|
||||||
glPopMatrix();
|
|
||||||
const int testCount = 100;
|
|
||||||
for (int j = 0; j < testCount; ++j) {
|
|
||||||
// Draw backgrounds around where the text will appear
|
|
||||||
// Draw the text itself
|
|
||||||
_textRenderer[i]->draw(offsets[i].x, offsets[i].y, str.toLocal8Bit().constData(),
|
|
||||||
glm::vec4(COLORS[i], 1.0f));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void QTestWindow::draw() {
|
void QTestWindow::draw() {
|
||||||
if (!isVisible()) {
|
if (!isVisible()) {
|
||||||
|
@ -242,8 +201,6 @@ void QTestWindow::draw() {
|
||||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
||||||
glViewport(0, 0, _size.width() * devicePixelRatio(), _size.height() * devicePixelRatio());
|
glViewport(0, 0, _size.width() * devicePixelRatio(), _size.height() * devicePixelRatio());
|
||||||
|
|
||||||
renderText();
|
|
||||||
|
|
||||||
_context->swapBuffers(this);
|
_context->swapBuffers(this);
|
||||||
glFinish();
|
glFinish();
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue