Introduction
Dans ce tutoriel, nous commençons à étudier les diverses transformations que peut subir un objet en 3D, lui permettant d’être affiché sur l’écran tout en maintenant l’impression de profondeur dans la scène.
Contexte
Dans ce tutoriel, nous commençons à étudier les diverses transformations que peut subir un objet en 3D, lui permettant d’être affiché sur l’écran tout en maintenant l’impression de profondeur dans la scène.
La méthode commune pour réaliser cela est de représenter chaque transformation en utilisant une matrice, de multiplier les matrices une à une, puis de multiplier la position du sommet par le produit final.
Chacun de ces tutoriels sera dédié à l’étude d’une transformation.
Ici nous étudions la transformation de translation qui est responsable du déplacement d’un objet le long d’un vecteur de taille et de direction quelconques.
Supposons que vous vouliez déplacer le triangle de l’image de gauche à l’emplacement de droite :
Une façon d’y arriver serait de fournir le vecteur de déplacement (dans notre cas (1, 1)) en tant que variable uniforme au shader et tout simplement de l’ajouter à chaque sommet traité. Cependant cela casse la méthode de multiplication d’un groupe de matrice de manière à obtenir une seule transformation complète.
En outre, nous verrons par la suite que la translation n’est généralement pas la première transformation appliquée, donc il faudra alors multiplier la position par la matrice des transformations antérieures à la translation, puis ajouter la translation et enfin multiplier le résultat par la matrice des transformations postérieures à la translation…
C’est pour le moins alambiqué.
Une meilleure façon de faire est de construire une matrice qui représente la translation et de l’intégrer au processus de multiplication des matrices.
Mais peut-on trouver une matrice qui, multipliée par le point (0, 0) – sommet en bas à gauche du triangle de gauche – donne le point (1, 1) ?
En réalité ce n’est pas possible avec une matrice 2D (ni une matrice 3D, pour le point (0, 0, 0)).
De manière générale, on peut dire qu’il nous faut une matrice M qui selon un point P(x, y, z) et un vecteur V(v1, v2, v3) donnés fournit M * P = P1(x + v1, y + v2, z + v3). Autrement dit, cela signifie que cette matrice M déplace P vers l’emplacement P+V.
Nous voyons que chaque composante de P1 est la somme d’une composante de P et de la composante correspondante de V. La partie gauche de chaque équation de la somme est fournie par la matrice identité :
Il semble donc que nous devrions commencer par la matrice identité et déterminer les changements qui vont compléter la partie droite de l’équation de la somme de chaque composante (… + V1… + V2… + V3).
Voyons à quoi ressemblerait cette matrice identité améliorée :
De ce calcul nous pouvons tirer deux conclusions :
- a, b, c, d, e et f doivent valoir zéro sinon chaque composante va être influencée par les deux autres (nous sommes donc de retour avec la matrice identité) ;
- Comme x, y et z ont un effet sur les trois composantes du résultat, quand ils valent zéro le résultat vaut le vecteur nul (et nous ne pourrons pas translater le vecteur nul)
Nous voulons une matrice qui fournisse la partie droite suivante du calcul :
Nous devons donc trouver un moyen d’ajouter v1 – v3 comme nous le voyons ci-dessus et a […] f peuvent valoir zéro. Le résultat final sera notre vecteur déplacé.
C’est comme si nous voulions ajouter une quatrième colonne à la matrice mais alors notre calcul serait invalide. Nous ne pouvons pas multiplier une matrice 3 x 4 et un vecteur 3 x 1.
La règle est que l’on ne peut multiplier des matrices que si elles sont de la forme N x M et M x N.
Donc nous devons ajouter une quatrième composante au vecteur. Une bonne quatrième composante sera 1 car nous pouvons mettre v1 – v3 sur la quatrième colonne de la matrice et elles seront inchangées dans le résultat, car multipliées par 1.
Cependant notre matrice est toujours invalide, selon la règle ci-dessus. En effet, multiplier une matrice 3 x 4 par un vecteur 4 x 1 est toujours invalide, ajouter une quatrième ligne à la matrice et la mettre 4 x 4 la rend valide.
Finalement voici notre matrice de translation :
Maintenant, même si x, y et z valent zéro, nous pouvons toujours les déplacer à n’importe quel emplacement.
La représentation d’un vecteur 3D en vecteur 4D de cette manière, est appelée « coordonnées homogènes » et est très populaire et utile au graphisme 3D. La quatrième composante est appelée « w ».
En fait le symbole interne gl_Position
du vertex shader que nous avons vu dans le tutoriel précédent est un vecteur 4D et la composante « w » tient un rôle très important pour transformer la projection 3D en 2D.
La notation commune est d’utiliser w = 1 pour les points et w = 0 pour les vecteurs. La raison est que les points peuvent être déplacés mais pas les vecteurs.
On peut changer la taille d’un vecteur ou sa direction mais tous les vecteurs de même taille et direction sont considérés comme égaux, quelle que soit leur position d’origine. On peut simplement utiliser l’origine du repère pour tous les vecteurs.
Définir w = 0 et multiplier une matrice de translation par le vecteur retournera le même vecteur.
Explication du code
struct Matrix4f<br /> { float m[4][4]; };
Nous avons ajouté la définition d’une matrice 4 x 4 au fichier math_3d.h. Elle sera utilisée pour la plupart des matrices de transformation à partir de maintenant.
GLuint gWorldLocation;
Nous utilisons cet identificateur pour accéder à la variable uniforme contenant la matrice monde dans le shader. Nous l’appelons « monde », car ce que nous allons faire à l’objet est le déplacer là où nous le voulons, dans le système de coordonnées de notre monde virtuel.
Matrix4f World; World.m[0][0] = 1.0f; World.m[0][1] = 0.0f; World.m[0][2] = 0.0f; World.m[0][3] = sinf(Scale); World.m[1][0] = 0.0f; World.m[1][1] = 1.0f; World.m[1][2] = 0.0f; World.m[1][3] = 0.0f; World.m[2][0] = 0.0f; World.m[2][1] = 0.0f; World.m[2][2] = 1.0f; World.m[2][3] = 0.0f; World.m[3][0] = 0.0f; World.m[3][1] = 0.0f; World.m[3][2] = 0.0f; World.m[3][3] = 1.0f;
Dans la fonction de rendu nous préparons une matrice 4 x 4 et la remplissons en accord avec l’explication ci-dessus. Nous définissons v2 et v3 à zéro pour ne pas avoir de changement sur les composantes Y et Z de l’objet et définissons v1 au résultat du sinus.
Ainsi la coordonnée X sera déplacée d’une valeur qui oscillera entre -1 et 1. Tout ce qu’il nous reste à faire est de charger cette matrice dans le shader.
glUniformMatrix4fv(gWorldLocation, 1, GL_TRUE, &World.m[0][0]);
Voici un autre exemple de fonction glUniform*
pour charger des données dans les variables uniformes du shader. Cette fonction-ci charge une matrice 4 x 4 et il existe aussi des versions pour les matrices 2 x 2, 2 x 3, 2 x 4, 3 x 2, 3 x 3, 3 x 4, 4 x 2 et 4 x 3.
Le premier paramètre est l’emplacement de la variable uniforme (récupéré après la compilation du shader, via glGetUniformLocation
).
Le second paramètre indique le nombre de matrices que nous mettons à jour. Nous y mettons 1 pour une matrice mais nous pouvons aussi utiliser cette fonction pour mettre à jour toutes les matrices de multiplication en un appel.
Le troisième paramètre déroute parfois les nouveaux venus. Il indique si la matrice fournie est exprimée ligne par ligne ou colonne par colonne.
Le fait est que les matrices C/C++ sont exprimées ligne par ligne par défaut. Cela signifie que lorsque l’on remplit un tableau bidimensionnel avec des valeurs, elles sont rangées en mémoire ligne après ligne avec le « haut » à l’adresse la plus basse.
Par exemple, en regardant le tableau suivant :
int a[2][3]; a[0][0] = 1; a[0][1] = 2; a[0][2] = 3; a[1][0] = 4; a[1][1] = 5; a[1][2] = 6;
Visuellement, le tableau ressemble à la matrice suivante :
1 2 3
4 5 6
Et l’organisation en mémoire ressemble ça : 1 2 3 4 5 6 (avec 1 à l’adresse la plus basse).
Donc notre troisième paramètre de glUniformMatrix4fv
est GL_TRUE,
car nous fournissons une matrice ligne par ligne. Nous pourrions aussi le mettre à GL_FALSE
, mais nous devrions alors transposer notre matrice (l’organisation en mémoire resterait identique, mais OpenGL interpréterait les quatre premières valeurs comme la première colonne et ainsi de suite, et agirait en conséquence).
Le quatrième paramètre est simplement l’adresse de début de la matrice en mémoire.
Le reste de la source provient du code du shader.
uniform mat4 gWorld;
Ceci est la variable uniforme d’une matrice 4 x 4. mat2
et mat3
sont aussi disponibles.
gl_Position = gWorld * vec4(Position, 1.0);
La position des sommets du triangle dans le tampon de sommets est un vecteur à trois composantes, mais nous avons convenu de la nécessité d’une quatrième composante ayant pour valeur 1.
Nous avons donc deux options :
- Placer des sommets avec 4 composantes dans le tampon de sommets
- Ajouter la quatrième composante dans le vertex shader
Il n’y aucun avantage à utiliser la première option : chaque position de sommet consommera quatre octets supplémentaires en mémoire pour une composante qui vaudra toujours 1.
Il est plus efficace de rester avec un vecteur à trois composantes et concaténer la composante « w » dans le shader. En GLSL cela est fait en utilisant vec4(Position, 1.0)
.
Nous multiplions la matrice par ce vecteur et stockons le résultat dans gl_Position
.
En résumé, à chaque image nous générons une matrice de translation qui déplace la coordonnée X d’une valeur qui oscille entre -1 et 1. Le shader multiplie la position de chaque sommet par cette matrice ce qui se traduit par le déplacement d’un objet de gauche à droite et de droite à gauche.
Dans la plupart des cas, l’un des côtés du triangle sortira de la boîte normalisée après le vertex shader et le clipper le découpera. Nous ne verrons donc que la partie qui se trouve dans la boîte normalisée.
Résultat
Remerciements
Merci à Etay Meiri de me permettre de traduire ses tutoriels.