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 | |||
There are a couple of functions in | 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:<br />* It uses the fixed-function pipeline. (GL_QUAD_STRIP's and DisplayLists). Im sure this '''can''' be done in a "more modern&quot; way with VBO's, but my OpenGL knowlegde is not yet at that level.<br />* it relies on GLU for polygon tesselation. There might be better alternatives around or even some within Qt.<br />* No real character set (or even UTF) handling. It only uses the first 256 characters.<br />* 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&quot; of a glyph and then create the "wrapping&quot; 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 | 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<br /><code><br />#include <QOpenGLFunctions&gt;<br />#include <QString&gt;<br />#include <QFont&gt;<br />#include <QFontMetricsF&gt; | |||
class Text3D<br />{<br />public:<br /> Text3D();<br /> void initfont(QFont & f, int thickness); // set up a font and specify the "thickness&quot;<br /> void print(QString text); // print it in 3D! | |||
private:<br /> void buildglyph(GLuint b, int c); // create one displaylist for character "c&quot;<br /> QFont * font;<br /> QFontMetricsF *fm;<br /> float glyphthickness;<br /> GLuint base; // the "base&quot; of our displaylists<br />};<br /></code> | |||
The | The implementation file: text3d.cpp<br /><code><br />#include <QFont&gt;<br />#include <QList&gt;<br />#include <QPainter&gt;<br />#include <QOpenGLFunctions&gt;<br />#include <QChar&gt;<br />#include <gl/GLU.h&gt;<br />#include "text3d.h&quot; | ||
typedef void (__stdcall *TessFuncPtr)(); // defintion of the callback function type | |||
Text3D::Text3D() // nothing special in the constructor<br /> : glyphthickness(1.0f)<br /> , base(0)<br />{}<br /></code> | |||
The initialization just loops through the first 256 char's and calls buildglyph() for each of them.<br /><code><br />void<br />Text3D::initfont(QFont & f, float thickness)<br />{<br /> font = &f;<br /> fm = new QFontMetricsF(f);<br /> glyphthickness = thickness;<br /> if(base) // if we have display lists already, delete them first<br /> glDeleteLists(base, 256); | |||
base = glGenLists(256); // generate 256 display lists<br /> if(base == 0)<br /> {<br /> qDebug() << "cannot create display lists.";<br /> throw;<br /> } | |||
for(int i=0; i&lt;256;+''i) // loop to build the first 256 glyphs<br /> buildglyph(base+i, (char)i);<br />}<br /></code><br />The print() function uses glCallLists() to "interpret&quot; a complete string. See below how the char-by-char advance works.<br /><code><br />void<br />Text3D::print(QString text)<br />{<br /> glPushAttrib(GL_LIST_BIT); // Pushes The Display List Bits<br /> glListBase(base); // Sets The Base Character to 0<br /> glCallLists(text.length(), GL_UNSIGNED_BYTE, text.toLocal8Bit()); // Draws The Display List Text<br /> glPopAttrib(); // Pops The Display List Bits<br />}<br /></code><br />At the beginning we need to set up both, the tesselation and the display list.<br /><code><br />void<br />Text3D::buildglyph(GLuint listbase, int c) // this is the main "workhorse&quot; function. Create a displaylist with<br /> // ID "listbase&quot; from character "c&quot; | |||
<br /> GLUtriangulatorObj *tobj;<br /> QPainterPath path;<br /> path.addText(QPointF(0,0),*font, QString((char)c)); | |||
<br /> QList&lt;QPolygonF&gt; poly = path.toSubpathPolygons(); // get the glyph outline as a list of paths | |||
<br /> // set up the tesselation<br /> tobj = gluNewTess();<br /> gluTessCallback(tobj, GLU_TESS_BEGIN, (TessFuncPtr)glBegin);<br /> gluTessCallback(tobj, GLU_TESS_VERTEX, (TessFuncPtr)glVertex3dv);<br /> gluTessCallback(tobj, GLU_TESS_END, (TessFuncPtr)glEnd);<br /> gluTessProperty(tobj, GLU_TESS_WINDING_RULE, GLU_TESS_WINDING_ODD); | |||
<br /> glNewList(listbase, GL_COMPILE); // start a new list<br /> glShadeModel(GL_FLAT);<br /> gluTessBeginPolygon(tobj, 0 ); // start tesselate | |||
<br /> // first, calculate number of vertices.<br /> int elements = 0; // number of total vertices in one glyph, counting all paths.<br /> for (QList&lt;QPolygonF&gt;::iterator it = poly.begin(); it != poly.end(); it)<br /> {<br /> elements''= ('''it).size();<br /> }<br /></code><br />Now it's ready to tesselate the "front plate&quot; polygon.<br /><code><br /> GLdouble''' vertices = (GLdouble ''') malloc(elements''' 3 * sizeof(GLdouble));<br /> int j = 0;<br /> for (QList&lt;QPolygonF&gt;::iterator it = poly.begin(); it != poly.end(); it+'') // enumerate paths<br /> {<br /> gluTessBeginContour(tobj);<br /> int i = 0;<br /> for (QPolygonF::iterator p = (*it).begin(); p != it->end(); p) // enumerate vertices<br /> {<br /> int off = j+i;<br /> vertices[off+0] = p->rx();<br /> vertices[off+1] = <s>p</s>>ry();<br /> vertices[off+2] = 0; // setting Z offset to zero.<br /> gluTessVertex(tobj, &vertices[off], &vertices[off] );<br /> i''=3; // array math<br /> }<br /> gluTessEndContour(tobj);<br /> j ''= (*it).size()*3; // some more array math<br /> }<br /> gluTessEndPolygon(tobj);<br /></code><br />Do the whole tesselation a second time with an offset applied for the "back plate&quot;. The "offset&quot; (thickness) is set in<br /><code><br /> gluTessBeginPolygon(tobj, 0 );<br /> j = 0;<br /> for (QList&lt;QPolygonF&gt;::iterator it = poly.begin(); it != poly.end(); it)<br /> {<br /> gluTessBeginContour(tobj);<br /> int i = 0;<br /> for (QPolygonF::iterator p = (*it).begin(); p != it->end(); p)<br /> {<br /> int off = j+i;<br /> vertices[off+0] = p->rx();<br /> vertices[off+1] = <s>p</s>>ry();<br /> vertices[off+2] = -glyphthickness; // Z offset set to "minus glyphtickness&quot;<br /> gluTessVertex(tobj, &vertices[off], &vertices[off] );<br /> i''=3;<br /> }<br /> gluTessEndContour(tobj);<br /> j ''= (*it).size()*3;<br /> }<br /> gluTessEndPolygon(tobj); | |||
<br /> free(vertices); // no need for the vertices anymore<br /></code><br />The "wrapping&quot; between the two "plates&quot; is simple compared to the tesselation.<br /><code> | |||
<br /> for (QList&lt;QPolygonF&gt;::iterator it = poly.begin(); it != poly.end(); it)<br /> {<br /> glBegin(GL_QUAD_STRIP);<br /> QPolygonF::iterator p;<br /> for (p = (*it).begin(); p != it->end(); p)<br /> {<br /> glVertex3f(p->rx(), <s>p</s>>ry(), 0.0f);<br /> glVertex3f(p->rx(), <s>p</s>>ry(), <s>glyphthickness);<br /> }<br /> p = (*it).begin();<br /> glVertex3f(p</s>>rx(), <s>p</s>>ry(), 0.0f); // draw the closing quad<br /> glVertex3f(p->rx(), <s>p</s>>ry(), <s>glyphthickness); // of the "wrapping&quot;<br /> glEnd();<br /> }<br /></code><br />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&quot; as before the call[[Image:|Image:]]!)<br /><code><br /> GLfloat gwidth = (float)fm</s>>width©;<br /> glTranslatef(gwidth ,0.0f,0.0f); | |||
<br /> glEndList();<br /> gluDeleteTess(tobj);<br />}<br /></code><br />The whole thing can actually be used in a init() and render() functions within a OpenGL object like this:<br /><code><br />init()<br />{<br /> text = "Qt is great!";<br /> QFont dfont("Comic Sans MS&quot;, 20);<br /> QFontMetrics fm(dfont);<br /> textwidth = fm.width(text);<br /> qDebug() << "width of text: " << textwidth; | |||
<br /> initfont(dfont,5);<br /> } | |||
<br />render()<br />{<br /> glEnable(GL_DEPTH_TEST); | |||
<br /> glMatrixMode(GL_MODELVIEW); // To operate on model-view matrix<br /> glLoadIdentity(); // Reset the model-view matrix<br /> glTranslatef(0, 0.0f, –500.0f); // Move right and into the screen | |||
<br /> glRotatef(rot, 1.0f, 0.0f, 0.0f); // Rotate On The X Axis<br /> glRotatef(rot*1.5f, 0.0f, 1.0f, 0.0f); // Rotate On The Y Axis<br /> glRotatef(rot*1.4f, 0.0f, 0.0f, 1.0f); // Rotate On The Z Axis | |||
<br /> glColor3f( 1.0f*float(cos(rot/20.0f)), // Animate the color<br /> 1.0f*float(sin(rot/25.0f)),<br /> 1.0f-0.5f*float(cos(rot/17.0f))<br /> ); | |||
<br /> glTranslatef(-textwidth/2.0f, 0.0f, 0.0f); // textwidth holds the pixel width of the text<br /> // Print GL Text To The Screen<br /> print(text); | |||
<br /> glDisable(GL_DEPTH_TEST); | |||
<br /> rot''=0.3f; // increase rot value<br /> if(rot > 2000.f) rot = 0.0f; // wrap around at 2000<br /> }<br /></code> | |||
== Update: == |
Revision as of 07:06, 24 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
<br />#include <QOpenGLFunctions&gt;<br />#include <QString&gt;<br />#include <QFont&gt;<br />#include <QFontMetricsF&gt;
class Text3D<br />{<br />public:<br /> Text3D();<br /> void initfont(QFont & f, int thickness); // set up a font and specify the "thickness&quot;<br /> void print(QString text); // print it in 3D!
private:<br /> void buildglyph(GLuint b, int c); // create one displaylist for character "c&quot;<br /> QFont * font;<br /> QFontMetricsF *fm;<br /> float glyphthickness;<br /> GLuint base; // the "base&quot; of our displaylists<br />};<br />
The implementation file: text3d.cpp
<br />#include <QFont&gt;<br />#include <QList&gt;<br />#include <QPainter&gt;<br />#include <QOpenGLFunctions&gt;<br />#include <QChar&gt;<br />#include <gl/GLU.h&gt;<br />#include "text3d.h&quot;
typedef void (__stdcall *TessFuncPtr)(); // defintion of the callback function type
Text3D::Text3D() // nothing special in the constructor<br /> : glyphthickness(1.0f)<br /> , base(0)<br />{}<br />
The initialization just loops through the first 256 char's and calls buildglyph() for each of them.
<br />void<br />Text3D::initfont(QFont & f, float thickness)<br />{<br /> font = &f;<br /> fm = new QFontMetricsF(f);<br /> glyphthickness = thickness;<br /> if(base) // if we have display lists already, delete them first<br /> glDeleteLists(base, 256);
base = glGenLists(256); // generate 256 display lists<br /> if(base == 0)<br /> {<br /> qDebug() << "cannot create display lists.";<br /> throw;<br /> }
for(int i=0; i&lt;256;+''i) // loop to build the first 256 glyphs<br /> buildglyph(base+i, (char)i);<br />}<br />
The print() function uses glCallLists() to "interpret" a complete string. See below how the char-by-char advance works.
<br />void<br />Text3D::print(QString text)<br />{<br /> glPushAttrib(GL_LIST_BIT); // Pushes The Display List Bits<br /> glListBase(base); // Sets The Base Character to 0<br /> glCallLists(text.length(), GL_UNSIGNED_BYTE, text.toLocal8Bit()); // Draws The Display List Text<br /> glPopAttrib(); // Pops The Display List Bits<br />}<br />
At the beginning we need to set up both, the tesselation and the display list.
<br />void<br />Text3D::buildglyph(GLuint listbase, int c) // this is the main "workhorse&quot; function. Create a displaylist with<br /> // ID "listbase&quot; from character "c&quot;
<br /> GLUtriangulatorObj *tobj;<br /> QPainterPath path;<br /> path.addText(QPointF(0,0),*font, QString((char)c));
<br /> QList&lt;QPolygonF&gt; poly = path.toSubpathPolygons(); // get the glyph outline as a list of paths
<br /> // set up the tesselation<br /> tobj = gluNewTess();<br /> gluTessCallback(tobj, GLU_TESS_BEGIN, (TessFuncPtr)glBegin);<br /> gluTessCallback(tobj, GLU_TESS_VERTEX, (TessFuncPtr)glVertex3dv);<br /> gluTessCallback(tobj, GLU_TESS_END, (TessFuncPtr)glEnd);<br /> gluTessProperty(tobj, GLU_TESS_WINDING_RULE, GLU_TESS_WINDING_ODD);
<br /> glNewList(listbase, GL_COMPILE); // start a new list<br /> glShadeModel(GL_FLAT);<br /> gluTessBeginPolygon(tobj, 0 ); // start tesselate
<br /> // first, calculate number of vertices.<br /> int elements = 0; // number of total vertices in one glyph, counting all paths.<br /> for (QList&lt;QPolygonF&gt;::iterator it = poly.begin(); it != poly.end(); it)<br /> {<br /> elements''= ('''it).size();<br /> }<br />
Now it's ready to tesselate the "front plate" polygon.
<br /> GLdouble''' vertices = (GLdouble ''') malloc(elements''' 3 * sizeof(GLdouble));<br /> int j = 0;<br /> for (QList&lt;QPolygonF&gt;::iterator it = poly.begin(); it != poly.end(); it+'') // enumerate paths<br /> {<br /> gluTessBeginContour(tobj);<br /> int i = 0;<br /> for (QPolygonF::iterator p = (*it).begin(); p != it->end(); p) // enumerate vertices<br /> {<br /> int off = j+i;<br /> vertices[off+0] = p->rx();<br /> vertices[off+1] = <s>p</s>>ry();<br /> vertices[off+2] = 0; // setting Z offset to zero.<br /> gluTessVertex(tobj, &vertices[off], &vertices[off] );<br /> i''=3; // array math<br /> }<br /> gluTessEndContour(tobj);<br /> j ''= (*it).size()*3; // some more array math<br /> }<br /> gluTessEndPolygon(tobj);<br />
Do the whole tesselation a second time with an offset applied for the "back plate". The "offset" (thickness) is set in
<br /> gluTessBeginPolygon(tobj, 0 );<br /> j = 0;<br /> for (QList&lt;QPolygonF&gt;::iterator it = poly.begin(); it != poly.end(); it)<br /> {<br /> gluTessBeginContour(tobj);<br /> int i = 0;<br /> for (QPolygonF::iterator p = (*it).begin(); p != it->end(); p)<br /> {<br /> int off = j+i;<br /> vertices[off+0] = p->rx();<br /> vertices[off+1] = <s>p</s>>ry();<br /> vertices[off+2] = -glyphthickness; // Z offset set to "minus glyphtickness&quot;<br /> gluTessVertex(tobj, &vertices[off], &vertices[off] );<br /> i''=3;<br /> }<br /> gluTessEndContour(tobj);<br /> j ''= (*it).size()*3;<br /> }<br /> gluTessEndPolygon(tobj);
<br /> free(vertices); // no need for the vertices anymore<br />
The "wrapping" between the two "plates" is simple compared to the tesselation.
<br /> for (QList&lt;QPolygonF&gt;::iterator it = poly.begin(); it != poly.end(); it)<br /> {<br /> glBegin(GL_QUAD_STRIP);<br /> QPolygonF::iterator p;<br /> for (p = (*it).begin(); p != it->end(); p)<br /> {<br /> glVertex3f(p->rx(), <s>p</s>>ry(), 0.0f);<br /> glVertex3f(p->rx(), <s>p</s>>ry(), <s>glyphthickness);<br /> }<br /> p = (*it).begin();<br /> glVertex3f(p</s>>rx(), <s>p</s>>ry(), 0.0f); // draw the closing quad<br /> glVertex3f(p->rx(), <s>p</s>>ry(), <s>glyphthickness); // of the "wrapping&quot;<br /> glEnd();<br /> }<br />
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:]]!)
<br /> GLfloat gwidth = (float)fm</s>>width©;<br /> glTranslatef(gwidth ,0.0f,0.0f);
<br /> glEndList();<br /> gluDeleteTess(tobj);<br />}<br />
The whole thing can actually be used in a init() and render() functions within a OpenGL object like this:
<br />init()<br />{<br /> text = "Qt is great!";<br /> QFont dfont("Comic Sans MS&quot;, 20);<br /> QFontMetrics fm(dfont);<br /> textwidth = fm.width(text);<br /> qDebug() << "width of text: " << textwidth;
<br /> initfont(dfont,5);<br /> }
<br />render()<br />{<br /> glEnable(GL_DEPTH_TEST);
<br /> glMatrixMode(GL_MODELVIEW); // To operate on model-view matrix<br /> glLoadIdentity(); // Reset the model-view matrix<br /> glTranslatef(0, 0.0f, –500.0f); // Move right and into the screen
<br /> glRotatef(rot, 1.0f, 0.0f, 0.0f); // Rotate On The X Axis<br /> glRotatef(rot*1.5f, 0.0f, 1.0f, 0.0f); // Rotate On The Y Axis<br /> glRotatef(rot*1.4f, 0.0f, 0.0f, 1.0f); // Rotate On The Z Axis
<br /> glColor3f( 1.0f*float(cos(rot/20.0f)), // Animate the color<br /> 1.0f*float(sin(rot/25.0f)),<br /> 1.0f-0.5f*float(cos(rot/17.0f))<br /> );
<br /> glTranslatef(-textwidth/2.0f, 0.0f, 0.0f); // textwidth holds the pixel width of the text<br /> // Print GL Text To The Screen<br /> print(text);
<br /> glDisable(GL_DEPTH_TEST);
<br /> rot''=0.3f; // increase rot value<br /> if(rot > 2000.f) rot = 0.0f; // wrap around at 2000<br /> }<br />