Draw Text as 3D Objects with OpenGL

From Qt Wiki
Revision as of 15:36, 3 March 2015 by AutoSpider (talk | contribs) (Add "cleanup" tag)
Jump to: navigation, search
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
  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: 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 }