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

HTML5 09 (tutorial WebGL 8ème partie)

Dans le précédent billet, nous avons ajouté à notre panoplie la fonctionnalité "billboard", "panneau d'affichage" en Français (mais nous dirons simplement "panneau"). Si la fonctionnalité est disponible, on ne peut pas dire que nous en ayons fait un usage quelconque. Dans ce billet nous allons voir deux utilisations des panneaux.

Pour commencer nous allons utiliser les panneaux pour résoudre notre problème d'absence de zoom sur les points de la spirale en points.
Concrètement, le problème se traduit par l'absence d'indication de proximité associée aux points que nous dessinons. Les points sont des points, des pixels donc, et ils le restent que l'on soit près ou loin d'eux.
Si cela n'a rien d'étonnant, nous aimerions bien un comportement différent.
De loin, les points sont des points. De prêt, s'ils pouvaient devenir des gros points...
La première idée consiste à ce dire qu'un point est une sphère. Si l'on remplace les points par des sphères, on obtient exactement le résultat souhaité.
Le seul petit problème réside dans le fait qu'une sphère est un objet dont la définition requière un nombre relativement important de triangles (de faces si vous préférez).
On peut alors se dire que remplacer les sphères par des cercles toujours présentés face à la caméra est moins couteux. En fait, LA différence sera liée à la gestion de la lumière. Cette gestion est possible avec une sphère, pas vraiment avec un cercle. Cela dit, dans notre cas, la gestion de la lumière n'est pas exactement une priorité. Nous allons donc afficher un cercle en lieu et place de chaque point de la spirale en points.

En fait, tout semble déjà fait. Il suffit de ne plus afficher les points et de plaquer une texture représentant un cercle sur les bons panneaux.
Il y a cependant plusieurs problèmes à résoudre :

  1. comment afficher d'un côté des traits et de l'autre des panneaux avec une texture ? Les shaders sont différents ;
  2. Nos panneaux sont des rectangles (des carrés même). Comment faire pour que l'utilisateur voit bien des cercles et ne soit pas gêné par les coins des panneaux ?

Nous devons impérativement répondre à la première question avant de pouvoir envisager de répondre à la seconde.
La réponse est à la fois très simple et un poil compliquée à mettre en œuvre de manière transparente.
Très simple, car il suffit d'avoir plusieurs programmes, un par couple de shaders. Il "suffit" de sélectionner le bon avant de dessiner et le tour est joué.
Là où les choses se corsent, c'est que notre classe WebGL a vocation à masquer ce détail. Comment à la fois masquer et expliciter la chose.
La solution retenue consiste à retourner à l'appelant le programme une fois ce dernier compilé. Le code des méthodes setShaderProgram et setCustomShaderProgram est donc simplement complété par un return this.shaderProgram; final.
Pour une utilisation classique, rien ne change.
Pour une utilisation avec plusieurs programmes, l'appelant peut les mémoriser. Il lui suffit ensuite d'utiliser la nouvelle méthode useShaderProgrampour définir ponctuellement quel programme doit être mis en œuvre. Le code de cette méthode n'est pas très compliqué :

WebGL.prototype.useShaderProgram = function (shaderProgram) {
  this.shaderProgram = shaderProgram;
  this.context.useProgram(this.shaderProgram);
}

Nous pouvons maintenant créer une nouvelle page, WebGL_17.htm, qui reprend le code de la page WebGL_16.htm mais crée deux programmes shaders. Pour cela il suffit de remplacer la ligne this.gl.setShaderProgram("VARYING_COLOR");par les deux lignes suivantes :

this.textureProgram = this.gl.setShaderProgram("TEXTURE");
this.colorProgram = this.gl.setShaderProgram("VARYING_COLOR");

Le programme shader courant à l'issue de l'exécution de ces deux lignes est le second. Si nous affichons la nouvelle page maintenant, elle se comporte donc exactement comme la page WebGL_16.htm. Il nous faut maintenant appliquer une texture sur les panneaux correspondant aux points de la spirale. Pour cela nous devons commencer par définir une texture.

La première étape consiste à avoir une image que l'on utilise ensuite comme texture. Pour commencer nous, allons utiliser une image "png". Pourquoi "png" et pas "jpg" ? Parce que nous avons besoin de disposer d'un canal alpha. L'image en elle-même est très simple, un simple cercle plein. Attention, ce qui n'est pas le cercle doit être transparent (canal alpha à 0), alors que le cercle doit être totalement opaque (canal alpha à 1). Dans le rar lié à ce billet vous trouverez la magnifique image "circle.png" dans le répertoire "image". C'est elle qui illumine, le mot n'est pas trop fort, les captures d'écran à venir.
Pour la charger nous utilisons un tag HTML "img" créé dynamiquement. A ce stade il y a deux écoles. Attendre que l'image soit chargée avant de commencer la visualisation de la scène 3D, ou commencer à visualiser la scène 3D immédiatement. Dans la seconde solution, la texture n'apparait qu'en cours de route, mais la visualisation est immédiatement possible.
Dans notre cas, nous attendrons que l'image soit chargée avant de visualiser la scène. Si la chose ne vous convient pas, à vous de modifier le code de la page.
La partie impactée par le chargement de l'image se trouve à la fin du "constructeur" de notre classe Theater (à la place de this.gl.startAnimation();). La nouvelle version est :

function Theater(canvasElement) {
  ...
 
  this.textures = {};
  var sources = {
    circle: "image/circle.png"
  };
 
  this.loadTextures(sources, function () {
    // Lancement de l'animation.
    that.gl.startAnimation();
  });
}
 
Theater.prototype.loadTextures = function (sources, callback) {
  var gl = this.gl;
  var context = gl.getContext();
  var textures = this.textures;
  var loadedImages = 0;
  var numImages = 0;
  for (var src in sources) {
    // anonymous function to induce scope
    (function () {
      var key = src;
      numImages++;
      textures[key] = context.createTexture();
      textures[key].image = new Image();
      textures[key].image.onload = function () {
        gl.initTexture(textures[key]);
        if (++loadedImages >= numImages) {
          callback();
        }
      };
 
      textures[key].image.src = sources[key];
    })();
  }
};

Si vous avez lu HTML5 Canvas Cookbook, ce code doit fortement vous rappeler quelque chose.
Maintenant que nous avons notre texture, nous pouvons la plaquer sur les bons panneaux. Pour cela nous devons "améliorer" la définition des panneaux en ajoutant à la fin de la méthode initBillboardBuffers:

this.billboardBuffers.normalBuffer = this.gl.createArrayBuffer([
  0, 0, 1,
  0, 0, 1,
  0, 0, 1,
  0, 0, 1
]);
 
this.billboardBuffers.textureBuffer = this.gl.createArrayBuffer([
  0, 0,
  0, 1,
  1, 0,
  1, 1
]);

Pour finir nous devons appliquer la texture dans la méthode drawPointBillboards. Cela nécessite de changer le programme shader. Le code complet de la méthode est :

Theater.prototype.drawPointBillboards = function (points) {
  // On positionne le programme shader des textures.
  this.gl.useShaderProgram(this.textureProgram);
  // Comme on dessine toujours la même chose, on peut charger hors de la boucle.
  this.gl.pushPositionBuffer(this.billboardBuffers);
  this.gl.pushTextureBuffer(this.billboardBuffers, this.textures["circle"]);
  // 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;
    // Mise face caméra.
    this.gl.sphericalBillboard();
    // Les panneaux doivent être mis à l'echelle.
    this.gl.scale(0.25);
    // On dessine le panneau.
    this.gl.drawTriangleStripArrays(this.billboardBuffers);
    // Restauration de la matrice initiale.
    this.gl.restore();
  }
  // On rétablit le programme shader des couleurs.
  this.gl.useShaderProgram(this.colorProgram);
}

Si vous afficher cette page vous devez voir quelque chose comme (cliquez sur l'image pour l'agrandir) :
HTML5 09 01
De loin on peut y croire, de prêt on n'y croit plus. En effet les zones normalement transparentes et donc invisibles, sont malheureusement visibles. Pourquoi ?

Parce que la gestion du canal alpha n'est pas bonne. En fait elle est inexistante, car nous ne l'avons pas définie. En effet la gestion de la profondeur et la gestion du canal alpha doivent être explicitement activées, et éventuellement configurées, dans le cas où la configuration par défaut ne convient pas.
En ce qui concerne la gestion de la profondeur, nous l'avons activée à la fin du constructeur de la "classe" WebGL. C'est la ligne de code this.context.enable(this.context.DEPTH_TEST); qui réalise la chose. En ce qui concerne la prise en compte du canal alpha, nous n'avons rien définie.
Pour voir à quoi conduit la prise en compte du canal alpha, nous allons désactiver la gestion de la profondeur. Nous éviterons ainsi de cumuler les deux effets et auront la certitude que ce que nous voyons est bien dû à la prise en compte du canal alpha.
Pour cela nous ne touchons pas à la "classe" WebGL, nous manipulons directement WebGL depuis notre page HTML. Nous ajoutons donc, au début du "constructeur" de la "classe" Theater, juste après la première ligne, le code suivant :

var context = this.gl.getContext();
context.disable(context.DEPTH_TEST);
context.enable(context.BLEND);
context.blendFunc(context.SRC_ALPHA, context.ONE);

Que fait ce code ?
Il supprime la gestion de la profondeur et active le "blend" (mélange en Français).
Qu'est-ce que le "blend" ?
Le principe du blend est fort simple. Chaque pixel possède une couleur (R, G, B, A). Lorsque l'on veut dessiner un pixel, on le dessine forcement sur un pixel existant (si rien n'a été dessiné, tous les pixels existants sont à la couleur de fond). Le blend permet de définir quel sera la couleur finale du pixel compte tenu de la couleur courante (destination, dans la documentation WebGL) et de la couleur que l'on veut appliquer (source, dans la dite documentation).
Dans notre cas nous définissons la règle à appliquer dans la ligne de code context.blendFunc(context.SRC_ALPHA, context.ONE);.
Le premier paramètre indique que l'on applique à la couleur source la valeur de son canal alpha. Dans notre cas elle ne prend que deux valeurs, 1 au niveau du cercle, 0 hors du cercle. En conséquence la valeur RGB de la source est, soit sa valeur initiale (lorsque alpha est à 1) soit rien (lorsque alpha est à 0).
Au niveau de la destination, le second paramètre indique que la couleur est inchangée.
Dit autrement, on efface tous les éléments de l'image que nous voulons dessiner dont le canal alpha est à 0.
Le résultat est le suivant (cliquez sur l'image pour l'agrandir) :
HTML5 09 02
Si vous regardez les panneaux se trouvant devant les "points", vous constaterez que l'on voit à travers. Ce n'est pas vraiment l'effet recherché. Pour ceux qui auraient un doute, les panneaux des axes sont les derniers éléments dessinés de la scène.
Un tour sur la toile et il apparait que le second paramètre employé n'est pas le bon. Il faut utiliser ONE_MINUS_SRC_ALPHA et non pas ONE.
On obtient alors (cliquez sur l'image pour l'agrandir) :
HTML5 09 03
Le résultat est maintenant conforme à ce qui est attendu compte tenu du fait que la pronfondeur n'est pas gérée. Ceci explique que les panneaux des axes, qui sont dessinés en dernier, soient devant les "point", même lorsqu'il devraient être derrière.
Réintroduisons la gestion de la profondeur, pour que tout rentre dans l'ordre. On obtient alors le résultat suivant (cliquez sur l'image pour l'agrandir) :
HTML5 09 04
Même de loin il semble y avoir un problème. En se rapprochant, aucun doute il y a un problème :
HTML5 09 05

D'où vient le problème ?
Le problème survient lorsque les éléments sont dessinés de l'avant vers l'arrière. La gestion de la profondeur dit qu'il y a quelque chose devant. Comme il y a quelque chose devant, on ne dessine pas ce qui est derrière. Manque de chance, ce qui est devant est la partie transparente de nos "points". Résultat on voit, "bêtement", apparaître le fond présent au moment où le devant a été dessiné.
Comment remédier à ce problème ?
La première solution consiste à dessiner les éléments dans le bon ordre. Donc en premier ce qui est au fond. Là où la chose se corse c'est que la notion de "au fond" est à considérer par rapport à la caméra. Dit autrement, il faut entièrement calculer la position de tous les éléments par rapport à la caméra, les trier en fonction de leur profondeur, à ne pas confondre avec leur éloignement, et les dessiner du plus au fond vers le plus devant.
Si cette solution fonctionne dans tous les cas de figure, elle n'est pas simple à mettre en œuvre. Heureusement, il y a une autre solution.
La seconde solution consiste à ne pas prendre en compte, au niveau du tampon de profondeur, les pixels ayant un alpha sinon à 0 du moins inférieur à un seuil. Cette solution ne fonctionne pas forcement dans les cas intermédiaires (difficulté à définir le "bon" seuil), elle est par contre parfaitement adaptée à notre cas.
Comment peut-on obtenir ce résultat ?

On ne peut obtenir ce résultat qu'en modifiant le code du fragment shader. Il nous faut tester le canal alpha du point et s'il a une valeur qui répond aux conditions que nous fixons, nous ne le traitons pas. La solution est radicale, si nous l'éliminons au niveau du fragment shader c'est comme si le point n'existait pas dans la texture.
Coder un seuil "en dur" est assez limitant. Il nous faut pouvoir modifier la valeur. Il serait également souhaitable que notre modification soit sans effet sur l'existant. A cet effet le code du fragment shader devient :

case this.TEXTURE:
  return "#ifdef GL_ES\n" +
         "precision highp float;\n" +
         "#endif\n" +
         "uniform float uAlphaLimit;\n" +
         "varying vec2 vTextureCoord;\n" +
         "uniform sampler2D uSampler;\n" +
         "void main(void) {\n" +
         "gl_FragColor = texture2D(uSampler, vec2(vTextureCoord.s, vTextureCoord.t));\n" +
         "if (gl_FragColor.a <= uAlphaLimit)\n" +
         "  discard;\n" +
         "}";

Le code du fragment shader éliminera maintenant tous les points dont l'alpha sera inférieur ou égal au seuil. Reste à définir ce dernier.
Le seuil est intégré à la classe WebGL, avec une valeur par défaut qui garantisse aucun changement par rapport à l'existant. Pour finir, le seuil est pris en compte lors des manipulations du shader. Le code impacté est le suivant :

var WebGL = function (canvasElement) {
  ...
  this.alphaTextureLimit = -1.0;
  ...
}
 
WebGL.prototype.setAlphaTextureLimit = function (value) {
  this.alphaTextureLimit = value;
}
 
WebGL.prototype.pushTextureBuffer = function (buffers, texture) {
  ...
  this.context.uniform1f(this.shaderProgram.uAlphaLimit, this.alphaTextureLimit);
};
 
WebGL.prototype.initTextureShader = function () {
  ...
  this.shaderProgram.uAlphaLimit = this.context.getUniformLocation(this.shaderProgram, "uAlphaLimit");
};

Nous pouvons maintenant utiliser cette nouvelle possibilité. Il nous suffit de remplacer tout le code que nous avons ajouté dans la page, depuis le début de ce billet, par :

this.gl.setAlphaTextureLimit(0.5);

Le résultat est le suivant :
HTML5 09 06
Vous pouvez zoomer si vous le désirez, même de près c'est bon :
HTML5 09 07
Certains se demandent peut-être pourquoi avoir défini un seuil à 0,5. En fait l'anti aliasing des logiciels de dessin conduit généralement le bord d'une figure à être légèrement transparent. Ce seuil permet d'obtenir un bon résultat, au moins avec le "png" fournit (on peut sans problème mettre moins que 0,5).

Nous allons maintenant nous occuper des panneaux associés aux axes. Nous souhaiterions afficher le nom de l'axe et donner une indication quant à son sens.
Il y a plusieurs manières de faire cela, nous allons opter pour un affichage du genre "X" et "-X".
En répliquant ce que nous venons de faire pour les points, nous pouvons créer six "png" et les appliquer sous la forme de texture. Cela donnera un résultat correct mais n'est pas très pratique. Dans notre cas, nous savons à priori ce que nous voulons afficher, créer les png est donc possible. Ce n'est pas forcément le cas le plus général.
Pour aborder le cas général, il faut être en mesure d'afficher n'importe quel texte. Il y a alors principalement deux approches :

  1. La première, utilisée depuis des années dans les jeux, consiste à disposer d'une image contenant tous les caractères d'une police de caractères donnée. Lorsque l'on doit afficher un texte, on va chercher les caractères un par un dans l'image, et on recompose le texte final. Pour cela, on doit disposer d'une grille permettant de découper correctement l'image pour retrouver chaque caractère. Attention pouvoir découper correctement le caractère ne suffit pas, il faut aussi savoir comment utiliser le caractère lors de la recomposition du texte. Si vous coller ensemble un p et un b sans savoir que le bas du p doit être placé plus bas de n pixel que le bas du b, vous n'obtiendrez pas "pb" mais un vrai problème.
  2. La seconde, assez facile à trouver sur la toile, consiste à utiliser un canvas caché dans lequel on affiche, en 2D, le texte. Le canvas sait le faire sans problème. On utilise alors ce canvas comme image source lors de la création de la texture. C'est ce que vous pouvez voir ici ou (à priori le second reprend le premier).

Nous allons partir de WebGL text using a Canvas Texture, l'original donc, pour faire ce dont nous avons besoin et qui est légèrement différent. En effet, nous ne souhaitons pas interagir avec la page, nous souhaitons même intégrer la génération des textures dans notre classe WebGL. Première conséquence, la page HTML n'a pas à nous fournir un canvas pour la 2D, nous allons le créer dynamiquement. Nous modifions donc le code de notre classe WebGL de la manière suivante :

var WebGL = function (canvasElement) {
  ...
  // Tout le nécessaire pour la gestion des textes sous forme de textures.
  this.textureCanvas = document.createElement("canvas");
  this.textureCanvas.id = "textureCanvas";
  this.textureContext = this.textureCanvas.getContext('2d');
  // Constantes permettant de gérer le positionnement du texte.
  // Horizontalement (left (ou start), right (ou end) et center) :
  this.TEXT_LEFT = "left";
  this.TEXT_RIGHT = "right";
  this.TEXT_CENTER = "center";
  // Verticalement (bottom, ideographic, alphabetic, middle, hanging et top) :
  this.TEXT_BOTTOM = "bottom";
  this.TEXT_MIDDLE = "middle";
  this.TEXT_TOP = "top";
  // La configuration de la police de caractères.
  this.textureFontFamily = "Calibri";
  this.textureFontHeight = "60";
  this.textureTextAlign = "center";
  this.textureTextBaseline = "middle";
  this.textureTextFaceColor = "white";
  this.textureTextBackColor = "rgba(0, 0, 0, 0)";
  this.textureMaxWidth = "80";
  // Doit-on générer une texture carrée ?
  this.textureIsSquare = true;
  ...
}
 
WebGL.prototype.setTextTextureInfo = function (fontHeight, fontFamily, textAlign, textBaseline, textColour, backgroundColour, maxWidth, squareTexture) {
  this.textureFontFamily = fontFamily;
  this.textureFontHeight = fontHeight;
  this.textureTextAlign = textAlign;
  this.textureTextBaseline = textBaseline;
  this.textureTextFaceColor = textColour;
  this.textureTextBackColor = backgroundColour;
  this.textureMaxWidth = maxWidth;
  this.textureIsSquare = squareTexture;
}
 
WebGL.prototype.createTexturesFromText = function (text) {
  // Ecriture dans le canvas 2D.
  this.draw2DText(text);
 
  // Utilisation du canvas 2D pour créer une texture.
  var ctx = this.context;
  var texture = ctx.createTexture();
  ctx.pixelStorei(ctx.UNPACK_FLIP_Y_WEBGL, true);
  ctx.bindTexture(ctx.TEXTURE_2D, texture);
  ctx.texImage2D(ctx.TEXTURE_2D, 0, ctx.RGBA, ctx.RGBA, ctx.UNSIGNED_BYTE, this.textureCanvas);
  ctx.texParameteri(ctx.TEXTURE_2D, ctx.TEXTURE_MAG_FILTER, ctx.LINEAR);
  ctx.texParameteri(ctx.TEXTURE_2D, ctx.TEXTURE_MIN_FILTER, ctx.LINEAR_MIPMAP_NEAREST);
  ctx.generateMipmap(ctx.TEXTURE_2D);
  ctx.bindTexture(ctx.TEXTURE_2D, null);
  return texture;
};
 
WebGL.prototype.getPowerOfTwo = function (value, pow) {
  var pow = pow || 1;
  while (pow < value) {
    pow *= 2;
  }
  return pow;
}
 
WebGL.prototype.createMultilineText = function (textToWrite, maxWidth, text) {
  textToWrite = textToWrite.replace("\n", " ");
  var currentText = textToWrite;
  var futureText;
  var subWidth = 0;
  var maxLineWidth = 0;
 
  var wordArray = textToWrite.split(" ");
  var wordsInCurrent, wordArrayLength;
  wordsInCurrent = wordArrayLength = wordArray.length;
 
  var ctx = this.textureContext;
  while (ctx.measureText(currentText).width > maxWidth && wordsInCurrent > 1) {
    wordsInCurrent--;
    var linebreak = false;
 
    currentText = futureText = "";
    for (var i = 0; i < wordArrayLength; i++) {
      if (i < wordsInCurrent) {
        currentText += wordArray[i];
        if (i + 1 < wordsInCurrent) { currentText += " "; }
      }
      else {
        futureText += wordArray[i];
        if (i + 1 < wordArrayLength) { futureText += " "; }
      }
    }
  }
  text.push(currentText);
  maxLineWidth = ctx.measureText(currentText).width;
 
  if (futureText) {
    subWidth = this.createMultilineText(futureText, maxWidth, text);
    if (subWidth > maxLineWidth) {
      maxLineWidth = subWidth;
    }
  }
 
  return maxLineWidth;
}
 
WebGL.prototype.draw2DText = function (textToWrite) {
  var canvasX, canvasY;
  var textX, textY;
  var text = [];
 
  var textHeight = parseInt(this.textureFontHeight, 10);
  var maxWidth = this.textureMaxWidth;
 
  var ctx = this.textureContext;
  ctx.font = this.textureFontHeight + "px " + this.textureFontFamily;
 
  if (maxWidth && ctx.measureText(textToWrite).width > maxWidth) {
    maxWidth = this.createMultilineText(textToWrite, maxWidth, text);
    canvasX = this.getPowerOfTwo(maxWidth);
  } else {
    text.push(textToWrite);
    canvasX = this.getPowerOfTwo(ctx.measureText(textToWrite).width);
  }
  canvasY = this.getPowerOfTwo(textHeight * (text.length + 1));
  if (this.textureIsSquare) {
    (canvasX > canvasY) ? canvasY = canvasX : canvasX = canvasY;
  }
 
  this.textureCanvas.width = canvasX;
  this.textureCanvas.height = canvasY;
 
  switch (this.textureTextAlign) {
    case "left":
      textX = 0;
      break;
    case "center":
      textX = canvasX / 2;
      break;
    case "right":
      textX = canvasX;
      break;
  }
  textY = canvasY / 2;
 
  ctx.fillStyle = this.textureTextBackColor;
  ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
  ctx.fillStyle = this.textureTextFaceColor;
  ctx.textAlign = this.textureTextAlign;
  ctx.textBaseline = this.textureTextBaseline;
  ctx.font = this.textureFontHeight + "px " + this.textureFontFamily;
 
  var offset = (canvasY - textHeight * (text.length + 1)) * 0.5;
  for (var i = 0; i < text.length; i++) {
    if (text.length > 1) {
      textY = (i + 1) * textHeight + offset;
    }
    ctx.fillText(text[i], textX, textY);
  }
}

Le principe d'utilisation de ce code est très simple. L'appelant utilise setTextTextureInfo pour définir précisément l'aspect du texte, et createTexturesFromText pour faire générer la texture correspondant au texte fournit. Les autres fonctions ne servent qu'à générer la texture et ne sont pas destinées à être appelées.
En ce qui concerne le code lui-même, il est directement issu de WebGL text using a Canvas Texture.

Pour afficher les noms des axes nous ajoutons le code suivant à la page WebGL_18.htm, qui reprend le code de la page WebGL_17.htm:

function Theater(canvasElement) {
  ...
  // Les autres membres.
  this.refNames = [];
  ...
  // Chargement des textures.
  this.createRefTextures();
  ...
}
 
// Création des textures de type texte.
Theater.prototype.createRefTextures = function () {
  var gl = this.gl;
  var textures = this.textures;
 
  // Configuration de la police utilisée pour générer la texture.
  gl.setTextTextureInfo("50" , "Calibri", gl.TEXT_CENTER, gl.TEXT_MIDDLE, "white", "rgba(0, 0, 0, 0)", 30, true);
  // Génération des textures.
  for (var n in this.refNames) {
    var nom = this.refNames[n];
    textures[nom] = gl.createTexturesFromText(nom);
  }
};
 
Theater.prototype.initRefBuffers = function () {
  ...
  // Les noms des extrémités des axes.
  this.refNames = ["-X", "X", "-Y", "Y", "-Z", "Z"];
}
 
Theater.prototype.drawAxisBillboards = function (points, names) {
  // On positionne le programme shader des textures.
  this.gl.useShaderProgram(this.textureProgram);
  // Comme on dessine toujours la même chose, on peut charger hors de la boucle.
  //this.gl.pushVaryingColorBuffer(this.billboardBuffers);
  this.gl.pushPositionBuffer(this.billboardBuffers);
  // On dessine un panneau à chaque point.
  var i = 0;
  var n = 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;
    // Mise face caméra.
    this.gl.cylindricalBillboard();
    // On dessine le panneau.
    this.gl.pushTextureBuffer(this.billboardBuffers, this.textures[names[n]]);
    n++;
    this.gl.drawTriangleStripArrays(this.billboardBuffers);
    // Restauration de la matrice initiale.
    this.gl.restore();
  }
  // On rétablit le programme shader des couleurs.
  this.gl.useShaderProgram(this.colorProgram);
}
 
Theater.prototype.drawSpirals = function () {
  ...
  
    // Dessin des panneaux d'affichage aux extrémités des axes.
    this.drawAxisBillboards(this.refPoints, this.refNames);
    // Restauration de la matrice initiale.
    this.gl.restore();
  }
}

Si vous affichez cette version de la page vous devez obtenir le résultat suivant (après zoom) :
HTML5 09 08

Ce magnifique résultat clôt notre série de billets d'initiation à WebGL. Il est certain que la classe WebGL mérite d'être améliorée, à chacun de la faire évoluer comme il le souhaite.

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

A bientôt pour de nouvelles aventures.

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