WebGL leçon 2 : ajout de couleurs

Giles Thomas

Traducteur

Jérôme Marsaguet

Dans ce tutoriel, vous apprendrez à ajouter des couleurs à votre triangle et carré de la leçon précédente.

Commentez

- Titre : WebGL leçon 2 : ajout de couleurs

- Auteur : Giles Thomas

- Traducteur : Jérôme Marsaguet

- Parution : 9 mai 2013

- Mise à jour : 9 mai 2013

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2013 Giles Thomas. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- Navigation

- I. Introduction

- II. Leçon

- III. Remerciements

- Navigation

Navigation

Tutoriel précédent : un triangle et un carré

Sommaire

Tutoriel suivant : un peu de mouvement

I - Introduction

Bienvenue dans mon deuxième tutoriel WebGL ! Cette fois-ci, nous allons nous intéresser à la façon de mettre de la couleur dans la scène. Il est basé sur le troisième tutoriel OpenGL de NeHe.

Voici à quoi ressemble la leçon lorsqu'elle est exécutée sur un navigateur qui prend en charge WebGL :

Cliquez ici pour voir la version live WebGL si vous avez un navigateur qui la prend en charge ; voici comment en obtenir un si vous n'en avez pas.

Plus de détails sur comment tout cela fonctionne ci-dessous…

Ces cours sont destinés aux personnes ayant de bonnes connaissances en programmation, mais sans réelle expérience dans la 3D, le but est de vous donner les moyens de démarrer, avec une bonne compréhension de ce qui se passe dans le code, afin que vous puissiez commencer à produire vos propres pages Web 3D aussi rapidement que possible. Si vous n'avez pas déjà lu le premier tutoriel, je vous conseille de le lire avant celui-ci — ici je vais seulement expliquer les différences avec le nouveau code.

Comme précédemment, il peut y avoir des bogues et des idées fausses dans ce tutoriel. Si vous repérez quelque chose de faux, faites-le-moi savoir dans les commentaires et je le corrigerai dès que possible.

Il y a deux façons d'obtenir le code de cet exemple, il suffit d'« Afficher la source » pendant que vous visionnez la version Live, ou si vous utilisez GitHub, vous pouvez le cloner (ainsi que les autres leçons) à partir du dépôt. Dans tous les cas, une fois que vous avez le code, chargez-le dans votre éditeur de texte favori et jetez-y un œil.

II - Leçon

La plupart de ce qui suit devrait sembler assez similaire au premier tutoriel. En parcourant de haut en bas, nous :

- définissons les vertex et fragment shader, en utilisant les balises HTML <script> avec les types « x-shader/x-vertex » et « x-shader/x-fragment » ;

- initialisons un contexte WebGL dans initGL ;

- chargeons les shaders dans un programme objet WebGL en utilisant getShader et initShaders ;

- définissons les matrices de modèle-vue mvMatrix et de projection pMatrix avec la fonction setMatrixUniforms pour les envoyer vers la division JavaScript / WebGL afin que les shaders puissent les voir ;

- chargeons des buffers contenant des informations sur les objets dans la scène avec initBuffers ;

- dessinons la scène elle-même, dans la bien connue drawScene ;

- définissons une fonction webGLStart pour tout mettre en place au début ;

- enfin, nous fournissons le code HTML minimal requis pour afficher tout cela.

Les seules choses qui ont changé depuis le code de la première leçon sont les shaders, initBuffers, et la fonction drawScene. Afin d'expliquer comment les changements s'appliquent, vous devez en savoir un peu plus sur le pipeline de rendu WebGL. Voici un diagramme :

Le diagramme montre, de façon très simplifiée, comment les données transmises aux fonctions JavaScript dans drawScene sont transformées en pixels affichés dans le canvas WebGL sur l'écran. Il ne montre que les étapes nécessaires à l'explication de cette leçon. Nous verrons des versions plus détaillées dans les leçons à venir.

Au plus haut niveau, le processus fonctionne comme ceci : chaque fois que vous appelez une fonction comme drawArrays, WebGL traite les données que vous avez précédemment spécifié sous la forme d'attributs (comme les tampons utilisés pour les vertex dans la première leçon) et les variables uniformes (utilisées pour les matrices de projection et modèle-vue), et les transmet au vertex shader.

Il fait ceci en appelant le vertex shader une fois pour chaque vertex, chaque fois avec les attributs correctement définis pour le vertex ; les variables uniformes sont également passées, mais comme l'indique leur nom, elles ne changent pas d'un appel à l'autre. Le vertex shader traite ces données — dans la première leçon, il a appliqué les matrices de projection et modèle-vue de sorte que les vertex soient tous en perspective et déplacés en fonction de l'état actuel de la matrice modèle-vue — et met ses résultats dans ce que l'on appelle des variables varying. Il peut fournir en sortie un certain nombre de variables varying ; en particulier une donnée est obligatoire : gl_Position, qui contient les coordonnées du vertex une fois que le shader a terminé de travailler dessus.

Une fois le vertex shader terminé, WebGL effectue le nécessaire pour convertir l'image 3D à partir de ces variables varying vers une image 2D, puis appelle le fragment shader une fois pour chaque pixel de l'image. (Dans certains systèmes graphiques 3D, vous entendrez fragment shaders qualifiés de pixels shaders pour cette raison.) Bien sûr, cela signifie qu'il appelle le fragment shader pour les pixels qui n'ont pas de vertex — autrement dit ceux situé entre les pixels sur lesquels les vertex se terminent. Pour ces derniers, il remplit des points dans les positions entre les vertex par un processus appelé interpolation linéaire — pour les positions des vertex qui composent notre triangle, ce processus « comble » l'espace délimité par les vertex avec des points pour former un triangle visible. Le but du fragment shader est de retourner la couleur pour chacun de ces points interpolés, et il le fait dans une variable varying nommée gl_FragColor.

Une fois le fragment shader terminé, ses résultats sont encore modifiés avec un peu de WebGL (encore une fois, nous en parlerons dans une prochaine leçon) et ils sont mis dans le frame buffer, qui est finalement affiché sur l'écran.

J'espère que, maintenant, il est clair que le point le plus important que cette leçon enseigne est de savoir comment envoyer la couleur des vertex au fragment shader à partir du code JavaScript quand nous n'avons pas d'accès direct de l'un vers l'autre.

Pour ce faire nous utilisons le fait que nous pouvons passer un certain nombre de variables varying depuis le vertex shader, non seulement la position, et pouvons ensuite les récupérer dans le fragment shader. Donc, nous passons la couleur au vertex shader qui peut ensuite la mettre directement dans une variable varying que le fragment shader pourra récupérer.

Idéalement, cela nous donne des dégradés de couleurs gratuitement. Toutes les variables varying fixées par le vertex shader sont interpolées linéairement lors de la génération des fragments entre les vertex, et pas seulement les positions. L'interpolation linéaire de la couleur entre les vertex nous donne des dégradés lisses, comme ceux que vous pouvez voir dans le triangle de l'image ci-dessus.

Regardons le code, nous allons travailler à travers les changements par rapport à la leçon 1. Tout d'abord, le vertex shader. Il a beaucoup changé, donc voici le nouveau code :

attribute vec3 aVertexPosition;
attribute vec4 aVertexColor;

uniform mat4 uMVMatrix;
uniform mat4 uPMatrix;

varying vec4 vColor;

void main(void) {
gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);
vColor = aVertexColor;
}

Ce que cela veut dire, c'est que nous avons deux attributs — des entrées qui varient d'un vertex à un autre — appelées aVertexPosition et aVertexColor, deux variables uniformes non-varying appelées uMVMatrix et uPMatrix, et une sortie sous la forme d'une variable varying appelée vColor.

Dans le corps du shader, nous calculons la gl_Position (qui est implicitement définie comme une variable varying pour chaque vertex shader) de la même manière que nous l'avons fait dans la première leçon, et tout ce que nous faisons avec la couleur est de la passer directement à l'aide de l'attribut d'entrée vers la variable varying de sortie.

Une fois que ceci a été exécuté pour chaque vertex, l'interpolation est effectuée pour générer les fragments, et ceux-ci sont transmis au fragment shader :

precision mediump float;

varying vec4 vColor;

void main(void) {
gl_FragColor = vColor;
}

Ici, après la formulation de la précision en virgule flottante, nous prenons la variable varying d'entrée vColor contenant la couleur mélangée en sortie de l'interpolation linéaire, la retournons immédiatement en tant que couleur de ce fragment — autrement dit, de ce pixel.

C'est tout pour les différences dans les shaders entre cette leçon et la dernière. Il y a deux autres changements. Le premier est minime, dans initShaders nous prenons maintenant les références de deux attributs plutôt qu'un, les lignes supplémentaires sont mises en évidence ci-dessous :

var shaderProgram;
function initShaders() {
var fragmentShader = getShader(gl, "shader-fs");
var vertexShader = getShader(gl, "shader-vs");

shaderProgram = gl.createProgram();
gl.attachShader(shaderProgram, vertexShader);
gl.attachShader(shaderProgram, fragmentShader);
gl.linkProgram(shaderProgram);

if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
alert("Could not initialise shaders");
}

gl.useProgram(shaderProgram);

shaderProgram.vertexPositionAttribute = gl.getAttribLocation(shaderProgram, "aVertexPosition");
gl.enableVertexAttribArray(shaderProgram.vertexPositionAttribute);

shaderProgram.vertexColorAttribute = gl.getAttribLocation(shaderProgram, "aVertexColor"); // NOUVEAU
gl.enableVertexAttribArray(shaderProgram.vertexColorAttribute); // NOUVEAU

shaderProgram.pMatrixUniform = gl.getUniformLocation(shaderProgram, "uPMatrix");
shaderProgram.mvMatrixUniform = gl.getUniformLocation(shaderProgram, "uMVMatrix");
}

Ce code, que nous avons passé sous silence dans une certaine mesure dans la première leçon, sert à obtenir les emplacements des attributs. Maintenant cela devrait être assez clair : il montre comment obtenir une référence sur les attributs à faire passer au vertex shader pour chaque vertex. Dans la première leçon, nous avons juste recueilli l'attribut de position des vertex. Maintenant tout aussi naturellement, nous obtenons l'attribut de couleur.

Les autres changements dans cette leçon sont dans initBuffers, qui doit maintenant mettre en place des tampons pour les positions et les couleurs des vertex, et drawScene, qui doit les passer à WebGL.

Premièrement, en regardant initBuffers nous définissons de nouvelles variables globales pour les tampons de couleur du triangle et du carré :

var triangleVertexPositionBuffer;
var triangleVertexColorBuffer; // NOUVEAU
var squareVertexPositionBuffer;
var squareVertexColorBuffer; // NOUVEAU

Puis, juste après avoir créé le tampon de la position des vertex du triangle, nous précisons les couleurs ds vertex :

function initBuffers() {
triangleVertexPositionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexPositionBuffer);
var vertices = [
0.0,  1.0,  0.0,
-1.0, -1.0,  0.0,
1.0, -1.0,  0.0
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
triangleVertexPositionBuffer.itemSize = 3;
triangleVertexPositionBuffer.numItems = 3;

triangleVertexColorBuffer = gl.createBuffer(); // NOUVEAU
gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexColorBuffer); // NOUVEAU
var colors = [
1.0, 0.0, 0.0, 1.0,
0.0, 1.0, 0.0, 1.0,
0.0, 0.0, 1.0, 1.0
]; // NOUVEAU
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW); // NOUVEAU
triangleVertexColorBuffer.itemSize = 4; // NOUVEAU
triangleVertexColorBuffer.numItems = 3; // NOUVEAU

Ainsi, les valeurs que nous donnons aux couleurs sont dans une liste, un ensemble de valeurs pour chaque vertex, tout comme les positions. Cependant, il y a une différence intéressante entre les deux tampons de tableaux : alors que les positions des vertex sont déterminées par trois nombres chacun pour les coordonnées X, Y et Z, leurs couleurs sont déterminées par quatre éléments — rouge, vert, bleu et alpha. Alpha, si vous ne connaissez pas, est une mesure d'opacité (0 pour transparent, 1 pour totalement opaque) et sera utile dans les leçons suivantes. Ce changement du nombre de données par élément dans le tampon requiert une modification de l'itemSize qui leur est associé.

Ensuite, nous faisons le code équivalent pour le carré, cette fois, nous utilisons la même couleur pour chaque vertex, de sorte à générer les valeurs pour le tampon avec une boucle :

squareVertexPositionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer);
vertices = [
1.0,  1.0,  0.0,
-1.0,  1.0,  0.0,
1.0, -1.0,  0.0,
-1.0, -1.0,  0.0
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
squareVertexPositionBuffer.itemSize = 3;
squareVertexPositionBuffer.numItems = 4;

squareVertexColorBuffer = gl.createBuffer(); // NOUVEAU
gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexColorBuffer); // NOUVEAU
colors = [] // NOUVEAU
for (var i=0; i < 4; i++) {
colors = colors.concat([0.5, 0.5, 1.0, 1.0]);
} // NOUVEAU
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW); // NOUVEAU
squareVertexColorBuffer.itemSize = 4; // NOUVEAU
squareVertexColorBuffer.numItems = 4; // NOUVEAU

Maintenant, nous avons toutes les données de nos objets au sein de quatre tampons, donc la prochaine modification consiste à faire utiliser les nouvelles données à drawScene. Le nouveau code est mis en évidence et devrait être facile à comprendre :

function drawScene() {
gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

mat4.perspective(45, gl.viewportWidth / gl.viewportHeight, 0.1, 100.0, pMatrix);

mat4.identity(mvMatrix);

mat4.translate(mvMatrix, [-1.5, 0.0, -7.0]);
gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexPositionBuffer);
gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, triangleVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);

gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexColorBuffer); // NOUVEAU
gl.vertexAttribPointer(shaderProgram.vertexColorAttribute, triangleVertexColorBuffer.itemSize, gl.FLOAT, false, 0, 0); // NOUVEAU

setMatrixUniforms();
gl.drawArrays(gl.TRIANGLES, 0, triangleVertexPositionBuffer.numItems);

mat4.translate(mvMatrix, [3.0, 0.0, 0.0]);
gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer);
gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, squareVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);

gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexColorBuffer); // NOUVEAU
gl.vertexAttribPointer(shaderProgram.vertexColorAttribute, squareVertexColorBuffer.itemSize, gl.FLOAT, false, 0, 0); // NOUVEAU

setMatrixUniforms();
gl.drawArrays(gl.TRIANGLE_STRIP, 0, squareVertexPositionBuffer.numItems);
}

Et le prochain changement… attendez, il n'y en a pas d'autre ! C'est tout ce qu'il fallait faire pour ajouter de la couleur à notre scène WebGL. J'espère aussi que vous êtes maintenant à l'aise avec les bases des shaders et la façon dont les données circulent entre elles.

C'est tout pour cette leçon — j'espère que cela a été plus facile que la première ! Si vous avez des questions, des commentaires ou des corrections, s'il vous plaît n'hésitez pas à laisser un commentaire.

La prochaine fois, nous ajouterons du code pour animer la scène en faisant tourner le triangle et le carré.

III - Remerciements

Le travail sur ce qui se passait réellement dans le pipeline de rendu a été grandement facilité en se référençant au guide de programmation OpenGL ES 2.0, que Jim Pick recommandait sur son blog WebGL. À jamais, je suis redevable envers NeHe pour son tutoriel OpenGL à l'origine des codes de cette leçon.

Merci à LittleWhite pour sa relecture attentive et zoom61 pour sa relecture orthographique.

Navigation

Tutoriel précédent : un triangle et un carré

Sommaire

Tutoriel suivant : un peu de mouvement