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 ()
.
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>
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
, 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 ()
pour récupérer un objet VertexShader
. Ensuite, il faut appeler ses méthodes .addInput ()
, .addUniform ()
et .addOutput ()
pour fournir la liste des éléments correspondants du programme GLSL du shader. Un dernier appel à .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); }");
Attention à l'ordre des appels à la méthode décrivant le format de point. En effet, ces appels à
.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 ()
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
, ce qui affiche un message sur chaque erreur rencontrée. Ce niveau peut être combiné à 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
à l'aide du paramètre nommé debugLevel
. Tout ceci vaut aussi pour les deux autres autres objets, FragmentShader
et 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 ()
qui retourne un objet 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; }");
La troisième et dernière étape consiste à créer le programme. Un appel à la fabrique
libWEBGL.createProgram ()
retourne un objet Program
. Ce dernier doit être lié aux shaders, ce qui s'effectue en appelant sa méthode .build ()
:
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 ()
ou.use (true)
pour activer le programme ;.setUniform ()
pour affecter une valeur à une uniforme utilisée par un shader ;.load ()
pour charger les points et les indices ;.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);
A la fin de l'application, il est de bon ton de faire le ménage :
vertexShader.destroy (); fragmentShader.destroy (); program.destroy ();
Des règles d'utilisation pas trop contraignantes
Les méthodes d'un objet
VertexShader
ou FragmentShader
sont les suivantes :
.setDebugLevel () | 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 et libWEBGL.DEBUG_TRACE , ou libWEBGL.DEBUG_NONE . |
.getError () | Retourne le message décrivant la dernière erreur rencontrée. |
.reset () | Rénitialise le shader : les entrées, les sorties, les uniformes et le programme GLSL sont supprimés. |
.destroy () | 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. |
.addInput () | 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 () . |
.addOutput () | Ajoute une sortie - un varying du point de vue du programme GLSL d'un vertex shader, l'équivalent de gl_FragColor du point de vue de celui d'un fragment shader. |
.addUniform () | Ajoute une uniforme. |
.build () | Compile le shader pour qu'il puisse être fourni à Program.build () . |
Les méthodes
.addInput ()
, .addOutput ()
et .addUniform ()
prennent trois paramètres :
name |
Le nom, sous forme d'une chaîne de caractères. | ||||||||||||||
precision |
La précision, décrite par une constante correspondant à une précision GLSL :
|
||||||||||||||
type |
Le type, décrit par une constante correspondant à un type GLSL :
|