Ce cours est visible gratuitement en ligne.

J'ai tout compris !
Développez vos applications 3D avec OpenGL 3.3

Développez vos applications 3D avec OpenGL 3.3

Mis à jour le vendredi 21 juin 2013
  • Moyen

La troisième dimension (Partie 1/2)

Le chapitre précédent était un peu compliqué. Mais grâce à lui, nous sommes maintenant capables d'utiliser les transformations pour façonner un monde 3D. Dans ce nouveau chapitre, nous allons mettre en commun ce que l'on sait sur OpenGL avec ce que l'on a appris concernant les matrices.

J'ai préféré diviser ce chapitre en deux parties, étant donné que nous allons aborder pas mal de nouvelles notions. Dans cette première partie, nous allons nous concentrer sur l'implémentation des matrices dans notre programme, puis dans la seconde partie, nous implémenterons la troisième dimension et surtout nous apprendrons à l'utiliser ! :p

La matrice de projection

Présentation

Dans le chapitre précédent, nous avons vu ce qu'étaient les matrices ainsi que leur fonctionnement. Nous nous sommes surtout concentrés sur la matrice modelview qui permet de placer nos objets dans un monde 3D. Les transformations que nous avons vues (translation, rotation et homothétie) sont appliquées sur elle.

Dans ce chapitre-la maintenant, nous allons nous concentrer sur une autre matrice : la matrice de projection.

Sa seule utilité est de permettre l'affichage d'un monde 3D sur notre écran qui lui ne possède que 2 dimensions. Cependant, vous vous imaginez bien que ce processus n'est pas aussi simple que cela, on a quand même besoin de quelques paramètres pour permettre une telle opération.

Pour expliquer son fonctionnement, je vais prendre une bonne vieille fonction connue des habitués d'OpenGL : la fonction gluPerspective().

Euh ouai, mais moi je viens ici pour apprendre OpenGL, je ne connais pas cette fonction. :colere2:

Ne vous inquiétez pas, je vais vous expliquer le fonctionnement de cette fonction en détails, et j'ai plutôt intérêt à le faire étant donné que nous allons la recoder entièrement. :p

Comme je vous l'ai dit dans les chapitres précédents, une bonne partie des anciennes fonctions d'OpenGL a disparu avec la nouvelle version. La fonction gluPerspective a elle aussi disparu (indirectement). Il nous faudra donc la coder de nos propres mains. Et pour vous faciliter la tâche, je vais vous expliquer en détails son fonctionnement.

Premièrement, voici le prototype de la fonction :

void gluPerspective(double angle, double ratio, double near, double far);

Hum tout plein de paramètres intéressants ^^ . Je vais commencer par les deux derniers paramètres : near et far.

Near

Le paramètre Near correspond à la distance entre votre écran et ce qui sera affiché.

Image utilisateur

La petite zone entre l'écran et la plaque est une zone où rien ne sera affiché. Même s'il y a un objet dans cet intervalle il ne sera pas affiché.

Pourquoi me direz-vous ? Simplement parce que les mathématiques nous l'imposent, il nous faut obligatoirement une zone non-affichable pour que le calcul de la projection puisse s'effectuer. Il se passe la même chose avec la division par zéro, c'est impossible. Il faut obligatoirement que le dénominateur soit au moins un peu plus grand (ou plus petit mais on s'en moque) que zéro.

Le paramètre near représente donc ce petit intervalle nécessaire au calcul de la projection.

Far

Le paramètre far est un peu plus simple, on peut dire que c'est l'inverse de near.

C'est également une distance entre votre écran et ce qui sera affiché. La différence est que tout objet se trouvant au delà de cette distance ne sera pas affiché.

Image utilisateur

Au final, pour qu'un objet puisse s'afficher sur l'écran, il faut qu'il se situe entre les zones near et far, sinon il ne sera pas affiché.

Ratio

Je pense que vous avez tous entendu parler des télévisions 4/3 (quatre tiers) et 16/9 (seize neuvièmes). Le ratio est le rapport entre la longueur de la télé et sa hauteur. Dans la plupart des cas nous avons soit un ratio de 4/3 soit un ratio de 16/9, voire 16/10 pour les jeux-vidéo.

Pour nous, le ratio s'appliquera à la fenêtre SDL que l'on a codée. Dans le chapitre 3, nous avions créé une fenêtre de 800 pixels par 600 pixels. Le ratio sera donc de 800/600, et si on simplifie la fraction on a un ratio de 4/3.

Dans les jeux-vidéo, on propose généralement plusieurs modes d'affichage afin de s'adapter à l'écran du joueur. En effet, tous les joueurs n'ont pas forcément le même écran. Nous donnerons au final comme paramètre la division entre la longueur de la fenêtre SDL et sa hauteur.

Angle

Ce paramètre est le plus spécial de la fonction. C'est en fait l'angle de vue avec lequel nous allons voir la scène. Plus cet angle sera petit, plus on aura l'impression de faire un effet de zoom sur la scène. Et à l'inverse, plus il sera grand, plus on aura l'impression que la scène s'éloigne et se déforme.

Voici trois exemples de la même scène avec trois angles de vue différents :

Image utilisateur
Image utilisateur
Image utilisateur

Merci à Kayl pour sa scène de test. ^^
Dans la première image, on utilise un angle de 70°, dans la deuxième un angle de 30° et dans la troisième un angle de 100°.

L'avantage d'un angle plus petit est que l'on a l'impression de faire un zoom de jumelle ou de sniper ;) . A l'inverse, un angle plus grand créera une ambiance plus particulière, mais ce genre d'angle s'utilise rarement.

En général, l'angle de vision "normal" est de 70° (70 degrés) et c'est d'ailleurs ce que l'on mettra dans notre code.

Utilisation de la librairie GLM

Dans les précédentes versions d'OpenGL, il existait une multitude de fonctionnalités mathématiques qui étaient gérées par OpenGL même, les matrices en faisait évidemment partie. Cependant, ces fonctionnalités sont maintenant supprimées et il faut trouver un autre moyen de gérer nos matrices nous-même.

Heureusement pour nous, il existe une librairie qui s'appelle GLM (pour OpenGLMathematics). Vous l'avez déjà téléchargée normalement donc vous n'avez rien à faire. Cette librairie nous permet d'utiliser les matrices, et bien plus encore, sans avoir à nous soucier de tout programmer à la main. Elle inclut même certaines fonctions dépréciées comme gluPerspective(). :)

Nous allons donc utiliser GLM pour simuler toutes les fonctions dont nous aurons besoin.

Utilisation de la projection

Inclusion des matrices

Toutes ces belles matrices (que vous adorez j'en suis sûr :p ) ne nous servent pas à grand chose pour l'instant. Nous allons maintenant les intégrer dans notre code OpenGL pour enfin voir ce qu'elles ont véritablement dans le ventre.

Reprenons le code de la boucle principale que nous avons laissé au chapitre 4 :

void SceneOpenGL::bouclePrincipale()
{
    // Variables
	
    bool terminer(false);
    float vertices[] = {-0.5, -0.5,   0.0, 0.5,   0.5, -0.5};
    float couleurs[] = {1.0, 0.0, 0.0,  0.0, 1.0, 0.0,  0.0, 0.0, 1.0};
	
	
    // Shader
	
    Shader shaderCouleur("Shaders/couleur2D.vert", "Shaders/couleur2D.frag");
    shaderCouleur.charger();
	
	
    // Boucle principale
	
    while(!terminer)
    {
        /* ***** Code d'affichage ***** */
    }
}

Je n'ai pas mis tout le code, seule cette partie nous intéresse pour le moment.

Les en-têtes de GLM

Vous savez maintenant que l'on a besoin de deux matrices pour faire fonctionner un jeu 3D : la matrice de projection et la modelview. Nous allons donc les déclarer dans notre code grâce à librairie GLM.

Pour commencer, veuillez inclure les en-têtes suivants dans le fichier SceneOpenGL.h qui permettent à notre programme d'utiliser les matrices :

// Includes OpenGL

....


// Includes GLM

#include <glm/glm.hpp>
#include <glm/gtx/transform.hpp>
#include <glm/gtc/type_ptr.hpp>


// Autres includes

#include <SDL2/SDL.h>
#include <iostream>
#include <string>
#include "Shader.h"

La première inclusion permet d'utiliser les fonctionnalités principales de GLM, la deuxième les transformations et la dernière permet de récupérer les valeurs des matrices (nous verrons cela dans un instant).

Elle est bizarre ton extension c'est marqué .hpp c'est normal ?

Oui tout à fait. :)

La particularité de GLM c'est qu'elle est codée directement dans les headers, il n'y a pas de librairie pré-compilée comme la SDL par exemple. Grâce à ce système, GLM est compatible avec toutes les plateformes et il n'y a pas besoin de fournir de fichier spécifique pour Linux, Windows, etc. Ni même pour compilateur (.a, .dll, etc.)

Inclusion des matrices

Après toute cette théorie nous pouvons enfin coder nos premières matrices. L'objectif est de créer deux matrices du nom de projection et modelview.

Pour cela, nous allons déclarer deux objets de type mat4, pour matrice carrée d'ordre 4. Il faut utiliser le namespace glm en même temps que la déclaration :

// Matrices projection et modelview

glm::mat4 projection;
glm::mat4 modelview;

Tous les objets et méthodes de GLM doivent utiliser le namespace glm. Pour éviter d'avoir à le taper à chaque fois, ajoutez la ligne de code suivante dans chaque classe où vous utiliser les matrices :

// Permet d'éviter la ré-écriture du namespace glm::

using namespace glm;

Avec cette ligne de code, la déclaration des matrices devient :

// Matrices projection et modelview

mat4 projection;
mat4 modelview;

Maintenant que les matrices sont déclarées, nous allons pouvoir les initialiser.

La matrice projection

Occupons-nous d’abord de la projection. Dans la partie précédente, nous avons parlé d'une ancienne fonction du nom de gluPerspective(). Celle-ci a disparu avec OpenGL 3 mais GLM l'a gentiment recoder pour nous sous la forme d'une méthode, voici son prototype :

mat4 perspective(double angle, double ratio, double near, double far);

On remarque qu'elle contient exactement les mêmes paramètres, pratique n'est-ce pas ? La seule différence est qu'elle renvoie un objet de type mat4, cet objet doit être affecté à la matrice projection.

Nous appellerons cette méthode avec les paramètres suivants :

  • Un angle de 70° (qui correspond à un angle de vision normal).

  • Un ratio en fonction de la taille de la fenêtre, ici un ratio de m_largeurFenetre / m_hauteurFenetre.

  • Le paramètre near avec une valeur de 1.

  • Le paramètre far avec une valeur de 100.

Voici son appel :

// Matrices

mat4 projection;
mat4 modelview;


// Initialisation

projection = perspective(70.0, (double) m_largeurFenetre / m_hauteurFenetre, 1.0, 100.0);
La matrice modelview

Pour la matrice modelview c'est un peu plus simple. Pour l'initialiser, nous allons leur donner les valeurs d'une matrice d'identité (avec les 1 en diagonale). Elle ne doit pas avoir que des valeurs nulles car si nous multiplions des vecteurs par des 0, il risquerait d'y avoir un gros problème avec notre scène finale. :-°

Pour cela, nous allons utiliser un constructeur qui ne demande qu'une seule valeur : la valeur 1.0.

// Matrices

mat4 projection;
mat4 modelview;


// Initialisation

projection = perspective(70.0, (double) m_largeurFenetre / m_hauteurFenetre, 1.0, 100.0);
modelview = mat4(1.0);

Le constructeur sait tout seul qu'il doit utiliser une matrice d’identité, cela éviter d'avoir à écrire toutes les valeurs à la main pour mettre des 1.0 en diagonal. :p

La boucle principale

Vous vous souvenez qu'au début du chapitre je vous ai parlé du comportement des matrices avec la boucle principale du programme ? Je vous avais dit qu'à chaque tour de boucle, il fallait réinitialiser la matrice modelview pour ne pas avoir les anciennes valeurs du tour précédent.

Cette réinitialisation se fait exactement de la même façon que l'initialisation. C'est-à-dire que l'on affecte le résultat du constructeur mat4(1.0) à la matrice modelview. Nous devons faire ceci juste après le nettoyage de l'écran :

while(!terminer)
{
    // Gestion des évènements

    SDL_WaitEvent(&m_evenements);
		
    if(m_evenements.window.event == SDL_WINDOWEVENT_CLOSE)
        terminer = true;


    // Nettoyage de l'écran

    glClear(GL_COLOR_BUFFER_BIT);


    // Réinitialisation de la matrice modelview

    modelview = mat4(1.0);
}

Interaction entre le programme et les matrices

Notre code OpenGL devient de plus en plus beau :D . Mais les matrices ne nous servent toujours pas à grand chose elles n'interagissent toujours pas avec notre programme.

Je ne vais pas m'étaler sur le sujet pour le moment, mais sachez que l'interaction entre les matrices et le programme se fait dans le shader. C'est lui qui va faire tous les calculs pour projeter le monde 3D sur notre écran.

Pour le moment, le shader que nous avons chargé (couleur2D) est incapable de gérer la projection, il faut en charger un autre. Si vous n'avez rien modifié dans le dossier "Shaders" du chapitre 4, vous devriez trouver des codes sources qui s'appellent couleur3D.vert et couleur3D.frag(à ne pas confondre avec couleur2D). Si vous ne trouvez pas ces fichiers, re-téléchargez le dossier complet depuis le chapitre 4.

À la différence du premier shader, couleur3D est, lui, capable de gérer la projection. Nous allons donc le charger à la place de son prédécesseur :

// Shader gérant la couleur et les matrices

Shader shaderCouleur("Shaders/couleur3D.vert", "Shaders/couleur3D.frag");
shaderCouleur.charger();

Ne vous inquiétez pas si le fragment shader ne change pas, c'est normal. ;)

Le shader n'est pas le seul à devoir changer, les vertices doivent elles aussi subir une modification. Il faut leur ajouter une troisième dimension pour les rendre compatibles avec la projection.

Ça veut dire que nos triangles seront en 3D ?

Oui et non ... En fait, ils sont en 3D mais pour le moment nous sommes mal placés pour les voir de cette façon. Nous verrons dans la deuxième partie de ce chapitre comment "bien se placer". Ne vous inquiétez pas ça arrivera très vite. ;)

Bref au final, il suffit d'ajouter la coordonnée Z à tous les vertices pour qu'ils soient en 3D. Nous mettrons la valeur -1 pour le moment :

float vertices[] = {-0.5, -0.5, -1.0,   0.0, 0.5, -1.0,   0.5, -0.5, -1.0};

Vu que nous ajoutons une coordonnée à nos vertices, il va falloir modifier un paramètre dans la fonction glVertexAttribPointer. Le paramètre size permet de spécifier la taille d'un vertex, donc son nombre de coordonnées. On le passe désormais à 3 car nous avons 3 coordonnées :

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, vertices);

Revenons à notre shader, vu que c'est lui qui gère les calculs de projection, il va falloir lui envoyer les matrices que nous avons déclarées pour qu'il puisse travailler correctement. L'envoi de matrice au shader se fait avec cette fonction :

glUniformMatrix4fv(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value);
  • location : permet de savoir où envoyer les matrices à l'intérieur même du shader.

  • count : permet de savoir combien de matrice on envoie. On n'enverra qu'une seule matrice à la fois.

  • transpose : booléen qui permet d'inverser ou non la matrice qu'on envoie. Dans notre cas, on lui donnera toujours la valeur GL_FALSE.

  • value : est un pointeur sur le tableau de valeurs de la matrice. Nous utiliserons la méthode value_ptr() de la librairie GLM

Je vous expliquerai en détails le fonctionnement de cette fonction dans le chapitre sur les shaders. Sachez juste qu'elle nous permet d'envoyer nos matrices au shader.

Nous lui enverrons ces valeurs :

  • location : glGetUniformLocation(shaderCouleur.getProgramID(), "Le_nom_de_la_matrice"). Oui c'est bien une fonction (on la verra également dans le chapitre sur les shaders). Le paramètre en rouge est une chaine de caractères, nous lui donnerons la valeur "modelview" et "projection".

  • count : 1 (le chiffre 1) pour une matrice.

  • transpose : GL_FALSE.

  • value : Nous lui donnerons le résultat de la méthode value_ptr() de nos objets mat4.

Grâce à cette fonction, notre shader va pouvoir travailler avec les matrices que nous avons déclarées dans le main.

Nous appellerons cette fonction deux fois vu que nous avons deux matrices à envoyer. Ces appels se feront juste avant une fonction que vous connaissez bien : glDrawArrays().

Dans notre cas, nous ferons ces appels toujours de la même façon :

// On envoie les matrices au shader

glUniformMatrix4fv(glGetUniformLocation(shaderCouleur.getProgramID(), "modelview"), 1, GL_FALSE, value_ptr(modelview));
glUniformMatrix4fv(glGetUniformLocation(shaderCouleur.getProgramID(), "projection"), 1, GL_FALSE, value_ptr(projection));

Si on les intègre au code d'affichage ça donne ceci :

// On spécifie quel shader utiliser

glUseProgram(shaderCouleur.getProgramID());


    // Envoi des vertices et des couleurs

    ....


    // On envoie les matrices au shader

    glUniformMatrix4fv(glGetUniformLocation(shaderCouleur.getProgramID(), "modelview"), 1, GL_FALSE, value_ptr(modelview));
    glUniformMatrix4fv(glGetUniformLocation(shaderCouleur.getProgramID(), "projection"), 1, GL_FALSE, value_ptr(projection));


    // On affiche le polygone

    glDrawArrays(GL_TRIANGLES, 0, 3);


    // Désactivation des Vertex Attrib

    ....


// On n'utilise plus le shader

glUseProgram(0);

Je pense vous avoir parlé de tout, compilons tout ça pour voir que ça donne :

void SceneOpenGL::bouclePrincipale()
{
    // Variables
	
    bool terminer(false);
    float vertices[] = {-0.5, -0.5, -1.0,   0.0, 0.5, -1.0,   0.5, -0.5, -1.0};
    float couleurs[] = {1.0, 0.0, 0.0,  0.0, 1.0, 0.0,  0.0, 0.0, 1.0};
	
	
    // Shader
	
    Shader shaderCouleur("Shaders/couleur3D.vert", "Shaders/couleur3D.frag");
    shaderCouleur.charger();


    // Matrices

    mat4 projection;
    mat4 modelview;

    projection = perspective(70.0, (double) m_largeurFenetre / m_hauteurFenetre, 1.0, 100.0);
    modelview = mat4(1.0);

	
    // Boucle principale
	
    while(!terminer)
    {
        // Gestion des évènements

        SDL_WaitEvent(&m_evenements);

        if(m_evenements.window.event == SDL_WINDOWEVENT_CLOSE)
            terminer = true;


        // Nettoyage de l'écran

        glClear(GL_COLOR_BUFFER_BIT);


        // Réinitialisation de la matrice modelview

        modelview = mat4(1.0);


        // On spécifie quel shader utiliser

        glUseProgram(shaderCouleur.getProgramID());


            // On remplie puis on active le tableau Vertex Attrib 0

            glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, vertices);
            glEnableVertexAttribArray(0);


            // Même chose avec le tableau Vertex Attrib 1

            glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 0, couleurs);
            glEnableVertexAttribArray(1);


            // On envoie les matrices au shader

            glUniformMatrix4fv(glGetUniformLocation(shaderCouleur.getProgramID(), "modelview"), 1, GL_FALSE, value_ptr(modelview));
            glUniformMatrix4fv(glGetUniformLocation(shaderCouleur.getProgramID(), "projection"), 1, GL_FALSE, value_ptr(projection));


            // On affiche le polygone

            glDrawArrays(GL_TRIANGLES, 0, 3);


            // On désactive les tableaux Vertex Attrib puisque l'on n'en a plus besoin

            glDisableVertexAttribArray(1);
            glDisableVertexAttribArray(0);


        // On n'utilise plus le shader

        glUseProgram(0);


        // Actualisation de la fenêtre

        SDL_GL_SwapWindow(m_fenetre);
    }
}

Vous devriez avoir cette fenêtre :

Image utilisateur

C'est tout ? C'est pratiquement la même chose qu'avant. :(

À première vue oui, il ne se passe rien de plus sauf peut-être une légère déformation de notre triangle. Mais sachez que dans votre carte graphique il se passe pas mal de choses. De plus, nous pouvons maintenant utiliser les transformations dans nos programmes et ce sont justement elles qui forment la base d'un jeu-vidéo. Sans transformations, nous serions encore en train de jouer au Pong. :lol:

Exemples de transformations

Introduction

On va se reposer un peu dans cette partie, il n'y a rien de nouveau, on utilisera juste ce que l'on a déjà codé.

Tout d'abord, on va changer un peu nos vertices pour avoir un triangle plus petit. Sinon nous ne pourrions pas vraiment voir ce que donnent les transformations :

float vertices[] = {0.0, 0.0, -1.0,  0.5, 0.0, -1.0,  0.0, 0.5, -1.0};

D'origine, voici la position du triangle :

Image utilisateur

Les transformations

La translation

Prenons un vecteur V de coordonnées (0.4, 0.0, 0.0) pour effectuer une translation du repère (matrice modelview) sur 0.4 unité sur l'axe X. Normalement, le triangle devrait se retrouver sur la gauche.

Pour faire une translation avec GLM, nous allons utiliser la méthode translate() dont voici le prototype :

mat4 translate(mat4 matrice, vec3 translation);
  • mat4 : matrice qui sera multipliée par la matrice de translation. Il s'agit ici de modelview

  • translation : objet vecteur à 3 coordonnées. Il correspond ici un point dans l'espace avec 3 coordonnées (x, y et z)

La méthode renvoie la matrice donnée en paramètre avec la translation ajoutée.

Pour contenter l'objet vec3, nous allons juste appeler le constructeur vec3 avec les coordonnées de la translation que l'on veut faire, ici (0.4, 0.0, 0.0).

L'appel à la méthode ressemble au final à ceci :

// Translation

modelview = translate(modelview, vec3(0.4, 0.0, 0.0));

On identifie bien ce que fait cette ligne de code. On appelle la méthode translate() qui va modifier la matrice modelview à l'aide du vecteur de coordonnées (0.4, 0.0, 0.0).

Nous devons inclure cette ligne juste avant l'envoi des matrices au shader :

// On spécifie quel shader utiliser

glUseProgram(shaderCouleur.getProgramID());


    // Remplissage des tableaux Vertex Attrib 0 et 1

    ....


    // Translation

    modelview = translate(modelview, vec3(0.4, 0.0, 0.0));


    // On envoie les matrices au shader

    glUniformMatrix4fv(glGetUniformLocation(shaderCouleur.getProgramID(), "modelview"), 1, GL_FALSE, value_ptr(modelview));
    glUniformMatrix4fv(glGetUniformLocation(shaderCouleur.getProgramID(), "projection"), 1, GL_FALSE, value_ptr(projection));


    // Affichage

    ....


// On n'utilise plus le shader

glUseProgram(0);

Ce qui nous donne :

Image utilisateur

Le triangle ne connait aucun changement, nous n'avons pas modifié ses coordonnées. En revanche, le repère lui a changé. On l'a translaté selon le vecteur V, le triangle se trouve donc un peu plus loin.

Bon, sur une si petite forme on ne voit pas trop l'intérêt, mais sur un personnage de plus de 1000 vectices on voit la différence croyez-moi. :p

La rotation

Nous allons maintenant faire pivoter le triangle d'un angle de 60° selon l'axe Z. L'axe Z est pointé vers nous mais nous ne le voyons pas, nous verrons cela dans la deuxième partie du chapitre.

Nous utiliserons pour cela la méthode GLMrotate() :

mat4 rotate(mat4 matrice, double angle, vec3 axis);

Les paramètres sont sensiblement les mêmes :

  • matrice : matrice qui sera multipliée par la rotation. Il s'agit ici de modelview

  • angle : angle de la rotation

  • axis : vecteur représentant l'axe de la rotation

Pour effectuer une rotation de 60° sur l'axe Z, il suffit donc d'appeler la méthode rotate() de cette façon :

// Rotation

modelview = rotate(modelview, 60.0f, vec3(0.0, 0.0, 1.0));

On inclut cette ligne juste avant d'envoyer les matrices au shader :

// On spécifie quel shader utiliser

glUseProgram(shaderCouleur.getProgramID());


    // Remplissage des tableaux Vertex Attrib 0 et 1

    ....


    // Rotation

    modelview = rotate(modelview, 60.0f, vec3(0.0, 0.0, 1.0));


    // On envoie les matrices au shader

    glUniformMatrix4fv(glGetUniformLocation(shaderCouleur.getProgramID(), "modelview"), 1, GL_FALSE, value_ptr(modelview));
    glUniformMatrix4fv(glGetUniformLocation(shaderCouleur.getProgramID(), "projection"), 1, GL_FALSE, value_ptr(projection));


    // Affichage

    ....


// On n'utilise plus le shader

glUseProgram(0);
Image utilisateur

Hop, le triangle a pivoté de 60° (dans le sens trigonométrique). Encore une fois, on modifie le repère, on ne touche pas aux coordonnées du triangle.

L'homothétie

Allez encore une petit exemple. Nous allons faire une homothétie en multipliant par 2 les unités de mesure du repère. 1 unité sera plus longue, donc le triangle paraitra plus gros.

On appellera pour cela la méthode scale() :

mat4 scale(mat4 matrice, vec3 factors);
  • matrice : matrice qui sera multipliée par la rotation. Il s'agit ici de modelview

  • factors : vecteur contenant les 3 facteurs de redimensionnement pour les X, Y et Z

On appellera cette méthode ainsi pour agrandir le repère de 2 unités sur les axes X et Y. L'axe Z n'est pas encore visible pour nous, on le laisse donc comme ça.

// Homothétie

modelview = scale(modelview, vec3(2, 2, 1));

Et comme d'habitude, on l'appelle juste avant d'envoyer les matrices au shader :

// On spécifie quel shader utiliser

glUseProgram(shaderCouleur.getProgramID());


    // Remplissage des tableaux Vertex Attrib 0 et 1

    ....


    // Homothétie

    modelview = scale(modelview, vec3(2, 2, 1));


    // On envoie les matrices au shader

    glUniformMatrix4fv(glGetUniformLocation(shaderCouleur.getProgramID(), "modelview"), 1, GL_FALSE, value_ptr(modelview));
    glUniformMatrix4fv(glGetUniformLocation(shaderCouleur.getProgramID(), "projection"), 1, GL_FALSE, value_ptr(projection));


    // Affichage

    ....


// On n'utilise plus le shader

glUseProgram(0);
Image utilisateur

Le triangle est tellement gros qu'il sort de la fenêtre :lol: . Bref vous avez compris le principe.

Je vais même vous montrer un truc sympa avec les homothéties, on va inverser le triangle. C'est une technique utilisée dans les jeux-vidéo pour les effets de reflets (comme un effet de miroir, d'eau, ...) :

// Inversion du repère

modelview = scale(modelview, vec3(1, -1, 1));

Les axes X et Z ne sont pas modifiés, ils sont multipliés par 1. Mais l'axe Y lui est inversé, voici ce que ça donne :

Image utilisateur
Ordre des transformations

Je vous ai dit au tout début que l'ordre des transformations était important. Vous pouvez désormais le constater par vous-même. Tout d'abord, on va translater notre triangle selon le vecteur V de coordonnées (0.4, 0.0, 0.0) puis on va le faire pivoter d'un angle de 60° sur l'axe Z :

// Translation puis rotation

modelview = translate(modelview, vec3(0.4, 0, 0));
modelview = rotate(modelview, 60.0f, vec3(0, 0, 1));

Voici le rendu :

Image utilisateur

Maintenant, on fait l'inverse :

// Rotation puis translation

modelview = rotate(modelview, 60.0f, vec3(0, 0, 1));
modelview = translate(modelview, vec3(0.4, 0, 0));

Rendu :

Image utilisateur

On n'obtient pas la même chose dans les deux exemples. Faites attention à l'ordre des transformations c'est important. ;)

Multi-affichage

Courage on aborde le dernier point :p . Dans le deuxième chapitre nous avons vu comment afficher plusieurs triangles sans les transformations. Maintenant que nous les avons, on va voir comment se faciliter la vie.

Si vous affichez plusieurs fois la même chose, pas besoin de remplacer les valeurs du tableau car les valeurs sont toutes les mêmes :p . On ne fait qu'appliquer les transformations sur le repère puis on ré-affiche le triangle. Bien sûr, si on change les valeurs de la matrice modelview alors il faut la ré-envoyer au shader :

// On spécifie quel shader utiliser

glUseProgram(shaderCouleur.getProgramID());


    // On translate le premier triangle

    modelview = translate(modelview, vec3(0.4, 0, 0));


    // On envoie les matrices au shader

    glUniformMatrix4fv(glGetUniformLocation(shaderCouleur.getProgramID(), "modelview"), 1, GL_FALSE, value_ptr(modelview));
    glUniformMatrix4fv(glGetUniformLocation(shaderCouleur.getProgramID(), "projection"), 1, GL_FALSE, value_ptr(projection));


    /* ***** Affichage du premier triangle ***** */

    glDrawArrays(GL_TRIANGLES, 0, 3);




    // On fait pivoter le deuxième triangle puis on le translate

    modelview = rotate(modelview, 60.0f, vec3(0, 0, 1));
    modelview = translate(modelview, vec3(0.4, 0, 0));


    // On envoie une deuxième fois les matrices au shader

    glUniformMatrix4fv(glGetUniformLocation(shaderCouleur.getProgramID(), "modelview"), 1, GL_FALSE, value_ptr(modelview));
    glUniformMatrix4fv(glGetUniformLocation(shaderCouleur.getProgramID(), "projection"), 1, GL_FALSE, value_ptr(projection));


    /* ***** Affichage du deuxième triangle ***** */

    glDrawArrays(GL_TRIANGLES, 0, 3);


// On n'utilise plus le shader

glUseProgram(0);

Je vous conseille de faire des petits tests avec les transformations, entrainez-vous avec avant de passer à la deuxième partie de ce chapitre. ;)

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

Nous arrivons à la fin de cette première partie. Nous avons vu pas mal de notions et il est important que vous les compreniez. Encore une fois, amusez-vous à faire des transformations pour vous familiariser avec OpenGL. Si vous vous sentez prêts, passez à la deuxième partie (qui est plus facile à comprendre que cette partie ;) ).

L'auteur

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