Navigation

Tutoriel précédent : un peu de mouvement   Sommaire   Tutoriel suivant : introduction aux textures

II. Introduction

Bienvenue dans la quatrième leçon de mes tutoriels WebGL. Cette fois, nous allons afficher quelques objets 3D. Ce tutoriel est basé sur la cinquième leçon des tutoriels OpenGL de NeHe.

Voici à quoi ressemble la leçon lorsque vous l'exécutez dans un navigateur supportant WebGL :



Cliquez ici pour voir la démonstration dans sa version WebGL, si votre navigateur le supporte. Cliquez-ici pour en avoir un s'il ne supporte pas WebGL.

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. Si vous n'avez pas lu le premier, ni le second ni le troisième tutoriel, vous devriez le faire avant de lire celui-ci. Je n'expliquerai ici que les différences entre le code de la troisième leçon et le nouveau code.

Comme précédemment, il peut y avoir des problèmes de conception dans ce tutoriel. Si vous trouvez quoi que ce soit de faux, avertissez-moi aussi vite que possible.

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 autres leçons) à partir du dépôt. Dans les deux cas, une fois que vous avez le code, chargez-le dans votre éditeur de code préféré et jetez-y un coup d'œil.

III. Leçon

Les différences entre le code de cette leçon et le précédent sont concentrées dans les fonctions animate, initBuffers et drawScene. Si vous allez tout de suite à la fonction animate, vous allez voir un changement mineur : les variables rTri et rSquare contenant la rotation actuelle des deux objets de la scène ont été renommées. Nous avons aussi inversé la direction de la rotation du cube (car cela est plus joli), donc nous avons maintenant :

 
Sélectionnez
      rPyramid += (90 * elapsed) / 1000.0; 
      rCube -= (75 * elapsed) / 1000.0;

C'est tout pour cette fonction. Allons voir la fonction drawScene. Juste au-dessus de la déclaration nous avons la définition des nouvelles variables :

 
Sélectionnez
  var rPyramid = 0; 
  var rCube = 0;

Ensuite vient l'entête de la fonction, suivie de notre code d'initialisation et celui pour se déplacer à l'endroit où nous voulons dessiner la pyramide. Une fois que cela est fait, nous la tournons sur l'axe des Y comme nous le faisions dans la leçon précédente pour le triangle :

 
Sélectionnez
    mat4.rotate(mvMatrix, degToRad(rPyramid), [0, 1, 0]);

… et nous la dessinons. La seule différence entre ce code et celui de la leçon précédente dans laquelle nous dessinions un triangle coloré est que dans le nouveau code dans lequel nous dessinons cette aussi belle pyramide, nous dessinons plus de vertex et utilisons plus de couleurs. Tout cela sera géré dans la fonction initBuffers (que nous analyserons dans un instant). Cela signifie qu'il n'y a pas de différence à part dans les noms des tampons :

 
Sélectionnez
    gl.bindBuffer(gl.ARRAY_BUFFER, pyramidVertexPositionBuffer); 
    gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, pyramidVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0); 

    gl.bindBuffer(gl.ARRAY_BUFFER, pyramidVertexColorBuffer); 
    gl.vertexAttribPointer(shaderProgram.vertexColorAttribute, pyramidVertexColorBuffer.itemSize, gl.FLOAT, false, 0, 0); 

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

OK, c'était facile. Maintenant, regardons le code pour le cube. La première étape est de le tourner. Cette fois, au lieu de le tourner autour de l'axe des X, nous allons le tourner autour d'un axe qui est (par rapport à la caméra) orienté vers le ciel, sur la droite et devant nous.

 
Sélectionnez
    mat4.rotate(mvMatrix, degToRad(rCube), [1, 1, 1]);

Ensuite, nous dessinons le cube. Cette étape est un peu plus compliquée. Il y a trois façons de dessiner un cube :

  1. utiliser un ensemble de triangles liés. Si le cube avait possédé une couleur unie, cela aurait été facile. Nous aurions pu utiliser les positions des vertex que nous utilisions jusqu'à présent pour dessiner la face de devant, puis ajouter deux autres points pour rajouter une face, puis deux autres et ainsi de suite. Cela aurait été efficace. Malheureusement, nous voulons que chaque face ait une couleur différente. Comme chaque vertex spécifie un coin du cube, chaque coin sera partagé entre trois faces. Nous aurions eu besoin de spécifier chaque vertex trois fois et le faire aurait été tellement compliqué que je ne le détaillerai pas ;
  2. tricher et dessiner le cube en dessinant six carrés séparés, un par face, avec un ensemble de positions de vertex et de couleurs indépendantes. La première version de cette leçon (avant le 30 octobre 2009) le faisait et cela fonctionnait. Par contre, ce n'est pas une bonne pratique, car chaque fois que vous indiquez à WebGL d'afficher un nouvel objet, cela prend du temps. Il est bien mieux de minimiser le nombre d'appels à la fonction drawArrays ;
  3. spécifier le cube comme six carrés, chacun constitué de deux triangles en envoyant le tout à WebGL en un seul rendu. Cela est similaire à la façon de faire avec les ensembles de triangles, mais comme nous définissons les triangles entièrement au lieu de les spécifier en ajoutant un point au précédent, il est plus facile de fixer la couleur de chaque face. Cela a aussi l'avantage d'un code plus propre et me permet d'introduire une nouvelle fonction : drawElements. Donc, c'est la méthode que nous utiliserons.

La première étape consiste à associer les tampons contenant les positions des vertex et les couleurs du cube qui ont été créées dans initBuffers avec les attributs appropriés, tout comme nous l'avons fait pour la pyramide.

 
Sélectionnez
    gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexPositionBuffer); 
    gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, cubeVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0); 

    gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexColorBuffer); 
    gl.vertexAttribPointer(shaderProgram.vertexColorAttribute, cubeVertexColorBuffer.itemSize, gl.FLOAT, false, 0, 0); 

La prochaine étape consiste à dessiner les triangles. Il y a comme un problème ici. Prenons la face de devant. Nous avons quatre positions de vertex pour celle-ci et chacune possède une couleur. Toutefois, la face doit être dessinée à l'aide de deux triangles et comme nous utilisons de simples triangles, on doit spécifier leurs vertex individuellement. Plutôt que d'utiliser un ensemble de triangles qui partagent leurs vertex, nous devons définir au total six vertex. Mais nous n'en avons que quatre dans notre tampon.

Ce que nous devons faire est de spécifier quelque chose comme « dessine un triangle constitué des trois premiers vertex du tampon, puis dessine-en un autre constitué du premier, du troisième et du quatrième ». Cela permettra de dessiner notre face. Le rendu du reste du cube est similaire et c'est exactement ce que nous faisons.

Nous utilisons quelque-chose appelé tampon d'éléments et un nouvel appel à drawElements pour cela. Tout comme les précédents tampons que nous utilisions, le tampon d'éléments sera rempli avec les valeurs appropriées dans la fonction initBuffers et contiendra une liste de vertex utilisant un index partant de zéro pour spécifier les éléments de nos tableaux de positions et de couleurs. Nous allons voir cela dans un moment.

Afin de l'utiliser, nous déterminons comme actif notre tampon d'éléments (WebGL garde séparément les tampons de vertex et les tampons d'éléments actifs, donc nous devons spécifier lequel est à lier lors de l'appel à gl.bindBuffer), ensuite, nous utilisons le code habituel pour envoyer les matrices de modèle-vue et de projection à la carte graphique. Ensuite nous appelons drawElements pour dessiner les triangles :

 
Sélectionnez
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, cubeVertexIndexBuffer); 
    setMatrixUniforms(); 
    gl.drawElements(gl.TRIANGLES, cubeVertexIndexBuffer.numItems, gl.UNSIGNED_SHORT, 0); 

C'est tout pour drawScene. Le code restant est dans la fonction initBuffers et est particulièrement évident. Nous définissons les tampons avec de nouveaux noms pour correspondre aux nouveaux objets que nous gérons et nous en ajoutons un nouveau pour le tampon d'index des vertex du cube :

 
Sélectionnez
  var pyramidVertexPositionBuffer; 
  var pyramidVertexColorBuffer; 
  var cubeVertexPositionBuffer; 
  var cubeVertexColorBuffer; 
  var cubeVertexIndexBuffer;

Nous ajoutons les valeurs dans le tampon des positions des vertex de la pyramide pour toutes les faces tout en appliquant le changement approprié à numItems :

 
Sélectionnez
    pyramidVertexPositionBuffer = gl.createBuffer(); 
    gl.bindBuffer(gl.ARRAY_BUFFER, pyramidVertexPositionBuffer); 
    var vertices = [ 
        // Face avant 
         0.0,  1.0,  0.0, 
        -1.0, -1.0,  1.0, 
         1.0, -1.0,  1.0, 
        // Face droite 
         0.0,  1.0,  0.0, 
         1.0, -1.0,  1.0, 
         1.0, -1.0, -1.0, 
        // Face arrière 
         0.0,  1.0,  0.0, 
         1.0, -1.0, -1.0, 
        -1.0, -1.0, -1.0, 
        // Face gauche 
         0.0,  1.0,  0.0, 
        -1.0, -1.0, -1.0, 
        -1.0, -1.0,  1.0 
    ]; 
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW); 
    pyramidVertexPositionBuffer.itemSize = 3; 
    pyramidVertexPositionBuffer.numItems = 12;

… de même pour le tampon des couleurs des vertex de la pyramide :

 
Sélectionnez
    pyramidVertexColorBuffer = gl.createBuffer(); 
    gl.bindBuffer(gl.ARRAY_BUFFER, pyramidVertexColorBuffer); 
    var colors = [ 
        // Face avant 
        1.0, 0.0, 0.0, 1.0, 
        0.0, 1.0, 0.0, 1.0, 
        0.0, 0.0, 1.0, 1.0, 
        // Face droite
        1.0, 0.0, 0.0, 1.0, 
        0.0, 0.0, 1.0, 1.0, 
        0.0, 1.0, 0.0, 1.0, 
        // Face arrière
        1.0, 0.0, 0.0, 1.0, 
        0.0, 1.0, 0.0, 1.0, 
        0.0, 0.0, 1.0, 1.0, 
        // Face gauche
        1.0, 0.0, 0.0, 1.0, 
        0.0, 0.0, 1.0, 1.0, 
        0.0, 1.0, 0.0, 1.0 
    ]; 
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW); 
    pyramidVertexColorBuffer.itemSize = 4; 
    pyramidVertexColorBuffer.numItems = 12;

… ainsi que pour le tampon des positions des vertex du cube :

 
Sélectionnez
    cubeVertexPositionBuffer = gl.createBuffer(); 
    gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexPositionBuffer); 
    vertices = [ 
      // Face avant
      -1.0, -1.0,  1.0, 
       1.0, -1.0,  1.0, 
       1.0,  1.0,  1.0, 
      -1.0,  1.0,  1.0, 

      // Face arrière
      -1.0, -1.0, -1.0, 
      -1.0,  1.0, -1.0, 
       1.0,  1.0, -1.0, 
       1.0, -1.0, -1.0, 

      // Face du dessus
      -1.0,  1.0, -1.0, 
      -1.0,  1.0,  1.0, 
       1.0,  1.0,  1.0, 
       1.0,  1.0, -1.0, 

      // Face du dessous
      -1.0, -1.0, -1.0, 
       1.0, -1.0, -1.0, 
       1.0, -1.0,  1.0, 
      -1.0, -1.0,  1.0, 

      // Face arrière
       1.0, -1.0, -1.0, 
       1.0,  1.0, -1.0, 
       1.0,  1.0,  1.0, 
       1.0, -1.0,  1.0, 

      // Face gauche
      -1.0, -1.0, -1.0, 
      -1.0, -1.0,  1.0, 
      -1.0,  1.0,  1.0, 
      -1.0,  1.0, -1.0, 
    ]; 
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW); 
    cubeVertexPositionBuffer.itemSize = 3; 
    cubeVertexPositionBuffer.numItems = 24;

Le tampon des couleurs est légèrement plus complexe, car nous utilisons une boucle pour créer une liste de vertex de couleurs faisant en sorte que nous n'ayons pas besoin de spécifier chaque couleur quatre fois, une pour chaque vertex suffit :

 
Sélectionnez
    cubeVertexColorBuffer = gl.createBuffer(); 
    gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexColorBuffer); 
    colors = [ 
      [1.0, 0.0, 0.0, 1.0],     // Face avant
      [1.0, 1.0, 0.0, 1.0],     // Face arrière
      [0.0, 1.0, 0.0, 1.0],     // Face du dessus
      [1.0, 0.5, 0.5, 1.0],     // Face du dessous
      [1.0, 0.0, 1.0, 1.0],     // Face droite
      [0.0, 0.0, 1.0, 1.0],     // Face gauche
    ]; 
    var unpackedColors = []; 
    for (var i in colors) { 
      var color = colors[i]; 
      for (var j=0; j < 4; j++) { 
        unpackedColors = unpackedColors.concat(color); 
      } 
    } 
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(unpackedColors), gl.STATIC_DRAW); 
    cubeVertexColorBuffer.itemSize = 4; 
    cubeVertexColorBuffer.numItems = 24;

Finalement, nous définissons le tampon d'éléments (encore une fois notez la différence du premier paramètre des fonctions gl.bindBuffer et gl.bufferData) :

 
Sélectionnez
    cubeVertexIndexBuffer = gl.createBuffer(); 
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, cubeVertexIndexBuffer); 
    var cubeVertexIndices = [ 
      0, 1, 2,      0, 2, 3,    // Face avant
      4, 5, 6,      4, 6, 7,    // Face arrière
      8, 9, 10,     8, 10, 11,  // Face du dessus
      12, 13, 14,   12, 14, 15, // Face du dessous
      16, 17, 18,   16, 18, 19, // Face droite
      20, 21, 22,   20, 22, 23  // Face gauche
    ] 
    gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(cubeVertexIndices), gl.STATIC_DRAW); 
    cubeVertexIndexBuffer.itemSize = 1; 
    cubeVertexIndexBuffer.numItems = 36;

Rappelez-vous, chaque nombre dans le tampon est un index pour les tampons de positions et de couleurs. Donc, la première ligne, associée aux instructions pour dessiner les triangles dans drawScene signifie que nous obtenons un triangle utilisant les vertex 0, 1, 2 puis, les vertex 0, 2, 3. Comme les deux triangles sont de la même couleur et qu'ils sont adjacents, le résultat est un carré utilisant les vertex 0, 1, 2, 3. Répétez cela pour toutes les faces du cube et vous l'avez !

Maintenant, vous savez comment créer des scènes WebGL avec des objets 3D et vous savez comment réutiliser les vertex que vous avez spécifiés dans les tampons en utilisant un tampon d'éléments et la fonction drawElements. Si vous avez une quelconque question, commentaire ou correction, veuillez laisser un commentaire.

La prochaine fois, nous allons découvrir l'application de texture.

IV. Remerciements

Comme toujours, je suis extrêmement redevable envers NeHe pour son tutoriel OpenGL pour le script de cette leçon. Le WebKit de boîte rotative de Chris Marrin a été une source d'inspiration pour l'adaptation de cette leçon aux tampons d'éléments.

Merci à Winjerome pour ses corrections et ced pour sa relecture orthographique.

Navigation

Tutoriel précédent : un peu de mouvement   Sommaire   Tutoriel suivant : introduction aux textures