Introduction
Lisez d'abord l'introduction à Three.js, car elle constitue la base de ce document.
Je vais maintenant aborder les nuanceurs. WebGL est une technologie brillante, et comme je l'ai déjà dit, Three.js (et d'autres bibliothèques) font un travail fantastique pour vous simplifier la tâche. Cependant, il arrivera un moment où vous voudrez obtenir un effet spécifique ou approfondir le fonctionnement de ces éléments étonnants qui s'affichent à l'écran. Les nuanceurs feront alors presque certainement partie de l'équation. Vous voudrez peut-être passer des bases du dernier tutoriel à quelque chose d'un peu plus complexe.
Je vais partir du principe que vous utilisez Three.js, car elle nous facilite grandement la tâche en termes de démarrage du nuanceur. Vous découvrirez d'abord le contexte des nuanceurs, puis des étapes avancées plus loin dans ce document.
1. Nos deux nuanceurs
WebGL n'offre pas l'utilisation du pipeline fixe, ce qui signifie qu'il ne vous donne aucun moyen de rendre vos éléments prêts à l'emploi. Il propose le pipeline programmable, qui est plus puissant, mais aussi plus difficile à comprendre et à utiliser. En bref, le pipeline programmable signifie qu'en tant que programmeur, vous êtes responsable de l'affichage des sommets, etc., à l'écran. Les nuanceurs font partie de ce pipeline, et il en existe deux types :
- Nuanceurs de sommets
- Nuanceurs de fragments
Je suis sûr que vous serez d'accord avec moi pour dire que ces deux éléments ne veulent absolument rien dire en eux-mêmes. Ce que vous devez savoir, c'est qu'ils s'exécutent entièrement sur le GPU de votre carte graphique. Cela signifie que nous voulons leur décharger tout ce que nous pouvons, en laissant notre processeur faire d'autres tâches. Un GPU moderne est fortement optimisé pour les fonctions requises par les nuanceurs. Il est donc intéressant de pouvoir l'utiliser.
2. Nuanceurs de sommets
Prenons une forme primitive standard, comme une sphère. Elle est composée de sommets, n'est-ce pas ? Un nuanceur de sommets reçoit chacun de ces sommets à son tour et peut les modifier. C'est au nuanceur de sommets de décider ce qu'il fait avec chacun d'eux, mais il a une responsabilité : il doit à un moment donné définir un élément appelé gl_Position, un vecteur flottant 4D, qui correspond à la position finale du sommet à l'écran. En soi, il s'agit d'un processus assez intéressant, car nous parlons en fait d'obtenir une position 3D (un sommet avec x, y, z) sur un écran 2D ou projetée sur celui-ci. Heureusement pour nous, si nous utilisons quelque chose comme Three.js, nous disposons d'un moyen abrégé de définir la gl_Position sans que les choses ne deviennent trop lourdes.
3. Nuanceurs de fragments
Nous avons donc notre objet avec ses sommets, et nous les avons projetés sur l'écran 2D, mais qu'en est-il des couleurs que nous utilisons ? Qu'en est-il de la texture et de l'éclairage ? C'est exactement à cela que sert le nuanceur de fragments. Tout comme le nuanceur de sommets, le nuanceur de fragments n'a qu'une seule tâche obligatoire : il doit définir ou ignorer la variable gl_FragColor, un autre vecteur flottant 4D, qui correspond à la couleur finale de notre fragment.
Mais qu'est-ce qu'un fragment ? Pensez à trois sommets qui forment un triangle. Chaque pixel de ce triangle doit être dessiné. Un fragment correspond aux données fournies par ces trois sommets dans le but de dessiner chaque pixel de ce triangle. Pour cette raison, les fragments reçoivent des valeurs interpolées de leurs sommets constitutifs. Si un sommet est coloré en rouge et que son voisin est bleu, nous verrons les valeurs de couleur s'interpoler du rouge au bleu en passant par le violet.
4. Variables de nuanceur
Vous pouvez effectuer trois déclarations de variables : Uniforms, Attributes et Varyings. Ces éléments peuvent être déroutants, car ils ne correspondent à aucune autre langue courante. Voici comment vous pouvez les considérer :
Les uniforms sont envoyés aux nuanceurs de sommets et aux nuanceurs de fragments, et contiennent des valeurs qui restent les mêmes sur l'ensemble de la frame en cours de rendu. Un bon exemple serait la position d'une lumière.
Les attributs sont des valeurs appliquées à des sommets individuels. Les attributs ne sont disponibles que pour le nuanceur de sommets. Il peut s'agir, par exemple, d'une couleur distincte pour chaque sommet. Les attributs ont une relation de un à un avec les sommets.
Les varyings sont des variables déclarées dans le nuanceur de sommets que nous voulons partager avec le nuanceur de fragments. Pour ce faire, nous nous assurons de déclarer une variable varying du même type et du même nom dans le nuanceur de sommets et dans le nuanceur de fragments. Un exemple classique serait la normale d'un sommet, car elle peut être utilisée dans les calculs d'éclairage.
Plus loin, nous utiliserons les trois types pour que vous puissiez vous faire une idée de leur application réelle.
Non, regardez l'un des nuanceurs les plus simples que vous pouvez créer.
5. Bonjourno World
Voici donc le Hello World des nuanceurs de sommets :
/**
* Multiply each vertex by the model-view matrix
* and the projection matrix (both provided by
* Three.js) to get a final vertex position
*/
void main() {
gl_Position = projectionMatrix *
modelViewMatrix *
vec4(position,1.0);
}
et voici la même chose pour le nuanceur de fragments :
/**
* Set the color to a lovely pink.
* Note that the color is a 4D Float
* Vector, R,G,B and A and each part
* runs from 0.0 to 1.0
*/
void main() {
gl_FragColor = vec4(1.0, 0.0, 1.0, 1.0);
}
Ce n'est pas trop compliqué, n'est-ce pas ?
Dans le nuanceur de sommets, Three.js nous envoie quelques uniforms. Ces deux uniforms sont des matrices 4D, appelées matrice de modèle-vue et matrice de projection. Vous n'avez pas besoin de savoir exactement comment elles fonctionnent, même s'il est toujours préférable de comprendre comment les choses font ce qu'elles font si vous le pouvez. En bref, elles permettent de projeter la position 3D du sommet sur la position 2D finale à l'écran.
Je les ai en fait omises dans l'extrait d'exemple, car Three.js les ajoute en haut de votre code de nuanceur. Vous n'avez donc pas à vous en soucier. En réalité, il ajoute beaucoup plus d'éléments, tels que les données d'éclairage, les couleurs des sommets et les normales des sommets. Si vous le faisiez sans Three.js, vous devriez créer et définir vous-même tous ces uniforms et attributs. C'est la vérité.
6. Utiliser un MeshShaderMaterial
Nous avons donc configuré un nuanceur, mais comment l'utiliser avec Three.js ?
/**
* Assume we have jQuery to hand and pull out
* from the DOM the two snippets of text for
* each of our shaders
*/
var shaderMaterial = new THREE.MeshShaderMaterial({
vertexShader: $('vertexshader').text(),
fragmentShader: $('fragmentshader').text()
});
Three.js compilera et exécutera ensuite vos nuanceurs associés au maillage auquel vous attribuez ce matériau. Il n'y a pas plus simple. Enfin, probablement, mais nous parlons de 3D qui s'exécute dans votre navigateur. Je suppose donc que vous vous attendez à un certain niveau de complexité.
Nous pouvons en fait ajouter deux autres propriétés à notre MeshShaderMaterial : les uniforms et les attributs. Ils peuvent tous deux prendre des vecteurs, des entiers ou des flottants, mais comme je l'ai mentionné précédemment, les uniforms sont les mêmes pour l'ensemble de la frame ou pour tous les sommets. Ils ont donc tendance à être des valeurs uniques. Les attributs, en revanche, sont des variables par sommet. Ils doivent donc être un tableau. Il doit exister une relation de un à un entre le nombre de valeurs dans le tableau d'attributs et le nombre de sommets dans le maillage.
7. Étapes suivantes
Nous allons maintenant passer un peu de temps à ajouter une boucle d'animation, des attributs de sommets et un uniform. Nous allons également ajouter une variable varying afin que le nuanceur de sommets puisse envoyer des données au nuanceur de fragments. Le résultat final est que notre sphère rose apparaîtra éclairée par le haut et sur le côté, et qu'elle sera pulsée. C'est un peu étrange, mais j'espère que cela vous permettra de bien comprendre les trois types de variables, ainsi que leur relation entre eux et la géométrie sous-jacente.
8. Fausse lumière
Modifiez maintenant la couleur pour qu'il ne s'agisse pas d'un objet de couleur unie. Nous pourrions examiner comment Three.js gère l'éclairage, mais comme vous pouvez l'imaginer, c'est plus complexe que nécessaire. Nous allons donc le simuler. Vous devriez absolument consulter les fantastiques nuanceurs qui font partie de Three.js, ainsi que ceux de l' incroyable projet WebGL de Chris Milk et Google, Rome. Revenons à nos nuanceurs. Nous allons mettre à jour notre nuanceur de sommets pour fournir à chaque sommet une normale au nuanceur de fragments. Pour ce faire, nous utilisons un varying :
// create a shared variable for the
// VS and FS containing the normal
varying vec3 vNormal;
void main() {
// set the vNormal value with
// the attribute value passed
// in by Three.js
vNormal = normal;
gl_Position = projectionMatrix *
modelViewMatrix *
vec4(position,1.0);
}
Dans le nuanceur de fragments, nous allons configurer le même nom de variable, puis utiliser le produit scalaire de la normale du sommet avec un vecteur qui représente une lumière qui brille par le haut et à droite de la sphère. Le résultat net nous donne un effet semblable à une lumière directionnelle dans un package 3D.
// same name and type as VS
varying vec3 vNormal;
void main() {
// calc the dot product and clamp
// 0 -> 1 rather than -1 -> 1
vec3 light = vec3(0.5,0.2,1.0);
// ensure it's normalized
light = normalize(light);
// calculate the dot product of
// the light to the vertex normal
float dProd = max(0.0, dot(vNormal, light));
// feed into our frag color
gl_FragColor = vec4(dProd, dProd, dProd, 1.0);
}
La raison pour laquelle le produit scalaire fonctionne est que, étant donné deux vecteurs, il produit un nombre qui vous indique à quel point les deux vecteurs sont "similaires". Avec des vecteurs normalisés, s'ils pointent exactement dans la même direction, vous obtenez une valeur de 1. S'ils pointent dans des directions opposées, vous obtenez -1. Nous prenons ce nombre et l'appliquons à notre éclairage. Ainsi, un sommet en haut à droite aura une valeur proche ou égale à 1 (entièrement éclairé), tandis qu'un sommet sur le côté aura une valeur proche de 0 et qu'un sommet à l'arrière aura une valeur de -1. Nous limitons la valeur à 0 pour tout ce qui est négatif, mais lorsque vous branchez les nombres, vous obtenez l'éclairage de base que nous voyons.
Étapes suivantes Il serait peut-être intéressant d'essayer de modifier certaines positions de sommets.
9. Attributs
Ce que j'aimerais que nous fassions maintenant, c'est associer un nombre aléatoire à chaque sommet à l'aide d'un attribut. Nous utiliserons ce nombre pour pousser le sommet le long de sa normale. Le résultat net sera une sorte de boule à pointes étrange qui changera chaque fois que vous actualiserez la page. Elle ne sera pas encore animée (ce sera la prochaine étape), mais quelques actualisations de la page vous montreront qu'elle est aléatoire.
Commencez par ajouter l'attribut au nuanceur de sommets :
attribute float displacement;
varying vec3 vNormal;
void main() {
vNormal = normal;
// push the displacement into the three
// slots of a 3D vector so it can be
// used in operations with other 3D
// vectors like positions and normals
vec3 newPosition = position +
normal *
vec3(displacement);
gl_Position = projectionMatrix *
modelViewMatrix *
vec4(newPosition,1.0);
}
Quel est le résultat ?
Pas grand-chose de différent, en fait. En effet, l'attribut n'a pas été configuré dans le MeshShaderMaterial. Le nuanceur utilise donc une valeur nulle à la place. Pour l'instant, il s'agit d'un espace réservé. Dans un instant, nous ajouterons l'attribut au MeshShaderMaterial dans le code JavaScript, et Three.js les associera automatiquement.
Il convient également de noter que j'ai dû attribuer la position mise à jour à une nouvelle variable vec3, car l'attribut d'origine, comme tous les attributs, est en lecture seule.
10. Mettre à jour le MeshShaderMaterial
Passons directement à la mise à jour de notre MeshShaderMaterial avec l'attribut nécessaire pour alimenter notre déplacement. Pour rappel, les attributs sont des valeurs par sommet. Nous avons donc besoin d'une valeur par sommet dans notre sphère. Exemple :
var attributes = {
displacement: {
type: 'f', // a float
value: [] // an empty array
}
};
// create the material and now
// include the attributes property
var shaderMaterial = new THREE.MeshShaderMaterial({
attributes: attributes,
vertexShader: $('#vertexshader').text(),
fragmentShader: $('#fragmentshader').text()
});
// now populate the array of attributes
var vertices = sphere.geometry.vertices;
var values = attributes.displacement.value
for(var v = 0; v < vertices.length; v++) {
values.push(Math.random() * 30);
}
Nous voyons maintenant une sphère déformée, mais ce qui est intéressant, c'est que tout le déplacement se produit sur le GPU.
11. Animer
Nous devrions absolument animer cet élément. Comment faire ? Nous devons mettre en place deux éléments :
- Un uniform pour animer le déplacement à appliquer dans chaque frame. Nous pouvons utiliser le sinus ou le cosinus, car ils vont de -1 à 1.
- Une boucle d'animation dans le code JS
Nous allons ajouter l'uniform au MeshShaderMaterial et au nuanceur de sommets. Commençons par le nuanceur de sommets :
uniform float amplitude;
attribute float displacement;
varying vec3 vNormal;
void main() {
vNormal = normal;
// multiply our displacement by the
// amplitude. The amp will get animated
// so we'll have animated displacement
vec3 newPosition = position +
normal *
vec3(displacement *
amplitude);
gl_Position = projectionMatrix *
modelViewMatrix *
vec4(newPosition,1.0);
}
Ensuite, mettons à jour le MeshShaderMaterial :
// add a uniform for the amplitude
var uniforms = {
amplitude: {
type: 'f', // a float
value: 0
}
};
// create the final material
var shaderMaterial = new THREE.MeshShaderMaterial({
uniforms: uniforms,
attributes: attributes,
vertexShader: $('#vertexshader').text(),
fragmentShader: $('#fragmentshader').text()
});
Nos nuanceurs sont terminés pour le moment. Mais pour l'instant, nous semblons avoir fait un pas en arrière. Cela est dû en grande partie au fait que notre valeur d'amplitude est à 0 et que, comme nous la multiplions par le déplacement, nous ne voyons rien changer. Nous n'avons pas non plus configuré la boucle d'animation. Nous ne voyons donc jamais ce 0 changer.
Dans notre code JavaScript, nous devons maintenant encapsuler l'appel de rendu dans une fonction, puis utiliser requestAnimationFrame pour l'appeler. Nous devons également y mettre à jour la valeur de l'uniform.
var frame = 0;
function update() {
// update the amplitude based on
// the frame value
uniforms.amplitude.value = Math.sin(frame);
frame += 0.1;
renderer.render(scene, camera);
// set up the next call
requestAnimFrame(update);
}
requestAnimFrame(update);
Conclusion
Voilà, c'est terminé ! Vous pouvez maintenant voir qu'il s'anime d'une manière étrange (et un peu étrange) et pulsée.
Nous pourrions aborder bien d'autres sujets concernant les nuanceurs, et j'espère que cette introduction vous a été utile. Vous devriez maintenant être en mesure de comprendre les nuanceurs lorsque vous les voyez et d'avoir confiance en vous pour créer vos propres nuanceurs étonnants.