WebGL pas à pas

Séance 1 · Travaux pratiques

TP 1 · Premier triangle

Vous allez écrire, sans aucune bibliothèque, le programme WebGL complet qui affiche un triangle, puis une maison. C'est le TP le plus « bas niveau » du module : tout ce qui suivra réutilise ce que vous écrivez aujourd'hui.

Durée 1 h 30Dossier tp1/depart/Cours Le pipeline graphique

Télécharger le code de départtp1-depart.zip · 3 Ko · à décompresser, puis ouvrir le dossier tp1/depart/ dans VS Code

Objectifs

  • Obtenir un contexte WebGL 2 et effacer le canvas.
  • Écrire, compiler et déboguer un vertex shader et un fragment shader.
  • Envoyer des sommets au GPU (buffer, VAO) et les dessiner.
tp1/depart/ ├── index.html la page : un canvas de 600 × 600 pixels ├── style.css la mise en forme (à ne pas modifier) └── main.js votre code : tous les TODO sont ici

Mise en route

  • Ouvrez le dossier tp1/depart dans VS Code.
  • Clic droit sur index.html → Open with Live Server. La page s'ouvre à une adresse du type http://127.0.0.1:5500/index.html.
  • Ouvrez la console du navigateur (F12, onglet Console).
Vérification

La console affiche « main.js chargé ». Gardez-la ouverte pendant tout le TP.

Le contexte et le fond

Le contexte gl est l'objet par lequel passent tous les appels à WebGL.

  • TODO 1.1 : récupérez le contexte avec canvas.getContext("webgl2"). S'il vaut null, levez une erreur (throw new Error("…")).
  • TODO 1.2 : dans dessiner(), appelez gl.viewport(0, 0, canvas.width, canvas.height), choisissez une couleur de fond avec gl.clearColor(r, g, b, 1.0) puis effacez avec gl.clear(gl.COLOR_BUFFER_BIT).
Vérification

Le canvas prend la couleur choisie (un bleu nuit, par exemple 0.08, 0.1, 0.2). Si le canvas reste noir sans erreur, vérifiez que dessiner() est bien appelée.

Question 1

Les composantes de clearColor vont de 0 à 1. Quelle couleur obtient-on avec (1, 1, 0, 1) ? Et avec (0.5, 0.5, 0.5, 1) ?

Les shaders

Un shader est un petit programme GLSL, écrit dans une chaîne JavaScript, que le GPU compile.

  • TODO 2.1 : dans vertexSource, déclarez in vec2 a_position; avant main, et dans main écrivez gl_Position = vec4(a_position, 0.0, 1.0);
  • TODO 2.2 : dans fragmentSource, donnez une valeur à couleur, par exemple vec4(1.0, 0.55, 0.2, 1.0).
  • TODO 2.3 : complétez createShader en suivant les commentaires. La vérification d'erreur n'est pas optionnelle : sans elle, une faute de frappe dans un shader donne un écran noir sans explication.
  • TODO 2.4 : créez le programme : const program = createProgram(gl, vertexSource, fragmentSource);
Vérification : provoquez une erreur

Rien ne change à l'écran, c'est normal. Pour vérifier que votre gestion d'erreur fonctionne, supprimez volontairement le point-virgule après gl_Position = … et rechargez. La console doit afficher un message du type :

Uncaught Error: Erreur de compilation du shader :
ERROR: 0:6: '}' : syntax error

Le 0:6 signifie : ligne 6 du shader. Corrigez, puis essayez aussi d'écrire vec3 à la place de vec4 dans le fragment shader. Lisez le message, puis corrigez.

Je suis bloqué sur createShader
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
  const message = gl.getShaderInfoLog(shader);
  // ... supprimer le shader, puis lever l'erreur
}

Le triangle

  • TODO 3.1 : créez un Float32Array contenant 3 sommets, soit 6 nombres. Rappel : l'espace de découpage va de −1 à 1 et l'axe y monte.
  • TODO 3.2 : gl.createBuffer(), gl.bindBuffer(gl.ARRAY_BUFFER, …), gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW).
  • TODO 3.3 : créez et liez un VAO (gl.createVertexArray, gl.bindVertexArray), récupérez l'emplacement de l'attribut avec gl.getAttribLocation(program, "a_position"), activez-le avec gl.enableVertexAttribArray(loc), puis décrivez-le avec gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0).
  • TODO 3.4 : dans dessiner(), après l'effacement : gl.useProgram(program), gl.bindVertexArray(vao), gl.drawArrays(gl.TRIANGLES, 0, 3).
Vérification

Un triangle de votre couleur apparaît sur le fond. Si ce n'est pas le cas : l'emplacement renvoyé par getAttribLocation vaut-il -1 (faute de frappe dans le nom) ? Les coordonnées sont-elles entre −1 et 1 ?

Question 2 : les paramètres de vertexAttribPointer

Que signifient le deuxième et le troisième paramètre ? Que se passerait-il avec 3 au lieu de 2 ?

Expériences

Modifiez le code pour répondre aux questions suivantes. Notez vos réponses : elles serviront pour la suite.

Question 3
  1. Placez un sommet en (1.5, 0). Que voit-on ? Pourquoi ?
  2. Dans index.html, passez le canvas à width="900" height="450". Que devient votre triangle ? Expliquez.
  3. Supprimez l'appel à gl.clear. Le résultat change-t-il ? (Remettez-le ensuite.)
  4. Déplacez gl.bindVertexArray(vao) avant gl.useProgram. Est-ce que ça change quelque chose ? Et si on l'enlève complètement de dessiner() ? Pourquoi le triangle s'affiche-t-il encore ?

Une maison

Remplacez le triangle par une maison composée d'un mur carré et d'un toit triangulaire. Un carré se dessine avec deux triangles.

  • Sur papier, faites le croquis de la maison dans le repère [−1, 1] et numérotez les sommets.
  • Écrivez les 9 sommets (3 triangles × 3 sommets) dans le Float32Array.
  • Adaptez le nombre de sommets dans drawArrays.
Vérification

Une maison d'une seule couleur, sans trou ni triangle manquant. Si un morceau manque, vérifiez le dernier paramètre de drawArrays.

Bonus

  • A. Remplacez gl.TRIANGLES par gl.LINE_LOOP, puis gl.POINTS. Pour voir les points, ajoutez gl_PointSize = 10.0; dans le vertex shader.
  • B. Dessinez l'initiale de votre prénom avec des triangles.
  • C. Ajoutez une porte d'une autre couleur. Ajoutez ses 6 sommets à la fin du même buffer, créez un second programme avec un autre fragment shader, et dessinez la porte avec gl.drawArrays(gl.TRIANGLES, 9, 6) (à partir du sommet 9, 6 sommets) après avoir appelé useProgram sur ce second programme. Pour réutiliser le même VAO, l'attribut doit avoir le même emplacement dans les deux programmes : écrivez layout(location = 0) in vec2 a_position; dans les vertex shaders.

La semaine prochaine, nous verrons comment faire tout cela beaucoup plus simplement, avec une couleur par sommet.