Navigation

Tutoriel précédent : lumière ponctuelle   Sommaire   Tutoriel suivant : surbrillance spéculaire et chargement de modèle JSON

II. Introduction

Bienvenue dans ma treizième leçon de la série sur WebGL ! Dans celle-ci, nous allons étudier l'éclairage par pixel, qui demande plus de travail à la carte graphique que l'éclairage par vertex que nous utilisions jusqu'à maintenant, mais qui donne des résultats plus réalistes. Nous allons aussi découvrir comment vous pouvez alterner les shaders utilisés par votre code en changeant le programme shader utilisé par WebGL.

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 sphère et un cube tourner. Les deux objets seront probablement blancs pendant quelques instants, le temps que les textures se chargent, mais une fois que cela est fait vous devriez voir que la sphère est une lune et le cube (pas à l'échelle), une boîte en bois. La scène est similaire à celle que nous avions dans la douzième leçon, mais nous sommes plus proches des objets afin de mieux voir à quoi ils ressemblent. Comme précédemment, les deux objets sont éclairés par une lumière ponctuelle placée entre et si vous souhaitez changer la position ou la couleur de la lumière, il y a des contrôles sous le canvas WebGL, ainsi que des cases à cocher pour activer et désactiver la lampe, alterner entre l'éclairage par pixel ou par vertex et activer ou désactiver les textures.

Essayez d'activer et de désactiver l'éclairage par pixel. Vous devriez voir très clairement la différence sur la boîte. Le centre est évidemment plus clair avec l'éclairage par pixel. La différence avec la lune est plus légère. Les bordures où la lumière s'atténue sont plus douces et moins crénelées avec l'éclairage par pixel par rapport à l'éclairage par vertex. Vous allez probablement voir ceci plus facilement en désactivant les textures.

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 nouvelles choses. La leçon est basée sur la douziè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

Commençons par décrire exactement pourquoi cela vaut le coup d'utiliser plus de puissance graphique pour l'éclairage par pixel. Vous vous souvenez du diagramme de la septième leçon.

Image non disponible

Comme vous le savez, la clarté d'une surface est déterminée par l'angle entre sa normale et les rayons arrivant d'une source lumineuse. Jusqu'à présent, notre éclairage était calculé dans le vertex shader en combinant les normales spécifiées pour chaque vertex avec la direction de la lumière arrivant sur ces vertex. Cela nous fournissait un facteur de lumière qui était passé du vertex shader au pixel shader grâce à une variable « varying » pour être finalement utilisé comme luminosité pour la couleur du pixel. Ce facteur de lumière, comme toutes les variables « varying » est interpolé linéairement pour tous les pixels entre deux vertex. Donc, dans le diagramme, le point B sera complètement éclairé, car la lumière est parallèle avec la normale en ce point. Mais A sera plus sombre, car la lumière l'atteint avec une certaine inclinaison et les pixels entre les deux points seront doucement dégradés du plus éclairé au plus sombre. Cela paraîtra correct.

Image non disponible

Mais, imaginez maintenant que la lumière soit plus haute, comme montré dans ce nouveau diagramme. Les points A et C seront plus sombres, car la lumière les atteint avec une certaine inclinaison. Nous calculons l'éclairage seulement au niveau du vertex, donc le point B recevra la luminosité moyenne de A et C faisant que le point sera sombre. Cela n'est pas correct. La lumière est parallèle à la normale de la surface au point B, donc il devrait être plus clair que les autres. Pour calculer l'éclairage au niveau des pixels et non au niveau des vertex, nous devons évidemment faire les calculs pour chaque pixel.

Calculer l'éclairage pour chaque pixel signifie que nous avons besoin de leur position (pour trouver la direction de la lumière) et de leur normale. Nous pouvons les récupérer à partir du vertex shader. Les deux données seront interpolées linéairement, donc la position se tiendra sur une ligne entre les vertex, et les normales varieront doucement. Cette ligne droite est exactement ce que nous voulons, car les normales de A et de C sont les mêmes, donc les normales seront les mêmes pour tous les pixels, ce qui est parfait.

Cela explique pourquoi le cube dans notre page Web est mieux et plus réaliste avec l'éclairage par pixel. Mais, il y a un autre avantage et c'est ce qui nous donne un joli effet pour les formes constituées de multiples surfaces planes pour approximer les surfaces courbes, telles que notre sphère. Si les normales de deux vertex sont différentes, alors les normales interpolées des pixels donneront un effet de surface courbe. En prenant cela en considération, l'éclairage par pixel est une forme de ce que l'on appelle le « Phong shading » et cette image sur Wikipédia montre mieux l'effet que je ne pourrais l'expliquer en une centaine de mots. Vous pouvez le voir dans la démonstration. Si vous utilisez l'éclairage par vertex, vous pouvez voir les bordures de l'ombre (où la lumière ponctuelle n'a plus d'effet et la lumière ambiante reprend le dessus) qui semblent un peu « irrégulières ». Cela est dû au fait que la sphère est constituée de triangles et que vous pouvez voir leurs côtés. Lorsque vous passez à l'éclairage par pixel, vous pouvez voir que la transition est plus douce, donnant un meilleur effet d'arrondi.

Bien, c'est fini avec la théorie. Regardons le code ! Les shaders se trouvent en haut du fichier, étudions-les en premier. Comme cet exemple utilise au choix l'éclairage par vertex ou l'éclairage par pixel, suivant la case à cocher « per-fragment », il possède le vertex et le pixel shaders des deux types d'éclairage (on aurait pu écrire des shaders gérant les deux cas, mais cela aurait été plus compliqué à lire). La méthode pour alterner les shaders est une chose qui viendra par la suite, pour le moment, notez que nous distinguons les deux en utilisant des id différents pour identifier les scripts dans la page Web. Les premiers sont les shaders pour l'éclairage par vertex et sont exactement les mêmes que ceux que nous utilisions dans la septième leçon, donc je vous montre juste leur balise « script » afin que vous puissiez suivre avec le fichier :

 
Sélectionnez
<script id="per-vertex-lighting-fs" type="x-shader/x-fragment"> 
<script id="per-vertex-lighting-vs" type="x-shader/x-vertex">

Ensuite nous trouvons le pixel shader pour l'éclairage par pixel.

 
Sélectionnez
<script id="per-fragment-lighting-fs" type="x-shader/x-fragment"> 
  precision mediump float; 

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

  uniform bool uUseLighting; 
  uniform bool uUseTextures; 

  uniform vec3 uAmbientColor; 

  uniform vec3 uPointLightingLocation; 
  uniform vec3 uPointLightingColor; 

  uniform sampler2D uSampler; 

  void main(void) { 
    vec3 lightWeighting; 
    if (!uUseLighting) { 
      lightWeighting = vec3(1.0, 1.0, 1.0); 
    } else { 
      vec3 lightDirection = normalize(uPointLightingLocation - vPosition.xyz); 

      float directionalLightWeighting = max(dot(normalize(vTransformedNormal), lightDirection), 0.0); 
      lightWeighting = uAmbientColor + uPointLightingColor * directionalLightWeighting; 
    } 

    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); 
  } 
</script>

Vous pouvez voir qu'il est très semblable au vertex shader que nous utilisions jusqu'à présent. Il effectue exactement les mêmes calculs pour trouver la direction de la lumière puis pour combiner la normale à la direction afin d'avoir le facteur d'éclairage. La différence est que les entrées de ce calcul proviennent des variables « varying » au lieu des attributs de vertex et le facteur résultant est immédiatement combiné à la couleur de la texture du « sample » au lieu d'être retourné pour être utilisé par la suite. Il est aussi nécessaire de noter que vous devez normaliser la variable « varying » de la normale. La normalisation, pour rappel, ajuste le vecteur pour que sa longueur soit d'une unité. En effet, l'interpolation entre deux vecteurs de longueur unité ne résultera pas nécessairement en un vecteur de longueur unité, mais juste en un vecteur qui pointera dans la bonne direction. La normalisation les corrigera. (Merci à Glut d'avoir corrigé cela dans les commentaires.)

Comme tout le travail est effectué dans le pixel shader, le vertex shader pour l'éclairage par pixel est très simple :

 
Sélectionnez
<script id="per-fragment-lighting-vs" type="x-shader/x-vertex"> 
  attribute vec3 aVertexPosition; 
  attribute vec3 aVertexNormal; 
  attribute vec2 aTextureCoord; 

  uniform mat4 uMVMatrix; 
  uniform mat4 uPMatrix; 
  uniform mat3 uNMatrix; 

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

  void main(void) { 
    vPosition = uMVMatrix * vec4(aVertexPosition, 1.0); 
    gl_Position = uPMatrix * vPosition; 
    vTextureCoord = aTextureCoord; 
    vTransformedNormal = uNMatrix * aVertexNormal; 
  } 
</script>

Nous devons toujours trouver la position du vertex après l'application de la matrice de modèle-vue et multiplier la normale avec la matrice des normales, mais maintenant, nous gardons les résultats dans des variables « varying » pour une utilisation future dans le pixel shader.

C'est tout pour les shaders ! Le reste du code est très proche de celui des leçons précédentes à une exception près. Jusqu'à présent, nous n'avions utilisé qu'un seul vertex shader et qu'un seul pixel shader par page WebGL. Celle-ci utilise deux paires, une pour l'éclairage par vertex et la seconde pour l'éclairage par pixel. Maintenant, vous vous souvenez de la première leçon que l'objet programme que nous utilisons pour passer les shaders à la carte graphique ne peut avoir qu'un seul fragment shader et qu'un seul vertex shader. Cela signifie que nous avons besoin de deux programmes et d'alterner celui utilisé suivant la case à cocher « per-fragment ».

La méthode pour ce faire est simple. Notre fonction initShaders est modifiée comme ceci :

 
Sélectionnez
  var currentProgram; 
  var perVertexProgram; 
  var perFragmentProgram; 
  function initShaders() { 
    perVertexProgram = createProgram("per-vertex-lighting-fs", "per-vertex-lighting-vs"); 
    perFragmentProgram = createProgram("per-fragment-lighting-fs", "per-fragment-lighting-vs"); 
  }

Donc, nous avons deux programmes dans des variables globales séparées, un pour l'éclairage par vertex et l'autre pour l'éclairage par pixel et une variable currentProgram pour garder celui actuellement utilisé. La fonction createProgram que nous utilisons pour les créer est simplement une version paramétrée du code que nous avions dans la fonction initShaders, je ne vais donc pas la remettre ici.

Nous passons au programme approprié juste au début de la fonction drawScene :

 
Sélectionnez
  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); 

    var perFragmentLighting = document.getElementById("per-fragment").checked; 
    if (perFragmentLighting) { 
      currentProgram = perFragmentProgram; 
    } else { 
      currentProgram = perVertexProgram; 
    } 
    gl.useProgram(currentProgram);

Nous devons le faire avant toute autre chose, car lorsque nous faisons le code de dessin (par exemple, définir les variables uniformes ou attacher les tampons d'attributs) nous avons besoin que le programme soit correctement défini, sinon nous pourrions utiliser le mauvais :

 
Sélectionnez
    var lighting = document.getElementById("lighting").checked; 
    gl.uniform1i(currentProgram.useLightingUniform, lighting);

Vous pouvez voir que cela signifie que pour chaque appel à drawScene, nous utilisons un et un seul programme. Il change seulement entre les appels. Si vous vous demandez si vous pouvez ou non utiliser différents shaders à plusieurs instants dans drawScene, afin que différentes parties de la scène soient dessinées avec différents shaders, par exemple pour avoir des parties utilisant un éclairage par vertex et d'autres parties un éclairage par pixel, alors la réponse est oui ! Cela n'était pas nécessaire pour cet exemple, mais c'est parfaitement valide et cela peut être utile.

Cela conclut cette leçon ! Vous savez maintenant comment utiliser plusieurs programmes pour alterner vos shaders et implémenter un éclairage par pixel. La prochaine fois, nous allons voir le dernier morceau sur l'éclairage que j'ai mentionné dans la septième leçon : la surbrillance spéculaire.

IV. Remerciements

Comme précédemment, la texture de la lune provient du site JPL de la NASA et le code pour générer la sphère est basé sur cette démonstration, qui a été originellement écrite par l'équipe du WebKit. Je les remercie chaleureusement.

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

Navigation

Tutoriel précédent : lumière ponctuelle   Sommaire   Tutoriel suivant : surbrillance spéculaire et chargement de modèle JSON