OpenGL Moderne

Tutoriel 3 : matrices

Opengl-tutorial.org

Traducteur

Alexandre Laurent

« Les moteurs ne déplacent pas du tout le vaisseau. Le vaisseau reste où il est et les moteurs déplacent l'univers autour de lui. » Futurama.

C'est le tutoriel le plus important de la série. Assurez-vous de le lire au moins huit fois.

Commentez

- Titre : OpenGL Moderne

- Auteur : Opengl-tutorial.org

- Traducteur : Alexandre Laurent

- Parution : 2 avril 2014

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2014 opengl-tutorial.org. 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

- Navigation

- I. Coordonnées homogènes

- II. Matrices de transformation

- II-A. Une introduction aux matrices

- II-B. Matrices de translation

- II-C. La matrice d'identité

- II-D. Matrices de mise à l'échelle

- II-E. Matrices de rotation

- II-F. Accumuler les transformations

- III. Les matrices de modèle, de vue et de projection

- III-A. La matrice de modèle

- III-B. La matrice de vue

- III-C. La matrice de projection

- III-D. Accumulation des transformations : la matrice ModelViewProjection

- IV. Rassembler le tout

- V. Exercices

- VI. Remerciements

- Navigation

Navigation

Tutoriel précédent : le premier triangle

Sommaire

Tutoriel suivant : un cube coloré

I - Coordonnées homogènes

Jusqu'à présent, on a considéré les sommets 3D comme des triplets (x, y, z). On va introduire w, ce qui donnera des vecteurs (x, y, z, w).

Cela deviendra bientôt clair, mais pour le moment, retenez ceci :

- si w == 1, alors le vecteur (x, y, z, 1) est une position dans l'espace ;

- si w == 0, alors le vecteur (x, y, z, 0) est une direction.

(En fait, retenez-le à vie.)

Quelle différence cela fait-il ? Eh bien, pour une rotation, cela ne change rien. Lorsque vous tournez un point ou une direction, vous obtenez le même résultat. Par contre, pour une translation (lorsque vous déplacez le point dans une certaine direction), les choses sont différentes. Que signifie « déplacer une direction » ? Pas grand-chose.

Les coordonnées homogènes permettent d'utiliser une simple formule mathématique pour gérer ces deux cas.

II - Matrices de transformation

II-A - Une introduction aux matrices

De façon simple, une matrice est un tableau de nombres avec un nombre prédéfini de lignes et colonnes. Par exemple, une matrice 2 x 3 ressemble à ceci :

Dans les graphismes 3D, nous utilisons uniquement des matrices 4 x 4. Elles vont permettre de transformer les vecteurs (x, y, z, w). Cela s'effectue en multipliant le sommet par la matrice :

Matrix x Sommet (dans cet ordre !!) = SommetTransformé

Ce n'est pas aussi effrayant qu'il y paraît. Placez votre doigt de la main gauche sur le 'a' et votre doigt de la main droite sur le 'x'. C'est 'ax'. Déplacez votre main gauche sur le prochain nombre (b) et votre main droite sur le prochain nombre (y). Vous obtenez 'by'. Encore une fois : 'cz'. Une nouvelle fois : 'dw'. ax + by + cz + dw. Vous avez votre nouveau 'x'. Faites de même pour chaque ligne et vous obtiendrez votre nouveau vecteur (x, y, z, w).

C'est très ennuyeux à calculer et on va le faire souvent, donc, votre ordinateur va le faire pour vous.

En C++, avec GLM :

glm::mat4 myMatrix;
glm::vec4 myVector;
// remplir myMatrix et myVector d'une façon ou d'une autre
glm::vec4 transformedVector = myMatrix * myVector; // Encore, dans cet ordre ! C'est important.

En GLSL :

mat4 myMatrix;
vec4 myVector;
// remplir myMatrix et myVector d'une façon ou d'une autre
vec4 transformedVector = myMatrix * myVector; // Super, c'est presque pareil qu'avec GLM

(avez-vous copié/collé ceci dans votre code ? Allez-y, essayez.)

II-B - Matrices de translation

Ce sont les plus simples matrices de transformation à comprendre. Une matrice de translation ressemble à ceci :

où X, Y, Z sont les valeurs que vous voulez ajouter à votre position.

Donc, si l'on veut déplacer le vecteur (10, 10, 10, 1) de dix unités sur l'axe des X, nous obtenons :

(Faites-le ! Faiiiites-le!)

… et on obtient un vecteur homogène (20, 10, 10, 1) ! Rappelez-vous, le 1 signifie que c'est une position et non pas une direction. Donc notre transformation n'a pas modifié le fait que nous utilisons une position, ce qui est une bonne chose.

Voyons voir maintenant ce qui se passe pour un vecteur qui représente une direction vers l'axe -Z : (0, 0, -1, 0)

… soit, notre direction originale (0, 0, -1, 0), ce qui est juste, car comme je l'ai dit précédemment, le déplacement d'une direction n'a aucun sens.

Comment traduire cela en code ?

En C++, avec GLM :

#include <glm/transform.hpp> // après <glm/glm.hpp>

glm::mat4 myMatrix = glm::translate(10.0f, 0.0f, 0.0f);
glm::vec4 myVector(10.0f, 10.0f, 10.0f, 0.0f);
glm::vec4 transformedVector = myMatrix * myVector; // devinez le résultat

En GLSL : Bon, en fait, vous ne le ferez presque jamais. La plupart du temps, vous utilisez glm::translate() en C++ pour calculer votre matrice et vous l'envoyez au GLSL et vous la multipliez simplement par votre vecteur :

vec4 transformedVector = myMatrix * myVector;

II-C - La matrice d'identité

Celle-ci est spéciale. Elle ne fait rien. Mais je la mentionne car c'est aussi important que de savoir que A fois 1.0 donne A.

En C++ :

glm::mat4 myIdentityMatrix = glm::mat4(1.0f);

II-D - Matrices de mise à l'échelle

Les matrices de mise à l'échelle sont aussi assez simples :

Si vous souhaitez redimensionner un vecteur (une position ou une direction, peu importe) par 2.0 dans toutes les directions :

et la valeur de w ne change pas. Vous pouvez vous demander : que signifie le sens d'une mise à l'échelle d'une direction ? Eh bien, souvent, pas grand-chose. Généralement, on ne le fait pas sauf dans quelques (rares) cas où cela peut être pratique.

La matrice d'identité n'est qu'un cas spécifique des matrices de redimensionnement, avec (X, Y, Z) = (1, 1, 1). C'est aussi un cas spécifique des matrices de translation avec (X, Y, Z) = (0, 0, 0).

En C++ :

// Utilisez #include <glm/gtc/matrix_transform.hpp> et #include <glm/gtx/transform.hpp>
glm::mat4 myScalingMatrix = glm::scale(2.0f, 2.0f ,2.0f);

II-E - Matrices de rotation

Elles sont assez compliquées. Je vais passer les détails ici, sachant qu'il n'est pas important de connaître leur fonctionnement pour un usage quotidien. Pour plus d'informations, jetez un œil à la FAQ sur les matrices (ou cette populaire FAQ sur les matrices et quaternions (en anglais)).

En C++ :

// Utilisez #include <glm/gtc/matrix_transform.hpp> et #include <glm/gtx/transform.hpp>
glm::vec3 myRotationAxis( ??, ??, ??);
glm::rotate( angle_en_degrees, myRotationAxis );

II-F - Accumuler les transformations

Voilà, on sait comment tourner, déplacer et redimensionner nos vecteurs. Cela serait bien si on pouvait combiner ces transformations. Cela est possible en multipliant les matrices ensemble, par exemple :

VecteurTransforme = MatrixTranslation * MatriceRotation * MatriceRedimensionnement * OriginalVecteur;

Cette ligne effectue la mise à l'échelle en premier, puis la rotation et finalement, la translation. C'est ainsi que la multiplication de matrice fonctionne.

Le résultat serait différent en écrivant ces opérations dans un autre ordre. Testez vous-même :

- faites un pas en avant (attention à votre ordinateur) et tourner vers la gauche ;

- tournez vers la gauche et faites un pas en avant.

En fait, l'ordre utilisé ci-dessus est celui que vous allez normalement utiliser pour les personnages et objets de votre jeu : la mise à l'échelle en premier, si besoin ; puis sa direction ; finalement la translation. Par exemple, pour un vaisseau (pour simplifier, les rotations ont été retirées) :

- La mauvaise façon :

- vous déplacez le vaisseau de (10, 0, 0). Son centre est maintenant à dix unités de l'origine ;

- vous redimensionnez la taille de votre vaisseau d'un facteur 2. Toutes les coordonnées sont multipliées par 2 par rapport à l'origine, qui est très loin… finalement vous avez un gros vaisseau, mais centré à 2*20 = 20. Chose que vous ne voulez pas.

- La bonne façon :

- vous redimensionnez la taille de votre vaisseau d'un facteur 2. Vous obtenez un gros vaisseau, centré sur l'origine ;

- vous déplacez votre vaisseau. Il conserve la même taille et il est au bon endroit.

Les multiplications matrices par matrices sont très proches des multiplications matrices par vecteur, donc encore une fois, je vais passer quelques détails et vous rediriger sur la FAQ des matrices si nécessaire. Pour le moment, on demande simplement à l'ordinateur de le faire :

En C++ avec GLM :

glm::mat4 myModelMatrix = myTranslationMatrix * myRotationMatrix * myScaleMatrix;
glm::vec4 myTransformedVector = myModelMatrix * myOriginalVector;

En GLSL :

mat4 transform = mat2 * mat1;
vec4 out_vec = transform * in_vec;

III - Les matrices de modèle, de vue et de projection

Pour la suite du tutoriel, on supposera savoir comment dessiner le modèle 3D favori de Blender : le singe Suzanne.

Les matrices de modèle, de vue et de projection sont des outils pratiques pour différencier proprement les transformations. Vous pouvez ne pas les utiliser (après tout, c'est ce que l'on a fait dans les deux premiers tutoriels). Mais vous devriez. C'est la façon dont tout le monde fait, car cela est une approche propre, comme on va le voir.

III-A - La matrice de modèle

Le modèle, tout comme notre triangle rouge adoré, est défini par un ensemble de sommets. Les coordonnées X, Y, Z de ces sommets sont définies par rapport au centre de l'objet : ce qui veut dire que si un vertex est en (0, 0, 0), il est au centre de l'objet.

On aimerait pouvoir déplacer cet objet, peut-être car le joueur le contrôle avec le clavier et la souris. Facile, vous devez simplement apprendre à le faire : déplacement * rotation * redimensionnement, et c'est fini. Vous appliquez cette matrice sur tous vos sommets à chaque image (en GLSL, pas en C++) et tout bouge. Quelque chose qui ne bouge pas sera au centre du monde.

Vos sommets sont maintenant dans l'espace monde. C'est la signification de la flèche noire dans l'image ci-dessous : on s'est déplacé de l'espace du modèle (tous les sommets sont définis par rapport au centre du modèle) à l'espace monde (tous les sommets sont définis par rapport au centre du monde).

Nous pouvons résumer cela avec le diagramme suivant :

III-B - La matrice de vue

Voici une nouvelle fois la citation de Futurama :

Les moteurs ne déplacent pas du tout le vaisseau. Le vaisseau reste où il est et les moteurs déplacent l'univers autour de lui.

Lorsque vous y pensez, la même chose s'applique aux caméras. Si vous souhaitez voir une montagne à partir d'un autre angle, vous pouvez déplacer soit la caméra… soit la montagne. Bien que cela ne soit pas possible dans la vraie vie, c'est très simple et pratique dans le monde de l'infographie.

Au début, votre caméra est à l'origine de l'espace monde. Afin de déplacer le monde, vous introduisez une nouvelle matrice, tout simplement. Imaginez que vous voulez déplacer la caméra de trois unités vers la droite (+X). C'est équivalent à déplacer l'ensemble du monde (les modèles inclus) trois unités sur la GAUCHE ! (-X). Pendant que votre cerveau fond, voici comment faire :

// Utilisez #include <glm/gtc/matrix_transform.hpp> et #include <glm/gtx/transform.hpp>
glm::mat4 ViewMatrix = glm::translate(-3.0f, 0.0f ,0.0f);

Encore une fois, l'image ci-dessous illustre ce phénomène : on s'est déplacé de l'espace monde (tous les sommets sont définis par rapport au centre du monde, comme nous l'avions fait dans la section précédente) vers l'espace caméra (tous les sommets sont définis par rapport à la caméra).

Avant que votre tête n'explose, profitez de la superbe fonction glm::LookAt de GLM :

glm::mat4 CameraMatrix = glm::LookAt(
cameraPosition, // la position de votre caméra, dans l'espace monde
cameraTarget,   // l'endroit que vous souhaitez regarder, dans l'espace monde
upVector        // certainement glm::vec3(0,1,0), mais (0,-1,0) vous fera voir à l'envers, ce qui peut être tout aussi bien
);

Voici le diagramme obligatoire :

Ce n'est pas encore fini.

III-C - La matrice de projection

On est maintenant dans l'espace caméra. Cela signifie qu'après toutes ces transformations, un sommet ayant les coordonnées x == 0 et y == 0 devrait être affiché au centre de l'écran. Mais on ne peut pas utiliser uniquement les coordonnées x et y pour déterminer où un objet devrait être placé à l'écran : sa distance par rapport à la caméra (z) est aussi importante ! Pour deux sommets avec les même coordonnées x et y, le sommet avec la plus grande coordonnée z sera plus au centre de l'écran que l'autre.

Cela s'appelle la perspective :

Et heureusement pour nous, cette projection peut être représentée par une matrice 4 x 4(1) :

// Génère une matrice très compliquée à lire, mais néanmoins une matrice 4x4 standard
glm::mat4 projectionMatrix = glm::perspective(
FoV,         // Le champ de vision horizontal, en degrés : la valeur de « zoom ». Pensez « lentille de caméra ». Habituellement entre 90° (très large) et 30° (assez zoomé)
4.0f / 3.0f, // Ratio d'aspect. Dépend de la taille de votre fenêtre. Notez que 4/3 == 800/600 == 1280/960, cela vous dit quelque chose ?
0.1f,        // Plan de découpe proche. Gardez-la la plus grande possible, ou vous allez avoir des problèmes de précision.
100.0f       // Plan de découpe lointain. Gardez-la aussi petite que possible.
);

Une dernière fois :

On s'est déplacé de l'espace caméra (tous les sommets sont définis par rapport à la caméra) vers l'espace homogène (tous les sommets sont définis dans un petit cube). Tout ce qui est dans le cube apparaîtra à l'écran.

Et voici le diagramme final :

Voici un nouveau schéma afin que vous compreniez mieux ce qui se passe avec la projection. Avant la projection, nous avons les objets en bleu, dans l'espace caméra, et la forme rouge représente le champ de la caméra : la partie de la scène que la caméra est capable de voir.

Et voici ce que l'on obtient en multipliant tout par la matrice de projection :

Dans cette image, le champ est devenu un cube parfait (entre -1 et 1 sur tous les axes, même si c'est un peu dur à voir) et tous les objets en bleu ont été déformés de la même façon. Donc, les objets proches de la caméra (= proche de la face du cube que nous ne pouvons pas voir) sont gros, les autres sont plus petits. Cela ressemble à la réalité !

Voici à quoi cela ressemble lorsque l'on est à « l'intérieur » du champ :

C'est l'image que vous obtenez ! C'est simplement un peu trop carré, donc une autre transformation mathématique est appliquée pour correspondre à la taille de la fenêtre :

Et c'est cette image qui est affichée !

III-D - Accumulation des transformations : la matrice ModelViewProjection

… une simple multiplication de matrices comme vous les aimez déjà :

// C++ : calcul de la matrice
glm::mat4 MVPmatrix = projection * view * model; // Souvenez-vous : c'est inversé !

// GLSL : application de la matrice
transformed_vertex = MVP * in_vertex;

IV - Rassembler le tout

- Première étape : générer la matrice ModelViewProjection (MVP). Cela doit être fait pour chaque modèle que vous affichez.

// Matrice de projection : Champ de vision de 45° , ration 4:3, distance d'affichage : 0.1 unités <-> 100 unités
glm::mat4 Projection = glm::perspective(45.0f, 4.0f / 3.0f, 0.1f, 100.0f);
// Matrice de la caméra
glm::mat4 View       = glm::lookAt(
glm::vec3(4,3,3), // La caméra est à (4,3,3), dans l'espace monde
glm::vec3(0,0,0), // et regarde l'origine
glm::vec3(0,1,0)  // La tête est vers le haut (utilisez 0,-1,0 pour regarder à l'envers)
);
// Matrice de modèle : une matrice d'identité (le modèle sera à l'origine)
glm::mat4 Model      = glm::mat4(1.0f);  // Changez pour chaque modèle !
// Notre matrice ModelViewProjection : la multiplication des trois  matrices
glm::mat4 MVP        = Projection * View * Model; // Souvenez-vous, la multiplication de matrice fonctionne dans l'autre sens

- Seconde étape : passez les matrices au GLSL

// Obtient un identifiant pour notre variable uniforme "MVP".
// Seulement au moment de l'initialisation.
GLuint MatrixID = glGetUniformLocation(programID, "MVP");

// Envoie notre transformation au shader actuel dans la variable uniforme "MVP"
// Pour chaque modèle affiché, comme la MVP sera différente (au moins pour la partie M)
glUniformMatrix4fv(MatrixID, 1, GL_FALSE, &MVP[0][0]);

- Troisième étape : utilisation des matrices pour transformer les sommets dans le GLSL.

in vec3 vertexPosition_modelspace;
uniform mat4 MVP;

void main(){

// Obtient la position du sommet, dans l'espace de découpe : MVP * position
vec4 v = vec4(vertexPosition_modelspace,1); // Transforme un vecteur 4D homogène, vous vous souvenez ?
gl_Position = MVP * v;
}

- Fini ! Voici le triangle du second tutoriel, toujours à l'origine (0, 0, 0), mais vu en perspective à partir du point (4, 3, 3), la tête en haut (0, 1, 0), avec un champ de vision de 45°.

Dans le sixième tutoriel vous allez apprendre à modifier ces valeurs dynamiquement à l'aide du clavier et de la souris pour créer une caméra comme dans les jeux, mais avant, on doit apprendre comment donner quelques couleurs à nos modèles 3D (tutoriel 4) et des textures (tutoriel 5).

V - Exercices

- Essayez de changer la glm::perspective.

- Au lieu d'utiliser une projection en perspective, utilisez une projection orthogonale (glm::ortho).

- Modifiez ModelMatrix pour déplacer, tourner et redimensionner le triangle.

- Faites la même chose, mais dans un ordre différent. Que remarquez-vous ? Quel est le « meilleur » ordre que vous utiliseriez pour le personnage ?

VI - Remerciements

Cet article est une traduction autorisée dont le texte original peut être trouvé sur opengl-tutorial.org.

Navigation

Tutoriel précédent : le premier triangle

Sommaire

Tutoriel suivant : un cube coloré

(1)[…]heureusement pour nous, cette projection peut être représentée par une matrice 4 x 4 : mais en réalité, ce n'est pas correct. Une perspective n'est pas affine, et donc, ne peut être complètement représentée par une matrice. Après avoir été multipliées par la matrice de projection, les coordonnées homogènes sont divisées par la composante 'w'. Cette composante 'w' se trouve être -Z (car la matrice de projection a été construite ainsi). Ainsi, les points qui sont très loin de l'origine sont divisés par un grand Z ; leurs coordonnées X et Y deviennent petites, les points se rapprocheront les uns des autres, les objets semblent plus petits ; et c'est cela qui donne la perspective.