WebGL permet de dessiner des graphismes 2D et 3D accélérés par le GPU directement dans une page web. Derrière une simple balise canvas, cette API donne accès à une partie du pipeline graphique de la machine : sommets, shaders, buffers, couleurs et rasterisation.

WebGL est une API de bas niveau. Contrairement à une bibliothèque comme Three.js, elle ne fournit pas directement de caméra, de scène, de lumière ou de modèle 3D. Cette proximité avec le GPU la rend plus exigeante, mais aussi particulièrement utile pour comprendre ce qui se passe réellement lorsqu’un navigateur affiche une scène interactive.

Qu’est-ce que WebGL ?

WebGL est une API JavaScript inspirée d’OpenGL ES. Elle permet au navigateur d’envoyer des données et des instructions au processeur graphique, généralement appelé GPU.

Le GPU est conçu pour exécuter un grand nombre d’opérations similaires en parallèle. Cette architecture convient parfaitement au calcul de la position des sommets, à l’interpolation des couleurs et à la génération des pixels d’une image.

Dans une application WebGL, JavaScript prépare les données et configure le rendu. De petits programmes appelés shaders sont ensuite exécutés par le GPU.

WebGL ne remplace pas le fonctionnement habituel du navigateur présenté dans le rôle du navigateur web. Il ajoute un contexte de rendu spécialisé à un élément canvas, tandis que le reste de la page continue d’être structuré avec HTML, stylisé avec CSS et contrôlé avec JavaScript.

Le pipeline graphique en quelques étapes

Pour afficher un triangle, WebGL suit une chaîne de traitement appelée pipeline graphique.

  1. JavaScript crée une liste de sommets.
  2. Les sommets sont transférés dans un buffer situé côté GPU.
  3. Un vertex shader calcule la position de chaque sommet.
  4. WebGL assemble les sommets pour former un triangle.
  5. Le triangle est transformé en fragments, c’est-à-dire en candidats pixels.
  6. Un fragment shader calcule la couleur de chaque fragment.
  7. Le résultat est écrit dans le canvas.

Cette séparation est fondamentale. Le code JavaScript ne dessine pas chaque pixel manuellement. Il décrit les données, configure le pipeline, puis demande au GPU d’effectuer le rendu.

Créer un canvas et obtenir le contexte WebGL

Commençons par une page minimale contenant un canvas.

<canvas width="640" height="360">
  Votre navigateur ne prend pas en charge le canvas.
</canvas>
<script type="module" src="./main.ts"></script>

Le contexte WebGL constitue le point d’entrée de l’API.

const canvas = document.querySelector("canvas");

if (!canvas) {
  throw new Error("Canvas introuvable");
}

const gl = canvas.getContext("webgl");

if (!gl) {
  throw new Error("WebGL n’est pas disponible dans ce navigateur");
}

La variable gl expose les méthodes nécessaires pour créer des shaders, transférer des données et lancer le rendu.

WebGL utilise un système de coordonnées particulier. L’axe horizontal et l’axe vertical vont généralement de -1 à 1 dans l’espace final attendu par le pipeline. Le point (0, 0) se trouve donc au centre du canvas.

Écrire les premiers shaders

WebGL 1 utilise un langage appelé GLSL ES pour les shaders. Le vertex shader ci-dessous reçoit une position en deux dimensions et produit une position compatible avec le pipeline graphique.

attribute vec2 a_position;

void main() {
  gl_Position = vec4(a_position, 0.0, 1.0);
}

a_position est un attribut : sa valeur peut être différente pour chaque sommet. gl_Position est une variable spéciale attendue par WebGL. Elle contient quatre composantes : x, y, z et w.

Le fragment shader détermine ici une couleur orange identique pour tous les fragments.

precision mediump float;

void main() {
  gl_FragColor = vec4(1.0, 0.45, 0.1, 1.0);
}

Les couleurs WebGL sont généralement exprimées avec des nombres compris entre 0.0 et 1.0. Les quatre composantes représentent le rouge, le vert, le bleu et l’alpha.

Compiler un shader

Les shaders sont fournis à WebGL sous forme de chaînes de caractères. Ils doivent être compilés avant leur utilisation.

function createShader(
  gl: WebGLRenderingContext,
  type: number,
  source: string
): WebGLShader {
  const shader = gl.createShader(type);

  if (!shader) {
    throw new Error("Impossible de créer le shader");
  }

  gl.shaderSource(shader, source);
  gl.compileShader(shader);

  const compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS);

  if (!compiled) {
    const message = gl.getShaderInfoLog(shader) ?? "Erreur inconnue";
    gl.deleteShader(shader);
    throw new Error(`Erreur de compilation du shader : ${message}`);
  }

  return shader;
}

Il est important de consulter getShaderInfoLog. Une erreur GLSL, comme un nom de variable incorrect ou un point-virgule oublié, empêcherait autrement le rendu sans fournir d’explication visible dans la page.

Relier les shaders dans un programme

Le vertex shader et le fragment shader doivent être rassemblés dans un programme WebGL.

function createProgram(
  gl: WebGLRenderingContext,
  vertexShader: WebGLShader,
  fragmentShader: WebGLShader
): WebGLProgram {
  const program = gl.createProgram();

  if (!program) {
    throw new Error("Impossible de créer le programme WebGL");
  }

  gl.attachShader(program, vertexShader);
  gl.attachShader(program, fragmentShader);
  gl.linkProgram(program);

  const linked = gl.getProgramParameter(program, gl.LINK_STATUS);

  if (!linked) {
    const message = gl.getProgramInfoLog(program) ?? "Erreur inconnue";
    gl.deleteProgram(program);
    throw new Error(`Erreur d’édition des liens : ${message}`);
  }

  return program;
}

La compilation vérifie chaque shader séparément. L’édition des liens vérifie qu’ils peuvent fonctionner ensemble et que leurs entrées et sorties sont compatibles.

Envoyer les sommets au GPU

Notre triangle utilise trois sommets, chacun représenté par deux nombres.

const positions = new Float32Array([
   0.0,  0.7,
  -0.7, -0.6,
   0.7, -0.6
]);

Un tableau Float32Array fournit une représentation binaire précise et compacte adaptée aux échanges avec le GPU.

Les données sont transférées dans un buffer WebGL.

const positionBuffer = gl.createBuffer();

if (!positionBuffer) {
  throw new Error("Impossible de créer le buffer");
}

gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);

STATIC_DRAW indique que les données seront peu modifiées et utilisées plusieurs fois pour le rendu. Il s’agit d’une indication d’usage, pas d’une interdiction de les mettre à jour.

Configurer l’attribut de position

Le shader connaît le nom a_position, mais WebGL doit encore savoir dans quel buffer trouver ses valeurs et comment les lire.

const positionLocation = gl.getAttribLocation(program, "a_position");

gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.enableVertexAttribArray(positionLocation);

gl.vertexAttribPointer(
  positionLocation,
  2,
  gl.FLOAT,
  false,
  0,
  0
);

Le nombre 2 signifie que chaque sommet contient deux composantes. gl.FLOAT correspond au type utilisé dans le Float32Array.

Cette configuration montre une caractéristique importante de WebGL : une grande partie de l’API fonctionne comme une machine à états. Une opération telle que bufferData ou vertexAttribPointer agit sur les ressources actuellement liées.

Effacer et dessiner le canvas

Avant de dessiner, il faut définir la zone de rendu, choisir une couleur de fond et activer le programme.

gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
gl.clearColor(0.06, 0.08, 0.12, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);

gl.useProgram(program);
gl.drawArrays(gl.TRIANGLES, 0, 3);

gl.drawArrays lance réellement le travail du GPU. Le mode gl.TRIANGLES demande d’interpréter chaque groupe de trois sommets comme un triangle. Le dernier argument indique que trois sommets doivent être traités.

Si le programme, le buffer et l’attribut sont correctement configurés, un triangle orange apparaît sur le fond sombre.

Pourquoi WebGL semble-t-il si verbeux ?

Afficher un seul triangle demande beaucoup de préparation. Cette complexité vient du fait que WebGL expose des mécanismes génériques capables de rendre des milliers d’objets avec des matériaux, des textures et des transformations différentes.

Dans un projet réel, il est conseillé de séparer progressivement les responsabilités :

  • compilation et validation des shaders ;
  • création des programmes ;
  • gestion des buffers ;
  • définition des géométries ;
  • boucle de rendu ;
  • gestion de la caméra et des transformations.

Les bibliothèques comme Three.js construisent justement des abstractions au-dessus de ces opérations. Elles proposent des concepts plus directs comme Scene, Camera, Mesh, Geometry et Material. Comprendre WebGL permet ensuite de mieux diagnostiquer les problèmes de shaders, de performances ou de mémoire rencontrés avec ces outils.

WebGL reste également distinct de WebGPU. WebGPU expose une API plus moderne et plus explicite, adaptée au rendu comme au calcul parallèle. Vous pouvez en découvrir une première application dans WebGPU en TypeScript.

Les notions à retenir

WebGL est une API de rendu bas niveau accessible depuis JavaScript. JavaScript configure les ressources et les états, tandis que le GPU exécute les shaders et produit les pixels.

Un premier rendu repose sur quelques éléments essentiels :

  • un canvas et un contexte WebGL ;
  • un vertex shader ;
  • un fragment shader ;
  • un programme reliant les shaders ;
  • un buffer contenant les sommets ;
  • une configuration d’attribut ;
  • un appel de dessin.

Les prochaines étapes consisteront à ajouter des couleurs par sommet, des transformations avec des matrices, une animation avec requestAnimationFrame, puis une véritable caméra 3D. Ces bases permettront ensuite d’aborder Three.js sans considérer la bibliothèque comme une boîte noire.

Pour approfondir les détails de l’API, la documentation WebGL de MDN et le WebGL Reference Card du Khronos Group constituent deux références utiles.