Canalblog
Suivre ce blog Administration + Créer mon blog

Fanfois et ses papillons

19 août 2012

WebGL, Quel livre pour débuter ?

Depuis mon dernier billet sur WebGL, je n’ai pas arrêté WebGL, bien au contraire et j’ai finalisé la bibliothèque WebGL que je devais réaliser.

Cela dit la chose ne s’est pas faite sans douleurs et il m’a fallu lire tout ce que j’ai trouvé sur le sujet. Heureusement, à part chez MS, le sujet soulève un certain intérêt et les éditeurs sortent des livres sur le sujet. Dans ce billet je vais vous donner mon opinion sur les différents livres que j’ai lus pour arriver à coder la librairie. Au moment où j’écris ce billet, il n’existe pas d’autre livre papier sur le sujet. Notez que des sorties sont en cours, typiquement Apress et O’Reilly sortent leur livre sur le sujet à la fin du mois (déjà disponible en eBook). Il est donc fort possible, et ce serait même logique, que le meilleur livre soit à venir. En effet, si les auteurs ne sont pas trop convaincus qu’ils détiennent LA vérité, ils devraient lire ce qui est déjà sorti et y prendre des (bonnes) idées.

Une remarque avant de passer aux livres eux même : ils sont TOUS en anglais. Je sais que ce blog est en français, mais là, il n’y a pas moyen de couper à l’anglais. A ce jour ma route n’a pas croisée celle d’un livre en français sur WebGL, même d’un mauvais livre sur WebGL.

Ceux qui ont lu mes billets précédents sur WebGL savent que je suis parti de du livre HTML5 Canvas Cookbook.
Concernant WebGL, ce livre est le moins détaillé du lot. La chose est totalement normale, ce n’est pas le sujet du livre. Le sujet du livre c’est principalement l’utilisation 2D du canvas et, comme nous le verrons, Packt, l’éditeur, à un livre dédié à WebGL.
A contrario, c’est le seul livre qui parle de l’aspect 2D du canvas, les autres sont exclusivement dédiés à WebGL. Comme nous l’avons vu dans les billets précédents, cela peut parfois servir. Cela nous a permis de mettre du texte sur nos panneaux.
Pour information, un collègue de bureau est parti de ce livre pour faire de la 2D. Même si son code final est assez éloigné de ce que propose le livre, le livre l’a très clairement bien aidé. Au début il a repris directement le code du livre. Après le livre montre comment résoudre certaines situations de manière simple, mais l’auteur lui-même invite ses lecteurs à se tourner vers sa solution (payante) pour une solution plus robuste et performante. Il n’est donc pas surprenant que mon collègue ait du coder quelque chose de plus performant, et à priori robuste, que ce qui est montré dans le livre.
Cela dit même si les explications d’HTML5 Canvas Cookbook sont très limités (pour ainsi dire inexistantes) concernant l’aspect WebGL, sa pièce à la Doom est sympa et son étude pas forcement inutile. De là à conseiller le livre pour s’initier à WebGL, non, plus maintenant, il y a mieux.

Compte tenu de l’ordre des parutions, j’ai ensuite lu Professional WebGL Programming: Developing 3D Graphics for the Web.
Ce livre n’est pas celui que je conseillerais comme premier livre sur WebGL car il manque à mon avis un certain nombre d’éléments que j’aurais aimé y trouver. Compte tenu du fait que le titre du livre contient "Professional", cela tend à indiquer, chez cet éditeur, que ce n’est pas un livre pour débutant mais bien pour quelqu’un qui veut aller plus au fond des choses. Bon ce n’est pas non plus un livre de 800 pages, il est donc évident que l’on n’ira pas au fond des abysses. CEla conduit à un livre curieux car ce livre contient à l’évidence un texte destiné à des "professionnels", mais il ne traite pas vraiment de certains aspects fort utiles (les bases mathématiques par exemple).
Typiquement c’est à ce jour le seul livre paru qui ait un vrai souci de robustesse et dit clairement que l’on peut très bien perdre son contexte WebGL en cours de route. Il donne d’ailleurs le code à mettre en place et donne un (bon) conseil pour éviter les problèmes. Visiblement les autres auteurs (enfin ceux dont le livre est paru le 18 aout 2012) ne sont pas au courant car ils codent exactement ce qu’il n’est pas recommandé de faire si on veut gérer facilement la situation.
Pour aider à la robustesse du code il faut un outil pour le secouer. Ce livre donne une solution (que j’ai intégré à la version débogue de ma librairie).
Vous souhaitez avoir de bonne performance, regardez comment vous rangez les données dans les tampon WebGL. Là encore seul ce livre parle de cet aspect.
En résumé, compte tenu que j’avais déjà pas mal ramé (la série de billets sur WebGL qui est antérieure à ce billet avait déjà été publiée) ce livre ne m’a pas appris grand-chose sur ce que l’on peut faire avec WebGL et je ne suis pas certain que quelqu’un qui n’a jamais fait de 3D y trouve son compte. Il m’a par contre appris à écrire du code plus robuste et professionnellement c’est très important.
Je recommande donc la lecture de ce livre pour cet aspect. Si cet aspect n’est pas important pour vous, lisez plutôt le livre du prochain paragraphe.
Pour être franc, je pense vraiment que l’éditeur a poussé l’auteur à finir le livre à une date donnée pour sortir le premier livre sur le sujet. C’est dommage, car sauf à faire rapidement une seconde édition (que je ne rachèterais pas, car j’ai la version papier du livre), la chose ne tient pas sur le moyen terme (2 mois c’est du moyen terme court).

Par ordre chronologique, j’ai ensuite lu un livre, que je recommande très chaudement à tous ceux qui ont un peu (voir totalement) oublié leur cours de mathématique du lycée (au moins en section scientifique). Comme il n’est pas sur WebGL, je le saute (temporairement), et je passe à WebGL Beginner's Guide.
J’ai acheté ce livre (en eBook) pratiquement le jour de sa sortie (la semaine suivante en fait). Dans ces conditions j’ai soumis quelques erreurs à l’éditeur car la section errata était encore vierge. Je n’ai pas trouvé que les erreurs dans le texte étaient très gênantes, j’ai d’ailleurs "oublié" d’en signaler certaines car elles n’affectaient en rien le sens du texte et tenaient plus de l’erreur de typographie. Par contre j’avais trouvé inadmissible que le code qui accompagnait le livre ne tourne pas pour les chapitres 3 et 8. J’étais d’autant plus énervé que j’ai acheté ce livre pour le code du chapitre 8 (le "picking"). En effet, si je savais ce qu’il fallait coder (merci la toile), je voulais une implémentation de référence, mon passé JavaScript étant relativement mince. Je tiens à signaler que je n’ai trouvé que 3 bogues (dont un, celui du chapitre 8, est lié un une modification faites par quelqu’un qui a dû trouver un "else" inutile et l’a enlevé tardivement dans le cycle de "relecture", car on trouve dans le livre une version du code sans la suppression malheureuse), que je les ai signalés à l’éditeur et que ce dernier a mis à jour le code que vous pourrez récupérer chez lui en une semaine. Compte tenu du fait que la semaine en question était celle du 15 Août, je trouve que ce délais est tout à l’honneur de l’éditeur, Packt, qui a tout de même dû lire le mail d’un charlot aigri qui lui disait que son code était naze, constater que le charlot n’en était peut-être pas un, valider la correction proposé par le charlot (et en profiter pour corriger la gestion du contrôle graphique concerné), mettre à jour son site, et, chose qui fait toujours plaisir aux charlots aigris, lui envoyer un mail pour lui dire que le code corrigé était en ligne.
Cela étant dit, actuellement, c’est le livre que je conseillerais pour découvrir WebGL. Il couvre pratiquement tout ce qui est nécessaire pour faire une première application WebGL. Si je l’avais lu avant d’écrire les billets sur WebGL, il m’aurait manqué certains éléments comme le "billboard", qui est très important pour la librairie que je devais réaliser. Il m’aurait également manqué un moyen pour passer d’un mode de caméra à l’autre. Avec l’approche du livre, on a bien deux gestions différentes de la caméra, mais la façon dont sont introduites les deux méthodes (ordre des matrices rotation et translation dans la multiplication) ne permet pas d’envisager de passer de l’une à l’autre de manière transparente. La chose est évidemment possible, et le livre donne presque la solution avec la façon dont est gérée la caméra dans la classe dédié à cette dernière. Cela dit, je ne pense pas qu’un lecteur qui n’aurait que le livre comme source d’information y arriverait (mais en aurait-il besoin ?). Il faut disposer d’un autre éclairage pour y parvenir. A la différence du livre précédant, ce livre fait fini. Les exemples de code sont mieux dans le sens où il présente des pages nettement mieux finies au niveau de l’aspect visuel. Dommage que ne soient pas abordés les aspects débogue, perte de contexte WebGL, et optimisation des tampons WebGL, ce qui vous obligera à lire le livre précédent.

Que penser des livres à paraître prochainement (en fait dispo en eBook) ?
Même si je ne les ai pas intégralement lus, simplement rapidement parcourus, la simple lecture de leur table des matières montre une différence fondamentale. Les deux livres s’orientent vers l’utilisation d’une ou plusieurs librairies JavaScripts s’occupant pour vous des basses œuvres. La présentation de ce qu’est le graphisme 3D est réalisé au pas de charge, dans le premier chapitre. Cela peut être assez violent pour un lecteur novice en 3D. Il me semble donc qu’un novice n’a pas intérêt à commencer avec ces deux livres, sauf à vouloir faire des belles choses qui en mette plein la vue sans trop bien comprendre comment ça marche, grâce aux librairies.
Il est probable que beaucoup plébisciterons cette approche qui permet très certainement d’obtenir le meilleur ratio temps passé à coder / résultat obtenu. Malheureusement, si ce que l’on souhaite c’est savoir ce qui se passe vraiment, ce n’est pas la bonne solution.
Par contre, les lire dans un second temps, ou un premier si on connait déjà OpenGL, est probablement une bonne chose.
Je vous livrerais un avis plus complet dès que je les aurais fini (dans un mois ?).

Ma recommandation actuelle (18 aout 2012) est donc :

  1. WebGL Beginner's Guide est LE livre pour débuter en WebGL.
  2. Si vous voulez améliorer la robustesse de votre code, lisez également Professional WebGL Programming: Developing 3D Graphics for the Web. En plus de cela, la présentation étant différente, cela vous permettra sans doute d’éclaircir certains points que le premier livre n’avait pas totalement illuminés.

Maintenant il y a un pavé, un gros pavé, que j’ai lu entre ces deux livres, qui peut rendre des services à tous ceux qui veulent vraiment comprendre ce qui se passe. Ce livre n’est pas un livre sur WebGL. Ce n’est pas non plus un livre sur OpenGL. C’est un livre sur les mathématiques nécessaires pour faire de la 3D sur un ordinateur (surtout les matrices mais pas que). Il s’agit de 3D Math Primer for Graphics and Game Development, 2nd Edition.
Si vous avez du mal avec les maths (ou l’anglais) passez votre chemin.
Si par contre le problème n’est pas votre compréhension générale des maths mais votre mémoire, ce livre est fait pour vous. Enfin presque. Les auteurs ont pris un parti qui m’a surpris et surtout qui n’est pas celui de OpenGL et donc de WebGL. S’ils expliquent la relation entre les deux formulations d’un vecteur, ligne ou colonne, ils ont choisis la version ligne. En bon petit écolier des années 1960-1970, j’ignorais complètement que l’on pouvait utiliser des vecteurs lignes, mes études scientifiques ne m’ont fait côtoyer que les vecteurs colonnes. Cela n’empêche pas le livre d’être une source réelle d’informations, même si nous, pauvre "WebGListe", devront reprendre toutes les formules vu que WebGL utilise les vecteurs colonnes et que tout est donc "à l’envers".

Bon WebGL.

Publicité
Publicité
18 août 2012

Windows 8 release, premières impressions

Il y a presque un an, il était possible de tester Windows 8 sur une tablette. A l’époque Microsoft n’en testait officiellement qu’une, l’ASUS EeeSlate EP121. Si vous vous référez à mon post de l’époque, Installer Windows 8 sur une tablette, vous constaterez que tout ne fonctionnait pas sur cette tablette.
Impossible de faire fonctionner le gyroscope, un comble pour une tablette.
Résultat des courses, une déception certaine car c'était un des points qui m'intéressait le plus avec le "touch".

Une (longue) recherche sur internet plus tard, on avait un fautif. Evidemment pas Microsoft, ben non sur un forum Microsoft... ASUS donc, qui ne fournissait pas le bon driver. Pour excuser ASUS (ou son fournisseur de gyroscope), il était précisé que Microsoft avait changé le modèle des drivers avec Windows 8. En conséquence un driver Windows 7 ne faisait pas l’affaire.
C'est en tout cas ce que l'on pouvait lire sur le bon forum de Microsoft (cf. le billet de l'époque). L'excuse est probablement seulement partiellement valide, car pour faire fonctionner le Bluetooth ... on installait le driver Windows 7 !!!

Si je n’ai rien posté depuis c’est pour une raison fort simple, les deux versions suivantes de Windows 8 n’ont strictement rien changé à ce problème. J'ai donc continué à passer mon chemin.
En cherchant bien sur la toile, on pouvait trouver une magouille, pour la version suivante, mais je n’étais pas assez motivé pour la mettre en œuvre (et à l’époque j’étais en plein ménage, avec l’aspirateur pour ceux qui ne suivraient pas tous mes billets).
La "Release Preview" n’a pas résolu le problème, pire, si je me fie à ce que j’ai trouvé sur la toile, la magouille pour le gyroscope ne fonctionnait plus.

LA question que je me posais était donc la suivante, la version finale permettrait-elle d’avoir une tablette EP121 opérationnelle ou devrais-je la remettre (définitivement) en Windows 7 ?

Pourquoi définitivement ?

Parce que Metro et moi ça fait deux. La chose est d’ailleurs connue depuis longtemps, je viens travailler en moto.
Blague à part, Metro je n’aime pas. Sur mon téléphone passe encore, sur une tablette c'est très limite et sur un ordinateur portable ou fixe la question ne se pose même pas.
Il y a probablement une raison à cela, l'âge du capitaine.
Non pas parce que je veux conserver la même IHM qu'avant. Si c'était le cas, il faut me remettre l'IHM superbement archaïque de mon Apple ][ E de quand j'étais jeune, et vous pas né (vous l'étiez, désolé).
Non, l'âge du capitaine influe sur sa conception de la liberté, et sa vision très négative de la conservation par d'autres de ses affaires privées.
En clair, le cloud c'est géniale, mais exclusivement pour les autres. Facebook, Twitter et autre, même combat. Je n'ai aucun besoin de me faire greffer un téléphone dans la tête, et dans le même état d'esprit, avoir des tuiles qui se mettent à jour toutes seules pour indiquer que toto à "twitté" un truc de la mort qui tue n'a aucun intérêt pour moi. Après que ça puisse servir de temps en temps ok, mais il y a déjà ce qu'il faut depuis plus de 10 ans pour ça.
Il y a tout de même des limites à mon aspect parano, les serveurs de mails de mes domaines ne sont plus sur des machines à moi mais chez big brother (selon moi), à savoir Google, mais c'est gratuit (ben sinon ...). Cela dit tout le monde sait, enfin ceux qui ont ces adresses, savent que sur ces adresses on ne met aucune info à caractère privé.
Tiens, je vais vous faire rire. Pour des raisons liées à la programmation et non à l'ergonomie, mon téléphone portable est un Phone 7. Devinez où sont mes contacts ?
Gagné, dans un autre téléphone, qui ne sert à rien comme téléphone, mais qui accepte de stocker les contacts sur la SIM, ce que Phone 7 n'accepte pas de faire (si un jour un génie de chez MS peut me dire pourquoi...). Inutile de vous dire ce qui arrivera le jour où j'abandonnerai toute velléité de programmation phone 7.

Bon revenons à Windows 8, alors ça marche ou pas ?

Roulement de tambour…

Eh bien OUI, tout fonctionne.
Pas directement à l'installation depuis la clé USB. Non à l'installation il y a un petit problème pour deux drivers (mais pas le gyroscope). Du coup, dès que vous avez configuré le Wi-Fi, Windows Update vous ramène de nouveaux drivers. Je considère que cette étape fait partie de l'installation, il n'y a donc pas de problème à l'issue de l'installation, tout fonctionne.

Maintenant que peut-on faire avec cette tablette ?
Microsoft a fait de gros effort pour qu'au lancement il y ait des applications disponibles. Le catalogue ne peut pas rivaliser, aujourd'hui, avec celui de la pomme. Ceux qui trouve que ce n'est pas normale me font penser aux crétins (et je suis polis) qui me gueulaient dessus pendant mon service militaire parce que je n'avançais pas aussi vite qu'eux pendant le footing (fait en plein canicule à 11h00 du matin sur une voie ferrée, c'est plus drôle). Si quelqu'un qui vient de se mettre à quelque chose et le fait mieux que quelqu'un qui est à fond sur le truc depuis des années, c'est ça qui est anormale, pas le contraire.
Cela dit il y a déjà des choses très sympas. Des jeux par exemple mais qui n'ont rien d'exceptionnel. Non je vais mettre en avant une application éducative et française.
Une précision. Que ce soit sur la pomme ou sur le Windows Store, je n'installe que des applications gratuites.
Si vous avez des enfants de plus de 6 ans (c'est mon cas) et qu'ils ne sont pas encore trop vieux pour accepter une suggestion, faites un tour du côté du "Larousse illustré", c'est vraiment très bien. A niveau du contenu mais également de l'ergonomie (même si le retour en arrière est pénible quand on doit re-scroller pendant 10mn pour revenir où on était). J'ai cette application depuis plusieurs mois et vraiment elle est bien. Je l'ai récupérée car je connais les développeurs qui l'on réalisée, mais je l'utilise parce qu'elle est bien. Restera-t-elle gratuite longtemps ?

Etant à fond dans WebGL (et Microsoft ...), je ne jouerais probablement pas beaucoup avec Windows 8. Cela dit je n'ai plus trop d'excuses car maintenant il fonctionne sur l'ASUS EP121 et Visual Studio Express devrait permettre de coder dessus (devrait car je n'ai pas testé, je joue actuellement avec la version Web de Visual Studio 2012 Express).

A bientôt pour parler de WebGL, mon sujet hot du moment.

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.

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.

23 mai 2012

HTML5 07 (tutorial WebGL 6ème partie)

Dans le précédent billet, nous avons permis à l'utilisateur de manipuler la caméra et donc de visualiser la scène comme il le souhaite. Par contre la scène que nous lui proposons est nécessairement simple. En effet nous sommes actuellement obligés de la dessiner en une seule fois. Ce fonctionnement ne nous permettra pas de réaliser des scènes plus complexes. Nous devons remédier à ce problème.

Pour expliciter la chose, nous allons considérer que nous voulons peupler notre scène avec plusieurs spirales (chaque spirale viendra avec ses axes).
Nous cherchons donc à dupliquer ce que nous savons faire actuellement en plusieurs endroits de la scène.
C'est la base de la construction d'une scène 3D. On modélise un objet en 3D en le centrant sur l'origine. Lors du rendu de la scène, on dessine cet objet et on le déplace pour le positionner correctement dans la scène. Si l'objet doit être représenté plusieurs fois on recommence le processus autant de fois que nécessaire. Généralement plusieurs types d'objets doivent être rendus, la même approche est utilisée pour chaque type d'objet.
Pour faire la chose de manière efficace, il est important de limiter les opérations sur les matrices qui peuvent, surtout si elles sont nombreuses, plomber sérieusement les performances.
Il se trouve que lors de la construction de la scène, certaines positions vont être partagées par plusieurs objets.
L'exemple le plus évident étant les déplacements induits par la caméra. Chaque dessin d'un objet de la scène implique de recalculer la matrice mvMatrix induite par la caméra. En fait nous avons besoin de la matrice, pas forcément de la recalculer à chaque fois. Nous pouvons très bien la calculer une fois, la sauver quelque part et la récupérer à chaque fois que nous en aurons besoin.
Pour cela tout le monde utilise une pile de matrices.
Avant de dessiner un élément particulier de la scène, on sauve la matrice courante dans la pile. Après avoir dessiné l'objet on restaure la matrice en la récupérant dans la pile. Avec cette méthode, le dessin d'un objet ne provoque pas de modification de la matrice courante (sauf pendant le dessin lui-même). On peut donc utiliser la matrice courante pour dessiner l'objet suivant, il n'est pas nécessaire de reconstruire cette matrice.

La première chose à faire consiste donc à ajouter une pile de matrice dans notre classe WebGL et de la doter des méthodes permettant d'empiler et de dépiler. Nous ajoutons donc (seul le code ajouté est présenté) :

var WebGL = function (canvasElement) {
  ...
 
  this.mvMatrixStack = [];
 
  ...
}
 
WebGL.prototype.save = function () {
  var copy = this.mat4.create();
  this.mat4.set(this.mvMatrix, copy);
  this.mvMatrixStack.push(copy);
};
 
WebGL.prototype.restore = function () {
  if (this.mvMatrixStack.length == 0) {
    throw "Invalid popMatrix!";
  }
  this.mvMatrix = this.mvMatrixStack.pop();
};

Voyons comment utiliser ces deux méthodes.
Pour commencer nous créons une nouvelle page HTML, strictement identique à WebGL_10.htm, que nous appelons WebGL_11.htm.
Il nous faut simplement travailler en quatre étapes :

  • pour commencer nous avons besoin de définir les points et les couleurs d'une spirale. Nous avons déjà une méthode pour cela, elle s'appelle initBuffers. Dans un contexte où il est possible d'avoir plusieurs types d'objets, ce nom n'est pas très précis. Nous renommons cette méthode initSpiralBuffers;
  • il nous faut maintenant savoir combien il y aura de spirales et où il faudra les placer. Pour cela nous ajoutons une nouvelle méthode initSpiralBuffers. Son code est le suivant :
    function initSpiralPositions() {
      var spiralPositions = [];
      // L'espace disponible est de -50 à +50, -20 à +20 verticalement.
      // Sachant qu'une spirale s'inscrit dans un volume de 20 unités (-10 à +10),
      // on défini les limites possibles.
      var hLimit = 40;
      var vLimit = 10;
      // Génération du nombre de spirales (entre 1 et 10).
      var spiralCount = Math.floor((Math.random() * 10) + 1);
      // Génération des positions.
      for (var n = 0; n < spiralCount; n++) {
        var spiralPos = {};
        spiralPos.x = (Math.random() * hLimit * 2) - hLimit;
        spiralPos.y = (Math.random() * vLimit * 2) - vLimit;
        spiralPos.z = (Math.random() * hLimit * 2) - hLimit;
        spiralPos.rotationX = Math.random() * Math.PI * 2;
        spiralPos.rotationY = Math.random() * Math.PI * 2;
        spiralPos.rotationZ = Math.random() * Math.PI * 2;
        spiralPositions.push(spiralPos);
      }
      return spiralPositions;
    }
    
    Le code proposé initialise tous les éléments permettant de positionner un objet. C'est pour la démonstration. A l'arrivée il y a de grandes chances que ce soit un peu brouillon. Les laisser toutes verticales peut ordonner un peu la scène ;
  • il faut savoir dessiner les spirales. C'est le travail de la méthode drawSpirals:
    function drawSpirals(gl, spiralPositions, spiralBuffers, angle) {
      for (var n = 0; n < spiralPositions.length; n++) {
        // Sauvegarde de la matrice courante.
        gl.save();
        // Application de la position particulière.
        var spiralPos = spiralPositions[n];
        gl.translate(spiralPos.x, spiralPos.y, spiralPos.z);
        gl.rotate(spiralPos.rotationX, 1, 0, 0);
        gl.rotate(spiralPos.rotationY, 0, 1, 0);
        gl.rotate(spiralPos.rotationZ, 0, 0, 1);
        // Application de l'animation.
        gl.rotate(angle, 0, 1, 0);
        // Dessin de la spirale courante.
        gl.pushVaryingColorBuffer(spiralBuffers);
        gl.pushPositionBuffer(spiralBuffers);
        gl.drawLineArrays(spiralBuffers);
        // Restauration de la matrice initiale.
        gl.restore();
      }
    }
    
  • dans la phase d'initialisation, il convient maintenant d'appeler initSpiralBuffers et initSpiralPositions. Lors du dessin de la scène c'est à drawSpirals qu'il faut faire appel, après avoir mis en place la dite scène.

Si l'on veut ajouter un autre type d'objet, il suffit de définir les trois méthodes correspondantes au nouveau type, et le tour est joué.

Justement, ajoutons un nouveau type d'objet. Nous allons ajouter une nouvelle sorte de spirales, les spirales représentées par des points.

Notre classe WebGL ne sait pas afficher des points. Conséquence, nous devons d'abord lui apprendre à le faire.

Pour cela nous définissons une nouvelle méthode drawPointArrays. Son code n'est pas des plus compliqué :

WebGL.prototype.drawPointArrays = function (buffers) {
  this.setMatrixUniforms();
  // draw arrays
  this.context.drawArrays(this.context.POINTS, 0, buffers.positionBuffer.numElements / 3);
};

L'intérêt de définir une méthode drawPointElements semble très limité. Il n'y a pas grand intérêt à utiliser les indexes pour dessiner des points.

Maintenant que nous disposons de la fonction, utilisons là.
Nous n'allons pas faire très compliqué. Nous allons pratiquement reprendre le code de la spirale mais au lieu des traits, nous placerons simplement des points. Attention, nous allons mélanger les points et les traits car nous tracerons toujours les axes.
Pour dessiner une spirale à l'aide de traits, nous avons dessiné deux choses. Les axes et la spirale. Comme les deux choses étaient de la même nature nous les avons placées dans une unique fonction.
Pour dessiner une spirale en point nous devons dessiner deux choses, les axes et les points formant la spirale. Les deux choses étant de nature différente nous allons les séparer.
Nous allons donc avoir :

  1. une méthode pour définir ce qu'est un objet référentiel ;
  2. une méthode pour définir ce qu'est un objet spirale en traits ;
  3. une méthode pour définir ce qu'est un objet spirale en points ;
  4. une méthode pour définir où est quoi. Pour construire quelque chose de regardable cette méthode ne fera pas appel au hasard;
  5. une méthode assemble les axes et les spirales, soit en traits, soit en point et les dessine au bon endroit. Il aurait été possible de découper cette méthode en trois méthodes mais cela n'a pas été fait.

Cela nous donne, dans une nouvelle page WebGL_12.htm, le code 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">
      // Définition d'un objet référentiel.
      function initRefBuffers(gl) {
        var refBuffers = {};
 
        // Les points des axes.
        refBuffers.positionBuffer = 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);
        }
        refBuffers.colorBuffer = gl.createArrayBuffer(colorVertices);
 
        return refBuffers;
      }
 
      // Définition d'un objet spirale en traits.
      function initLineSpiralBuffers(gl) {
        var lineSpiralBuffers = {}
 
        // Les points de la spirale.
        var points = [];
        var x = 10.0;
        var y = -10.0
        var z = 0.0;
        var teta = 0.0;
        points = points.concat(x);
        points = points.concat(y);
        points = points.concat(z);
        while (true) {
          teta += Math.PI / 16;
          x = 10 * Math.cos(teta);
          y += 0.25;
          z = 10 * Math.sin(teta);
 
          points = points.concat(x);
          points = points.concat(y);
          points = points.concat(z);
 
          if (y >= 10.0)
            break;
 
          points = points.concat(x);
          points = points.concat(y);
          points = points.concat(z);
        }
 
        lineSpiralBuffers.positionBuffer = gl.createArrayBuffer(points);
 
        // Les couleurs des points de la spirale.
        var colorVertices = [];
        for (var i = 0; i < lineSpiralBuffers.positionBuffer.numElements; i++) {
          colorVertices = colorVertices.concat([1.0, 1.0, 1.0, 1.0]);
        }
        lineSpiralBuffers.colorBuffer = gl.createArrayBuffer(colorVertices);
 
        return lineSpiralBuffers;
      }
 
      // Définition d'un objet spirale en points.
      function initDotSpiralBuffers(gl) {
        var dotSpiralBuffers = {}
 
        // 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);
        }
 
        dotSpiralBuffers.positionBuffer = gl.createArrayBuffer(points);
 
        // Les couleurs des points de la spirale.
        var colorVertices = [];
        for (var i = 0; i < dotSpiralBuffers.positionBuffer.numElements; i++) {
          colorVertices = colorVertices.concat([1.0, 1.0, 1.0, 1.0]);
        }
        dotSpiralBuffers.colorBuffer = gl.createArrayBuffer(colorVertices);
 
        return dotSpiralBuffers;
      }
 
      // Position et nature des spirales.
      function initSpiralPositions() {
        var spiralPositions = [];
        // La première.
        var spiralPos1 = {};
        spiralPos1.isDot = false;
        spiralPos1.x = -10;
        spiralPos1.y = 0;
        spiralPos1.z = 0;
        spiralPos1.rotationX = 0.2;
        spiralPos1.rotationY = 0;
        spiralPos1.rotationZ = 0;
        spiralPositions.push(spiralPos1);
        // La seconde.
        var spiralPos2 = {};
        spiralPos2.isDot = true;
        spiralPos2.x = 10;
        spiralPos2.y = 0;
        spiralPos2.z = 0;
        spiralPos2.rotationX = 0.2;
        spiralPos2.rotationY = 0;
        spiralPos2.rotationZ = 0;
        spiralPositions.push(spiralPos2);
 
        return spiralPositions;
      }
 
      function drawSpirals(gl, spiralPositions, refBuffers, lineSpiralBuffers, dotSpiralBuffers, angle) {
        for (var n = 0; n < spiralPositions.length; n++) {
          // Sauvegarde de la matrice courante.
          gl.save();
          // Application de la position particulière.
          var spiralPos = spiralPositions[n];
          gl.translate(spiralPos.x, spiralPos.y, spiralPos.z);
          gl.rotate(spiralPos.rotationX, 1, 0, 0);
          gl.rotate(spiralPos.rotationY, 0, 1, 0);
          gl.rotate(spiralPos.rotationZ, 0, 0, 1);
          // Application de l'animation.
          if (spiralPos.isDot) {
            gl.rotate(-angle, 0, 1, 0);
          } else {
            gl.rotate(angle, 0, 1, 0);
          }
          // Dessin du référentiel.
          gl.pushVaryingColorBuffer(refBuffers);
          gl.pushPositionBuffer(refBuffers);
          gl.drawLineArrays(refBuffers);
          // Dessin de la spirale.
          if (spiralPos.isDot) {
            gl.pushVaryingColorBuffer(dotSpiralBuffers);
            gl.pushPositionBuffer(dotSpiralBuffers);
            gl.drawPointArrays(dotSpiralBuffers);
          } else {
            gl.pushVaryingColorBuffer(lineSpiralBuffers);
            gl.pushPositionBuffer(lineSpiralBuffers);
            gl.drawLineArrays(lineSpiralBuffers);
          }
          // Restauration de la matrice initiale.
          gl.restore();
        }
      }
 
      function stage(gl, spiralPositions, refBuffers, lineSpiralBuffers, dotSpiralBuffers, angle) {
        // Effacement de l'image précédente.
        gl.clear();
 
        // Initialisation des matrices (caméra appliquée).
        gl.initializeMatrix();
 
        // Définition de la perspective.
        // "Ouverture" de l'objectif, 45°.
        // Zone visible, 0,1 à 100.
        gl.perspective(45, 0.1, 100.0);
 
        // Dessin des spirales.
        drawSpirals(gl, spiralPositions, refBuffers, lineSpiralBuffers, dotSpiralBuffers, angle);
      }
 
      function initWebGL(canvasElement) {
        // Création de notre classe d'aide.
        var gl = new WebGL(canvasElement);
 
        // Initialisation des shaders.
        gl.setShaderProgram("VARYING_COLOR");
 
        // Définition de la couleur de fond.
        gl.setClearColor(0.0, 0.0, 0.0, 1);
 
        // Définition des points et des couleurs d'un référentiel.
        var refBuffers = initRefBuffers(gl);
        // Définition des points et des couleurs d'une spirale en traits.
        var lineSpiralBuffers = initLineSpiralBuffers(gl);
        // Définition des points et des couleurs d'une spirale en points.
        var dotSpiralBuffers = initDotSpiralBuffers(gl);
        // Définition des positions et des orientations des n spirales.
        var spiralPositions = initSpiralPositions();
 
        // Positionnement initial de la caméra.
        gl.setInitialCameraPosition(0, 0, 50, 0, 0, 0);
        gl.resetCameraPosition();
        // Définition des vitesses de déplacement de la caméra.
        //gl.setHorizontalSpeed(20);
        //gl.setVerticalSpeed(10);
 
        // Activation de la caméra.
        gl.activateCameraTracking();
 
        // L'angle de rotation au tour de Y.
        var angle = 0;
 
        // Mise en place de la scène.
        gl.setStage(function () {
          // Calcul du nouvel angle.
          var angularVelocity = Math.PI / 4; // radians / second
          var angleEachFrame = angularVelocity * gl.getTimeInterval() / 1000;
          angle += angleEachFrame;
 
          stage(gl, spiralPositions, refBuffers, lineSpiralBuffers, dotSpiralBuffers, angle);
        });
 
        // Lancement de l'animation.
        gl.startAnimation();
      }
 
      function OnGLCanvasCreated(canvasElement, elementId) {
        window.setTimeout(function () { initWebGL(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>

A l'écran, le résultat devrait être :
html5 07 01

Pour clore ce billet, nous allons ajouter à notre classe WebGL les méthodes permettant de dessiner une scène 3D classique.
En particulier nous demanderons à WegGL de gérer la profondeur lors du rendu de la scène. Nous aurions déjà dû le faire, mais avec nos traits d'un pixel de large la chose n'est pas évidente à voir. Actuellement, le dernier objet dessiné détermine ce que l'on voit. Normalement ce n'est pas l'ordre du rendu des objets qui devrait déterminer ce que l'on voit, c'est la profondeur par rapport à l'observateur. Ce qui est derrière, même dessiné en dernier, ne doit pas être visible mais masqué par ce qui est devant.
Pour valider que tout est bon, nous reconstruirons le dernier exemple du livre HTML5 Canvas Cookbook en l'adaptant à notre version de la classe WebGL.

Pour ce qui est du code de la classe WebGL, le voici (seul le code ajouté ou modifié est présenté) :

var WebGL = function (canvasElement) {
  ...
  // Les constantes définissant les types de shader possible.
  this.TEXTURE = "TEXTURE";
  this.TEXTURE_DIRECTIONAL_LIGHTING = "TEXTURE_DIRECTIONAL_LIGHTING";
 
  ...
 
  // Initialisation générale du contexte WebGL.
  this.context.enable(this.context.DEPTH_TEST);
};
 
WebGL.prototype.clear = function () {
  this.context.viewport(0, 0, this.context.viewportWidth, this.context.viewportHeight);
  this.context.clear(this.context.COLOR_BUFFER_BIT | this.context.DEPTH_BUFFER_BIT);
};
 
WebGL.prototype.pushTextureBuffer = function (buffers, texture) {
  this.context.bindBuffer(this.context.ARRAY_BUFFER, buffers.textureBuffer);
  this.context.vertexAttribPointer(this.shaderProgram.textureCoordAttribute, 2, this.context.FLOAT, false, 0, 0);
  this.context.activeTexture(this.context.TEXTURE0);
  this.context.bindTexture(this.context.TEXTURE_2D, texture);
  this.context.uniform1i(this.shaderProgram.samplerUniform, 0);
};
 
WebGL.prototype.pushNormalBuffer = function (buffers) {
  this.context.bindBuffer(this.context.ARRAY_BUFFER, buffers.normalBuffer);
  this.context.vertexAttribPointer(this.shaderProgram.vertexNormalAttribute, 3, this.context.FLOAT, false, 0, 0);
};
 
WebGL.prototype.drawTriangleArrays = function (buffers) {
  this.setMatrixUniforms();
 
  // draw arrays
  this.context.drawArrays(this.context.TRIANGLES, 0, buffers.positionBuffer.numElements / 3);
};
 
WebGL.prototype.drawTriangleElements = function (buffers) {
  this.setMatrixUniforms();
 
  // draw elements
  this.context.drawElements(this.context.TRIANGLES, buffers.indexBuffer.numElements, this.context.UNSIGNED_SHORT, 0);
};
 
WebGL.prototype.enableLighting = function () {
  this.context.uniform1i(this.shaderProgram.useLightingUniform, true);
};
 
WebGL.prototype.setAmbientLighting = function (red, green, blue) {
  this.context.uniform3f(this.shaderProgram.ambientColorUniform, parseFloat(red), parseFloat(green), parseFloat(blue));
};
 
WebGL.prototype.setDirectionalLighting = function (x, y, z, red, green, blue) {
  // directional lighting
  var lightingDirection = [x, y, z];
  var adjustedLD = this.vec3.create();
  this.vec3.normalize(lightingDirection, adjustedLD);
  this.vec3.scale(adjustedLD, -1);
  this.context.uniform3fv(this.shaderProgram.lightingDirectionUniform, adjustedLD);
 
  // directional color
  this.context.uniform3f(this.shaderProgram.directionalColorUniform, parseFloat(red), parseFloat(green), parseFloat(blue));
};
 
WebGL.prototype.getFragmentShaderGLSL = function (shaderType) {
  switch (shaderType) {
    ...
    case this.TEXTURE:
      return "#ifdef GL_ES\n" +
            "precision highp float;\n" +
            "#endif\n" +
            "varying vec2 vTextureCoord;\n" +
            "uniform sampler2D uSampler;\n" +
            "void main(void) {\n" +
            "gl_FragColor = texture2D(uSampler, vec2(vTextureCoord.s, vTextureCoord.t));\n" +
            "}";
    case this.TEXTURE_DIRECTIONAL_LIGHTING:
      return "#ifdef GL_ES\n" +
            "precision highp float;\n" +
            "#endif\n" +
            "varying vec2 vTextureCoord;\n" +
            "varying vec3 vLightWeighting;\n" +
            "uniform sampler2D uSampler;\n" +
            "void main(void) {\n" +
            "vec4 textureColor = texture2D(uSampler, vec2(vTextureCoord.s, vTextureCoord.t));\n" +
            "gl_FragColor = vec4(textureColor.rgb * vLightWeighting, textureColor.a);\n" +
            "}";
  }
};
 
WebGL.prototype.getVertexShaderGLSL = function (shaderType) {
  switch (shaderType) {
    ...
    case this.TEXTURE:
      return "attribute vec3 aVertexPosition;\n" +
            "attribute vec2 aTextureCoord;\n" +
            "uniform mat4 uMVMatrix;\n" +
            "uniform mat4 uPMatrix;\n" +
            "varying vec2 vTextureCoord;\n" +
            "void main(void) {\n" +
            "gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);\n" +
            "vTextureCoord = aTextureCoord;\n" +
            "}";
    case this.TEXTURE_DIRECTIONAL_LIGHTING:
      return "attribute vec3 aVertexPosition;\n" +
            "attribute vec3 aVertexNormal;\n" +
            "attribute vec2 aTextureCoord;\n" +
            "uniform mat4 uMVMatrix;\n" +
            "uniform mat4 uPMatrix;\n" +
            "uniform mat3 uNMatrix;\n" +
            "uniform vec3 uAmbientColor;\n" +
            "uniform vec3 uLightingDirection;\n" +
            "uniform vec3 uDirectionalColor;\n" +
            "uniform bool uUseLighting;\n" +
            "varying vec2 vTextureCoord;\n" +
            "varying vec3 vLightWeighting;\n" +
            "void main(void) {\n" +
            "gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);\n" +
            "vTextureCoord = aTextureCoord;\n" +
            "if (!uUseLighting) {\n" +
            "vLightWeighting = vec3(1.0, 1.0, 1.0);\n" +
            "} else {\n" +
            "vec3 transformedNormal = uNMatrix * aVertexNormal;\n" +
            "float directionalLightWeighting = max(dot(transformedNormal, uLightingDirection), 0.0);\n" +
            "vLightWeighting = uAmbientColor + uDirectionalColor * directionalLightWeighting;\n" +
            "}\n" +
            "}";
  }
};
 
// Initialisation des shaders gérés par notre .
WebGL.prototype.initShaders = function (shaderType) {
  ...
 
  switch (shaderType) {
    ...
    case this.TEXTURE:
      this.initTextureShader();
      break;
    case this.TEXTURE_DIRECTIONAL_LIGHTING:
      this.initTextureShader();
      this.initNormalShader();
      this.initLightingShader();
      break;
  }
};
 
WebGL.prototype.initTextureShader = function () {
  this.shaderProgram.textureCoordAttribute = this.context.getAttribLocation(this.shaderProgram, "aTextureCoord");
  this.context.enableVertexAttribArray(this.shaderProgram.textureCoordAttribute);
  this.shaderProgram.samplerUniform = this.context.getUniformLocation(this.shaderProgram, "uSampler");
};
 
WebGL.prototype.initNormalShader = function () {
  this.shaderProgram.vertexNormalAttribute = this.context.getAttribLocation(this.shaderProgram, "aVertexNormal");
  this.context.enableVertexAttribArray(this.shaderProgram.vertexNormalAttribute);
  this.shaderProgram.nMatrixUniform = this.context.getUniformLocation(this.shaderProgram, "uNMatrix");
};
 
WebGL.prototype.initLightingShader = function () {
  this.shaderProgram.useLightingUniform = this.context.getUniformLocation(this.shaderProgram, "uUseLighting");
  this.shaderProgram.ambientColorUniform = this.context.getUniformLocation(this.shaderProgram, "uAmbientColor");
  this.shaderProgram.lightingDirectionUniform = this.context.getUniformLocation(this.shaderProgram, "uLightingDirection");
  this.shaderProgram.directionalColorUniform = this.context.getUniformLocation(this.shaderProgram, "uDirectionalColor");
};
 
WebGL.prototype.initTexture = function (texture) {
  this.context.pixelStorei(this.context.UNPACK_FLIP_Y_WEBGL, true);
  this.context.bindTexture(this.context.TEXTURE_2D, texture);
  this.context.texImage2D(this.context.TEXTURE_2D, 0, this.context.RGBA, this.context.RGBA, this.context.UNSIGNED_BYTE, texture.image);
  this.context.texParameteri(this.context.TEXTURE_2D, this.context.TEXTURE_MAG_FILTER, this.context.NEAREST);
  this.context.texParameteri(this.context.TEXTURE_2D, this.context.TEXTURE_MIN_FILTER, this.context.LINEAR_MIPMAP_NEAREST);
  this.context.generateMipmap(this.context.TEXTURE_2D);
  this.context.bindTexture(this.context.TEXTURE_2D, null);
};
 
// Mise en place des matrices de transformations.
WebGL.prototype.setMatrixUniforms = function () {
  this.context.uniformMatrix4fv(this.shaderProgram.pMatrixUniform, false, this.pMatrix);
  this.context.uniformMatrix4fv(this.shaderProgram.mvMatrixUniform, false, this.mvMatrix);
 
  var normalMatrix = this.mat3.create();
  this.mat4.toInverseMat3(this.mvMatrix, normalMatrix);
  this.mat3.transpose(normalMatrix);
  this.context.uniformMatrix3fv(this.shaderProgram.nMatrixUniform, false, normalMatrix);
};

Le code de la page se "limite" alors à ceci :

<!DOCTYPE HTML>
<html>
  <head>
    <title>3D World (HTML5 Canvas Cookbook)</title>
    <!-- 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">
      /*************************************
      * Controller
      */
      function Controller(canvasElement) {
        this.view = new View(this, canvasElement);
        this.gl = new WebGL(canvasElement);
        this.gl.setShaderProgram("TEXTURE_DIRECTIONAL_LIGHTING");
        this.model = new Model(this);
 
        // Positionnement initial de la caméra.
        this.gl.setInitialCameraPosition(0, 1.5, 5, 0, 0, 0);
        this.gl.resetCameraPosition();
        // Définition des vitesses de déplacement de la caméra.
        this.gl.setHorizontalSpeed(8);
        this.gl.setVerticalSpeed(1);
 
        // Activation de la caméra.
        this.gl.activateCameraTracking();
 
        var sources = {
          crate: "image/crate.jpg",
          metalFloor: "image/metalFloor.jpg",
          metalWall: "image/metalWall.jpg",
          ceiling: "image/ceiling.jpg"
        };
 
        var that = this;
        this.loadTextures(sources, function () {
          that.gl.setStage(function () {
            that.view.stage();
          });
          that.gl.startAnimation();
        });
      }
 
      Controller.prototype.loadTextures = function(sources, callback){
        var gl = this.gl;
        var context = gl.getContext();
        var textures = this.model.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];
          })();
        }
      };
 
      /*************************************
      * Model
      */
      function Model(controller) {
        this.controller = controller;
        this.cubeBuffers = {};
        this.floorBuffers = {};
        this.wallBuffers = {};
        this.textures = {};
        this.cratePositions = [];
 
        this.initBuffers();
        this.initCratePositions();
      }
 
      Model.prototype.initCratePositions = function () {
        var crateRange = 45;
        // randomize 20 floor crates
        for (var n = 0; n < 20; n++) {
          var cratePos = {};
          cratePos.x = (Math.random() * crateRange * 2) - crateRange;
          cratePos.y = 0;
          cratePos.z = (Math.random() * crateRange * 2) - crateRange;
          cratePos.rotationY = Math.random() * Math.PI * 2;
          this.cratePositions.push(cratePos);
 
          if (Math.round(Math.random() * 3) == 3) {
            var stackedCratePosition = {};
            stackedCratePosition.x = cratePos.x;
            stackedCratePosition.y = 2.01;
            stackedCratePosition.z = cratePos.z;
            stackedCratePosition.rotationY = cratePos.rotationY + ((Math.random() * Math.PI / 8) - Math.PI / 16);
            this.cratePositions.push(stackedCratePosition);
          }
        }
      };
 
      Model.prototype.initCubeBuffers = function(){
        var gl = this.controller.gl;
        this.cubeBuffers.positionBuffer = gl.createArrayBuffer([
          -1, -1,  1,  1, -1,  1,  1,  1,  1, -1,  1,  1, // Front face
          -1, -1, -1, -1,  1, -1,  1,  1, -1,  1, -1, -1, // Back face
          -1,  1, -1, -1,  1,  1,  1,  1,  1,  1,  1, -1, // Top face
          -1, -1, -1,  1, -1, -1,  1, -1,  1, -1, -1,  1, // Bottom face
           1, -1, -1,  1,  1, -1,  1,  1,  1,  1, -1,  1, // Right face
          -1, -1, -1, -1, -1,  1, -1,  1,  1, -1,  1, -1  // Left face
        ]);
 
        this.cubeBuffers.normalBuffer = gl.createArrayBuffer([
           0,  0,  1,  0,  0,  1,  0,  0,  1,  0,  0,  1, // Front face
           0,  0, -1,  0,  0, -1,  0,  0, -1,  0,  0, -1, // Back face
           0,  1,  0,  0,  1,  0,  0,  1,  0,  0,  1,  0, // Top face
           0, -1,  0,  0, -1,  0,  0, -1,  0,  0, -1,  0, // Bottom face
           1,  0,  0,  1,  0,  0,  1,  0,  0,  1,  0,  0, // Right face
          -1,  0,  0, -1,  0,  0, -1,  0,  0, -1,  0,  0  // Left face
        ]);
 
        this.cubeBuffers.textureBuffer = gl.createArrayBuffer([
          0, 0, 1, 0, 1, 1, 0, 1, // Front face
          1, 0, 1, 1, 0, 1, 0, 0, // Back face
          0, 1, 0, 0, 1, 0, 1, 1, // Top face
          1, 1, 0, 1, 0, 0, 1, 0, // Bottom face
          1, 0, 1, 1, 0, 1, 0, 0, // Right face
          0, 0, 1, 0, 1, 1, 0, 1  // Left face
        ]);
 
        this.cubeBuffers.indexBuffer = gl.createElementArrayBuffer([
           0,  1,  2,  0,  2,  3, // Front face
           4,  5,  6,  4,  6,  7, // Back face
           8,  9, 10,  8, 10, 11, // Top face
          12, 13, 14, 12, 14, 15, // Bottom face
          16, 17, 18, 16, 18, 19, // Right face
          20, 21, 22, 20, 22, 23  // Left face
        ]);
      };
 
      Model.prototype.initFloorBuffers = function () {
        var gl = this.controller.gl;
        this.floorBuffers.positionBuffer = gl.createArrayBuffer([
          -50, 0, -50, -50, 0, 50, 50, 0, 50, 50, 0, -50
        ]);
 
        this.floorBuffers.textureBuffer = gl.createArrayBuffer([
          0, 25, 0, 0, 25, 0, 25, 25
        ]);
 
        this.floorBuffers.indexBuffer = gl.createElementArrayBuffer([
          0, 1, 2, 0, 2, 3
        ]);
 
        // floor normal points upwards
        this.floorBuffers.normalBuffer = gl.createArrayBuffer([
          0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0
        ]);
      };
 
      Model.prototype.initWallBuffers = function(){
        var gl = this.controller.gl;
        this.wallBuffers.positionBuffer = gl.createArrayBuffer([
          -50, 5, 0, 50, 5, 0, 50, -5, 0, -50, -5, 0
        ]);
 
        this.wallBuffers.textureBuffer = gl.createArrayBuffer([
          0, 0, 25, 0, 25, 1.5, 0, 1.5
        ]);
 
        this.wallBuffers.indexBuffer = gl.createElementArrayBuffer([
          0, 1, 2, 0, 2, 3
        ]);
 
        // floor normal points upwards
        this.wallBuffers.normalBuffer = gl.createArrayBuffer([
          0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1
        ]);
      };
 
      Model.prototype.initBuffers = function () {
        this.initCubeBuffers();
        this.initFloorBuffers();
        this.initWallBuffers();
      };
 
      /*************************************
      * View
      */
      function View(controller, canvasElement) {
        this.controller = controller;
        this.canvas = canvasElement;
        this.canvas.width = window.innerWidth;
        this.canvas.height = window.innerHeight;
      }
 
      View.prototype.drawFloor = function () {
        var controller = this.controller;
        var gl = controller.gl;
        var model = controller.model;
        var floorBuffers = model.floorBuffers;
 
        gl.save();
        gl.translate(0, -1.1, 0);
        gl.pushPositionBuffer(floorBuffers);
        gl.pushNormalBuffer(floorBuffers);
        gl.pushTextureBuffer(floorBuffers, model.textures.metalFloor);
        gl.pushIndexBuffer(floorBuffers);
        gl.drawTriangleElements(floorBuffers);
        gl.restore();
      };
 
      View.prototype.drawCeiling = function(){
        var controller = this.controller;
        var gl = controller.gl;
        var model = controller.model;
        var floorBuffers = model.floorBuffers;
 
        gl.save();
        gl.translate(0, 8.9, 0);
        // use floor buffers with ceiling texture
        gl.pushPositionBuffer(floorBuffers);
        gl.pushNormalBuffer(floorBuffers);
        gl.pushTextureBuffer(floorBuffers, model.textures.ceiling);
        gl.pushIndexBuffer(floorBuffers);
        gl.drawTriangleElements(floorBuffers);
        gl.restore();
      };
 
      View.prototype.drawCrates = function () {
        var controller = this.controller;
        var gl = controller.gl;
        var model = controller.model;
        var cubeBuffers = model.cubeBuffers;
 
        for (var n = 0; n < model.cratePositions.length; n++) {
          gl.save();
          var cratePos = model.cratePositions[n];
          gl.translate(cratePos.x, cratePos.y, cratePos.z);
          gl.rotate(cratePos.rotationY, 0, 1, 0);
          gl.pushPositionBuffer(cubeBuffers);
          gl.pushNormalBuffer(cubeBuffers);
          gl.pushTextureBuffer(cubeBuffers, model.textures.crate);
          gl.pushIndexBuffer(cubeBuffers);
          gl.drawTriangleElements(cubeBuffers);
          gl.restore();
        }
      };
 
      View.prototype.drawWalls = function(){
        var controller = this.controller;
        var gl = controller.gl;
        var model = controller.model;
        var wallBuffers = model.wallBuffers;
        var metalWallTexture = model.textures.metalWall;
 
        gl.save();
        gl.translate(0, 3.9, -50);
        gl.pushPositionBuffer(wallBuffers);
        gl.pushNormalBuffer(wallBuffers);
        gl.pushTextureBuffer(wallBuffers, metalWallTexture);
        gl.pushIndexBuffer(wallBuffers);
        gl.drawTriangleElements(wallBuffers);
        gl.restore();
 
        gl.save();
        gl.translate(0, 3.9, 50);
        gl.rotate(Math.PI, 0, 1, 0);
        gl.pushPositionBuffer(wallBuffers);
        gl.pushNormalBuffer(wallBuffers);
        gl.pushTextureBuffer(wallBuffers, metalWallTexture);
        gl.pushIndexBuffer(wallBuffers);
        gl.drawTriangleElements(wallBuffers);
        gl.restore();
 
        gl.save();
        gl.translate(50, 3.9, 0);
        gl.rotate(Math.PI * 1.5, 0, 1, 0);
        gl.pushPositionBuffer(wallBuffers);
        gl.pushNormalBuffer(wallBuffers);
        gl.pushTextureBuffer(wallBuffers, metalWallTexture);
        gl.pushIndexBuffer(wallBuffers);
        gl.drawTriangleElements(wallBuffers);
        gl.restore();
 
        gl.save();
        gl.translate(-50, 3.9, 0);
        gl.rotate(Math.PI / 2, 0, 1, 0);
        gl.pushPositionBuffer(wallBuffers);
        gl.pushNormalBuffer(wallBuffers);
        gl.pushTextureBuffer(wallBuffers, metalWallTexture);
        gl.pushIndexBuffer(wallBuffers);
        gl.drawTriangleElements(wallBuffers);
        gl.restore();
      };
 
      View.prototype.stage = function(){
        var controller = this.controller;
        var gl = controller.gl;
        var model = controller.model;
        var view = controller.view;
 
        gl.clear();
        gl.initializeMatrix();
        gl.perspective(45, 0.1, 150.0);
 
        // enable lighting
        gl.enableLighting();
        gl.setAmbientLighting(0.5, 0.5, 0.5);
        gl.setDirectionalLighting(-0.25, -0.25, -1, 0.8, 0.8, 0.8);
 
        view.drawFloor();
        view.drawWalls();
        view.drawCeiling();
        view.drawCrates();
      }
 
      function OnGLCanvasCreated(canvasElement, elementId) {
        window.setTimeout(function () {
          new Controller(canvasElement);
        }, 500);
      }
 
      function OnGLCanvasFailed(canvasElement, elementId) {
        alert("WebGL n'est pas supporté par votre butineur !");
      }
    </script>
  </head>
  <body>
    <script id="WebGLCanvasCreationScript" type="text/javascript">
      WebGLHelper.CreateGLCanvasInline('glCanvas', OnGLCanvasCreated, OnGLCanvasFailed)
    </script>
  </body>
</html>

Ce code et plus concis car toutes la gestion de l'interaction avec l'utilisateur se trouve maintenant dans la classe WebGL. Avec cette version, il est même possible de se déplacer verticalement vu que la classe WebGL sait le faire.

Nous voici arrivé à la fin de ce billet. Nous sommes maintenant capables de rendre tout type de scène, même relativement complexe.
Evidement l'introduction de la partie 3D classique a été assez violente. Encore une fois, comme cela a déjà été écrit dans le second billet de cette série, Internet regorge de bons tutoriaux sur la question. En faire un nouveau, très probablement moins bon, ne présente aucun intérêt.

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

A bientôt pour la suite de nos aventures.

Publicité
Publicité
22 mai 2012

HTML5 06 (tutorial WebGL 5ème partie)

Dans ce billet nous allons permettre à l'utilisateur de déplacer la caméra. En effet, si nous avons introduit la caméra dans le précédent billet, elle est pour l'instant fixe.

Le code de gestion de la caméra est directement inspiré de celui que l'on peut trouver dans le livre HTML5 Canvas Cookbook. Il y cependant certaines différences :

  • la gestion des interactions avec l'utilisateur a été intégrée à la classe WebGL. Il suffit donc d'appeler une méthode, activateCameraTracking, pour que l'utilisateur puisse interagir avec la position et l'orientation de la caméra. Cela lui permet de modifier sa vision de la scène. Si cette méthode n'est pas appelée, la situation est celle que nous connaissions jusque-là ;
  • une méthode permet de définir la position initiale de la caméra. C'est setInitialCameraPosition. Cette position est celle à laquelle la caméra reviendra si on la réinitialise. La réinitialisation s'obtient à l'aide de la méthode resetCameraPosition ;
  • la caméra peut monter ou descendre. Pour cela il faut utiliser les touches "page up" et "page down" ;
  • le roulis n'est pas géré et reste toujours à 0. Il ne l'est pas non plus dans le livre, où il n'existe même pas. Introduire sa gestion n'est pas forcement simple. Traditionnellement, la gestion des angles est réalisée à l'aide de la souris. Introduire un angle supplémentaire avec la souris n'est pas impossible, la chose a été décrite il y a presque 20 ans, mais est au-delà de ces billets. Cela dit, à part dans les simulateurs de vol, cela n'apporte pas grand-chose à l'utilisateur et rend généralement la compréhension de ce qui se passe plus aléatoire. La gêne ne devrait donc pas être trop importante ;

Pour gérer la caméra un certain nombre de nouveaux éléments ont été introduit dans la classe WebGL. Voyons ce qui est introduit dans le constructeur (seul le code ajouté est présenté) :

var WebGL = function (canvasElement) {
  ...
 
  // Constantes permettant de gérer les mouvements de caméra demandés par l'utilisateur.
  this.ARRET = "ARRET";
  this.AVANT = "AVANT";
  this.ARRIERE = "ARRIERE";
  this.GAUCHE = "GAUCHE";
  this.DROITE = "DROITE";
  this.HAUT = "HAUT";
  this.BAS = "BAS";
 
  // Gestion des mouvements de translation demandés par l'utilisateur.
  this.avantArriere = this.ARRET;
  this.droiteGauche = this.ARRET;
  this.hautBas = this.ARRET;
  this.vitesseHorizontale = 10; // unités par seconde.
  this.vitesseVerticale = 5; // unités par seconde.
 
  // Gestion des mouvements de rotation demandés par l'utilisateur.
  this.mouseDownPos = null;
  this.mouseDownPente = 0; // Rotation autour de X.
  this.mouseDownAzimut = 0; // Rotation autour de Y.
  this.mouseDownRoulis = 0; // Rotation autour de Z (pas géré).
 
  // La caméra.
  this.cameraOn = false;
  this.camera = {
    x: 0,
    y: 0,
    z: 0,
    pente: 0,
    azimut: 0,
    roulis: 0 // pas géré.
  };
  this.initialCameraPos = {
    x: 0,
    y: 0,
    z: 0,
    pente: 0,
    azimut: 0,
    roulis: 0
  };
 
  ...
};

Les premiers éléments définis permettent de gérer les déplacements suivant les trois axes. Comme nous le verrons, ces déplacements sont effectués au clavier.
La vitesse de déplacement suivant les axes X et Z est la même. La vitesse de déplacement verticale est gérée séparément. Comme nous le verrons plus tard, chaque vitesse dispose d'une fonction permettant de la modifier. Il est donc possible d'ajuster les vitesses de déplacement à la scène.
Les rotations se font à l'aide de la souris. Les variables nécessaires à cette gestion sont déclarées, y compris une variable pouvant permettre de gérer le roulis. Le code actuelle ne permet pas de modifier le roulis et n'en tient pas compte. Si vous avez des problèmes avec les noms des rotations, la rotation au tour de X correspond à la pente, la rotation au tour de Y à l'azimut et celle au tour de Z au roulis.
Un booléen permet de savoir si la caméra est activée. Ce booléen permet de gagner du temps, en évitant les calculs liés aux déplacement de la caméra, lorsque cette dernière est inactive. Elle est inactive si l'utilisateur ne peut pas modifier sa position.
Pour finir les positions courante et initiale de la caméra sont définie. La position initiale est la position dans laquelle est repositionnée la caméra lors d'une réinitialisation. Elle peut être modifiée à l'aide de la méthode setInitialCameraPosition.

Viennent ensuite les méthodes permettant d'activer la caméra et de modifier sont paramètrage par défaut :

WebGL.prototype.setInitialCameraPosition = function (x, y, z, pente, azimut, roulis) {
  this.initialCameraPos.x = x;
  this.initialCameraPos.y = y;
  this.initialCameraPos.z = z;
  this.initialCameraPos.pente = pente;
  this.initialCameraPos.azimut = azimut;
  this.initialCameraPos.roulis = roulis;
}
 
WebGL.prototype.resetCameraPosition = function () {
  this.camera.x = this.initialCameraPos.x;
  this.camera.y = this.initialCameraPos.y;
  this.camera.z = this.initialCameraPos.z;
  this.camera.pente = this.initialCameraPos.pente;
  this.camera.azimut = this.initialCameraPos.azimut;
  this.camera.roulis = this.initialCameraPos.roulis;
}
 
WebGL.prototype.setHorizontalSpeed = function (s) {
  this.vitesseHorizontale = s;
};
 
WebGL.prototype.setVerticalSpeed = function (s) {
  this.vitesseVerticale = s;
};
 
WebGL.prototype.activateCameraTracking = function () {
  this.cameraOn = true;
 
  var that = this;
  this.canvas.addEventListener("mousedown", function (evt) {
    that.handleMouseDown(evt);
  }, false);
  this.canvas.addEventListener("mousemove", function (evt) {
    that.handleMouseMove(evt);
  }, false);
  document.addEventListener("mouseup", function (evt) {
    that.mouseDownPos = null;
  }, false);
  document.addEventListener("mouseout", function (evt) {
    // On ne sait plus où est la souris => comme mouseup.
    that.mouseDownPos = null;
  }, false);
  document.addEventListener("keydown", function (evt) {
    that.handleKeyDown(evt);
  }, false);
  document.addEventListener("keyup", function (evt) {
    that.handleKeyUp(evt);
  }, false);
};

Le nom des méthodes est assez explicite, il n'est probablement pas utile de décrire plus ce qu'elles font, à part peut-être pour activateCameraTracking.
activateCameraTracking rend possible une interaction de l'utilisateur avec la caméra. Si elle n'est pas appelée, la caméra est fixe et peut simplement être positionnée à l'aide d'un appel à setInitialCameraPosition suivit d'un appel à resetCameraPosition. Fondamentalement, activateCameraTracking, met en place des gestionnaires d'événement sur le clavier et la souris.
On peut noter que le "touch" n'est pas pris en compte. Cela nécessiterait de définir des zones dans lesquels l'utilisateur devrait placer ces doigts pour avancer ou reculer ainsi que pour agir sur les angles de la caméra. Tant que ces zones et leur utilisation ne seront pas standardisées, il est probable que chaque développeur préférera les définir et les gérer lui-même. A vous de jouer...

Pour finir un certain nombre de méthodes internes permettent à la chose de fonctionner. Ces méthodes sont :

WebGL.prototype.handleKeyDown = function (evt) {
  var keycode = ((evt.which) || (evt.keyCode));
  switch (keycode) {
    case 33:
      // page up key
      this.hautBas = this.HAUT;
      break;
    case 34:
      // page down key
      this.hautBas = this.BAS;
      break;
    case 37:
      // left key
      this.droiteGauche = this.GAUCHE;
      break;
    case 38:
      // up key
      this.avantArriere = this.AVANT;
      break;
    case 39:
      // right key
      this.droiteGauche = this.DROITE;
      break;
    case 40:
      // down key
      this.avantArriere = this.ARRIERE;
      break;
  }
};
 
WebGL.prototype.handleKeyUp = function (evt) {
  var keycode = ((evt.which) || (evt.keyCode));
  switch (keycode) {
    case 33:
      // page up key
      this.hautBas = this.ARRET;
      break;
    case 34:
      // page down key
      this.hautBas = this.ARRET;
      break;
    case 37:
      // left key
      this.droiteGauche = this.ARRET;
      break;
    case 38:
      // up key
      this.avantArriere = this.ARRET;
      break;
    case 39:
      // right key
      this.droiteGauche = this.ARRET;
      break;
    case 40:
      // down key
      this.avantArriere = this.ARRET;
      break;
    case 82:
      // r key
      this.resetCameraPosition();
      break;
  }
};
 
WebGL.prototype.getMousePos = function (evt) {
  return {
    x: evt.clientX,
    y: evt.clientY
  };
};
 
WebGL.prototype.handleMouseDown = function (evt) {
  this.mouseDownPos = this.getMousePos(evt);
  this.mouseDownPente = this.camera.pente;
  this.mouseDownAzimut = this.camera.azimut;
};
 
WebGL.prototype.handleMouseMove = function (evt) {
  var mouseDownPos = this.mouseDownPos;
  if (mouseDownPos !== null) {
    var mousePos = this.getMousePos(evt);
 
    // Mise à jour de la pente.
    var yDiff = mousePos.y - mouseDownPos.y;
    this.camera.pente = this.mouseDownPente + yDiff / this.canvas.height;
 
    // Mise à jour de l'azimut.
    var xDiff = mousePos.x - mouseDownPos.x;
    this.camera.azimut = this.mouseDownAzimut + xDiff / this.canvas.width;
  }
};
 
WebGL.prototype.applyCameraPos = function () {
  if (this.cameraOn) {
    // Calcul de la nouvelle position de la caméra.
    if (this.avantArriere != this.ARRET) {
      var direction = this.avantArriere == this.AVANT ? -1 : 1;
      var distEachFrame = direction * this.vitesseHorizontale * this.getTimeInterval() / 1000;
      this.camera.z += distEachFrame * Math.cos(this.camera.azimut);
      this.camera.x += distEachFrame * Math.sin(this.camera.azimut);
    }
 
    if (this.droiteGauche != this.ARRET) {
      var direction = this.droiteGauche == this.DROITE ? 1 : -1;
      var distEachFrame = direction * this.vitesseHorizontale * this.getTimeInterval() / 1000;
      this.camera.z += distEachFrame * Math.cos(this.camera.azimut + Math.PI / 2);
      this.camera.x += distEachFrame * Math.sin(this.camera.azimut + Math.PI / 2);
    }
 
    if (this.hautBas != this.ARRET) {
      var direction = this.hautBas == this.HAUT ? 1 : -1;
      var distEachFrame = direction * this.vitesseVerticale * this.getTimeInterval() / 1000;
      this.camera.y += distEachFrame;
    }
  }
 
  // Application de la position de la caméra à la scène.
  this.mat4.rotateX(this.mvMatrix, -this.camera.pente);
  this.mat4.rotateY(this.mvMatrix, -this.camera.azimut);
  this.mat4.rotateZ(this.mvMatrix, -this.camera.roulis);
  this.mat4.translate(this.mvMatrix, [-this.camera.x, -this.camera.y, -this.camera.z]);
};

handleKeyDown et handleKeyUp gèrent les événements claviers. On peut constater que les touches gérées sont les flèches et non pas le classique "Doomesque", Z Q S D X Espace. Ce choix permet de s'affranchir de la gestion fine du clavier. En effet Z Q S D X Espace c'est pour un clavier français. Si l'utilisateur à un clavier non français, il risque de ne pas aimer. Le but n'étant pas de d'écrire une bibliothèque de gestion de tous les types de clavier possibles, les flèches permettent d'éviter le mal de tête.
On notera tout de même l'utilisation de la touche R pour Réinitialiser (Reset in English) la caméra.

Pour la gestion de la souris on trouve handleMouseDown, handleMouseMove et getMousePos. La première mémorise la position de la souris au "down", la seconde calcule les angles définis par la position courante. La troisième permet de récupérer la position de la souris.

Pour finir, applyCameraPos calcule les modifications en cours de la position de la caméra et applique la position courante de la caméra à la matrice mvMatrix. Comme cela a été présenté dans le second billet de ce tutorial WebGL, "HTML5 03 (tutorial WebGL, 2ème partie)", l'ordre géométrique des opérations est l'inverse de celui des matrices. Du point de vue géométrique on commence donc par la translation, ensuite on pivote autour de l'axe Z, puis de l'axe Y et enfin de l'axes X. Les axes sont ceux du monde WebGL.
Il faut noter que si la projection n'est pas utilisée, il est toujours possible d'utiliser la caméra. Cela dit les vitesses de déplacement par défaut ne sont absolument pas adaptée à cette situation. Il convient alors de les fixer à des valeurs de l'ordre de 0,2 unités par seconde pour la vitesse horizontale et 0,1 unités par seconde pour la vitesse verticale.

Pour que l'utilisation de la caméra soit transparente, la méthode identity a été modifiée. En effet, l'utilisation des vecteurs colonnes en WebGL, et donc l'inversion des multiplications des matrices par rapport à la transformation géométrique (cf. le second billet de ce tutorial WebGL), nous permet d'introduire une fois pour toute la matrice de transformation de la caméra ("View transform") dans notre matrice de transformation globale. Du coup la fonction identity porte mal son nom. Elle a donc été renommée en initialiseMatrix pour mieux correspondre à ce qu'elle fait. Ce renommage nécessite de reprendre toutes les pages, sauf WebGL_01.htm et WebGL_02.htm, pour utiliser le bon nom.
Le code d'initialiseMatrix est :

WebGL.prototype.initialiseMatrix = function () {
  this.mat4.identity(this.mvMatrix);
  this.mat4.identity(this.pMatrix);
 
  // Application de la caméra.
  this.applyCameraPos();
};

En résumé, pour jouer avec la caméra il faut :

  • lui donner une position initiale, à l'aide de setInitialCameraPosition, et l'appliquer à l'aide de resetCameraPosition. Si ce n'est pas fait, la position initiale est l'origine du contexte WebGL, la direction est parallèle à l'axe Z pointant vers les z négatifs. Si on ne souhaite pas que l'utilisateur puisse la déplacer, il n'y a rien d'autre à faire ;
  • fixer des vitesses de déplacement compatibles avec l'espace 3D dans lequel la caméra doit évoluer. Pour cela il faut utiliser setHorizontalSpeed et setVerticalSpeed. Les valeurs par défaut sont adaptées à un espace de l'ordre de 100 unités ;
  • activer la gestion des déplacements de la caméra à l'aide d'activateCameraTracking.

Appliqué à notre page WebGL_10.htm, cela revient simplement à modifier la méthode initWebGL en lui ajoutant la ligne gl.activateCameraTracking(); avant de définir la méthode de dessin de la scène. Disons avant la ligne où l'angle de rotation utilisé dans l'animation est défini (var angle = 0;). C'est tout, ce n'est donc pas très compliqué.

Si vous n'avez pas bien compris, ou si vous ne voulez pas taper le code, vous trouverez le code à jour ici.

Dans le prochain billet nous complexifierons la scène.

21 mai 2012

HTML5 05 (tutorial WebGL, 4ème partie)

Dans ce billet nous allons nous occuper de l'observateur que l'on appelle généralement la caméra.

Pour l'instant nous n'avons rien fait de particulier. En conséquence nous voyons un espace 3D qui se réduit à un "cube" centré sur l'origine, le point de coordonnées (0,0,0) si vous avez un doute. Le volume de ce "cube" couvre la zone -1, 1 sur les trois axes.
Dernier élément permettant de comprendre ce que nous voyons, c'est la manière dont cet espace 3D est projeté sur notre écran. Considérez que la vitre à travers laquelle vous regardez se trouve être la face avant du dit cube. Tous les points se trouvant à l'arrière de cette face y sont projetés parallèlement à l'axe des Z (projection orthogonale).

Pour vous aider à visualiser la chose nous allons créer une nouvelle page HTML, WebGL_07.htm, dans laquelle nous ajoutons au référentiel formé par nos 3 axes un trait supplémentaire.
Ce trait ne va pas être quelconque. Nous voulons qu'il soit projeté sous la forme d'un point lorsque l'axe Z, du référentiel dessiné, se trouve juste devant nous. Comme l'animation fait tourner notre figure, nous ne verrons un point que deux fois par tour. Pour faciliter la lisibilité, nous tracerons ce trait en noir. Si vous le souhaitez, vous pouvez aussi passer le fond en blanc, ce qui n'est pas fait dans le code de la page que je vous propose :

<!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">
      function initBuffers(gl) {
        var lineBuffers = {}
 
        // Les points de nos axes.
        lineBuffers.positionBuffer = gl.createArrayBuffer([
          // X
          -0.5,  0.0,  0.0,
           0.5,  0.0,  0.0,
          // Y
           0.0, -0.5,  0.0,
           0.0,  0.5,  0.0,
          // Z
           0.0,  0.0, -0.5,
           0.0,  0.0,  0.5,
          // Trait.
          -0.9,  0.3, -1.0,
          -0.9,  0.3,  1.0
 
        ]);
 
        // Les couleurs de nos 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
          [0.0, 0.0, 0.0, 1.0], // trait - Noir
          [0.0, 0.0, 0.0, 1.0]  // trait - Noir
        ];
        var colorVertices = [];
        for (var n in colors) {
          var color = colors[n];
          colorVertices = colorVertices.concat(color);
        }
        lineBuffers.colorBuffer = gl.createArrayBuffer(colorVertices);
 
        return lineBuffers;
      }
 
      function stage(gl, lineBuffers, angle) {
        // Effacement de l'image précédente.
        gl.clear();
 
        // Déplacement de l'objet.
        gl.identity();
        gl.rotate(angle, 0, 1, 0);
 
        // Dessin de l'image courante.
        gl.pushVaryingColorBuffer(lineBuffers);
        gl.pushPositionBuffer(lineBuffers);
        gl.drawLineArrays(lineBuffers);
      }
 
      function initWebGL(canvasElement) {
        // Création de notre classe d'aide.
        var gl = new WebGL(canvasElement);
 
        // Initialisation des shaders.
        gl.setShaderProgram("VARYING_COLOR");
 
        // Définition de la couleur de fond.
        gl.setClearColor(1, 0.98, 0.8, 1);
 
        // Définition des points et des couleurs.
        var lineBuffers = initBuffers(gl);
 
        // L'angle de rotation au tour de Y.
        var angle = 0;
 
        // Mise en place de la scène.
        gl.setStage(function () {
          // Calcule du nouvel angle.
          var angularVelocity = Math.PI / 4; // radians / second
          var angleEachFrame = angularVelocity * gl.getTimeInterval() / 1000;
          angle += angleEachFrame;
 
          stage(gl, lineBuffers, angle);
        });
 
        // Lancement de l'animation.
        gl.startAnimation();
      }
 
      function OnGLCanvasCreated(canvasElement, elementId) {
        window.setTimeout(function () { initWebGL(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>

Actuellement notre trait est un segment de droite qui coure du point (-0.9, 0.3, -1.0) au point (-0.9, 0.3, 1.0). Si vous affichez la page, vous constaterez que notre référentiel n'est plus basculé. En conséquence X et Z ne sont distinguables que par leur couleur.
Amusez-vous à reculer le segment au-delà de Z=-0.9 et observez ce qui se passe.
Autre paramètre avec lequel vous pouvez jouer, la longueur du segment suivant l'axe Z. Allez au-delà de la zone -1, 1.
Cela devrait vous permettre de bien "matérialiser" la zone visible et le mode de projection.

Un dernier point. Notre "cube" n'est pas vraiment carré. Ces coordonnées sont bien celle d'un cube, cela dit, à l'écran la chose n'est vraie que si le canvas contenant notre "cube" est lui-même carré. Pour vous en convaincre, modifiez le code de la page pour que la rotation se fasse au tour de l'axe Z. Il ne fait aucun doute que l'angle des axes X et Y ne reste pas droit au cours de la rotation (sauf si le canvas et un carré). La copie d'écran suivante montre ce que vous pouvez constater.
html5 05 01

Maintenant comment faire tenir une scène quelconque dans notre cube ?
Comment introduire de la perspective ?

En introduisant une seconde matrice, la matrice de projection, généralement appelée pMatrix. L'introduction d'une seconde matrice permet de traiter indépendamment l'animation de la scène et la manière dont nous la regardons. Cette matrice doit être introduite dans le code des shaders et bien sûr dans le code alimentant les shaders.
Dans ce billet nous n'utiliserons que du code shader inclus à notre classe WebGL. Le code impacté par l'introduction de cette matrice, pMatrix, dans la classe WebGLest le suivant :

var WebGL = function (canvasElement) {
  // Le canvas et le contexte WebGL (ou son équivalent IEWebGL).
  this.canvas = canvasElement;
  this.context = WebGLHelper.GetGLContext(canvasElement);
  this.stage = undefined;
 
  // Le nécessaire pour gérer l'animation.
  this.t = 0;
  this.timeInterval = 0;
  this.startTime = 0;
  this.lastTime = 0;
  this.frame = 0;
  this.animating = false;
 
  // Provided by Paul Irish.
  // Permet de faire fonctionner l'animation sur tous les butineurs.
  window.requestAnimFrame = (function (callback) {
    return window.requestAnimationFrame ||
           window.webkitRequestAnimationFrame ||
           window.mozRequestAnimationFrame ||
           window.oRequestAnimationFrame ||
           window.msRequestAnimationFrame ||
           function (callback) {
             window.setTimeout(callback, 1000 / 60);
           };
  })();
 
  // Encapsulation des matrices globales de glMatrix.
  this.mat3 = mat3;
  this.mat4 = mat4;
  this.vec3 = vec3;
 
  // Les constantes définissant les types de shader possible.
  this.FIXED_COLOR = "FIXED_COLOR";
  this.VARYING_COLOR = "VARYING_COLOR";
 
  // Le nécessaire pour WebGL.
  this.shaderProgram = null;
  this.mvMatrix = this.mat4.create();
  this.pMatrix = this.mat4.create();
 
  // La fonction qui permet à setCustomShaderProgram initialiser les shaders.
  this.initCustomShaders = undefined;
 
  // Doit-on afficher des alertes ?
  this.showAlerts = true;
 
  // Initialisation générale du contexte WebGL.
  this.context.viewportWidth = this.canvas.width;
  this.context.viewportHeight = this.canvas.height;
};
 
WebGL.prototype.identity = function () {
  this.mat4.identity(this.mvMatrix);
  this.mat4.identity(this.pMatrix);
};
 
WebGL.prototype.perspective = function (viewAngle, minDist, maxDist) {
  this.mat4.perspective(viewAngle, this.context.viewportWidth / this.context.viewportHeight, minDist, maxDist, this.pMatrix);
};
 
WebGL.prototype.getVertexShaderGLSL = function (shaderType) {
  switch (shaderType) {
    case this.FIXED_COLOR:
      return "attribute vec3 aVertexPosition;\n" +
            "uniform mat4 uMVMatrix;\n" +
            "uniform mat4 uPMatrix;\n" +
            "void main(void) {\n" +
            "gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);\n" +
            "}";
    case this.VARYING_COLOR:
      return "attribute vec3 aVertexPosition;\n" +
            "attribute vec4 aVertexColor;\n" +
            "uniform mat4 uMVMatrix;\n" +
            "uniform mat4 uPMatrix;\n" +
            "varying vec4 vColor;\n" +
            "void main(void) {\n" +
            "gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);\n" +
            "vColor = aVertexColor;\n" +
            "}";
  }
};
 
WebGL.prototype.initPositionShader = function () {
  this.shaderProgram.vertexPositionAttribute = this.context.getAttribLocation(this.shaderProgram, "aVertexPosition");
  this.context.enableVertexAttribArray(this.shaderProgram.vertexPositionAttribute);
  this.shaderProgram.pMatrixUniform = this.context.getUniformLocation(this.shaderProgram, "uPMatrix");
  this.shaderProgram.mvMatrixUniform = this.context.getUniformLocation(this.shaderProgram, "uMVMatrix");
};
 
WebGL.prototype.setMatrixUniforms = function () {
  this.context.uniformMatrix4fv(this.shaderProgram.pMatrixUniform, false, this.pMatrix);
  this.context.uniformMatrix4fv(this.shaderProgram.mvMatrixUniform, false, this.mvMatrix);
};

Vous aurez constaté l'apparition d'une, et d'une seule, nouvelle méthode, perspective.
Première chose importante, toutes nos pages fonctionnent à l'identique. Il n'y a donc pas obligation d'utiliser pMatrix. Cela est dû au fait de la méthode identity initialise les deux matrices. En conséquence, toutes les autres méthodes manipulant les matrices pMatrix et mvMatrix doivent être appelées après identity qui, elle, doit toujours être appelées.
Pour positionner pMatrix nous utiliserons perspective. Cette dernière possède trois paramètres.
Le premier permet de définir l'angle du "cône" (pyramide tronquée pour être plus précis car la section est rectangulaire) de notre "objectif" (l'objectif de notre caméra).
Le second défini la position du plan à partir duquel nous voyons quelque chose. Ce plan est perpendiculaire à l'axe Z et ce que nous définissons c'est la distance de ce plan par rapport à l'origine.
Le troisième défini la position du plan au-delà duquel nous ne voyons plus rien. Ce plan est naturellement perpendiculaire à l'axe Z et donc parallèle au plan précédent.

Cela dit cette fonction définit implicitement d'autres choses sur lesquels nous n'avons pas la main.
Pour commencer, elle positionne la caméra sur l'origine et la braque vers l'arrière en plein sur l'axe Z. Si l'on revient aux paramètres de la fonction, les deuxième et troisième correspondent donc en fait à des points situés sur la partie négative de l'axe Z
Autre chose, elle introduit un ratio entre la largeur et la hauteur. Cela a pour effet de rétablir les angles droits. Si vous reprenez le code de la page où vous faites tourner le référentiel autour de l'axe des Z, que vous le modifiez légèrement pour utiliser la méthode perspective, vous constaterez que vous obtenez cela :
html5 05 02
Si vous avez du mal à obtenir ce résultat, voici le code complet de la page WebGL_08.htmcorrespondante à la capture :

<!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">
      function initBuffers(gl) {
        var lineBuffers = {}
 
        // Les points de nos axes.
        lineBuffers.positionBuffer = gl.createArrayBuffer([
          // X
          -0.5,  0.0,  0.0,
           0.5,  0.0,  0.0,
          // Y
           0.0, -0.5,  0.0,
           0.0,  0.5,  0.0,
          // Z
           0.0,  0.0, -0.5,
           0.0,  0.0,  0.5
        ]);
 
        // Les couleurs de nos 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);
        }
        lineBuffers.colorBuffer = gl.createArrayBuffer(colorVertices);
 
        return lineBuffers;
      }
 
      function stage(gl, lineBuffers, angle) {
        // Effacement de l'image précédente.
        gl.clear();
 
        // Initialisation des matrices.
        gl.identity();
 
        // Définition de la perspective.
        gl.perspective(45, 0.1, 10.0);
 
        // Déplacement de l'objet.
        gl.translate(0.0, 0.0, -1.0);
        gl.rotate(angle, 0, 0, 1);
 
        // Dessin de l'image courante.
        gl.pushVaryingColorBuffer(lineBuffers);
        gl.pushPositionBuffer(lineBuffers);
        gl.drawLineArrays(lineBuffers);
      }
 
      function initWebGL(canvasElement) {
        // Création de notre classe d'aide.
        var gl = new WebGL(canvasElement);
 
        // Initialisation des shaders.
        gl.setShaderProgram("VARYING_COLOR");
 
        // Définition de la couleur de fond.
        gl.setClearColor(1, 0.98, 0.8, 1);
 
        // Définition des points et des couleurs.
        var lineBuffers = initBuffers(gl);
 
        // L'angle de rotation au tour de Z.
        var angle = 0;
 
        // Mise en place de la scène.
        gl.setStage(function () {
          // Calcule du nouvel angle.
          var angularVelocity = Math.PI / 4; // radians / second
          var angleEachFrame = angularVelocity * gl.getTimeInterval() / 1000;
          angle += angleEachFrame;
 
          stage(gl, lineBuffers, angle);
        });
 
        // Lancement de l'animation.
        gl.startAnimation();
      }
 
      function OnGLCanvasCreated(canvasElement, elementId) {
        window.setTimeout(function () { initWebGL(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>

On constate immédiatement deux choses :

  1. le référentiel est plus gros ;
  2. les angles sont bien droits. Conséquences, les axes ne pointent plus vers les coins du canvas (sauf s'il est carré).

Détaillons le code de notre page pour comprendre ce qui se passe.
Le positionnement du point de vue est réalisé à l'aide de l'appelle suivant : gl.perspective(45, 0.1, 10.0);.
Cela signifie que nous avons un angle de vue, un "cône", de 45°. La pointe de ce "cône" est placée à l'origine du contexte WebGL. L'axe du "cône" est l'axe Z vers les z négatifs. Nous voyons ce qui se trouve dans ce "cône" entre les positions -0,1 et -10,0 de l'axe Z.
Pour rendre notre référentiel visible, nous l'avons reculé à l'aide de gl.translate(0.0, 0.0, -1.0);. Mais pourquoi est-il plus gros ?
Deux interprétations sont possibles :

  1. nous sommes plus près ;
  2. notre "focale" est trop petite.

Pour le premier point, on comprend bien que, maintenant que la perspective est introduite, plus l'objet est près, plus il est gros. Amusez-vous à reculer la scène pour vous en convaincre. Si vous ne voyez plus rien, c'est probablement que vous êtes allé trop loin. N'oubliez pas que nous avons limitez la zone visible à 0,1 - 10,0.
Après avoir remis le référentiel à -1,0, modifiez l'angle de l'objectif. Si vous le réduisez, l'objet grossi. Si vous l'augmentez, l'objet diminue.

Maintenant que se passe-t-il si nous laissons notre scène en (0, 0, 0) ?
Pour cela, créons une nouvelle page, WebGL_09.htm donc, dans laquelle nous reprenons le positionnement du référentiel que nous avions mis en place lors du précédent billet. La fonction stageest alors :

function stage(gl, lineBuffers, angle) {
  // Effacement de l'image précédente.
  gl.clear();
 
  // Initialisation des matrices.
  gl.identity();
 
  // Définition de la perspective.
  gl.perspective(45, 0.1, 10.0);
 
  // Déplacement de l'objet.
  // On met l'objet dans le champs de vision.
  //gl.translate(0.0, 0.0, -0.08);
  //gl.translate(0.0, 0.0, -0.002);
  // Rotation au tour de X.
  gl.rotate(Math.PI * 0.3, 1, 0, 0);
  // Rotation au tour de Y.
  gl.rotate(angle, 0, 1, 0);
 
  // Dessin de l'image courante.
  gl.pushVaryingColorBuffer(lineBuffers);
  gl.pushPositionBuffer(lineBuffers);
  gl.drawLineArrays(lineBuffers);
}

Avec le code ci-dessus vous ne devez plus rien voir. Est-ce normal ?
Pour tenter de répondre à cette question, enlevez les commentaires de la ligne gl.translate(0.0, 0.0, -0.08);. Vous devez voir quelque chose comme
html5 05 03
La zone non dessinée correspond donc à ce qui se trouve avant le plan situé à z=-0,1.
Mais comment expliquer que sans translation nous ne voyons plus rien ?
Une explication peut-être que la figure n'est plus dans le cône.
Augmentons donc l'angle passé à la fonction perspective.
A priori ce n'est pas la solution car nous ne voyons toujours rien.
Si nous cumulons avec la translation à -0,08, l'effet est bien celui attendu.

La rotation initiale au tour de l'axe X introduit un doute quant au fait que notre scène puisse sortir du "cône" de visibilité. Eliminons se doute en mettant en commentaire cette rotation.

Sans cette rotation, il est maintenant certain que les axes X et Z, de notre référentiel dessiné, resteront toujours dans le plan XZ du contexte WebGL. En conséquence, quel que soit l'angle du cône, à un moment ou un autre nous devrions voir quelque chose. Rappelons que nous traçons la portion -0,5 à 0,5 de nos axes, nous dépassons donc sans problème le plan initial qui se trouve en -0,1 même lorsque le centre de nos axes est au centre du contexte WebGL.

Et bien même dans ces conditions, vous ne verrez probablement rien en dessous d'une translation inférieure à -0,002, où il faut déjà être particulièrement attentif (avec un fond noir c'est plus facile).
Pour expliquer ce phénomène il y a deux possibilités. De manière imagée, on peut considérer que cela est dû à un effet de loupe qui fait qu'un tout petit déplacement de notre référentiel dessiné, provoque une grande augmentation de la zone invisible. La zone visible semble alors repoussée à l'infini. De manière plus mathématique, les projections nous conduisent à diviser par un nombre qui tend vers 0, le résultat tend donc bien vers l'infini.
Pour le matérialiser, faite des translations de plus en plus petites. La limite pour voir quelque chose se situe vers -0,002 avec un angle de 45°.

Maintenant que nous avons introduit la perspective, et ses déformations, comment peut-on bouger la caméra ?
En fait on ne la bouge pas !
La chose coûterait trop cher en temps de calcul et n'est pas très simple à coder. Alors comment fait-on ?
En bougeant la scène dans le sens contraire.
En effet bouger la caméra vers la gauche revient à déplacer la scène vers la droite d'une même distance.
Pour les rotations, même constat.

Mais alors, qu'est-ce qui différentie un mouvement de caméra de l'animation "normale" de la scène" ?
Une seule chose.
Un mouvement de caméra est provoqué par l'utilisateur.
Si ce n'est pas le cas, il s'agit d'un film pas de la présentation d'un "monde 3D".

Nous introduirons l'interaction de l'utilisateur dans le prochain billet. En ce qui concerne le présent billet, nous allons simplement construire une scène un poil plus intéressante.
Déjà, grâce à la perspective, nous ne sommes plus cantonnés au petit monde initial que constitue le "cube" représentant l'espace 3D WebGL. Nous allons donc utiliser une plage plus importante pour notre monde virtuel. D'autre part il s'agirait d'avoir quelque chose à visualiser. Nous allons donc tracer une courbe.

Le code de notre nouvel page, WebGL_10.htm, est 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">
      function initBuffers(gl) {
        var lineBuffers = {}
 
        // Les points de nos axes.
        var points = [
          // X
          -10.0,  0.0,  0.0,
           10.0,  0.0,  0.0,
          // Y
           0.0, -10.0,  0.0,
           0.0,  10.0,  0.0,
          // Z
           0.0,  0.0, -10.0,
           0.0,  0.0,  10.0
        ];
        var x = 10.0;
        var y = -10.0
        var z = 0.0;
        var teta = 0.0;
        points = points.concat(x);
        points = points.concat(y);
        points = points.concat(z);
        while (true) {
          teta += Math.PI / 16;
          x = 10 * Math.cos(teta);
          y += 0.25;
          z = 10 * Math.sin(teta);
 
          points = points.concat(x);
          points = points.concat(y);
          points = points.concat(z);
 
          if (y >= 10.0)
            break;
 
          points = points.concat(x);
          points = points.concat(y);
          points = points.concat(z);
        }
 
        lineBuffers.positionBuffer = gl.createArrayBuffer(points);
 
        // Les couleurs de nos 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);
        }
        for (var i = 0; i < lineBuffers.positionBuffer.numElements - 6; i++) {
          colorVertices = colorVertices.concat([0.0, 0.0, 0.0, 1.0]);
        }
        lineBuffers.colorBuffer = gl.createArrayBuffer(colorVertices);
 
        return lineBuffers;
      }
 
      function stage(gl, lineBuffers, angle) {
        // Effacement de l'image précédente.
        gl.clear();
 
        // Initialisation des matrices.
        gl.identity();
 
        // Définition de la perspective.
        // "Ouverture" de l'objectif, 45°.
        // Zone visible, 0,1 à 100.
        gl.perspective(45, 0.1, 100.0);
 
        // Déplacement de l'objet.
        // On met l'objet dans le champs de vision.
        gl.translate(0.0, 0.0, -50.0);
        // Rotation au tour de X pour permettre la visualisation du plan XZ.
        gl.rotate(Math.PI * 0.1, 1, 0, 0);
        // Rotation au tour de Y.
        gl.rotate(angle, 0, 1, 0);
 
        // Dessin de l'image courante.
        gl.pushVaryingColorBuffer(lineBuffers);
        gl.pushPositionBuffer(lineBuffers);
        gl.drawLineArrays(lineBuffers);
      }
 
      function initWebGL(canvasElement) {
        // Création de notre classe d'aide.
        var gl = new WebGL(canvasElement);
 
        // Initialisation des shaders.
        gl.setShaderProgram("VARYING_COLOR");
 
        // Définition de la couleur de fond.
        gl.setClearColor(1, 0.98, 0.8, 1);
 
        // Définition des points et des couleurs.
        var lineBuffers = initBuffers(gl);
 
        // L'angle de rotation au tour de Y.
        var angle = 0;
 
        // Mise en place de la scène.
        gl.setStage(function () {
          // Calcule du nouvel angle.
          var angularVelocity = Math.PI / 4; // radians / second
          var angleEachFrame = angularVelocity * gl.getTimeInterval() / 1000;
          angle += angleEachFrame;
 
          stage(gl, lineBuffers, angle);
        });
 
        // Lancement de l'animation.
        gl.startAnimation();
      }
 
      function OnGLCanvasCreated(canvasElement, elementId) {
        window.setTimeout(function () { initWebGL(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>

A l'écran cela donne :
html5 05 04
Notre courbe est donc une spirale, calculée avec un pas que vous pouvez modifier (dans le code) pour faire plus ou moins rond suivant ce que vous désirez, et ce que votre machine supporte.
Nous dessinons maintenant dans un volume plus important. Les coordonnées que nous utilisons sur chaque axe vont de -10 à +10.
La zone visible va de 0,1 à 100.
Pour faire simple, le centre de notre dessin à été placé en z=-50, au milieu de la zone visible.

Rendez-vous au prochain billet pour introduire les interactions utilisateur

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

16 mai 2012

HTML5 04 (tutorial WebGL, 3ème partie)

Dans le précédent billet nous avons pu visualiser les trois axes du référentiel du contexte WebGL. Nous avons même réussi à faire se déplacer le référentiel. Cela dit, à moins d'être très attentif et de ne pas quitter des yeux le référentiel, il est assez hasardeux de dire à un instant t, quel est l'axe X ou quel est l'axe Z. L'axe Y étant l'axe de rotation, il est plus facile à identifier.
Pour faciliter l'identification des axes, nous allons attribuer à chacun une couleur particulière.

Pour faire cela nous allons devoir modifier le code des DEUX shaders. Evidement cette modification devra s'accompagner une modification équivalente de leur initialisation.
Cela met en évidence le fait que le code des shaders ne peut pas être totalement quelconque par rapport à notre classe WebGL.
Pour gérer ce problème il y a deux approches :

  1. inclure le code des shaders dans la classe. C'est la solution adoptée dans le livre HTML5 Canvas Cookbook ;
  2. laisser chacun définir le code de ses shaders et fournir du coup une librairie contenant beaucoup moins de chose. C'est la solution adoptée dans le livre Professional WebGL Programming: Developing 3D Graphics for the Web.

La solution retenue pour ces billets est la première, tout en laissant la porte ouverte (ce que ne fait pas le code du livre) à des shaders "custom". La version finale de la classe WebGL que nous construisons peut d'ailleurs être prise pour une version "améliorée" de celle du livre. Améliorée, car elle offre de nouvelles possibilités, pour peu de l'on garde ce qui est présent dans la version du livre.
Dans cette optique, nous allons redécouper le code actuel de notre classe pour nous conformer au découpage de la classe WebGL de HTML5 Canvas Cookbook.
La nouvelle version du Javascript, WebGL_2.js, est la suivante :

/******************************************************************************
 * Classe permettant de faciliter la gestion de WebGL avec prise en compte
 * des l'animation.
 * Ce code est inspiré de celui du livre HTML5 Canvas Cookbook qui se trouve
 * à l'adresse suivante : http://www.html5canvastutorials.com/cookbook/
 ******************************************************************************/
var WebGL = function (canvasElement) {
  // Le canvas et le contexte WebGL (ou son équivalent IEWebGL).
  this.canvas = canvasElement;
  this.context = WebGLHelper.GetGLContext(canvasElement);
  this.stage = undefined;
 
  // Le nécessaire pour gérer l'animation.
  this.t = 0;
  this.timeInterval = 0;
  this.startTime = 0;
  this.lastTime = 0;
  this.frame = 0;
  this.animating = false;
 
  // Provided by Paul Irish.
  // Permet de faire fonctionner l'animation sur tous les butineurs.
  window.requestAnimFrame = (function (callback) {
    return window.requestAnimationFrame ||
           window.webkitRequestAnimationFrame ||
           window.mozRequestAnimationFrame ||
           window.oRequestAnimationFrame ||
           window.msRequestAnimationFrame ||
           function (callback) {
             window.setTimeout(callback, 1000 / 60);
           };
  })();
 
  // Encapsulation des matrices globales de glMatrix.
  this.mat3 = mat3;
  this.mat4 = mat4;
  this.vec3 = vec3;
 
  // Les constantes définissant les types de shader possible.
  this.FIXED_COLOR = "FIXED_COLOR";
  this.VARYING_COLOR = "VARYING_COLOR";
 
  // Le nécessaire pour WebGL.
  this.shaderProgram = null;
  this.mvMatrix = this.mat4.create();
 
  // La fonction qui permet à setCustomShaderProgram initialiser les shaders.
  this.initCustomShaders = undefined;
 
  // Doit-on afficher des alertes ?
  this.showAlerts = true;
 
  // Initialisation générale du contexte WebGL.
  this.context.viewportWidth = this.canvas.width;
  this.context.viewportHeight = this.canvas.height;
};
 
/******************************************************************************
 * Méthodes générales.
 ******************************************************************************/
WebGL.prototype.getContext = function () {
  return this.context;
};
 
WebGL.prototype.getCanvas = function () {
  return this.canvas;
};
 
WebGL.prototype.setClearColor = function (r, g, b, a) {
  this.context.clearColor(r, g, b, a);
};
 
WebGL.prototype.clear = function () {
  this.context.viewport(0, 0, this.context.viewportWidth, this.context.viewportHeight);
  this.context.clear(this.context.COLOR_BUFFER_BIT);
};
 
WebGL.prototype.setStage = function (func) {
  this.stage = func;
};
 
/******************************************************************************
 * Gestion de l'animation.
 ******************************************************************************/
WebGL.prototype.isAnimating = function () {
  return this.animating;
};
 
WebGL.prototype.getFrame = function () {
  return this.frame;
};
 
WebGL.prototype.startAnimation = function () {
  this.animating = true;
  var date = new Date();
  this.startTime = date.getTime();
  this.lastTime = this.startTime;
 
  if (this.stage !== undefined) {
    this.stage();
  }
 
  this.animationLoop();
};
 
WebGL.prototype.stopAnimation = function () {
  this.animating = false;
};
 
WebGL.prototype.getTimeInterval = function () {
  return this.timeInterval;
};
 
WebGL.prototype.getTime = function () {
  return this.t;
};
 
WebGL.prototype.getFps = function () {
  return this.timeInterval > 0 ? 1000 / this.timeInterval : 0;
};
 
WebGL.prototype.animationLoop = function () {
  var that = this;
 
  this.frame++;
  var date = new Date();
  var thisTime = date.getTime();
  this.timeInterval = thisTime - this.lastTime;
  this.t += this.timeInterval;
  this.lastTime = thisTime;
 
  if (this.stage !== undefined) {
    this.stage();
  }
 
  if (this.animating) {
    requestAnimFrame(function () {
      that.animationLoop();
    });
  }
};
 
/******************************************************************************
 * Gestion de WebGL.
 ******************************************************************************/
// Construction du shader program en utilisant les codes shader internes.
WebGL.prototype.setShaderProgram = function (shaderType) {
  // Récupération des codes sources.
  var fragmentGLSL = this.getFragmentShaderGLSL(shaderType);
  var vertexGLSL = this.getVertexShaderGLSL(shaderType);
 
  // Création du programme.
  this.buildShaderProgram(fragmentGLSL, vertexGLSL);
 
  // Initialisation des shaders.
  this.initShaders(shaderType);
};
 
// Construction du shader program en utilisant des codes shader externes.
// Il faut impérativement définir initCustomShaders à l'aide de
// setInitCustomShaders AVANT d'appeler setCustomShaderProgram.
WebGL.prototype.setInitCustomShaders = function (func) {
  this.initCustomShaders = func;
};
 
WebGL.prototype.setCustomShaderProgram = function (vertexId, fragmentId) {
  // Récupération des codes sources.
  var vertexCode = document.getElementById(vertexId).firstChild.nodeValue;
  var fragmentCode = document.getElementById(fragmentId).firstChild.nodeValue;
 
  // Création du programme.
  this.buildShaderProgram(fragmentCode, vertexCode);
 
  // Initialisation des shaders.
  if (this.initCustomShaders !== undefined) {
    this.initCustomShaders();
  }
  else {
    console.log("initCustomShaders is undefined");
    if (this.showAlerts) {
      alert("initCustomShaders is undefined");
    }
  }
};
 
// Méthodes de manipulation des matrices.
WebGL.prototype.identity = function () {
  this.mat4.identity(this.mvMatrix);
};
 
WebGL.prototype.translate = function (x, y, z) {
  this.mat4.translate(this.mvMatrix, [x, y, z]);
};
 
WebGL.prototype.rotate = function (angle, x, y, z) {
  this.mat4.rotate(this.mvMatrix, angle, [x, y, z]);
};
 
// Création/initialisation d'un tampon WebGL.
WebGL.prototype.createArrayBuffer = function (vertices) {
  var buffer = this.context.createBuffer();
  buffer.numElements = vertices.length;
  this.context.bindBuffer(this.context.ARRAY_BUFFER, buffer);
  this.context.bufferData(this.context.ARRAY_BUFFER, new Float32Array(vertices), this.context.STATIC_DRAW);
  return buffer;
};
 
// Méthodes associant les tampons WebGL avec les shaders internes.
WebGL.prototype.pushPositionBuffer = function (buffers) {
  this.context.bindBuffer(this.context.ARRAY_BUFFER, buffers.positionBuffer);
  this.context.vertexAttribPointer(this.shaderProgram.vertexPositionAttribute, 3, this.context.FLOAT, false, 0, 0);
};
 
WebGL.prototype.pushFixedColor = function (r, g, b, a) {
  this.context.uniform4fv(this.shaderProgram.uColor, [r, g, b, a]);
};
 
WebGL.prototype.pushVaryingColorBuffer = function (buffers) {
  this.context.bindBuffer(this.context.ARRAY_BUFFER, buffers.colorBuffer);
  this.context.vertexAttribPointer(this.shaderProgram.vertexColorAttribute, 4, this.context.FLOAT, false, 0, 0);
};
 
// Méthodes provoquant le rendu de la scène à partir de points.
WebGL.prototype.drawLineArrays = function (buffers) {
  this.setMatrixUniforms();
  // draw arrays
  this.context.drawArrays(this.context.LINES, 0, buffers.positionBuffer.numElements / 3);
};
 
/******************************************************************************
 * Méthodes de services WebGL internes à la classe.
 * Ne devraient pas être appelées de l'extérieur.
 ******************************************************************************/
// Les différents codes shader gérés par la classe.
WebGL.prototype.getFragmentShaderGLSL = function (shaderType) {
  switch (shaderType) {
    case this.FIXED_COLOR:
      return "#ifdef GL_ES\n" +
            "precision highp float;\n" +
            "#endif\n" +
            "uniform vec4 uColor;\n" +
            "void main(void) {\n" +
            "gl_FragColor = uColor;\n" +
            "}";
    case this.VARYING_COLOR:
      return "#ifdef GL_ES\n" +
            "precision highp float;\n" +
            "#endif\n" +
            "varying vec4 vColor;\n" +
            "void main(void) {\n" +
            "gl_FragColor = vColor;\n" +
            "}";
  }
};
 
WebGL.prototype.getVertexShaderGLSL = function (shaderType) {
  switch (shaderType) {
    case this.FIXED_COLOR:
      return "attribute vec3 aVertexPosition;\n" +
            "uniform mat4 uMVMatrix;\n" +
            "void main(void) {\n" +
            "gl_Position = uMVMatrix * vec4(aVertexPosition, 1.0);\n" +
            "}";
    case this.VARYING_COLOR:
      return "attribute vec3 aVertexPosition;\n" +
            "attribute vec4 aVertexColor;\n" +
            "uniform mat4 uMVMatrix;\n" +
            "varying vec4 vColor;\n" +
            "void main(void) {\n" +
            "gl_Position = uMVMatrix * vec4(aVertexPosition, 1.0);\n" +
            "vColor = aVertexColor;\n" +
            "}";
  }
};
 
// Initialisation des shaders gérés par notre .
WebGL.prototype.initShaders = function (shaderType) {
  this.initPositionShader();
 
  switch (shaderType) {
    case this.FIXED_COLOR:
      this.initFixedColorShader();
      break;
    case this.VARYING_COLOR:
      this.initVaryingColorShader();
      break;
  }
};
 
WebGL.prototype.initPositionShader = function () {
  this.shaderProgram.vertexPositionAttribute = this.context.getAttribLocation(this.shaderProgram, "aVertexPosition");
  this.context.enableVertexAttribArray(this.shaderProgram.vertexPositionAttribute);
  this.shaderProgram.mvMatrixUniform = this.context.getUniformLocation(this.shaderProgram, "uMVMatrix");
};
 
WebGL.prototype.initFixedColorShader = function () {
  this.shaderProgram.uColor = this.context.getUniformLocation(this.shaderProgram, "uColor");
};
 
WebGL.prototype.initVaryingColorShader = function () {
  this.shaderProgram.vertexColorAttribute = this.context.getAttribLocation(this.shaderProgram, "aVertexColor");
  this.context.enableVertexAttribArray(this.shaderProgram.vertexColorAttribute);
};
 
// Génération du programme shader.
WebGL.prototype.buildShaderProgram = function (fragmentGLSL, vertexGLSL) {
  // Compilation du fragment shader.
  var fragmentShader = this.context.createShader(this.context.FRAGMENT_SHADER);
  this.context.shaderSource(fragmentShader, fragmentGLSL);
  this.context.compileShader(fragmentShader);
 
  // Compilation du vertex shader.
  var vertexShader = this.context.createShader(this.context.VERTEX_SHADER);
  this.context.shaderSource(vertexShader, vertexGLSL);
  this.context.compileShader(vertexShader);
 
  // Création du programme.
  this.shaderProgram = this.context.createProgram();
  this.context.attachShader(this.shaderProgram, vertexShader);
  this.context.attachShader(this.shaderProgram, fragmentShader);
  this.context.linkProgram(this.shaderProgram);
 
  // Pour savoir s'il y a eu un problème dans les phases précédentes.
  if (!this.context.getShaderParameter(fragmentShader, this.context.COMPILE_STATUS)) {
    console.log(this.context.getShaderInfoLog(fragmentShader));
    if (this.showAlerts) {
      alert("Could not compile fragment shaders");
    }
  }
  if (!this.context.getShaderParameter(vertexShader, this.context.COMPILE_STATUS)) {
    console.log(this.context.getShaderInfoLog(vertexShader));
    if (this.showAlerts) {
      alert("Could not compile vertex shaders");
    }
  }
  if (!this.context.getProgramParameter(this.shaderProgram, this.context.LINK_STATUS)) {
    console.log(this.context.getProgramInfoLog(program));
    if (this.showAlerts) {
      alert("Could not initialize shaders");
    }
  }
 
  this.context.useProgram(this.shaderProgram);
};
 
// Mise en place des matrices de transformations.
WebGL.prototype.setMatrixUniforms = function () {
  this.context.uniformMatrix4fv(this.shaderProgram.mvMatrixUniform, false, this.mvMatrix);
};

Voyons maintenant comment utiliser cette nouvelle version de la classe WebGL.js ?

Commençons par une version "custom", c'est à dire une version où le code des shaders est placé dans des balises scripts dédiées.
Pour cela recopions le code de la page WebGL_02.htm pour créer la page WebGL_03.htm. Cela fait nous pouvons remodeler le code pour l'adapter à son nouveau contexte. Nous obtenons alors :



<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 id="vertex" type="x-shader">
      attribute vec3 aVertexPosition;
 
      uniform mat4 uMVMatrix;
 
      void main(void) {
        gl_Position = uMVMatrix * vec4(aVertexPosition, 1.0);
      }
    </script>
    <script id="fragment" type="x-shader">
      #ifdef GL_ES
        precision highp float;
      #endif
 
      uniform vec4 uColor;
 
      void main(void) {
        gl_FragColor = uColor;
      }
    </script>
    <script type="text/javascript">
      function stage(gl, vbuffer, angle) {
        // Effacement de l'image précédente.
        gl.clear();
 
        // Déplacement de l'objet.
        gl.identity();
        //gl.translate(0.5, 0.0, 0.0);
        // Rotation au tour de X.
        gl.rotate(Math.PI * 0.3, 1, 0, 0);
        // Rotation au tour de Y.
        gl.rotate(angle, 0, 1, 0);
        //gl.translate(0.5, 0.0, 0.0);
 
        // Dessin de l'image courante.
        gl.context.uniform4fv(gl.shaderProgram.uColor, [1.0, 0.0, 0.0, 1.0]);
        gl.context.vertexAttribPointer(gl.shaderProgram.aVertexPosition, 3, gl.context.FLOAT, false, 0, 0);
        gl.context.uniformMatrix4fv(gl.shaderProgram.uMVMatrix, false, gl.mvMatrix);
        gl.context.drawLineArrays(gl.context.LINES, 0, vbuffer.numElements / 3);
      }
 
      function initWebGL(canvasElement) {
        // Création de notre classe d'aide.
        var gl = new WebGL(canvasElement);
 
        // Initialisation des shaders.
        gl.setInitCustomShaders(function () {
          gl.shaderProgram.uColor = gl.context.getUniformLocation(gl.shaderProgram, "uColor");
          gl.shaderProgram.aVertexPosition = gl.context.getAttribLocation(gl.shaderProgram, "aVertexPosition");
          gl.context.enableVertexAttribArray(gl.shaderProgram.aVertexPosition);
          gl.shaderProgram.uMVMatrix = gl.context.getUniformLocation(gl.shaderProgram, "uMVMatrix");
        });
        gl.setCustomShaderProgram("vertex", "fragment");
 
        // Définition de la couleur de fond.
        gl.setClearColor(1, 0.98, 0.8, 1);
 
        // Définition des points de nos axes.
        var vertices = new Float32Array([
            -0.5,  0.0,  0.0, 0.5, 0.0, 0.0, // axe des X
             0.0, -0.5,  0.0, 0.0, 0.5, 0.0, // axe des Y
             0.0,  0.0, -0.5, 0.0, 0.0, 0.5  // axe des Z
          ]);
        var vbuffer = gl.createArrayBuffer(vertices);
 
        // L'angle de rotation au tour de Y.
        var angle = 0;
 
        // Mise en place de la scène.
        gl.setStage(function () {
          // Calcule du nouvel angle.
          var angularVelocity = Math.PI / 4; // radians / second
          var angleEachFrame = angularVelocity * gl.getTimeInterval() / 1000;
          angle += angleEachFrame;
 
          stage(gl, vbuffer, angle);
        });
 
        // Lancement de l'animation.
        gl.startAnimation();
      }
 
      function OnGLCanvasCreated(canvasElement, elementId) {
        window.setTimeout(function () { initWebGL(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>

On peut voir que c'est la méthode setCustomShaderProgram qui est utilisée pour générer le programme shader. De ce fait, initCustomShaders doit être définie avant. Cela est réalisé avec la méthode setInitCustomShaders.
Pour dessiner la scène, on trouve à la fin de la méthode locale stage, le code positionnant les tampons et forçant le dessin.

Voyons maintenant la solution utilisant le code des shaders se trouvant dans le classe WebGL. Dans le rar disponible à la fin de ce billet il s'agit de la page WebGL_04.htm:



<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">
      function stage(gl, lineBuffers, angle) {
        // Effacement de l'image précédente.
        gl.clear();
 
        // Déplacement de l'objet.
        gl.identity();
        //gl.translate(0.5, 0.0, 0.0);
        // Rotation au tour de X.
        gl.rotate(Math.PI * 0.3, 1, 0, 0);
        // Rotation au tour de Y.
        gl.rotate(angle, 0, 1, 0);
        //gl.translate(0.5, 0.0, 0.0);
 
        // Dessin de l'image courante.
        gl.pushFixedColor(1.0, 0.0, 0.0, 1.0);
        gl.pushPositionBuffer(lineBuffers);
        gl.drawLines(lineBuffers);
      }
 
      function initWebGL(canvasElement) {
        // Création de notre classe d'aide.
        var gl = new WebGL(canvasElement);
 
        // Initialisation des shaders.
        gl.setShaderProgram("FIXED_COLOR");
 
        // Définition de la couleur de fond.
        gl.setClearColor(1, 0.98, 0.8, 1);
 
        // Définition des tampons nécessaires à la construction de la scène.
        var lineBuffers = {}
        // Les points de nos axes.
        lineBuffers.positionBuffer = gl.createArrayBuffer([
        // X
          -0.5,  0.0,  0.0,
           0.5,  0.0,  0.0,
        // Y
           0.0, -0.5,  0.0,
           0.0,  0.5,  0.0,
        // Z
           0.0,  0.0, -0.5,
           0.0,  0.0,  0.5
        ]);
        // La couleur étant fixe c'est fini.
 
        // L'angle de rotation au tour de Y.
        var angle = 0;
 
        // Mise en place de la scène.
        gl.setStage(function () {
          // Calcule du nouvel angle.
          var angularVelocity = Math.PI / 4; // radians / second
          var angleEachFrame = angularVelocity * gl.getTimeInterval() / 1000;
          angle += angleEachFrame;
 
          stage(gl, lineBuffers, angle);
        });
 
        // Lancement de l'animation.
        gl.startAnimation();
      }
 
      function OnGLCanvasCreated(canvasElement, elementId) {
        window.setTimeout(function () { initWebGL(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>

Sans surprise cette version est plus compacte vue qu'elle ne comporte plus de code source shader et qu'elle délègue à WebGL la gestion fine des shaders. Notons qu'il faut tout de même ne pas oublier d'appeler les méthodes offertes par WebGL pour positionner les tampons et forcer le dessin.

Nous allons partir de cette dernière version pour créer une nouvelle page, WebGL_05.htm, dans laquelle chaque axe à sa propre couleur.
Si vous avez lu attentivement le code de la classe WebGL, vous avez dû voir qu'il y a déjà ce qu'il faut.
Les shaders correspondants sont obtenus à l'aide de l'appel setShaderProgram("VARYING_COLOR"); qui remplace setShaderProgram("FIXED_COLOR");.
Evidemment, il faut définir les couleurs associées à chaque élément dessiné. Nous n'allons pas faire compliqué. X en rouge, Y en bleu et Z en vert. Comme nous avons maintenant plusieurs tableaux à définir nous allons ajoutez une méthode locale dédiée, initBuffers. Son code est le suivant :

function initBuffers(gl) {
  var lineBuffers = {}
 
  // Les points de nos axes.
  lineBuffers.positionBuffer = gl.createArrayBuffer([
  // X
    -0.5,  0.0,  0.0,
     0.5,  0.0,  0.0,
  // Y
     0.0, -0.5,  0.0,
     0.0,  0.5,  0.0,
  // Z
     0.0,  0.0, -0.5,
     0.0,  0.0,  0.5
  ]);
 
  // Les couleurs de nos axes.
  var colors = [
    [1.0, 0.0, 0.0, 1.0], // X - Red
    [0.0, 1.0, 0.0, 1.0], // Y - Green
    [0.0, 0.0, 1.0, 1.0]  // Z - Blue
  ];
  var colorVertices = [];
  for (var n in colors) {
    var color = colors[n];
    for (var i = 0; i < 2; i++) {
      colorVertices = colorVertices.concat(color);
    }
  }
  lineBuffers.colorBuffer = gl.createArrayBuffer(colorVertices);
 
  return lineBuffers;
}

A part appeler la méthode que nous venons de définir, l'autre modification à faire consiste à remplacer gl.pushFixedColor(1.0, 0.0, 0.0, 1.0); par gl.pushVaryingColorBuffer(lineBuffers);. Cela fait vous devez obtenir le résultat suivant :
html5 04 01
Imaginons que nous voulions, en plus, distinguer la partie négative de la partie positive. Une première solution consiste à tracer des demi-axes de couleur différente. Cela s'obtient en modifiant initBuffersde la manière suivante :

function initBuffers(gl) {
  var lineBuffers = {}
 
  // Les points de nos axes.
  lineBuffers.positionBuffer = gl.createArrayBuffer([
  // X
    -0.5,  0.0,  0.0,
     0.0,  0.0,  0.0,
     0.0,  0.0,  0.0,
     0.5,  0.0,  0.0,
  // Y
     0.0, -0.5,  0.0,
     0.0,  0.0,  0.0,
     0.0,  0.0,  0.0,
     0.0,  0.5,  0.0,
  // Z
     0.0,  0.0, -0.5,
     0.0,  0.0,  0.0,
     0.0,  0.0,  0.0,
     0.0,  0.0,  0.5
  ]);
 
  // Les couleurs de nos 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];
    for (var i = 0; i < 2; i++) {
      colorVertices = colorVertices.concat(color);
    }
  }
  lineBuffers.colorBuffer = gl.createArrayBuffer(colorVertices);
 
  return lineBuffers;
}

Le résultat est alors :
html5 04 02
C'est bien, mais ce qui serait encore mieux, ce serait un dégradé qui passe progressivement d'une couleur à l'autre. Et bien la chose est encore plus simple à obtenir car WebGL interpole toujours pour obtenir les valeurs intermédiaires :

function initBuffers(gl) {
  var lineBuffers = {}
 
  // Les points de nos axes.
  lineBuffers.positionBuffer = gl.createArrayBuffer([
  // X
    -0.5,  0.0,  0.0,
     0.5,  0.0,  0.0,
  // Y
     0.0, -0.5,  0.0,
     0.0,  0.5,  0.0,
  // Z
     0.0,  0.0, -0.5,
     0.0,  0.0,  0.5
  ]);
 
  // Les couleurs de nos 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);
  }
  lineBuffers.colorBuffer = gl.createArrayBuffer(colorVertices);
 
  return lineBuffers;
}

Voici le résultat :
html5 04 03
Evidement le choix du jaune sur fond jaune pâle est (très) discutable. Cela dit c'est l'ensemble des couleurs qui est bof. Par contre au niveau du principe, cela ouvre la porte à plein de choses.

Pour finir ce billet nous allons nous pencher sur un problème qui vous a surement sauté aux yeux dans l'avant dernière version d'initBuffers. Cette dernière contient beaucoup, trop, de définition du même point. L'origine en l'occurrence.
Ce problème étant relativement courant, et pouvant parfois couter très cher en mémoire, il existe une solution. Définir les points une seule fois dans un tableau de point et fournir un tableau d'indices qui donne la vraie définition de la scène.
Nous allons ajouter à notre classe WebGL les méthodes requises. Cela fait, nous construirons une nouvelle version de la page, WebGL_06.htm, qui utilise cette méthode de construction de la scène.
Commençons par ajouter les méthodes nécessaires dans la classe WebGL:

// Création/initialisation d'un tampon d'entiers WebGL.
WebGL.prototype.createElementArrayBuffer = function (vertices) {
  var buffer = this.context.createBuffer();
  buffer.numElements = vertices.length;
  this.context.bindBuffer(this.context.ELEMENT_ARRAY_BUFFER, buffer);
  this.context.bufferData(this.context.ELEMENT_ARRAY_BUFFER, new Uint16Array(vertices), this.context.STATIC_DRAW);
  return buffer;
};
 
WebGL.prototype.pushIndexBuffer = function (buffers) {
  this.context.bindBuffer(this.context.ELEMENT_ARRAY_BUFFER, buffers.indexBuffer);
};
 
// Méthodes provoquant le rendu de la scène à partir d'indexes.
WebGL.prototype.drawLineElements = function (buffers) {
  this.setMatrixUniforms();
  // draw elements
  this.context.drawElements(this.context.LINES, buffers.indexBuffer.numElements, this.context.UNSIGNED_SHORT, 0);
};

Cela fait nous pouvons modifier initBuffers:

function initBuffers(gl) {
  var lineBuffers = {}
 
  // Les points de nos axes.
  lineBuffers.positionBuffer = gl.createArrayBuffer([
    // L'origine
     0.0, 0.0, 0.0,
    // X
    -0.5,  0.0,  0.0,
     0.5,  0.0,  0.0,
    // Y
     0.0, -0.5,  0.0,
     0.0,  0.5,  0.0,
    // Z
     0.0,  0.0, -0.5,
     0.0,  0.0,  0.5
  ]);
 
  // Les couleurs de nos axes.
  var colors = [
    [0.0, 0.0, 0.0, 1.0], // Origine - Noir
    [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);
  }
  lineBuffers.colorBuffer = gl.createArrayBuffer(colorVertices);
 
  // Les indexes des points à utiliser pour former le référentiel.
  lineBuffers.indexBuffer = gl.createElementArrayBuffer([
    1, 0, 0, 2, // X
    3, 0, 0, 4, // Y
    5, 0, 0, 6  // Z
  ]);
 
  return lineBuffers;
}

Ce code n'est pas très compliqué à comprendre. Le point important réside dans le fait que les couleurs sont associées aux points, pas aux indexes.
En clair, compte tenu du fait que l'origine n'est définie qu'une seule fois dans le tableau des points, il aura toujours la même couleur. Le résultat n'est donc pas équivalent à ce que nous avions précédemment.
html5 04 04

Cela clôture ce billet. Dans le prochain billet nous nous occuperons de l'observateur, la camera donc.

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

15 mai 2012

HTML5 03 (tutorial WebGL, 2ème partie)

Dans le précédent billet nous avons dessiné les trois axes du référentiel du contexte WebGL. Malheureusement nous ne distinguions pas bien l'axe Z. Dans ce billet nous allons rendre visible l'axe des Z de notre référentiel.

Pour cela il y a deux solutions :

  1. bouger l'objet que nous regardons, ici le référentiel ;
  2. bouger l'observateur, disons la caméra.

Nous allons commencer par bouger l'objet.
Pour bouger l'objet nous allons utiliser des matrices.
Une remarque. Les matrices que nous utiliserons seront des matrices 4x4. Pourquoi 4x4 et pas 3x3 ?
Parce que nous regardons un monde en 3D à travers un écran en 2D. Il en résulte qu'il y a nécessairement une projection de la 3D vers la 2D. Pour pouvoir faire toutes les manipulations possibles, typiquement les translations, en n'ayant à utiliser qu'une seule multiplication (par une matrice) du vecteur représentant un point dans l'espace 3D, il faut utiliser les Coordonnées homogènes. Dans ce système, nous avons en plus de nos trois coordonnées classiques, x y et z, une quatrième coordonnée appelée w (pour weight).
Pour tous nos exemples nous laisserons w à 1, ce qui fait que si nos matrices seront bien 4x4, elles seront assez assimilable à des matrices 3 x 3

La matrice la plus simple à comprendre permet de réaliser un déplacement suivant un axe, une translation donc. Ci-dessous, la formule magique de la translation :
>html5 03 01
Notez que l'image ci-dessus est une capture d'écran de Firefox en train d'afficher la page HTML5 suivante :



<html>
  <head>
    <title>Translation</title>
    <meta charset="utf-8" />
  </head>
  <body>
    <math xmlns="http://www.w3.org/1998/Math/MathML">
      <mrow>
        <mfenced open="[" close="]">
          <mtable>
            <mtr>
              <mtd><msup><mi>x</mi><mi>'</mi></msup></mtd>
            </mtr>
            <mtr>
              <mtd><msup><mi>y</mi><mi>'</mi></msup></mtd>
            </mtr>
            <mtr>
              <mtd><msup><mi>z</mi><mi>'</mi></msup></mtd>
            </mtr>
            <mtr>
              <mtd><msup><mi>w</mi><mi>'</mi></msup></mtd>
            </mtr>
          </mtable>
        </mfenced>
        <mo>=</mo>
        <mfenced open="[" close="]">
          <mtable>
            <mtr>
              <mtd><mi>1</mi></mtd>
              <mtd><mi>0</mi></mtd>
              <mtd><mi>0</mi></mtd>
              <mtd><msub><mi>t</mi><mn>x</mn></msub></mtd>
            </mtr>
            <mtr>
              <mtd><mi>0</mi></mtd>
              <mtd><mi>1</mi></mtd>
              <mtd><mi>0</mi></mtd>
              <mtd><msub><mi>t</mi><mn>y</mn></msub></mtd>
            </mtr>
            <mtr>
              <mtd><mi>0</mi></mtd>
              <mtd><mi>0</mi></mtd>
              <mtd><mi>1</mi></mtd>
              <mtd><msub><mi>t</mi><mn>z</mn></msub></mtd>
            </mtr>
            <mtr>
              <mtd><mi>0</mi></mtd>
              <mtd><mi>0</mi></mtd>
              <mtd><mi>0</mi></mtd>
              <mtd><mi>1</mi></mtd>
            </mtr>
          </mtable>
        </mfenced>
        <mo>x</mo>
        <mfenced open="[" close="]">
          <mtable>
            <mtr>
              <mtd><mi>x</mi></mtd>
            </mtr>
            <mtr>
              <mtd><mi>y</mi></mtd>
            </mtr>
            <mtr>
              <mtd><mi>z</mi></mtd>
            </mtr>
            <mtr>
              <mtd><mi>1</mi></mtd>
            </mtr>
          </mtable>
        </mfenced>
        <mo>=</mo>
        <mfenced open="[" close="]">
          <mtable>
            <mtr>
              <mtd><mi>x</mi><mo>+</mo><msub><mi>t</mi><mn>x</mn></msub></mtd>
            </mtr>
            <mtr>
              <mtd><mi>y</mi><mo>+</mo><msub><mi>t</mi><mn>y</mn></msub></mtd>
            </mtr>
            <mtr>
              <mtd><mi>z</mi><mo>+</mo><msub><mi>t</mi><mn>z</mn></msub></mtd>
            </mtr>
            <mtr>
              <mtd><mi>1</mi></mtd>
            </mtr>
          </mtable>
        </mfenced>
      </mrow>
    </math>
  </body>
</html>

Malheureusement, tous les butineurs n'affichent pas correctement MathML (qui est pourtant dans HTML5), d'où la copie d'écran. Désolé pour les puristes de MathML, j'ai insisté sur le rendu graphique dans Firefox, pas sur l'aspect mathématique de ma formule.

Il ne nous reste plus qu'à modifier notre vertex shader pour qu'il applique une matrice de translation à tous les points qu'il voit passer.
Il y a juste un petit problème, Javascript ne permet pas la manipulation directe des matrices, ni des vecteurs d'ailleurs. Nous allons donc devoir faire appelle à une librairie Javascript pour tout ce qui est manipulation des matrices et des vecteurs.
Il y a plusieurs candidats possibles, mais les livres que j'ai consultés et beaucoup de tutoriaux trouvés sur le web utilisent glMatrix de Brandon Jones, même s'ils testent parfois, mais sans les retenir, d'autres librairies. Je ne vais donc pas chercher plus loin et faire la même chose.
Vous trouverez deux versions de cette librairie. L'une est lisible (gl-matrix.js), l'autre est compacte (gl-matrix-min.js). Vous les trouverez toutes les deux dans le rar proposé à la fin de ce billet, je vous invite cependant à récupérer une version à jour sur le site dédié à glMatrix. La version utilisée par les exemples sera toujours la version compacte (<script type="text/javascript" src="script/gl-matrix-min.js">).

Maintenant que nous pouvons manipuler des matrices, nous pouvons modifier le vertex shader. Son code devient :

<script id="vertex" type="x-shader">
  attribute vec3 aVertexPosition;
 
  uniform mat4 uMVMatrix;
 
  void main(void) {
    gl_Position = uMVMatrix * vec4(aVertexPosition, 1.0);
  }
</script>

La matrice s'appelle de manière classique uMVMatrix, "u" pour uniform et "MV" pour Model View (la matrice permet de passer de l'objet générique, le Modèle, à l'objet correctement positionné devant la caméra). Cette matrice est en fait obtenues à partir de deux matrices, en les multipliant. Au départ l'objet est décrit dans son propre système de coordonnées. Il faut d'abord le positionner dans le monde WebGL. Cette transformation correspond à la matrice de transformation du modèle et donne la première matrice et la première lettre, le M (mais en Anglais, donc Model). Mais ce n'est pas fini car nous voyons l'objet à l'aide d'une caméra (fictive). Il faut donc transformer le référentiel WebGL pour se placer dans le système de coordonnées de la caméra, car c'est ce que voit la caméra que l'on demandera d'afficher à la carte graphique. En Anglais on parle de "View transform" d'où le V pour cette seconde matrice.
La mauvaise nouvelle c'est que nous devons obligatoirement positionner cette matrice si nous voulons voir quelque chose. En effet si vous visualisez la page dans son état actuel, le référentiel a disparu.
Dans la partie Javascript, après la définition de l'ensemble des points que nous voulons traiter, nous ajoutons la construction de notre matrice de translation.
Pour l'instant nous allons simplement décaler notre référentiel d'un quart d'écran suivant l'axe des X. Pour cela nous partons de la matrice identité, celle qui ne fait rien, et y ajoutons une translation. glMatrix dispose d'une méthode dédié. Cela donne le code suivant :

mvMatrice = mat4.identity();
mat4.translate(mvMatrice, [0.5, 0.0, 0.0]);

Nous devons maintenant passer cette matrice au vertex shader. Pour cela nous ajoutons le code suivant juste avant de dessiner :

program.uMVMatrix = gl.getUniformLocation(program, "uMVMatrix");
gl.uniformMatrix4fv(program.uMVMatrix, false, mvMatrice);

Si vous affichez la page dans un butineur supportant WebGL vous devez voir le résultat suivant :
html5 03 02

Evidement notre translation n'a pas rendu l'axe des Z beaucoup plus visible. Pour cela, la rotation s'impose à nous.
Là encore nous devons multiplier notre vecteur position par une matrice. Si l'on considère que l'on veut faire tourner notre point au tour de l'un des axes, il nous faut utiliser la matrice correspondante ci-dessous (dans l'ordre X, Y et Z).
html5 03 03
Evidement il s'agira généralement d'une combinaison de plusieurs rotations.
C'est même notre cas. En effet une simple rotation au tour de l'un des axes ne suffira pas. Nous avons matérialisé les axes par des traits symétriques par rapport à l'origine (-0.5 à 0.5). Une rotation par rapport à X ou Y ne permettra pas de rendre visible Z qui sera toujours confondu, compte tenu de la projection implicite, avec celui qui n'aura pas été utilisé pour la rotation.
Nous devons, soit dessiner uniquement la portion 0.0 à 0.5 des axes, soit combiner 2 rotations. Nous allons, dans un premier temps, combiner deux rotations.
Nous commençons par une rotation par rapport à Y de 45° et poursuivons par une rotation par rapport à X de 45°.
En partant du code précédent il n'y a qu'à modifier la construction de notre matrice laMatrice. Le reste du code n'est pas impacté. Cela nous donne grâce à glMatrix :

mvMatrice = mat4.identity();
mat4.rotateY(mvMatrice, Math.PI / 4);
mat4.rotateX(mvMatrice, Math.PI / 4);

Si vous affichez la page dans un butineur supportant WebGL vous devez voir le résultat suivant :
html5 03 04
Peut-être n'est-ce pas ce que vous pensiez avoir comme résultat. Pour vous aider, le résultat avec les demi-axes est le suivant :
html5 03 05

Deux remarques très importantes avant de continuer.
Les rotations sont faites par rapport aux axes du référentiel de WebGL, pas ceux du référentiel de notre objet (qui est justement une représentation d'un référentiel). Si vous réfléchissez en termes de référentiel lié à l'objet, il faut enchaîner les rotations dans l'ordre inverse.
WebGL utilise des vecteurs colonnes et non pas des vecteurs lignes (comme DirectX et donc XNA). Il ne s'agit pas simplement d'une convention d'écriture des vecteurs destinée à utiliser moins de place lors de l'écriture sur le papier du vecteur. Cela a également un fort impact sur les mathématiques mises en œuvre. Typiquement le texte précédent, "Nous commençons par une rotation par rapport à Y de 45° et poursuivons par une rotation par rapport à X de 45°." dit ce qui est fait dans le code au niveau de la matrice. Au niveau géométrique, c'est en fait l'inverse qui est réalisé. Cela est dû au fait que WebGL est en vecteur colonne. En conséquence les matrices doivent être placées dans l'ordre inverse de l'ordre géométrique.
Comme il y a deux inversions, vous pouvez avoir bon en vous trompant les deux fois, et trouver que le texte décrit exactement ce qui se passe. Si vous n'avez qu'une erreur, c'est plus compliquer. Si vous ajouter une translation les choses tendent rapidement vers le mystérieux.
Evidement la valeur de 45° n'a pas été choisie totalement au hasard, elle permet de ne pas avoir trop mal au crâne si ces "détails" vous sont inconnus.

Ce n'est pas plus clair, même avec ces deux remarques ?
En fait, ce qui peut gêner c'est que l'on ne voit pas la figure évoluer dans le temps. Du coup il faut utiliser ces doigts pour figurer le référentiel et les tourner devant soi pour voir ce qui se passe.
Le plus parlant serait de voir bouger notre référentiel. Nous allons donc introduire de l'animation.

Pour introduire de l'animation nous nous ajoutons un script, "WebGL.js", à notre solution. Ce script est là pour nous fournir, à terme, le nécessaire à la fois pour l'animation et encapsuler WebGL.
Si vous lisez des livres comme HTML5 Canvas Cookbook ou Professional WebGL Programming: Developing 3D Graphics for the Web, ils proposent chacun leur version de ce même script.
Le nôtre ne sera pas le même, il tendra cependant fortement vers la version de HTML5 Canvas Cookbook. Pourquoi ?
La raison est simple, ils ne sont pas adaptés à notre problématique courante. Ils ne dessinent pas des traits, ni même des points, mais des objets. Malheureusement ce n'est pas la même chose.

Voici une première version de notre script :

var WebGL = function (canvasElement) {
  // Le canvas et le contexte WebGL (ou son équivalent IEWebGL).
  this.canvas = canvasElement;
  this.context = WebGLHelper.GetGLContext(canvasElement);
  this.stage = undefined;
 
  // Le nécessaire pour gérer l'animation.
  this.t = 0;
  this.timeInterval = 0;
  this.startTime = 0;
  this.lastTime = 0;
  this.frame = 0;
  this.animating = false;
 
  // Provided by Paul Irish.
  // Permet de faire fonctionner l'animation sur tous les butineurs.
  window.requestAnimFrame = (function (callback) {
    return window.requestAnimationFrame ||
           window.webkitRequestAnimationFrame ||
           window.mozRequestAnimationFrame ||
           window.oRequestAnimationFrame ||
           window.msRequestAnimationFrame ||
           function (callback) {
             window.setTimeout(callback, 1000 / 60);
           };
  })();
 
  // Encapsulation des matrices globales de glMatrix.
  this.mat3 = mat3;
  this.mat4 = mat4;
  this.vec3 = vec3;
 
  // Le nécessaire pour WebGL.
  this.shaderProgram = null;
  this.mvMatrix = this.mat4.create();
 
  // Initialisation générale du contexte WebGL.
  this.context.viewportWidth = this.canvas.width;
  this.context.viewportHeight = this.canvas.height;
};
 
/*******************************
 * Méthodes générales.
 *******************************/
WebGL.prototype.getContext = function () {
  return this.context;
};
 
WebGL.prototype.getCanvas = function () {
  return this.canvas;
};
 
WebGL.prototype.setClearColor = function (r, g, b, a) {
  this.context.clearColor(r, g, b, a);
};
 
WebGL.prototype.clear = function () {
  this.context.viewport(0, 0, this.context.viewportWidth, this.context.viewportHeight);
  this.context.clear(this.context.COLOR_BUFFER_BIT);
};
 
WebGL.prototype.setStage = function (func) {
  this.stage = func;
};
 
/*******************************
* Gestion de l'animation.
*******************************/
WebGL.prototype.isAnimating = function () {
  return this.animating;
};
 
WebGL.prototype.getFrame = function () {
  return this.frame;
};
 
WebGL.prototype.startAnimation = function () {
  this.animating = true;
  var date = new Date();
  this.startTime = date.getTime();
  this.lastTime = this.startTime;
 
  if (this.stage !== undefined) {
    this.stage();
  }
 
  this.animationLoop();
};
 
WebGL.prototype.stopAnimation = function () {
  this.animating = false;
};
 
WebGL.prototype.getTimeInterval = function () {
  return this.timeInterval;
};
 
WebGL.prototype.getTime = function () {
  return this.t;
};
 
WebGL.prototype.getFps = function () {
  return this.timeInterval > 0 ? 1000 / this.timeInterval : 0;
};
 
WebGL.prototype.animationLoop = function () {
  var that = this;
 
  this.frame++;
  var date = new Date();
  var thisTime = date.getTime();
  this.timeInterval = thisTime - this.lastTime;
  this.t += this.timeInterval;
  this.lastTime = thisTime;
 
  if (this.stage !== undefined) {
    this.stage();
  }
 
  if (this.animating) {
    requestAnimFrame(function () {
      that.animationLoop();
    });
  }
};
 
/*******************************
* Gestion de WebGL.
*******************************/
WebGL.prototype.setShaderProgram = function (vertexId, fragmentId) {
  // Récupération des codes sources.
  var vertexCode = document.getElementById(vertexId).firstChild.nodeValue;
  var fragmentCode = document.getElementById(fragmentId).firstChild.nodeValue;
 
  // Compilation du vertex shader.
  var vertexShader = this.context.createShader(this.context.VERTEX_SHADER);
  this.context.shaderSource(vertexShader, vertexCode);
  this.context.compileShader(vertexShader);
 
  // Compilation du fragment shader.
  var fragmentShader = this.context.createShader(this.context.FRAGMENT_SHADER);
  this.context.shaderSource(fragmentShader, fragmentCode);
  this.context.compileShader(fragmentShader);
 
  // Création du programme.
  this.shaderProgram = this.context.createProgram();
  this.context.attachShader(this.shaderProgram, vertexShader);
  this.context.attachShader(this.shaderProgram, fragmentShader);
  this.context.linkProgram(this.shaderProgram);
 
  // Pour savoir s'il y a eu un problème dans les phases précédentes.
  if (!this.context.getShaderParameter(vertexShader, this.context.COMPILE_STATUS)) {
    console.log(this.context.getShaderInfoLog(vertexShader));
    alert("Could not compile vertex shaders");
  }
  if (!this.context.getShaderParameter(fragmentShader, this.context.COMPILE_STATUS)) {
    console.log(this.context.getShaderInfoLog(fragmentShader));
    alert("Could not compile fragment shaders");
  }
  if (!this.context.getProgramParameter(this.shaderProgram, this.context.LINK_STATUS)) {
    console.log(this.context.getProgramInfoLog(program));
    alert("Could not initialize shaders");
  }
 
  // Mise en place du programme.
  this.context.useProgram(this.shaderProgram);
 
  // Initialisation des shaders.
  this.shaderProgram.uColor = this.context.getUniformLocation(this.shaderProgram, "uColor");
  this.shaderProgram.aVertexPosition = this.context.getAttribLocation(this.shaderProgram, "aVertexPosition");
  this.context.enableVertexAttribArray(this.shaderProgram.aVertexPosition);
  this.shaderProgram.uMVMatrix = this.context.getUniformLocation(this.shaderProgram, "uMVMatrix");
};
 
WebGL.prototype.identity = function () {
  this.mat4.identity(this.mvMatrix);
};
 
WebGL.prototype.translate = function (x, y, z) {
  this.mat4.translate(this.mvMatrix, [x, y, z]);
};
 
WebGL.prototype.rotate = function (angle, x, y, z) {
  this.mat4.rotate(this.mvMatrix, angle, [x, y, z]);
};
 
WebGL.prototype.createArrayBuffer = function (vertices) {
  var buffer = this.context.createBuffer();
  buffer.numElements = vertices.length;
  this.context.bindBuffer(this.context.ARRAY_BUFFER, buffer);
  this.context.bufferData(this.context.ARRAY_BUFFER, vertices, this.context.STATIC_DRAW);
  return buffer;
};
 
WebGL.prototype.configureShaders = function (vbuffer) {
  this.context.uniform4fv(this.shaderProgram.uColor, [1.0, 0.0, 0.0, 1.0]);
  this.context.vertexAttribPointer(this.shaderProgram.aVertexPosition, 3, this.context.FLOAT, false, 0, 0);
  this.context.uniformMatrix4fv(this.shaderProgram.uMVMatrix, false, this.mvMatrix);
};
 
WebGL.prototype.drawLines = function (vbuffer) {
  this.context.drawArrays(this.context.LINES, 0, vbuffer.numElements / 3);
};

Si vous comparez avec le code de notre page, vous retrouvez tout notre code WebGL d'initialisation et de mise en place. Vous trouvez également du code destiné à gérer l'animation.

Nous allons maintenant référencer ce nouveau script dans notre page. A cette occasion nous allons en profiter pour recopier le code de notre page dans une nouvelle page que nous nommerons WebGL_02.htm.
Après avoir instancié la classe contenue dans notre nouveau script, nous enlevons le code qui a été déplacé dans la dite classe.
Reste à ajouter le code de l'animation qui se contente faire tourner le référentiel au tour de l'axe des Y.
Cela donne :

<!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.js"></script>
    <script id="vertex" type="x-shader">
      attribute vec3 aVertexPosition;
 
      uniform mat4 uMVMatrix;
 
      void main(void) {
        gl_Position = uMVMatrix * vec4(aVertexPosition, 1.0);
      }
    </script>
    <script id="fragment" type="x-shader">
      #ifdef GL_ES
        precision highp float;
      #endif
 
      uniform vec4 uColor;
 
      void main(void) {
        gl_FragColor = uColor;
      }
    </script>
    <script type="text/javascript">
      function stage(gl, vbuffer, angle) {
        // Effacement de l'image précédente.
        gl.clear();
 
        // Rotation au tour de l'axe des Y.
        gl.identity();
        gl.rotate(angle, 0, 1, 0);
 
        // Dessin de l'image courante.
        gl.configureShaders(vbuffer);
        gl.drawLines(vbuffer);
      }
 
      function initWebGL(canvasElement) {
        // Création de notre classe d'aide.
        var gl = new WebGL(canvasElement);
 
        // Initialisation des shaders.
        gl.setShaderProgram("vertex", "fragment");
 
        // Définition de la couleur de fond.
        gl.setClearColor(1, 0.98, 0.8, 1);
 
        // Définition des points de nos axes.
        var vertices = new Float32Array([
            -0.5, 0.0, 0.0, 0.5, 0.0, 0.0, // axe des X
             0.0, -0.5, 0.0, 0.0, 0.5, 0.0, // axe des Y
             0.0, 0.0, -0.5, 0.0, 0.0, 0.5  // axe des Z
          ]);
        var vbuffer = gl.createArrayBuffer(vertices);
 
        // L'angle de rotation au tour de Y.
        var angle = 0;
 
        // Mise en place de la scène.
        gl.setStage(function () {
          // Calcule du nouvel angle.
          var angularVelocity = Math.PI / 4; // radians / second
          var angleEachFrame = angularVelocity * gl.getTimeInterval() / 1000;
          angle += angleEachFrame;
 
          stage(gl, vbuffer, angle);
        });
 
        // Lancement de l'animation.
        gl.startAnimation();
      }
 
      function OnGLCanvasCreated(canvasElement, elementId) {
        window.setTimeout(function () { initWebGL(canvasElement); }, 1000);
      }
 
      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>

Tout de suite c'est mieux.
Comment ça "NON" ?
Bon ajoutons une rotation "préalable" au tour de l'axe des X, gl.rotate(Math.PI * 0.3, 1, 0, 0); par exemple.
Là il n'y a plus de problème. Alles klar, Herr Kommissar ?
html5 03 06

Pour ce billet nous allons nous en tenir à cette version. Dans le prochain billet nous allons différentier nos axes en utilisant des couleurs différentes pour chaque axe.
En attendant libre à vous de combiner divers déplacements et constater par vous-même les effets.
En particulier combinez une rotation et une translation (le code de la translation est présent dans le code joint à ce billet). Testez les deux combinaisons. La différence ne devrait pas vous échapper.

A ce sujet, si vous souhaitez récupérer le code à jour, c'est ici

La suite au prochain épisode.

8 mai 2012

HTML5 02 (tutorial WebGL, 1ère partie)

Dans ce billet nous allons commencer à apprendre à faire une page HTML5 qui mette en œuvre WebGL pour faire un dessin.

Comme vous le savez probablement déjà, WebGL n'est pas supporté par tout le monde. Pour que notre page puisse espérer fonctionner avec Internet Explorer nous ferons donc appelle à IEWebGL.
Si vous ne l'avez pas déjà installé, je vous invite à le faire. Si vous ne le faites pas, il vous faudra utiliser un autre butineur qu'Internet Explorer. Pour l'installer une simple visite sur le site IEWebGL suffit, tout y est expliqué (en Anglais, mais le gros bouton "Download IEWebGL" ne devrait pas vous poser de problèmes).

Dans le précédent billet, nous avons vu qu'IEWebGL permettait très simplement, donc avec très peu de modifications, d'adapter une page écrite pour WebGL pour la rendre IE compatible. Par contre il n'y avait pas la moindre explication concernant la façon dont on écrit du code WebGL.
Le code de départ était issu du livre HTML5 Canvas Cookbook et le livre lui-même ne détaille pas vraiment le code WebGL.
Le but de ce billet, et des suivant, est justement de voir en détail comment faire un dessin simplissime en WebGL
En conséquence il est nécessaire de disposer d'un butineur qui permette de faire tourner WebGL. Dans le cas contraire vous ne pourrez pas voir par vous-même les pages HTML5 que nous allons réaliser.

Personnellement je vais utiliser Visual Web Developer 2010 Express pour écrire les pages à venir. D'origine il ne comprend pas HTML5, mais il est possible de lui en apprendre un peu avec Web Standards Update for Microsoft Visual Studio 2010 SP1. Malheureusement le Canvas n'est pas dans le lot des tags traités par le msi.
Attention, je ne dis pas que Visual Web Developer 2010 Express est requis. Il se trouve simplement que Visual Web Developer 2010 Express me plait plus pour écrire le code des pages que mon éditeur habituel qui est Scite. Surtout il est possible de définir assez simplement des templates, que ce soit de page ou de site.

Dans le cadre de Visual Web Developer 2010 Express, nous commençons par créer un nouveau projet de type "Application Web ASP.NET vide". Il nous faut choisir un langage .NET. Ce n'est pas fondamental car nous ne ferons que du Javascript dans les pages. Cela dit il faut tout de même en choisir un, je suis personnellement attaché à C#. Au niveau du nom, "TutoWebGL" le fait très bien.
Nous pouvons maintenant ajouter un nouvel élément de type "Page HTML" que nous nommons "WebGL_01.htm".
Comme nous l'avons vu dans le précédent billet, IEWebGL s'utilise de manière quasiment transparente à l'aide du Javascript webglhelper.js. Le bogue n'étant pas encore corrigé, au moment de l'écriture de ce billet, dans la version disponible sur le site, http://iewebgl.com/scripts/webglhelper.js, nous utiliserons une version locale pour notre projet. Nous ajoutons donc un nouveau dossier "script" à notre projet et y mettons le Javascript récupéré à l'adresse précédente. Cela fait nous corrigeons la ligne 72 pour qu'elle deviennent if (element.style.cssText) cnv.style.cssText = element.style.cssText;.
Nous pouvons maintenant remplacer le code de notre page web par le code 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>
    <script id="vertex" type="x-shader"></script>
    <script id="fragment" type="x-shader"></script>
    <script type="text/javascript">
      function initWebGL(canvasElement) {
        canvas = canvasElement
        gl = WebGLHelper.GetGLContext(canvasElement);
      }
      function OnGLCanvasCreated(canvasElement, elementId) {
        window.setTimeout(function () { initWebGL(canvasElement); }, 1000);
      }
      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>

Que trouve-t-on dans ce code ?

  • insertion du Javascript webglhelper.js. C'est lui gère pour nous le fait que le butineur dispose de ce qu'il faut ou pas. En conséquence nous ne mettons pas un tag canvas dans notre page mais un tag script id="WebGLCanvasCreationScript" ;
  • le tag script id="WebGLCanvasCreationScript". C'est lui qui sera remplacé, soit par un Canvas, soit par un objet IEWebGL. Il contient l'appel à la méthode CreateGLCanvasInline qui effectue la création de l'élément qui va bien et l'insert dans le DOM de la page. Les indications width, height et style seront en fait reportées sur l'objet qui sera substitué au script. Suivant la façon dont la création se passe, les méthodes OnGLCanvasCreated et OnGLCanvasFailed passées en paramètre seront appelées ;
  • OnGLCanvasFailed est appelée si la création d'un contexte WebGL (ou IEWebGL) c'est avérée impossible. Le code proposé est simpliste et peut être modifié en cas de besoin ;
  • OnGLCanvasCreated est appelée si la création d'un contexte WebGL (ou IEWebGL) a été possible. Cette fonction devrait pouvoir directement faire ce qui est voulu, mais un bogue d'initialisation d'IEWebGL fait qu'il est nécessaire d'attendre "un peu" si vous voulez pouvoir manipuler le contexte WebGL sans souci. Le code qui devrait s'y trouver à été déporté dans initWebGL ;
  • initWebGL contient le code Javascript qui permet de réaliser le dessin WebGL. La première chose à faire est donc la récupération du contexte WebGL dans lequel nous travaillerons. En WebGL tout le code n'est pas du Javascript. Il y a aussi du code "shader". Deux emplacements ont été prévus pour l'accueillir ;
  • les deux scripts de type x-shader accueilleront le code "shader".

Maintenant que le cadre est défini essayons de l'utiliser.

Une remarque avant de se lancer.
LE grand classique pour apprendre OpenGL c'est "NeHe OpenGL tutorials" qui commence .
Du coup le grand classique pour aborder WebGL, qui repose sur OpenGL, c'est de reprendre les mêmes exemples adaptés (WebGL ne contient pas tout OpenGL). Pleins de sites le font et souvent très bien, à votre moteur de recherche préféré.
Du coup je ne le fais pas. Déjà parce qu'à part le fait que le texte serait en français, je ne voie pas l'intérêt. L'autre raison, la vraie, c'est que mon besoin concret n'est pas couvert, du moins au début. Du coup je ne vais pas présenter les choses de la même façon, je vais suivre une approche plus géométrique de la question. Dis autrement, je me moque un peu de dessiner des objets avec des textures, je veux comprendre la 3D et comment l'utiliser. Si cela est bon, les textures c'est plus un problème graphique (graphiste et carte graphique) qu'un problème de programmation.

Nous allons donc commencer par les trois axes de notre espace de dessin. Notre but est donc de les matérialiser à l'écran.

Avant de dessiner nos trois axes, nous allons commencer par le fond de notre dessin. L'idée n'étant pas de se faire mal à la tête, nous allons faire simple avec un fond uniforme.
La bonne nouvelle c'est que c'est super simple à faire.
Il suffit de changer la couleur avec laquelle le fond est effacé et d'effacer.
Un dernier détail, il faut définir la zone occupée par notre contexte WebGL dans le Canvas, et de préférence avant d'effacer.
Remis dans le bon ordre cela donne le code suivant :

gl.viewport(0, 0, canvas.width, canvas.height); 
gl.clearColor(1, 0.98, 0.8, 1);
gl.clear(gl.COLOR_BUFFER_BIT);

La première ligne ne devrait pas vous poser de problème, on dessine bien sur tout le Canvas.
La seconde ligne définie une couleur d'effacement. Au lieu d'utiliser des valeurs comprises entre 0 et 255, il faut des valeurs comprise entre 0 et 1. Le "Lemon Chiffon" ne s'obtient pas avec un R-G-B-A de 255-250-205-255 mais plutôt de 1-0.98-0.8-1.
Pour "peindre" le fond avec la couleur que nous venons de définir, nous demandons donc l'effacement du contexte WebGL.
Ce code diffère de ce que l'on rencontre pour la manipulation classique du Canvas, mais reste du Javascript. Il est donc assez facilement lisible. L'écrire peut par contre demander un peu de temps et la consultation de la documentation, WebGL Specification.

Passons maintenant au dessin de nos axes X, Y et Z.
Les choses se compliquent immédiatement. Nous allons devoir mettre le doigt, et probablement plus, dans du code "shader".
Là si vous n'en avez jamais fait, comme moi, la chose peut être un poil plus douloureuse. Si vous ne savez pas ce qu'est une matrice la chose peut même être assez compliquée à aborder.
Alors non, une matrice n'est pas un concept détaillé dans une série de films du début des années 2000. C'est un objet mathématique bien plus ancien. Si vous en doutez, jetez un œil sur Wikipédia.
J'ai parlé des matrices en premier, mais qui dit matrices dit généralement vecteurs. Le code des shaders est donc bourré de vecteurs, généralement à quatre dimensions.
Pour dessiner quelque chose en WebGL il faut passer par deux shaders. L'un pour définir la zone dessinée, le vertex shader, l'autre pour définir la couleur de chaque élément (point, trait ou triangle) dessiné, le fragment shader.

Pour dessiner un simple point nous avons donc besoin de définir nos deux "shaders". Le premier permet de définir le point lui-même, le second la couleur.
Pour dessiner un fragment de droite il nous faut définir, de manière relativement naturelle, deux points.
Pour dessiner un triangle, deviner quoi, il en faut trois, des points. Pour dessiner une figure plus complexe, les choses se gâtent nettement car il faut assembler des triangles. Si vous avez un doute sur la faisabilité de la chose, il n'y a pas à en avoir. Tous les dessins en 3D que vous pouvez voir sont faits, soit à l'aide d'assemblages de triangles, soit point par point. La seconde solution donne un résultat meilleur au niveau des textures, mais fait suffisamment exploser les temps de calcul et la taille mémoire nécessaire pour que la chose soit moins employée et surtout soit réservée aux objets fixes. Vous pouvez tourner au tour, zoomer, mais l'objet n'est pas recalculé, il ne change pas.

Le vertex shader se met dans le script vertexque nous lui avons préparé. Nous y plaçons un code qui nous permettra de dessiner un point dont nous fourniront les trois coordonnées (x, y et z). Le code et le suivant :

attribute vec3 aVertexPosition;
 
void main() {
  gl_Position = vec4(aVertexPosition, 1.0);
}

Avec un poil d'intuition, on comprend que l'on fournit un vecteur à trois dimensions (vec3) nommé aVertexPosition, qui est utilisé comme base pour construire un vecteur à quatre dimensions (vec4). Par contre il peut être un peu plus compliqué de comprendre pourquoi ce vecteur est placé dans gl_Position. En fait gl_Position est le nom, imposé, de l'élément retourné par notre méthode main. C'est en quelque sorte un return.
Dernier point, que contient le vecteur à quatre dimensions ?
Les trois premières sont donc x, y et z. La quatrième correspond à la coordonnée homogène. Je vous laisse consulter Coordonnées homogènespour plus d'informations. Ici nous imposons 1 ce qui nous permet d'avoir aucune "déformation".

Le fragment shader se place dans le script fragment. Dans notre cas il retournera simplement la couleur que nous lui fournirons. Le code est le suivant :

#ifdef GL_ES
  precision highp float;
#endif
 
uniform vec4 uColor;
 
void main() {
  gl_FragColor = uColor;
}

Là encore le nom gl_FragColor n'est pas libre, il remplace le return.
Ce shader conduira tous les points à être dessinés avec la même couleur.

Comme cela les shaders ne sont pas utilisables. Il convient de les compiler et de les lier pour former un "program". Nous ajoutons donc le code suivant à notre méthode Javascrip InitWebGL:

var v = document.getElementById("vertex").firstChild.nodeValue;
var f = document.getElementById("fragment").firstChild.nodeValue;
 
var vs = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vs, v);
gl.compileShader(vs);
 
var fs = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fs, f);
gl.compileShader(fs);
 
program = gl.createProgram();
gl.attachShader(program, vs);
gl.attachShader(program, fs);
gl.linkProgram(program);
 
gl.useProgram(program);

Si ce code ne s'invente pas forcement tout seul, il se lit par contre assez facilement.
Comme souvent, s'il y a un problème il ne se passe rien. Rien, de rien. Donc pour savoir si quelque chose ne se passe pas bien, il est préférable d'aller à la pêche aux infos. C'est ce que fait ce code, que nous ajoutons à InitWebGL avant l'instruction useProgram :

if (!gl.getShaderParameter(vs, gl.COMPILE_STATUS))
  console.log(gl.getShaderInfoLog(vs));
if (!gl.getShaderParameter(fs, gl.COMPILE_STATUS))
  console.log(gl.getShaderInfoLog(fs));
if (!gl.getProgramParameter(program, gl.LINK_STATUS))
  console.log(gl.getProgramInfoLog(program));

Passons au dessin lui-même. Pour ceux qui ont oublié, nous voulons dessiner les trois axes.
La première chose à connaître c'est le système de coordonnées.
Alors là c'est extrêmement simple, pour chaque axe, la zone visible se situe entre -1 et 1. Cela est vrai quel que soit l'axe. C'est donc à nous de faire ce qu'il faut pour que l'on voit, ou pas, les points. Le centre de l'espace WebGL se confond avec celui du Canvas hôte. Le coin supérieur gauche est à -1, 1, 0 et le coin inférieur droit à 1, -1, 0. Il est normal que z soit toujours à 0 vu que notre Canvas est comme notre écran en 2D.
Pour dessiner nos trois axes nous devons donc définir six points. Pour qu'ils nous permettent de nous faire une idée des distances, nous allons les dessiner sur leur section -0,5 0,5. Nous définissons donc un tableau qui contient toutes les coordonnées de nos points, axe par axe, deux points par axe :

var vertices = new Float32Array([
  -0.5,  0.0,  0.0, 0.5, 0.0, 0.0, // axe des X
   0.0, -0.5,  0.0, 0.0, 0.5, 0.0, // axe des Y
   0.0,  0.0, -0.5, 0.0, 0.0, 0.5  // axe des Z
  ]);

Nous devons maintenant les placer dans le tampon courant de WebGL. Une fois la chose faite, toutes le actions à venir leurs seront appliquées. C'est le principe d'OpenGL, et donc de WebGL, les tampons sont globaux. Avantage, il n'est pas nécessaire de passer beaucoup de paramètres aux méthodes. Inconvénient, il faut savoir ce qui est placé dans les tampons pour comprendre ce qui se passe. Le code est le suivant :

vbuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vbuffer);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
 
itemSize = 3;
numItems = vertices.length / itemSize;

Nous pouvons maintenant les lier aux codes des shaders pour qu'ils les traitent :

program.uColor = gl.getUniformLocation(program, "uColor");
gl.uniform4fv(program.uColor, [1.0, 0.0, 0.0, 1.0]);
 
program.aVertexPosition = gl.getAttribLocation(program, "aVertexPosition");
gl.enableVertexAttribArray(program.aVertexPosition);
gl.vertexAttribPointer(program.aVertexPosition, itemSize, gl.FLOAT, false, 0, 0);

A ce stade tout est prêt mais rien n'est visible. Il faut maintenant demander l'affichage :

gl.drawArrays(gl.LINES, 0, numItems);

Si votre butineur le permet vous devriez voir le résultat suivant :

HTML5 02 01

Evidement certains seront déçus. Ils ne trouvent pas l'axe des Z. En fait il est bien là, mais compte tenu de la projection, qui est faite par défaut en le suivant, il est réduit à un point qui se confond avec l'origine.
Pour le voir, il va falloir, soit déplacer notre objet 3D que forment les trois axes, soit modifier le point de vue.
Ce sera l'objet du prochain billet.

Pour ceux qui se serait loupé dans les copier/coller, ils trouveront ici un rar correspondant au projet sous Visual Web Developer 2010 Express.

Publicité
Publicité
1 2 3 4 5 > >>
Fanfois et ses papillons
Publicité
Archives
Publicité