Multithreaded-OpenGL Rendering mit Picking-Fähigkeit mit Qt

Unlike the rest of the website this page is in German. I drag it along, since still quite a few people read it. However, it hasn’t been updated for a while and is only provided in case somebody still finds it useful.

Im Netz findet man einige Beispiele zum Thema Picking mit OpenGL (zum Beispiel Lesson 32 bei Nehe) und zum Thema Multithreaded-OpenGL Rendering mit Qt wird man bei Trolltechs Qt Quarterly: Glimpsing the Third Dimension fündig. Ein wenig schwieriger sieht es da schon aus, wenn man nach Code für GL-Picking mit Qt sucht. Aber auch hier gibt es mindestens ein Beispiel. Ich konnte jedoch kein einfaches Beispiel für Multithreaded-OpenGL Rendering mit Picking-Fähigkeit mit Qt finden.

Nun könnte man sagen: Na klar, ist ja auch so einfach. Genau! Und damit sich keiner mit einer so einfachen Sache aufhalten muss, kommt hier das Bespiel:

Beispiel: Multithreaded Cube

Dieses Beispiel ist eine Kombination aus zwei bereits existierenden. Das erste ist ein kleines Programm aus dem Buch C++ GUI Programming with Qt 3 von Jasmin Blanchette und Mark Summerfield (Kapitel 8; Programm: cube). Es demonstriert GL-Picking mit einem QGLWidget. Der Code kann hier heruntergeladen werden. Das zweite Beispiel ist der schon erwähnte Qt Quarterly Artikel Glimpsing the Third Dimension.

Der hier dargestellte Code übernimmt die Funktionalität des cube Programms, verlagert jedoch das Rendering in einen separaten Thread. Darüber hinaus wird GL-Picking in einer Funktion demonstriert, die nicht nur aus dem GL-Render-Thread aufgerufen werden kann.

Update

Ich habe das Beispiel von Qt3 nach Qt4 portiert (siehe Download). Dabei habe ich noch einige kleiner Mängel beseitigt, so dass es jetzt auch problemlos mit dem gcc-4.0 übersetzt werden kann.

2. Update

Aus einem mir nicht ersichtlichen Grund funktioniert dieses Beispiel nicht mit der derzeit populären Version 6.8.2 vom Xorg XServer (Debian Etch, Ubuntu Breezy Badger, u.a.). Der Aufruf von XInitThreads() resultiert in einem Mehr-oder-weniger-Absturz beim Anlegen des QApplication-Objektes. Dagegen gibt es keine Probleme mit Xfree86 (Debian Sarge) oder (ich wage es nicht zu sagen) MS Windows.

3. Update

Robert Visser schrieb mir: I looked into this problem on the internet and found a solution that worked for me: recompile qt4, with tablet support disabled (use an additional flag during configure -no-tablet ). This worked for me!

4. Update

Nach einer kleinen Änderung funktioniert jetzt alles auch mit GCC 4.1.2 und Qt 4.2.2

Screenshot

_images/threadedcube_screenshot.png

Das Wichtigste vom Quelltext im Überblick

main.cpp

35 #ifdef Q_WS_X11
36 #include <X11/Xlib.h>  // for XInitThreads() call
37 #endif

...

43 #ifdef Q_WS_X11
44     // this needs to be the first in the app to make Xlib calls thread save
45     // needed for OpenGl rendering threads
46     XInitThreads();
47 #endif

Dieser Header ist unter X11 wichtig um Xlib-Aufrufe mit XInitThreads() thread-safe machen zu können. Der Rest des Codes in main.cpp ist der normale Code für die Initialisierung einer minimalen Qt-Anwendung.

exampleglwidget.cpp

 39 ExampleGLWidget::ExampleGLWidget(QWidget *parent, const char *name)
 40     : QGLWidget(parent, name),
 41     glt(*this)
 42
 43 {
 44     setFormat(QGLFormat(DoubleBuffer | DepthBuffer));
 45
 46     // Buffer swap is handled in the rendering thread
 47     setAutoBufferSwap(false);
 48
 49     // start the rendering thread
 50     initRendering();
 51 }

Hier ist der Konstruktor der QGLWidget Subklasse. Es wird der OpenGL-Rendering-Thread glt mit einer Referenz auf das ExampleGLWidget initialisiert. Desweitern wird das automatische Umschalten des Buffers abgeschaltet, da dies im Render-Thread geschieht. Der Aufruf in Zeile 50 aktiviert den Render-Thread.

 78 void ExampleGLWidget::mouseDoubleClickEvent(QMouseEvent *event)
 79 {
 80     // get the name of the clicked surface
 81     int face = glt.faceAtPosition(event->pos());
 82     if (face != -1)
 83     {
 84         QColor color = QColorDialog::getColor(glt.faceColors[face],
 85                                               this);
 86         if (color.isValid())
 87         {
 88             glt.faceColors[face] = color;
 89         }
 90     }
 91 }

Diese Funktion demonstriert das Picking. Der Aufruf in Zeile 81 holt die ID-Nummer der angeklickten Würfel-Fläche. faceAtPosition() ist eine Methode der Rendering-Thread-Klasse, in der das GL-Picking implementiert ist (siehe unten). Zu beachten ist hier, dass dieser Methoden-Aufruf aus dem Haupt-GUI-Thread heraus erfolgt. Aus diesem Grund müssen alle Aufrufe in der faceAtPosition() besonders gesichert werden.

124 void ExampleGLWidget::resizeEvent( QResizeEvent * _e )
125 {
126     // signal the rendering thread that a resize is needed
127     glt.resizeViewport(_e->size());
128
129     render();
130 }

Ist eine Größenänderung des OpenGL-Viewports nötig, zum Beispiel weil die Fenstergröße geändert wurde, so wird die neue Größe dem Rendering-Thread mitgeteilt: resizeViewport(). Danach weckt ein render() den Thread auf und die Änderung wird umgesetzt.

132 void ExampleGLWidget::lockGLContext( )
133 {
134     // lock the render mutex for the calling thread
135     renderMutex().lock();
136     // make the render context current for the calling thread
137     makeCurrent();
138 }
139
140 void ExampleGLWidget::unlockGLContext( )
141 {
142     // release the render context for the calling thread
143     // to make it available for other threads
144     doneCurrent();
145     // unlock the render mutex for the calling thread
146     renderMutex().unlock();
147 }
148

Die beiden obigen Methoden dienen dazu, den Rendering-Kontext des QGLWidgets aus mehren Threads heraus benutzen zu können. In diesem Beispiel ist dies wichtig, da das Picking aus dem Haupt-GUI-Thread heraus aufgerufen wird. lockGLContext() sichert über einen QMutex den Zugriff auf den Rendering-Context für den aufrufenden Thread und aktiviert ihn: makeCurrent(). Analog dazu macht unlockGLContext() dies wieder rückgängig und gibt den Rendering-Context frei.

149 void ExampleGLWidget::render( )
150 {
151     // let the wait condition wake up the waiting thread
152     renderCondition().wakeAll();
153 }

Die render() Methode tut nicht mehr, als mit Hilfe einer QWaitCondition den wartenden Rendering-Thread aufzuwecken, damit dieser einen neuen Frame erzeugen kann.

examplerenderthread.cpp

 73 void ExampleRenderThread::run( )
 74 {
 75     // lock the render mutex of the Gl widget
 76     // and makes the rendering context of the glwidget current in this thread
 77     glw.lockGLContext();
 78

Den GL-Rendering-Kontext für das Rendern aus diesem Thread sichern.

 79     // general GL init
 80     initializeGL();
 81
 82     // do as long as the flag is true
 83     while( render_flag )
 84     {
 85         // resize the GL viewport if requested
 86         if (resize_flag)
 87         {
 88             resizeGL(viewport_size.width(), viewport_size.height());
 89             resize_flag = false;
 90         }
 91
 92         // render code goes here
 93         paintGL();
 94
 95         // swap the buffers of the GL widget
 96         glw.swapBuffers();
 97

Da das automatische Wechseln des Buffers im QGLWidget deaktiviert wurde, muss es hier manuell nach dem Zeichnen geschehen.

 98         glw.doneCurrent(); // release the GL render context to make picking work!
 99
100         // wait until the gl widget says that there is something to render
101         // glwidget.lockGlContext() had to be called before (see top of the function)!
102         // this will release the render mutex until the wait condition is met
103         // and will lock the render mutex again before exiting
104         // waiting this way instead of insane looping will not waste any CPU ressources
105         glw.renderCondition().wait(&glw.renderMutex());
106
107         glw.makeCurrent(); // get the GL render context back

Der Thread gibt nach jedem Frame den Rendering-Kontext frei und legt sich schlafen. Wenn er über die QWaitCondition wieder geweckt wird, wird der Rendering-Kontext erneut für diesen Thread gesichert.

112     }
113     // unlock the render mutex before exit
114     glw.unlockGLContext();
115 }
116

Am Ende muss der Rendering-Kontext wieder freigegeben werden.

146 void ExampleRenderThread::draw()
147 {

...

177     for (int i = 0; i < 6; ++i)
178     {
179         // assign names for each surface
180         // this make picking work
181         glLoadName(i);
182         glBegin(GL_QUADS);
183         glw.qglColor(faceColors[i]);
184         for (int j = 0; j < 4; ++j)
185         {
186             glVertex3f(coords[i][j][0], coords[i][j][1],
187                        coords[i][j][2]);
188         }
189         glEnd();
190     }
191 }
192

Zeile 181 ist der Schlüssel für das Picking: jeder Würfelfläche wird hier ein “Name” zugewiesen, über den diese identifiziert werden kann. Leider gibt es davon nur 64. Aber das ist ein anderes Thema.

193 int ExampleRenderThread::faceAtPosition(const QPoint &pos)
194 {
195     // we need to lock the rendering context
196     glw.lockGLContext();
197
198     // this is the same as in every OpenGL picking example
199     const int MaxSize = 512; // see below for an explanation on the buffer content
200     GLuint buffer[MaxSize];
201     GLint viewport[4];
202
203     glGetIntegerv(GL_VIEWPORT, viewport);
204     glSelectBuffer(MaxSize, buffer);
205     // enter select mode
206     glRenderMode(GL_SELECT);
207
208     glInitNames();
209     glPushName(0);
210
211     glMatrixMode(GL_PROJECTION);
212     glPushMatrix();
213     glLoadIdentity();
214     gluPickMatrix((GLdouble)pos.x(),
215                   (GLdouble)(viewport[3] - pos.y()),
216                   5.0, 5.0, viewport);
217     GLfloat x = (GLfloat)viewport_size.width() / viewport_size.height();
218     glFrustum(-x, x, -1.0, 1.0, 4.0, 15.0);
219     draw();
220     glMatrixMode(GL_PROJECTION);
221     glPopMatrix();
222
223
224     // finally release the rendering context again
225     if (!glRenderMode(GL_RENDER))
226     {
227         glw.unlockGLContext();
228         return -1;
229     }
230     glw.unlockGLContext();
231
232     // Each hit takes 4 items in the buffer.
233     // The first item is the number of names on the name stack when the hit occured.
234     // The second item is the minimum z value of all the verticies that intersected
235     // the viewing area at the time of the hit. The third item is the maximum z value
236     // of all the vertices that intersected the viewing area at the time of the hit
237     // and the last item is the content of the name stack at the time of the hit
238     // (name of the object). We are only interested in the object name
239     // (number of the surface).
240
241     // return the name of the clicked surface
242     return buffer[3];
243 }

Hier nun die Implementation des GL-Picking. Damit diese Methode gefahrlos aus einem anderen Thread aufgerufen werden kann, muss sie sich erst den Rendering-Kontext sichern. Wurde eine Fläche des Würfels angeklickt so findet sich der “Name” der Fläche (siehe Zeile 181 oben) im Hit-Buffer an der vierten Stelle.

Lizenz

Dieser Code ist unter der GNU Public License (GPL) lizensiert.