Difference between revisions of "Draw Text as 3D Objects with OpenGL"

From Qt Wiki
Jump to: navigation, search
(Add to category 3D)
 
(8 intermediate revisions by 3 users not shown)
Line 1: Line 1:
h1. Draw Text as 3D objects with OpenGL
+
{{LangSwitch}}
 +
[[Category:3D]]
 +
There are a couple of functions in WGL ([http://msdn.microsoft.com/en-us/library/windows/desktop/ee417756(v=vs.85).aspx Windows Graphics Library]) which can be used to draw text as nice 3D objects in OpenGL. There is a well known example at [http://nehe.gamedev.net/tutorial/outline_fonts/15004/ NeHe]. 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 [http://stackoverflow.com/questions/3514935/3d-text-on-qglwidget-in-qt-4-6-3/3516254#3516254 this example on Stackexchange] that got me started.
  
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.
  
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 &quot;more modern&amp;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" 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 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-&quot;plane&amp;quot; of a glyph and then create the &quot;wrapping&amp;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 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
 +
<code>
 +
#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
 +
};
 +
</code>
 +
 
 +
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
 +
 
 +
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.
 +
<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
 +
    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);
 +
}
 +
</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>
  
text3d.h<br /><code><br />#include &lt;QOpenGLFunctions&amp;gt;<br />#include &lt;QString&amp;gt;<br />#include &lt;QFont&amp;gt;<br />#include &lt;QFontMetricsF&amp;gt;
+
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>
  
class Text3D<br />{<br />public:<br /> Text3D();<br /> void initfont(QFont &amp; f, int thickness); // set up a font and specify the &quot;thickness&amp;quot;<br /> void print(QString text); // print it in 3D!
+
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>
  
private:<br /> void buildglyph(GLuint b, int c); // create one displaylist for character &quot;c&amp;quot;<br /> QFont * font;<br /> QFontMetricsF *fm;<br /> float glyphthickness;<br /> GLuint base; // the &quot;base&amp;quot; of our displaylists<br />};<br /></code>
+
The whole thing can actually be used in a init() and render() functions within a OpenGL object like this:
 +
<code>
 +
void 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);
 +
}
  
The implementation file: text3d.cpp<br /><code><br />#include &lt;QFont&amp;gt;<br />#include &lt;QList&amp;gt;<br />#include &lt;QPainter&amp;gt;<br />#include &lt;QOpenGLFunctions&amp;gt;<br />#include &lt;QChar&amp;gt;<br />#include &lt;gl/GLU.h&amp;gt;<br />#include &quot;text3d.h&amp;quot;
+
void render() {
 +
    glEnable(GL_DEPTH_TEST);
  
typedef void (__stdcall *TessFuncPtr)(); // defintion of the callback function type
+
    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
  
Text3D::Text3D() // nothing special in the constructor<br /> : glyphthickness(1.0f)<br /> , base(0)<br />{}<br /></code>
+
    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
  
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 &amp; f, float thickness)<br />{<br /> font = &amp;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);
+
    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))
 +
    );
  
base = glGenLists(256); // generate 256 display lists<br /> if(base == 0)<br /> {<br /> qDebug() &lt;&lt; &quot;cannot create display lists.&quot;;<br /> throw;<br /> }
 
  
for(int i=0; i&amp;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 &quot;interpret&amp;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 &quot;workhorse&amp;quot; function. Create a displaylist with<br /> // ID &quot;listbase&amp;quot; from character &quot;c&amp;quot;
+
    glTranslatef(-textwidth/2.0f, 0.0f, 0.0f); // textwidth holds the pixel width of the text
<br /> GLUtriangulatorObj *tobj;<br /> QPainterPath path;<br /> path.addText(QPointF(0,0),*font, QString((char)c));
+
    // Print GL Text To The Screen
<br /> QList&amp;lt;QPolygonF&amp;gt; poly = path.toSubpathPolygons(); // get the glyph outline as a list of paths
+
    print(text);
<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&amp;lt;QPolygonF&amp;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 &quot;front plate&amp;quot; polygon.<br /><code><br /> GLdouble''' vertices = (GLdouble ''') malloc(elements''' 3 * sizeof(GLdouble));<br /> int j = 0;<br /> for (QList&amp;lt;QPolygonF&amp;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-&gt;end(); p) // enumerate vertices<br /> {<br /> int off = j+i;<br /> vertices[off+0] = p-&gt;rx();<br /> vertices[off+1] = <s>p</s>&gt;ry();<br /> vertices[off+2] = 0; // setting Z offset to zero.<br /> gluTessVertex(tobj, &amp;vertices[off], &amp;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 &quot;back plate&amp;quot;. The &quot;offset&amp;quot; (thickness) is set in<br /><code><br /> gluTessBeginPolygon(tobj, 0 );<br /> j = 0;<br /> for (QList&amp;lt;QPolygonF&amp;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-&gt;end(); p)<br /> {<br /> int off = j+i;<br /> vertices[off+0] = p-&gt;rx();<br /> vertices[off+1] = <s>p</s>&gt;ry();<br /> vertices[off+2] = -glyphthickness; // Z offset set to &quot;minus glyphtickness&amp;quot;<br /> gluTessVertex(tobj, &amp;vertices[off], &amp;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 &quot;wrapping&amp;quot; between the two &quot;plates&amp;quot; is simple compared to the tesselation.<br /><code>
 
<br /> for (QList&amp;lt;QPolygonF&amp;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-&gt;end(); p)<br /> {<br /> glVertex3f(p-&gt;rx(), <s>p</s>&gt;ry(), 0.0f);<br /> glVertex3f(p-&gt;rx(), <s>p</s>&gt;ry(), <s>glyphthickness);<br /> }<br /> p = (*it).begin();<br /> glVertex3f(p</s>&gt;rx(), <s>p</s>&gt;ry(), 0.0f); // draw the closing quad<br /> glVertex3f(p-&gt;rx(), <s>p</s>&gt;ry(), <s>glyphthickness); // of the &quot;wrapping&amp;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 &quot;state&amp;quot; as before the call[[Image:|Image:]]!)<br /><code><br /> GLfloat gwidth = (float)fm</s>&gt;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 = &quot;Qt is great!&quot;;<br /> QFont dfont(&quot;Comic Sans MS&amp;quot;, 20);<br /> QFontMetrics fm(dfont);<br /> textwidth = fm.width(text);<br /> qDebug() &lt;&lt; &quot;width of text: &quot; &lt;&lt; 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);
+
    glDisable(GL_DEPTH_TEST);
<br /> glDisable(GL_DEPTH_TEST);
 
<br /> rot''=0.3f; // increase rot value<br /> if(rot &gt; 2000.f) rot = 0.0f; // wrap around at 2000<br /> }<br /></code>
 
  
== Update: ==
+
    rot=0.3f; // increase rot value
 +
    if (rot > 2000.f)
 +
        rot = 0.0f; // wrap around at 2000
 +
}
 +
</code>

Latest revision as of 15:44, 23 November 2016

En Ar Bg De El Es Fa Fi Fr Hi Hu It Ja Kn Ko Ms Nl Pl Pt Ru Sq Th Tr Uk Zh

There are a couple of functions in WGL (Windows Graphics Library) which can be used to draw text as nice 3D objects in OpenGL. There is a well known example at NeHe. 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 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

  1. include <QOpenGLFunctions>
  2. include <QString>
  3. include <QFont>
  4. 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

  1. include <QFont>
  2. include <QList>
  3. include <QPainter>
  4. include <QOpenGLFunctions>
  5. include <QChar>
  6. include <gl/GLU.h>
  7. 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: void 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);

}

void 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

}