Navigation▲
Tutoriel précédent : clavier et filtres de texture | Sommaire | Tutoriel suivant : le tampon de profondeur, transparence et mélange |
II. Introduction▲
Bienvenue dans mon septième tutoriel WebGL basé sur une partie du tutoriel 7 des tutoriels OpenGL de NeHe que je n'ai pas abordée dans la sixième leçon. Dans cet article, nous allons aborder la façon d'ajouter un éclairage simple dans vos pages WebGL ; cela requiert un peu plus de travail que sous OpenGL, mais j'ai l'espoir que c'est assez facile à comprendre.
Voici à quoi ressemble la leçon lorsqu'elle est exécutée sur un navigateur qui prend en charge WebGL :
Cliquez pour lire la 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 en rotation lente, avec de l'éclairage semblant venir d'un point situé vers l'avant (c'est-à-dire entre vous et le cube) légèrement en haut à droite.
Vous pouvez utiliser la case à cocher sous le canvas pour éteindre ou allumer la lumière afin d'observer son effet. Vous pouvez aussi changer la couleur des éclairages directionnel et ambiant (plus de précisions par la suite) et l'orientation de la lumière directionnelle. Essayez de jouer un peu avec ; c'est particulièrement amusant d'essayer des effets spéciaux avec des valeurs RGB d'éclairage directionnel supérieures à un (vous perdrez toutefois une grande partie de la texture pour des valeurs supérieures à cinq). En outre, comme là, vous pouvez également utiliser les touches directionnelles pour faire tourner la boîte plus rapidement ou plus lentement ; Page Précédente et Page Suivante pour zoomer et dézoomer. Cette fois-ci nous utilisons le meilleur filtre de texture, la touche 'F' ne sert plus.
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 sixième leçon et le nouveau code.
Il peut y avoir des bogues et problèmes de conception dans ce tutoriel. Si vous trouvez quoi que ce soit de faux, faites-le-moi savoir dans les commentaires et je le corrigerai dès que possible.
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. Dans tous les cas, une fois que vous avez le code, chargez-le dans votre éditeur de texte favori et jetez-y un œil.
III. Leçon▲
Avant de rentrer dans les détails sur la façon d'effectuer l'éclairage dans WebGL, je commencerai par les mauvaises nouvelles. WebGL n'a absolument aucun support intégré pour l'éclairage. À la différence d'OpenGL, qui vous permet de spécifier au moins huit sources lumineuses, et gère tout à votre place, WebGL vous laisse tout faire vous-même. Mais - et c'est un grand « mais » - l'éclairage est plutôt facile une fois expliqué. Si vous êtes à l'aise avec ce qui touche aux shaders, nous en avons fini, vous n'aurez aucun problème avec l'éclairage - et maintenant que vous avez progressé, vous comprendrez plus facilement le code de vos propres lumières ! Après tout, l'éclairage OpenGL est trop basique pour faire des scènes vraiment réalistes - il ne gère pas les ombres, par exemple, et peut donner des effets grossiers sur des surfaces courbées - donc n'importe quoi dépassant de simples scènes nécessitera de toute façon de le coder à la main.
OK. Commençons par réfléchir à ce que nous voulons de l'éclairage. L'objectif est de pouvoir simuler un certain nombre de sources de lumière dans la scène. Ces sources n'ont pas besoin d'être visibles elles-mêmes, mais elles ont besoin d'éclairer les objets 3D de façon réaliste, afin que le côté de l'objet exposé à la lumière soit brillant et le côté éloigné de la lumière soit sombre. Autrement dit, nous voulons être en mesure de spécifier un ensemble de sources de lumière, puis nous voulons travailler sur la façon dont l'ensemble des lumières affecte chaque partie de notre scène 3D. À présent, je suis sûr que vous connaissez suffisamment WebGL pour savoir que cela va impliquer l'utilisation des shaders. Plus précisément, dans cette leçon nous allons écrire les vertex shaders qui gèrent l'éclairage. Nous allons travailler sur comment la lumière affecte chaque vertex afin d'ajuster leur couleur. Pour l'instant nous ne le ferons que pour une lumière, employer plusieurs lumières reviendra à répéter la même procédure pour chacune et additionner les résultats.
Puisque nous travaillons sur l'éclairage par vertex, les effets de la lumière sur les pixels situés entre les vertex seront calculés par l'interpolation linéaire habituelle.
Ce qui signifie que les espaces entre les vertex seront illuminés comme s'ils étaient à plat ; idéalement, parce que nous dessinons un cube, c'est exactement ce que nous voulons ! Pour les surfaces courbées, où vous voulez calculer les effets de l'éclairage pour chaque pixel de façon indépendante, vous pouvez utiliser une technique appelée éclairage par fragment (ou par pixel), ce qui donne de bien meilleurs effets. Nous verrons l'éclairage par fragment dans une prochaine leçon. Ce que nous faisons ici est appelé, assez logiquement, éclairage par vertex.
OK, passons à l'étape suivante : si notre tâche consiste à écrire un vertex shader qui travaille sur la manière dont une seule source de lumière affecte la couleur du vertex, que faisons-nous ? Eh bien, un bon point de départ est le modèle de réflexion Phong. Je l'ai trouvé plus facile à comprendre en commençant par les points suivants :
- alors qu'il n'y a dans le monde réel qu'un type de lumière, il est opportun de prétendre qu'il y en a deux pour les graphismes :
- La lumière qui vient d'une direction spécifique qui n'éclaire que les objets situés dans cette direction. Nous l'appellerons lumière directionnelle,
- La lumière qui vient de partout et éclaire tout de façon uniforme, indépendamment de la face exposée. On l'appelle lumière ambiante. (Bien sûr, dans le monde réel c'est juste la lumière directionnelle qui a été diffusée par la réflexion sur les autres objets, l'air, la poussière, etc.) Mais pour satisfaire nos besoins, nous allons les modéliser séparément ;
- lorsque la lumière frappe une surface, elle se reflète de deux façons :
- Diffuse : c'est-à-dire indépendamment de l'angle d'attaque, elle rebondit de manière égale dans toutes les directions. Quel que soit l'angle d'observation, la brillance de la lumière reflétée est entièrement commandée par l'angle sous lequel la lumière frappe la surface. Plus l'angle d'incidence est élevé, plus la réflexion est sombre. La réflexion diffuse pourrait faire penser à un objet qui est éclairé,
- Spéculaire : de manière semblable à 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ée. 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.
Le modèle de Phong ajoute une touche supplémentaire à ce système composé de quatre étapes, en considérant que toutes les lumières possèdent deux propriétés :
- Les valeurs RVB de la lumière diffuse qu'elles produisent ;
- Les valeurs RVB de la lumière spéculaire qu'elles produisent.
…et que tous les matériaux en possèdent quatre :
- Les valeurs RVB de la lumière ambiante qu'ils reflètent ;
- Les valeurs RVB de la lumière diffuse qu'ils reflètent ;
- Les valeurs RVB de la lumière spéculaire qu'ils reflètent ;
- La brillance de l'objet, qui détermine les détails de la réflexion spéculaire.
La couleur de chaque point de la scène est une combinaison de la couleur de la lumière qui l'éclaire, des couleurs du matériau et des effets de lumière. Donc pour définir complètement la lumière dans une scène selon le modèle Phong, nous avons besoin de deux propriétés par lumière et quatre par point de la surface de notre objet. La lumière ambiante, de sa nature même, n'est liée à aucune lumière particulière, mais nous avons aussi besoin d'un moyen de stocker son intensité globale dans la scène. Certaines fois il est plus facile de simplement spécifier un niveau ambiant pour chaque source de lumière et les ajouter au sein d'un seul terme.
Quoi qu'il en soit, une fois que nous avons ces informations, nous pouvons travailler sur les couleurs associées aux réflexions ambiante, directionnelle et spéculaire de la lumière sur tous les points, puis les ajouter ensemble pour obtenir la couleur globale. Voici un excellent diagramme sur Wikipédia montrant comment cela fonctionne. Tout ce que notre shader doit faire est : calculer les contributions sur chaque vertex des couleurs rouge, vert et bleu des lumières ambiante, diffuse et spéculaire ; les utiliser pour pondérer les composantes RVB, les additionner ensemble et renvoyer le résultat.
Maintenant, pour cette leçon, nous allons faire simple et seulement considérer les lumières diffuse et ambiante en ignorant la spéculaire. Nous allons utiliser le cube texturé de la dernière leçon et supposerons que les couleurs de la texture sont les valeurs des réflexions diffuse et ambiante. Finalement, nous considèrerons seulement un type de lumière diffuse - la lumière directionnelle est plus simple. Expliquons cela avec un schéma.
La lumière arrivant vers une surface selon une direction peut être de deux types - une simple lumière directionnelle qui est dans la même direction à travers toute la scène, et la lumière qui vient d'un seul endroit de la scène (vue sous un angle différent dans différents endroits donc).
Pour l'éclairage directionnel simple, l'angle d'incidence de la lumière au niveau des vertex sur une face donnée - aux points A et B sur le schéma - est toujours le même. Pensez à la lumière du soleil : tous les rayons sont parallèles.
Si, au contraire, la lumière arrive d'un point de la scène, l'angle de la lumière sera différent selon chaque vertex ; au point A dans ce second schéma, l'angle est d'environ 45 degrés, alors qu'au point B, il est d'environ 90 degrés par rapport à la surface.
Cela signifie que pour l'éclairage ponctuel, nous devons travailler sur la direction d'où vient la lumière pour chaque vertex, alors que pour un éclairage directionnel nous avons seulement besoin d'une valeur pour la source. Cela rend l'éclairage ponctuel un peu plus compliqué, donc cette leçon n'utilisera qu'un simple éclairage directionnel. L'éclairage ponctuel viendra plus tard (et cela ne devrait pas être trop difficile pour vous de travailler sur le vôtre de toute façon :-))
Donc, maintenant nous avons affiné le problème. Nous savons que toutes les lumières de notre scène vont provenir d'une direction particulière, direction qui ne changera pas d'un vertex à l'autre. Nous pouvons donc la mettre dans une variable uniforme, à laquelle le shader peut accéder. Nous savons aussi que l'effet de la lumière sur chaque vertex sera déterminé par l'angle formé avec la surface de notre objet en ce vertex, nous avons donc besoin de représenter l'orientation de la surface d'une quelconque manière. La meilleure façon de faire ceci en 3D consiste à spécifier le vecteur normal à la surface au niveau du vertex, ce qui nous permet de caractériser la direction face à la surface par trois nombres. (En 2D, nous pourrions tout aussi bien utiliser la tangente - c'est-à-dire la direction de la surface elle-même au niveau du vertex - mais en 3D la tangente peut être inclinée dans deux sens, de sorte que nous aurions besoin de deux vecteurs pour le décrire, tandis que la normale nous permet de n'en utiliser qu'un.)
Une fois que nous avons la normale, il reste un dernier élément nécessaire avant de pouvoir écrire notre shader. Étant donné le vecteur normal d'une surface en un vertex et le vecteur décrivant la direction de provenance de la lumière, nous devons travailler sur la quantité de lumière réfléchie par la surface de façon diffuse. Elle s'avère être proportionnelle au cosinus de l'angle entre ces deux vecteurs. Si la normale est de 0 degré (autrement dit, la lumière frappe pleinement la surface, à 90 degrés par rapport à la surface dans toutes les directions), alors nous pouvons dire qu'elle reflète toute la lumière. Si l'angle de la lumière à la normale est de 90 degrés, rien n'est réfléchi. Tout ce qui se trouve entre suit la courbe du cosinus. (Si l'angle est supérieur à 90 degrés, alors nous obtiendrions en théorie des quantités négatives de la lumière réfléchie. C'est évidemment absurde, nous utilisons la plus grande des valeurs entre le cosinus et zéro.)
Idéalement pour nous, calculer le cosinus de l'angle entre deux vecteurs est un calcul trivial, si les deux ont une longueur unité, on prend leur produit scalaire. Par commodité, les produits scalaires sont intégrés dans les shaders, en utilisant la fonction logiquement nommée dot.
Ouf ! C'était beaucoup de théorie pour commencer - mais maintenant nous savons que tout ce que nous devons faire pour obtenir un éclairage directionnel fonctionnel est :
- maintenir un ensemble de normales, un pour chaque vertex ;
- avoir un vecteur de direction de la lumière ;
- calculer le produit scalaire de la normale du vertex et du vecteur lumière et pondérer les couleurs de manière appropriée, en ajoutant également une composante pour l'éclairage ambiant.
Jetons un coup d'œil au fonctionnement dans le code. Nous allons commencer par la fin et remonter. Évidemment, le code HTML de cette leçon diffère de la précédente. Nous avons tous les champs de saisie supplémentaires, mais je ne vous ennuierai avec ces détails-là… passons à JavaScript, où notre première escale est la fonction initBuffers. Dans celle-ci, juste après le code créant le tampon contenant les positions des vertex mais avant le code effectuant la même chose pour les coordonnées de texture, vous verrez du code servant à mettre en place les normales. Cela devrait vous sembler familier maintenant :
cubeVertexNormalBuffer =
gl.createBuffer
(
);
gl.bindBuffer
(
gl.
ARRAY_BUFFER,
cubeVertexNormalBuffer);
var vertexNormals =
[
// Face avant
0
.
0
,
0
.
0
,
1
.
0
,
0
.
0
,
0
.
0
,
1
.
0
,
0
.
0
,
0
.
0
,
1
.
0
,
0
.
0
,
0
.
0
,
1
.
0
,
// Face arrière
0
.
0
,
0
.
0
,
-
1
.
0
,
0
.
0
,
0
.
0
,
-
1
.
0
,
0
.
0
,
0
.
0
,
-
1
.
0
,
0
.
0
,
0
.
0
,
-
1
.
0
,
// Face du dessus
0
.
0
,
1
.
0
,
0
.
0
,
0
.
0
,
1
.
0
,
0
.
0
,
0
.
0
,
1
.
0
,
0
.
0
,
0
.
0
,
1
.
0
,
0
.
0
,
// Face de dessous
0
.
0
,
-
1
.
0
,
0
.
0
,
0
.
0
,
-
1
.
0
,
0
.
0
,
0
.
0
,
-
1
.
0
,
0
.
0
,
0
.
0
,
-
1
.
0
,
0
.
0
,
// Face de droite
1
.
0
,
0
.
0
,
0
.
0
,
1
.
0
,
0
.
0
,
0
.
0
,
1
.
0
,
0
.
0
,
0
.
0
,
1
.
0
,
0
.
0
,
0
.
0
,
// Face de gauche
-
1
.
0
,
0
.
0
,
0
.
0
,
-
1
.
0
,
0
.
0
,
0
.
0
,
-
1
.
0
,
0
.
0
,
0
.
0
,
-
1
.
0
,
0
.
0
,
0
.
0
,
];
gl.bufferData
(
gl.
ARRAY_BUFFER,
new Float32Array
(
vertexNormals),
gl.
STATIC_DRAW);
cubeVertexNormalBuffer.
itemSize =
3
;
cubeVertexNormalBuffer.
numItems =
24
;
C'est assez simple. Le nouveau changement se situe un peu plus bas, dans la fonction drawscene, ce code sert à lier ce tampon au bon attribut du shader :
gl.bindBuffer
(
gl.
ARRAY_BUFFER,
cubeVertexNormalBuffer);
gl.vertexAttribPointer
(
shaderProgram.
vertexNormalAttribute,
cubeVertexNormalBuffer.
itemSize,
gl.
FLOAT,
false,
0
,
0
);
Après cela, toujours dans drawScene, nous avons supprimé le code de la sixième leçon qui servait à sélectionner les différentes textures, nous n'avons ici qu'une texture à utiliser :
gl.bindTexture
(
gl.
TEXTURE_2D,
crateTexture);
La partie suivante est un peu plus compliquée. Premièrement nous vérifions si la case « lumière » est cochée puis définissons une variable uniforme pour dire aux shaders qu'elle l'est ou non.
var lighting =
document
.getElementById
(
"lighting"
).
checked;
gl.uniform1i
(
shaderProgram.
useLightingUniform,
lighting);
Ensuite, si l'éclairage est activé, on extrait les valeurs de rouge, vert et bleu pour l'éclairage ambiant comme spécifié dans les champs de saisie au bas de la page et les envoyons aussi aux shaders :
if (
lighting) {
gl.uniform3f
(
shaderProgram.
ambientColorUniform,
parseFloat
(
document
.getElementById
(
"ambientR"
).
value),
parseFloat
(
document
.getElementById
(
"ambientG"
).
value),
parseFloat
(
document
.getElementById
(
"ambientB"
).
value)
);
Nous voulons ensuite envoyer la direction de la lumière :
var lightingDirection =
[
parseFloat
(
document
.getElementById
(
"lightDirectionX"
).
value),
parseFloat
(
document
.getElementById
(
"lightDirectionY"
).
value),
parseFloat
(
document
.getElementById
(
"lightDirectionZ"
).
value)
];
var adjustedLD =
vec3.create
(
);
vec3.normalize
(
lightingDirection,
adjustedLD);
vec3.scale
(
adjustedLD,
-
1
);
gl.uniform3fv
(
shaderProgram.
lightingDirectionUniform,
adjustedLD);
Vous pouvez voir que nous ajustons le vecteur direction avant de le passer au shader en utilisant le module vec3 qui, comme mat4 que nous avons utilisé pour nos matrices modèle-vue et projection, fait partie de glMatrix. Le premier ajustement, vec3.normalize, l'agrandit ou le réduit afin d'avoir une longueur de 1 ; vous vous souvenez que pour que le cosinus de l'angle entre deux vecteurs soit égal au produit scalaire, les deux doivent avoir une longueur unité. Les normales définies précédemment avaient toutes la bonne longueur, mais comme la direction de l'éclairage est donnée par l'utilisateur (et l'on ne voudrait pas lui imposer d'avoir à normaliser les vecteurs lui-même), nous la convertissons. Le deuxième ajustement consiste à multiplier le vecteur de la lumière par le scalaire -1 - c'est-à-dire inverser sa direction. Ceci, car nous avons spécifié le sens de l'éclairage en termes d'où la lumière va, tandis que les calculs précédents étaient en termes d'où la lumière provient. Une fois cela fait, nous le transmettons aux shaders en utilisant gl.uniform3fv, qui met un Float32Array comportant trois éléments (utilisé par les fonctions vec3) dans une variable uniforme.
La partie de code suivante est plus simple, on se contente de recopier les composantes couleurs de la lumière directionnelle vers la bonne variable uniforme du shader :
gl.uniform3f
(
shaderProgram.
directionalColorUniform,
parseFloat
(
document
.getElementById
(
"directionalR"
).
value),
parseFloat
(
document
.getElementById
(
"directionalG"
).
value),
parseFloat
(
document
.getElementById
(
"directionalB"
).
value)
);
}
C'est tout pour les changements dans drawScene. On avance dans le code de gestion des touches, il y a des changements simples supprimant la gestion de la touche F, que nous pouvons ignorer. Le prochain changement intéressant est dans la fonction setMatrixUniforms, qui comme vous vous souvenez effectue la copie des matrices modèle-vue et projection vers les variables uniformes du shader. Nous avons ajouté quatre lignes pour copier une nouvelle matrice, basée sur la matrice modèle-vue :
var normalMatrix =
mat3.create
(
);
mat4.toInverseMat3
(
mvMatrix,
normalMatrix);
mat3.transpose
(
normalMatrix);
gl.uniformMatrix3fv
(
shaderProgram.
nMatrixUniform,
false,
normalMatrix);
Comme vous pourriez l'espérer de quelque chose se nommant matrice normale, elle est utilisée pour la transformation des normales. :-) Nous ne pouvons pas les transformer de la même manière que les positions des vertex en utilisant la matrice modèle-vue habituelle, car les normales seraient converties par nos translations et rotations. Par exemple, si nous ignorons la rotation et supposons que nous effectuons une translation de (0, 0, -5), la normale (0, 0, 1) deviendrait (0, 0, -4), ce qui est non seulement trop long, mais aussi la mauvaise direction. Nous pourrions contourner cela ; vous avez sans doute remarqué que, dans les vertex shaders, lorsque nous multiplions les positions des vertex de trois éléments par la matrice modèle-vue de 4x4 éléments, nous étendons la position à quatre éléments en rajoutant un 1 à la fin afin de les rendre compatibles. Ce 1 ne sert pas juste à combler ce vide, mais aussi à prendre en compte la translation en plus de la rotation et autres transformations. Donc en ajoutant un 0 à la place, nous pourrions ignorer cette translation. Ceci fonctionnerait parfaitement bien pour nous maintenant, mais ne gèrerait malheureusement pas les cas où notre matrice modèle-vue inclurait différentes transformations, plus précisément le redimensionnement et le cisaillement. Si nous avions par exemple une matrice modèle-vue qui doublait la taille des objets que nous dessinions, leurs normales doubleraient de longueur également, même avec un zéro à la fin - ce qui causerait de sérieux problèmes de lumière. Ainsi, afin de ne pas tomber dans de mauvaises habitudes, nous le faisons correctement. :-)
La bonne façon d'obtenir des normales pointant dans la bonne direction est d'utiliser la transposée inverse de la matrice 3x3 en haut à gauche de la matrice modèle-vue. Vous trouverez plus d'explications ici, vous pourrez aussi trouver les commentaires de Coolcat utiles (faits sur une version antérieure de cette leçon). (Merci aussi à Shy pour ses autres conseils.)
Quoi qu'il en soit, une fois que nous avons calculé cette matrice et fait ce qu'il fallait, elle est mise dans les variables uniformes du shader, tout comme les autres matrices.
Nous progressons à travers le code à partir de là, il y a quelques changements triviaux apportés au code de chargement de texture pour lui faire juste charger une texture Mipmap au lieu des trois de la dernière fois, et de nouvelles lignes dans initShaders pour initialiser l'attribut vertexNormalAttribute dans le programme afin que drawScene puisse l'utiliser pour envoyer les normales aux shaders. Nous faisons de même pour toutes les variables uniformes nouvellement introduites. Rien de ceci ne nécessite plus de détails, nous passons donc directement aux shaders.
Le fragment shader est plus simple, regardons-le d'abord :
precision mediump float
;
varying
vec2
vTextureCoord;
varying
vec3
vLightWeighting;
uniform
sampler2D
uSampler;
void
main
(
void
) {
vec4
textureColor =
texture2D
(
uSampler, vec2
(
vTextureCoord.s, vTextureCoord.t));
gl_FragColor =
vec4
(
textureColor.rgb *
vLightWeighting, textureColor.a);
}
Comme vous pouvez le constater, nous extrayons la couleur de la texture, tout comme dans la sixième leçon. Mais avant de la retourner, nous ajustons les valeurs R, G et B avec une variable varying appelée vLightWeighting, un vecteur de trois éléments, qui (comme on peut s'y attendre) détient des facteurs d'ajustement pour le rouge, le vert et le bleu, calculés à partir de l'éclairage par le vertex shader.
Alors, comment ceci marche-t-il ? Regardons le code du vertex shader ; les nouvelles lignes sont en rouge :
attribute
vec3
aVertexPosition;
attribute
vec3
aVertexNormal; // NOUVEAU
attribute
vec2
aTextureCoord;
uniform
mat4
uMVMatrix;
uniform
mat4
uPMatrix;
uniform
mat3
uNMatrix; // NOUVEAU
uniform
vec3
uAmbientColor; // NOUVEAU
uniform
vec3
uLightingDirection; // NOUVEAU
uniform
vec3
uDirectionalColor; // NOUVEAU
uniform
bool
uUseLighting; // NOUVEAU
varying
vec2
vTextureCoord;
varying
vec3
vLightWeighting; // NOUVEAU
void
main
(
void
) {
gl_Position
=
uPMatrix *
uMVMatrix *
vec4
(
aVertexPosition, 1
.0
);
vTextureCoord =
aTextureCoord;
if
(!
uUseLighting) {
// NOUVEAU
vLightWeighting =
vec3
(
1
.0
, 1
.0
, 1
.0
);
}
else
{
// NOUVEAU
vec3
transformedNormal =
uNMatrix *
aVertexNormal;
float
directionalLightWeighting =
max
(
dot
(
transformedNormal, uLightingDirection), 0
.0
);
vLightWeighting =
uAmbientColor +
uDirectionalColor *
directionalLightWeighting;
}
}
Le nouvel attribut, aVertexNormal, détient bien sûr les normales des vertex que nous définissons dans initBuffers passons au shader dans la fonction drawScene. uNMatrix est notre matrice normale, uUseLighting la variable uniforme indiquant si l'éclairage est allumé ; uAmbientColor, uDirectionalColor et uLightingDirection sont les valeurs rentrées par l'utilisateur dans les champs de saisie de la page Web.
À la lumière des mathématiques étudiées ci-dessus, la structure du code devrait être assez facile à comprendre. La principale sortie du shader est la variable varying vLightWeighting, que nous venons de voir et qui est utilisée pour régler la couleur de l'image dans le fragment shader. Si la lumière est éteinte, nous utilisons la valeur par défaut (1, 1, 1), ce qui signifie que les couleurs ne doivent pas être modifiées. Si la lumière est allumée, nous obtenons l'orientation de la normale en appliquant la matrice normale, puis prenons le produit scalaire de la normale et de la direction de la lumière pour obtenir la quantité de lumière réfléchie (avec un minimum de zéro, comme je l'ai mentionné précédemment). On peut alors travailler sur notre pondération finale de la lumière pour le fragment shader en multipliant les composantes de couleurs de la lumière directionnelle par cette pondération, puis en ajoutant la couleur de l'éclairage ambiant. Le résultat est exactement ce dont le fragment shader a besoin, nous avons donc fini !
Maintenant vous avez tout appris de cette leçon : vous avez une base solide pour comprendre le fonctionnement de l'éclairage dans les systèmes graphiques comme WebGL et devriez connaître en détail la mise en œuvre des deux formes les plus simples de l'éclairage, ambiante et directionnelle.
Si vous avez des questions, des commentaires ou des corrections, s'il vous plaît n'hésitez pas à laisser un commentaire ci-dessous !
La prochaine fois, nous verrons le mélange de couleurs que nous allons utiliser pour fabriquer des objets qui sont partiellement transparents.
IV. Remerciements▲
La page Wikipédia sur le shading Phong m'a grandement aidé à écrire cet article, en particulier en essayant de donner du sens aux mathématiques. La différence entre la matrice nécessaire à l'ajustement des positions des vertex et celle des normales a été beaucoup plus claire par la lecture du tutoriel Lighthouse 3D, surtout lorsque Coolcat a éclairé les choses par ses commentaires. La boîte tournante de Chris Marrin (avec une nouvelle version par Jacob Seidelin) était aussi un guide utile, de même que la boîte tournante de Peter Nitsch. Comme toujours, je dois beaucoup à Nehe et son tutoriel OpenGL pour le script pour cette leçon.
Merci à LittleWhite pour sa relecture attentive et ClaudeLELOUP pour sa relecture orthographique.
Navigation▲
Tutoriel précédent : clavier et filtres detexture | Sommaire | Tutoriel suivant : le tampon de profondeur,transparence et mélange |