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.
À 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.
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.
- Vertex shader : exécuté une fois par sommet. Son unique obligation est d'écrire la position finale du sommet dans la variable
gl_Position. - 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.
- Fragment shader : exécuté une fois par fragment. Il calcule la couleur du pixel et l'écrit dans sa variable de sortie.
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.
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.
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
}
#version 300 esdoit être la toute première ligne de la chaîne, sans espace ni ligne vide avant. Écrivez donc`#version 300 escollé au guillemet.- GLSL est strict sur les types :
1est un entier,1.0est un flottant.vec4(1, 0, 0, 1)est accepté (conversion dans un constructeur), maisfloat x = 1;provoque une erreur. - Les couleurs vont de 0.0 à 1.0, pas de 0 à 255.
Les types à connaître
| Type | Contenu | Exemple |
|---|---|---|
float | un nombre à virgule | float t = 0.5; |
vec2, vec3, vec4 | 2, 3 ou 4 flottants | vec3 rouge = vec3(1.0, 0.0, 0.0); |
mat3, mat4 | matrices (séances 4 et 6) | mat3 m = mat3(1.0); (identité) |
sampler2D | une 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.
- Buffer : un tableau de nombres stocké sur le GPU.
- Attribut (
indans 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 :
- 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). - La page est-elle bien servie par Live Server (
http://127.0.0.1:5500/…) et non ouverte enfile://? - 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.
- Les coordonnées sont-elles bien entre −1 et 1 ? Le nombre de sommets passé à
drawArraysest-il juste ?
Résumé
- Le GPU exécute deux petits programmes : le vertex shader (un appel par sommet, il place) et le fragment shader (un appel par pixel, il colore).
- Les positions sortent du vertex shader dans l'espace de découpage, de −1 à +1.
- Initialisation (une fois) : contexte, programme, buffer, VAO. Dessin (chaque image) :
clear,useProgram,bindVertexArray,drawArrays. - WebGL est une machine à états : on sélectionne, puis on agit.