Navigation

Tutoriel précédent : ajout de couleurs   Sommaire   Tutoriel suivant : quelques objets 3D

II. Introduction

Bienvenue dans mon troisième tutoriel de la série sur WebGL. Cette fois nous allons faire bouger les choses. Le tutoriel se base sur le quatrième tutoriel OpenGL de NeHe.

Voici le rendu de cette leçon lorsque exécutée dans un navigateur supportant WebGL.



Cliquez ici pour voir la version WebGL si votre navigateur supporte WebGL ou ici pour en avoir un.

Ci-dessous l'explication de la démonstration…

L'avertissement habituel : 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 et le second tutoriel« dire au système 3D qu'il y a un carré à cet endroit X la première fois que je le dessine, puis pour indiquer de le déplacer de dire au système 3D que le carré mentionné plus tôt a été déplacé à ce point Y. À la place, ce qui se passe est que vous » indiquez au système 3D qu'il y a un carré au point X et la prochaine fois que vous le dessinez, d'indiquer au système 3D que le carré est au point Y puis la fois prochaine au point Z et ainsi de suite.

J'espère que ce dernier paragraphe a rendu les choses plus claires pour quelques personnes.

Peu importe, cela signifie que jusqu'à présent notre code utilisait la fonction drawScene pour tout dessiner. Afin d'animer les objets nous allons arranger les éléments afin que cette fonction soit appelée périodiquement et dessine les objets avec une différence entre chaque appel. Commençons par le bas du fichier index.html et voyons à quoi cela ressemble. Premièrement, regardons la fonction qui démarre tout lorsque la page est chargée, webGLStart :

 
Sélectionnez
  function webGLStart() { 
    var canvas = document.getElementById("lesson03-canvas"); 
    initGL(canvas); 
    initShaders() 
    initBuffers(); 

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

    tick(); 
  } 

Ici, le seul changement est qu'à la place d'appeler drawScene à la fin pour dessiner la scène, nous appelons une nouvelle fonction, tick. Cette fonction doit être appelée régulièrement. Elle met à jour l'état d'animation de la scène (par exemple, l'animation du triangle de la rotation d'angle de 81 degrés à la rotation d'angle de 82 degrés) dessine la scène et fait en sorte d'être rappelée une prochaine fois à un moment approprié. C'est la prochaine fonction du fichier, donc regardons son code.

 
Sélectionnez
  function tick() { 
    requestAnimFrame(tick); 

La première ligne permet de faire en sorte que la fonction tick soit appelée une nouvelle fois lorsque la fenêtre doit être redessinée. requestAnimFrame est une fonction fournie par le biais d'un fichier de code « webgl-utils.js » fourni par Google que nous incluons à la page à l'aide d'une balise <script> au tout début de la page. Cela nous offre une méthode indépendante des navigateurs de demander de rappeler la fonction la prochaine fois qu'ils souhaiteront redessiner la scène WebGL. Par exemple, la prochaine fois que l'écran met à jour l'image. Maintenant, les fonctions pour faire ceci existent dans tous les navigateurs supportant WebGL mais celles-ci ont des noms différents (par exemple, Firefox possède une fonction appelée mozRequestAnimationFrame). À l'avenir, ils devraient tous utiliser requestAnimationFrame. Pour le moment, nous pouvons utiliser l'utilitaire WebGL de Google afin d'avoir un appel fonctionnant partout.

Il est à noter que vous pouvez obtenir un effet similaire en demandant au code JavaScript d'appeler requesAnimFrame dans la fonction régulièrement drawScene, par exemple en utilisant la fonction setInterval du langage. Nombreux sont les premiers codes (ainsi que les précédentes versions de ces tutoriels) qui le faisaient et cela fonctionnait, jusqu'à ce que les utilisateurs aient plus d'un onglet WebGL ouvert. Le problème est que les fonctions gérées avec setinterval sont appelées sans se soucier si l'onglet auquel elle appartient est affiché ou non. L'utiliser signifiait que les ordinateurs travaillaient pour afficher tous les onglets WebGL, tout le temps, qu'ils soient cachés ou non. Cela était évidemment une mauvaise chose et ce fut la raison de l'introduction de la fonction requestAnimationFrame, car les fonctions appelées avec celle-ci ne sont appelées que si l'onglet est visible

Voici la suite de la fonction tick :

 
Sélectionnez
    drawScene(); 
    animate(); 
  } 

Donc, une fois que nous avons programmé la fonction tick pour être appelée la prochaine fois que le navigateur souhaite redessiner l'image, nous en dessinons une nouvelle et nous mettons à jour notre état pour la prochaine. Regardons les fonctions drawScene et animate l'une après l'autre.

drawScene est environ aux deux tiers de la page index.html. La première chose à noter est que juste avant la déclaration de la fonction, nous avons défini deux nouvelles variables.

 
Sélectionnez
  var rTri = 0; 
  var rSquare = 0; 

Elles sont utilisées pour garder une trace de la rotation du triangle et du carré. Les deux commencent par être tournés de zéro degré et avec le temps ces nombres augmenteront, comme vous le verrez plus tard, entraînant la rotation continue des objets. (petite note : l'utilisation de variables globales, dans des applications 3D autres que des démonstrations, est une mauvaise chose. Je présente une meilleure structuration du code dans la leçon 9)

Le prochain changement dans la fonction drawScene arrive à l'endroit où l'on dessine le triangle. Je vous montre tout le code qui effectue le rendu pour des raisons de contexte.

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

    mat4.identity(mvMatrix); 

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

    mvPushMatrix(); // Nouveau
    mat4.rotate(mvMatrix, degToRad(rTri), [0, 1, 0]); // Nouveau

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

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

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

    mvPopMatrix(); // Nouveau

Afin d'expliquer ce qui se passe ici, revenons à la première leçon. J'y ai dit :

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 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 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.

Vous vous rappelez que l'état actuel est conservé dans la matrice modèle-vue. Partant de cela, le but de cet appel :

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

… est évident. Nous changeons l'état de la rotation de la matrice modèle-vue conservé, en tournant de rTri degrés autour de l'axe vertical (chose spécifiée par le vecteur passé en troisième paramètre). Cela signifie que lorsque le triangle est dessiné, il sera tourné de rTri degrés. Notez que mat4.rotate utilise des angles en radians. Personnellement, je trouve les degrés plus faciles à manipuler, donc j'ai écrit une simple fonction de conversion degToRad utilisée ici.

Et, qu'en est-il des appels à mvPushMatrix et mvPopMatrix ? Comme vous pouvez le deviner à partir des noms des fonctions, elles sont liées à la matrice modèle-vue. Revenons à mon exemple du dessin d'un robot et disons que votre code au plus haut niveau nécessite de se déplacer au point A, de dessiner le robot puis de se décaler du point A et de dessiner une théière. Le code qui dessine le robot peut faire toutes sortes de changements à la matrice modèle-vue. Il peut commencer avec le corps, puis descendre pour les jambes, puis revenir en haut pour la tête et finir avec les bras. Le problème est que si vous essayez par la suite d'appliquer un déplacement, vous n'allez pas vous déplacer à partir du point A, mais à partir du dernier décalage. Si votre robot a levé ses bras, votre théière sera en lévitation. Ce n'est pas une bonne chose.

Évidemment, nous avons besoin d'une méthode pour conserver l'état de la matrice modèle-vue avant de dessiner le robot et de restaurer cet état par la suite. C'est, bien sûr, ce que font mvPushMatrix et mvPopMatrix. mvPushMatrix envoie la matrice sur une pile et mvPopMatrix remplace la matrice actuelle par celle tout en haut de la pile. L'utilisation d'une pile signifie que vous pouvez avoir autant de code de rendu imbriqué que vous voulez, chacun manipulant la matrice de modèle-vue et la restaurant après. Donc, une fois que nous avons fini de dessiner notre triangle tourné, nous restaurons la matrice modèle-vue avec mvPopMatrix faisant que ce code :

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

… déplace dans la scène dans une image de référence non tournée. (Si ce n'est toujours pas clair, je conseille de copier le code et de voir ce qui se passe si vous enlevez le code push/pop et que vous le réexécutiez une nouvelle fois. Il va certainement « click » très vite).

Donc, ces trois changements font que le triangle tourne autour de l'axe vertical de par son centre sans affecter le carré. Il y a trois autres lignes semblables faisant tourner le carré autour de l'axe horizontal de par son centre.

 
Sélectionnez
    mvPushMatrix(); // Nouveau
    mat4.rotate(mvMatrix, degToRad(rSquare), [1, 0, 0]); // Nouveau

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

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

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

    mvPopMatrix(); // Nouveau
  }

… et c'est tout pour les changements du code de dessin dans drawScene.

Évidemment, l'autre truc que nous devons faire pour animer notre scène est de changer les valeurs de rTri et rSquare au fil du temps, afin qu'à chaque fois que la scène est dessinée, elle soit légèrement différente. Cela, bien sûr, se passe dans la nouvelle fonction animate, qui ressemble à ceci :

 
Sélectionnez
  var lastTime = 0; 
  function animate() { 
    var timeNow = new Date().getTime(); 
    if (lastTime != 0) { 
      var elapsed = timeNow - lastTime; 

      rTri += (90 * elapsed) / 1000.0; 
      rSquare += (75 * elapsed) / 1000.0; 
    } 
    lastTime = timeNow; 
  }

Une méthode simple pour animer la scène serait de simplement tourner notre triangle et notre carré avec une valeur fixe à chaque appel de la fonction animate (ce que faisait la leçon OpenGL originale sur laquelle est basée celle-ci). Mais ici, j'ai choisi de faire quelque chose que je pense un peu mieux en pratique. La valeur, que j'utilise pour tourner les objets, est déterminée par le temps qui s'est écoulé depuis le dernier appel à la fonction. Précisément, le triangle tourne de 90 degrés par seconde et le carré de 75 degrés par seconde. La bonne chose par rapport à cette méthode est que tout le monde observe la même vitesse d'animation de la scène et cela indépendamment de la vitesse de la machine. Les personnes avec des machines plus lentes (pour lesquelles les fonctions gérées avec requestAnimFrame seront appelées moins fréquemment) verront des images saccadées. Cela n'est pas important pour une démonstration comme celle-ci, mais évidemment, cela pourrait être un gros problème avec des jeux ou autres.

Donc, c'est tout pour le code qui anime et dessine la scène. Regardons le code de support que nous avons dû ajouter, mvPushMatrix et mvPopMatrix.

 
Sélectionnez
  var mvMatrix = mat4.create(); 
  var mvMatrixStack = []; // Nouveau
  var pMatrix = mat4.create(); 

  function mvPushMatrix() { // Nouveau
    var copy = mat4.create(); // Nouveau
    mat4.set(mvMatrix, copy); // Nouveau
    mvMatrixStack.push(copy); // Nouveau
  } // Nouveau

  function mvPopMatrix() { // Nouveau
    if (mvMatrixStack.length == 0) { // Nouveau
      throw "Invalid popMatrix!"; // Nouveau
    } // Nouveau
    mvMatrix = mvMatrixStack.pop(); // Nouveau
  } // Nouveau

Il ne devrait rien y avoir de surprenant. Nous avons une liste pour contenir la pile de matrices et nous définissons push et pop.

Il reste une seule nouvelle chose à expliquer : la fonction degToRad que j'ai mentionnée plus tôt. Si vous vous souvenez de vos cours de mathématiques, vous n'aurez aucune surprise…

 
Sélectionnez
    function degToRad(degrees) { 
        return degrees * Math.PI / 180; 
    } 

Et… c'est tout ! Il n'y a aucun autre changement à étudier. Maintenant, vous savez animer des scènes WebGL simples. Si vous avez une quelconque question, commentaire ou correction, veuillez laisser un commentaire.

La prochaine fois, (pour citer la préface de la leçon 5 de NeHe) nous allons « créer de vrais objets 3D, plutôt que des objets 2D dans un monde 3D ». Cliquez ici pour savoir comment.

III. Remerciements

Le code des fonctions mvPushMatrix et mvPopMatrix est adapté de la visionneuse de créatures de Spore Vladimir Vukićević. Merci aussi à Google pour la publication de leur très utile fichier d'aide webgl-utils.js, et bien sûr, je suis extrêmement redevable envers NeHe pour ses tutoriels OpenGL à l'origine de cette leçon.

Merci à Winjerome pour ses corrections et zoom61 pour sa correction orthographique.

Navigation

Tutoriel précédent : ajout de couleurs   Sommaire   Tutoriel suivant : quelques objets 3D