Navigation

Tutoriel précédent : quelques objets 3D   Sommaire   Tutoriel suivant : clavier et filtres de texture

II. Introduction

Bienvenue dans mon cinquième tutoriel WebGL, basé sur le sixième tutoriel des tutoriels OpenGL de NeHe. Cette fois nous allons ajouter une texture à l'objet 3D. C'est tout, nous allons le découvrir en utilisant une image chargée à partir d'un fichier à part. C'est une méthode très utile pour ajouter des détails à votre scène 3D sans avoir à dessiner un objet incroyablement complexe. Imaginez un mur de pierres dans un jeu de labyrinthe. Vous ne voulez certainement pas un modèle différent pour chaque bloc de mur, donc vous créez une image d'une maçonnerie et vous recouvrez le mur avec. Un mur complet peut maintenant être un objet unique.

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



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

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 tutoriels précédents, vous devriez le faire avant de lire celui-ci. Je n'expliquerai ici que les différences entre le code de la quatrième leçon et le nouveau code.

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

Pour faire simple, l'application d'une texture est une technique pour trouver la couleur des pixels constituant l'objet 3D. Comme vous avez pu le voir dans la deuxième leçon, les couleurs sont spécifiées par les fragment shaders, donc nous devons charger l'image et l'envoyer au fragment shader. Le fragment shader nécessite aussi de savoir quelle partie de l'image est à utiliser, donc nous devons aussi lui envoyer cette information.

Commençons par regarder le code qui charge la texture. Nous l'appelons juste après le début de l'exécution de la page JavaScript, dans webGLStart en bas de la page (le nouveau code est en rouge) :

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

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

Regardons à la fonction initTexture. Elle se situe à environ un tiers du code source et tout est nouveau dans celle-ci :

 
Sélectionnez
  var neheTexture; 
  function initTexture() { 
    neheTexture = gl.createTexture(); 
    neheTexture.image = new Image(); 
    neheTexture.image.onload = function() { 
      handleLoadedTexture(neheTexture) 
    } 

    neheTexture.image.src = "nehe.gif"; 
  }

Donc, nous créons une variable globale pour contenir la structure. Évidemment dans le monde réel vous aurez plusieurs textures et n'utiliserez pas de variables globales, mais nous gardons les choses simples pour le moment. Nous utilisons gl.createTexture pour créer une référence sur la texture contenue dans la variable globale, puis nous créons un objet Image de JavaScript et le conservons dans un nouvel attribut que nous attachons à la texture, encore une fois en utilisant les avantages du JavaScript nous permettant de définir n'importe quel champ sur n'importe quel objet. Les objets texture n'ont pas de champ image par défaut, mais il nous est pratique d'en avoir un, donc nous le créons. La prochaine étape est de faire en sorte que l'objet Image charge et contienne l'image que nous voulons appliquer sur le cube, mais avant de le faire, nous lui attachons une fonction callback. Celle-ci sera appelée lorsque l'image aura été totalement chargée. Il est donc plus sûr de la charger en premier. Une fois celle-ci initialisée, nous définissons la propriété src de l'image et c'est fini. L'image se charge de façon asynchrone : le code définissant src de l'image se terminera immédiatement et un thread chargera en arrière-plan l'image à partir du serveur. Une fois cela fait, notre callback sera appelé, appelant à son tour handleLoadedTexture() :

 
Sélectionnez
  function handleLoadedTexture(texture) { 
    gl.bindTexture(gl.TEXTURE_2D, texture); 
    gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true); 
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, texture.image); 
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); 
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); 
    gl.bindTexture(gl.TEXTURE_2D, null); 
  }

La première chose à faire est d'indiquer à WebGL que notre texture est la texture « active ». Les fonctions WebGL s'appliquant aux textures opèrent toutes sur la texture « active » au lieu de prendre la texture en paramètre. La fonction bindTexture() permet de rendre une texture active. Son mécanisme est similaire à celui de la fonction gl.bindBuffer que nous avons vue précédemment.

Ensuite, nous indiquons à WebGL que toutes les images que nous chargeons doivent être retournées verticalement. Nous devons le faire à cause de la différence de coordonnées. Nous utilisons des coordonnées de texture qui, comme celles que nous utilisons normalement en mathématiques, augmentent lorsque vous vous déplacez selon l'axe vertical. Cela est cohérent avec les coordonnées X, Y, Z que nous utilisons pour les positions de vertex. Au contraire, la majorité des systèmes informatiques graphiques, par exemple le format GIF que nous utilisons comme texture, utilisent des coordonnées qui augmentent lorsque vous vous déplacez vers le bas suivant l'axe vertical. L'axe horizontal est le même dans les deux systèmes de coordonnées. La différence sur l'axe vertical signifie que pour WebGL, l'image GIF que nous utilisons comme texture est déjà retournée verticalement et nous devons la « re-retourner ». (Merci à Ilmari Heikkinen pour sa clarification dans les commentaires.)

La prochaine étape est d'envoyer notre texture nouvellement chargée dans l'espace réservé aux textures de notre carte graphique grâce à la fonction texImage2D. Les paramètres sont dans l'ordre suivant : le type de l'image utilisée, le niveau de détails (que nous analyserons dans une prochaine leçon), le format que nous voulons utiliser pour sauvegarder notre texture dans la carte graphique (répété deux fois pour des raisons que nous verrons plus tard), la taille de chaque « canal » de l'image (qui est, le type de données utilisé pour stocker le rouge, vert, bleu) et finalement, l'image elle-même.

Les deux prochaines lignes spécifient des paramètres spéciaux pour le redimensionnement de la texture. Le premier indique à WebGL ce qu'il doit faire lorsque la texture remplit une grande partie de l'écran par rapport à la taille de l'image. En d'autres mots, il donne des renseignements sur comment l'agrandir. Le second est l'équivalent pour la réduction. Vous pouvez spécifier plusieurs types de redimensionnement. NEAREST est le moins attractif de tous, il indique que l'image doit être utilisée comme telle, ce qui signifie qu'elle sera très pixelisée lorsque l'on s'en rapprochera. Par contre, il possède l'avantage d'être très rapide même sur les machines lentes. Dans la prochaine leçon, nous inspecterons l'utilisation de différents indices de redimensionnement, vous laissant la possibilité de comparer l'aspect et les performances de chaque indice.

Une fois cela fait, nous définissons la texture active à null. Cela n'est pas nécessaire, mais c'est une bonne pratique. Une sorte de nettoyage.

Donc, c'est tout pour le code nécessaire au chargement de la texture. Regardons ensuite la fonction initBuffers. Celle-ci, bien sûr, a perdu tout le code relatif à la pyramide que nous avions dans la quatrième leçon, mais le changement le plus important est le remplacement du tampon de couleurs des vertex du cube par un nouveau : le tampon de coordonnées de texture. Il ressemble à ceci :

 
Sélectionnez
    cubeVertexTextureCoordBuffer = gl.createBuffer(); 
    gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexTextureCoordBuffer); 
    var textureCoords = [ 
      // Face avant 
      0.0, 0.0, 
      1.0, 0.0, 
      1.0, 1.0, 
      0.0, 1.0, 

      // Face arrière 
      1.0, 0.0, 
      1.0, 1.0, 
      0.0, 1.0, 
      0.0, 0.0, 

      // Face du dessus 
      0.0, 1.0, 
      0.0, 0.0, 
      1.0, 0.0, 
      1.0, 1.0, 

      // Face du dessous
      1.0, 1.0, 
      0.0, 1.0, 
      0.0, 0.0, 
      1.0, 0.0, 

      // Face de droite
      1.0, 0.0, 
      1.0, 1.0, 
      0.0, 1.0, 
      0.0, 0.0, 

      // Face de gauche
      0.0, 0.0, 
      1.0, 0.0, 
      1.0, 1.0, 
      0.0, 1.0, 
    ]; 
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(textureCoords), gl.STATIC_DRAW); 
    cubeVertexTextureCoordBuffer.itemSize = 2; 
    cubeVertexTextureCoordBuffer.numItems = 24;

Vous devriez maintenant être à l'aise avec ce genre de code et voir que tout ce que nous faisons est de spécifier un nouvel attribut pour chaque vertex dans un tampon et que cet attribut a deux valeurs par vertex. Ces coordonnées spécifient, en coordonnées x, y cartésiennes, le lieu d'attachement de la texture au vertex. Pour expliquer ces coordonnées, nous traitons la texture comme large de 1.0 et haute de 1.0, donc (0, 0) est le coin bas gauche, (1, 1) le coin haut droit. La conversion entre ces coordonnées et la résolution réelle de l'image est gérée par WebGL.

Ceci est le seul changement dans initBuffers, donc déplaçons-nous jusqu'à la fonction drawScene. Les changements les plus intéressants dans cette fonction sont, bien entendu, ceux nous permettant d'utiliser la texture. Par contre, avant de les analyser, il y a un certain nombre de changements liés au simple fait de la suppression de la pyramide et que le cube tourne sur lui-même d'une nouvelle façon. Je ne les décrirai pas en détails, sachant que le code est simple à comprendre. Ils sont mis en avant en rouge dans le morceau de code en haut de la fonction drawScene :

 
Sélectionnez
  var xRot = 0; 
  var yRot = 0; 
  var zRot = 0; 
  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); 

    mat4.identity(mvMatrix); 

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

    mat4.rotate(mvMatrix, degToRad(xRot), [1, 0, 0]); 
    mat4.rotate(mvMatrix, degToRad(yRot), [0, 1, 0]); 
    mat4.rotate(mvMatrix, degToRad(zRot), [0, 0, 1]); 

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

Il y a aussi des changements correspondants dans la fonction animate afin de mettre à jour xRot, yRot et zRot, mais je ne les analyserai pas.

Ceci fait, regardons le code pour la texture. Dans initBuffers nous avons initialisé un tampon contenant les coordonnées de texture donc ici nous devons le lier à l'attribut approprié afin que le shader puisse le voir :

 
Sélectionnez
    gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexTextureCoordBuffer); 
    gl.vertexAttribPointer(shaderProgram.textureCoordAttribute, cubeVertexTextureCoordBuffer.itemSize, gl.FLOAT, false, 0, 0); 

… et maintenant WebGL sait quel morceau de texture chaque vertex utilise. Nous devons lui dire d'utiliser la texture que nous avons chargée précédemment, puis de dessiner le cube :

 
Sélectionnez
    gl.activeTexture(gl.TEXTURE0); 
    gl.bindTexture(gl.TEXTURE_2D, neheTexture); 
    gl.uniform1i(shaderProgram.samplerUniform, 0); 

    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, cubeVertexIndexBuffer); 
    setMatrixUniforms(); 
    gl.drawElements(gl.TRIANGLES, cubeVertexIndexBuffer.numItems, gl.UNSIGNED_SHORT, 0);

Ce qui se passe dans ce code est quelque peu complexe. WebGL peut gérer jusqu'à 32 textures durant n'importe quel appel à une fonction comme gl.drawElements, et elles sont numérotées de TEXTURE0 à TEXTURE31. Ce que nous faisons est d'indiquer dans les deux premières lignes que la texture zéro est celle que nous avons chargée précédemment puis à la troisième ligne nous passons la valeur zéro à une variable uniforme du shader (qui, comme pour les autres variables uniformes que nous avons utilisées pour les matrices, est extraite du program shader dans la fonction initShaders). Cela indique au shader que nous utilisons la texture zéro. Nous verrons plus tard comment l'utiliser.

Peu importe, lorsque ces trois lignes-là sont exécutées, nous sommes prêts à y aller, donc nous utilisons simplement le même code que précédemment pour dessiner les triangles constituant le cube.

Le seul nouveau code à expliquer concerne des changements dans les shaders. Regardons le vertex shader en premier :

 
Sélectionnez
  attribute vec3 aVertexPosition; 
  attribute vec2 aTextureCoord; 

  uniform mat4 uMVMatrix; 
  uniform mat4 uPMatrix; 

  varying vec2 vTextureCoord; 

  void main(void) { 
    gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0); 
    vTextureCoord = aTextureCoord; 
  }

Cela est très proche des choses que nous utilisions pour les couleurs de la deuxième leçon. Tout ce que nous faisons est d'accepter les coordonnées de texture (une fois encore, à la place des couleurs) comme un attribut par vertex et de les passer directement à une variable « varying ».

Une fois appelé pour chaque vertex, WebGL sortira les valeurs pour les fragments (pour rappel : qui ne sont juste que des pixels) entre les vertex en utilisant une interpolation linéaire entre ceux-ci. Tout comme cela était fait dans la seconde leçon avec les couleurs. Donc, un fragment à mi-chemin entre deux vertex ayant pour coordonnées de texture (1, 0) et (0, 0) recevra comme coordonnées de texture (0.5, 0) et un fragment à mi-chemin entre (0, 0) et (1, 1) recevra (0.5, 0.5). Prochain arrêt, le fragment shader :

 
Sélectionnez
  precision mediump float; 

  varying vec2 vTextureCoord; 

  uniform sampler2D uSampler; 

  void main(void) { 
    gl_FragColor = texture2D(uSampler, vec2(vTextureCoord.s, vTextureCoord.t)); 
  }

Donc, nous prenons les coordonnées de texture interpolées et nous avons une variable de type sampler, qui est la façon de représenter une texture pour un shader. Dans drawScene, notre texture était attachée à gl.TEXTURE0 et la variable uniforme uSampler était définie à la valeur zéro, donc ce sampler représente notre texture. Tous les shaders utilisent la fonction texture2D pour récupérer la couleur appropriée à partir de la texture en utilisant les coordonnées. Les textures utilisent traditionnellement s et t pour leurs coordonnées à la place de x et y et le langage de shader supporte ces alias. Nous pouvons aussi simplement utiliser vTextureCoord.x et vTextureCoord.y.

Une fois que nous avons la couleur pour le fragment, nous avons fini ! Nous avons un objet texturé à l'écran.

Donc, c'est tout pour cette fois. Maintenant vous connaissez tout de cette leçon : comment ajouter une texture à un objet WebGL 3D en chargeant une image, indiquer à WebGL de l'utiliser suivant les coordonnées de texture de votre objet et les utiliser dans le shader ainsi que la texture.

Si vous avez une question quelconque, des commentaires ou des corrections, veuillez laisser un commentaire.

Sinon, regardez la prochaine leçon, dans laquelle vous apprendrez comment avoir des entrées clavier JavaScript basiques pour animer vos scènes. Ainsi vous pourrez rendre votre scène interactive avec la personne visualisant votre page. Nous allons l'utiliser pour permettre au lecteur de changer la rotation du cube, de zoomer et de dézoomer et de changer les indices donnés par WebGL pour contrôler le redimensionnement des textures.

IV. Remerciements

La boite tournante de Chris Marrin a été d'une grande aide lors de l'écriture de cette leçon, tout comme l'était cette extension de la démonstration de Jacob Seidelin. Comme toujours, je suis extrêmement redevable envers NeHe pour son tutoriel OpenGL pour le script de cette leçon.

Merci à Winjerome pour ses corrections et f-leb pour sa relecture orthographique.

Navigation

Tutoriel précédent : quelques objets 3D   Sommaire   Tutoriel suivant : clavier et filtres de texture