Canalblog
Editer l'article Suivre ce blog Administration + Créer mon blog
Publicité
Fanfois et ses papillons
4 juin 2012

HTML5 08 (tutorial WebGL 7ème partie)

Pour ceux qui ne l'auraient pas deviné à la lecture des précédents billets, le but de cette série de billets n'est pas de réaliser le nouveau jeu de la mort qui tue. Non, le but est de rendre en 3D des points et/ou des courbes et de permettre à l'utilisateur de manipuler la chose comme il le souhaite.
Dans le précédent billet nous avons presque réalisé cet objectif. Presque, car l'utilisateur n'a pas l'ombre d'un début d'information sur ce qu'il voit. D'autre part lorsque l'on zoom sur la version "nuage de points", il devient rapidement impossible de comprendre quoi que ce soit. En effet les points sont réellement des points, du coup en zoomant il arrive bien souvent qu'il n'y ait plus qu'un ou deux pixels de visibles à l'écran. Alors, certes, cela est dû au fait qu'il n'y a plus que deux ou trois points dans la zone visible, n'empêche que si les points grossissaient cela aiderait à comprendre que l'on est trop près.
Dans ce billet nous allons commencer à nous attaquer à quatre problèmes :

  • afficher un objet qui ait une surface ;
  • faire en sorte qu'un objet soit toujours face à la caméra ;
  • avoir des point plus gros lorsque l'on zoom ;
  • écrire un texte dans notre scène ;

Avant de nous lancer dans cette opération, qui risque d'être plus longue qu'il n'y parait, nous allons commencer par améliorer le code de notre dernière page HTML, à savoir WebGL_12.htm. Comme d'habitude nous allons la recopier dans une nouvelle page, WebGL_13.htm. Nous allons introduire deux modifications.

La première est liée à WebGL lui-même.
Pour tracer nos spirales nous avons été contraints de définir deux objets types. Un correspond aux spirales en traits, l'autre aux spirales en points.
Cette contrainte provient en fait du mode de dessin. Lorsque nous traçons la spirale en traits, nous traçons n segments de droites. Chaque segment est défini par ces deux extrémités. Jusque-là il n'y a pas grand-chose à dire.
Il y a cependant une caractéristique que nous n'avons pas prise en compte, la fin du segment n constitue le début du segment n+1. Il se trouve que cette particularité est assez courante, du coup WebGL sait la prendre en compte.
Nous ajoutons donc à notre classe WebGLle code suivant :

WebGL.prototype.drawLineStripArrays = function (buffers) {
  this.setMatrixUniforms();
  this.context.drawArrays(this.context.LINE_STRIP, 0, buffers.positionBuffer.numElements / 3);
};
 
WebGL.prototype.drawLineLoopArrays = function (buffers) {
  this.setMatrixUniforms();
  this.context.drawArrays(this.context.LINE_LOOP, 0, buffers.positionBuffer.numElements / 3);
};
 
WebGL.prototype.drawLineStripElements = function (buffers) {
  this.setMatrixUniforms();
  this.context.drawElements(this.context.LINE_STRIP, buffers.indexBuffer.numElements, this.context.UNSIGNED_SHORT, 0);
};
 
WebGL.prototype.drawLineLoopElements = function (buffers) {
  this.setMatrixUniforms();
  this.context.drawElements(this.context.LINE_LOOP, buffers.indexBuffer.numElements, this.context.UNSIGNED_SHORT, 0);
};

Ces deux nouveaux modes de dessin des lignes permettent de dessiner des lignes brisées où chaque fin de segment est le début du segment à venir. La version LINE_LOOP conduit quant à elle à une ligne fermée, un ultime segment étant ajouté entre le dernier point et le premier.
Avec la méthode drawLineStripArrays, il nous est maintenant possible de dessiner les deux types de spirale avec un seul model, celui actuellement dédié à la spirale en points.

La seconde modification a pour but de limiter le nombre de paramètres devant être passés à chaque méthode. Nous allons donc réorganiser le code de de la page HTML en introduisant une "classe" Theater. Cela permet de passer par les données membres et de simplifier les signatures des méthodes.
Le code complet de WebGL_13.htmest le suivant :

<!DOCTYPE HTML>
<html>
  <head>
    <!-- required to use WebGLHelper object -->
    <!-- <script type="text/javascript" src="http://iewebgl.com/scripts/webglhelper.js"></script> -->
    <script type="text/javascript" src="script/webglhelper.js"></script>
    <!-- La librairie de manipulation de matrices de Brandon Jones -->
    <script type="text/javascript" src="script/gl-matrix-min.js"></script>
    <!-- Notre librairie d'animation et d'aide WebGL -->
    <script type="text/javascript" src="script/WebGL_2.js"></script>
    <script type="text/javascript">
      // La classe responsable de dessiner notre monde.
      function Theater(canvasElement) {
        // Création de notre classe d'aide.
        this.gl = new WebGL(canvasElement);
 
        // Les autres membres.
        this.refBuffers = {};
        this.spiralBuffers = {};
        this.spiralPositions = [];
        this.angle = 0.0;
 
        // Initialisation des shaders.
        this.gl.setShaderProgram("VARYING_COLOR");
 
        // Définition de la couleur de fond.
        this.gl.setClearColor(0.0, 0.0, 0.0, 1);
 
        // Initialisations des objets composant la scène.
        this.initRefBuffers();
        this.initSpiralBuffers();
        this.initSpiralPositions();
 
        // Positionnement initial de la caméra.
        this.gl.setInitialCameraPosition(0, 0, 50, 0, 0, 0);
        this.gl.resetCameraPosition();
 
        // Activation de la caméra.
        this.gl.activateCameraTracking();
 
        // Mise en place de la scène.
        var that = this;
        this.gl.setStage(function () {
          // Calcul du nouvel angle.
          var angularVelocity = Math.PI / 4; // radians / second
          var angleEachFrame = angularVelocity * that.gl.getTimeInterval() / 1000;
          that.angle += angleEachFrame;
 
          that.stage();
        });
 
        // Lancement de l'animation.
        this.gl.startAnimation();
      }
 
      // Définition de l'objet référentiel type.
      Theater.prototype.initRefBuffers = function () {
        // Les points des axes.
        this.refBuffers.positionBuffer = this.gl.createArrayBuffer([
          -10,   0,   0, 10,  0,  0, // X
            0, -10,   0,  0, 10,  0, // Y
            0,   0, -10,  0,  0, 10  // Z
        ]);
 
        // Les couleurs des extrémités des axes.
        var colors = [
          [1.0, 0.0, 1.0, 1.0], // X - Rose
          [1.0, 0.0, 0.0, 1.0], // X - Red
          [1.0, 1.0, 0.0, 1.0], // Y - Jaune
          [0.0, 1.0, 0.0, 1.0], // Y - Green
          [0.0, 1.0, 1.0, 1.0], // Z - Turquoise
          [0.0, 0.0, 1.0, 1.0]  // Z - Blue
        ];
        var colorVertices = [];
        for (var n in colors) {
          var color = colors[n];
          colorVertices = colorVertices.concat(color);
        }
        this.refBuffers.colorBuffer = this.gl.createArrayBuffer(colorVertices);
      }
 
      // Définition de l'objet spirale type.
      Theater.prototype.initSpiralBuffers = function () {
        // Les points de la spirale.
        var points = [];
        var x = -10.0;
        var y = -10.0
        var z = 0.0;
        var teta = 0.0;
        while (y <= 10.0) {
          points = points.concat(x);
          points = points.concat(y);
          points = points.concat(z);
 
          teta += Math.PI / 32;
          x = -10 * Math.cos(teta);
          y += 0.125;
          z = 10 * Math.sin(teta);
        }
        this.spiralBuffers.positionBuffer = this.gl.createArrayBuffer(points);
 
        // Les couleurs des points de la spirale.
        var colorVertices = [];
        for (var i = 0; i < this.spiralBuffers.positionBuffer.numElements; i++) {
          colorVertices = colorVertices.concat([1.0, 1.0, 1.0, 1.0]);
        }
        this.spiralBuffers.colorBuffer = this.gl.createArrayBuffer(colorVertices);
      }
 
      // Position et nature des spirales.
      Theater.prototype.initSpiralPositions = function () {
        // La première.
        var spiralPos1 = {};
        spiralPos1.isDot = false;
        spiralPos1.x = -12;
        spiralPos1.y = 0;
        spiralPos1.z = 0;
        spiralPos1.rotationX = 0.2;
        spiralPos1.rotationY = 0;
        spiralPos1.rotationZ = 0;
        this.spiralPositions.push(spiralPos1);
        // La seconde.
        var spiralPos2 = {};
        spiralPos2.isDot = true;
        spiralPos2.x = 12;
        spiralPos2.y = 0;
        spiralPos2.z = 0;
        spiralPos2.rotationX = 0.2;
        spiralPos2.rotationY = 0;
        spiralPos2.rotationZ = 0;
        this.spiralPositions.push(spiralPos2);
      }
 
      Theater.prototype.drawSpirals = function () {
        for (var n = 0; n < this.spiralPositions.length; n++) {
          // Sauvegarde de la matrice courante.
          this.gl.save();
          // Application de la position particulière.
          var spiralPos = this.spiralPositions[n];
          this.gl.translate(spiralPos.x, spiralPos.y, spiralPos.z);
          this.gl.rotate(spiralPos.rotationX, 1, 0, 0);
          this.gl.rotate(spiralPos.rotationY, 0, 1, 0);
          this.gl.rotate(spiralPos.rotationZ, 0, 0, 1);
          // Application de l'animation.
          if (spiralPos.isDot) {
            this.gl.rotate(-this.angle, 0, 1, 0);
          } else {
            this.gl.rotate(this.angle, 0, 1, 0);
          }
          // Dessin du référentiel.
          this.gl.pushVaryingColorBuffer(this.refBuffers);
          this.gl.pushPositionBuffer(this.refBuffers);
          this.gl.drawLineArrays(this.refBuffers);
          // Dessin de la spirale.
          this.gl.pushVaryingColorBuffer(this.spiralBuffers);
          this.gl.pushPositionBuffer(this.spiralBuffers);
          if (spiralPos.isDot) {
            this.gl.drawPointArrays(this.spiralBuffers);
          } else {
            this.gl.drawLineStripArrays(this.spiralBuffers);
          }
          // Restauration de la matrice initiale.
          this.gl.restore();
        }
      }
 
      Theater.prototype.stage = function () {
        // Effacement de l'image précédente.
        this.gl.clear();
 
        // Initialisation des matrices (caméra appliquée).
        this.gl.initializeMatrix();
 
        // Définition de la perspective.
        // "Ouverture" de l'objectif, 45°.
        // Zone visible, 0,1 à 100.
        this.gl.perspective(45, 0.1, 100.0);
 
        // Dessin des spirales.
        this.drawSpirals();
      }
 
      function OnGLCanvasCreated(canvasElement, elementId) {
        window.setTimeout(function () { new Theater(canvasElement); }, 500);
      }
 
      function OnGLCanvasFailed(canvasElement, elementId) {
        alert("Votre butineur ne supporte pas WebGL !");
      }
    </script>
  </head>
  <body>
    <script id="WebGLCanvasCreationScript" type="text/javascript" width="800" height="500" style="border:1px solid black;">
      WebGLHelper.CreateGLCanvasInline('glCanvas', OnGLCanvasCreated, OnGLCanvasFailed)
    </script>
  </body>
</html>

Si vous visualiser cette nouvelle version de la page, vous ne devriez pas sentir une grosse différence. Pourtant notre spirale en traits comporte deux fois plus de traits, elle est donc plus "lisse" que la version précédente.

Passons maintenant à de vraies modifications. Nous allons nous attaquer aux deux premières tâches de la liste que nous avons établi au début de ce billet.
Nous allons positionner un certain nombre de rectangles dans notre scène. Ces rectangles en sont vraiment, ils sont donc en 2D.
Attention, pour que la suite du code fonctionne, il est nécessaire de placer le rectangle type dans le plan XY et que son "centre" soit à l'origine (au point de coordonnées 0,0,0). Si ce n'est pas le cas, il faudra ajouter les corrections nécessaires pour se ramener à cette situation.
Pour afficher un rectangle, il faut deux triangles. On les colle ensemble, diagonale sur diagonale, et le tour est joué.
Là encore il s'agit d'un grand classique et WebGL sait gérer ce genre de chose comme un grand. Si la diagonale est commune, pourquoi définir deux fois les points qui la composent ?
Il n'y a effectivement aucune raison de le faire.
Comme nous allons dessiner des panneaux de différentes tailles à partir du même objet type, nous avons besoin de disposer d'une fonction réalisant une mise à l'échelle.
Pour tout cela, nous ajoutons à notre classe WebGLle code correspondant :

WebGL.prototype.scale = function (ratio) {
  this.mat4.scale(this.mvMatrix, [ratio, ratio, ratio]);
};
 
WebGL.prototype.drawTriangleStripArrays = function (buffers) {
  this.setMatrixUniforms();
  this.context.drawArrays(this.context.TRIANGLE_STRIP, 0, buffers.positionBuffer.numElements / 3);
};
 
WebGL.prototype.drawTriangleFanArrays = function (buffers) {
  this.setMatrixUniforms();
  this.context.drawArrays(this.context.TRIANGLE_FAN, 0, buffers.positionBuffer.numElements / 3);
};
 
WebGL.prototype.drawTriangleStripElements = function (buffers) {
  this.setMatrixUniforms();
  this.context.drawElements(this.context.TRIANGLE_STRIP, buffers.indexBuffer.numElements, this.context.UNSIGNED_SHORT, 0);
};
 
WebGL.prototype.drawTriangleFanElements = function (buffers) {
  this.setMatrixUniforms();
  this.context.drawElements(this.context.TRIANGLE_FAN, buffers.indexBuffer.numElements, this.context.UNSIGNED_SHORT, 0);
};

Passons maintenant à l'ajout de nos rectangles. Comme ils nous servirons à afficher des informations, nous devrions les appeller "panneau d'affichage", soit "billboard" en anglais, mais par facilité nous dirons simplement "panneau".
Pour préparer le futur nous allons en mettre sur les axes et dans la spirale en points. Nous n'allons toutefois pas les afficher exactement de la même façon. Ceux des axes auront la base "posée" sur l'axe, ceux de la spirale en points seront centrés sur chaque point.
Pour faire cela, nous avons besoin de définir ce qu'est un panneau, de savoir où les dessiner et bien évidement de les dessiner.
Nous faisons toutes ces modifications dans une nouvelle page, WebGL_14.htm, dont le code contient les modifications suivantes (par rapport à celui de WebGL_13.htm) :

function Theater(canvasElement) {
  ...
  this.refPoints = [];
  this.spiralPoints = [];
  this.billboardBuffers = {};
  ...
  this.initBillboardBuffers();
  ...
}
 
Theater.prototype.initRefBuffers = function () {
  // Les points des axes.
  this.refPoints = [
    -10,   0,   0, 10,  0,  0, // X
      0, -10,   0,  0, 10,  0, // Y
      0,   0, -10,  0,  0, 10  // Z
  ];
  this.refBuffers.positionBuffer = this.gl.createArrayBuffer(this.refPoints);
  ...
}
 
Theater.prototype.initSpiralBuffers = function () {
  // Les points de la spirale.
  var x = -10.0;
  var y = -10.0
  var z = 0.0;
  var teta = 0.0;
  while (y <= 10.0) {
    this.spiralPoints = this.spiralPoints.concat(x);
    this.spiralPoints = this.spiralPoints.concat(y);
    this.spiralPoints = this.spiralPoints.concat(z);
 
    teta += Math.PI / 32;
    x = -10 * Math.cos(teta);
    y += 0.125;
    z = 10 * Math.sin(teta);
  }
  this.spiralBuffers.positionBuffer = this.gl.createArrayBuffer(this.spiralPoints);
  ...
}
 
Theater.prototype.initBillboardBuffers = function () {
  // Deux triangles définissent un panneau d'affichage.
  this.billboardBuffers.positionBuffer = this.gl.createArrayBuffer([
    -1, -1, 0, 1, 1, 0, -1, 1, 0,
    -1, -1, 0, 1, -1, 0,  1, 1, 0
  ]);
 
  // Les couleurs des points du panneau d'affichage.
  var colorVertices = [];
  for (var i = 0; i < this.billboardBuffers.positionBuffer.numElements; i++) {
    colorVertices = colorVertices.concat([0.7450980392, 0.7450980392, 0.7450980392, 1]); // Grey
  }
  this.billboardBuffers.colorBuffer = this.gl.createArrayBuffer(colorVertices);
}
 
Theater.prototype.drawAxisBillboards = function (points) {
  // On dessine un panneau à chaque point.
  var i = 0;
  while (i < points.length) {
    // Sauvegarde de la matrice courante.
    this.gl.save();
    // On se positionne sur le point.
    // Le panneau devant reposer sur le point, on doit décaler sa position.
    this.gl.translate(points[i], points[i + 1] + 1, points[i + 2]);
    i += 3;
    // On dessine le panneau.
    this.gl.pushVaryingColorBuffer(this.billboardBuffers);
    this.gl.pushPositionBuffer(this.billboardBuffers);
    this.gl.drawTriangleStripArrays(this.billboardBuffers);
    // Restauration de la matrice initiale.
    this.gl.restore();
  }
}
 
Theater.prototype.drawPointBillboards = function (points) {
  // On dessine un panneau à chaque point.
  var i = 0;
  while (i < points.length) {
    // Sauvegarde de la matrice courante.
    this.gl.save();
    // On se positionne sur le point.
    // Le panneau devant être centré sur le point, c'est bon.
    this.gl.translate(points[i], points[i + 1], points[i + 2]);
    i += 3;
    // Les panneaux doivent être mis à l'echelle.
    this.gl.scale(0.25);
    // On dessine le panneau.
    this.gl.pushVaryingColorBuffer(this.billboardBuffers);
    this.gl.pushPositionBuffer(this.billboardBuffers);
    this.gl.drawTriangleStripArrays(this.billboardBuffers);
    // Restauration de la matrice initiale.
    this.gl.restore();
  }
}
 
Theater.prototype.drawSpirals = function () {
  for (var n = 0; n < this.spiralPositions.length; n++) {
    // Sauvegarde de la matrice courante.
    this.gl.save();
    // Application de la position particulière.
    var spiralPos = this.spiralPositions[n];
    this.gl.translate(spiralPos.x, spiralPos.y, spiralPos.z);
    this.gl.rotate(spiralPos.rotationX, 1, 0, 0);
    this.gl.rotate(spiralPos.rotationY, 0, 1, 0);
    this.gl.rotate(spiralPos.rotationZ, 0, 0, 1);
    // Application de l'animation.
    if (spiralPos.isDot) {
      this.gl.rotate(-this.angle, 0, 1, 0);
    } else {
      this.gl.rotate(this.angle, 0, 1, 0);
    }
    // Dessin du référentiel.
    this.gl.pushVaryingColorBuffer(this.refBuffers);
    this.gl.pushPositionBuffer(this.refBuffers);
    this.gl.drawLineArrays(this.refBuffers);
    // Dessin de la spirale.
    this.gl.pushVaryingColorBuffer(this.spiralBuffers);
    this.gl.pushPositionBuffer(this.spiralBuffers);
    if (spiralPos.isDot) {
      this.gl.drawPointArrays(this.spiralBuffers);
      // Dessin des panneaux d'affichage sur les points de la spirale.
      this.drawPointBillboards(this.spiralPoints);
    } else {
      this.gl.drawLineStripArrays(this.spiralBuffers);
    }
    // Dessin des panneaux d'affichage aux extrémités des axes.
    this.drawAxisBillboards(this.refPoints);
    // Restauration de la matrice initiale.
    this.gl.restore();
  }
}

Si vous visualisez cette page vous devez voir quelque chose comme :
HTML5 08 01
Nous avons bien nos panneaux. Il y a toutefois un détail qui cloche. Lorsque la spirale tourne, les panneaux tournent avec elle. Jusque-là la chose est normale. Le problème c'est que ce mouvement n'étant pas compensé par une rotation des panneaux, ils ne restent pas face à nous (face à la caméra en fait). On obtient alors quelque chose comme :
HTML5 08 02
Imaginez que ces panneaux soient là pour nous renseigner. Dans le cas de la seconde capture, il serait assez compliqué de lire ce qu'il y aurait d'indiqué sur les panneaux. Il serait plus intéressant qu'ils restent toujours face à nous. C'est ce que l'on appelle, de manière classique en 3D, des "panneaux d'affichage", "billboard" en Anglais pour ceux qui ont suivis. Nous avons déjà le nom, il ne reste plus qu'à coder la chose.

Si vous lancez votre moteur de recherche favori sur la toile, il risque de vous remonter principalement deux liens :

Si vous êtes anglophone, c'est tout bon. Si vous ne l'êtes pas, c'est un poil plus compliqué. Heureusement il y a ce billet.

En gros il convient de se poser trois questions :

  • le panneau doit-il toujours faire face à sa cible, même lorsque cette dernière prend de l'altitude ? Dit de manière plus géométrique, le panneau tourne-t-il sur un point ou sur un axe ? S'il s'agit d'un axe, est-ce un axe particulier de la scène ? Au hasard, ne serait-ce pas l’axe vertical (Y) ?
  • tous les panneaux de la scène ont-ils la même orientation ? Là encore une formulation plus géométrique, leur perpendiculaire est-elle parallèle à un vecteur particulier. Au hasard, l'axe de la caméra ? Si ce n’est pas le cas, ils regardent vraiment tous le même point. Conséquence, il faut calculer individuellement leur orientation ;
  • regardent-ils la caméra ou autre chose ? Si vous vous demandez à quoi peut servir le fait de faire regarder autre chose aux panneaux, sachez qu'il y a plein de cas où cela est fort utile, que ce soit dans un jeu ou dans une cinématique. Dans un jeu, cela permet aux panneaux de suivre une cible. Remplacez panneaux par tour de défense, vous devez commencer à comprendre. Dans une cinématique cela peut, par exemple, permettre à tout le monde regarde le héros se déplacer dans la rue.

Compte tenu de notre objectif, nous allons déjà répondre de manière définitive à la troisième question. La cible des panneaux est bien la caméra.
Nous pouvons également affirmer qu'une rotation au tour d'un axe ne nous intéresse pas. L'utilisateur peut réaliser tous les mouvements qu'il souhaite, à tout moment il doit voir de face les panneaux qui sont dans son champ de vision.
Nous pourrions également répondre à la seconde question, mais la réponse pouvant avoir un fort impact sur le temps de calcul, il peut être intéressant d'y regarder de plus prêt.

Pour conserver une bonne encapsulation, nous allons créer des méthodes "xxxBillboard" sur notre classe WebGL. On pourrait souhaiter les placer dans la classe mat4. Le problème réside alors dans le fait que mat4 n'est pas à nous. Conséquence, cela imposerait de toujours modifier les scripts gl-matrix.js et gl-matrix-min.js lors de chaque récupération sur le web (sauf à faire intégrer la chose dans "glMatrix").

Commençons par le code le plus simple qui correspond à "Cheating - Fast and Easy Spherical Billboards" dans OpenGL @ Lighthouse 3D - Billboarding Tutorial. Nous ajoutons une méthode à la classe WebGL que nous nommons globalSphericalBillboard. Global, car elle ne tient pas compte des positions exacts de l'objet et de la caméra, mais juste de l'orientation de la caméra. En fait les axes de l'objet sont alignés sur ceux de la caméra. Son code est le suivant :

WebGL.prototype.globalSphericalBillboard = function () {
  this.mvMatrix[0] = 1.0;
  this.mvMatrix[1] = 0;
  this.mvMatrix[2] = 0;
  this.mvMatrix[4] = 0;
  this.mvMatrix[5] = 1.0;
  this.mvMatrix[6] = 0;
  this.mvMatrix[8] = 0;
  this.mvMatrix[9] = 0;
  this.mvMatrix[10] = 1.0;
};

Pour bien visualiser les différences, nous allons ajouter une nouvelle page pour chaque version de "billboard". Nous inaugurons la série avec WebGL_15.htm. Il suffit d'ajouter un appel à globalSphericalBillboard dans la méthode drawPointBillboards de notre page.
Attention tout de même, comme nous tapons un poil violement dans la matrice mvMatrix, toutes les mises à l'échelle seront perdues. Dans notre cas il n'y a donc qu'un emplacement possible pour l'appel, juste avant la mise à l'échelle car il ne vous a pas échappé que le faire avant la translation aurait un effet négatif quant au bon positionnement des panneaux.
Si vous visualisez la page, la différence entre les panneaux des axes et ceux de la spirale ne devrait pas vous échapper :
HTML5 08 03
Même si nous avons dit que la solution ne nous intéressait pas, regardons ce que donne le mode "cylindrique". La méthode à ajouter à la classe WebGL s'appelle globalCylindricalBillboard, et son code est le suivant :

WebGL.prototype.globalCylindricalBillboard = function () {
  this.mvMatrix[0] = 1.0;
  this.mvMatrix[1] = 0;
  this.mvMatrix[2] = 0;
  this.mvMatrix[8] = 0;
  this.mvMatrix[9] = 0;
  this.mvMatrix[10] = 1.0;
};

Toujours dans la page WebGL_15.htm, nous utilisons cette nouvelle version dans drawAxisBillboards. A l'écran cela donne :
HTML5 08 04
A ce stade on peut se demander qu'elle est la différence. Pour percevoir cette différence il faut prendre de l'altitude. Pour garantir l'effet, faites pivoter les spirales pour les voir du dessus. Vous devez alors voir quelque chose comme :
HTML5 08 05
Cette capture montre bien que les panneaux de la spirale regardent toujours vers la caméra mais pas ceux des axes. Cela est dû au fait que les panneaux des axes conservent toujours leur axes Y parallèle à celui de l'espace WebGL (nous avons laissé dans la matrice la transformation de cet axe). En conséquence, si l'axe de la caméra est proche de l'axe Y de l'espace WebGL, les panneaux ne sont plus visible que par leur tranche.

Maintenant en quoi la gestion de ces panneaux, surtout les sphériques, peut-elle être améliorée ?
Si les méthodes comportent le mot "global" ce n'est pas pour rien. Tous les panneaux regardent exactement dans la même direction. Cette direction est définie par l'axe de la caméra, c'est son inverse. Evidemment, dans le cas de globalCylindricalBillboard il y a une contrainte supplémentaire qui limite l'alignement.
De loin, tout va bien. De prêt, la chose peut parfois poser un problème. Sur la capture suivante, on peut constater que les panneaux ne nous font pas vraiment face, ils regardent derrière nous :
HTML5 08 06
Dans notre cas, cela n'est pas forcement gênant pour les panneaux des axes. En revanche, pour les panneaux de la spirale, cela le deviendrait (l’explication dans le prochain billet).
La solution consiste alors à calculer pour chaque panneau la vraie direction dans laquelle il doit regarder. L'article NeHe Productions: Billboarding How To explique très bien la chose et nous allons faire comme il l'indique.
Nous allons cependant tenir compte de quelques particularités de notre situation pour simplifier le code. A l'arrivée le code n'est pas très compréhensible car il ne présente pas les calculs complets. En effet, il se trouve que nous avons pas mal de 0 dans nos vecteurs et que l'on peut en profiter pour simplifier, et surtout accélérer, les calculs.
Si vous prenez l'article NeHe Productions: Billboarding How To, et que vous tenez compte du fait que notre matrice mvMatrixest déjà en coordonnées caméra lorsque nous intervenons (ce qui simplifie notablement le vecteur up), vous n'aurez aucune difficulté à retrouver le code proposé :

WebGL.prototype.sphericalBillboard = function () {
  var up = this.vec3.create([0, 1, 0]);
 
  var look = this.vec3.create([-this.mvMatrix[12], -this.mvMatrix[13], -this.mvMatrix[14]]);
  this.vec3.normalize(look);
 
  var right = this.vec3.create();
  this.vec3.cross(up, look, right);
 
  this.mvMatrix[0] = right[0];
  this.mvMatrix[1] = 0;
  this.mvMatrix[2] = right[2];
  this.mvMatrix[4] = 0;
  this.mvMatrix[5] = 1;
  this.mvMatrix[6] = 0;
  this.mvMatrix[8] = look[0];
  this.mvMatrix[9] = look[1];
  this.mvMatrix[10] = look[2];
};
 
WebGL.prototype.cylindricalBillboard = function () {
  var up = this.vec3.create([0, 1, 0]);
 
  var look = this.vec3.create([-this.mvMatrix[12], -this.mvMatrix[13], -this.mvMatrix[14]]);
  this.vec3.normalize(look);
 
  var right = this.vec3.create();
  this.vec3.cross(up, look, right);
 
  this.mvMatrix[0] = right[0];
  this.mvMatrix[1] = 0;
  this.mvMatrix[2] = right[2];
  this.mvMatrix[8] = look[0];
  this.mvMatrix[9] = look[1];
  this.mvMatrix[10] = look[2];
};

Créons une nouvelle page HTML, WebGL_16.htm, qui reprend exactement le code de WebGL_15.htm mais utilise les nouvelles versions "xxxBillboard". A l'écran cela donne :
HTML5 08 07
Lorsque nous nous approchons, nous pouvons voir les panneaux pivoter individuellement pour nous faire face.

Nous avons maintenant des panneaux adaptés à nos besoins. Si vous souhaitez être complet, il vous faudra ajouter les panneaux cylindriques dont l'axe de symétrie n'est pas parallèle à l'axe Y de l'espace WebGL. L'article NeHe Productions: Billboarding How To nous a donné tout ce qui est nécessaire de savoir. Il suffit d'intégrer le vecteur up, qui devra être fourni par l'appelant, et donc enlever les optimisations liées au fait que nous étions parallèle à Y.

Dans le prochain billet nous utiliserons nos panneaux pour réaliser les deux derniers points de la liste établie au début de ce billet.

Si vous souhaitez récupérer le code à jour, c'est ici

La suite au prochain numéro.

Publicité
Publicité
Commentaires
Fanfois et ses papillons
Publicité
Archives
Publicité