Dans un
article précédent, il a été question d'un moyen pour faciliter la conception de shaders lors de l'écriture d'applications WebGL en JavaScript.
A l'occasion de la reprise d'un projet fondé sur ces technologies, il est apparu possible de simplifier plus encore non seulement la conception, mais aussi l'utilisation, et ce non seulement des shaders, mais aussi des programmes de WebGL.
Comme les helpers, la solution proposée ici s'appuie sur
WebGL 2, en mettant notamment à profit ce nouvel objet : le
Vertex Array Object, ou VAO pour les intimes.
D'ailleurs, ce sera l'occasion de préciser la manière dont cet objet fonctionne exactement, tant il est vrai que les explications fournies par Khronos peuvent être un peu difficiles à suivre...
Une solution simple à utiliser
Les concepts de base de WebGL ont été assez détaillés
ici et
là sur ce blog pour qu'il ne soit pas utile d'y revenir. On présumera donc que le lecteur les maîtrise, notamment qu'il sait créer un programme à partir d'un vertex shader et d'un fragment shader écrits en GLSL, et utiliser ce programme pour afficher au moins une primitive.
A ce titre, le lecteur aura fait l'expérience de la pénibilité de la conception des shaders. C'est pour faciliter cette tâche que des helpers avaient été proposés
ici sur ce blog. Quoique fort utiles, ces helpers présentaient l'inconvénient d'être à cheval entre deux concepts, celui d'une aide durant l'écriture du code et celui d'une aide durant l'exécution du code en question. En tâchant de clarifier ce positionnement, il est apparu qu'il serait plus globalement utile d'intégrer une aide portant sur le programme, au sens de celui créé par un appel à
gl.createProgram ()
gl.createProgram ().
La solution s'appuie sur plusieurs objets réunis au sein d'une bibliothèque,
libWEBGL.js, dont le format a été présenté
ici. Il y est fait appel très simplement par une simple inclusion de script :
<script type="text/javascript" src="libWEBGL.js"></script>
- <script type="text/javascript" src="libWEBGL.js"></script>
<script type="text/javascript" src="libWEBGL.js"></script>
Cliquez ici pour accéder à une page de test minimaliste, et
ici pour télécharger une archive contenant cette page et la bibliothèque libWEBGL sur laquelle le code JavaScript de la page s'appuie.
Comment cela fonctionne ? Après avoir récupéré le context WebGL d'un canvas, par la suite désigné par gl
gl, il faut procéder en trois étapes.
La première étape consiste à créer un vertex shader. Pour cela, il convient d'appeler la fabrique libWEBGL.createVertexShader ()
libWEBGL.createVertexShader () pour récupérer un objet VertexShader
VertexShader. Ensuite, il faut appeler ses méthodes .addInput ()
.addInput (), .addUniform ()
.addUniform () et .addOutput ()
.addOutput () pour fournir la liste des éléments correspondants du programme GLSL du shader. Un dernier appel à .build ()
.build () pour fournir le code source de programme, et la création du vertex shader est terminée. Plutôt simple et flexible, non ?
vertexShader = libWEBGL.createVertexShader (gl);
vertexShader.setDebugLevel (libWEBGL.DEBUG_MESSAGE | libWEBGL.DEBUG_TRACE);
vertexShader.addInput ("A_xy", libWEBGL.PRECISION_LOWP, libWEBGL.TYPE_VEC2);
vertexShader.addInput ("A_mL", libWEBGL.PRECISION_LOWP, libWEBGL.TYPE_MAT3);
vertexShader.addInput ("A_rgb", libWEBGL.PRECISION_LOWP, libWEBGL.TYPE_VEC3);
vertexShader.addUniform ("U_mP", libWEBGL.PRECISION_LOWP, libWEBGL.TYPE_MAT3);
vertexShader.addOutput ("V_rgba", libWEBGL.PRECISION_LOWP, libWEBGL.TYPE_VEC4);
vertexShader.build ("void main (void) { gl_Position = vec4 (U_mP * A_mL * vec3 (A_xy, 1.0), 1.0); V_rgba = vec4 (A_rgb, 1.0); }");
- vertexShader = libWEBGL.createVertexShader (gl);
- vertexShader.setDebugLevel (libWEBGL.DEBUG_MESSAGE | libWEBGL.DEBUG_TRACE);
- vertexShader.addInput ("A_xy", libWEBGL.PRECISION_LOWP, libWEBGL.TYPE_VEC2);
- vertexShader.addInput ("A_mL", libWEBGL.PRECISION_LOWP, libWEBGL.TYPE_MAT3);
- vertexShader.addInput ("A_rgb", libWEBGL.PRECISION_LOWP, libWEBGL.TYPE_VEC3);
- vertexShader.addUniform ("U_mP", libWEBGL.PRECISION_LOWP, libWEBGL.TYPE_MAT3);
- vertexShader.addOutput ("V_rgba", libWEBGL.PRECISION_LOWP, libWEBGL.TYPE_VEC4);
- vertexShader.build ("void main (void) { gl_Position = vec4 (U_mP * A_mL * vec3 (A_xy, 1.0), 1.0); V_rgba = vec4 (A_rgb, 1.0); }");
vertexShader = libWEBGL.createVertexShader (gl);
vertexShader.setDebugLevel (libWEBGL.DEBUG_MESSAGE | libWEBGL.DEBUG_TRACE);
vertexShader.addInput ("A_xy", libWEBGL.PRECISION_LOWP, libWEBGL.TYPE_VEC2);
vertexShader.addInput ("A_mL", libWEBGL.PRECISION_LOWP, libWEBGL.TYPE_MAT3);
vertexShader.addInput ("A_rgb", libWEBGL.PRECISION_LOWP, libWEBGL.TYPE_VEC3);
vertexShader.addUniform ("U_mP", libWEBGL.PRECISION_LOWP, libWEBGL.TYPE_MAT3);
vertexShader.addOutput ("V_rgba", libWEBGL.PRECISION_LOWP, libWEBGL.TYPE_VEC4);
vertexShader.build ("void main (void) { gl_Position = vec4 (U_mP * A_mL * vec3 (A_xy, 1.0), 1.0); V_rgba = vec4 (A_rgb, 1.0); }");
Attention à l'ordre des appels à la méthode décrivant le format de point. En effet, ces appels à .addInput ()
.addInput () doivent être effectués dans l'ordre dans lequel les éléments correspondants du format de point seront fournis dans le tableau des points qui sera chargé dans le programme. En l'espèce, chaque point se présente sous la forme d'une séquence de 14 éléments qui sont donc, dans l'ordre, les coordonnées du point, la matrice de transformation du point, et les composantes de la couleur du point.
Au passage, noter l'appel à .setDebugLevel ()
.setDebugLevel () qui permet de spécifier le niveau de détail des informations affichées dans la console, utiles pour le débogage. Par défaut, le niveau de détail est libWEBGL.DEBUG_MESSAGE
libWEBGL.DEBUG_MESSAGE, ce qui affiche un message sur chaque erreur rencontrée. Ce niveau peut être combiné à libWEBGL.DEBUG_TRACE
libWEBGL.DEBUG_TRACE pour afficher de plus le contenu de la pile. Il est possible de spécifier le niveau de détail dès la création de l'objet VertexShader
VertexShader à l'aide du paramètre nommé debugLevel
debugLevel. Tout ceci vaut aussi pour les deux autres autres objets, FragmentShader
FragmentShader et Program
Program.
La seconde étape consiste à créer un fragment shader. Elle suit exactement le même modèle que l'étape précédente, puisqu'il s'agit d'appeler une fabrique libWEBGL.createFragmentShader ()
libWEBGL.createFragmentShader () qui retourne un objet FragmentShader
FragmentShader à configurer pareillement :
fragmentShader = libWEBGL.createFragmentShader (gl);
fragmentShader.setDebugLevel (libWEBGL.DEBUG_MESSAGE | libWEBGL.DEBUG_TRACE);
fragmentShader.addInput ("V_rgba", libWEBGL.PRECISION_LOWP, libWEBGL.TYPE_VEC4);
fragmentShader.addOutput ("rgba", libWEBGL.PRECISION_LOWP, libWEBGL.TYPE_VEC4);
fragmentShader.build ("void main (void) { rgba = V_rgba; }");
- fragmentShader = libWEBGL.createFragmentShader (gl);
- fragmentShader.setDebugLevel (libWEBGL.DEBUG_MESSAGE | libWEBGL.DEBUG_TRACE);
- fragmentShader.addInput ("V_rgba", libWEBGL.PRECISION_LOWP, libWEBGL.TYPE_VEC4);
- fragmentShader.addOutput ("rgba", libWEBGL.PRECISION_LOWP, libWEBGL.TYPE_VEC4);
- fragmentShader.build ("void main (void) { rgba = V_rgba; }");
fragmentShader = libWEBGL.createFragmentShader (gl);
fragmentShader.setDebugLevel (libWEBGL.DEBUG_MESSAGE | libWEBGL.DEBUG_TRACE);
fragmentShader.addInput ("V_rgba", libWEBGL.PRECISION_LOWP, libWEBGL.TYPE_VEC4);
fragmentShader.addOutput ("rgba", libWEBGL.PRECISION_LOWP, libWEBGL.TYPE_VEC4);
fragmentShader.build ("void main (void) { rgba = V_rgba; }");
La troisième et dernière étape consiste à créer le programme. Un appel à la fabrique libWEBGL.createProgram ()
libWEBGL.createProgram () retourne un objet Program
Program. Ce dernier doit être lié aux shaders, ce qui s'effectue en appelant sa méthode .build ()
.build () :
program = libWEBGL.createProgram (gl);
program.setDebugLevel (libWEBGL.DEBUG_MESSAGE | libWEBGL.DEBUG_TRACE);
program.build (vertexShader, fragmentShader);
- program = libWEBGL.createProgram (gl);
- program.setDebugLevel (libWEBGL.DEBUG_MESSAGE | libWEBGL.DEBUG_TRACE);
- program.build (vertexShader, fragmentShader);
program = libWEBGL.createProgram (gl);
program.setDebugLevel (libWEBGL.DEBUG_MESSAGE | libWEBGL.DEBUG_TRACE);
program.build (vertexShader, fragmentShader);
Dès lors, il est possible d'utiliser le programme à souhait, ce qui s'effectue selon cette séquence d'appels à ses méthodes :
.use ()
.use () ou .use (true)
.use (true) pour activer le programme ;
.setUniform ()
.setUniform () pour affecter une valeur à une uniforme utilisée par un shader ;
.load ()
.load () pour charger les points et les indices ;
.use (false)
.use (false) pour désactiver le programme après le rendu.
Dans le test, cela donne :
// Activer le programme (il est possible de ne le faire qu'une fois dans Demo () si c'est le seul programme utilisé)
program.use ();
// Spécifier la matrice de projection
matrixP = libWEBGL.createMatrix2D ();
if (wglContext.width > wglContext.height)
matrixP.setScaling (wglContext.height / wglContext.width, 1.0);
else
matrixP.setScaling (1.0, wglContext.width / wglContext.height);
program.setUniform ("U_mP", matrixP.m);
// Récupérer les points et indices du quad
vertices = new Array ();
indices = new Array ();
quad.load (vertices, indices);
nbVerticesPerPrimitives = quad.nbVerticesPerPrimitives;
nbPrimitives = quad.nbPrimitives;
// Charger les points et les indices
program.load (vertices, indices);
// Demander le rendu
gl.clearColor (0.0, 0.0, 0.0, 1.0);
gl.clear (gl.COLOR_BUFFER_BIT);
gl.drawElements (gl.TRIANGLES, nbVerticesPerPrimitives * nbPrimitives, gl.UNSIGNED_SHORT, 0);
// Désactiver le programme (il est possible de s'en passer si c'est le seul programme utilisé)
program.use (false);
- // Activer le programme (il est possible de ne le faire qu'une fois dans Demo () si c'est le seul programme utilisé)
- program.use ();
- // Spécifier la matrice de projection
- matrixP = libWEBGL.createMatrix2D ();
- if (wglContext.width > wglContext.height)
- matrixP.setScaling (wglContext.height / wglContext.width, 1.0);
- else
- matrixP.setScaling (1.0, wglContext.width / wglContext.height);
- program.setUniform ("U_mP", matrixP.m);
- // Récupérer les points et indices du quad
- vertices = new Array ();
- indices = new Array ();
- quad.load (vertices, indices);
- nbVerticesPerPrimitives = quad.nbVerticesPerPrimitives;
- nbPrimitives = quad.nbPrimitives;
- // Charger les points et les indices
- program.load (vertices, indices);
- // Demander le rendu
- gl.clearColor (0.0, 0.0, 0.0, 1.0);
- gl.clear (gl.COLOR_BUFFER_BIT);
- gl.drawElements (gl.TRIANGLES, nbVerticesPerPrimitives * nbPrimitives, gl.UNSIGNED_SHORT, 0);
- // Désactiver le programme (il est possible de s'en passer si c'est le seul programme utilisé)
- program.use (false);
// Activer le programme (il est possible de ne le faire qu'une fois dans Demo () si c'est le seul programme utilisé)
program.use ();
// Spécifier la matrice de projection
matrixP = libWEBGL.createMatrix2D ();
if (wglContext.width > wglContext.height)
matrixP.setScaling (wglContext.height / wglContext.width, 1.0);
else
matrixP.setScaling (1.0, wglContext.width / wglContext.height);
program.setUniform ("U_mP", matrixP.m);
// Récupérer les points et indices du quad
vertices = new Array ();
indices = new Array ();
quad.load (vertices, indices);
nbVerticesPerPrimitives = quad.nbVerticesPerPrimitives;
nbPrimitives = quad.nbPrimitives;
// Charger les points et les indices
program.load (vertices, indices);
// Demander le rendu
gl.clearColor (0.0, 0.0, 0.0, 1.0);
gl.clear (gl.COLOR_BUFFER_BIT);
gl.drawElements (gl.TRIANGLES, nbVerticesPerPrimitives * nbPrimitives, gl.UNSIGNED_SHORT, 0);
// Désactiver le programme (il est possible de s'en passer si c'est le seul programme utilisé)
program.use (false);
A la fin de l'application, il est de bon ton de faire le ménage :
vertexShader.destroy ();
fragmentShader.destroy ();
program.destroy ();
- vertexShader.destroy ();
- fragmentShader.destroy ();
- program.destroy ();
vertexShader.destroy ();
fragmentShader.destroy ();
program.destroy ();
Des règles d'utilisation pas trop contraignantes
Les méthodes d'un objet VertexShader
VertexShader ou FragmentShader
FragmentShader sont les suivantes :
Modifie le niveau de détail des informations affichées dans la console en cas d'erreur, sur la base d'une combinaison des drapeaux libWEBGL.DEBUG_MESSAGE libWEBGL.DEBUG_MESSAGE et libWEBGL.DEBUG_TRACE libWEBGL.DEBUG_TRACE, ou libWEBGL.DEBUG_NONE libWEBGL.DEBUG_NONE. |
Retourne le message décrivant la dernière erreur rencontrée. |
Rénitialise le shader : les entrées, les sorties, les uniformes et le programme GLSL sont supprimés. |
Détruit le shader, qui ne doit dès lors plus être utilisé. Attention, car cette destruction n'est pas vérifiée lors d'une tentative de réutilisation. |
Ajoute une entrée - un attribut du point de vue du programme GLSL d'un vertex shader, un varying du point de vue de celui d'un fragment shader. Attention, car l'ordre dans lequel les attributs sont ajoutés doit correspondre à celui dans lequel les données qui les alimentent sont fournies dans le tableau des points fourni à Program.load () Program.load (). |
Ajoute une sortie - un varying du point de vue du programme GLSL d'un vertex shader, l'équivalent de gl_FragColor gl_FragColor du point de vue de celui d'un fragment shader. |
Ajoute une uniforme. |
Compile le shader pour qu'il puisse être fourni à Program.build () Program.build (). |
Les méthodes .addInput ()
.addInput (), .addOutput ()
.addOutput () et .addUniform ()
.addUniform () prennent trois paramètres :
Le nom, sous forme d'une chaîne de caractères. |
La précision, décrite par une constante correspondant à une précision GLSL :
Aucune. Utiliser si type type est libWEBGL.TYPE_SAMPLER2D libWEBGL.TYPE_SAMPLER2D |
lowp lowp |
mediump mediump |
highp highp |
|
Le type, décrit par une constante correspondant à un type GLSL :
float float |
vec2 vec2 |
vec3 vec3 |
vec4 vec4 |
mat3 mat3 |
mat4 mat4 |
sampler2D sampler2D |
|
Qui regarde dans le code de libWEBGL.js s'étonnera qu'il n'existe aucune différence notable entre les objets VertexShader
VertexShader et FragmentShader
FragmentShader, qui dérivent d'un objet Shader
Shader. C'est que les fonctionnalités de WebGL ne nécessitaient pas de les distinguer plus que cela. Si ces objets existent, c'est simplement pour ne pas insulter l'avenir.
Les méthodes d'un objet Program
Program sont les suivantes :
Modifie le niveau de détail des informations affichées dans la console en cas d'erreur, sur la base d'une combinaison des drapeaux libWEBGL.DEBUG_MESSAGE libWEBGL.DEBUG_MESSAGE et libWEBGL.DEBUG_TRACE libWEBGL.DEBUG_TRACE, ou libWEBGL.DEBUG_NONE libWEBGL.DEBUG_NONE. |
Retourne le message décrivant la dernière erreur rencontrée. |
Rénitialise le programme : les shaders sont détachés, et le programme GLSL est supprimé. |
Détruit le programme, qui ne doit dès lors plus être utilisé. Attention, car cette destruction n'est pas vérifiée lors d'une tentative de réutilisation. |
Lie le programme à partir d'un vertex shader et d'un fragment shader compilés qui lui sont attachés à cette occasion. |
Détache un shader du programme. |
Modifier la valeur d'une uniforme. |
Active ou désactive le programme. |
Charge les points et les indices depuis des tableaux en vue d'un appel à gl.drawElements () gl.drawElements (). |
Les règles d'utilisation sont plus strictes que ce que permet WebGL. En particulier, ce dernier permet de modifier / détacher / attacher un shader à un programme utilisé, manoeuvres qui sont interdites dans libWEBGL aussitôt que le programme est lié.
Concernant les shaders, les règles sont :
- un shader peut être attaché à N >= 0 programmes ;
- un shader ne peut pas être détaché d'un programme utilisé ;
- un shader ne peut pas être réinitialisé tant qu'il est attaché à un programme (a fortiori à plusieurs) ;
- un shader ne peut pas être détruit tant qu'il ne peut pas être réinitialisé ;
- un shader ne peut pas être compilé tant qu'il est attaché à un programme (a fortiori à plusieurs).
Concernant les programmes, les règles sont :
- un programme ne peut pas charger des points et des indices s'il n'est pas utilisé ;
- un programme peut toujours être réinitialisé ;
- lors de la réinitialisation d'un programme, les shaders qui lui sont attachés en sont détachés ;
- un programme peut toujours être détruit ;
- lors de la destruction d'un programme, ce programme est réinitialisé ;
- un programme ne peut pas être lié si un vertex shader et un fragment shader compilés ne lui sont pas attachés ;
- un programme ne peut pas être utilisé s'il n'a pas été lié ;
- un programme ne peut pas être utilisé si un vertex shader et un fragment shader compilés ne lui sont pas attachés ;
- un programme ne peut affecter de valeur à une uniforme que s'il est utilisé.
Le respect de ces règles est assuré par des tests systématiques dans les différentes méthodes qu'elles concernent. Si elle détecte une infraction, une telle méthode retourne un booléen à false
false, et affiche un message explicite dans la console.
Attention ! Il n'existe aucune mécanique générale pour s'assurer qu'un unique objet Program
Program est en cours d'utilisation. Cela pourrait poser un problème lors de l'appel à Program.load ()
Program.load () car cette méthode charge les points et les indices dans les VBOs présumés liés à ARRAY_BUFFER et à ELEMENT_ARRAY_BUFFER lors d'un précédent appel à Program.use ()
Program.use (). Il faut donc éviter une séquence telle que :
program0.use ();
program1.use ();
program0.load (vertices, indices); // Chargera dans les VBOs liés par program1.use () et non par program0.use () !
- program0.use ();
- program1.use ();
- program0.load (vertices, indices); // Chargera dans les VBOs liés par program1.use () et non par program0.use () !
program0.use ();
program1.use ();
program0.load (vertices, indices); // Chargera dans les VBOs liés par program1.use () et non par program0.use () !
Précision sur le fonctionnement d'un VAO
Comme c'était déjà le cas des helpers que ces wrappers viennent donc remplacer, ces wrappers s'appuient sur un Vertex Array Object, ou VAO.
Pour rappel, il ne faut pas confondre le Vertex Buffer Object, ou VBO, et le Vertex Array Buffer, ou VAO :
-
Un VBO est un buffer destiné à être lié par un appel à
gl.bindBuffer ()
gl.bindBuffer () à l'un des buffers internes : ARRAY_BUFFER
ARRAY_BUFFER pour les points, et ELEMENT_ARRAY_BUFFER
ELEMENT_ARRAY_BUFFER pour les indices.
-
Un VAO est un moyen pratique pour éviter d'avoir à écrire la longue suite d'appels à différentes fonctions de WebGL nécessaire pour décrire comment WebGL doit lire les points et les indices lors d'un rendu à venir.
Le VAO est documenté par Khronos
ici, mais il faut bien reconnaître que les explications données peuvent être assez difficiles à suivre. Elles deviennent bien plus intelligibles et intéressantes à lire après avoir saisi en quoi consiste exactement ce fameux état que la VAO est censé permettre de mémoriser et de restaurer si facilement, et comment utiliser le VAO à cette fin.
Pour comprendre en quoi l'état d'un VAO consiste, il faut se référer au tableau 6.2, page 247 de
la spécification d'OpenGL ES 3.0.6 qui, comme chacun sait, constitue le standard de référence pour WebGL 2. Ce tableau recense les variables qui composent cet état. Il mérite toutefois d'être repris en mettant en face de chaque variable la ou les fonctions de WebGL dont l'effet de l'appel sur une variable est mémorisé par le VAO :
gl.enableVertexAttribArray () gl.enableVertexAttribArray ()
gl.disableVertexAttribArray () gl.disableVertexAttribArray () |
gl.vertexAttribPointer (..., size, ...) gl.vertexAttribPointer (..., size, ...)
gl.vertexAttribIPointer (..., size, ...) // WebGL 2 gl.vertexAttribIPointer (..., size, ...) // WebGL 2 |
gl.vertexAttribPointer (..., stride, ...) gl.vertexAttribPointer (..., stride, ...)
gl.vertexAttribIPointer (..., stride, ...) // WebGL 2 gl.vertexAttribIPointer (..., stride, ...) // WebGL 2 |
gl.vertexAttribPointer (..., type, ...) gl.vertexAttribPointer (..., type, ...)
gl.vertexAttribIPointer (..., type, ...) // WebGL 2 gl.vertexAttribIPointer (..., type, ...) // WebGL 2 |
gl.vertexAttribPointer (..., normalized, ...) gl.vertexAttribPointer (..., normalized, ...)
gl.vertexAttribIPointer (..., normalized, ...) // WebGL 2 gl.vertexAttribIPointer (..., normalized, ...) // WebGL 2 |
gl.vertexAttribI4[u]i[v] () // WebGL 2 gl.vertexAttribI4[u]i[v] () // WebGL 2 |
gl.vertexAttribDivisor () // WebGL 2 gl.vertexAttribDivisor () // WebGL 2 |
gl.vertexAttribPointer (index, size, type, normalized, stride, offset) gl.vertexAttribPointer (index, size, type, normalized, stride, offset) |
gl.bindBuffer (gl.ELEMENT_ARRAY_BUFFER, buffer) gl.bindBuffer (gl.ELEMENT_ARRAY_BUFFER, buffer) |
Voir les explications ci-après. |
En effet, un VAO se crée ainsi... :
// Créer une fois pour toute le VAO
vao = gl.createVertexArray ();
// Lier le VAO pour commencer à lui faire mémoriser la manière de lire les points et les indices
gl.bindVertexArray (vao);
// Procéder à des appels qui décrivent cette manière de lire en prenant pour exemple un format de point (x, y, r, g, b, a)
gl.bindBuffer (gl.ARRAY_BUFFER, bufferV); // Attention, ce lien n'est pas mémorisé par le VAO (cf. plus loin)
gl.bindBuffer (gl.ELEMENT_ARRAY_BUFFER, bufferI);
gl.enableVertexAttribArray (this.A_xy);
gl.vertexAttribPointer (this.A_xy, 2, gl.FLOAT, false, 6 * Float32Array.BYTES_PER_ELEMENT, 0);
gl.enableVertexAttribArray (this.A_rgba);
gl.vertexAttribPointer (this.A_rgba, 4, gl.FLOAT, false, 6 * Float32Array.BYTES_PER_ELEMENT, 2 * Float32Array.BYTES_PER_ELEMENT);
// Délier le VAO pour qu'il arrête de mémoriser
gl.bindVertexArray (null);
- // Créer une fois pour toute le VAO
- vao = gl.createVertexArray ();
- // Lier le VAO pour commencer à lui faire mémoriser la manière de lire les points et les indices
- gl.bindVertexArray (vao);
- // Procéder à des appels qui décrivent cette manière de lire en prenant pour exemple un format de point (x, y, r, g, b, a)
- gl.bindBuffer (gl.ARRAY_BUFFER, bufferV); // Attention, ce lien n'est pas mémorisé par le VAO (cf. plus loin)
- gl.bindBuffer (gl.ELEMENT_ARRAY_BUFFER, bufferI);
- gl.enableVertexAttribArray (this.A_xy);
- gl.vertexAttribPointer (this.A_xy, 2, gl.FLOAT, false, 6 * Float32Array.BYTES_PER_ELEMENT, 0);
- gl.enableVertexAttribArray (this.A_rgba);
- gl.vertexAttribPointer (this.A_rgba, 4, gl.FLOAT, false, 6 * Float32Array.BYTES_PER_ELEMENT, 2 * Float32Array.BYTES_PER_ELEMENT);
- // Délier le VAO pour qu'il arrête de mémoriser
- gl.bindVertexArray (null);
// Créer une fois pour toute le VAO
vao = gl.createVertexArray ();
// Lier le VAO pour commencer à lui faire mémoriser la manière de lire les points et les indices
gl.bindVertexArray (vao);
// Procéder à des appels qui décrivent cette manière de lire en prenant pour exemple un format de point (x, y, r, g, b, a)
gl.bindBuffer (gl.ARRAY_BUFFER, bufferV); // Attention, ce lien n'est pas mémorisé par le VAO (cf. plus loin)
gl.bindBuffer (gl.ELEMENT_ARRAY_BUFFER, bufferI);
gl.enableVertexAttribArray (this.A_xy);
gl.vertexAttribPointer (this.A_xy, 2, gl.FLOAT, false, 6 * Float32Array.BYTES_PER_ELEMENT, 0);
gl.enableVertexAttribArray (this.A_rgba);
gl.vertexAttribPointer (this.A_rgba, 4, gl.FLOAT, false, 6 * Float32Array.BYTES_PER_ELEMENT, 2 * Float32Array.BYTES_PER_ELEMENT);
// Délier le VAO pour qu'il arrête de mémoriser
gl.bindVertexArray (null);
...et il s'utilise ainsi :
// Lier le VAO, car en plus de lui faire mémoriser, cela commence par lui faire rétablir tout ce qu'il avait mémorisé
gl.bindVertexArray (vao);
// Commander le rendu
gl.drawElements (gl.TRIANGLES, nbVerticesPerPrimitives * nbPrimitives, gl.UNSIGNED_SHORT, 0);
// Délier le VAO
gl.bindVertexArray (null);
// A la toute fin, quand le vao ne sera plus utilisé, le détruire
gl.deleteVertexArray (vao);
- // Lier le VAO, car en plus de lui faire mémoriser, cela commence par lui faire rétablir tout ce qu'il avait mémorisé
- gl.bindVertexArray (vao);
- // Commander le rendu
- gl.drawElements (gl.TRIANGLES, nbVerticesPerPrimitives * nbPrimitives, gl.UNSIGNED_SHORT, 0);
- // Délier le VAO
- gl.bindVertexArray (null);
- // A la toute fin, quand le vao ne sera plus utilisé, le détruire
- gl.deleteVertexArray (vao);
// Lier le VAO, car en plus de lui faire mémoriser, cela commence par lui faire rétablir tout ce qu'il avait mémorisé
gl.bindVertexArray (vao);
// Commander le rendu
gl.drawElements (gl.TRIANGLES, nbVerticesPerPrimitives * nbPrimitives, gl.UNSIGNED_SHORT, 0);
// Délier le VAO
gl.bindVertexArray (null);
// A la toute fin, quand le vao ne sera plus utilisé, le détruire
gl.deleteVertexArray (vao);
On le voit, le VAO semble répéter les appels aux fonctions qui permettent de spécifier la manière dont les points et les indices doivent être lus lors du rendu. Tout cela est fort simple à comprendre, à une subtilité près, qui concerne la mémorisation du lien aux buffers internes.
Dans le cas d'un ELEMENT_ARRAY_BUFFER, ou buffer des indices, le VAO mémorise le buffer actuellement lié à ELEMENT_ARRAY_BUFFER. Autrement dit, pour l'utiliser :
// Démarrer la mémorisation
gl.bindVertexArray (vao);
// Lier bufferA à ELEMENT_ARRAY_BUFFER
gl.bindBuffer (gl.ELEMENT_ARRAY_BUFFER, bufferA);
// Arrêter la mémorisation
gl.bindVertexArray (null);
// Lier bufferA à ELEMENT_ARRAY_BUFFER
gl.bindBuffer (gl.ELEMENT_ARRAY_BUFFER, bufferB);
// Rappeler la mémorisation
gl.bindVertexArray (vao);
// Vérifier que bufferA est de nouveau lié à ELEMENT_ARRAY_BUFFER
if (gl.getParameter (gl.ELEMENT_ARRAY_BUFFER_BINDING) === bufferA)
console ("Binding restored");
- // Démarrer la mémorisation
- gl.bindVertexArray (vao);
- // Lier bufferA à ELEMENT_ARRAY_BUFFER
- gl.bindBuffer (gl.ELEMENT_ARRAY_BUFFER, bufferA);
- // Arrêter la mémorisation
- gl.bindVertexArray (null);
- // Lier bufferA à ELEMENT_ARRAY_BUFFER
- gl.bindBuffer (gl.ELEMENT_ARRAY_BUFFER, bufferB);
- // Rappeler la mémorisation
- gl.bindVertexArray (vao);
- // Vérifier que bufferA est de nouveau lié à ELEMENT_ARRAY_BUFFER
- if (gl.getParameter (gl.ELEMENT_ARRAY_BUFFER_BINDING) === bufferA)
- console ("Binding restored");
// Démarrer la mémorisation
gl.bindVertexArray (vao);
// Lier bufferA à ELEMENT_ARRAY_BUFFER
gl.bindBuffer (gl.ELEMENT_ARRAY_BUFFER, bufferA);
// Arrêter la mémorisation
gl.bindVertexArray (null);
// Lier bufferA à ELEMENT_ARRAY_BUFFER
gl.bindBuffer (gl.ELEMENT_ARRAY_BUFFER, bufferB);
// Rappeler la mémorisation
gl.bindVertexArray (vao);
// Vérifier que bufferA est de nouveau lié à ELEMENT_ARRAY_BUFFER
if (gl.getParameter (gl.ELEMENT_ARRAY_BUFFER_BINDING) === bufferA)
console ("Binding restored");
Au passage, noter l'existence de la fonction gl.getParameter ()
gl.getParameter () qui permet de consulter une variable de l'état de WebGL.
Dans le cas d'un ARRAY_BUFFER, ou buffer des points, c'est plus complexe. Pour comprendre, il faut se rappeler comment gl.vertexAttribPointer ()
gl.vertexAttribPointer () fonctionne. Cette fonction permet de décrire la manière dont les données qui alimentent un attribut doivent être lues depuis un VBO, ce dernier étant celui qui est lié à ARRAY_BUFFER au moment où la fonction est appelée. Par exemple, pour décrire la manière dont les coordonnées (x, y) doivent être lues de bufferVxyz
bufferVxyz pour alimenter un attribut A_xyz
A_xyz, et les composantes (r, g, b, a) des couleurs doivent être lues de bufferVrgba
bufferVrgba pour alimenter un attribut A_rgba
A_rgba :
// Récupérer les indices des attributs à configurer
var A_xyz = gl.getAttribLocation (program, "A_xyz");
var A_rgba = gl.getAttribLocation (program, "A_rgba");
// Démarrer la mémorisation
gl.bindVertexArray (vao);
// Faire de bufferVxyz l'ARRAY_BUFFER actuel
gl.bindBuffer (gl.ARRAY_BUFFER, bufferVxyz);
// Mémoriser où lire de quoi alimenter A_xyz à partir de l'ARRAY_BUFFER actuel, donc de bufferVzyz
gl.vertexAttribPointer (A_xyz, ...);
// Faire de bufferVxyz l'ARRAY_BUFFER actuel
gl.bindBuffer (gl.ARRAY_BUFFER, bufferVrgba);
// Mémoriser où lire de quoi alimenter A_rgba à partir de l'ARRAY_BUFFER actuel, donc de bufferVrgba
gl.vertexAttribPointer (A_rgba, ...);
// Arrêter la mémorisation
gl.bindVertexArray (null);
- // Récupérer les indices des attributs à configurer
- var A_xyz = gl.getAttribLocation (program, "A_xyz");
- var A_rgba = gl.getAttribLocation (program, "A_rgba");
- // Démarrer la mémorisation
- gl.bindVertexArray (vao);
- // Faire de bufferVxyz l'ARRAY_BUFFER actuel
- gl.bindBuffer (gl.ARRAY_BUFFER, bufferVxyz);
- // Mémoriser où lire de quoi alimenter A_xyz à partir de l'ARRAY_BUFFER actuel, donc de bufferVzyz
- gl.vertexAttribPointer (A_xyz, ...);
- // Faire de bufferVxyz l'ARRAY_BUFFER actuel
- gl.bindBuffer (gl.ARRAY_BUFFER, bufferVrgba);
- // Mémoriser où lire de quoi alimenter A_rgba à partir de l'ARRAY_BUFFER actuel, donc de bufferVrgba
- gl.vertexAttribPointer (A_rgba, ...);
- // Arrêter la mémorisation
- gl.bindVertexArray (null);
// Récupérer les indices des attributs à configurer
var A_xyz = gl.getAttribLocation (program, "A_xyz");
var A_rgba = gl.getAttribLocation (program, "A_rgba");
// Démarrer la mémorisation
gl.bindVertexArray (vao);
// Faire de bufferVxyz l'ARRAY_BUFFER actuel
gl.bindBuffer (gl.ARRAY_BUFFER, bufferVxyz);
// Mémoriser où lire de quoi alimenter A_xyz à partir de l'ARRAY_BUFFER actuel, donc de bufferVzyz
gl.vertexAttribPointer (A_xyz, ...);
// Faire de bufferVxyz l'ARRAY_BUFFER actuel
gl.bindBuffer (gl.ARRAY_BUFFER, bufferVrgba);
// Mémoriser où lire de quoi alimenter A_rgba à partir de l'ARRAY_BUFFER actuel, donc de bufferVrgba
gl.vertexAttribPointer (A_rgba, ...);
// Arrêter la mémorisation
gl.bindVertexArray (null);
Le VAO ne mémorise pas quel VBO est lié à ARRAY_BUFFER en général. Il le mémorise par attribut. Autrement dit, il mémorise la configuration des attributs, en mémorisant pour chaque attribut :
- la configuration telle que décrite par les valeurs fournies à
gl.vertexAttribPointer ()
gl.vertexAttribPointer () ;
- le VBO qui était lié à ARRAY_BUFFER lors de l'appel à cette fonction, d'où le sens à donner à la variable "VERTEX ATTRIB ARRAY BUFFER BINDING" du tableau 6.2 présenté plus tôt.
Il ne faut donc pas s'attendre à ce que le rappel d'un VAO re-lie un VBO à ARRAY_BUFFER comme ce rappel re-lie un VBO à ELEMENT_ARRAY_BUFFER. Ce n'est vrai qu'à l'échelle d'un attribut, pas à celle du programme, comme il est possible de le vérifier :
// Lier bufferV0 à ARRAY_BUFFER et bufferI0 à ELEMENT_ARRAY_BUFFER
gl.bindBuffer (gl.ARRAY_BUFFER, bufferV0);
gl.bindBuffer (gl.ELEMENT_ARRAY_BUFFER, bufferI0);
// Utiliser le VAO pour mémoriser de nouveaux liens de bufferV1 et bufferI1 à ces buffers
gl.bindVertexArray (vao);
gl.bindBuffer (gl.ARRAY_BUFFER, bufferV1);
gl.bindBuffer (gl.ELEMENT_ARRAY_BUFFER, bufferI1);
gl.bindVertexArray (null);
// Lier bufferV2 à ARRAY_BUFFER et bufferI2 à ELEMENT_ARRAY_BUFFER
gl.bindBuffer (gl.ARRAY_BUFFER, bufferV2);
gl.bindBuffer (gl.ELEMENT_ARRAY_BUFFER, bufferI2);
// Rappeler la mémorisation
gl.bindVertexArray (vao);
// Vérifier que bufferV2 est toujours lié à ARRAY_BUFFER, mais que bufferI1 est de nouveau lié à ELEMENT_ARRAY_BUFFER
if (gl.getParameter (gl.ARRAY_BUFFER_BINDING) === bufferV2)
console ("bufferV2 binding to ARRAY_BUFFER restored");
if (gl.getParameter (gl.ELEMENT_ARRAY_BUFFER_BINDING) === bufferI1)
console ("bufferI1 bindind to ELEMENT_ARRAY_BUFFER restored");
- // Lier bufferV0 à ARRAY_BUFFER et bufferI0 à ELEMENT_ARRAY_BUFFER
- gl.bindBuffer (gl.ARRAY_BUFFER, bufferV0);
- gl.bindBuffer (gl.ELEMENT_ARRAY_BUFFER, bufferI0);
- // Utiliser le VAO pour mémoriser de nouveaux liens de bufferV1 et bufferI1 à ces buffers
- gl.bindVertexArray (vao);
- gl.bindBuffer (gl.ARRAY_BUFFER, bufferV1);
- gl.bindBuffer (gl.ELEMENT_ARRAY_BUFFER, bufferI1);
- gl.bindVertexArray (null);
- // Lier bufferV2 à ARRAY_BUFFER et bufferI2 à ELEMENT_ARRAY_BUFFER
- gl.bindBuffer (gl.ARRAY_BUFFER, bufferV2);
- gl.bindBuffer (gl.ELEMENT_ARRAY_BUFFER, bufferI2);
- // Rappeler la mémorisation
- gl.bindVertexArray (vao);
- // Vérifier que bufferV2 est toujours lié à ARRAY_BUFFER, mais que bufferI1 est de nouveau lié à ELEMENT_ARRAY_BUFFER
- if (gl.getParameter (gl.ARRAY_BUFFER_BINDING) === bufferV2)
- console ("bufferV2 binding to ARRAY_BUFFER restored");
- if (gl.getParameter (gl.ELEMENT_ARRAY_BUFFER_BINDING) === bufferI1)
- console ("bufferI1 bindind to ELEMENT_ARRAY_BUFFER restored");
// Lier bufferV0 à ARRAY_BUFFER et bufferI0 à ELEMENT_ARRAY_BUFFER
gl.bindBuffer (gl.ARRAY_BUFFER, bufferV0);
gl.bindBuffer (gl.ELEMENT_ARRAY_BUFFER, bufferI0);
// Utiliser le VAO pour mémoriser de nouveaux liens de bufferV1 et bufferI1 à ces buffers
gl.bindVertexArray (vao);
gl.bindBuffer (gl.ARRAY_BUFFER, bufferV1);
gl.bindBuffer (gl.ELEMENT_ARRAY_BUFFER, bufferI1);
gl.bindVertexArray (null);
// Lier bufferV2 à ARRAY_BUFFER et bufferI2 à ELEMENT_ARRAY_BUFFER
gl.bindBuffer (gl.ARRAY_BUFFER, bufferV2);
gl.bindBuffer (gl.ELEMENT_ARRAY_BUFFER, bufferI2);
// Rappeler la mémorisation
gl.bindVertexArray (vao);
// Vérifier que bufferV2 est toujours lié à ARRAY_BUFFER, mais que bufferI1 est de nouveau lié à ELEMENT_ARRAY_BUFFER
if (gl.getParameter (gl.ARRAY_BUFFER_BINDING) === bufferV2)
console ("bufferV2 binding to ARRAY_BUFFER restored");
if (gl.getParameter (gl.ELEMENT_ARRAY_BUFFER_BINDING) === bufferI1)
console ("bufferI1 bindind to ELEMENT_ARRAY_BUFFER restored");
Pour terminer, noter l'existence de la fonction gl.getVertexAttrib ()
gl.getVertexAttrib () qui permet de visualiser la configuration d'un attribut telle que mémorisée par le VAO, notamment le buffer qui était lié à ARRAY_BUFFER quand gl.vertexAttribPointer ()
gl.vertexAttribPointer () a été appelée.