WebGL pas à pas

Séance 1 · Cours

Le GPU et le pipeline graphique

Avant d'écrire une ligne de WebGL, il faut savoir à qui l'on parle : une carte graphique qui ne sait faire qu'une chose, dessiner des triangles, mais qui la fait des millions de fois par seconde.

Durée 30 minTP associé TP 1 · Premier triangle

À la fin de cette séance, vous saurez

  • expliquer le rôle du vertex shader et du fragment shader ;
  • placer un point dans l'espace de découpage (de −1 à +1) ;
  • écrire les deux phases d'un programme WebGL : initialisation puis dessin ;
  • lire une erreur de compilation de shader dans la console.

1. Pourquoi une carte graphique ?

Un écran Full HD compte environ 2 millions de pixels. Pour une animation fluide, il faut recalculer la couleur de chacun 60 fois par seconde, soit 120 millions de couleurs par seconde, sans compter les calculs de géométrie.

Un processeur (CPU) possède quelques cœurs très polyvalents. Une carte graphique (GPU) en possède des milliers, beaucoup plus simples, qui exécutent le même petit programme en même temps sur des données différentes. Calculer la couleur d'un pixel ne dépend pas de ses voisins : c'est le cas idéal pour le GPU.

À retenir

Le CPU (votre JavaScript) prépare les données et donne les ordres. Le GPU exécute en parallèle de petits programmes appelés shaders. Les deux ont chacun leur mémoire : il faut copier explicitement les données du CPU vers le GPU.

WebGL, c'est quoi ?

WebGL est une API JavaScript, disponible dans tous les navigateurs, qui permet de piloter le GPU pour dessiner dans un élément <canvas>. WebGL 2 reprend OpenGL ES 3.0, la version d'OpenGL des téléphones. Les bibliothèques 3D du Web (Three.js, Babylon.js), les cartes interactives, de nombreux jeux en ligne ou des outils comme Figma reposent dessus.

WebGL est une API de bas niveau : il n'existe pas de fonction drawCircle() ou drawCube(). On ne dessine que des points, des lignes et des triangles, et tout le reste se construit à partir d'eux.

2. Le pipeline : du sommet au pixel

Pour dessiner un triangle, on fournit au GPU la position de ses trois sommets (vertices en anglais, vertex au singulier). Le GPU les fait passer par une chaîne d'étapes appelée pipeline graphique. Deux de ces étapes sont programmables par nous : ce sont les shaders.

JavaScript CPU [x0,y0, x1,y1, x2,y2] Vertex shader 1 exécution par sommet (×3) « où est-il ? » Rastérisation automatique : quels pixels sont dans le triangle ? Fragment shader 1 exécution par pixel couvert « quelle couleur ? » Image canvas écrit en GLSL écrit en GLSL tout ceci s'exécute sur le GPU
Le trajet d'un triangle. Les deux étapes colorées sont les programmes que vous écrivez.
  1. Vertex shader : exécuté une fois par sommet. Son unique obligation est d'écrire la position finale du sommet dans la variable gl_Position.
  2. Assemblage et rastérisation (étape fixe, non programmable) : le GPU relie les sommets en triangles, puis détermine quels pixels de l'écran sont recouverts. Chaque pixel candidat s'appelle un fragment.
  3. Fragment shader : exécuté une fois par fragment. Il calcule la couleur du pixel et l'écrit dans sa variable de sortie.
DémoLa rastérisation

Baissez la résolution : chaque carré est un pixel. Le fragment shader a été exécuté une fois pour chaque carré coloré, et seulement pour eux.

3. L'espace de découpage (clip space)

Quelle que soit la taille du canvas (300 × 150 ou 1920 × 1080 pixels), le vertex shader doit fournir des positions dans un repère fixe : x et y vont de −1 à +1. Le point (0, 0) est au centre, (−1, −1) en bas à gauche, (1, 1) en haut à droite. L'axe y monte, contrairement aux coordonnées CSS.

Tout ce qui sort de ce carré est découpé (clipped), d'où son nom. Le GPU se charge ensuite de convertir ces coordonnées en pixels du canvas.

DémoDéplacez les sommets

Faites glisser les trois sommets. Le tableau ci-dessous est exactement celui que le JavaScript envoie au GPU.

Sortez un sommet du cadre : le triangle est coupé au bord.

Question

Le canvas fait 800 × 400 pixels. Vous dessinez un triangle dont les sommets sont (−0,5 ; −0,5), (0,5 ; −0,5) et (0 ; 0,5). Sa base mesure 1 unité et sa hauteur 1 unité. Mesure-t-il autant de pixels en largeur qu'en hauteur ? Nous corrigerons ce problème à la séance 4.

4. Les shaders en GLSL

Les shaders s'écrivent en GLSL (OpenGL Shading Language), un langage proche du C. En JavaScript, on les écrit dans une chaîne de caractères, puis WebGL les compile pour le GPU.

#version 300 es
// "in" : une donnée différente pour chaque sommet, lue depuis un buffer
in vec2 a_position;

void main() {
  // gl_Position attend 4 composantes (x, y, z, w) : on complète avec z = 0 et w = 1
  gl_Position = vec4(a_position, 0.0, 1.0);
}
#version 300 es
precision highp float;   // précision des calculs flottants (obligatoire ici)

out vec4 couleur;        // la sortie : la couleur du pixel (rouge, vert, bleu, opacité)

void main() {
  couleur = vec4(1.0, 0.5, 0.1, 1.0);   // orange, opaque
}
Pièges classiques
  • #version 300 es doit être la toute première ligne de la chaîne, sans espace ni ligne vide avant. Écrivez donc `#version 300 es collé au guillemet.
  • GLSL est strict sur les types : 1 est un entier, 1.0 est un flottant. vec4(1, 0, 0, 1) est accepté (conversion dans un constructeur), mais float x = 1; provoque une erreur.
  • Les couleurs vont de 0.0 à 1.0, pas de 0 à 255.

Les types à connaître

TypeContenuExemple
floatun nombre à virgulefloat t = 0.5;
vec2, vec3, vec42, 3 ou 4 flottantsvec3 rouge = vec3(1.0, 0.0, 0.0);
mat3, mat4matrices (séances 4 et 6)mat3 m = mat3(1.0); (identité)
sampler2Dune texture (séance 5)uniform sampler2D u_image;

Les composantes d'un vecteur se lisent avec .x .y .z .w (ou .r .g .b .a pour une couleur), et on peut en prendre plusieurs à la fois : v.xy, c.rgb, p.yx. Cette écriture s'appelle le swizzling. Les opérations + - * / agissent composante par composante.

5. Le programme JavaScript en deux phases

Un programme WebGL se découpe toujours de la même façon. L'initialisation s'exécute une seule fois : on prépare tout ce qui doit aller sur le GPU. Le dessin s'exécute à chaque image : on choisit ce qu'on utilise, puis on lance le dessin.

Phase 1 : initialisation

const canvas = document.getElementById("canvas");
const gl = canvas.getContext("webgl2");          // 1. le contexte : notre accès au GPU

// 2. compiler les deux shaders et les relier en un « programme »
//    vertexSource et fragmentSource : les deux chaînes GLSL ci-dessus (entre backticks)
//    createProgram : fonction que vous écrirez au TP 1 (createShader ×2, attachShader, linkProgram)
const program = createProgram(gl, vertexSource, fragmentSource);

// 3. copier les sommets dans un buffer, dans la mémoire du GPU
const positions = new Float32Array([
  -0.5, -0.5,   // sommet 0
   0.5, -0.5,   // sommet 1
   0.0,  0.5,   // sommet 2
]);
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);

// 4. expliquer au GPU comment lire ce buffer pour remplir a_position
const vao = gl.createVertexArray();
gl.bindVertexArray(vao);
const loc = gl.getAttribLocation(program, "a_position");
gl.enableVertexAttribArray(loc);
gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);  // 2 flottants par sommet

Phase 2 : dessin

gl.viewport(0, 0, canvas.width, canvas.height);  // dessiner sur tout le canvas
gl.clearColor(0.1, 0.1, 0.15, 1.0);              // couleur de fond
gl.clear(gl.COLOR_BUFFER_BIT);                   // effacer avec cette couleur

gl.useProgram(program);          // quels shaders ?
gl.bindVertexArray(vao);         // quelles données ?
gl.drawArrays(gl.TRIANGLES, 0, 3);  // dessiner des triangles, à partir du sommet 0, 3 sommets

WebGL est une machine à états

Vous l'avez remarqué : on ne passe presque jamais d'objet en paramètre. gl.bufferData ne dit pas dans quel buffer écrire. Il écrit dans le buffer actuellement « lié » par gl.bindBuffer. De même, gl.drawArrays utilise le programme sélectionné par useProgram et les données du VAO lié par bindVertexArray.

C'est comme l'outil actif d'un logiciel de dessin : on sélectionne d'abord (bind, use), on agit ensuite. Beaucoup de bugs WebGL viennent d'un objet qu'on a oublié de sélectionner, ou d'un autre resté sélectionné par erreur.

À retenir : le vocabulaire
  • Buffer : un tableau de nombres stocké sur le GPU.
  • Attribut (in dans le vertex shader) : une donnée propre à chaque sommet, lue dans un buffer.
  • VAO (Vertex Array Object) : la « notice de lecture » qui associe chaque attribut à son buffer et dit combien de nombres lire par sommet.
  • Programme : un vertex shader et un fragment shader compilés et reliés ensemble.

6. Quand rien ne s'affiche

WebGL échoue souvent en silence : écran noir, pas de message. Ayez le réflexe de la console (F12) et vérifiez dans l'ordre :

  1. Une erreur de compilation de shader ? Le message donne le numéro de ligne, par exemple ERROR: 0:6: 'gl_Position' : undeclared identifier (ligne 6 du shader).
  2. La page est-elle bien servie par Live Server (http://127.0.0.1:5500/…) et non ouverte en file:// ?
  3. Le fond s'efface-t-il de la bonne couleur ? Si oui, le contexte fonctionne et le problème vient des données ou des shaders.
  4. Les coordonnées sont-elles bien entre −1 et 1 ? Le nombre de sommets passé à drawArrays est-il juste ?
DémoLire une erreur de compilation

Résumé