Cet article est la suite de WebGL - les bases. Avant de poursuivre je pense qu'il faut discuter de ce que WebGL et la carte graphique font vraiment. Il y a en gros 2 parties dans le travail de la carte graphique. La première partie traite les vertices (du moins leurs données) et les convertit en vertices dans l'espace de coordonnées. La deuxième partie colore les pixels d'après la première partie.
Quand vous appelez
gl.drawArrays(gl.TRIANGLES, 0, 9);
Le 9 ici signifie "il y a 9 vertices à traiter" donc voilà 9 vertices rendus.
Sur la gauche il y a les données fournies. Le shader de vertex est une fonction que vous écrivez en
GLSL. Il est appelé une fois pour chaque vertex.
Vous écrivez quelques opérations et renseignez la variable gl_Position
avec une valeur
dans l'espace de projection pour le vertex en cours. La carte graphique prend cette valeur et la met en mémoire.
En supposant que vous faîtes des TRIANGLES
, à chaque fois que cette première partie génère
3 points la carte graphique s'en sert pour faire un triangle. Elle regarde quels pixels sont
concernés entre ces trois points, et pixélise le triangle. Pour chaque pixel elle va appeler
notre shader de fragment pour nous demander quelle couleur nous voulons pour ce pixel. Le
shader de fragment doit renseigner la variable gl_FragColor
avec la couleur finale de chaque pixel.
Tout ceci est passionnant mais comme vous voyez dans nos exemples jusqu'à maintenant le shader de fragment a très peu d'information. Heureusement on peut lui en envoyer davantage. On définit des “varyings” pour chaque valeur qu'on veut transmettre du shader de vertex au shader de fragment.
Un exemple simple est d'envoyer les coordonnées en espace de projection directement du shader de vertex au shader de fragment.
On va dessiner un simple triangle. Après notre exemple précédent changeons notre F en triangle.
// Remplit le tampon avec les valeurs qui définissent un triangle
function creerGeometrie(gl) {
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array([
0, -100,
150, 125,
-175, 100]),
gl.STATIC_DRAW);
}
Et on n'a qu'à rendre 3 points.
// Rendu de la scène
function rendreLaScene() {
...
// Rendu de la géométrie.
* gl.drawArrays(gl.TRIANGLES, 0, 3);
}
Ensuite dans notre shader de vertex on déclare une varying pour passer des données au shader de fragment.
varying vec4 v_couleur;
...
void main() {
// Multiplie la position par la matrice
gl_Position = vec4((u_matrix * vec3(a_position, 1)).xy, 0, 1);
// Convertit de l'espace de projection en espace de couleur :
// L'espace de projection va de -1.0 à +1.0
// L'espace de couleur va de 0.0 à 1.0
* v_couleur = gl_Position * 0.5 + 0.5;
}
Et ensuite on déclare la même varying dans le shader de fragment.
precision mediump float;
*varying vec4 v_couleur;
void main() {
* gl_FragColor = v_couleur;
}
WebGL va faire le lien entre la varying du shader de vertex et celle qui a les mêmes type et nom dans le shader de fragment.
Voici la version fonctionnelle.
Bougez, tournez et changez l'échelle du triangle. Puisque les couleurs sont calculées depuis l'espace de projection elles ne changent pas avec les transformations. Elles sont relatives à l'arrière-plan.
Maintenant réfléchissons. On ne calcule que 3 points. Notre shader de vertex n'est appelé que 3 fois et donc il ne calcule que 3 couleurs. Pourtant notre triangle en a davantage. C'est pour cela qu'on appelle ces variables transmises entre shaders, des varying.
WebGL prends les 3 valeurs qu'on a calculées pour chaque point et pixélise le triangle en interpolant entre les valeurs renseignées. Pour chaque pixel le shader de fragment est appelé avec la valeur de la varying interpolée pour ce pixel.
Dans l'exemple précédent on a commencé avec 3 vertices.
Vertices | |
---|---|
0 | -100 |
150 | 125 |
-175 | 100 |
Notre shader de vertex applique une matrice pour translater, tourner, changer d'échelle et convertir en espace de projection. Les valeurs par défaut sont translation = 200, 150, rotation = 0, échelle = 1,1 donc on a en fait seulement une translation. Puisque la taille de notre tampon est de 400x300 (taille du canvas) notre shader de vertex applique la matrice et ensuite calcule les 3 vertices suivants dans l'espace de projection.
Valeurs renseignées à gl_Position | ||
---|---|---|
0.000 | 0.660 | |
0.750 | -0.830 | |
-0.875 | -0.660 |
Il convertit aussi celle dans l'espace de couleur et les renseigne dans les varying v_couleur qu'on a déclarées.
Valeurs renseignées à v_couleur | ||
---|---|---|
0.5000 | 0.830 | 0.5 |
0.8750 | 0.086 | 0.5 |
0.0625 | 0.170 | 0.5 |
Ces 3 valeurs écrites dans v_couleur sont ensuite interpolées et passées au shader de fragment pour chaque pixel.
On peut aussi envoyer plus de données au shader de vertex et les transmettre ensuite au shader de fragment. Par exemple, dessinons un rectangle, soit 2 triangles, en 2 couleurs. Pour cela on va indiquer un autre attribut dans le shader de vertex pour lui envoyer cette nouvelle information, et on va la transmettre directement au shader de fragment.
attribute vec2 a_position;
+attribute vec4 a_couleur;
...
varying vec4 v_couleur;
void main() {
...
// Copie la couleur de l'attribut à la varying.
* v_couleur = a_couleur;
}
Maintenant il nous faut indiquer les couleurs dans nos géométries.
// créé les pointeurs pour nos données de vertices
var emplacementPosition = gl.getAttribLocation(program, "a_position");
+ var emplacementCouleur = gl.getAttribLocation(program, "a_couleur");
...
+ // Créé un tampon pour les couleurs
+ var tampon = gl.createBuffer();
+ gl.bindBuffer(gl.ARRAY_BUFFER, tampon);
+ gl.enableVertexAttribArray(emplacementCouleur);
+ gl.vertexAttribPointer(emplacementCouleur, 4, gl.FLOAT, false, 0, 0);
// Créé les valeurs des couleurs
+ creerCouleurs(gl);
...
+// Remplit le tampon avec les couleurs pour les deux triangles
+// qui forment le rectangle.
+function creerCouleurs(gl) {
+ // Créé deux couleurs au hasard
+ var r1 = Math.random();
+ var b1 = Math.random();
+ var g1 = Math.random();
+
+ var r2 = Math.random();
+ var b2 = Math.random();
+ var g2 = Math.random();
+
+ gl.bufferData(
+ gl.ARRAY_BUFFER,
+ new Float32Array(
+ [ r1, b1, g1, 1,
+ r1, b1, g1, 1,
+ r1, b1, g1, 1,
+ r2, b2, g2, 1,
+ r2, b2, g2, 1,
+ r2, b2, g2, 1]),
+ gl.STATIC_DRAW);
+}
Voici le résultat
Notons que nous avons deux couleurs homogènes dans les triangles. Pourtant on passe bien les couleurs à nos varying et elles sont toujours interpolées pour chaque pixel. Mais on a mis les mêmes couleurs aux sommets d'un même triangle, d'où ce résultat. Si on indique des couleurs différentes l'interpolation redevient visible :
// Remplit le tampon avec les couleurs pour les deux triangles
// qui forment le rectangle.
function creerCouleurs(gl) {
// Créé une couleur différente par vertex
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array(
* [ Math.random(), Math.random(), Math.random(), 1,
* Math.random(), Math.random(), Math.random(), 1,
* Math.random(), Math.random(), Math.random(), 1,
* Math.random(), Math.random(), Math.random(), 1,
* Math.random(), Math.random(), Math.random(), 1,
* Math.random(), Math.random(), Math.random(), 1]),
gl.STATIC_DRAW);
}
L'interpolation de nos varying est visible.
Rien de très excitant je suppose mais ça démontre l'utilisation de plusieurs attributs et la transmission d'information entre les deux shaders. Si vous jetez un oeil aux exemples de traitement d'image on verra qu'ils utilisent aussi un attribut spécial pour transmettre les coordonnées de texture.
Les tampons (buffer) sont l'espace mémoire dans la carte graphique dans lequel on place les informations
de géométrie. gl.createBuffer
créé un tampon.
gl.bindBuffer
active le tampon pour une action à suivre.
gl.bufferData
place des données dans le tampon.
Une fois que les données sont dans le tampon on doit indiquer au programme WebGL comment lire ces données et où sont les attributs à envoyer à notre shader de vertex.
Pour cela on demande d'abord au programme WebGL l'emplacement qu'il a assigné aux attributs (le pointeur). Par exemple dans le code précédent on a
// créé les pointeurs pour nos données de vertices
var emplacementPosition = gl.getAttribLocation(programme, "a_position");
var emplacementCouleur = gl.getAttribLocation(programme, "a_couleur");
Une fois qu'on connaît l'emplacement d'un attribut on soumet deux commandes.
gl.enableVertexAttribArray(emplacement);
Cette commande dit à WebGL qu'on veut fournir des données depuis un tampon
gl.vertexAttribPointer(
emplacement,
nombreDeComposantes,
typeDeDonnée,
donnéesNormaliséesOuNon,
tailleDeLaFenêtreDeLecture,
décalage);
Et cette commande dit à WebGL d'utiliser les données du dernier tampon activé gl.bindBuffer,
combien de composants il y a dans l'attribut par vertex (1 - 4), de quel type de données il s'agit
(BYTE
, FLOAT
, INT
, UNSIGNED_SHORT
, etc.), la taille (en byte) de la fenêtre de lecture,
et le décalage de départ de la première composante de l'attribut.
Le nombre de composantes est toujours compris entre 1 et 4.
Si vous utilisez un tampon par type de données alors la fenêtre de lecture et le décalage de départ peuvent être laissés à 0. 0 pour la fenêtre de lecture signifie "utilise une fenêtre qui correspond au type et à la la taille des données indiquées". 0 pour le décalage signifie que le démarrage de la lecture commence à la première donnée du tampon. Mais indiquer d'autres valeurs devient plus compliqué et bien que ça ait des avantages en performance, ça ne vaut pas toujours le coup à moins que vous poussiez les limites de votre carte graphique.
J'espère que ça éclaire les notions de tampon et d'attribut.
Rendez-vous ensuite à shaders et GLSL.