Apercevoir la troisième dimension

| Trond Kjernåsen | traducteur : Guillaume Belz | Qt Quarterly |

Le module QGL de Qt facilite l'intégration de rendu OpenGL dans des applications Qt. Dans cet article, on va montrer comment créer un QGLContext personnalisé qui implémente des fonctionnalités spécifiques non proposées par QGL. On présentera également un exemple simple d'application multiplateforme et multithread avec Qt et OpenGL.

Commentez

- Titre : Apercevoir la troisième dimension

- Auteurs : Trond Kjernåsen, traducteur : Guillaume Belz, Qt Quarterly

- Parution : 8 février 2012

- Mise à jour : 17 février 2012

- Licence : Copyright ® 2003 Trond Kjernåsen. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. L'article original

- II. Créer un contexte personnalisé

- III. Écrire des applications GL multithreadées

- IV. Conseils généraux

- V. Remerciements

I - L'article original

Qt Quarterly est une revue trimestrielle électronique proposée par Qt à destination des développeurs et utilisateurs de Qt. Vous pouvez trouver les versions originales.

Nokia, Qt, Qt Quarterly et leurs logos sont des marques déposées de Nokia Corporation en Finlande et/ou dans les autres pays. Les autres marques déposées sont détenues par leurs propriétaires respectifs.

Cet article est la traduction de l'article Glimpsing the Third Dimension de Trond Kjernåsen paru dans la Qt Quarterly Issue 6.

Cet article est une traduction d'un des tutoriels écrits par Nokia Corporation and/or its subsidiary(-ies) et inclus dans la documentation de Qt, en anglais. Les éventuels problèmes résultant d'une mauvaise traduction ne sont pas imputables à Nokia.

NDT. Cet article a été écrit en 2003 pour Qt 3.2. Les mises à jour pour Qt 4.8 sont indiquées en note de bas de page. Même si cet article date un peu, il reste intéressant pour présenter la programmation multiplateforme et multithread avec QtOpenGL.

II - Créer un contexte personnalisé

En raison des différences d'implémentation des versions de GL (OpenGL(1)) et des fonctionnalités supportées, il peut être nécessaire de dériver QGLContext pour mieux contrôler le format du contexte GL choisi. Le premier exemple montre comment créer un contexte GL avec un tampon de profondeur de 32 bits, s'il est disponible.

QGLContext possède un ensemble de fonctions spécifiques pour chaque plateforme pour tester et choisir un format de contexte en se basant sur le QGLFormat activé pour un widget en particulier. Ces fonctions sont :

- choosePixelFormat () pour Windows ;

- chooseVisual () pour X11 ;

- chooseMacVisual () pour Mac OS X.

Tout ce qu'il faut faire est de réimplémenter chacune de ces fonctions dans une classe dérivée pour les plateformes visées. Ci-dessous, on présente un exemple d'implémentation multiplateforme.

Définition de la classe commune

#include <qapplication.h>
#include <qgl.h>

#if defined(Q_WS_X11)
#    include <GL/glx.h>
#endif
#if defined(Q_WS_MAC)
#include <agl.h>
#endif

class CustomContext : public QGLContext
{
public:
CustomContext(const QGLFormat &fmt, QPaintDevice *dev)
: QGLContext(fmt, dev) {}

protected:
#if defined(Q_WS_WIN)
int choosePixelFormat(void *p, HDC hdc);
#elif defined(Q_WS_X11)
void *chooseVisual();
#elif defined(Q_WS_MAC)
void *chooseMacVisual(GDHandle gdev);
#endif
};

L'implémentation de la fonction spécifique à Windows :

#if defined(Q_WS_WIN)
int CustomContext::choosePixelFormat(void *p, HDC pdc)
{
PIXELFORMATDESCRIPTOR *pfd = (PIXELFORMATDESCRIPTOR *)p;
int pfiMax = DescribePixelFormat(pdc, 0, 0, NULL);
int pfi;
for (pfi = 1; pfi <= pfiMax; pfi++) {
DescribePixelFormat(pdc, pfi, sizeof(PIXELFORMATDESCRIPTOR), pfd);
if (pfd->cDepthBits == 32)
return pfi;
}
pfi = QGLContext::choosePixelFormat(pfd, pdc);
qWarning("32-bit depth unavailable: using %d bits", pfd->cDepthBits);
return pfi;
}
#endif

Dans l'implémentation pour Windows, on parcourt la liste des formats de pixel disponibles jusqu'à trouver celui qui a un tampon de profondeur de 32 bits. Dans une application réelle, on aura probablement d'autres critères à vérifier également. Si un tampon de profondeur de 32 bits ne peut être trouvé, on retombe sur l'implémentation par défaut de QGLContext.

Ensuite, l'implémentation pour X11 :

#if defined(Q_WS_X11)
void *CustomContext::chooseVisual()
{
GLint attribs[] = {GLX_RGBA, GLX_DEPTH_SIZE, 32, None};
XVisualInfo *vis = glXChooseVisual(device()->x11Display(),
device()->x11Screen(), attribs);
if (vis) {
GLint depth = 0;
glXGetConfig(device()->x11Display(), vis, GLX_DEPTH_SIZE, &depth);
if (depth != 32)
qWarning("32-bit depth unavailable: using %d bits", depth);
return vis;
}
return QGLContext::chooseVisual();
}
#endif

Sous X11, on peut demander directement un tampon de profondeur de 32 bits. Cependant, la réussite de glXChooseVisual() n'assure par l'obtention d'un tampon de la profondeur souhaitée. glXChooseVisual() va retourner le contexte qui répond au mieux à la spécification passée dans le tableau de paramètres. Si aucun contexte ne peut être obtenu, on retombe sur l'implémentation par défaut.

Enfin, l'implémentation pour Mac OS X :

#if defined(Q_WS_MAC)
void *CustomContext::chooseMacVisual(GDHandle gdev)
{
GLint attribs[] = {AGL_ALL_RENDERERS, AGL_RGBA, AGL_DEPTH_SIZE, 32, AGL_NONE};
AGLPixelFormat fmt = aglChoosePixelFormat(NULL, 0, attribs);
if (fmt) {
GLint depth;
aglDescribePixelFormat(fmt, AGL_DEPTH_SIZE, &depth);
if (depth != 32)
qWarning("32-bit depth unavailable: using %d bits", depth);
return fmt;
}
return QGLContext::chooseMacVisual(gdev);
}
#endif

La fonction aglChoosePixelFormat() est similaire à la fonction glXChooseVisual() de X11 : elle retourne un format de pixel qui correspond le mieux à la spécification. Si elle ne retourne rien, on retombe sur l'implémentation par défaut.

On utilise le contexte en l'affectant à un QGLWidget par le biais de la fonction QGLWidget::setContext(). Elle n'est pas documentée dans l'API publique de Qt, parce qu'elle ne fonctionne pas comme prévu dans toutes les situations sur toutes les plateformes. Elle ne fonctionnera que si le widget n'a pas encore été affiché. Dans Qt 3.2, QGLWidget est fourni avec un nouveau constructeur qui prend un paramètre QGLContext : cela rend setContext () redondant. Attribuer un nouveau contexte peut être fait comme ceci :

MyGLWidget gl;
CustomContext *cx = new CustomContext(gl.format(), &gl);
gl.setContext(cx);
gl.show();

On note que le contexte personnalisé est créé sur le tas. Le QGLWidget prend la responsabilité du pointeur sur CustomContext et détruit ce dernier lorsque le widget GL est détruit GL, selon la méthode habituelle de Qt.

III - Écrire des applications GL multithreadées

Il est parfaitement possible d'écrire des applications GL multiplateformes et multithreadées avec Qt. Dans cette section, on présente un programme simple qui montre comment créer des threads qui peuvent réaliser un rendu dans les widgets GL créés dans le thread principal de Qt.

Tout d'abord, on doit s'assurer que Qt est configuré avec le support des threads et la bibliothèque GL utilisée est thread-safe. Les bibliothèques GL fournies avec les versions récentes de Windows et Mac OS X sont thread-safe, mais, sous Unix/X11, cela n'est peut-être pas toujours le cas, ce point reste toujours à vérifier.

Un problème supplémentaire sous X11 est que Xlib (et donc GLX) n'est pas intrinsèquement thread-safe ; appeler Xlib dans deux threads différents en même temps se traduit généralement par un plantage. Les fonctions de GLX qui sont appelées par le module QGL (par exemple, pour échanger des contextes GL ou échanger des tampons) appellent également Xlib, ce qui signifie que ces appels doivent être protégés sous X11. La solution simple est d'appeler XInitThreads() avant de créer l'objet QApplication dans le programme. XInitThreads() doit être le premier appel à Xlib dans une application pour qu'elle fonctionne de manière fiable. Cela aura pour effet de rendre Xlib thread-safe.

XInitThreads() est un ajout relativement récent à Xlib, toutes les implémentations ne supportent donc pas cette fonction. Il est possible de contourner ce problème en sérialisant soi-même les appels à Xlib avec la bibliothèque mutex de Qt. Cela implique d'ajouter des appels à qApp->lock() et qApp->unlock() dans le code de la boucle de rendu, ainsi que de changer la façon dont les objets GLWidget et GLThread sont détruits : on doit s'assurer que GLThread se termine avant de détruire les GLWidget (par exemple, en envoyant un événement personnalisé au GLWidget juste avant la fin du thread).

Dans Qt, on ne doit pas créer de widgets en dehors du thread principal, puisque les classes basées sur QObject (y compris QGLWidget) ne sont ni réentrantes ni thread-safe. La solution est de créer le widget dans le thread principal et de laisser les threads de rendu réaliser le rendu (c'est-à-dire d'effectuer les appels à OpenGL).

Une façon de faire est de créer une sous-classe de QGLWidget contenant une sous-classe de QThread avec des fonctions pour démarrer et arrêter le rendu.

Voici un exemple de cette approche avec un thread réalisant le rendu en continu d'un tétraèdre tournant. Cependant, le rendu peut facilement être modifié pour réagir aux événements de mise à jour de QGLWidget. L'exemple commence par une sous-classe de QThread, GLThread. En voici la définition :

class GLThread : public QThread
{
public:
GLThread(GLWidget *glWidget);
void resizeViewport(const QSize &size);
void run();
void stop();

private:
bool doRendering;
bool doResize;
int w;
int h;
int rotAngle;
GLWidget *glw;
};

Son implémentation.

GLThread::GLThread(GLWidget *gl)
: QThread(), glw(gl)
{
doRendering = true;
doResize = false;
}

void GLThread::stop()
{
doRendering = false;
}

void GLThread::resizeViewport(const QSize &size)
{
w = size.width();
h = size.height();
doResize = true;
}

void GLThread::run()
{
srand(QTime::currentTime().msec());
rotAngle = rand() % 360;

glw->makeCurrent();
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
glOrtho(-5.0, 5.0, -5.0, 5.0, 1.0, 100.0);
glMatrixMode(GL_MODELVIEW);
glViewport(0, 0, 200, 200);
glClearColor(0.0, 0.0, 0.0, 1.0);
glShadeModel(GL_SMOOTH);
glEnable(GL_DEPTH_TEST);

while (doRendering) {
if (doResize) {
glViewport(0, 0, w, h);
doResize = false;
}
// Rendering code goes here
glw->swapBuffers();
msleep(40);
}
}

La fonction stop() arrêtera le rendu et terminera le thread. La fonction resizeViewport() peut être appelée par le thread principal pour informer le thread de rendu que la taille du widget a changé et lui laisser redimensionner la fenêtre GL en conséquence.

Le cœur de GLThread est sa fonction run(), qui réalise le rendu GL. Un appel à makeCurrent() active le contexte du widget GL dans ce thread. Puisque l'on réalise l'échange des tampons soi-même, on doit appeler SwapBuffers(). On est autorisé à faire ces appels avec QGLWidget, car ils sont adaptés avec des appels spécifiques en fonction des plateformes (GLX, AGL ou WGL) et ne génèrent aucun événement dans Qt. Cela aurait causé des problèmes sous X11 s'il n'y avait pas des appels thread-safe à Xlib.

On a omis le code de rendu, il n'est pas pertinent. Avant de boucler, on met le thread en sommeil pendant un petit moment en appelant msleep(), de sorte que les threads n'entrent pas en concurrence et encombrent le système avec des appels de rendu. La fonction msleep() permet aussi d'améliorer indirectement la vitesse du rendu.

La classe GLWidget.

class GLWidget : public QGLWidget
{
public:
GLWidget(QWidget *parent);
void startRendering();
void stopRendering();

protected:
void resizeEvent(QResizeEvent *evt);
void paintEvent(QPaintEvent *);
void closeEvent(QCloseEvent *evt);

GLThread glt;
};

GLWidget::GLWidget(QWidget *parent)
: QGLWidget(parent, "GLWidget", 0, WdestructiveClose), glt(this)
{
setAutoBufferSwap(false);
resize(320, 240);
}

void GLWidget::startRendering()
{
glt.start();
}

void GLWidget::stopRendering()
{
glt.stop();
glt.wait();
}

void GLWidget::resizeEvent(QResizeEvent *evt)
{
glt.resizeViewport(evt->size());
}

void GLWidget::paintEvent(QPaintEvent *)
{
// Handled by the GLThread.
}

void GLWidget::closeEvent(QCloseEvent *evt)
{
stopRendering();
QGLWidget::closeEvent(evt);
}

La classe héritant de GLWidget réserve un espace pour la fenêtre GL. Puisque le thread de rendu intercepte toutes les mises à jour destinées au widget, la fonction paintEvent() est réimplémentée pour ne rien faire. Il est inutile de réagir aux paintEvent(), puisque le rendu est relancé toutes les quarante millisecondes. Cependant, si la scène est statique, on n'a pas besoin de communiquer l'événement de mise à jour au thread de rendu. De même, on ne peut pas redimensionner la fenêtre GL dans resizeEvent(). Au lieu de cela, on informe le thread de rendu qu'un redimensionnement est nécessaire et on le laisse s'en occuper. Puisque l'on s'occupe soi-même de l'échange des tampons, on appelle setAutoBufferSwap(false) dans le constructeur de GLWidget.

Maintenant, on rassemble ces classes dans une application. La classe AppWindow est une classe dérivée triviale de QMainWindow contenant un QWorkspace. Elle propose des fonctions pour créer un nouveau widget dont le rendu est réalisé dans un thread séparé. Elle peut aussi fermer ce widget et terminer son thread de rendu.

class AppWindow: public QMainWindow
{
Q_OBJECT
public:
AppWindow();

protected:
void closeEvent(QCloseEvent *evt);

private slots:
void newThread();
void killThread();

private:
QWorkspace *ws;
};

AppWindow::AppWindow()
: QMainWindow(0)
{
QPopupMenu *menu = new QPopupMenu(this);
menuBar()->insertItem("&Thread", menu);
menu->insertItem("&New thread", this, SLOT(newThread()), CTRL+Key_N);
menu->insertItem("&End current thread", this, SLOT(killThread()), CTRL+Key_K);
menu->insertSeparator();
menu->insertItem("E&xit", qApp, SLOT(quit()), CTRL+Key_Q);

ws = new QWorkspace(this);
setCentralWidget(ws);
}

void AppWindow::closeEvent(QCloseEvent *evt)
{
QWidgetList windows = ws->windowList();
for (int i = 0; i < int(windows.count()); ++i) {
GLWidget *window = (GLWidget *)windows.at(i);
window->stopRendering();
}
QMainWindow::closeEvent(evt);
}

void AppWindow::newThread()
{
QWidgetList windows = ws->windowList();
GLWidget *widget = new GLWidget(ws);
widget->setCaption("Thread #" + QString::number(windows.count() + 1));
widget->show();
widget->startRendering();
}

void AppWindow::killThread()
{
GLWidget *widget = (GLWidget *)ws->activeWindow();
if (widget) {
widget>stopRendering();
delete widget
}
}

Le constructeur de AppWindow ajoute un menu pour créer et détruire des fenêtres GLWidget. Si l'utilisateur ferme l'application, on arrête le rendu de chaque fenêtre avant de passer l'événement de fermeture. Lorsque l'utilisateur crée un nouveau thread, on crée directement un nouveau GLWidget et appelle startRendering(). Lorsque l'utilisateur arrête un thread, on appelle killThread() pour arrêter le rendu puis on supprime le widget.

#include <qapplication.h>
#include "appwindow.h"
#ifdef Q_WS_X11
#    include <X11/Xlib.h>
#endif

int main(int argc, char *argv[])
{
#ifdef Q_WS_X11
XInitThreads();
#endif
QApplication app(argc, argv);
AppWindow aw;
app.setMainWidget(&aw);
aw.show();
return app.exec();
}

IV - Conseils généraux

En cas de rendu en dehors de la fonction paintGL(), on doit toujours faire appel à makeCurrent() en premier, sinon on pourrait réaliser le rendu dans le mauvais widget. Si on utilise des display lists et que l'on souhaite dessiner des pixmaps, on doit mettre en place ces display lists à l'intérieur de la fonction initializeGL(). Ceci est nécessaire car un nouveau contexte GL est créé lors du rendu d'une pixmap.

Si vous avez des problèmes pour compiler Qt avec OpenGL sous X11, assurez-vous que les fichiers d'en-têtes et les bibliothèques d'OpenGL sont correctement installés. Le script de configuration recherche seulement dans quelques endroits standard et, si les en-têtes d'OpenGL n'y sont pas, l'autodétection échouera. Utilisez l'option -v de configure pour le détail des erreurs. Vous pouvez ajouter des chemins de recherche avec les paramètres -I et -L de configure. Vous devez également faire en sorte que, si la bibliothèque GL est liée à la bibliothèque pthread, Qt est également threadé ; cela peut être fait en configurant Qt avec l'option -thread.

V - Remerciements

Merci à dourouc05 et Claude Leloup pour leur relecture de cet article et leurs conseils.

Au nom de toute l'équipe Qt, j'aimerais adresser le plus grand remerciement à Nokia pour nous avoir autorisé à traduire cet article !

(1)OpenGL est une marque déposée de Silicon Graphics Inc aux États-Unis et certains autres pays.