Navigation

Tutoriel précédent : éclairage par pixel et programmes multiples   Sommaire   Tutoriel suivant : les textures spéculaires

II. Introduction

Bienvenue dans ma quatorzième leçon de la série sur WebGL ! Dans celle-ci, nous allons voir la dernière partie du modèle de réflexion de Phong que nous avions présenté dans la septième leçon : la surbrillance spéculaire. Les « reflets » sur une surface lustrée qui rendent la scène encore plus réaliste.

Voici ce que le résultat sera lorsque vous exécuterez cette leçon dans un navigateur qui supporte WebGL :



Cliquez ici et vous verrez la version WebGL en ligne si votre navigateur le supporte ; cliquez ici pour en avoir un s'il ne supporte pas WebGL.

Vous allez voir une théière en rotation, et à mesure qu'elle tourne vous allez voir un reflet constant sur la moitié gauche et la poignée du couvercle. Il y a aussi quelques autres reflets ponctuels sur le bec et l'anse lorsqu'ils atteignent l'angle parfait pour capturer la lumière. Vous pouvez activer ou désactiver cette surbrillance spéculaire avec la case à cocher en dessous. Vous pouvez aussi désactiver complètement la lumière et alterner entre les textures : aucune, « galvanisée » qui est celle utilisée par défaut (et qui est un échantillon sous licence « Creative common » des excellentes textures de Arroway), et, juste pour le plaisir, de la Terre (propriété de l'Agence Spatiale Européenne/Envisat), qui est étrangement attractive sur une théière. :)

Vous pouvez aussi contrôler le facteur de brillance (« shininess ») de la théière avec les champs en bas de la page. De grands nombres signifient une surbrillance plus petite, tranchante. Vous pouvez aussi contrôler la position, la couleur de la réflexion ainsi que celle de la couleur diffuse de la lumière. Apprenez 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 tutoriels précédents, vous devriez le faire avant de lire celui-ci. Ici, je vais seulement expliquer les nouveaux éléments. La leçon est basée sur la treizième leçon, donc vous devez vous assurer de l'avoir comprise.

Il y a deux façons de récupérer le code de cet exemple : soit en affichant le code avec le menu « 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.

III. Leçon

Une fois que vous avez le code, ouvrez-le dans un éditeur. Nous allons commencer par le haut et descendre au fur et à mesure. Cela a l'avantage de commencer par le fragment shader, là où les changements sont les plus intéressants. Avant celui-ci, vous pouvez remarquer qu'il y a un petit changement depuis la treizième leçon : nous n'avons plus les shaders pour l'éclairage par vertex. L'éclairage par vertex ne gère pas si bien que cela les surbrillances spéculaires (vu qu'elles sont étalées sur une face complète), donc nous ne nous embêtons pas avec.

Le premier shader que vous allez voir dans le fichier est donc le fragment shader pour l'éclairage par pixel. La première partie définit la précision et la déclaration des variables dont certaines sont nouvelles et dont l'une d'elles a été renommée maintenant que la lumière ponctuelle possède deux composantes : la couleur diffuse et la couleur spéculaire.

 
Sélectionnez
  precision mediump float;

  varying vec2 vTextureCoord;
  varying vec3 vTransformedNormal;
  varying vec4 vPosition;

  uniform float uMaterialShininess; // Nouveau

  uniform bool uShowSpecularHighlights; // Nouveau
  uniform bool uUseLighting;
  uniform bool uUseTextures;

  uniform vec3 uAmbientColor;

  uniform vec3 uPointLightingLocation;
  uniform vec3 uPointLightingSpecularColor; // Nouveau
  uniform vec3 uPointLightingDiffuseColor; // Renommée

  uniform sampler2D uSampler;

Ce code ne devrait pas avoir besoin de plus d'explications. Ce sont juste des valeurs que vous pouvez changer à partir de la page HTML et qui sont envoyées au shader pour les traiter. Avançons dans le shader. La première chose à gérer est le cas où l'utilisateur a désactivé la lampe et le code est identique à celui de la dernière fois :

 
Sélectionnez
  void main(void) {
    vec3 lightWeighting;
    if (!uUseLighting) {
      lightWeighting = vec3(1.0, 1.0, 1.0);
    } else {

Maintenant nous gérons la lumière et bien sûr, c'est là que cela devient intéressant :

 
Sélectionnez
      vec3 lightDirection = normalize(uPointLightingLocation - vPosition.xyz);
      vec3 normal = normalize(vTransformedNormal); // Nouveau

      float specularLightWeighting = 0.0; // Nouveau
      if (uShowSpecularHighlights) { // Nouveau

Donc, que se passe-t-il ici  ? Et bien nous calculons la direction de la lumière tout comme nous le faisions pour l'éclairage par pixel. Ensuite nous normalisons le vecteur normal du pixel, une fois encore, comme précédemment. Rappelez-vous, lorsque les normales des vertex sont interpolées linéairement pour créer les normales par pixel, le résultat ne donne pas nécessairement des vecteurs de longueur unité, donc nous devons les normaliser pour corriger cela. Mais cette fois, nous allons garder le résultat dans une variable locale. Puis, nous définissons un facteur pour la luminosité supplémentaire venant de la surbrillance spéculaire. Elle est actuellement de zéro et le restera si la surbrillance spéculaire est désactivée, sinon nous devons la calculer.

Donc, comment est déterminée la surbrillance spéculaire ? Vous vous souvenez peut-être de l'explication du modèle de réflexion de Phong de la septième leçon : la surbrillance spéculaire est créée par la portion de lumière d'une source lumineuse qui rebondit sur la surface comme le ferait un miroir :

La partie de la lumière réfléchie rebondit sur la surface avec le même angle sous lequel elle l'a frappé. Dans ce cas, la brillance de la lumière réfléchie sur le matériau dépend de si vous vous situez ou non dans la ligne dans laquelle elle a été réfléchie. C'est-à-dire qu'elle ne dépend pas seulement de l'angle sous lequel la lumière frappe la surface, mais aussi de l'angle entre votre ligne de mire et la surface. Cette réflexion spéculaire est ce qui provoque les « reflets » ou la « surbrillance » sur les objets. L'intensité de la réflexion spéculaire peut évidemment varier d'un matériau à l'autre ; le bois brut aura probablement très peu de réflexion spéculaire alors que le métal fortement poli en aura beaucoup.

L'équation spécifique pour trouver la luminosité de la réflexion spéculaire est :

Image non disponible

où Rm est le vecteur (normalisé) parfaitement réfléchi d'un rayon de lumière venant de la source lors du rebond sur le point de la surface actuellement en considération. V est le vecteur (normalisé, lui aussi) pointant dans la direction de la caméra et α est une constante décrivant le facteur de brillance. Plus il est élevé, plus la surface semble polie. Vous vous rappelez sans doute que le produit scalaire de deux vecteurs correspond au cosinus de l'angle entre eux. Cela signifie que cette partie de l'équation donne 1 si la lumière de la source est réfléchie directement dans la caméra (lorsque Rm et V sont parallèles et donc que l'angle entre les deux vecteurs est zéro, donc le cosinus de zéro est 1) et la lumière s'assombrit assez doucement lorsque la lumière est moins directement réfléchie. En prenant cette valeur et en la mettant à la puissance de α, cela donne un effet de « compression » : cela garde une valeur de 1 lorsque les deux vecteurs sont parallèles, mais diminue plus rapidement pour les autres cas. Vous pouvez voir cela si vous définissez la constante « shininess » à (disons) 512 sur la page de démonstration.

Sachant cela, les premières choses que nous devons trouver sont la direction de la camera, V et la direction du rayon parfaitement réfléchi, Rm. Commençons par V, car c'est plus simple ! Notre scène est construite dans l'espace de coordonnées de la vue, comme nous l'avons vu dans la dixième leçon. En réalité, cela signifie que nous dessinons la scène comme si la caméra se positionnait en (0, 0, 0) et qu'elle regardait vers le côté négatif de l'axe des Z. Lorsque X augmente, nous allons sur la droite et lorsque Y augmente, nous nous élevons. Ainsi, la direction de la caméra à l'origine pour n'importe quel point n'est simplement que la valeur négative de ses coordonnées. Nous avons les coordonnées du pixel, interpolées linéairement à partir des coordonnées du vertex, dans vPosition, donc nous déterminons sa valeur négative et nous la normalisons. C'est tout !

 
Sélectionnez
        vec3 eyeDirection = normalize(-vPosition.xyz);

Maintenant, regardons Rm. Cela nécessiterait un peu plus de travail s'il n'y avait pas une fonction GLSL très pratique appelée reflect, définie comme suit :

reflect (I, N) : pour le vecteur incident I et l'orientation de surface N, retourne la direction de réflexion.

Le vecteur incident est la direction du rayon de lumière touchant la surface en ce pixel, qui est l'opposé de la direction de la lumière en partant du pixel (ce que nous avons déjà dans lightDirection). L'orientation de la surface est appelée N car c'est simplement la normale, que nous avons aussi. Sachant tout cela, c'est simple :

 
Sélectionnez
        vec3 reflectionDirection = reflect(-lightDirection, normal);

Maintenant que nous avons cela, la dernière étape est évidemment très simple :

 
Sélectionnez
        specularLightWeighting = pow(max(dot(reflectionDirection, eyeDirection), 0.0), uMaterialShininess);
      }

C'est tout ce dont nous avons besoin pour trouver la contribution de la composante spéculaire de l'éclairage du pixel. La prochaine étape est de trouver le facteur de contribution diffuse en utilisant la même logique que précédemment (sauf que nous utilisons notre variable locale pour la normale normalisée).

 
Sélectionnez
      float diffuseLightWeighting = max(dot(normal, lightDirection), 0.0);

Finalement, nous utilisons les trois facteurs, la couleur diffuse et la couleur spéculaire ainsi que la couleur ambiante pour trouver l'effet de la lumière pour ce pixel. C'est une simple extension de ce que nous avons vu avant :

 
Sélectionnez
      lightWeighting = uAmbientColor
        + uPointLightingSpecularColor * specularLightWeighting // Nouveau
        + uPointLightingDiffuseColor * diffuseLightWeighting; // Renommé    }

Une fois cela fait, nous avons le poids de la lumière et donc nous pouvons utiliser le même code que dans la treizième leçon pour déterminer la couleur du pixel selon la texture et la lumière :

 
Sélectionnez
    vec4 fragmentColor;
    if (uUseTextures) {
      fragmentColor = texture2D(uSampler, vec2(vTextureCoord.s, vTextureCoord.t));
    } else {
      fragmentColor = vec4(1.0, 1.0, 1.0, 1.0);
    }
    gl_FragColor = vec4(fragmentColor.rgb * lightWeighting, fragmentColor.a);
  }

C'est tout pour le pixel shader !

Continuons dans le code. Si vous regardez les différences avec la treizième leçon, la prochaine chose que vous allez remarquer est le retour de la fonction initShaders dans sa forme la plus simple, créant un seul programme shader tout en initialisant quelques nouvelles variables uniformes pour la composante spéculaire. Un peu plus bas dans le code, la fonction initTextures charge maintenant les textures de la Terre et du fer galvanisé à la place des textures de la lune et de la boîte. Encore plus bas, la fonction setMatrixUniforms est simplifiée et ne s'occupe plus que d'un programme shader, tout comme initShaders. Nous avons enfin atteint quelque chose d'intéressant.

À la place d'avoir la fonction initBuffers pour créer les tampons WebGL contenant les divers attributs par vertex qui définissent l'aspect de la théière, nous avons deux fonctions : handleLoadedTeapot et loadTeapot. Le principe sera similaire à celui de la dixième leçon, mais cela est intéressant de le revoir. Analysons la fonction loadTeapot (même si elle est placée après dans le code) :

 
Sélectionnez
  function loadTeapot() {
    var request = new XMLHttpRequest();
    request.open("GET", "Teapot.json");
    request.onreadystatechange = function() {
      if (request.readyState == 4) {
        handleLoadedTeapot(JSON.parse(request.responseText));
      }
    }
    request.send();
  }

La structure globale est donc similaire à celle de la dixième leçon. Nous créons une nouvelle XMLHttpRequest et l'utilisons pour charger le fichier Teapot.json. Cela se passe de façon asynchrone, donc nous attachons une fonction callback qui sera appelée à différentes étapes du chargement et dans cette fonction nous effectuons quelques tâches lorsque le chargement atteint un readyState de 4, signifiant la fin du chargement.

Le morceau intéressant est ce qui arrive ensuite. Le fichier que nous chargeons est au format JSON, qui signifie essentiellement qu'il est déjà écrit en JavaScript. Ouvrons-le pour voir. Le fichier décrit un objet JavaScript contenant des listes de positions de vertex, de normales, de coordonnées de texture et un ensemble d'indices de vertex décrivant entièrement la théière. Nous pourrions bien sûr embarquer le code directement dans notre fichier index.html, mais si vous construisez des modèles complexes avec de multiples sous-objets, vous voudrez les avoir dans différents fichiers.

(La question du format que vous devez utiliser pour les objets dans vos applications WebGL est intéressante. Vous pouvez les concevoir avec plusieurs logiciels qui peuvent produire des fichiers dans de nombreux formats de modèle, allant du .obj au .3DS. Dans le futur, il semble que l'un d'entre eux sera capable de produire des modèles dans un format natif JavaScript, qui, je pense, ressemblera au modèle JSON que j'ai utilisé pour la théière. Pour l'instant, vous devez considérer ce tutoriel comme un exemple pour charger un modèle préconçu dans le format JSON et non un exemple de la meilleure méthode. Image non disponible)

Donc, nous avons le code qui charge un fichier au format JSON et déclenche une action une fois chargé. L'action convertit le texte JSON en données que nous pouvons utiliser. Nous pourrions utiliser la fonction eval pour le convertir en un objet JavaScript, mais cela est généralement mal vu, donc nous utilisons à la place la fonction intégrée JSON.parse pour analyser l'objet. Une fois cela fait, nous le passons à la fonction handleLoadedTeapot :

 
Sélectionnez
  var teapotVertexPositionBuffer;
  var teapotVertexNormalBuffer;
  var teapotVertexTextureCoordBuffer;
  var teapotVertexIndexBuffer;
  function handleLoadedTeapot(teapotData) {
    teapotVertexNormalBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, teapotVertexNormalBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(teapotData.vertexNormals), gl.STATIC_DRAW);
    teapotVertexNormalBuffer.itemSize = 3;
    teapotVertexNormalBuffer.numItems = teapotData.vertexNormals.length / 3;

    teapotVertexTextureCoordBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, teapotVertexTextureCoordBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(teapotData.vertexTextureCoords), gl.STATIC_DRAW);
    teapotVertexTextureCoordBuffer.itemSize = 2;
    teapotVertexTextureCoordBuffer.numItems = teapotData.vertexTextureCoords.length / 2;

    teapotVertexPositionBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, teapotVertexPositionBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(teapotData.vertexPositions), gl.STATIC_DRAW);
    teapotVertexPositionBuffer.itemSize = 3;
    teapotVertexPositionBuffer.numItems = teapotData.vertexPositions.length / 3;

    teapotVertexIndexBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, teapotVertexIndexBuffer);
    gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(teapotData.indices), gl.STATIC_DRAW);
    teapotVertexIndexBuffer.itemSize = 1;
    teapotVertexIndexBuffer.numItems = teapotData.indices.length;

    document.getElementById("loadingtext").textContent = "";
  }

Il n'y a rien à mettre en avant dans cette fonction. Elle prend juste les différentes listes de l'objet JSON chargé et les place dans des tableaux WebGL qui sont alors envoyés à la carte graphique à l'aide de tampons. Une fois que tout est prêt, nous effaçons un div du document HTML qui indiquait à l'utilisateur que le modèle était en cours de chargement, tout comme nous le faisions dans la dixième leçon.

Donc, le modèle est chargé. Quoi d'autre ? Il y a la fonction drawScene qui nécessite maintenant d'afficher une théière avec l'angle approprié (après avoir vérifié que le modèle est chargé), mais il n'y a rien de bien nouveau ici. Regardez le code et assurez-vous que vous le comprenez, mais je doute qu'il vous surprenne.

Après, la fonction animate possède quelques modifications pour faire tourner la théière au lieu de la lune et de la boîte. La fonction webGLStart doit appeler la fonction loadTeapot à la place de initBuffers. Et finalement, le code HTML possède une balise div et son code CSS associé pour afficher le message « Loading world... » tant que le modèle n'est pas chargé complètement. De plus, de nouveaux contrôles ont été ajoutés pour les paramètres de la surbrillance spéculaire.

Après ça, c'est fini ! Vous savez maintenant écrire des shaders pour afficher la surbrillance spéculaire et charger un modèle préconçu à partir d'un fichier JSON. La prochaine fois nous étudierons une technique un peu plus avancée : comment utiliser les textures d'une façon plus intéressante que ce que nous faisions actuellement, à l'aide d'une texture spéculaire.

IV. Remerciements

La texture galvanisée est un échantillon sous licence Creative Common de Arroway et la texture de la planète Terre vient de l'Agence Spatiale Européenne/Envisat.

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

Navigation

Tutoriel précédent : éclairage par pixel et programmes multiples   Sommaire   Tutoriel suivant : les textures spéculaires