Cette page vous donne les différences entre la révision choisie et la version actuelle de la page.
qt_opengl_-_generer_un_terrain [2014/12/11 17:18] gbdivers |
qt_opengl_-_generer_un_terrain [2015/08/10 12:10] (Version actuelle) gbdivers |
||
---|---|---|---|
Ligne 1: | Ligne 1: | ||
====== Qt OpenGL - Générer un terrain ====== | ====== Qt OpenGL - Générer un terrain ====== | ||
- | |||
- | **Sommaire** | ||
* [[qt_opengl_-_introduction|]] | * [[qt_opengl_-_introduction|]] | ||
Ligne 15: | Ligne 13: | ||
Cette partie présente la structure de base utilisée pour réaliser notre application de génération de terrain. Après avoir rappelé les mécanismes de base pour réaliser du rendu 3D dans une application Qt, nous détaillerons la problématique du chargement de notre terrain. Enfin, un système simple de mesure des performances, utilisé tout au long du tutoriel, sera mis en place. | Cette partie présente la structure de base utilisée pour réaliser notre application de génération de terrain. Après avoir rappelé les mécanismes de base pour réaliser du rendu 3D dans une application Qt, nous détaillerons la problématique du chargement de notre terrain. Enfin, un système simple de mesure des performances, utilisé tout au long du tutoriel, sera mis en place. | ||
- | |||
- | L'ensemble du code de ce chapitre se trouve dans le projet appelé "OpenGL - code minimal" (fichier zip). Dans cet exemple, on n'affiche pas la heightmap mais un simple repère orthonormé. | ||
{{ :opengl-buffers.png |}} | {{ :opengl-buffers.png |}} | ||
+ | |||
+ | L'ensemble du code de ce chapitre se trouve sur [[https://github.com/GuillaumeBelz/qt-opengl|GitHub]], dans le projet appelé "OpenGL - code minimal". Dans cet exemple, on n'affiche pas la heightmap mais un simple repère orthonormé. | ||
+ | |||
===== La classe HeightmapWidget ===== | ===== La classe HeightmapWidget ===== | ||
Ligne 47: | Ligne 46: | ||
{{ :heightmap.png |}} | {{ :heightmap.png |}} | ||
- | Qt fournit la classe QVector3D pour stocker des coordonnées 3D. Cette classe gère les opérations mathématiques usuelles en 3D (addition et soustraction de vecteurs, multiplication par une constante, etc.) et peut être utilisée comme base pour le calcul matriciel (avec les classes QMatrix). Si on n'utilise pas Qt, il est possible de créer une structure similaire : struct vector3D { float x, y, z; };. Pour stocker l'ensemble des points formant le terrain, nous utiliserons un QVector de QVector3D. Un std::vector ou n'importe quel conteneur fera également l'affaire. L'avantage avec les vecteurs dans ce cas est que les données sont stockées dans des blocs mémoire contigus. Il sera donc possible d'envoyer directement au processeur graphique un bloc de données, sous forme de buffer (voir le chapitre sur les Vertex Buffer Object pour l'utilisation des buffers). | + | Qt fournit la classe QVector3D pour stocker des coordonnées 3D. Cette classe gère les opérations mathématiques usuelles en 3D (addition et soustraction de vecteurs, multiplication par une constante, etc.) et peut être utilisée comme base pour le calcul matriciel (avec les classes QMatrix). Si on n'utilise pas Qt, il est possible de créer une structure similaire : ''struct vector3D { float x, y, z; };''. Pour stocker l'ensemble des points formant le terrain, nous utiliserons un QVector de QVector3D. Un std::vector ou n'importe quel conteneur fera également l'affaire. L'avantage avec les vecteurs dans ce cas est que les données sont stockées dans des blocs mémoire contigus. Il sera donc possible d'envoyer directement au processeur graphique un bloc de données sous forme de buffer (voir le chapitre sur les Vertex Buffer Object pour l'utilisation des buffers). |
{{ :mapping.png |}} | {{ :mapping.png |}} | ||
- | Le nombre de points de notre carte est conservé dans les variables vertices_by_x et vertices_by_z et le nombre de quadrilatères dans les variables quads_by_x et quads_by_z. En pratique, comme nous utilisons une grille uniforme, le nombre de quadrilatères par côté est le nombre de vertices par côté moins un. Nous créerons ces différentes variables pour la lisibilité. Certains algorithmes s'appliquent sur les vertices et d'autres sur les formes, il faut donc bien faire la distinction. Dans notre repère 3D, la hauteur est représentée grâce à l'axe y, les axes x et z sont utilisés pour représenter les coordonnées des points constituant le terrain. La raison de ce choix est que pour un repère orthonormé direct, si le plan horizontal correspond au plan xy, alors l'axe des z est orienté vers le bas. En prenant le plan xz comme plan horizontal, l'axe des y est correctement orienté vers le haut. | + | Le nombre de points de notre carte est conservé dans les variables ''vertices_by_x'' et ''vertices_by_z'' et le nombre de quadrilatères dans les variables ''quads_by_x'' et ''quads_by_z''. En pratique, comme nous utilisons une grille uniforme, le nombre de quadrilatères par côté est le nombre de vertices par côté moins un. Nous créerons ces différentes variables pour la lisibilité. Certains algorithmes s'appliquent sur les vertices et d'autres sur les formes, il faut donc bien faire la distinction. Dans notre repère 3D, la hauteur est représentée grâce à l'axe y, les axes x et z sont utilisés pour représenter les coordonnées des points constituant le terrain. La raison de ce choix est que pour un repère orthonormé direct, si le plan horizontal correspond au plan xy, alors l'axe des z est orienté vers le bas. En prenant le plan xz comme plan horizontal, l'axe des y est correctement orienté vers le haut. |
<code cpp-qt> | <code cpp-qt> | ||
Ligne 93: | Ligne 92: | ||
</code> | </code> | ||
- | La hauteur du point est calculée en fonction de la couleur (niveau de gris) du pixel. La fonction qGray est utilisée afin de récupérer le niveau de gris d'un pixel, ce niveau de gris variant de 0 à 255. Cette méthode a l'avantage de la simplicité mais n'est pas optimale. En particulier, le code des hauteurs sur 256 valeurs différentes peut être insuffisant en fonction des besoins. De plus, les images sont codées sur 32 bits alors que 8 bits sont suffisants pour coder 256 valeurs. On pourra optimiser cela en lisant directement un tableau de float. | + | La hauteur du point est calculée en fonction de la couleur (niveau de gris) du pixel. La fonction ''qGray()'' est utilisée afin de récupérer le niveau de gris d'un pixel, ce niveau de gris variant de 0 à 255. Cette méthode a l'avantage de la simplicité mais n'est pas optimale. En particulier, le code des hauteurs sur 256 valeurs différentes peut être insuffisant en fonction des besoins. De plus, les images sont codées sur 32 bits alors que 8 bits sont suffisants pour coder 256 valeurs. On pourra optimiser cela en lisant directement un tableau de ''float''. |
<code cpp-qt> | <code cpp-qt> | ||
Ligne 99: | Ligne 98: | ||
</code> | </code> | ||
- | Pour terminer, le point 3D est stocké dans le vecteur. Les coordonnées x et z sont centrées et normalisées entre -(MAP_SIZE / 2) et (MAP_SIZE / 2) et la coordonnée y est normalisée entre 0 et 2. MAP_SIZE est une constante réelle valant 5.0. | + | Pour terminer, le point 3D est stocké dans le vecteur. Les coordonnées x et z sont centrées et normalisées entre ''-(MAP_SIZE / 2)'' et ''(MAP_SIZE / 2)'' et la coordonnée y est normalisée entre 0 et 2. ''MAP_SIZE'' est une constante réelle valant 5.0. |
<code cpp-qt> | <code cpp-qt> | ||
Ligne 111: | Ligne 110: | ||
</code> | </code> | ||
- | Le code de chargement présenté ci-dessus peut être optimisé. En effet, l'ajout de contenu dans un vecteur grâce à la méthode push_back peut nécessiter un redimensionnement du vecteur à chaque ajout d'un point, entraînant des allocations et des copies inutiles. Une solution est de redimensionner le vecteur avant grâce à la méthode resize (avec pour taille le nombre de points constituant le terrain) puis d'utiliser un itérateur pour parcourir le vecteur. | + | Le code de chargement présenté ci-dessus peut être optimisé. En effet, l'ajout de contenu dans un vecteur grâce à la méthode ''push_back'' peut nécessiter un redimensionnement du vecteur à chaque ajout d'un point, entraînant des allocations et des copies inutiles. Une solution est de redimensionner le vecteur avant grâce à la méthode ''resize'' (avec pour taille le nombre de points constituant le terrain) puis d'utiliser un itérateur pour parcourir le vecteur. |
===== Initialisation de la vue ===== | ===== Initialisation de la vue ===== | ||
Ligne 119: | Ligne 118: | ||
{{ :opengl-minimal.png |}} | {{ :opengl-minimal.png |}} | ||
- | Dans l'exemple de heightmap présenté, notre heightmap est fixe au centre de la vue, aux coordonnées (0,0,0) et c'est la position de la caméra qui se déplacera autour du centre. Pour définir la position de la caméra, nous utilisons trois paramètres de rotation (un pour chaque axe x, y et z : x_rot, y_rot et z_rot) et un paramètre pour la distance de la caméra au centre (distance) qui sont initialisés dans initializeGL() : | + | Dans l'exemple de heightmap présenté, notre heightmap est fixe au centre de la vue, aux coordonnées (0,0,0) et c'est la position de la caméra qui se déplacera autour du centre. Pour définir la position de la caméra, nous utilisons trois paramètres de rotation (un pour chaque axe x, y et z : ''x_rot'', ''y_rot'' et ''z_rot'') et un paramètre pour la distance de la caméra au centre (distance) qui sont initialisés dans ''initializeGL()'' : |
<code cpp-qt> | <code cpp-qt> | ||
Ligne 130: | Ligne 129: | ||
</code> | </code> | ||
- | Lors de l'initialisation, il faut également définir la couleur de l'arrière-plan, qui sera utilisée à chaque mise à jour, avec la fonction Qt qglClearColor ou avec la fonction OpenGL glClearColor. La différence entre ces deux fonctions est que qglClearColor accepte des couleurs au format Qt (QColor). Il faut aussi activer le test de profondeur GL_DEPTH_TEST avec la fonction glEnable : | + | Lors de l'initialisation, il faut également définir la couleur de l'arrière-plan, qui sera utilisée à chaque mise à jour, avec la fonction Qt ''qglClearColor'' ou avec la fonction OpenGL ''glClearColor''. La différence entre ces deux fonctions est que ''qglClearColor'' accepte des couleurs au format Qt (QColor). Il faut aussi activer le test de profondeur ''GL_DEPTH_TEST'' avec la fonction ''glEnable'' : |
<code cpp-qt> | <code cpp-qt> | ||
Ligne 137: | Ligne 136: | ||
</code> | </code> | ||
- | Lors de l'affichage de la vue 3D, il faut commencer par dessiner l'arrière-plan en le remplissant avec la couleur définie par qglClearColor en appelant la fonction glClear. Ici, on choisit d'effacer le tampon de couleur (GL_COLOR_BUFFER_BIT) et le tampon de profondeur (GL_DEPTH_BUFFER_BIT). OpenGl permet de choisir comment on souhaite afficher les polygones avec la fonction glPolygonMode. Cette fonction prend deux paramètres : le premier indique sur quelle face est appliqué le mode (la face antérieure avec GL_FRONT, la face postérieure avec GL_BACK ou les deux avec GL_FRONT_AND_BACK ; pour rappel, la face antérieure est celle pour laquelle les vertices sont dans le sens horaire) ; le second paramètre indique le mode d'affichage (des points avec GL_POINT, des lignes avec GL_LINE ou des surfaces pleines avec GL_FILL). | + | Lors de l'affichage de la vue 3D, il faut commencer par dessiner l'arrière-plan en le remplissant avec la couleur définie par ''qglClearColor'' en appelant la fonction ''glClear''. Ici, on choisit d'effacer le tampon de couleur (''GL_COLOR_BUFFER_BIT'') et le tampon de profondeur (''GL_DEPTH_BUFFER_BIT''). OpenGL permet de choisir comment on souhaite afficher les polygones avec la fonction ''glPolygonMode''. Cette fonction prend deux paramètres : le premier indique sur quelle face est appliqué le mode (la face antérieure avec ''GL_FRONT'', la face postérieure avec ''GL_BACK'' ou les deux avec ''GL_FRONT_AND_BACK'' ; pour rappel, la face antérieure est celle pour laquelle les vertices sont dans le sens horaire) ; le second paramètre indique le mode d'affichage (des points avec ''GL_POINT'', des lignes avec ''GL_LINE'' ou des surfaces pleines avec ''GL_FILL''). |
<code cpp-qt> | <code cpp-qt> | ||
Ligne 147: | Ligne 146: | ||
Il ne reste plus qu'à fournir à la carte graphique les paramètres de projection. Lorsqu'on n'utilise pas les shaders, les paramètres des matrices sont définis de la façon suivante : | Il ne reste plus qu'à fournir à la carte graphique les paramètres de projection. Lorsqu'on n'utilise pas les shaders, les paramètres des matrices sont définis de la façon suivante : | ||
- | * on sélectionne la matrice sur laquelle on souhaite travailler avec glMatrixMode ; | + | * on sélectionne la matrice sur laquelle on souhaite travailler avec ''glMatrixMode'' ; |
- | * on applique ensuite à cette matrice différentes opérations avec les fonctions glLoadIdentity (recharge la matrice identité), glRotate (rotation autour d'un axe), gluLookAt et gluPerspective. | + | * on applique ensuite à cette matrice différentes opérations avec les fonctions ''glLoadIdentity'' (recharge la matrice identité), ''glRotate'' (rotation autour d'un axe), ''gluLookAt'' et ''gluPerspective''. |
<code cpp-qt> | <code cpp-qt> | ||
Ligne 166: | Ligne 165: | ||
glLoadIdentity(); | glLoadIdentity(); | ||
gluPerspective(60.0f, 1.0*width()/height(), 0.1f, 100.0f); | gluPerspective(60.0f, 1.0*width()/height(), 0.1f, 100.0f); | ||
+ | |||
+ | // On affiche un simple repère | ||
+ | glBegin(GL_LINES); | ||
+ | glColor3d(1,0,0); | ||
+ | glVertex3f(0.0f, 0.0f, 0.0f); | ||
+ | glVertex3f(1.0f, 0.0f, 0.0f); | ||
+ | glEnd(); | ||
+ | glBegin(GL_LINES); | ||
+ | glColor3d(0,0,1); | ||
+ | glVertex3f(0.0f, 0.0f, 0.0f); | ||
+ | glVertex3f(0.0f, 1.0f, 0.0f); | ||
+ | glEnd(); | ||
+ | glBegin(GL_LINES); | ||
+ | glColor3d(0,1,0); | ||
+ | glVertex3f(0.0f, 0.0f, 0.0f); | ||
+ | glVertex3f(0.0f, 0.0f, 1.0f); | ||
+ | glEnd(); | ||
</code> | </code> | ||
Par la suite, lorsque l'on utilisera les shaders, les matrices de projection seront envoyées directement aux shaders comme paramètres. | Par la suite, lorsque l'on utilisera les shaders, les matrices de projection seront envoyées directement aux shaders comme paramètres. | ||
- | Il faut également mettre à jour les dimensions de la vue OpenGL lorsque le widget est redimensionné. Pour cela, on appelle simplement la fonction glViewport en donnant les dimensions du widget : | + | Il faut également mettre à jour les dimensions de la vue OpenGL lorsque le widget est redimensionné. Pour cela, on appelle simplement la fonction ''glViewport'' en donnant les nouvelles dimensions du widget : |
<code cpp-qt> | <code cpp-qt> | ||
Ligne 181: | Ligne 197: | ||
===== Gestion de la souris et du clavier ===== | ===== Gestion de la souris et du clavier ===== | ||
- | Puisque que QGLWidget hérite de QWidget, il possible de récupérer les évènements de la souris ou du clavier à l'aide des fonctions MouseEvent et KeyEvent. Ces fonctions étant appelées à chaque évènement, il suffit de les surcharger pour obtenir le comportement souhaité. | + | Puisque que QGLWidget hérite de QWidget, il possible de récupérer les évènements de la souris ou du clavier à l'aide des fonctions ''MouseEvent'' et ''KeyEvent''. Ces fonctions étant appelées à chaque évènement, il suffit de les surcharger pour obtenir le comportement souhaité. |
- | Par exemple, si nous souhaitons modifier l'affichage de notre vue 3D lorsque l'on appuie sur la barre d'espace, il suffit de surcharger la fonction keyPressEvent : | + | Par exemple, si nous souhaitons modifier l'affichage de notre vue 3D lorsque l'on appuie sur la barre d'espace, il suffit de surcharger la fonction ''keyPressEvent'' : |
<code cpp-qt> | <code cpp-qt> | ||
Ligne 193: | Ligne 209: | ||
</code> | </code> | ||
- | Si l'on souhaite donner à l'utilisateur la possibilité de s'approcher ou de s'éloigner de la heightmap en agissant sur la molette de la souris, on peut surcharger la fonction wheelEvent : | + | Si l'on souhaite donner à l'utilisateur la possibilité de s'approcher ou de s'éloigner de la heightmap en agissant sur la molette de la souris, on peut surcharger la fonction ''wheelEvent'' : |
<code cpp-qt> | <code cpp-qt> | ||
Ligne 202: | Ligne 218: | ||
</code> | </code> | ||
- | Pour finir, on peut ajouter la possibilité de tourner autour de la heightmap à l'aide de la souris. Pour cela, on calcule le déplacement de la souris entre deux évènements de déplacement (ou entre l'appui sur le bouton et le premier déplacement) et l'on modifie les variables x_rot, y_rot et z_rot en fonction : | + | Pour finir, on peut ajouter la possibilité de tourner autour de la heightmap à l'aide de la souris. Pour cela, on calcule le déplacement de la souris entre deux évènements de déplacement (ou entre l'appui sur le bouton et le premier déplacement) et l'on modifie les variables ''x_rot'', ''y_rot'' et ''z_rot'' en fonction : |
<code cpp-qt> | <code cpp-qt> | ||
Ligne 235: | Ligne 251: | ||
Pour analyser les performances du rendu 3D, une solution serait de mesurer le temps mis par l'ordinateur pour exécuter la fonction de rendu puis de réaliser une moyenne de cette valeur pour avoir un résultat stable. Dans notre application, nous allons utiliser une solution équivalente qui consiste à compter le nombre d'exécutions de la fonction de rendu pendant une seconde, ce qui nous donnera le nombre d'images par seconde affichées à l'écran (FPS : Frame Per Second). | Pour analyser les performances du rendu 3D, une solution serait de mesurer le temps mis par l'ordinateur pour exécuter la fonction de rendu puis de réaliser une moyenne de cette valeur pour avoir un résultat stable. Dans notre application, nous allons utiliser une solution équivalente qui consiste à compter le nombre d'exécutions de la fonction de rendu pendant une seconde, ce qui nous donnera le nombre d'images par seconde affichées à l'écran (FPS : Frame Per Second). | ||
- | Pour mettre à jour le rendu 3D, nous utilisons un timer qui appellera la fonction updateGL à intervalle régulier. Lorsque l'on souhaite mesurer le nombre de FPS (images par seconde), on lance ce timer avec un intervalle de 0 ms. En utilisation courante, on limite le nombre de FPS, par exemple en lançant le timer avec un intervalle de temps de 20 ms. La variable frame_count permet de compter le nombre d'images par seconde tandis que la variable last_time enregistre le temps de la dernière mise à jour de la vue. | + | Pour mettre à jour le rendu 3D, nous utilisons un timer qui appellera la fonction updateGL à intervalles réguliers. Lorsque l'on souhaite mesurer le nombre de FPS (images par seconde), on lance ce timer avec un intervalle de 0 ms. En utilisation courante, on limite le nombre de FPS, par exemple en lançant le timer avec un intervalle de temps de 20 ms. La variable ''frame_count'' permet de compter le nombre d'images par seconde tandis que la variable ''last_time'' enregistre le temps de la dernière mise à jour de la vue. |
Attention, pour mesurer le nombre de FPS maximal, il est nécessaire de désactiver la synchronisation verticale sous Windows. La méthode varie en fonction du modèle, le lecteur se reportera aux spécifications données par le constructeur de la carte graphique. | Attention, pour mesurer le nombre de FPS maximal, il est nécessaire de désactiver la synchronisation verticale sous Windows. La méthode varie en fonction du modèle, le lecteur se reportera aux spécifications données par le constructeur de la carte graphique. | ||
Ligne 258: | Ligne 274: | ||
</code> | </code> | ||
- | Le décompte du nombre d'images affichées par seconde est réalisé, dans la fonction de rendu paintGL, à l'aide d'une variable incrémentée à chaque passe. Ceci combiné à deux variables de type QTime qui nous permettent de délimiter des intervalles de temps de une seconde. À chaque passe, on vérifie si au moins une seconde s'est écoulée depuis le dernier décompte. Si c'est le cas, le nombre de rendus effectués pendant l'intervalle est sauvegardé dans la variable last_count, le décompte est ensuite remis à zéro et le nouvel intervalle de temps démarre (fonction statique currentTime). | + | Le décompte du nombre d'images affichées par seconde est réalisé, dans la fonction de rendu ''paintGL'', à l'aide d'une variable incrémentée à chaque passe. Ceci combiné à deux variables de type QTime qui nous permettent de délimiter des intervalles de temps de une seconde. À chaque passe, on vérifie si au moins une seconde s'est écoulée depuis le dernier décompte. Si c'est le cas, le nombre de rendus effectués pendant l'intervalle est sauvegardé dans la variable ''last_count'', le décompte est ensuite remis à zéro et le nouvel intervalle de temps démarre (fonction statique ''currentTime''). |
<code cpp-qt> | <code cpp-qt> | ||
Ligne 267: | Ligne 283: | ||
if (last_time.msecsTo(new_time) >= 1000) | if (last_time.msecsTo(new_time) >= 1000) | ||
{ | { | ||
- | // on sauvegarde le FPS dans last_count et on réinitialise | + | // on sauvegarde le FPS dans last_count et on réinitialise |
last_count = frame_count; | last_count = frame_count; | ||
frame_count = 0; | frame_count = 0; | ||
Ligne 274: | Ligne 290: | ||
</code> | </code> | ||
- | Le nombre de FPS est affiché avec la fonction renderText . À noter qu'il est possible d'afficher du texte en donnant les coordonnées 2D (dans notre cas) ou 3D. | + | Le nombre de FPS est affiché avec la fonction ''renderText''. À noter qu'il est possible d'afficher du texte en donnant les coordonnées 2D (dans notre cas) ou 3D. |
<code cpp-qt> | <code cpp-qt> | ||
Ligne 286: | Ligne 302: | ||
===== Gestion des erreurs ===== | ===== Gestion des erreurs ===== | ||
- | OpenGL fonctionne sur le principe d'une machine à états. Les fonctions n'ont pas de boolean en retour de fonction pour indiquer qu'une erreur est survenue. En cas d'erreur, un flag interne à OpenGL est simplement activé. Il est donc possible (et même nécessaire) de vérifier qu'une erreur n'est pas survenue après un appel à une fonction OpenGL à l'aide de la fonction glGetError. | + | OpenGL fonctionne sur le principe d'une machine à états. Les fonctions n'ont pas de booléen en retour de fonction pour indiquer qu'une erreur est survenue. En cas d'erreur, un flag interne à OpenGL est simplement activé. Il est donc possible (et même nécessaire) de vérifier qu'une erreur n'est pas survenue après un appel à une fonction OpenGL à l'aide de la fonction ''glGetError''. |
+ | |||
+ | <code cpp> | ||
+ | while ((GLenum error = glGetError()) != GL_NO_ERROR) { | ||
+ | std::cerr << "OpenGL error: " << error << std::endl; | ||
+ | } | ||
+ | </code> | ||
+ | |||
+ | Voir aussi les debug context [[deboguer_avec_opengl_4|]] | ||
{{tag> OpenGL Qt}} | {{tag> OpenGL Qt}} |