WebGL pas à pas

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.

Durée 30 minTP associé TP 4 · Système solaire

À 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′).

TransformationParamètresFormule
Translationvecteur (tx, ty)x′ = x + tx
y′ = y + ty
Mise à l'échellefacteurs (sx, sy)x′ = sx · x
y′ = sy · y
Rotation autour de l'origineangle θ (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.

x′y′
=
cos θ−sin θsin θcos θ
xy
première colonne = image de l'axe x

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.

x′y′1
=
10tx01ty001
xy1
donne bien x′ = x + tx, y′ = y + ty

Les trois transformations deviennent toutes des matrices 3 × 3 :

T =
10tx01ty001
 R =
cos θ−sin θ0sin θcos θ0001
 S =
sx000sy0001

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.

L'ordre se lit de droite à gauche

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.

DémoComposer des transformations
En gris : la forme d'origine, centrée sur l'origine (croix). Les coordonnées sont en pixels, axe y vers le haut. Passez à « R · T · S » et faites varier l'angle : la forme tourne autour de l'origine au lieu de tourner sur elle-même.

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.

10tx01ty001
s'écrit [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 :

  1. diviser par la taille du canvas : de [0, largeur] à [0, 1] ;
  2. multiplier par 2 : vers [0, 2] ;
  3. soustraire 1 : vers [−1, 1] ;
  4. 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) :

P =
2/l0−10−2/h1001
et   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
DémoBras articulé

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é