Séance 4 · Cours
Transformations 2D et matrices
Déplacer, tourner, agrandir : trois opérations qui, écrites sous forme de matrices, se combinent en une seule. C'est l'outil central de toute la programmation graphique, en 2D comme en 3D.
À la fin de cette séance, vous saurez
- écrire les formules et les matrices de translation, rotation et mise à l'échelle ;
- expliquer pourquoi on passe en coordonnées homogènes (x, y, 1) ;
- composer des transformations dans le bon ordre ;
- travailler en pixels grâce à une matrice de projection, et animer des objets liés entre eux.
1. Les trois transformations de base
Un objet est décrit une fois pour toutes par ses sommets, dans son propre repère (centré sur lui-même, en général). Pour le placer dans la scène, on applique à chaque sommet (x, y) une transformation qui donne (x′, y′).
| Transformation | Paramètres | Formule |
|---|---|---|
| Translation | vecteur (tx, ty) | x′ = x + txy′ = y + ty |
| Mise à l'échelle | facteurs (sx, sy) | x′ = sx · xy′ = sy · y |
| Rotation autour de l'origine | angle θ (radians, sens anti-horaire) | x′ = x·cos θ − y·sin θy′ = x·sin θ + y·cos θ |
D'où vient la formule de rotation ?
Regardons ce que devient chaque axe. Le vecteur (1, 0), tourné de θ, arrive sur le cercle unité en (cos θ, sin θ). Le vecteur (0, 1), qui est un quart de tour plus loin, arrive en (−sin θ, cos θ). Or tout point s'écrit (x, y) = x · (1, 0) + y · (0, 1). Il suffit de remplacer chaque axe par son image :
(x′, y′) = x · (cos θ, sin θ) + y · (−sin θ, cos θ)
On retrouve les deux formules du tableau. Cette idée est la clé des matrices : les colonnes d'une matrice sont les images des axes.
2. Écrire les transformations avec des matrices
Mise à l'échelle et rotation s'écrivent comme un produit matrice × vecteur. Chaque ligne du résultat est la ligne de la matrice multipliée terme à terme par le vecteur, puis sommée.
Le problème de la translation
La translation x′ = x + tx ajoute une constante : aucune matrice 2 × 2 ne peut faire ça (une matrice envoie toujours (0, 0) sur (0, 0)). L'astuce consiste à ajouter une troisième coordonnée qui vaut toujours 1. On parle de coordonnées homogènes. La troisième colonne de la matrice est multipliée par ce 1 : elle devient la translation.
Les trois transformations deviennent toutes des matrices 3 × 3 :
3. Composer : une seule matrice pour tout
Pour appliquer d'abord une mise à l'échelle S, puis une rotation R, puis une translation T :
p′ = T · ( R · ( S · p ) ) = (T · R · S) · p
Le produit matriciel est associatif : on calcule une seule fois la matrice M = T · R · S en JavaScript, on l'envoie comme uniform, et le GPU l'applique à tous les sommets. Quelle que soit la complexité de la transformation, le vertex shader ne fait qu'une multiplication.
Dans T · R · S, c'est S qui s'applique en premier au point. Et l'ordre compte : le produit matriciel n'est pas commutatif. Tourner puis déplacer fait tourner l'objet sur lui-même puis le place. Déplacer puis tourner le fait tourner autour de l'origine, comme une planète autour du Soleil.
4. Dans le code
Côté GLSL
#version 300 es
in vec2 a_position;
uniform mat3 u_matrice;
void main() {
vec3 p = u_matrice * vec3(a_position, 1.0); // coordonnées homogènes
gl_Position = vec4(p.xy, 0.0, 1.0);
}
Côté JavaScript : stockage par colonnes
Une matrice 3 × 3 est envoyée au GPU comme un tableau de 9 nombres. WebGL attend les nombres colonne par colonne (column-major). C'est un piège fréquent : le tableau écrit dans le code ressemble à la matrice transposée.
[1, 0, 0, 0, 1, 0, tx, ty, 1]
export function translation(tx, ty) {
return [
1, 0, 0, // colonne 0 : image de l'axe x
0, 1, 0, // colonne 1 : image de l'axe y
tx, ty, 1, // colonne 2 : translation
];
}
export function rotation(angle) {
const c = Math.cos(angle), s = Math.sin(angle);
return [
c, s, 0, // image de l'axe x : (cos, sin)
-s, c, 0, // image de l'axe y : (-sin, cos)
0, 0, 1,
];
}
import * as m3 from "./m3.js";
const locMatrice = gl.getUniformLocation(program, "u_matrice"); // initialisation
const M = m3.mul(m3.translation(200, 100), m3.rotation(0.5), m3.scaling(2, 2)); // M = T · R · S
gl.useProgram(program);
gl.uniformMatrix3fv(locMatrice, false, M); // false : ne pas transposer (WebGL l'interdit de toute façon)
Vous écrirez au TP les fonctions scaling et multiply. Pour le produit, rappel : l'élément (ligne i, colonne j) de A · B est la somme des produits de la ligne i de A par la colonne j de B.
5. Travailler en pixels : la projection
Jusqu'ici nos coordonnées allaient de −1 à 1, et un carré paraissait étiré sur un canvas rectangulaire. Il est bien plus naturel de placer les objets en pixels, avec (0, 0) en haut à gauche comme en CSS. La conversion pixels → espace de découpage est une transformation comme les autres :
- diviser par la taille du canvas : de [0, largeur] à [0, 1] ;
- multiplier par 2 : vers [0, 2] ;
- soustraire 1 : vers [−1, 1] ;
- inverser l'axe y, qui descend en pixels mais monte en espace de découpage.
Ces quatre étapes tiennent dans une seule matrice, qu'on place en tête du produit (elle s'applique en dernier) :
M = P · T · R · S
Comme l'axe y descend, une rotation d'angle positif paraît tourner dans le sens des aiguilles d'une montre à l'écran.
6. Tourner autour d'un autre point
La rotation tourne autour de l'origine. Pour faire tourner un objet autour d'un point c (un pivot, une charnière), on ramène c à l'origine, on tourne, puis on remet en place :
Rc = T(c) · R(θ) · T(−c)
Le plus simple reste souvent de dessiner l'objet centré sur son pivot dès le départ, dans son propre repère.
7. Hiérarchies d'objets
Un bras de robot : l'avant-bras est attaché au bras, la main à l'avant-bras. Si le bras tourne, tout ce qui y est attaché doit suivre. On décrit chaque élément relativement à son parent et on multiplie les matrices en descendant la chaîne :
// chaque segment est un rectangle de longueur 1 qui part de l'origine (son pivot) vers +x
const P = m3.projection(canvas.width, canvas.height);
const mBras = m3.mul(P, m3.translation(epauleX, epauleY), m3.rotation(angle1));
const mAvantBras = m3.mul(mBras, m3.translation(longueurBras, 0), m3.rotation(angle2));
const mMain = m3.mul(mAvantBras, m3.translation(longueurAvantBras, 0), m3.rotation(angle3));
// pour dessiner, on ajoute l'échelle du segment, sans la transmettre aux enfants
gl.uniformMatrix3fv(locMatrice, false, m3.mul(mBras, m3.scaling(longueurBras, 20)));
gl.drawArrays(gl.TRIANGLES, 0, 6);
// ... idem pour l'avant-bras et la main : la main hérite de tous les mouvements
Tournez l'épaule : coude et poignet suivent sans qu'on recalcule leurs angles. Le même principe sert aux squelettes animés des personnages 3D, et au système solaire du TP.
Résumé
- Translation, rotation et mise à l'échelle s'écrivent toutes comme des matrices 3 × 3 en coordonnées homogènes (x, y, 1).
- Les colonnes d'une matrice sont les images des axes (et la translation).
M = P · T · R · S: se lit de droite à gauche. L'ordre change le résultat.- On calcule M une fois en JavaScript ; le shader fait
u_matrice * vec3(a_position, 1.0). - WebGL stocke les matrices par colonnes.
- Hiérarchie : matrice de l'enfant = matrice du parent · transformation locale.