Qt et OpenGL

Apports de Qt 5.1

| Sean Harmer | Alexandre Laurent |

Traducteur

Arnold Dumas

Cet article est le premier d'une série s'intéressant à l'art d'utiliser OpenGL avec Qt 5. Dans cet article, nous verrons dans un premier temps l'historique de Qt et de son support d'OpenGL puis nous découvrirons l'ensemble des nouvelles fonctionnalités de Qt 5.1 qui améliorent le support d'OpenGL. Les prochains articles décriront plus en détail d'autres fonctionnalités et présenteront des exemples mettant en scène l'usage d'OpenGL et de Qt.

Commentez

- Titre : Qt et OpenGL

- Auteurs : Sean Harmer, Alexandre Laurent

- Traducteur : Arnold Dumas

- Parution : 12 septembre 2013

- Mise à jour : 10 octobre 2013

- Licence : Copyright ® 2013 Sean Harmer. 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. Article original

- II. Un (très) bref historique de Qt et d'OpenGL

- III. Des fonctions partout !

- IV. Extensions d'OpenGL

- V. Remerciements

I - Article original

Le blog KDAB est rédigé par les ingénieurs de KDAB s'occupant des formations, de la consultance ainsi que du développement (de Qt et de produits additionnels). Vous pouvez trouver les  versions originales.

Cet article est une traduction de l'article original écrit par Sean Harmer paru le 15 mars 2013.

Cet article est une traduction de l'un des articles en anglais écrits par KDAB. Les éventuels problèmes résultant d'une mauvaise traduction ne sont pas imputables à KDAB.

II - Un (très) bref historique de Qt et d'OpenGL

Qt a une longue histoire quant au support de l'affichage avec OpenGL. La plupart des développeurs Qt connaissent QGLWidget et peut-être les différentes incarnations des moteurs de dessin basés sur OpenGL. Ces deux derniers permettent respectivement de bénéficier de la puissance d'OpenGL via des appels bruts ou alors d'utiliser l'API QPainter. En sus, Qt offre aussi quelques interfaces facilitant l'utilisation des objets OpenGL comme QGLShaderProgram, QGLFramebufferObject ou encore QGLBuffer.

Lors de la conception de Qt 5, les classes QGL* ont été marquées comme « finies » et de toutes nouvelles classes QOpenGL* remplaçantes ont fait leur apparition dans QtGUI. La raison de ces changements est que le nouveau moteur de rendu de Qt Quick 2 s'appuie sur OpenGL et fait donc partie intégrante des possibilités graphiques offertes par Qt. Les nouvelles classes QOpenGL* peuvent directement remplacer les vieilles classes QGL*. Lors de l'écriture de nouveau code, il est recommandé de les utiliser.

Qt 5.0 expose le même sous-ensemble de fonctionnalités OpenGL que Qt 4.8, qui se trouve être plus ou moins l'intersection d'OpenGL 2 et d'OpenGL ES 2. Il s'avère que ce sont les fonctionnalités nécessaires à Qt Quick 2. En plus des fonctionnalités de Qt 4.8, Qt 5.0 facilite grandement la création de fenêtres et de contextes OpenGL sur n'importe quelle plate-forme. Il n'est plus nécessaire de prendre en compte les particularités de chaque plate-forme pour créer un contexte OpenGL. Il suffit d'utiliser QOpenGLContext et vous ne vous arracherez plus les cheveux !

Qt 5.1 entreprend d'exposer toujours plus de fonctionnalités OpenGL afin de rendre son utilisation avec Qt simple et élégante, voire aussi amusante ! À cette fin, KDAB a investi des ressources significatives pour repousser les limites de Qt et OpenGL.

III - Des fonctions partout !

OpenGL est, pour être franc, un peu pénible à faire fonctionner sur certaines plates-formes. Une des principales raisons est la nécessité de résoudre l'adresse du point d'entrée de façon dynamique lors de l'exécution plutôt qu'à la compilation - l'éditeur de lien pourrait alors le faire sans souci. Par exemple, sur Microsoft Windows, l'adresse de n'importe quelle fonction introduite depuis OpenGL 1.1 doit être résolue à l'exécution. Ces fonctions représentent la quasi-totalité de celles utilisées dans une application OpenGL moderne !

Pour vous aider, Qt propose quelques fonctions utilitaires : QOpenGLContext::getProcAddress() et QOpenGLFunctions. La première résout manuellement les points d'entrée tandis que la seconde possède des fonctions membres permettant d'appeler celles d'OpenGL 2 et OpenGL ES 2. Ces aides sont appréciables, et vont dans le bon sens. Le problème est que QOpenGLFunctions se limite à ce qu'il expose (le sous-ensemble déjà mentionné plus tôt) et que la résolution manuelle des points d'entrée des autres fonctions est extrêmement propice aux erreurs. D'autres solutions existent, il est ainsi possible d'utiliser un solveur externe comme GLEW ou Glee. Il est néanmoins difficile de faire correctement fonctionner ces derniers avec Qt, notamment au niveau de l'ordre d'inclusion des fichiers d'en-tête.

Faites donc entrer QOpenGLContext::versionFunctions() ! Cette petite fonction qui ne paye pas de mine vous sera d'une aide inestimable. Elle peut obtenir un pointeur sur un objet dont les méthodes sont toutes les fonctions OpenGL relatives à une version et à un profil.

Par exemple, pour créer une sous-classe de QWindow dans laquelle le rendu utilise un code aussi simple que celui-ci suffit :

Window::Window( QScreen* screen )
: QWindow( screen ),
{
// Dit à Qt d'utiliser OpenGL pour cette fenêtre
setSurfaceType( OpenGLSurface );

// Spécifie le format et crée une surface spécifique à la plate-forme
QSurfaceFormat format;
format.setDepthBufferSize( 24 );
format.setMajorVersion( 4 );
format.setMinorVersion( 3 );
format.setSamples( 4 );
format.setProfile( QSurfaceFormat::CoreProfile );
setFormat( format );
create();

// Crée un contexte OpenGL
m_context = new QOpenGLContext;
m_context->setFormat( format );
m_context->create();

// Établit le contexte de cette fenêtre comme étant le courant
m_context->makeCurrent( this );

// Obtient un objet fonction et résout tous les points d'entrée
// m_funcs est déclaré de la sorte : QOpenGLFunctions_4_3_Core* m_funcs
m_funcs = m_context->versionFunctions();
if ( !m_funcs ) {
qWarning( "Impossible d'obtenir l'objet de la version d'OpenGL" );
exit( 1 );
}
m_funcs->initializeOpenGLFunctions();
}Window::Window( QScreen* screen )
: QWindow( screen ),
{
// Dit à Qt d'utiliser OpenGL pour cette fenêtre
setSurfaceType( OpenGLSurface );

// Spécifie le format et crée une surface spécifique à la plate-forme
QSurfaceFormat format;
format.setDepthBufferSize( 24 );
format.setMajorVersion( 4 );
format.setMinorVersion( 3 );
format.setSamples( 4 );
format.setProfile( QSurfaceFormat::CoreProfile );
setFormat( format );
create();

// Crée un contexte OpenGL
m_context = new QOpenGLContext;
m_context->setFormat( format );
m_context->create();

// Établit le contexte de cette fenêtre comme étant le courant
m_context->makeCurrent( this );

// Obtient un objet fonction et résout tous les points d'entrée
// m_funcs est déclaré de la sorte : QOpenGLFunctions_4_3_Core* m_funcs
m_funcs = m_context->versionFunctions();
if ( !m_funcs ) {
qWarning( "Impossible d'obtenir l'objet de la version d'OpenGL" );
exit( 1 );
}
m_funcs->initializeOpenGLFunctions();
}Window::Window( QScreen* screen )
: QWindow( screen ),
{
// Dit à Qt d'utiliser OpenGL pour cette fenêtre
setSurfaceType( OpenGLSurface );

// Spécifie le format et crée une surface spécifique à la plate-forme
QSurfaceFormat format;
format.setDepthBufferSize( 24 );
format.setMajorVersion( 4 );
format.setMinorVersion( 3 );
format.setSamples( 4 );
format.setProfile( QSurfaceFormat::CoreProfile );
setFormat( format );
create();

// Crée un contexte OpenGL
m_context = new QOpenGLContext;
m_context->setFormat( format );
m_context->create();

// Établit le contexte de cette fenêtre comme étant le courant
m_context->makeCurrent( this );

// Obtient un objet fonction et résout tous les points d'entrée
// m_funcs est déclaré de la sorte : QOpenGLFunctions_4_3_Core* m_funcs
m_funcs = m_context->versionFunctions();
if ( !m_funcs ) {
qWarning( "Impossible d'obtenir l'objet de la version d'OpenGL" );
exit( 1 );
}
m_funcs->initializeOpenGLFunctions();
}

Partant de là, il est possible, très simplement d'utiliser les fonctions membres de l'objet QOpenGLFunctions_4_3_Core.

// Définit un diviseur pour les attributs de sommets. Utilisé pour le rendu instancié
// Introduit par OpenGL 3.3
m_funcs->glVertexAttribDivisor( pointLocation, 1 );

// Répartit le traitement via un compute shader
// Introduit par OpenGL 4.3
m_funcs->glDispatchCompute( 512 / 16, 512 / 16, 1 );// Définit un diviseur pour les attributs de sommets. Utilisé pour le rendu instancié
// Introduit par OpenGL 3.3
m_funcs->glVertexAttribDivisor( pointLocation, 1 );

// Répartit le traitement via un compute shader
// Introduit par OpenGL 4.3
m_funcs->glDispatchCompute( 512 / 16, 512 / 16, 1 );// Définit un diviseur pour les attributs de sommets. Utilisé pour le rendu instancié
// Introduit par OpenGL 3.3
m_funcs->glVertexAttribDivisor( pointLocation, 1 );

// Répartit le traitement via un compute shader
// Introduit par OpenGL 4.3
m_funcs->glDispatchCompute( 512 / 16, 512 / 16, 1 );

Comme vous pouvez le voir, ce simple code donne accès à toutes les fonctions d'OpenGL, facilement et pour toutes les plates-formes supportées. En plus, les classes QOpenGLContext, QOpenGLFunctions_4_3 et similaires essayent vraiment de minimiser la quantité de travail nécessaire à la résolution des fonctions en partageant les backends contenant les pointeurs de fonctions. Cette approche prend en compte les adresses des fonctions spécifiques à un contexte (par exemple lors de l'utilisation de plusieurs fils d'exécution, de contextes ou de processeurs graphiques). Le code de ces classes est automatiquement généré par un utilitaire de sorte qu'elles soient facilement mises à jour lors de la sortie d'une nouvelle version d'OpenGL.

IV - Extensions d'OpenGL

OpenGL possède aussi un système d'extensions bien connu qui permet aux différents fabricants d'introduire de nouvelles fonctionnalités et API - même expérimentales - afin de voir si elles sont utiles et bien conçues. Malheureusement, si une extension introduit de nouvelles fonctions, ces dernières devront être résolues comme précédemment.

Il y a deux étapes à réaliser avant de pouvoir utiliser une extension OpenGL :

1. Vérifier si l'implémentation utilisée supporte l'extension ;

2. Si l'extension introduit une nouvelle API, alors il faut résoudre les points d'entrée.

Qt vous aide dans ces deux étapes. QOpenGLContext::hasExtension() détermine si une extension est supportée. La méthode OpenGLContext::extensions() permet de récupérer la liste complète des extensions supportées :

// Récupération des extensions supportées
QList extensions = m_context->extensions().toList();
std::sort( extensions );
qDebug() << "Extensions supportées (" << extensions.count() << ")";
foreach ( const QByteArray &extension, extensions )
qDebug() << "    " << extension;// Récupération des extensions supportées
QList extensions = m_context->extensions().toList();
std::sort( extensions );
qDebug() << "Extensions supportées (" << extensions.count() << ")";
foreach ( const QByteArray &extension, extensions )
qDebug() << "    " << extension;// Récupération des extensions supportées
QList extensions = m_context->extensions().toList();
std::sort( extensions );
qDebug() << "Extensions supportées (" << extensions.count() << ")";
foreach ( const QByteArray &extension, extensions )
qDebug() << "    " << extension;

Pour la seconde partie, nous aurons besoin de faire appel à notre bon vieil ami, QOpenGLContext::getProcAddress(). Le module Qt OpenGL Extensions est une nouveauté de Qt 5.1. Cette bibliothèque statique contient une classe pour toutes les extensions OpenGL listées par Khronos introduisant une nouvelle API. Afin d'utiliser une extension OpenGL, vous pouvez utiliser un code similaire :

// L'extension est-elle supportée ?
if ( !m_context->hasExtension( QByteArrayLiteral(
"GL_ARB_instanced_arrays" ) )
qFatal( "GL_ARB_instanced_arrays n'est pas supportée" );

// Instanciation de la classe d'aide et résolution des fonctions
QOpenGLExtension_ARB_instanced_arrays* m_instanceFuncs =
new QOpenGLExtension_ARB_instanced_arrays();
m_instanceFuncs->initializeOpenGLFunctions();

// Appel d'une fonction de l'extension
m_instanceFuncs->glVertexAttribDivisorARB( pointLocation, 1 );// L'extension est-elle supportée ?
if ( !m_context->hasExtension( QByteArrayLiteral(
"GL_ARB_instanced_arrays" ) )
qFatal( "GL_ARB_instanced_arrays n'est pas supportée" );

// Instanciation de la classe d'aide et résolution des fonctions
QOpenGLExtension_ARB_instanced_arrays* m_instanceFuncs =
new QOpenGLExtension_ARB_instanced_arrays();
m_instanceFuncs->initializeOpenGLFunctions();

// Appel d'une fonction de l'extension
m_instanceFuncs->glVertexAttribDivisorARB( pointLocation, 1 );// L'extension est-elle supportée ?
if ( !m_context->hasExtension( QByteArrayLiteral(
"GL_ARB_instanced_arrays" ) )
qFatal( "GL_ARB_instanced_arrays n'est pas supportée" );

// Instanciation de la classe d'aide et résolution des fonctions
QOpenGLExtension_ARB_instanced_arrays* m_instanceFuncs =
new QOpenGLExtension_ARB_instanced_arrays();
m_instanceFuncs->initializeOpenGLFunctions();

// Appel d'une fonction de l'extension
m_instanceFuncs->glVertexAttribDivisorARB( pointLocation, 1 );

De même que pour les fonctions au cœur d'OpenGL, le code pour les extensions est généré automatiquement afin de pouvoir facilement gérer de nouvelles versions d'OpenGL.

V - Remerciements

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

Je tiens à remercier Thibaut Cuvelier et Alexandre Laurent pour leurs conseils ainsi que ClaudeLELOUP pour sa relecture.