Navigation▲
Tutoriel précédent : sphères, matrices de rotation et évènements souris | Sommaire | Tutoriel suivant : éclairage par pixel et programmes multiples |
II. Introduction▲
Bienvenue dans mon douzième tutoriel de la série sur WebGL, le second qui n'est pas basé sur les tutoriels OpenGL de NeHe. Dans celui-ci, nous allons découvrir les lumières ponctuelles qui sont assez simples mais importantes et qui nous mèneront à des choses plus intéressantes. L'éclairage ponctuel est, comme vous pouvez vous y attendre, une lumière qui provient d'un point précis de la scène - contrairement à la lumière directionnelle que nous utilisions jusqu'à maintenant qui provient d'un point quelconque en dehors de la scène.
Voici ce que le résultat sera lorsque vous exécuterez cette leçon dans un navigateur qui supporte WebGL :
Cliquez pour lire la vidéo
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 en orbite. Les deux objets seront probablement blancs pendant un moment, le temps que les textures se chargent, mais une fois que cela est fait vous devriez voir que la sphère est la lune et le cube (pas à l'échelle), une boîte en bois. Les deux sont éclairées par une source de lumière ponctuelle placée entre elles. Si vous voulez changer la position de la lumière, sa couleur, etc., vous pouvez utiliser les contrôles sous le canvas WebGL.
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 onziè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 « 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 ce que nous essayons de faire avec la lumière ponctuelle. La différence avec une lumière directionnelle est qu'elle provient d'un point dans la scène. En l'espace d'un instant tout devrait devenir clair. Cela signifie que l'angle par lequel la lumière arrive est différent pour chaque point dans la scène. Donc, la méthode logique pour modéliser ce comportement est de calculer la direction de la lumière pour chaque vertex et d'appliquer exactement les mêmes calculs que ceux que nous avions faits pour la lumière directionnelle. Et c'est ce que nous faisons !
(Arrivé à ce point, vous pouvez penser qu'il serait préférable de ne pas calculer la direction de la lumière pour chaque vertex, mais pour tous les points entre les vertex, soit pour les fragments. Et vous avez totalement raison de penser cela, calculer l'éclairage de cette manière est plus dur pour la carte graphique mais tellement plus joli. Et c'est dans cette direction que nous allons dans la prochaine leçon.)
Maintenant que nous avons décidé quoi faire, cela vaut le coup de revoir la page de démonstration de cette leçon et de noter un nouveau détail : il n'y a pas d'objet dans la scène à la position de la lumière. Si vous souhaitez avoir un objet pour envoyer la lumière (disons, le soleil au centre du système solaire) alors vous devez définir la source de lumière et l'objet séparément. Ajouter l'objet devrait être très simple en vous basant sur les leçons précédentes. Dans ce tutoriel je n'expliquerai donc que le fonctionnement des lumières ponctuelles. Comme vous pouvez l'espérer de la description ci-dessus, c'est très simple en fait. La majorité des différences entre cette page et la onzième leçon consiste en l'affichage du cube et faire que les objets tournent…
Comme d'habitude, nous allons commencer par le bas de la page source HTML et remonter progressivement en analysant les différences entre ce fichier et celui de la leçon précédente. Le premier ensemble de changements se situe dans le corps « body » du code HTML, où les contrôles dans lesquels vous pouvez insérer la direction de la lumière ont été modifiés pour recevoir sa position. C'est assez simple et il n'y a aucun intérêt de le montrer ici, donc remontons jusqu'à la fonction webGLStart. Une fois encore, les changements sont simples. Cette leçon n'effectue aucune interaction avec la souris, donc aucun code de gestion de la souris et la fonction anciennement nommée initTexture s'appelle maintenant initTextures car nous en chargeons deux. Pas vraiment intéressant…
En remontant un peu, vous remarquerez que la fonction tick a gagné un nouvel appel de la fonction animate, afin que notre scène se mette à jour :
function tick
(
) {
requestAnimFrame
(
tick);
drawScene
(
);
animate
(
);
// NOUVEAU
}
Au-dessus vous trouverez la fonction animate, qui met simplement à jour deux variables globales décrivant la distance orbitale de la lune et du cube afin qu'ils parcourent 50 ° par seconde :
var lastTime =
0
;
function animate
(
) {
var timeNow =
new Date(
).getTime
(
);
if (
lastTime !=
0
) {
var elapsed =
timeNow -
lastTime;
moonAngle +=
0
.
05
*
elapsed;
cubeAngle +=
0
.
05
*
elapsed;
}
lastTime =
timeNow;
}
La prochaine fonction est drawScene, qui a subi quelques changements intéressants. Les premières lignes de code effacent le canvas et définissent notre matrice de perspective. Ensuite, s'enchaîne le code de la onzième leçon vérifiant si la case de l'éclairage est cochée, et l'envoi de la couleur ambiante à la carte graphique :
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 lighting =
document
.getElementById
(
"lighting"
).
checked;
gl.uniform1i
(
shaderProgram.
useLightingUniform,
lighting);
if (
lighting) {
gl.uniform3f
(
shaderProgram.
ambientColorUniform,
parseFloat
(
document
.getElementById
(
"ambientR"
).
value),
parseFloat
(
document
.getElementById
(
"ambientG"
).
value),
parseFloat
(
document
.getElementById
(
"ambientB"
).
value)
);
Ensuite, nous envoyons la position de notre lumière ponctuelle à la carte graphique à l'aide d'une variable uniforme. C'est l'équivalent du code qui envoyait la direction de la lumière dans la onzième leçon. La différence consiste en la suppression d'un élément et non en un rajout. Lorsque nous envoyions la direction de la lumière à la carte graphique, nous devions la transformer en un vecteur unitaire (c'est-à-dire la redimensionner pour avoir une longueur d'une unité) et inverse à sa direction. Il n'y a pas besoin de ce genre de chose cette fois : nous envoyons simplement les coordonnées de la lumière :
gl.uniform3f
(
shaderProgram.
pointLightingLocationUniform,
parseFloat
(
document
.getElementById
(
"lightPositionX"
).
value),
parseFloat
(
document
.getElementById
(
"lightPositionY"
).
value),
parseFloat
(
document
.getElementById
(
"lightPositionZ"
).
value)
);
Ensuite, nous faisons la même chose pour la couleur de la lumière ponctuelle et c'est fini pour le code de l'éclairage dans la fonction drawScene.
gl.uniform3f
(
shaderProgram.
pointLightingColorUniform,
parseFloat
(
document
.getElementById
(
"pointR"
).
value),
parseFloat
(
document
.getElementById
(
"pointG"
).
value),
parseFloat
(
document
.getElementById
(
"pointB"
).
value)
);
}
Ensuite, nous dessinons la sphère et le cube :
mat4.identity
(
mvMatrix);
mat4.translate
(
mvMatrix,
[
0
,
0
,
-
20
]
);
mvPushMatrix
(
);
mat4.rotate
(
mvMatrix,
degToRad
(
moonAngle),
[
0
,
1
,
0
]
);
mat4.translate
(
mvMatrix,
[
5
,
0
,
0
]
);
gl.activeTexture
(
gl.
TEXTURE0);
gl.bindTexture
(
gl.
TEXTURE_2D,
moonTexture);
gl.uniform1i
(
shaderProgram.
samplerUniform,
0
);
gl.bindBuffer
(
gl.
ARRAY_BUFFER,
moonVertexPositionBuffer);
gl.vertexAttribPointer
(
shaderProgram.
vertexPositionAttribute,
moonVertexPositionBuffer.
itemSize,
gl.
FLOAT,
false,
0
,
0
);
gl.bindBuffer
(
gl.
ARRAY_BUFFER,
moonVertexTextureCoordBuffer);
gl.vertexAttribPointer
(
shaderProgram.
textureCoordAttribute,
moonVertexTextureCoordBuffer.
itemSize,
gl.
FLOAT,
false,
0
,
0
);
gl.bindBuffer
(
gl.
ARRAY_BUFFER,
moonVertexNormalBuffer);
gl.vertexAttribPointer
(
shaderProgram.
vertexNormalAttribute,
moonVertexNormalBuffer.
itemSize,
gl.
FLOAT,
false,
0
,
0
);
gl.bindBuffer
(
gl.
ELEMENT_ARRAY_BUFFER,
moonVertexIndexBuffer);
setMatrixUniforms
(
);
gl.drawElements
(
gl.
TRIANGLES,
moonVertexIndexBuffer.
numItems,
gl.
UNSIGNED_SHORT,
0
);
mvPopMatrix
(
);
mvPushMatrix
(
);
mat4.rotate
(
mvMatrix,
degToRad
(
cubeAngle),
[
0
,
1
,
0
]
);
mat4.translate
(
mvMatrix,
[
5
,
0
,
0
]
);
gl.bindBuffer
(
gl.
ARRAY_BUFFER,
cubeVertexPositionBuffer);
gl.vertexAttribPointer
(
shaderProgram.
vertexPositionAttribute,
cubeVertexPositionBuffer.
itemSize,
gl.
FLOAT,
false,
0
,
0
);
gl.bindBuffer
(
gl.
ARRAY_BUFFER,
cubeVertexNormalBuffer);
gl.vertexAttribPointer
(
shaderProgram.
vertexNormalAttribute,
cubeVertexNormalBuffer.
itemSize,
gl.
FLOAT,
false,
0
,
0
);
gl.bindBuffer
(
gl.
ARRAY_BUFFER,
cubeVertexTextureCoordBuffer);
gl.vertexAttribPointer
(
shaderProgram.
textureCoordAttribute,
cubeVertexTextureCoordBuffer.
itemSize,
gl.
FLOAT,
false,
0
,
0
);
gl.activeTexture
(
gl.
TEXTURE0);
gl.bindTexture
(
gl.
TEXTURE_2D,
crateTexture);
gl.uniform1i
(
shaderProgram.
samplerUniform,
0
);
gl.bindBuffer
(
gl.
ELEMENT_ARRAY_BUFFER,
cubeVertexIndexBuffer);
setMatrixUniforms
(
);
gl.drawElements
(
gl.
TRIANGLES,
cubeVertexIndexBuffer.
numItems,
gl.
UNSIGNED_SHORT,
0
);
mvPopMatrix
(
);
}
C'est bon pour drawScene. En remontant dans le code, vous allez voir que la fonction initBuffers s'est vue ajoutée à notre code standard pour générer les tampons pour le cube et la sphère, et encore plus haut vous constaterez que initTextures charge maintenant deux textures au lieu d'une.
La modification suivante, la dernière, est la plus importante. Si vous défilez jusqu'en haut là où se situe le code du vertex shader, vous allez voir quelques modifications et ce sont celles-ci qui font toute la différence. Analysons-les à partir du début, les changements sont en rouge :
attribute
vec3
aVertexPosition;
attribute
vec3
aVertexNormal;
attribute
vec2
aTextureCoord;
uniform
mat4
uMVMatrix;
uniform
mat4
uPMatrix;
uniform
mat3
uNMatrix;
uniform
vec3
uAmbientColor;
uniform
vec3
uPointLightingLocation; // NOUVEAU
uniform
vec3
uPointLightingColor; // NOUVEAU
Donc, nous avons les variables uniformes pour la position et la couleur de la lumière, remplaçant les anciennes direction et couleur. Ensuite :
uniform
bool
uUseLighting;
varying
vec2
vTextureCoord;
varying
vec3
vLightWeighting;
void
main
(
void
) {
vec4
mvPosition =
uMVMatrix *
vec4
(
aVertexPosition, 1
.0
); // NOUVEAU
gl_Position
=
uPMatrix *
mvPosition; // NOUVEAU
Nous avons séparé notre vieux code en deux. Dans tous nos vertex shaders jusqu'à présent, nous avions appliqué la matrice modèle-vue et la matrice de projection à la position du vertex en une fois, comme ceci :
// Code de la leçon 11
gl_Position
=
uPMatrix *
uMVMatrix *
vec4
(
aVertexPosition, 1
.0
);
Maintenant, nous gardons la valeur intermédiaire, la position du vertex avec la matrice de modèle-vue appliquée, mais avant, la position a été corrigée pour permettre la perspective. Elle est utilisée dans le prochain morceau :
vTextureCoord =
aTextureCoord;
if
(!
uUseLighting) {
vLightWeighting =
vec3
(
1
.0
, 1
.0
, 1
.0
);
}
else
{
vec3
lightDirection =
normalize
(
uPointLightingLocation -
mvPosition.xyz); // NOUVEAU
La position de la lumière est dans l'espace de coordonnées monde, ainsi que la position du vertex, une fois multipliée avec la matrice de modèle-vue. Nous devons trouver la direction de la lumière par rapport à notre vertex actuel en termes de coordonnées et pour ce faire, nous devons simplement les soustraire. Une fois cela fait, nous devons normaliser le vecteur direction afin qu'il possède, tout comme notre ancien vecteur direction, une longueur unité. Ensuite, toutes les pièces sont en place pour effectuer le même calcul que nous avions pour l'éclairage directionnel, seuls quelques noms de variables ont changé :
vec3
transformedNormal =
uNMatrix *
aVertexNormal;
float
directionalLightWeighting =
max
(
dot
(
transformedNormal, lightDirection), 0
.0
); // MODIFIE
vLightWeighting =
uAmbientColor +
uPointLightingColor *
directionalLightWeighting; // MODIFIE
Et c'est tout ! Vous savez maintenant écrire des shaders pour effectuer l'éclairage ponctuel.
C'est tout pour le moment. La prochaine fois nous étudierons encore l'éclairage, pour améliorer le réalisme de notre scène en effectuant un éclairage au niveau du pixel et non plus du vertex.
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 Voïvode pour sa relecture orthographique.
Navigation▲
Tutoriel précédent : sphères, matrices de rotation et évènements souris | Sommaire | Tutoriel suivant : éclairage par pixel et programmes multiples |