Draw Text as 3D Objects with OpenGL

From Qt Wiki
Revision as of 17:08, 28 June 2015 by Wieland (talk | contribs) (Cleanup)
Jump to: navigation, search

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

}