WebGL leçon 9 : améliorer la structure du code avec plein d'objets mobiles

Giles Thomas

Traducteur

Alexandre Laurent

Dans ce tutoriel, vous allez apprendre à animer de multiples objets dans une scène et à architecturer votre code afin de créer des scènes complexes, facilement.

Commentez

- Titre : WebGL leçon 9 : améliorer la structure du code avec plein d'objets mobiles

- Auteur : Giles Thomas

- Traducteur : Alexandre Laurent

- Parution : 4 juillet 2013

- Mise à jour : 4 juillet 2013

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2013 Gilles Thomas. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- Navigation

- II. Introduction

- III. Leçon

- IV. Remerciements

- Navigation

Navigation

Tutoriel précédent : le tampon de profondeur, transparence et mélange

Sommaire

Tutoriel suivant : chargement d'un monde, et la plus basique des caméras

II - Introduction

Bienvenue dans ma neuvième leçon de la série des tutoriels WebGL, basée sur la neuvième leçon des tutoriels OpenGL de Nehe. Dans celle-ci, nous allons utiliser les objets JavaScript afin d'avoir de nombreux objets animés indépendamment les uns des autres dans notre scène 3D. Nous allons aussi voir comment changer la couleur des textures chargées et ce qu'il se passe lorsque nous mélangeons deux textures.

Voici le rendu de cette leçon lorsqu'exécutée dans un navigateur supportant WebGL :

Animation en format "youtube" non supportée.
Contenu uniquement consultable en ligne à cette adresse :
https://www.youtube.com/v/Ti3jbuY4VlI&

Cliquez ici pour voir la version live WebGL si votre navigateur supporte WebGL ou ici pour en avoir un. Vous devez voir un grand nombre d'étoiles de différentes couleurs autour de l'axe des X, tournant en spirale autour.

Vous pouvez utiliser la case à cocher en dessous du canvas pour activer un effet de « scintillement », que nous verrons tout à l'heure. Vous pouvez aussi utiliser les touches fléchées pour tourner l'animation autour de l'axe des X et utiliser les touches « Page précédente » et « Page suivante » pour dézoomer et zoomer.

Plus de détails sur comment cela fonctionne ci-dessous…

Ces leçons ciblent des personnes ayant une connaissance raisonnable de la programmation, mais aucune expérience dans la 3D. Le but est de vous former, en vous donnant une explication de ce qui se passe dans le code, afin que vous puissiez produire vos propres pages Web 3D aussi vite que possible. Si vous n'avez pas lu les précédentstutoriels, vous devriez le faire avant de lire celui-ci. Cette leçon ne détaille que les différences entre la huitième leçon et le nouveau code.

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

Il y a deux façons de récupérer le code de cet exemple : soit en affichant le code « voir source » lorsque vous regardez la page de démonstration, soit en utilisant GitHub : vous pouvez le cloner (ainsi que les autres leçons) à partir du dépôt. Dans les deux cas, une fois que vous avez le code, chargez-le dans votre éditeur de code préféré et jetez-y un coup d'œil.

III - Leçon

La meilleure façon de montrer les différences entre ce code et celui de la huitième leçon est de commencer par le bas de la page et remonter la source. Nous commençons par la fonction webGLStart. Voici ce à quoi ressemble la fonction :

function webGLStart() {
var canvas = document.getElementById("lesson09-canvas");
initGL(canvas);
initShaders();
initTexture();
initBuffers();
initWorldObjects(); // NOUVEAU

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

document.onkeydown = handleKeyDown;
document.onkeyup = handleKeyUp;

tick();
}

J'ai mis en avant un changement. L'appel à notre nouvelle fonction initWorldObjects. Cette fonction crée des objets JavaScript pour représenter la scène et nous allons l'analyser très bientôt. Mais avant, il est important de remarquer un autre changement. Toutes nos fonctions webGLStart précédentes avaient une ligne pour initialiser le test de profondeur :

gl.enable(gl.DEPTH_TEST);

Cette ligne a été enlevée dans cet exemple. Vous vous souvenez sûrement de la dernière fois que le mélange de couleurs et le test de profondeur ne fonctionnent pas ensemble, or nous utilisons le mélange tout le temps dans cet exemple. Le test de profondeur est désactivé par défaut, donc en retirant cette ligne de notre fonction nous définissons le comportement voulu.

La prochaine grande modification se situe dans la fonction animate. Précédemment nous utilisions cette mise à jour pour les nombreuses variables globales représentant les aspects dynamiques de notre scène. Par exemple, la valeur de l'angle utilisée dans la rotation du cube. La modification que nous avons effectuée est très simple : au lieu de mettre à jour les variables directement, nous bouclons à travers tous les objets de notre scène et indiquons à chacun d'entre eux de s'animer lui-même :

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

for (var i in stars) {         // NOUVEAU
stars[i].animate(elapsed);    // NOUVEAU
}                    // NOUVEAU
}
lastTime = timeNow;

}

Continuons de remonter le code et analysons la fonction drawScene. Cette fonction a beaucoup changé, je ne vais donc pas mettre en avant les changements. À la place, nous allons l'étudier au fur et à mesure. Premièrement :

function drawScene() {
gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

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

Ce code est juste le code habituel d'initialisation, inchangé depuis la première leçon.

gl.blendFunc(gl.SRC_ALPHA, gl.ONE);
gl.enable(gl.BLEND)

Ensuite, nous activons le mélange de couleurs. Nous utilisons le même mélange que dans la leçon précédente. Vous vous souvenez que cela permet aux objets de « briller à travers » les uns des autres. Heureusement, cela signifie aussi que les parties noires d'un objet sont dessinées comme si elles étaient transparentes. Pour comprendre comment cela fonctionne, regardez la description des fonctions de mélange de la leçon précédente. Cela signifie que lorsque nous dessinons les étoiles de notre scène, les parties noires sembleront transparentes. Évidemment, les parties les plus sombres de l'étoile seront les plus transparentes. Et comme l'étoile est dessinée avec cette texture :

… cela donne exactement l'effet que nous voulons.

Dans la prochaine partie du code :

mat4.identity(mvMatrix);
mat4.translate(mvMatrix, [0.0, 0.0, zoom]);
mat4.rotate(mvMatrix, degToRad(tilt), [1.0, 0.0, 0.0]);

Nous nous déplaçons simplement au centre de la scène et nous zoomons de façon appropriée. Nous inclinons la scène autour de l'axe des X. zoom et tilt sont des variables globales contrôlées par le clavier. Nous sommes presque prêts à dessiner la scène, donc vérifions premièrement si la case « twinkle » est cochée :

var twinkle = document.getElementById("twinkle").checked;

… et ensuite, tout comme nous l'avons fait pour l'animation, nous itérons à travers la liste des étoiles et indiquons à chacune d'elles de se dessiner. Nous passons en arguments les valeurs actuelles de décalage et de scintillement de la scène. Nous indiquons aussi quelle est la rotation « spin » actuelle. Elle est utilisée pour faire tourner les étoiles autour de leur centre afin qu'elles orbitent autour du centre de la scène.

for (var i in stars) {
stars[i].draw(tilt, spin, twinkle);
spin += 0.1;
}

}

Voilà pour drawScene. Nous avons maintenant vu que les étoiles sont capables de se dessiner et de s'animer elles-mêmes. Le prochain code permet de les créer :

var stars = [];
function initWorldObjects() {
var numStars = 50;

for (var i=0; i < numStars; i++) {
stars.push(new Star((i / numStars) * 5.0, i / numStars));
}
}

Voilà, une simple boucle créant 50 étoiles (vous pouvez souhaiter expérimenter avec des nombres plus ou moins grands). À chaque étoile nous donnons un premier paramètre décrivant une distance de départ au centre de la scène et comme second paramètre une vitesse de rotation. Les deux paramètres sont générés par rapport à la position de l'étoile dans la liste.

Le prochain code à voir est la définition de la classe pour les étoiles. Si vous n'êtes pas habitué à JavaScript, cela vous paraîtra étrange. (Si vous connaissez bien JavaScript, cliquez pour sauter mon explication du modèle objet.)

Le modèle objet JavaScript est très différent des autres langages. La façon la plus simple que j'ai trouvée pour le comprendre est que chaque objet est créé comme un dictionnaire (ou une table de hachage, ou un tableau associatif) et ensuite transformé en un véritable objet en plaçant des valeurs dedans. Les champs des objets sont simplement des entrées dans le dictionnaire qui correspondent à des valeurs et les méthodes, des entrées qui correspondent à des fonctions. Maintenant, nous ajoutons le fait que pour les mots-clés simples, la syntaxe foo.bar est un raccourci valide à la syntaxe foo["bar"] et que vous pouvez voir comment obtenir une syntaxe similaire aux autres langages tout en ayant un point de départ très éloigné.

Ensuite, lorsque vous êtes dans une fonction JavaScript, il existe une variable implicitement liée appelée this qui réfère au « propriétaire » de la fonction. Pour les fonctions globales, la variable correspond à un objet « window » global à chaque page, mais si vous ajoutez le mot-clé new avant, cela créera un tout nouvel objet à la place. Donc, si vous avez une fonction qui définit this.foo à 1 et this.bar à une fonction, alors assurez-vous de toujours l'appeler avec le mot-clé new, qui est identique à un constructeur combiné avec une définition de classe.

Ensuite, nous pouvons noter que si une fonction est appelée avec la syntaxe identique à l'invocation de méthode (qui est, foo.bar()) alors this est limité au propriétaire de la fonction (foo), tout comme nous pouvons nous y attendre. Donc les méthodes des objets peuvent effectuer des actions sur l'objet lui-même.

Finalement, il y a un attribut spécial associé à une fonction : prototype. C'est un dictionnaire de valeurs qui sont associées à chaque objet créé avec le mot-clé new dans cette fonction. C'est un bon moyen de définir des valeurs qui seront identiques pour chaque objet de cette « classe », par exemple, les méthodes.

[Merci murphy et doug pour les commentaires et cette page de Sergio Pereira pour m'avoir aidé à corriger la version originale de cette explication.]

Regardons la fonction que nous écrivons pour définir un objet Star pour cette scène.

function Star(startingDistance, rotationSpeed) {
this.angle = 0;
this.dist = startingDistance;
this.rotationSpeed = rotationSpeed;

// Définit une couleur de départ.
this.randomiseColors();
}

Donc, dans la fonction du constructeur, l'étoile est initialisée avec les valeurs que nous lui fournissons ainsi qu'un angle partant de zéro et se termine avec un appel à une méthode. La prochaine étape et de lier les méthodes aux fonctions prototypes associées de Star afin que tous les objets Star aient les mêmes méthodes. Premièrement, la méthode draw :

Star.prototype.draw = function(tilt, spin, twinkle) {
mvPushMatrix();

Donc, draw est définie pour prendre les paramètres que nous lui passons à partir de la fonction principale drawScene. Nous démarrons en poussant l'actuelle matrice de modèle-vue dans la pile de matrices afin de pouvoir la déplacer sans craindre d'avoir des effets de bord.

// Déplace à la position de l'étoile
mat4.rotate(mvMatrix, degToRad(this.angle), [0.0, 1.0, 0.0]);
mat4.translate(mvMatrix, [this.dist, 0.0, 0.0]);

Ensuite nous effectuons une rotation autour de l'axe Y avec l'angle de l'étoile et éloignons l'étoile du centre de par sa distance. Cela nous place à la bonne position pour dessiner l'étoile.

// Tourne l'étoile pour qu'elle soit face à la caméra
mat4.rotate(mvMatrix, degToRad(-this.angle), [0.0, 1.0, 0.0]);
mat4.rotate(mvMatrix, degToRad(-tilt), [1.0, 0.0, 0.0]);

Ces lignes sont nécessaires afin que les étoiles soient toujours affichées correctement même lorsque l'inclinaison de la scène est altérée en appuyant sur les touches fléchées. Elles sont dessinées en utilisant une texture 2D sur un quadrilatère, ce qui est correct lorsque vous les regardez de face, mais qui aurait l'apparence d'une ligne si nous regardions de côté une fois la scène inclinée. Pour des raisons similaires, nous avons aussi besoin d'appliquer une rotation inverse pour positionner l'étoile. Lorsque vous « annulez » une rotation de cette façon, vous devez l'appliquer dans l'ordre inverse de ce que vous avez fait la première fois. Donc premièrement, nous annulons la rotation de notre positionnement et ensuite l'inclinaison (ce qui est fait dans drawScene).

Les prochaines lignes dessinent l'étoile :

if (twinkle) {
// Dessine une étoile fixe avec la couleur de « scintillement »
gl.uniform3f(shaderProgram.colorUniform, this.twinkleR, this.twinkleG, this.twinkleB);
drawStar();
}

// Toutes les étoiles tournent autour de l'axe Z avec la même allure
mat4.rotate(mvMatrix, degToRad(spin), [0.0, 0.0, 1.0]);

// Dessine l'étoile avec sa couleur principale
gl.uniform3f(shaderProgram.colorUniform, this.r, this.g, this.b);
drawStar();

Ignorons le code qui applique l'effet de scintillement pour le moment. L'étoile est dessinée après avoir effectué la rotation autour de l'axe Z de la valeur spin passée en paramètre. Cela fait tourner l'étoile sur son propre centre pendant qu'elle tourne autour du centre de la scène. Nous envoyons la couleur de l'étoile à la carte graphique à l'aide d'une variable uniforme pour le shader puis nous appelons une fonction globale drawStar (que nous allons voir dans un instant).

Maintenant, que pensez-vous du code du scintillement ? Bon, l'étoile a deux couleurs associées : sa couleur standard et sa « couleur de scintillement ». Pour la faire scintiller, nous affichons une étoile fixe dans une couleur différente avant de la dessiner. Cela veut dire que les deux étoiles sont mélangées ensemble, donnant une belle couleur brillante, mais signifie aussi que les rayons sortant de la première étoile sont fixes alors que ceux sortant de la seconde étoile tournent, donnant un effet sympathique. C'est notre scintillement.

Donc, une fois que nous avons dessiné l'étoile, nous retirons la matrice modèle-vue de la pile et nous avons fini :

mvPopMatrix();
};

La prochaine méthode dont nous lions le prototype est celle pour animer l'étoile :

var effectiveFPMS = 60 / 1000;
Star.prototype.animate = function(elapsedTime) {

Tout comme dans les leçons précédentes, au lieu de simplement mettre à jour la scène aussi vite que possible, j'ai choisi de la mettre à jour à une allure stable pour tous, afin que les gens avec des ordinateurs plus rapides aient des animations plus douces et les personnes avec des machines plus lentes, plus saccadées. Maintenant, je pense que les nombres pour la vitesse angulaire à laquelle les étoiles orbitent autour du centre de la scène et pour la vitesse d'éloignement du centre ont été soigneusement calculés par NeHe. Donc plutôt que de tout dérégler, j'ai décidé qu'il était préférable de supposer que les valeurs avaient été calibrées pour du 60 images par seconde et donc d'utiliser ces valeurs ainsi que elapsedTime (qui, pour rappel est le temps écoulé entre chaque appel à la fonction animate) afin de dimensionner le déplacement entre chaque « tick » d'animation. La valeur de elapsedTime est en millisecondes et nous souhaitons avoir une valeur effective de 60/1000 images par milliseconde. Nous plaçons cette valeur dans une variable globale hors de la méthode animate pour ne pas la recalculer à chaque fois que nous dessinons une étoile (merci à deathy/Brainstorm pour cette suggestion).

Donc, maintenant que nous avons cette valeur, nous pouvons ajuster l'angle de l'étoile. C'est la distance de son orbite au centre de la scène définie par :

this.angle += this.rotationSpeed * effectiveFPMS * elapsedTime;

… et nous pouvons ajuster sa distance au centre en la déplaçant vers l'intérieur de la scène et réinitialisant ses couleurs à des valeurs aléatoires lorsqu'elle atteint finalement le centre :

// Diminue la distance, réinitialisant l'étoile vers l'extérieur
// de la spirale si elle a atteint le centre.
this.dist -= 0.01 * effectiveFPMS * elapsedTime;
if (this.dist < 0.0) {
this.dist += 5.0;
this.randomiseColors();
}

};

Le morceau final, complétant le prototype de l'objet Star est le code dont nous avons vu l'utilisation dans le constructeur et dernièrement dans le code d'animation pour générer des couleurs aléatoires pour l'étoile :

Star.prototype.randomiseColors = function() {
// Donne à l'étoile une couleur standard aléatoire
this.r = Math.random();
this.g = Math.random();
this.b = Math.random();

// Lorsque l'étoile scintille, nous la dessinons deux fois, une
// avec la couleur ci-dessous (fixe) et la seconde
// avec la couleur ci-dessus.
this.twinkleR = Math.random();
this.twinkleG = Math.random();
this.twinkleB = Math.random();
};

… et nous avons fini avec le prototype des étoiles. C'est ainsi qu'un objet étoile est créé avec des méthodes pour la dessiner et l'animer. Maintenant, juste au-dessus de ces fonctions, vous pouvez voir le code (plutôt terne) pour dessiner l'étoile : il dessine juste un quadrilatère avec la méthode vue dans la première leçon, utilisant une texture adéquate et les tampons de coordonnées de position et de texture :

function drawStar() {
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, starTexture);
gl.uniform1i(shaderProgram.samplerUniform, 0);

gl.bindBuffer(gl.ARRAY_BUFFER, starVertexTextureCoordBuffer);
gl.vertexAttribPointer(shaderProgram.textureCoordAttribute, starVertexTextureCoordBuffer.itemSize, gl.FLOAT, false, 0, 0);

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

setMatrixUniforms();
gl.drawArrays(gl.TRIANGLE_STRIP, 0, starVertexPositionBuffer.numItems);
}

Un peu plus haut, vous pouvez voir initBuffers, qui définit ces tampons de positions de vertex et de coordonnées de texture et le code adéquat dans handleKeys pour manipuler les fonctions de zoom « zoom » et d'inclinaison « tilt » lorsque l'utilisateur appuie sur les touches fléchées haut/bas et les touches Page précédente/Page suivante. Encore plus haut vous pourrez trouver les fonctions initTexture et handleLoadedTexture qui ont été mises à jour pour charger une nouvelle texture. Tout cela est tellement simple que je ne vais pas vous barber à les décrire.

Allons directement aux shaders, où vous pouvez voir la dernière modification nécessaire pour cette leçon. Tout le code lié à l'éclairage a été supprimé du vertex shader, qui est maintenant identique à celui de la cinquième leçon. Le fragment shader est un peu plus intéressant :

precision mediump float;

varying vec2 vTextureCoord;

uniform sampler2D uSampler;

uniform vec3 uColor;

void main(void) {
vec4 textureColor = texture2D(uSampler, vec2(vTextureCoord.s, vTextureCoord.t));
gl_FragColor = textureColor * vec4(uColor, 1.0);
}

… mais pas si intéressant que cela Tout ce que nous faisons, c'est prendre la couleur uniforme qui a été envoyée par le code de la méthode draw de l'étoile et l'utiliser pour teinter la texture, qui est monochrome. Cela signifie que notre étoile sera de la bonne couleur.

Et, c'est tout ! Maintenant, vous savez tout de cette leçon : comment créer des objets JavaScript pour représenter des éléments dans votre scène et leur donner des méthodes pour leur permettre d'être animés et dessinés séparément.

La prochaine fois, nous allons voir comment charger une scène à partir d'un simple fichier et réfléchir sur la façon de déplacer une caméra dans celle-ci afin de faire une sorte de mini Doom

IV - Remerciements

Comme toujours, je dois énormément à NeHe pour ses tutoriels OpenGL et le script de cette leçon.

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

Navigation

Tutoriel précédent : le tampon de profondeur, transparence et mélange

Sommaire

Tutoriel suivant : chargement d'un monde, et la plus basique des caméras