WebGL leçon 1 : un triangle et un carré

Giles Thomas

Traducteur

Alexandre Laurent

Dans ce tutoriel, vous apprendrez à dessiner un triangle et un carré dans une page Web.

Commentez

- Titre : WebGL leçon 1 : un triangle et un carré

- Auteur : Giles Thomas

- Traducteur : Alexandre Laurent

- Parution : 2 mai 2013

- Mise à jour : 2 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 :
débuter avec WebGL

Sommaire

Tutoriel suivant : ajout de couleurs

I - Introduction

Bienvenue dans mon premier tutoriel WebGL ! Cette première leçon repose sur le second tutoriel OpenGL de NeHe, qui est une voie populaire dans l'apprentissage de la 3D pour le développement de jeux. Il montre comment dessiner un triangle et un carré dans une page Web. Peut-être que cela n'est pas très passionnant en soi, mais, c'est une excellente introduction des fondements de WebGL. Si vous comprenez comment cela fonctionne, le reste devrait être simple…

Voici ce que le résultat sera lorsque vous exécuterez cette leçon dans un navigateur qui supporte WebGL :

Cliquez ici et vous verrez la version WebGL en ligne si votre navigateur le supporte ; cliquez ici pour en avoir un, s'il ne supporte pas WebGL.

Apprenez comment cela fonctionne ci-dessous…

Ces leçons ciblent des personnes ayant une connaissance raisonnable de la programmation, mais aucune expérience dans la 3D. Le but est de vous former, en vous donnant une explication de ce qui se passe dans le code, afin que vous puissiez produire vos propres pages Web 3D aussi vite que possible. J'écris ces articles tout en apprenant WebGL moi-même, donc il se peut (et cela est même certain !) qu'il y ait des erreurs. Utilisez-les, tout en sachant les risques. Par contre, je corrige les bogues et les erreurs de conception lorsque je les connais, donc si vous voyez quelque chose de cassé, dites-le-moi dans les commentaires.

Il y a deux façons de récupérer le code de cet exemple : soit en affichant le code « voir source » lorsque vous regardez à la page de démonstration, soit en utilisant GitHub : vous pouvez le cloner (ainsi que les leçons suivantes) à partir du dépôt. Dans tous les cas, dès que vous avez le code, chargez-le dans votre éditeur de texte préféré et lisez. Au premier coup d'œil, c'est plutôt intimidant, même si vous avez une connaissance innée de, disons, OpenGL. Soit, au début nous définissons un couple de « shaders », qui sont généralement vus comme relativement avancés… mais ne désespérez pas, c'est plus simple que cela en a l'air.

II - Leçon

Comme beaucoup de programmes, cette page WebGL commence par définir un ensemble de fonctions bas niveau qui sont utilisées dans le code haut niveau ci-dessous. Afin de l'expliquer, je vais commencer par la fin et remonter le tout, donc si vous suivez avec le code, sautez directement à la fin.

Vous allez voir le code HTML suivant :

<body onload="webGLStart();">
<a href="http://learningwebgl.com/blog/?p=28">&lt;&lt; Back to Lesson 1</a><br />

<canvas id="lesson01-canvas" style="border: none;" width="500" height="500"></canvas>

<br/>
<a href="http://learningwebgl.com/blog/?p=28">&lt;&lt; Back to Lesson 1</a><br />
</body>

C'est le code complet du corps de la page : tout le reste est en JavaScript (bien que si vous visualisiez le code avec « voir source », vous pourriez voir un peu de bazar pour l'analyse de mon site Web, que vous pouvez ignorer). Évidemment nous pouvons ajouter plus de code HTML entre les balises <body> et construire notre image WebGL dans une page Web normale, mais pour cette simple démonstration nous n'avons que les liens pour revenir sur mon blog et la balise <canvas>, qui est l'emplacement de l'application 3D. Les canvas sont une nouveauté pour HTML5 : ils apportent la possibilité d'afficher des éléments dessinés par le JavaScript dans les pages Web que ce soit en 2D et (avec WebGL) en 3D. Dans cette balise, nous ne spécifions rien de plus que les propriétés de la mise en page du canvas et nous laissons tout le code d'initialisation WebGL à la fonction JavaScript webGLStart, qui, comme vous pouvez le voir, sera appelée une fois la page chargée.

Défilons vers le haut pour atteindre la fonction et voyons voir à quoi elle ressemble :

function webGLStart()
{
var canvas = document.getElementById("lesson01-canvas");
initGL(canvas);
initShaders();
initBuffers();

gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.enable(gl.DEPTH_TEST);

drawScene();
}

Elle appelle les fonctions pour initialiser WebGL et les « shaders » que j'ai mentionnés plus tôt, en passant l'élément canvas sur lequel nous voulons dessiner nos objets 3D à la première fonction. Ensuite, l'initialisation des tampons se fait avec la fonction initBuffers. Les tampons sont des choses contenant les détails du triangle et du carré que nous allons dessiner. Nous allons parler d'eux plus en détail dans un moment. Ensuite, la fonction effectue quelques initialisations basiques de WebGL, indiquant que lors de l'effacement du canvas nous devons le rendre noir et que nous devons effectuer le test de profondeur (afin que les objets dessinés derrière d'autres soient cachés par ceux devant eux). Ces étapes sont implémentées en appelant les méthodes de l'objet gl. Nous allons voir comment il est initialisé plus tard. Finalement, la fonction appelle drawScene. Celle-ci (comme vous pouvez vous y attendre) dessine le triangle et le carré en utilisant les tampons.

Nous allons revenir sur initGL et initShaders plus tard, car elles sont importantes pour la compréhension du fonctionnement de la page, mais avant, regardons les fonctions initBuffers et drawScene.

Découvrons pas à pas initBuffers :

var triangleVertexPositionBuffer;
var squareVertexPositionBuffer;

Nous déclarons deux variables pour contenir les tampons. (Dans une page WebGL réelle, vous n'aurez pas de variable globale pour chaque objet dans la scène, mais nous le faisons ici pour garder les choses simples, comme nous ne faisons que commencer.)

Ensuite :

function initBuffers() {
triangleVertexPositionBuffer = gl.createBuffer();

Nous créons un tampon pour la position des vertex du triangle. Les vertex sont des points dans l'espace 3D qui définissent la forme de ce que nous dessinons. Pour notre triangle, nous allons avoir trois vertex (qui seront initialisés dans une minute). Le tampon est un espace mémoire sur la carte graphique. En envoyant les positions des vertex sur la carte à partir de notre code d'initialisation, nous allons pouvoir dessiner la scène, principalement en disant à WebGL de « dessiner les choses que nous lui avons indiquées avant ». Cela rend notre code très efficace, spécialement lorsque nous animons la scène et que nous voulons dessiner l'objet une dizaine de fois chaque seconde afin de le déplacer. Bien sûr, lorsque ce ne sont que trois positions de vertex comme dans ce cas, le coût d'envoi des données sur la carte graphique n'est pas énorme, mais si nous jouons avec d'immenses modèles contenant des dizaines de centaines de vertex, il peut y avoir un réel avantage de faire de cette façon. Ensuite :

gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexPositionBuffer);

Cette ligne indique à WebGL que les opérations suivantes sur les tampons doivent utiliser celui spécifié. Il y a toujours ce concept de « tableau tampon actif » et les fonctions agissent sur celui-ci plutôt que de vous laisser spécifier le tableau tampon avec lequel vous voulez travailler. Étrange, mais je suis certain que les raisons sont liées aux performances…

var vertices = [
0.0,  1.0,  0.0,
-1.0, -1.0,  0.0,
1.0, -1.0,  0.0
];

Ensuite, nous définissons la position de nos vertex dans une liste JavaScript. Vous pouvez voir qu'ils forment un triangle isocèle centré en (0, 0, 0).

gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);

Maintenant, nous créons un objet Float32Array basé sur notre liste JavaScript et nous indiquons à WebGL de l'utiliser pour remplir le tampon actuellement actif, qui, bien entendu, est triangleVertexPositionBuffer. Nous allons parler des Float32Array dans une leçon future, mais pour le moment tout ce que nous avons besoin de savoir est que c'est une technique pour transformer une liste JavaScript en une chose que l'on peut passer à WebGL afin de remplir ses tampons.

triangleVertexPositionBuffer.itemSize = 3;
triangleVertexPositionBuffer.numItems = 3;

La dernière chose que nous devons effectuer avec le tampon est de définir deux nouvelles propriétés. Elles ne proviennent pas de WebGL, mais elles seront très utiles plus tard. Un bon point (certains diraient un mauvais point) du JavaScript est qu'un objet n'a pas besoin de supporter explicitement une propriété spécifique pour que vous puissiez la définir. Donc même si les objets tampons n'avaient pas les propriétés itemSize et numItems, maintenant ils les ont. Nous les utilisons pour dire que ce tampon de neuf éléments représente trois positions de vertex différentes (numItems), composés eux-mêmes de trois nombres (itemSize).

Maintenant que nous avons défini complètement le tampon pour le triangle, nous pouvons faire de même pour le carré :

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;
}

Tout cela devrait être assez évident. Le carré possède quatre positions de vertex au lieu de trois et donc le tableau est plus grand et numItems diffère.

OK, donc nous avons ce qu'il nous faut pour envoyer les positions de vertex des deux objets à la carte graphique. Maintenant, regardons la fonction drawScene, qui est l'endroit où nous utilisons ces tampons pour dessiner l'image que nous allons voir. Explorons-la étape par étape :

function drawScene() {
gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight);

La première étape est de lui donner des indications sur la taille du canvas avec la fonction viewport. Nous reviendrons sur le pourquoi il est important de le faire dans une prochaine leçon. Vous devez juste savoir que cette fonction doit être appelée avec la taille du canvas avant de commencer à dessiner. Ensuite, nous effaçons le canvas afin de le préparer pour notre dessin :

gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

… puis :

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

Ici, nous initialisons la perspective avec celle par laquelle nous voulons voir la scène. Par défaut, WebGL dessinera les choses proches avec la même taille que celles qui sont lointaines (dans un style 3D connu sous le nom de « projection orthogonale »). Afin de rendre les objets lointains plus petits, nous devons indiquer la perspective que nous voulons utiliser. Pour cette scène, nous définissons notre champ de vision vertical à 45 °, nous indiquons aussi le rapport largeur sur hauteur de notre canvas et que nous ne voulons pas voir les choses plus proches que 0,1 et plus loin que 100 unités.

Comme vous pouvez le constater, la fonction perspective est fournie par le module mat4 et met en scène une intrigante variable nommée pMatrix. Plus d'informations viendront par la suite. J'espère pour le moment que sans connaître les détails, son utilisation est claire.

Maintenant que nous avons notre perspective définie, nous pouvons continuer à dessiner nos objets :

mat4.identity(mvMatrix);

La première étape consiste à se « déplacer » vers le centre de la scène 3D. Dans OpenGL, lorsque vous dessinez la scène, vous lui indiquez de dessiner chaque objet à un endroit « spécifique » avec une rotation « spécifique ». Donc, par exemple, vous dites « déplace-toi de 20 unités en avant, tourne de 32 degrés puis dessine le robot », la dernière partie étant un ensemble complexe d'instructions « déplace-toi de tant, tourne un peu, dessine ceci » en elle-même. C'est utile, car vous pouvez encapsuler le code « dessine un robot » dans une fonction puis déplacer facilement le robot en changeant le code de déplacement/rotation avant d'appeler la fonction.

La position et la rotation actuelles sont toutes les deux déterminées par la matrice. Comme vous l'avez sûrement appris à l'école, les matrices peuvent décrire les translations (déplacement d'un endroit à un autre), les rotations et les autres transformations géométriques. Pour des raisons que je n'approfondirai pas maintenant, vous pouvez utiliser une seule matrice 4x4 (et non 3x3) pour représenter n'importe quel nombre de transformations dans un espace 3D. Vous démarrez avec la matrice d'identité, une matrice qui représente une transformation ne faisant rien, puis vous la multipliez par la matrice qui décrit votre première transformation puis par la matrice de votre seconde transformation et ainsi de suite. La matrice résultante représente toutes vos transformations en une seule. La matrice que nous utilisons pour représenter le déplacement et la rotation actuels est appelée la matrice modèle-vue (model-view) et vous avez sûrement deviné que notre variable mvMatrix contient notre matrice modèle-vue et que la fonction mat4.identity que nous venons d'appeler définit la matrice comme matrice d'identité prête à recevoir nos translations et rotations. En d'autres mots, la matrice nous déplace vers un point à partir duquel nous commençons à dessiner le monde 3D.

Les lecteurs vigilants auront remarqué qu'au début de cette discussion sur les matrices j'ai dit « Dans OpenGL », et non pas « Dans WebGL ». Cela est dû au fait que WebGL n'intègre pas ces choses à la bibliothèque graphique. À la place, nous utilisons une bibliothèque de matrices externe, l'excellente glMatrix de Brandon Jones avec d'élégantes astuces WebGL afin d'obtenir le même effet. D'autres informations sur cette élégance plus tard.

Soit, continuons avec le code qui dessine le triangle sur la partie gauche de notre canvas.

mat4.translate(mvMatrix, [-1.5, 0.0, -7.0]);

Après s'être placé au centre de la scène 3D en définissant la matrice mvMatrix à l'identité, nous commençons le triangle en nous déplaçant de 1,5 unité sur la gauche (qui est dans le sens négatif sur l'axe des X) et de sept unités dans la scène (qui est, en s'éloignant de la caméra, le sens négatif sur l'axe des Z). (mat4.translate, comme vous pouvez le deviner, signifie « multiplier la matrice donnée par une matrice de translation construite à partir des paramètres suivants ».)

La prochaine étape est de dessiner quelque chose !

gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexPositionBuffer);
gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, triangleVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);

Donc, vous vous rappelez que pour utiliser un de nos tampons, vous devez appeler gl.bindBuffer pour indiquer le tampon sur lequel vous voulez intervenir et ensuite appeler le code agissant sur celui-ci. Ici, nous sélectionnons notre triangleVertexPositionBuffer puis nous indiquons à WebGL que les valeurs à l'intérieur doivent être utilisées comme positions de vertex. J'expliquerai un peu plus le fonctionnement après. Pour le moment, vous pouvez voir que nous utilisons la propriété itemSize que nous avons définie sur le buffer pour dire à WebGL que chaque élément dans le tampon contenait trois nombres.

Ensuite, nous avons :

setMatrixUniforms();

Cela indique à WebGL de prendre en compte notre matrice modèle-vue actuelle (et aussi la matrice de projection que nous verrons plus tard). Cela est nécessaire, car les matrices ne sont pas intégrées à WebGL. La façon de procéder est que vous pouvez faire tout ce que vous voulez en changeant la variable mvMatrix mais cela ne se produit que dans l'espace privé de JavaScript. setMatrixUniforms est une fonction définie plus haut dans ce fichier pour envoyer la matrice à la carte graphique.

Une fois que cela est fait, WebGL possède un tableau de nombres qui doivent être traités comme des positions de vertex et il connait nos matrices. La prochaine étape est de lui dire ce qu'il doit en faire :

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

Ou, dit autrement : « dessine les tableaux de vertex que je t'ai donnés précédemment comme triangles en commençant par l'élément 0 du tableau jusqu'à l'élément numItems ».

Une fois cela fait, WebGL a dessiné notre triangle. La prochaine étape est de dessiner notre carré :

mat4.translate(mvMatrix, [3.0, 0.0, 0.0]);

Nous commençons par déplacer notre matrice de modèle-vue de trois unités vers la droite. Rappelez-vous, nous sommes actuellement 1,5 unité sur la gauche et sept en avant. Ensuite :

gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer);
gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, squareVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);

Donc, nous indiquons à WebGL d'utiliser notre tampon pour le carré comme positions de vertex…

setMatrixUniforms();

… nous envoyons les matrices de modèle-vue et de projection une nouvelle fois (afin de prendre en compte le dernier mvTranslate), signifiant que nous pouvons finalement :

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

dessiner les points. Vous pouvez vous demander ce qu'est un « triangle strip » ? Eh bien, c'est une suite de triangles :-). Plus précisément, c'est une suite de triangles où les trois premiers vertex que vous donnez forment le premier triangle, puis les deux derniers de celui-ci plus le vertex suivant forment le second triangle et ainsi de suite. Dans ce cas, c'est une méthode simple et sale pour construire un carré. Dans des cas plus complexes, cela peut être une technique très utile pour construire des surfaces complexes en termes du nombre de triangles.

Bref, une fois cela fait, nous avons fini notre fonction drawScene.

}

Si vous êtes arrivé aussi loin, vous êtes définitivement prêt à expérimenter. Copiez le code dans un fichier local que ce soit à partir de GitHub ou du fichier de démonstration. Pour ce dernier, vous avez besoin de index.html et glMatrix-0.9.5.min.js. Exécutez en local pour être certain que cela fonctionne, puis essayez de changer quelques positions de vertex ci-dessus. En particulier, la scène actuelle est plutôt plate. Essayez de changer les valeurs sur l'axe des Z pour le carré à 2 ou -3 et voyez s'il devient plus grand ou plus petit selon son déplacement en avant ou en arrière. Ou essayez de modifier juste un ou deux vertex et observez la déformation suivant la perspective. Amusez-vous et ne vous occupez pas de moi, je vais attendre.

…

OK, vous êtes revenu. Regardons les fonctions de support qui ont rendu possible l'exécution du code que nous avons vu. Comme je l'ai dit avant, si vous vous contentez d'ignorer les détails et que vous copiez juste les fonctions d'aide situées au-dessus de initBuffers, vous pouvez probablement partir avec et construire des pages WebGL intéressantes (bien qu'uniquement en noir et blanc, les couleurs étant le sujet de la prochaine leçon). Mais aucun de ces détails n'est difficile à comprendre et en apprenant comment cela fonctionne vous écrirez sûrement du meilleur code WebGL par la suite.

Toujours avec moi ? Merci :-) Débarrassons-nous des fonctions les plus ennuyantes en premier. La première est appelée par webGLStart et c'est initGL. Elle est proche du haut de la page et voici une copie :

var gl;
function initGL(canvas) {
try {
gl = canvas.getContext("experimental-webgl");
gl.viewportWidth = canvas.width;
gl.viewportHeight = canvas.height;
} catch(e) {
}
if (!gl) {
alert("Could not initialise WebGL, sorry :-(");
}
}

C'est très simple. Comme vous pouvez le remarquer, les fonctions initBuffers et drawScene utilisent fréquemment un objet appelé gl, qui peut être identifié comme une sorte de « cœur » de WebGL. Cette fonction récupère ce « cœur », appelé contexte WebGL, et le récupère en le demandant au canvas passé en paramètre grâce à un nom standard de contexte. (Comme vous pouvez le deviner, un jour le nom du contexte changera de « experimental-webgl » pour devenir « webgl ». Je mettrai à jour cette leçon et mon blog lorsque cela arrivera. Abonnez-vous à mon flux RSS si vous souhaitez être prévenu et bien sûr, si vous souhaitez avoir des nouvelles sur WebGL toutes les semaines.) Une fois que nous avons le contexte, nous utilisons encore les avantages du JavaScript nous permettant d'ajouter n'importe quelle propriété sur n'importe quel objet afin de sauvegarder la largeur et hauteur du canvas associé. Ainsi, nous pouvons utiliser ces propriétés dans le code pour définir le viewport et la perspective au début de la fonction startScene. Une fois que cela est fait, notre contexte GL est prêt.

Après avoir appelé initGL, webGLStart appelle la fonction initShaders. Celle-ci, bien entendu, initialise les shaders (doh ;-)). Nous reviendrons sur celle-ci plus tard, car nous devons d'abord regarder notre matrice modèle-vue et notre matrice de projection que j'ai mentionnées plus tôt. Voici le code :

var mvMatrix = mat4.create();
var pMatrix = mat4.create();

Donc, nous définissons une variable appelée mvMatrix pour contenir la matrice de modèle-vue et une autre appelée pMatrix pour la matrice de projection et nous les initialisons à une valeur vide (tous zéros) pour commencer. Il est maintenant nécessaire d'en dire un peu plus sur la matrice de projection. Comme vous vous en souvenez, nous appliquons la fonction glMatrix mat4.perspective à cette variable pour définir notre perspective, tout au début de la fonction drawScene. Cela est dû au fait que WebGL ne supporte pas directement les perspectives, tout comme il ne supporte pas directement une matrice modèle-vue. Mais, tout comme le déplacement des objets et leur rotation qui sont encapsulés dans la matrice modèle-vue, le processus pour rendre proportionnellement les objets lointains plus petits que les objets proches est une des choses dans lesquelles les matrices excellent. Et, comme vous devez l'avoir deviné, la matrice de projection est celle qui le fait. La fonction mat4.perspective, avec son ratio et son champ de vision, remplit la matrice avec les valeurs qui donnent le genre de perspective que nous voulons.

Bien, maintenant nous avons tout vu sauf la fonction setMatrixUniforms, qui comme je l'ai dit précédemment, transfère les matrices de modèle-vue et projection de JavaScript à WebGL ainsi que l'effrayante gestion des shaders. Ceux-ci sont interconnectés, donc commençons par une introduction.

Vous pouvez vous demander ce qu'est un shader ? Bien, à un certain moment de l'histoire des graphismes 3D ils étaient possiblement ce qu'ils sous-entendaient : un morceau de code indiquant au système comment ombrer ou colorer des morceaux de la scène avant son affichage. Par contre, avec le temps, leurs objectifs se sont étendus à tel point qu'il serait mieux de les définir comme un morceau de code qui peut faire tout ce que nous voulons sur des morceaux de la scène avant que celle-ci ne soit dessinée. Et cela est très pratique, car (a) ils sont exécutés sur la carte graphique, donc ils font ce qu'ils doivent faire très rapidement et (b) le genre de transformations qu'ils peuvent faire est très pratique pour les petits exemples comme ceux-ci.

La raison pour laquelle nous parlons des shaders dans un exemple simple de WebGL (ils sont de niveau intermédiaire dans les tutoriels OpenGL) est que nous les utilisons pour récupérer le système WebGL, fonctionnant heureusement sur la carte graphique. Les shaders permettent d'appliquer notre matrice de modèle-vue et notre matrice de projection de notre scène sans avoir à déplacer tous les points et tous les vertex en JavaScript, ce qui serait (relativement) lent. C'est très pratique et évite un surcoût.

Donc, voici comment ils sont définis. Comme vous vous en souvenez, webGLStart appelle initShaders, donc regardons-la, étape par étape.

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);

Comme vous pouvez le voir, elle utilise une fonction appelée getShader pour récupérer deux choses, un « fragment shader » et un « vertex shader » pour les attacher ensuite à un truc WebGL appelé un « programme ». Un programme est un morceau de code qui vit du côté WebGL du système. Vous pouvez le considérer comme une façon de spécifier quelque chose qui peut être exécuté sur la carte graphique. Comme vous pouvez vous y attendre, vous pouvez l'associer à un nombre de shaders contenant chacun un morceau de code de ce programme. Précisément, chaque programme peut contenir un « fragment » et un « vertex shader ». Nous allons les détailler rapidement.

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

Une fois que la fonction a initialisé et attaché les « shaders », elle récupère une référence sur un « attribut », qui sera contenu dans un nouveau champ de l'objet du programme appelé vertexPositionAttribute. Une fois encore, nous utilisons les avantages de JavaScript pour définir n'importe quel champ de n'importe quel objet. Les objets programmes n'ont pas de champ vertexPositionAttribute par défaut, mais il nous est pratique de garder les deux valeurs ensemble, donc nous mettons l'attribut dans le nouveau champ du programme.

Donc, à quoi sert vertexPositionAtrribute ? Comme vous pouvez vous en souvenir, nous l'utilisons dans drawScene. Si vous regardez le code qui définit les positions des vertex du triangle dans le tampon approprié, vous allez voir que la chose que nous avons associée au tampon est cet attribut. Vous allez voir ce que cela signifie dans un moment. Pour l'instant, notez simplement que nous utilisons aussi gl.enableVertexAttribArray pour indiquer à WebGL que nous voulons fournir des valeurs pour l'attribut par le biais d'un tableau.

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

La dernière chose qu'effectue initShaders est de récupérer deux autres valeurs du programme : l'emplacement des variables appelées « variables uniformes ». Nous allons bientôt les rencontrer. Pour le moment, vous devez juste retenir qu'elles sont comme l'attribut, nous les stockons dans l'objet du programme par facilité.

Maintenant, jetons un coup d'œil à getShader :

function getShader(gl, id) {
var shaderScript = document.getElementById(id);
if (!shaderScript) {
return null;
}

var str = "";
var k = shaderScript.firstChild;
while (k) {
if (k.nodeType == 3)
str += k.textContent;
k = k.nextSibling;
}

var shader;
if (shaderScript.type == "x-shader/x-fragment") {
shader = gl.createShader(gl.FRAGMENT_SHADER);
} else if (shaderScript.type == "x-shader/x-vertex") {
shader = gl.createShader(gl.VERTEX_SHADER);
} else {
return null;
}

gl.shaderSource(shader, str);
gl.compileShader(shader);

if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
alert(gl.getShaderInfoLog(shader));
return null;
}

return shader;
}

C'est une autre de ces fonctions qui est plus simple qu'il n'y paraît. Tout ce que nous faisons ici est de chercher un élément dans notre page HTML qui a un identifiant qui correspond au paramètre passé, l'extrayant du contenu de la page pour en créer un fragment ou un vertex shader selon son type (nous verrons dans une future leçon la différence entre les deux). Finalement, le shader produit est passé à WebGL afin qu'il soit compilé sous une forme exécutable pour la carte graphique. Le code gère les erreurs et c'est bon ! Bien sûr, nous pouvons simplement définir les shaders comme chaines de caractères dans le code JavaScript et ne pas nous ennuyer avec leur extraction du HTML. Mais en faisant de la sorte, nous pouvons les rendre plus faciles à lire, car ils sont définis comme script de la page Web, comme s'ils étaient du JavaScript eux-mêmes.

Une fois la fonction analysée, nous pouvons regarder le code des shaders :

<script id="shader-fs" type="x-shader/x-fragment">
precision mediump float;

void main(void) {
gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
}
</script>

<script id="shader-vs" type="x-shader/x-vertex">
attribute vec3 aVertexPosition;

uniform mat4 uMVMatrix;
uniform mat4 uPMatrix;

void main(void) {
gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);
}
</script>

La première chose qu'il faut se rappeler est qu'ils ne sont pas écrits en JavaScript, même si l'ancêtre du langage est très proche. En vrai, ils sont écrits dans un langage spécifique au shader : le GLSL, partageant beaucoup avec le C (comme le fait JavaScript, bien entendu).

Le premier est le fragment shader et ne fait presque rien. Il contient un morceau de code obligatoire pour indiquer à la carte graphique la précision que nous souhaitons pour les nombres à virgule flottante (la précision « medium » est bonne car elle est supportée par tous les périphériques WebGL. La précision « highp » pour la haute précision ne fonctionne pas sur les mobiles), puis indique que tout ce qui doit être dessiné doit être blanc. (La prochaine leçon expliquera comment colorer les objets.)

Le second shader est un peu plus intéressant. C'est un vertex shader, qui, je vous le rappelle, est un morceau de code pour la carte graphique qui peut faire à peu près tout ce qu'il veut avec le vertex. Associé à lui, il possède deux variables uniformes appelées uMVMatrix et uPMatrix. Les variables uniformes sont utiles, car elles peuvent être accessibles depuis l'extérieur du shader, évidemment depuis l'extérieur du programme et plus précisément à partir des emplacements extraits avec la fonction initShaders et définies avec les valeurs des matrices de modèle-vue et de projection en utilisant le code ci-dessous. Vous pouvez comparer le programme shader à un objet (dans le sens orienté objet) et les variables uniformes comme des membres.

Maintenant, le shader est appelé pour tous les vertex et les vertex sont passés dans le code du shader en utilisant la variable aVertexPosition, grâce à l'utilisation de vertexPositionAttribute de la fonction drawScene lorsque nous avons associé l'attribut au tampon. Le petit morceau de code de la fonction main du shader ne fait que multiplier la position du vertex par les matrices de modèle-vue et de projection et envoie le résultat comme position finale du vertex.

Donc, webGLStart a appelé initShaders, qui a utilisé getShader pour charger le fragment et le vertex shader à partir des scripts de la page Web, afin qu'ils puissent être compilés et passés à WebGL pour être utilisés plus tard lors du rendu de notre scène 3D.

Après ça, le seul morceau de code non expliqué est setMatrixUniforms, qui est facile à comprendre une fois que vous connaissez tout ce qui est au-dessus. :-)

function setMatrixUniforms() {
gl.uniformMatrix4fv(shaderProgram.pMatrixUniform, false, pMatrix);
gl.uniformMatrix4fv(shaderProgram.mvMatrixUniform, false, mvMatrix);
}

Donc, en utilisant les références aux variables uniformes qui représentent notre matrice de projection et notre matrice modèle-vue récupérées de la fonction initShaders, nous envoyons à WebGL les valeurs de nos matrices JavaScript.

Ouf ! C'était immense pour une première leçon, mais heureusement que maintenant vous (et je) comprenez mieux le fonctionnement de la base nécessaire pour construire quelque chose de plus intéressant, coloré, animé et avec des modèles WebGL à trois dimensions. Pour découvrir tout cela, lisez la seconde leçon.

III - Remerciements

Évidemment, je dois énormément à NeHe pour ses tutoriels OpenGL et le script de cette leçon, mais je voudrais aussi remercier Benjamin DeLillo et Vladimir Vukićević pour leurs codes d'exemple WebGL, que j'ai analysés, certainement complètement incompris et peut-être noyés dans le code sur lequel j'ai basé ce tutoriel:-). Merci aussi à Brandon Jones pour glMatrix. Finalement, je remercie James Coglan qui a écrit la bibliothèque générique Sylvester. La première version de cette leçon l'utilisait à la place de glMatrix qui se concentre plus sur WebGL, sans laquelle cette seconde version n'aurait jamais existé.

Merci à Winjerome pour sa relecture attentive et à _Max_ et ClaudeLELOUP pour leur relecture orthographique.

Navigation

Tutoriel précédent :
débuter avec WebGL

Sommaire

Tutoriel suivant : ajout de couleurs