Rendu vectoriel avec WebGL

Comment utiliser la puissance de WebGL pour effectuer le rendu de figures vectorielles, c'est-à-dire d'objets bidimensionnels, dans une page Web, sachant que WebGL est une API pour le rendu d'objets tridimensionnels ? Par exemple, le rendu d'un triangle dont les couleurs assignées aux sommets sont interpolées sur toute la surface :
Rendu d'un triangle 2D avec interpolation de couleurs

La solution

Comme toute API moderne de rendu d'objets tridimensionnels, WebGL permet d'utiliser des vertex shaders (VS) et des fragment shaders (FS), de petits programmes qui sont exécutés pour chaque point à transformer et chaque pixel à rendre, respectivement. Il faut donc programmer un VS qui prend en entrée un point dont les coordonnées sont tout simplement restituées en sortie.

Le code JavaScript

C'est l'occasion de présenter le code JavaScript minimum requis pour programmer un rendu dans une page Web à l'aide de WebGL. Tout d'abord, il faut prévoir une surface de rendu dans la page, sous la forme d'un élément <canvas> :
<canvas id="tagCanvas" width="800px" height="600px"></canvas>
Le code parle ensuite de lui-même :
// Récupérer un pointeur sur la surface de rendu pour WebGL

var canvas = document.getElementById ("tagCanvas");
var gl = canvas.getContext ("webgl"); // Utiliser "experimental-webgl" avec Internet Explorer

// Effacer le contenu de la surface de rendu

gl.clearColor (0.0, 0.0, 0.0, 1.0);
gl.clear (gl.COLOR_BUFFER_BIT);

// Programmer un vertex shader

var shaderV = gl.createShader (gl.VERTEX_SHADER);
gl.shaderSource (shaderV, "attribute vec2 A_xy; attribute vec3 A_c; varying lowp vec4 V_c; void main (void) { gl_Position = vec4 (A_xy.x, A_xy.y, 0.0, 1.0); V_c = vec4 (A_c, 1.0); }");
gl.compileShader (shaderV);
if (!gl.getShaderParameter (shaderV, gl.COMPILE_STATUS))
	alert (gl.getShaderInfoLog (shaderV));

// Programmer un fragment shader	
	
var shaderF = gl.createShader (gl.FRAGMENT_SHADER);
gl.shaderSource (shaderF, "varying lowp vec4 V_c; void main (void) { gl_FragColor = V_c; }");
gl.compileShader (shaderF);
if (!gl.getShaderParameter (shaderF, gl.COMPILE_STATUS))
	alert (gl.getShaderInfoLog (shaderF));

// Combiner les shaders dans un programme
	
var program = gl.createProgram ();
gl.attachShader (program, shaderV);
gl.attachShader (program, shaderF);
gl.linkProgram (program);
gl.useProgram (program);

// Décrire la surface 2D sous forme de points colorés

var vertices = [ // (x, y, r, g, b)
	-1.0, -1.0, // (x, y)
	0.0, 1.0, 1.0, // (r, g, b)
	0.0, 1.0,
	1.0, 1.0, 0.0,
	1.0, -1.0,
	1.0, 0.0, 1.0
];

// Alimenter le vertex shader avec des coordonnées des points

var buffer = gl.createBuffer ();
gl.bindBuffer (gl.ARRAY_BUFFER, buffer);
gl.bufferData (gl.ARRAY_BUFFER, new Float32Array (vertices), gl.STATIC_DRAW);
var A_xy = gl.getAttribLocation (program, "A_xy");
gl.enableVertexAttribArray (A_xy);
gl.vertexAttribPointer (A_xy, 2, gl.FLOAT, false, 5 * Float32Array.BYTES_PER_ELEMENT, 0); // offset : 0 bytes / stride : 20 bytes (x, y, z, r, g, buffer) / read : 2 FLOAT (x, y)
var A_c = gl.getAttribLocation (program, "A_c");

// Alimenter le fragment shader avec les couleurs des points

gl.enableVertexAttribArray (A_c);
gl.vertexAttribPointer (A_c, 3, gl.FLOAT, false, 5 * Float32Array.BYTES_PER_ELEMENT, 2 * Float32Array.BYTES_PER_ELEMENT); // offset : 8 bytes (x, y) / stride : 20 bytes (x, y, r, g, buffer) / size : 3 FLOAT (r, g, buffer)

// Commander le rendu

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

// Nettoyer

gl.useProgram (null);
gl.detachShader (program, shaderV);
gl.deleteShader (shaderV);
gl.detachShader (program, shaderF);
gl.deleteShader (shaderF);
gl.deleteProgram (program);
gl.disableVertexAttribArray (A_xy);
gl.disableVertexAttribArray (A_c);
gl.bindBuffer (gl.ARRAY_BUFFER, null);
gl.deleteBuffer (buffer);

L'exemple

Cliquez ici pour accéder à une page de test minimaliste. Vous pourrez visualiser le code et le récupérer pour travailler avec (si vous utilisez Internet Explorer, substituez "experimental-webgl" à "webgl" dans l'instruction permettant de récupérer le contexte).

La logique

La mise en place de WebGL ne présente pas d'intérêt (c'est un problème technique, et la technique n'a par définition pas d'intérêt), aussi ne sera-t-il question que du code des shaders.
Le code du VS se contente de relayer en sortie les coordonnées du point fournies en entrée :
attribute vec2 A_xy;
attribute vec3 A_c;
varying lowp vec4 V_c;

void main (void) {
	gl_Position = vec4 (A_xy.x, A_xy.y, 0.0, 1.0);
	V_c = vec4 (A_c, 1.0);
}
Un shader est un programme écrit dans dans un langage particulier, en l'occurence GLSL 1.0 pour WebGL, (une implémentation d'Open GL ES 2.0). On trouve ici une excellente fiche de référence pour s'y retrouver.
Le VS est appelé pour traiter chacun des points d'une primitive en cours de rendu, soit ici un triangle. Il prend en entrée les coordonnées d'un point A_xy et sa couleur A_c qui sont extraits de ARRAY_BUFFER. Il doit alimenter une variable système gl_Position avec les coordonnées de la projection du point sur une surface de dimensions 2.0 x 2.0 dotée d'un repère d'axe X de gauche à droite [-1.0, 1.0] et d'axe Y de bas en haut [-1.0, 1.0]. Ces coordonnées sont utilisée par WebGL pour déterminer les pixels qui forment le triangle dans la surface de rendu. Ici, le VS se contente de relayer les coordonnées d'un sommet du triangle.
Le VS alimente aussi une variable qui correspond à la couleur du sommet. Cette variable est une varying, ce qui signifie que sa valeur sera calculée pour chacun des pixels formant le triangle dans la surface de rendu en interpolant les valeurs fournies pour chacun des sommets de ce dernier.
Une fois que le VS a traité les trois sommets du triangle, WebGL détermine donc les pixels qui le représentent dans la surface de rendu et appelle le FS pour chacun d'entre eux : c'est la rasterization.
varying lowp vec4 V_c;

void main (void) {
	gl_FragColor = V_c;
}
Le FS prend en entrée la couleur du pixel V_c. Tout comme le VS, le FS doit alimenter une variable système, gl_FragColor. Cette variable contient la couleur du pixel à tracer dans la surface de rendu, au format RGBA (chaque composante étant un flottant compris entre 0.0 et 1.0). Ici, le FS se contente de relayer la couleur qui résulte de l'interpolation des couleurs des sommets du triangle sur toute la surface de ce dernier.
Rendu vectoriel avec WebGL