Shader d’explosion de pixels avec WebGL (2/2)

Cet article est le second (et donc dernier) d'une série portant sur la production d'une animation de "pixels" s'éloignant d'un point d'origine en tournoyant (autour de leurs centres et autour du centre de l'écran), en grossissant et devenant toujours plus transparents jusqu'à disparaître de l'écran.
L'explosion de "pixels"
Dans le premier article, nous avons vu comment créer le vertex shader (VS) et fragment shader (FS) et les alimenter en données pour que tous les "pixels" soient transformés et rendus en appelant une seule fois drawElements () par étape de l'animation.
Reste à voir comment produire l'animation. A étape régulière, il faut :
  • mettre à jour l'angle de rotation β, le facteur de zoom Z et les composantes du vecteur de translation de chaque "pixels" ;
  • alimenter les shaders avec les données des points auxquelles ces données relatives à la transformation sont annexées ;
  • commander la transformation et le rendu des "pixels".

La solution

β, Z, dX et dY constituent l'état d'un "pixel". Il convient donc de créer un tableau contenant autant d'états qu'il y a de "pixels" et de modifier l'état de chaque pixel en incrémentant ces valeurs à chaque étape pour produire l'animation. Pour que chaque "pixel" semble avoir sa propre cinétique, la meilleure solution est de déterminer les incréments en les tirant au hasard dans des intervalles raisonnables, au sens où leurs bornes doivent être fixées empiriquement. L'état d'un "pixel" comprend donc ses valeurs de β, Z, dX et dY mais aussi leurs incréments spécfiques au "pixel".
Une fois calculées pour l'étape courante, les valeurs de β, Z, dX et dY doivent être mises à jour dans le buffer associé à ARRAY_BUFFER afin qu'elles puissent être transmises au FS via l'attribut A_bzdxdy, comme précisé dans le précédent article. C'est le point faible de la stratégie consistant à limiter les appels à drawElements () : le volume des données par point est accru - en plus de ses coordonnées et de sa couleur, il faut y joindre l'état de son "pixel" -, et ces données doivent être transférées en mémoire à chaque étape.
Comme les données des points (X, Y, R, G, B) transmises via A_xy et A_rgb sont fixées une fois pour toutes et que seuls les états des "pixels" (β, Z, dX, dY) transmis via A_bzdxdy sont modifiés à chaque étape, il est possible de limiter l'inconvénient en se contentant de mettre à jour ces états dans le buffer. WebGL dispose d'une fonction pour cela : bufferSubData (). Il s'agit donc d'appeler bufferData () une première fois pour associer à ARRAY_BUFFER un buffer de la taille requise (données des points et des états de leurs "pixels" respectifs) puis d'appeler bufferSubDate () pour mettre à jour le buffer partiellement.
Pour produire l'animation, la solution qui vient spontanément à l'esprit est de recourir à une des fonctions de l'objet window : setTimeout () qui serait rappelée à chaque étape, ou setInterval () qui serait appelée une fois pour toutes. Toutefois, ces fonctions sont bien trop imprécises pour assurer un rendu fluide - "dans la trame" pour reprendre un vocabulaire "old school" évoquant le délai entre deux balayages d'un même point par le faisceau d'électron d'un tube cathodique à 60Hz. Fort heureusement, l'objet window dispose d'une autre fonction bien plus précise : requestAnimationFrame ().

Le code JavaScript

Comme précisé dans le premier article, le programme est structuré en trois parties dont il reste deux parties à explorer :
  • l'initialisation de l'explosion assurée par explode () ;
  • le rendu d'une étape de l'explosion assurée par nextStep ().
Concernant explode (), le code est le suivant :
var pixelWidth, pixelHeight, nbPixels, nbSteps, tetaSpeed, betaSpeedMin, betaSpeedMax, zoomSpeedMin, zoomSpeedMax, xSpeedMin, xSpeedMax, ySpeedMin, ySpeedMax;
var vertices, indices, params;
var alpha, alphaSpeed;
var bufferV, bufferI;
var step;
var teta;

function explode () {
	var pIndices = [
		0, 3, 1,
		1, 3, 2
	];
	var r, g, b, offsetI;
	var i, j;

	// Lire les paramètres

	readValues ();
	var pVertices = [
		-pixelWidth / 2.0, pixelHeight / 2.0,
		pixelWidth / 2.0, pixelHeight / 2.0,
		pixelWidth / 2.0, -pixelHeight / 2.0,
		-pixelWidth / 2.0, -pixelHeight / 2.0,
	];

	// Créer les "pixels"

	vertices = new Array ();
	indices = new Array ();
	offsetI = 0;
	for (i = 0; i != nbPixels; i ++) {
		r = Math.random ();
		g = Math.random ();
		b = Math.random ();
		j = 0;
		do {
			vertices.push (pVertices[j ++]);
			vertices.push (pVertices[j ++]);
			vertices.push (r);
			vertices.push (g);
			vertices.push (b);
		} while (j != pVertices.length);
		for (j = 0; j != pIndices.length; j ++)
			indices.push (pIndices[j] + offsetI);
		offsetI += 4;
	}

	// Générer les états des "pixels"

	alpha = 1.0;
	alphaSpeed = -1.0 / nbSteps;
	params = new Array ();
	for (i = 0; i != nbPixels; i ++) {
		
		// Rajouter les paramètres des transformations des points du "pixel"

		params.push ({
			betaSpeed : (betaSpeedMin + Math.random (betaSpeedMax - betaSpeedMin)) * Math.PI / 180.0,
			zoomSpeed : zoomSpeedMin + Math.random () * (zoomSpeedMax - zoomSpeedMin),
			xSpeed : xSpeedMin + Math.random () * (xSpeedMax - xSpeedMin),
			ySpeed : ySpeedMin + Math.random () * (ySpeedMax - ySpeedMin)
		});

		// Rajouter les données des transformations des points du "pixel"

		for (j = 0; j != 4; j ++) {
			vertices.push (0.0); // Angle de rotation
			vertices.push (1.0); // Facteur de zoom
			vertices.push (0.0); // Translation horizontale
			vertices.push (0.0); // Translation verticale
		}
	}

	// Créer le buffer pour les données des points et y stocker ces données

	bufferV = gl.createBuffer ();
	gl.bindBuffer (gl.ARRAY_BUFFER, bufferV);
	gl.bufferData (gl.ARRAY_BUFFER, new Float32Array (vertices), gl.DYNAMIC_DRAW);

	// Créer le buffer pour les indices des points et y stocker ces indices

	bufferI = gl.createBuffer ();
	gl.bindBuffer (gl.ELEMENT_ARRAY_BUFFER, bufferI);
	gl.bufferData (gl.ELEMENT_ARRAY_BUFFER, new Uint16Array (indices), gl.STATIC_DRAW);

	// Alimenter les attributs avec les données des points

	gl.enableVertexAttribArray (A_xy);
	gl.vertexAttribPointer (A_xy, 2, gl.FLOAT, false, 5 * Float32Array.BYTES_PER_ELEMENT, 0);
	gl.enableVertexAttribArray (A_rgb);
	gl.vertexAttribPointer (A_rgb, 3, gl.FLOAT, false, 5 * Float32Array.BYTES_PER_ELEMENT, 2 * Float32Array.BYTES_PER_ELEMENT);
	gl.enableVertexAttribArray (A_bzdxdy);
	gl.vertexAttribPointer (A_bzdxdy, 4, gl.FLOAT, false, 4 * Float32Array.BYTES_PER_ELEMENT, nbPixels * 4 * 5 * Float32Array.BYTES_PER_ELEMENT);

	// Lancer l'animation

	document.getElementById ("tagExplode").disabled = true;
	step = 0;
	alpha = 1.0;
	teta = 0.0;
	window.requestAnimationFrame (function () { nextStep (); });
}
Le code de readValues () n'est pas reproduit ici. Il ne fait que lire des valeurs dans des champs de saisie HTML permettant à l'utilisateur de paramétrer l'animation (cf. l'exemple plus bas).
Concernant nextStep (), le code est le suivant :
function nextStep () {
	var i, j, k;
	var factor = 1.0 / nbSteps; // Fonction quelconque de nbSteps, ajustée empiriquement
	var mWorld;

	// Stocker l'état à jour du "pixel" de chaque point

	gl.bufferSubData (gl.ARRAY_BUFFER, nbPixels * 4 * 5 * Float32Array.BYTES_PER_ELEMENT, new Float32Array (vertices.slice (nbPixels * 4 * 5)));

	// Mettre à jour la matrice de transformation et l'alpha

	mWorld = getWorldMatrix (teta, 1.0, CANVAS_WIDTH, CANVAS_HEIGHT);
	gl.uniformMatrix4fv (U_mW, false, new Float32Array (mWorld));
	gl.uniform1f (U_a, alpha);

	// Effacer la surface de rendu et afficher les "pixels"

	gl.clearColor (0.0, 0.0, 0.0, 1.0);
	gl.clear (gl.COLOR_BUFFER_BIT);
	gl.enable (gl.BLEND);
	gl.blendEquationSeparate (gl.FUNC_ADD, gl.FUNC_ADD);
	gl.blendFuncSeparate (gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ZERO, gl.ONE);
	gl.drawElements (gl.TRIANGLES, 2 * 3 * nbPixels, gl.UNSIGNED_SHORT, 0);
	gl.disable (gl.BLEND);

	// Terminer ou continuer l'animation

	if (step != nbSteps) {
		step ++;
		alpha += alphaSpeed;
		teta += tetaSpeed * Math.PI / 180.0;
		k = nbPixels * 4 * 5; // Sauter les données des points
		for (i = 0; i != nbPixels; i ++) {
			for (j = 0; j != 4; j ++) {
				vertices[k] += params[i].betaSpeed;
				if (vertices[k] > 2.0 * Math.PI)
					vertices[k] -= 2.0 * Math.PI;
				if (vertices[k] < 0.0)
					vertices[k] += 2.0 * Math.PI;
				k ++;
				vertices[k ++] += params[i].zoomSpeed * factor;
				vertices[k ++] += params[i].xSpeed * factor;
				vertices[k ++] += params[i].ySpeed * factor;
			}
		}
		window.requestAnimationFrame (function () { nextStep () });
	}
	else {
		gl.disableVertexAttribArray (A_xy);
		gl.disableVertexAttribArray (A_rgb);
		gl.disableVertexAttribArray (A_bzdxdy);
		gl.bindBuffer (gl.ARRAY_BUFFER, null);
		gl.deleteBuffer (bufferV);
		gl.bindBuffer (gl.ELEMENT_ARRAY_BUFFER, null);
		gl.deleteBuffer (bufferI);
		document.getElementById ("tagExplode").disabled = false;
	}
}

L'exemple

Cliquez ici pour accéder à une page de test minimaliste permettant de produire l'animation en contrôlant différents paramètres. Vous pourrez visualiser le code et le récupérer pour travailler avec.

La logique

L'initialisation de l'explosion
Faisant le joint entre l'initialisation générale et le rendu d'une étape, l'initialisation de l'explosion concentre les opérations qu'il est impossible d'effectuer dans la première et dont il vaut mieux faire l'économie dans le seconde de ces phases.
Choix est fait d'exposer une interface qui permet à l'utilisateur de modifier des paramètres de l'explosion pour déterminer de manière empirique les valeurs produisant l'effet recherché :
Interface pour paramétrer une explosion
Il s'agit donc de lire les valeurs de ces paramètres et de préparer une nouvelle animation en conséquence : créer les buffers de points et d'indices, modifier les valeurs des uniformes.
Un "pixel" est décrit sous la forme de deux TRIANGLES. Créer les "pixels", c'est simplement créer les TRIANGLES qui les composent en boucle en alimentant deux tableaux : vertices[] et indices[]. Le nombre de pixels est nbPixels, les dimensions d'un "pixel" sont pixelWidth et pixelHeight :
var pVertices = [
	-pixelWidth / 2.0, pixelHeight / 2.0,
	pixelWidth / 2.0, pixelHeight / 2.0,
	pixelWidth / 2.0, -pixelHeight / 2.0,
	-pixelWidth / 2.0, -pixelHeight / 2.0,
];
var pIndices = [
	0, 3, 1,
	1, 3, 2
];

// Créer les "pixels"

vertices = new Array ();
indices = new Array ();
offsetI = 0;
for (i = 0; i != nbPixels; i ++) {
	r = Math.random ();
	g = Math.random ();
	b = Math.random ();
	j = 0;
	do {
		vertices.push (pVertices[j ++]);
		vertices.push (pVertices[j ++]);
		vertices.push (r);
		vertices.push (g);
		vertices.push (b);
	} while (j != pVertices.length);
	for (j = 0; j != pIndices.length; j ++)
		indices.push (pIndices[j] + offsetI);
	offsetI += 4;
}
A chaque "pixel" ses paramètres, regroupés dans une structure comprenant sa vitesse de rotation betaSpeed, sa vitesse de zoom zoomSpeed, ses vitesses de translations horizontale et verticale xSpeed et ySpeed. Chaque "pixel" dispose aussi d'un état, qui doit être reproduit pour chaque point à la fin du buffer des points. Ici encore, paramètres et états sont créés en boucle :
alpha = 1.0;
alphaSpeed = -1.0 / nbSteps;
params = new Array ();
for (i = 0; i != nbPixels; i ++) {
	
	// Rajouter les paramètres des transformations des points du "pixel"

	params.push ({
		betaSpeed : (betaSpeedMin + Math.random (betaSpeedMax - betaSpeedMin)) * Math.PI / 180.0,
		zoomSpeed : zoomSpeedMin + Math.random () * (zoomSpeedMax - zoomSpeedMin),
		xSpeed : xSpeedMin + Math.random () * (xSpeedMax - xSpeedMin),
		ySpeed : ySpeedMin + Math.random () * (ySpeedMax - ySpeedMin)
	});

	// Rajouter les données des transformations des points du "pixel" (état du "pixel")

	for (j = 0; j != 4; j ++) {
		vertices.push (0.0); // Angle de rotation
		vertices.push (1.0); // Facteur de zoom
		vertices.push (0.0); // Translation horizontale
		vertices.push (0.0); // Translation verticale
	}
}
Ces formalités accomplies, les choses sérieuses commencent. Il s'agit de créer le buffer de points et le buffer d'indices.
Le buffer d'indices est le plus simple. En effet, il n'est pas nécessaire de modifier les indices durant l'animation. Le buffer peut donc être alimenté en données une fois et une seule en spécifiant à WebGL qu'il sera statique via l'argument STATIC_DRAW de bufferData () :
bufferI = gl.createBuffer ();
gl.bindBuffer (gl.ELEMENT_ARRAY_BUFFER, bufferI);
gl.bufferData (gl.ELEMENT_ARRAY_BUFFER, new Uint16Array (indices), gl.STATIC_DRAW);
Le buffer des points est différent. En effet, il doit être alimenté en données qu'il n'est pas nécessaire de modifier durant l'animation - les données des points X, Y, R, G et B - et en données qu'il est nécessaire de modifier à chaque étape de l'animation - l'état du "pixel" β, Z, dX et dY.
La solution la plus simple serait de mettre toutes ces données dans un même tableau vertices[] et d'appeler bufferData () à chaque étape pour le copier intégralement dans le buffer associé à ARRAY_BUFFER. Toutefois, une solution plus optimisée est possible. Elle consiste à utiliser bufferData () pour copier les données des points une fois pour toutes lors de l'initialisation, puis à utiliser bufferSubData () pour copier les états des "pixels" lors de chaque étape.
Pour mettre en oeuvre cette solution, il est indispensable que le buffer soit créé à sa taille définitive, c'est-à-dire incluant les données des points et les états des "pixels", car sa taille ne peut être ajustée par la suite. Cela explique pourquoi vertices[] a été alimenté avec les données et les états et non seulement les données. Par ailleurs, il est intéressant d'indiquer à WebGL que le buffer pourra être modifié au fil des utilisations. Pour cela, l'argument DYNAMIC_DRAW est utilisé :
bufferV = gl.createBuffer ();
gl.bindBuffer (gl.ARRAY_BUFFER, bufferV);
gl.bufferData (gl.ARRAY_BUFFER, new Float32Array (vertices), gl.DYNAMIC_DRAW);
Le buffer créé et associé à ARRAY_BUFFER, il faut associer les attributs du VS à ARRAY_BUFFER. C'est l'occasion de préciser trois grandeurs à maîtriser lorsqu'il s'agit de demander à WebGL d'adresser des données. Par exemple, s'il s'agit d'utiliser vertexAttribPointer () pour que WebGL récupère dans ARRAY_BUFFER les trois nombres R, G et B correspondant à la couleur pour alimenter l'attribut A_rgb :
  • offset est le nombre d'octets à parcourir depuis le début pour tomber sur le premier octet de R du premier point ;
  • size est le nombre d'octets à lire pour récupérer R, G et B du premier point ;
  • stride est le nombre d'octets à parcourir après le premier octet de R du premier point pour atteindre le premier octet de R du second point (et ainsi de suite).
Offset, size et stride dans un buffer
In fine, cela donne :
gl.enableVertexAttribArray (A_xy);
gl.vertexAttribPointer (A_xy, 2, gl.FLOAT, false, 5 * Float32Array.BYTES_PER_ELEMENT, 0);
gl.enableVertexAttribArray (A_rgb);
gl.vertexAttribPointer (A_rgb, 3, gl.FLOAT, false, 5 * Float32Array.BYTES_PER_ELEMENT, 2 * Float32Array.BYTES_PER_ELEMENT);
gl.enableVertexAttribArray (A_bzdxdy);
gl.vertexAttribPointer (A_bzdxdy, 4, gl.FLOAT, false, 4 * Float32Array.BYTES_PER_ELEMENT, nbPixels * 4 * 5 * Float32Array.BYTES_PER_ELEMENT);
Après quelques initialisations de variables telles que le compteur des étapes, l'initialisation de l'explosion est terminée. Il est alors possible de commander le rendu de la première étape via la fonction idoine de l'objet window. Comme mentionné plus tôt, il faut écarter l'idée de recourir à setTimeout () ou setInterval () pour lui préférer requestAnimationFrame (), fonction nettement plus précise. Une astuce consiste à ne pas fournir la fonction nextStep () mais une fonction anonyme ad hoc pour optimiser le temps d'exécution, mais c'est peut-être un mythe... :
window.requestAnimationFrame (function () { nextStep (); });
Le rendu d'une étape de l'explosion
Après avoir contrôlé que l'étape courante n'est pas la dernière, la première tâche d'une étape d'une animation consiste à... commander la suivante : requestAnimationFrame () doit être appelée immédiatement. Toutefois, ce sera ici pour plus tard afin de simplifier le code.
Il faut ensuite mettre à jour le buffer associé à ARRAY_BUFFER. Comme convenu, la mise à jour est limitée aux états des "pixels". Pour cela, seule la partie de vertices[] qui les contient est utilisée lors d'un appel à bufferSubData (), cette partie étant isolée par un appel à slice (). Ce n'est certainement pas optimal, car slice () génère inutilement une copie des états dans un tableau temporaire. Toutefois, bufferSubData () ne permet pas de spécifier l'indice à partir duquel copier les données du tableau qui lui est transmis, du moins quand ce tableau est un Array. Pour optimiser, il faudrait stocker les états dans autre tableau que vertices[], sans toutefois oublier que vertices[] donne la taille du buffer (pas question donc de le faire maigrir pour autant).
gl.bufferSubData (gl.ARRAY_BUFFER, nbPixels * 4 * 5 * Float32Array.BYTES_PER_ELEMENT, new Float32Array (vertices.slice (nbPixels * 4 * 5)));
Une autre mise à jour s'impose, celles des uniformes. Il s'agit de U_mWorld, la matrice de transformation générale qui permet d'ajuster l'aspect ratio et d'appliquer une rotation aux "pixels" autour du centre de l'écran, U_a :
mWorld = getWorldMatrix (teta, 1.0, CANVAS_WIDTH, CANVAS_HEIGHT);
gl.uniformMatrix4fv (U_mW, false, new Float32Array (mWorld));
gl.uniform1f (U_a, alpha);
Les données désormais à jour, la transformation et le rendu peuvent être commandés. La surface de rendu est vidée, le blending est activé et les TRIANGLES sont transformés et rendus :
gl.clearColor (0.0, 0.0, 0.0, 1.0);
gl.clear (gl.COLOR_BUFFER_BIT);
gl.enable (gl.BLEND);
gl.blendEquationSeparate (gl.FUNC_ADD, gl.FUNC_ADD);
gl.blendFuncSeparate (gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ZERO, gl.ONE);
gl.drawElements (gl.TRIANGLES, 2 * 3 * nbPixels, gl.UNSIGNED_SHORT, 0);
gl.disable (gl.BLEND);
Désactivé par défaut dans WebGL, le blending s'active par appel à gl.enable (gl.BLEND). L'effet consiste à mélanger la couleur d'un pixel à afficher à celle du pixel déjà affiché en pondérant l'importance de chacune par son degré de transparence quantifié par la fameuse quatrième composante de la couleur, l'alpha. WebGL permet de nombreuses possibilités en la matière. Ici, on opte pour une interpolation linéaire entre la couleur de la source (le pixel du TRIANGLES à afficher) et la couleur de la destination (le pixel de la surface de rendu) :
  • Rd = Rs * As + Rd * (1 - As)
  • Gd = Gs * As + Gd * (1 - As)
  • Bd = Bs * As + Bd * (1 - As)
  • Ad = As
Ce résultat est atteint en utilisant deux fonctions : blendEquationSeparate () et blendFuncSeparate (). Sommairement :
  • blendEquationSeparate () permet de préciser l'équation de combinaison entre source et destination pour les composantes R, G et B d'une part, et pour la composante A d'autre part. En l'espèce, FUNC_ADD indique que la composante X, dont la valeur est Xs dans la source et Xd dans la destination, doit prendre la valeur à Xs * Sx + Xd * Dx, où Sx est la fonction source S () appliquée à X et Dx est la fonction destination D () appliquée à X.
  • blendFuncSeparate () permet de préciser les fonctions S () et D (), pour les composantes R, G et B d'une part, et pour la composante A d'autre part. En l'espèce :
    • SRC_ALPHA et ONE_MINUS_SRC_ALPHA indiquent que la composante R, G ou B doit prendre la valeur Xs * As + Xd * (1 - As) où As est la composante alpha de la source ;
    • ZERO et ONE indiquent que la composante A doit prendre la valeur As * 0 + Ad * 1 où As est la composante alpha de la source et Ad est la composante alpha de destination (autrement dit, Ad est préservée).
C'est enfin que l'animation proprement dite se déroule, pour autant que l'étape courante n'est pas la dernière. L'animation consiste à incrémenter l'angle de la rotation autour du centre de l'écran, décrémenter le degré de transparence (l'animation prend fin quand il atteint 0.0) et modifier l'état du "pixel" de chaque point par incrémentation de l'angle de rotation, du facteur de zoom, des composantes du vecteur de translation :
alpha += alphaSpeed;
teta += tetaSpeed * Math.PI / 180.0;
k = nbPixels * 4 * 5; // Sauter les données des points
for (i = 0; i != nbPixels; i ++) {
	for (j = 0; j != 4; j ++) {
		vertices[k] += params[i].betaSpeed;
		if (vertices[k] > 2.0 * Math.PI)
			vertices[k] -= 2.0 * Math.PI;
		if (vertices[k] < 0.0)
			vertices[k] += 2.0 * Math.PI;
		k ++;
		vertices[k ++] += params[i].zoomSpeed * factor;
		vertices[k ++] += params[i].xSpeed * factor;
		vertices[k ++] += params[i].ySpeed * factor;
	}
}
Il reste à appeler de nouveau requestAnimationFrame (), mais cet appel devrait en toute rigueur se dérouler au tout début de nextStep (), comme expliqué plus tôt :
window.requestAnimationFrame (function () { nextStep () });
La fin de l'explosion
Lorsque l'étape courante est la dernière étape, c'est le moment de faire place nette en supprimant les buffers après les avoir dissociés des attributs. En effet, l'utilisateur peut modifier le nombre de "pixels" via un champ de saisie HTML, mais les buffers ne peuvent pas être redimensionnés, si bien qu'ils doivent être recréés.
gl.disableVertexAttribArray (A_xy);
gl.disableVertexAttribArray (A_rgb);
gl.disableVertexAttribArray (A_bzdxdy);
gl.bindBuffer (gl.ARRAY_BUFFER, null);
gl.deleteBuffer (bufferV);
gl.bindBuffer (gl.ELEMENT_ARRAY_BUFFER, null);
gl.deleteBuffer (bufferI);
A la toute fin du programme, il ne reste ainsi plus qu'à éliminer les shaders et le programme :
function clean () {
	gl.useProgram (null);
	gl.detachShader (program, shaderV);
	gl.deleteShader (shaderV);
	gl.detachShader (program, shaderF);
	gl.deleteShader (shaderF);
	gl.deleteProgram (program);
}
Pour aller plus loin...
L'explosion de "pixels" qui a été longuement décrite dans cette série d'articles n'adopte probablement pas la forme la plus optimisée qui soit, mais elle est quand même relativement optimisée. Pour plus d'informations sur l'optimisation de l'usage des buffers, il convient de se reporter à la spécification d'OpenGL ainsi qu'à un excellent article d'Apple.
Shader d’explosion de pixels avec WebGL (2/2)