Draw Text as 3D Objects with OpenGL: Difference between revisions
No edit summary |
No edit summary |
||
Line 1: | Line 1: | ||
h1. Draw Text as 3D objects with OpenGL | h1. Draw Text as 3D objects with OpenGL | ||
There are a couple of functions in WGL ( | There are a couple of functions in WGL ("Windows Graphics Library":http://msdn.microsoft.com/en-us/library/windows/desktop/ee417756(v=vs.85).aspx) which can be used to draw text as nice 3D objects in OpenGL. There is a well known example at "NeHe":http://nehe.gamedev.net/tutorial/outline_fonts/15004/. However, this is not portable at all, and since I'm using Qt anyway, I was looking for a way to have this done with Qt. I was surprised that there was no such function already available within Qt, but then I stumbled across "this example on Stackexchange":http://stackoverflow.com/questions/3514935/3d-text-on-qglwidget-in-qt-4-6-3/3516254#3516254 that got me started. | ||
Before I get to the code, some drawbacks of this example: | Before I get to the code, some drawbacks of this example: | ||
* It uses the fixed-function pipeline. (GL_QUAD_STRIP's and DisplayLists). Im sure this '''can''' be done in a "more modern" way with VBO's, but my OpenGL knowlegde is not yet at that level. | |||
* it relies on GLU for polygon tesselation. There might be better alternatives around or even some within Qt. | |||
* No real character set (or even UTF) handling. It only uses the first 256 characters. | |||
* side effects on the matrix. | |||
The example uses QFont to get the font outline for each character (glyph). The basic idea is to create two flat outline-polygons for the front- and back- | The example uses QFont to get the font outline for each character (glyph). The basic idea is to create two flat outline-polygons for the front- and back-"plane" of a glyph and then create the "wrapping" in between the front- and backplane. Although it seems more difficult at first, it was pretty easy to create the wrapping in between the two outline-polygons with GL_QUAD_STRIP. The tricky bit was the polygon tesselation of the glyph outline, because the glyph-polygons are '''not''' concave and may have one or more holes. I'm using the polygon tesselation facility available in GLU. | ||
The text3d class can be subclassed by a GLWidget or GLWindow object. There are only 2 functions required to draw text: initfont() and print(). The initialization of the font cannot easily be done in the constructor, because the contest is probably not initialized during construction. Therefore the initfont(). | The text3d class can be subclassed by a GLWidget or GLWindow object. There are only 2 functions required to draw text: initfont() and print(). The initialization of the font cannot easily be done in the constructor, because the contest is probably not initialized during construction. Therefore the initfont(). | ||
text3d.h | text3d.h | ||
<code> | |||
#include <QOpenGLFunctions> | |||
#include <QString> | |||
#include <QFont> | |||
#include <QFontMetricsF> | |||
class Text3D | class Text3D | ||
{ | |||
public: | |||
Text3D(); | |||
void initfont(QFont & f, int thickness); // set up a font and specify the "thickness" | |||
void print(QString text); // print it in 3D! | |||
private: | private: | ||
void buildglyph(GLuint b, int c); // create one displaylist for character "c" | |||
QFont * font; | |||
QFontMetricsF *fm; | |||
float glyphthickness; | |||
GLuint base; // the "base" of our displaylists | |||
}; | |||
</code> | |||
The implementation file: text3d.cpp | The implementation file: text3d.cpp | ||
<code> | |||
#include <QFont> | |||
#include <QList> | |||
#include <QPainter> | |||
#include <QOpenGLFunctions> | |||
#include <QChar> | |||
#include <gl/GLU.h> | |||
#include "text3d.h" | |||
typedef void (__stdcall *TessFuncPtr)(); // defintion of the callback function type | typedef void (__stdcall *TessFuncPtr)(); // defintion of the callback function type | ||
Text3D::Text3D() // nothing special in the constructor | Text3D::Text3D() // nothing special in the constructor | ||
: glyphthickness(1.0f) | |||
, base(0) | |||
{} | |||
</code> | |||
The initialization just loops through the first 256 char's and calls buildglyph() for each of them. | The initialization just loops through the first 256 char's and calls buildglyph() for each of them. | ||
<code> | |||
void | |||
Text3D::initfont(QFont & f, float thickness) | |||
{ | |||
font = &f; | |||
fm = new QFontMetricsF(f); | |||
glyphthickness = thickness; | |||
if(base) // if we have display lists already, delete them first | |||
glDeleteLists(base, 256); | |||
base = glGenLists(256); // generate 256 display lists | base = glGenLists(256); // generate 256 display lists | ||
if(base == 0) | |||
{ | |||
qDebug() << "cannot create display lists."; | |||
throw; | |||
} | |||
for(int i=0; i | for(int i=0; i<256;+''i) // loop to build the first 256 glyphs | ||
buildglyph(base+i, (char)i); | |||
} | |||
</code> | |||
The print() function uses glCallLists() to "interpret" a complete string. See below how the char-by-char advance works. | |||
<code> | |||
void | |||
Text3D::print(QString text) | |||
{ | |||
glPushAttrib(GL_LIST_BIT); // Pushes The Display List Bits | |||
glListBase(base); // Sets The Base Character to 0 | |||
glCallLists(text.length(), GL_UNSIGNED_BYTE, text.toLocal8Bit()); // Draws The Display List Text | |||
glPopAttrib(); // Pops The Display List Bits | |||
} | |||
</code> | |||
At the beginning we need to set up both, the tesselation and the display list. | |||
<code> | |||
void | |||
Text3D::buildglyph(GLuint listbase, int c) // this is the main "workhorse" function. Create a displaylist with | |||
// ID "listbase" from character "c" | |||
< | GLUtriangulatorObj *tobj; | ||
QPainterPath path; | |||
path.addText(QPointF(0,0),*font, QString((char)c)); | |||
QList<QPolygonF> poly = path.toSubpathPolygons(); // get the glyph outline as a list of paths | |||
// set up the tesselation | |||
tobj = gluNewTess(); | |||
gluTessCallback(tobj, GLU_TESS_BEGIN, (TessFuncPtr)glBegin); | |||
gluTessCallback(tobj, GLU_TESS_VERTEX, (TessFuncPtr)glVertex3dv); | |||
gluTessCallback(tobj, GLU_TESS_END, (TessFuncPtr)glEnd); | |||
gluTessProperty(tobj, GLU_TESS_WINDING_RULE, GLU_TESS_WINDING_ODD); | |||
glNewList(listbase, GL_COMPILE); // start a new list | |||
glShadeModel(GL_FLAT); | |||
gluTessBeginPolygon(tobj, 0 ); // start tesselate | |||
// first, calculate number of vertices. | |||
int elements = 0; // number of total vertices in one glyph, counting all paths. | |||
for (QList<QPolygonF>::iterator it = poly.begin(); it != poly.end(); it) | |||
{ | |||
elements''= ('''it).size(); | |||
} | |||
</code> | |||
Now it's ready to tesselate the "front plate" polygon. | |||
<code> | |||
GLdouble''' vertices = (GLdouble ''') malloc(elements''' 3 * sizeof(GLdouble)); | |||
int j = 0; | |||
for (QList<QPolygonF>::iterator it = poly.begin(); it != poly.end(); it+'') // enumerate paths | |||
{ | |||
gluTessBeginContour(tobj); | |||
int i = 0; | |||
for (QPolygonF::iterator p = (*it).begin(); p != it->end(); p) // enumerate vertices | |||
{ | |||
int off = j+i; | |||
vertices[off+0] = p->rx(); | |||
vertices[off+1] = -p->ry(); | |||
vertices[off+2] = 0; // setting Z offset to zero. | |||
gluTessVertex(tobj, &vertices[off], &vertices[off] ); | |||
i''=3; // array math | |||
} | |||
gluTessEndContour(tobj); | |||
j ''= (*it).size()*3; // some more array math | |||
} | |||
gluTessEndPolygon(tobj); | |||
</code> | |||
Do the whole tesselation a second time with an offset applied for the "back plate". The "offset" (thickness) is set in | |||
<code> | |||
gluTessBeginPolygon(tobj, 0 ); | |||
j = 0; | |||
for (QList<QPolygonF>::iterator it = poly.begin(); it != poly.end(); it) | |||
{ | |||
gluTessBeginContour(tobj); | |||
int i = 0; | |||
for (QPolygonF::iterator p = (*it).begin(); p != it->end(); p) | |||
{ | |||
int off = j+i; | |||
vertices[off+0] = p->rx(); | |||
vertices[off+1] = -p->ry(); | |||
vertices[off+2] = -glyphthickness; // Z offset set to "minus glyphtickness" | |||
gluTessVertex(tobj, &vertices[off], &vertices[off] ); | |||
i''=3; | |||
} | |||
gluTessEndContour(tobj); | |||
j ''= (*it).size()*3; | |||
} | |||
gluTessEndPolygon(tobj); | |||
free(vertices); // no need for the vertices anymore | |||
</code> | |||
The "wrapping" between the two "plates" is simple compared to the tesselation. | |||
<code> | |||
for (QList<QPolygonF>::iterator it = poly.begin(); it != poly.end(); it) | |||
{ | |||
glBegin(GL_QUAD_STRIP); | |||
QPolygonF::iterator p; | |||
for (p = (*it).begin(); p != it->end(); p) | |||
{ | |||
glVertex3f(p->rx(), -p->ry(), 0.0f); | |||
glVertex3f(p->rx(), -p->ry(), -glyphthickness); | |||
} | |||
p = (*it).begin(); | |||
glVertex3f(p->rx(), -p->ry(), 0.0f); // draw the closing quad | |||
glVertex3f(p->rx(), -p->ry(), -glyphthickness); // of the "wrapping" | |||
glEnd(); | |||
} | |||
</code> | |||
This is where the char-by-char advance is done. Get the width from the font metrics and apply a glTranslate() with that value. This goes into the displaylist as well. (This may have side-effects as the matrix is not in the same "state" as before the call[[Image:|Image:]]!) | |||
<code> | |||
GLfloat gwidth = (float)fm->width©; | |||
glTranslatef(gwidth ,0.0f,0.0f); | |||
glEndList(); | |||
gluDeleteTess(tobj); | |||
} | |||
</code> | |||
The whole thing can actually be used in a init() and render() functions within a OpenGL object like this: | |||
<code> | |||
init() | |||
{ | |||
text = "Qt is great!"; | |||
QFont dfont("Comic Sans MS", 20); | |||
QFontMetrics fm(dfont); | |||
textwidth = fm.width(text); | |||
qDebug() << "width of text: " << textwidth; | |||
initfont(dfont,5); | |||
} | |||
render() | |||
{ | |||
glEnable(GL_DEPTH_TEST); | |||
glMatrixMode(GL_MODELVIEW); // To operate on model-view matrix | |||
glLoadIdentity(); // Reset the model-view matrix | |||
glTranslatef(0, 0.0f, –500.0f); // Move right and into the screen | |||
glRotatef(rot, 1.0f, 0.0f, 0.0f); // Rotate On The X Axis | |||
glRotatef(rot*1.5f, 0.0f, 1.0f, 0.0f); // Rotate On The Y Axis | |||
glRotatef(rot*1.4f, 0.0f, 0.0f, 1.0f); // Rotate On The Z Axis | |||
glColor3f( 1.0f*float(cos(rot/20.0f)), // Animate the color | |||
1.0f*float(sin(rot/25.0f)), | |||
1.0f-0.5f*float(cos(rot/17.0f)) | |||
); | |||
glTranslatef(-textwidth/2.0f, 0.0f, 0.0f); // textwidth holds the pixel width of the text | |||
// Print GL Text To The Screen | |||
print(text); | |||
glDisable(GL_DEPTH_TEST); | |||
rot''=0.3f; // increase rot value | |||
if(rot > 2000.f) rot = 0.0f; // wrap around at 2000 | |||
} | |||
</code> | |||
== Update: == | == Update: == |
Revision as of 09:30, 25 February 2015
h1. Draw Text as 3D objects with OpenGL
There are a couple of functions in WGL ("Windows Graphics Library":http://msdn.microsoft.com/en-us/library/windows/desktop/ee417756(v=vs.85).aspx) which can be used to draw text as nice 3D objects in OpenGL. There is a well known example at "NeHe":http://nehe.gamedev.net/tutorial/outline_fonts/15004/. However, this is not portable at all, and since I'm using Qt anyway, I was looking for a way to have this done with Qt. I was surprised that there was no such function already available within Qt, but then I stumbled across "this example on Stackexchange":http://stackoverflow.com/questions/3514935/3d-text-on-qglwidget-in-qt-4-6-3/3516254#3516254 that got me started.
Before I get to the code, some drawbacks of this example:
- It uses the fixed-function pipeline. (GL_QUAD_STRIP's and DisplayLists). Im sure this can be done in a "more modern" way with VBO's, but my OpenGL knowlegde is not yet at that level.
- it relies on GLU for polygon tesselation. There might be better alternatives around or even some within Qt.
- No real character set (or even UTF) handling. It only uses the first 256 characters.
- side effects on the matrix.
The example uses QFont to get the font outline for each character (glyph). The basic idea is to create two flat outline-polygons for the front- and back-"plane" of a glyph and then create the "wrapping" in between the front- and backplane. Although it seems more difficult at first, it was pretty easy to create the wrapping in between the two outline-polygons with GL_QUAD_STRIP. The tricky bit was the polygon tesselation of the glyph outline, because the glyph-polygons are not concave and may have one or more holes. I'm using the polygon tesselation facility available in GLU.
The text3d class can be subclassed by a GLWidget or GLWindow object. There are only 2 functions required to draw text: initfont() and print(). The initialization of the font cannot easily be done in the constructor, because the contest is probably not initialized during construction. Therefore the initfont().
text3d.h
#include <QOpenGLFunctions>
#include <QString>
#include <QFont>
#include <QFontMetricsF>
class Text3D
{
public:
Text3D();
void initfont(QFont & f, int thickness); // set up a font and specify the "thickness"
void print(QString text); // print it in 3D!
private:
void buildglyph(GLuint b, int c); // create one displaylist for character "c"
QFont * font;
QFontMetricsF *fm;
float glyphthickness;
GLuint base; // the "base" of our displaylists
};
The implementation file: text3d.cpp
#include <QFont>
#include <QList>
#include <QPainter>
#include <QOpenGLFunctions>
#include <QChar>
#include <gl/GLU.h>
#include "text3d.h"
typedef void (__stdcall *TessFuncPtr)(); // defintion of the callback function type
Text3D::Text3D() // nothing special in the constructor
: glyphthickness(1.0f)
, base(0)
{}
The initialization just loops through the first 256 char's and calls buildglyph() for each of them.
void
Text3D::initfont(QFont & f, float thickness)
{
font = &f;
fm = new QFontMetricsF(f);
glyphthickness = thickness;
if(base) // if we have display lists already, delete them first
glDeleteLists(base, 256);
base = glGenLists(256); // generate 256 display lists
if(base == 0)
{
qDebug() << "cannot create display lists.";
throw;
}
for(int i=0; i<256;+''i) // loop to build the first 256 glyphs
buildglyph(base+i, (char)i);
}
The print() function uses glCallLists() to "interpret" a complete string. See below how the char-by-char advance works.
void
Text3D::print(QString text)
{
glPushAttrib(GL_LIST_BIT); // Pushes The Display List Bits
glListBase(base); // Sets The Base Character to 0
glCallLists(text.length(), GL_UNSIGNED_BYTE, text.toLocal8Bit()); // Draws The Display List Text
glPopAttrib(); // Pops The Display List Bits
}
At the beginning we need to set up both, the tesselation and the display list.
void
Text3D::buildglyph(GLuint listbase, int c) // this is the main "workhorse" function. Create a displaylist with
// ID "listbase" from character "c"
GLUtriangulatorObj *tobj;
QPainterPath path;
path.addText(QPointF(0,0),*font, QString((char)c));
QList<QPolygonF> poly = path.toSubpathPolygons(); // get the glyph outline as a list of paths
// set up the tesselation
tobj = gluNewTess();
gluTessCallback(tobj, GLU_TESS_BEGIN, (TessFuncPtr)glBegin);
gluTessCallback(tobj, GLU_TESS_VERTEX, (TessFuncPtr)glVertex3dv);
gluTessCallback(tobj, GLU_TESS_END, (TessFuncPtr)glEnd);
gluTessProperty(tobj, GLU_TESS_WINDING_RULE, GLU_TESS_WINDING_ODD);
glNewList(listbase, GL_COMPILE); // start a new list
glShadeModel(GL_FLAT);
gluTessBeginPolygon(tobj, 0 ); // start tesselate
// first, calculate number of vertices.
int elements = 0; // number of total vertices in one glyph, counting all paths.
for (QList<QPolygonF>::iterator it = poly.begin(); it != poly.end(); it)
{
elements''= ('''it).size();
}
Now it's ready to tesselate the "front plate" polygon.
GLdouble''' vertices = (GLdouble ''') malloc(elements''' 3 * sizeof(GLdouble));
int j = 0;
for (QList<QPolygonF>::iterator it = poly.begin(); it != poly.end(); it+'') // enumerate paths
{
gluTessBeginContour(tobj);
int i = 0;
for (QPolygonF::iterator p = (*it).begin(); p != it->end(); p) // enumerate vertices
{
int off = j+i;
vertices[off+0] = p->rx();
vertices[off+1] = -p->ry();
vertices[off+2] = 0; // setting Z offset to zero.
gluTessVertex(tobj, &vertices[off], &vertices[off] );
i''=3; // array math
}
gluTessEndContour(tobj);
j ''= (*it).size()*3; // some more array math
}
gluTessEndPolygon(tobj);
Do the whole tesselation a second time with an offset applied for the "back plate". The "offset" (thickness) is set in
gluTessBeginPolygon(tobj, 0 );
j = 0;
for (QList<QPolygonF>::iterator it = poly.begin(); it != poly.end(); it)
{
gluTessBeginContour(tobj);
int i = 0;
for (QPolygonF::iterator p = (*it).begin(); p != it->end(); p)
{
int off = j+i;
vertices[off+0] = p->rx();
vertices[off+1] = -p->ry();
vertices[off+2] = -glyphthickness; // Z offset set to "minus glyphtickness"
gluTessVertex(tobj, &vertices[off], &vertices[off] );
i''=3;
}
gluTessEndContour(tobj);
j ''= (*it).size()*3;
}
gluTessEndPolygon(tobj);
free(vertices); // no need for the vertices anymore
The "wrapping" between the two "plates" is simple compared to the tesselation.
for (QList<QPolygonF>::iterator it = poly.begin(); it != poly.end(); it)
{
glBegin(GL_QUAD_STRIP);
QPolygonF::iterator p;
for (p = (*it).begin(); p != it->end(); p)
{
glVertex3f(p->rx(), -p->ry(), 0.0f);
glVertex3f(p->rx(), -p->ry(), -glyphthickness);
}
p = (*it).begin();
glVertex3f(p->rx(), -p->ry(), 0.0f); // draw the closing quad
glVertex3f(p->rx(), -p->ry(), -glyphthickness); // of the "wrapping"
glEnd();
}
This is where the char-by-char advance is done. Get the width from the font metrics and apply a glTranslate() with that value. This goes into the displaylist as well. (This may have side-effects as the matrix is not in the same "state" as before the call[[Image:|Image:]]!)
GLfloat gwidth = (float)fm->width©;
glTranslatef(gwidth ,0.0f,0.0f);
glEndList();
gluDeleteTess(tobj);
}
The whole thing can actually be used in a init() and render() functions within a OpenGL object like this:
init()
{
text = "Qt is great!";
QFont dfont("Comic Sans MS", 20);
QFontMetrics fm(dfont);
textwidth = fm.width(text);
qDebug() << "width of text: " << textwidth;
initfont(dfont,5);
}
render()
{
glEnable(GL_DEPTH_TEST);
glMatrixMode(GL_MODELVIEW); // To operate on model-view matrix
glLoadIdentity(); // Reset the model-view matrix
glTranslatef(0, 0.0f, –500.0f); // Move right and into the screen
glRotatef(rot, 1.0f, 0.0f, 0.0f); // Rotate On The X Axis
glRotatef(rot*1.5f, 0.0f, 1.0f, 0.0f); // Rotate On The Y Axis
glRotatef(rot*1.4f, 0.0f, 0.0f, 1.0f); // Rotate On The Z Axis
glColor3f( 1.0f*float(cos(rot/20.0f)), // Animate the color
1.0f*float(sin(rot/25.0f)),
1.0f-0.5f*float(cos(rot/17.0f))
);
glTranslatef(-textwidth/2.0f, 0.0f, 0.0f); // textwidth holds the pixel width of the text
// Print GL Text To The Screen
print(text);
glDisable(GL_DEPTH_TEST);
rot''=0.3f; // increase rot value
if(rot > 2000.f) rot = 0.0f; // wrap around at 2000
}