Navigation

II. Introduction

Bienvenue dans mon septième tutoriel WebGL basé sur le huitième tutoriel OpenGL de NeHe. Dans cette leçon, nous allons aborder le mélange et en même temps voir le fonctionnement du tampon de profondeur.

Voici à quoi ressemble la leçon lorsqu'elle est exécutée sur un navigateur qui prend en charge WebGL (bien que malheureusement, la transparence ne se voie pas très bien dans cette vidéo) :



Cliquez ici pour voir la version live WebGL si vous avez un navigateur qui la prend en charge ; voici comment en obtenir un si vous n'en avez pas. Vous devriez apercevoir un cube semi-transparent en rotation lente, semblant constitué de vitrail. L'éclairage ne devrait pas avoir changé depuis la dernière leçon.

Vous pouvez utiliser la case à cocher sous le canvas pour activer/désactiver le mélange et de ce fait l'effet de transparence. Vous pouvez aussi régler le facteur alpha (que nous expliquerons plus tard), et bien sûr l'éclairage.

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

Ces leçons sont destinées aux personnes ayant de bonnes connaissances en programmation, mais sans réelle expérience dans la 3D, le but est de vous donner les moyens de démarrer, avec une bonne compréhension de ce qui se passe dans le code, afin que vous puissiez commencer à produire vos propres pages Web 3D aussi rapidement 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 septième leçon et le nouveau code.

Il y a deux façons d'obtenir le code de cet exemple, il suffit d'« Afficher la source » pendant que vous visionnez la version live, ou si vous utilisez GitHub, vous pouvez le cloner (ainsi que les autres leçons) à partir du dépôt.

Mais avant de commencer par le code, il y a un peu de théorie à passer en revue. Pour débuter, je dois probablement vous expliquer ce qu'est le mélange ! Et pour cela, je vais d'abord devoir aborder le tampon de profondeur.

III. Le tampon de profondeur

Souvenez-vous de la deuxième leçon, lorsque vous dites à WebGL de dessiner quelque chose, cela passe par plusieurs étapes de traitement. D'un point de vue général, il :

  1. exécute le vertex shader sur tous les vertex ;
  2. effectue une interpolation linéaire entre les vertex, qui renseignent les fragments (que vous pouvez pour le moment considérer comme des pixels) qui ont besoin d'être dessinés ;
  3. exécute sur chaque fragment le fragment shader pour déterminer sa couleur ;
  4. l'écrit dans le frame buffer.

Pour le moment, le frame buffer est ce qui est finalement affiché. Mais que se passe-t-il si vous dessinez deux choses ? Par exemple, si vous dessinez un carré de centre (0, 0, -5) et un autre de même taille en (0, 0, -10) ? Vous ne voudriez pas que le second écrase le premier, parce qu'il est nettement plus loin et devrait être caché.

WebGL gère ceci avec le tampon de profondeur. Suite au traitement par le fragment shader, lorsque les fragments sont écrits dans le frame buffer, de même que les valeurs de couleurs RGBA, la valeur de la profondeur liée est aussi stockée. Ce n'est pas exactement la même valeur que la Z value associée au fragment. (Sans surprise, le tampon de profondeur est souvent aussi appelé le Z-buffer.)

Qu'est-ce que j'entends par « liée » ? Eh bien WebGL aime que les valeurs de profondeur soient entre 0 et 1, 0 pour le plus proche et 1 le plus éloigné. Tout ceci est caché par la matrice de projection créée par l'appel à perspective au début de drawScene. Tout ce que vous devez savoir pour le moment est que plus la valeur du tampon de profondeur sera grande, plus l'élément sera éloigné ; ce qui est l'opposé des coordonnées de notre normale.

OK, voilà pour le tampon de profondeur. Vous vous souvenez peut-être que dans le code utilisé pour initialiser notre contexte WebGL depuis la première leçon, nous avons la ligne suivante :

 
Sélectionnez
    gl.enable(gl.DEPTH_TEST);

Il s'agit d'une instruction destinée au système WebGL indiquant ce qu'il faut faire lors de l'écriture d'un nouveau fragment dans la mémoire du frame buffer. Essentiellement, elle signifie « prend en considération le tampon de profondeur ». Il est combiné avec un autre paramètre WebGL, la fonction de la profondeur. Il a une valeur raisonnable par défaut, mais si nous devions nous-même mettre activement sa valeur par défaut, cela ressemblerait à ceci :

 
Sélectionnez
    gl.depthFunc(gl.LESS);

Cela signifie « si notre fragment a une valeur de profondeur inférieure à la valeur actuelle, utilise la nouvelle plutôt que l'ancienne ». Ce test à lui seul, combiné au code pour l'activer suffit à nous donner un comportement logique : les objets proches dissimulent les plus éloignées. (Vous pouvez également utiliser d'autres valeurs pour la fonction de profondeur, mais je soupçonne qu'elles soient utilisées plus rarement.)

IV. Le mélange

Le mélange est simplement une alternative à ce procédé. Avec le test de profondeur, la fonction de profondeur permet de faire le choix de remplacer ou non le fragment existant par le nouveau. Lorsque nous effectuons le mélange, nous utilisons une fonction de mélange pour combiner les couleurs du fragment actuel et du nouveau pour en faire un tout nouveau, que nous écrivons alors dans le tampon.

Jetons un œil au code maintenant. Quasi-rien n'a changé depuis la septième leçon, presque toutes les choses importantes se trouvent dans un court morceau de drawScene. Tout d'abord, nous vérifions si la case « mélange » est cochée.

 
Sélectionnez
    var blending = document.getElementById("blending").checked;

Si oui, nous définissons la fonction qui sera utilisée pour combiner les deux fragments.

 
Sélectionnez
    if (blending) {
      gl.blendFunc(gl.SRC_ALPHA, gl.ONE);

Les paramètres de cette fonction définissent la façon dont le mélange est fait. C'est fastidieux, mais pas difficile. Définissons tout d'abord deux termes : le fragment source que nous dessinons en ce moment et le fragment de destination qui est déjà dans le frame buffer. Le premier paramètre de la fonction gl.blendFunc détermine le facteur de source, et le second le facteur de destination. Ces facteurs sont utilisés dans la fonction de mélange. Nous disons dans ce cas que le facteur de source est la valeur alpha du fragment source, et le facteur de destination a une valeur constante de 1. Il y a d'autres possibilités, par exemple si vous utilisez SRC_COLOR pour spécifier la couleur de la source, vous vous retrouvez avec des facteurs de source distincts pour le rouge, le vert, le bleu et l'alpha, dont chacun est égal aux composantes RGBA originales.

Imaginons maintenant que WebGL essaye de calculer les couleurs du fragment ayant des valeurs RGBA (Rd', Gd', Bd', Ad) à partir des valeurs (Rs', Gs', Bs', As) du fragment source.

Posons de plus les facteurs RGBA source (Sr', Sg', Sb', Sa) et destination (Dr', Dg', Db', Da).

WebGL calculera chaque composante couleur comme suit :

  • Rresult = Rs * Sr + Rd * Dr
  • Gresult = Gs * Sg + Gd * Dg
  • Bresult = Bs * Sb + Bd * Db
  • Aresult = As * Sa + Ad * Da

Dans notre cas donc, nous avons (juste pour la composante rouge afin de garder les choses simples) :

  • Rresullt = Rs * As + Rd

Ce n'est normalement pas un moyen idéal de créer de la transparence, mais il se trouve que cela fonctionne très bien dans notre cas lorsque la lumière est allumée. Ce point mérite d'être souligné : le mélange n'est pas la même chose que la transparence, c'est juste une technique utilisée (parmi d'autres) pour obtenir des effets de transparence. Cela m'a pris un certain temps à l'assimiler lorsque je parcourais les leçons Nehe, alors pardonnez-moi si j'insiste là-dessus maintenant.

OK, continuons :

 
Sélectionnez
      gl.enable(gl.BLEND);

Un point plutôt simple - comme beaucoup de choses dans WebGL, le mélange est désactivé par défaut, nous avons donc besoin de l'activer.

 
Sélectionnez
      gl.disable(gl.DEPTH_TEST);

Ceci est un peu plus intéressant, nous devons désactiver le test de profondeur. Si nous ne le faisons pas le mélange n'aura lieu que dans certains cas et pas dans d'autres. Par exemple, si nous dessinons une face de notre cube se trouvant derrière une plus en avant, alors lorsqu'elle sera tracée, elle sera écrite dans le frame buffer, puis celle située en avant sera mélangée par-dessus, c'est qui est bien ce que nous voulons. Cependant, si nous dessinons la face de devant en premier, la face arrière sera rejetée par le test de profondeur avant d'arriver à la fonction de mélange, de sorte qu'elle ne contribuera pas à l'image. Et ce n'est pas ce que nous voulons.

Les lecteurs attentifs auront remarqué de ceci (et de la fonction de mélange ci-dessus) qu'il y a dans le mélange une forte dépendance de l'ordre dans lequel vous dessinez les éléments que nous n'avions pas rencontrés dans les leçons précédentes. La suite plus tard ; finissons-en avec ce bout de code d'abord :

 
Sélectionnez
      gl.uniform1f(shaderProgram.alphaUniform, parseFloat(document.getElementById("alpha").value));

Nous chargeons ici une valeur alpha d'un champ de texte de la page et l'envoyons aux shaders. C'est parce que l'image que nous utilisons pour la texture n'a pas de canal alpha (juste RGB, donc chaque pixel a une valeur alpha implicite de 1) c'est donc agréable d'être en mesure d'ajuster la valeur alpha pour observer comment elle affecte l'image.

Le reste du code dans drawScene sert juste à gérer les choses de façon normale lorsque le mélange est désactivé :

 
Sélectionnez
    } else {
      gl.disable(gl.BLEND);
      gl.enable(gl.DEPTH_TEST);
    }

Il y a aussi un petit changement dans le fragment shader afin d'utiliser la valeur alpha lors du traitement de la texture :

 
Sélectionnez
  precision mediump float;

  varying vec2 vTextureCoord;
  varying vec3 vLightWeighting;

  uniform float uAlpha; // NOUVEAU

  uniform sampler2D uSampler;

  void main(void) {
     vec4 textureColor = texture2D(uSampler, vec2(vTextureCoord.s, vTextureCoord.t));
     gl_FragColor = vec4(textureColor.rgb * vLightWeighting, textureColor.a * uAlpha); // MODIFIÉ
  }

C'est tout ce qui a changé dans le code !

Revenons donc à cette remarque sur l'ordre de dessin. L'effet de transparence obtenu avec cet exemple est plutôt bon - cela ressemble vraiment à du vitrail. Maintenant regardez-le de nouveau, et changez la lumière directionnelle de sorte à ce qu'elle arrive d'une direction Z positive - enlevez juste le « - » du bon champ. Ça a l'air toujours aussi cool, mais l'effet réaliste de « vitrail » a disparu.

La raison est qu'avec l'éclairage initial, la face arrière du cube est toujours légèrement éclairée. Ce qui signifie que ses valeurs R, G et B sont petites, ainsi lorsque l'équation

  • Rresult = Rs * Ra + Rd

… est calculée, elles interviennent moins. Autrement-dit, l'éclairage fait que les éléments situés vers l'arrière sont moins visibles. Si nous inversons l'éclairage de sorte à avoir les éléments vers l'avant moins visibles, alors notre effet de transparence fonctionne parfaitement.

Finalement comment obtenir une transparence « correcte » ? Eh bien Image non disponiblela FAQ OpenGL dit que vous devez utiliser un facteur source SRC_ALPHA et un facteur destination de ONE_MINUS_SRC_ALPHA. Mais nous avons encore le problème que les fragments de source et de destination sont traités différemment, et donc dépendants de l'ordre des éléments dessinés. Et ce point nous amène finalement à ce que je pense être le sombre secret de la transparence dans Open-/WebGL, en citant de nouveau la FAQ OpenGL :

When using depth buffering in an application, you need to be careful about the order in which you render primitives. Fully opaque primitives need to be rendered first, followed by partially opaque primitives in back-to-front order. If you don't render primitives in this order, the primitives, which would otherwise be visible through a partially opaque primitive, might lose the depth test entirely.

« Lorsque vous utilisez un tampon de profondeur dans une application, vous devez faire attention à l'ordre de rendu des primitives. Les primitives totalement opaques doivent être rendues en premier, suivies par les partiellement opaques de l'arrière vers l'avant. Si vous ne respectez pas cet ordre, les primitives qui seraient normalement visibles au travers d'une primitive opaque, pourraient totalement échouer au test de profondeur. »

Et voilà. La transparence par le mélange est assez délicate et fastidieuse, mais si vous maîtrisez suffisamment bien les autres aspects de la scène, tels que l'éclairage dans cette leçon, vous pouvez l'obtenir sans trop de difficultés. Vous pouvez le faire correctement, mais devrez faire attention de dessiner les choses dans un ordre très spécifique pour obtenir un bon rendu.

Heureusement, le mélange peut servir à faire d'autres effets, comme vous le verrez dans la prochaine leçon. Mais pour l'instant, vous savez tout ce qu'il y a à apprendre dans cette leçon : vous avez une bonne compréhension du tampon de profondeur, et savez que le mélange peut être utilisé pour fournir de la transparence.

C'était pour moi la partie la plus difficile des leçons NeHe à comprendre lorsque je l'ai fait, et j'espère que j'ai réussi à rendre tout cela clair, si ce n'est plus clair que l'original.

Dans la prochaine leçon, nous commencerons à améliorer la structure du code afin de pouvoir accueillir un grand nombre d'objets différents dans la scène, sans toutes ces variables globales en désordre.

V. Remerciements

Parler à Jonathan Hartley (pour ne pas dire le codeur d'OpenGL lui-même) m'a éclairé sur le tampon de profondeur et le mélange, et la description du Z-buffer de Steve Baker m'a également été très utile. Comme toujours, je suis extrêmement redevable envers Nehe pour son tutoriel OpenGL pour le script de cette leçon.

Merci à LittleWhite pour sa relecture attentive et zoom61 pour sa relecture orthographique.

Navigation