• {0} Facile|{1} Moyenne|{2} Difficile

Ce cours est visible gratuitement en ligne.

Vous pouvez être accompagné et mentoré par un professeur particulier par visioconférence sur ce cours.

J'ai tout compris !

Mis à jour le 13/03/2017

Push et Pop

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

Les chapitres les plus compliqués de la partie 1 sont enfin derrière nous, les suivants le seront moins. Aujourd'hui, on va s'intéresser aux piles de matrices.

Retenez bien ce que nous allons voir, et profitez de ce chapitre assez soft pour vous reposer. :magicien:

La problématique

Introduction

Je profite de cette partie vous faire un aparté sur la matrice modelview. En effet, pour le moment il y a un petit problème avec notre façon de l'utiliser, nous allons étudier un cas pour voir ce qui ne va pas. Jusqu'à maintenant, nous ne nous en sommes pas vraiment rendus compte car nos modèles 3D sont assez simples. Mais maintenant que nous avons fait nos premiers pas dans la 3D, il vaut mieux prendre les bonnes habitudes dès le début. :)

Un problème bien ennuyeux ...

Pour comprendre le problème, nous allons revenir un peu sur les transformations. Vous savez maintenant que chaque transformation que vous faites sera appliquée sur le repère de votre espace 3D et pas seulement sur l'objet que vous voulez afficher. Si vous faites une translation puis une rotation c'est le repère entier qui va être modifié.

Imaginez que vous vouliez afficher un toit pour une maison. Il faudra tout d'abord se placer en haut de la maison, puis faire une rotation pour que le toit soit légèrement penché et afficher le tout.

Maintenant si vous voulez afficher le jardin, comment faites-vous pour revenir en bas de la maison ? Comment faites-vous pour annuler la translation et la rotation qui permettaient d'arriver jusqu'au toit ?

Pour ça, nous avons trois solutions :

  • Refaire la rotation et la translation dans le sens inverse.

  • Réinitialiser totalement la matrice modelview, puis la caméra et enfin se replacer en bas de la maison.

  • Sauvegarder la matrice, afficher le toit en la modifiant, puis annuler les transformations en la restaurant à son état sauvegardé.

La première solution peut vous paraitre la plus simple à mettre en œuvre et sur un exemple comme celui-ci on peut le penser. Mais imaginez une maison de 500 vertices, l'afficher nécessitera plusieurs transformations. Vous pensez vraiment refaire toutes ces transformations dans le sens inverse pour retrouver votre position ?

La réponse est bien sûr non, d'une part parce que se rappeler de toutes les transformations est trop fastidieux et d'autre part parce que ça vous couterait trop de ressources pour refaire tout dans le sens inverse. Surtout que votre maison sera affichée 50 fois par seconde, imaginez le fourbi. :o

La deuxième solution nous pose les mêmes problèmes, réinitialiser la matrice et se replacer nous fait perdre du temps et des ressources.

Nous utiliserons donc la troisième solution qui nous permet de sauvegarder la matrice quand nous en avons besoin. De cette façon, on peut modifier le repère à volonté sans être obligé de retenir sur toutes les transformations. Une fois que nous voudrons revenir à la position sauvegardée, il suffira simplement de restaurer la sauvegarde du repère. :D

Les piles

Avec OpenGL 2

Dans les précédentes versions d'OpenGL, il existait deux fonctions qui permettaient de sauvegarder et de restaurer les matrices facilement. Celles-ci s’appelaient :

  • glPush() : pour la sauvegarde

  • glPop() : pour la restauration

Ces deux fonctions fonctionnaient sur un système de pile qui permettait de stocker les sauvegardes les unes sur les autres.

Le principe d'une pile en programmation est d'entasser des variables ou des objets de même type les uns sur les autres comme une pile d'assiettes. Le but de la fonction glPush() était justement d'empiler des matrices entre elles pour former une pile de matrices :

Image utilisateur

On utilisait généralement les piles sur la matrice modelview, étant donné que c'est elle la plus utilisée. Elles étaient basées sur le principe LIFO (Last In First Out), littéralement sur le principe du dernier arrivé premier sorti. C'est-à-dire que la dernière sauvegarde était la première restaurée.

Prenons l'exemple des assiettes. Si vous empilez 18 assiettes et que vous voulez laver celle la plus en dessous. Il faudra d'abord laver les 17 premières assiettes qui sont au dessus. En revanche, si vous voulez laver la dernière arrivée, elle se trouvera en haut de la pile (l'endroit le plus simple à accéder).

Ok on sait ce qu'est une pile maintenant. :) Mais à quoi ça pouvait bien servir ?

Bonne question, les piles permettaient de sauvegarder la matrice modelview à un état donné pour pouvoir la restaurer plus tard. L'intérêt principal était de pouvoir empiler des matrices pour avoir un système de restauration assez simple : la dernière sauvegarde était la première restaurée.

De cette façon, nous n'avions plus besoin de réinitialiser la matrice modelview ou de retenir toutes les transformations pour savoir où se trouvait le repère.

Avec OpenGL 3

Comme beaucoup d'autres fonctions, OpenGL a déprécié l'utilisation de glPush() et glPop() mais bon il n'y a rien de surprenant là-dedans. En revanche, ce qui est surprenant c'est que la librairie GLM n'inclut pas de méthodes de substitution à ces fonctions. Nous ne pouvons donc pas utiliser les piles de matrices.

Ceci est dû au fait que les matrices sont maintenant des objets au sens propre du terme. C'est-à-dire que nous pouvons les manipuler, utiliser l'allocation dynamique dessus, intégrer la POO, etc. L'usage des piles n'est donc plus utile. C'est assez perturbant pour ceux qui les ont toujours utilisées mais lorsque l'on connait la puissance du C++, on se rend compte que ce n'est pas une si mauvaise idée que ça. :)

Au final, nous allons utiliser une autre manière de faire pour sauvegarder nos matrices. Les piles ne sont plus indispensables mais les sauvegardes, elles, le sont toujours. On ne peut pas résoudre notre problème de toit sinon. :p

Sauvegarde et Restauration

Substitution aux piles

Comme nous l'avons vu à l'instant, les piles ne sont maintenant inutilisables, cependant les sauvegardes doivent quand même être faites.

Pour les faire, nous allons utiliser une des propriétés magiques du C++ : l'opérateur =. En effet, lorsque cet opérateur est surchargé, il permet de pouvoir copier un objet dans un autre objet. C'est ainsi que l'on peut, par exemple, copier deux voitures sans problème :

// Copie d'une voiture

Voiture maCopie = voitureOriginale;

Pour notre problème de matrices, nous allons faire exactement la même chose :

  • Pour la sauvegarde : nous allons copier une matrice dans un objet sauvegarde

  • Pour la restauration : nous allons faire l'inverse et copier la sauvegarde dans la matrice originale

En code, cela donnerait :

// Sauvegarde de la matrice

mat4 sauvegardeModelview = modelview;


// Restauration de la matrice

modelview = sauvegardeModelview;

Facile non ? Je dirais même que c'est plus facile à comprendre que les piles. :p

Quand sauvegarder ?

Le problème des transformations

Maintenant que nous avons une méthode de substitution aux piles, nous allons pouvoir sauvegarder nos matrices dans nos programmes. En théorie, nous devrions faire cela à chaque fois que l'on fait une transformation. Par exemple, le cube du chapitre du chapitre précédent utilise une transformation, et plus précisément une rotation, qui nous permet de faire une pseudo-animation. Nous devons donc utiliser la sauvegarde de matrice ici.

Pourquoi me direz-vous ? Simplement parce que le prochain modèle que nous voudrons afficher (comme un autre cube) sera automatiquement affecté par la rotation. Ce qui fait qu'au lieu de faire pivoter le cube initial, nous les ferons pivoter tous les deux. Essayez ce code pour voir :

// Rotation du repère

modelview = rotate(modelview, angle, vec3(0, 1, 0));


// Affichage du premier cube

cube.afficher(projection, modelview);


// Affichage du second cube un peu plus loin

modelview = translate(modelview, vec3(10, 0, 0));
cube.afficher(projection, modelview);

Reculez votre caméra avant en la plaçant au point de coordonnées suivantes :

// Placement de la caméra

modelview = lookAt(vec3(6, 6, 6), vec3(3, 0, 0), vec3(0, 1, 0));

Compilez pour voir.

Vous verrez que les deux cubes sont affectés par la rotation.

Dans un jeu-vidéo, ça serait assez problématique si une simple rotation de caméra faisait pivoter tout un bâtiment. :p

Pour éviter ça, il faut sauvegarder l'état de la matrice modelview avant la rotation, puis la restaurer une fois le modèle affiché :

// Sauvegarde de la matrice modelview

mat4 sauvegardeModelview = modelview;


    // Rotation du repère

    modelview = rotate(modelview, angle, vec3(0, 1, 0));


    // Affichage du premier cube

    cube.afficher(projection, modelview);


// Restauration de la matrice

modelview = sauvegardeModelview;


// Affichage du second cube plus loin

modelview = translate(modelview, vec3(10, 0, 0));
cube.afficher(projection, modelview);

Si vous essayez ce code, vous remarquerez que le second cube ne pivote plus. La rotation n'est valable que pour le premier car la matrice avait été sauvegardée avant.

Un autre exemple

Bien entendu, le système de sauvegarde/restauration ne doit pas être utilisé seulement pour les rotations mais bien pour toutes les transformations (translation et homothétie comprises).

Ce qui fait que le code précédent est encore incomplet car la translation du second cube affectera les objets affichés après. Il faut donc réutiliser la sauvegarde de matrice :

// Affichage du premier cube

....


// Sauvegarde de la matrice modelview

mat4 sauvegardeModelview = modelview;


    // Affichage du second cube plus loin

    modelview = translate(modelview, vec3(2, 0, 0));
    cube.afficher(projection, modelview);


// Restauration de la matrice

modelview = sauvegardeModelview;
Le cas des objets proches

Ce système de sauvegarde nous permet de revenir à chaque fois au centre du repère. Nos modèles sont donc placés sans erreur vu que nous partons toujours du point de coordonnées (0, 0, 0). Aucune transformation ne les affecte.

Le seul cas où vous pourrez vous permettre de ne pas restaurer la matrice immédiatement est le cas où des objets se situeraient près les uns des autres. Par exemple, imaginez que votre cube devienne une caisse et que vous souhaitez en afficher plusieurs côte à côte. Vous n'allez pas sauvegarder votre matrice à chaque fois pour revenir quasiment au même point après.

Si elles sont assez proches, vous pourrez les afficher les unes à la suite des autres sans problème et ce même si vous utilisez d'autres transformations :

// Sauvegarde de la matrice modelview

mat4 sauvegardeModelview = modelview;


    // Affichage du premier cube (au centre du repère)

    cube.afficher(projection, modelview);


    // Affichage du deuxième cube

    modelview = translate(modelview, vec3(3, 0, 0));
    cube.afficher(projection, modelview);


    // Affichage du troisième cube

    modelview = translate(modelview, vec3(3, 0, 0));
    cube.afficher(projection, modelview);


// Restauration de la matrice

modelview = sauvegardeModelview;
Image utilisateur

Vous voyez ici que les cubes sont assez proches, il est donc inutile de revenir au centre du repère pour repartir afficher la caisse suivante.

Ce qu'il faut retenir

Pour résumer tout ce blabla un peu confus :

  • Vous devez sauvegarder et restaurer la matrice modelview à chaque fois que vous faites une transformation

  • Si vous affichez des objets assez proches, ne restaurez votre matrice qu'une fois toutes les transformations faites. Ceci afin d’économiser un peu de temps de calcul

  • Si vous ne faites pas de transformation, ne sauvegardez rien

C'est tout ce qu'il faut retenir dans ce chapitre. :)

Télécharger : Code Source C++ du Chapitre 8

Nous voici à la fin de ce chapitre. Nous avons appris à coder et à utiliser les sauvegardes de matrice. C'était un chapitre un peu technique mais il concernait une notion indispensable de la programmation avec OpenGL. N'oubliez pas qu'à partir de maintenant, nous utiliserons toujours les piles de matrices pour afficher nos modèles. ;)

Pour la suite du tuto je vous propose de vous reposer encore un peu, le prochain chapitre que nous allons aborder sera assez facile à comprendre. Il concernera les évènements avec la SDL 2.0. :)

Exemple de certificat de réussite
Exemple de certificat de réussite