Présentation des nuanceurs

Introduction

Je vous ai déjà présenté Three.js. Si vous ne l'avez pas lu, vous devriez le faire, car il s'agit de la base sur laquelle je vais m'appuyer dans cet article.

Je vais maintenant vous parler des nuanceurs. WebGL est génial, et comme je l'ai déjà dit, Three.js (et d'autres bibliothèques) fait un travail fantastique pour vous abstraire des difficultés. Toutefois, il est possible que vous souhaitiez obtenir un effet spécifique ou approfondir la façon dont ces éléments étonnants sont apparus à l'écran. Les nuanceurs feront presque certainement partie de cette équation. Si vous êtes comme moi, vous voudrez peut-être passer des éléments de base du dernier tutoriel à quelque chose d'un peu plus complexe. Je vais partir du principe que vous utilisez Three.js, car il effectue une grande partie du travail pour nous en termes de démarrage du nuanceur. Je vais également vous dire dès le départ que je vais d'abord expliquer le contexte des nuanceurs, et que la dernière partie de ce tutoriel est celle où nous allons aborder des sujets un peu plus avancés. En effet, les nuanceurs sont inhabituels à première vue et nécessitent un peu d'explication.

1. Nos deux nuanceurs

WebGL n'offre pas l'utilisation du pipeline fixe, ce qui signifie qu'il ne vous donne aucun moyen de générer vos éléments prêts à l'emploi. Toutefois, il propose le pipeline programmable, qui est plus puissant, mais aussi plus difficile à comprendre et à utiliser. En résumé, le pipeline programmable signifie que, en tant que programmeur, vous êtes responsable de l'affichage des sommets et autres éléments à l'écran. Les nuanceurs font partie de ce pipeline et il en existe deux types:

  1. Vertex shaders
  2. Nuancheurs de fragments

Je suis sûr que vous conviendrez que ces deux éléments ne signifient absolument rien en eux-mêmes. Sachez 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, laissant notre processeur effectuer d'autres tâches. Un GPU moderne est fortement optimisé pour les fonctions requises par les nuanceurs. Il est donc très utile de pouvoir l'utiliser.

2. Shaders de sommet

Prenez une forme primitive standard, comme une sphère. Il est constitué de sommets, non ? Un nuanceur de sommet reçoit chacun de ces sommets à tour de rôle et peut les modifier. C'est au nuanceur de sommets de déterminer ce qu'il fait réellement avec chacun d'eux, mais il a une responsabilité: il doit à un moment donné définir un élément appelé gl_Position, un vecteur float 4D, qui est 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 aurons un moyen abrégé de définir gl_Position sans que les choses ne deviennent trop lourdes.

3. Shaders de fragment

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 texturation et de l'éclairage ? C'est exactement ce à quoi sert le nuanceur de fragments. Tout comme le nuanceur de sommets, le nuanceur de fragment n'a qu'une seule tâche obligatoire: il doit définir ou supprimer la variable gl_FragColor, un autre vecteur float 4D, qui est 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. Par conséquent, les fragments reçoivent des valeurs interpolées de leurs sommets constituants. Si un sommet est de couleur rouge et que son voisin est bleu, les valeurs de couleur s'interpolent du rouge au bleu en passant par le violet.

4. Variables de nuanceur

En ce qui concerne les variables, vous pouvez effectuer trois déclarations: uniformes, attributs et variables. Lorsque j'ai entendu parler de ces trois éléments pour la première fois, j'ai été très confus, car ils ne correspondent à rien de ce que j'avais déjà travaillé. Voici comment les envisager:

  1. Les uniformes sont envoyées à la fois aux nuanceurs de vertex et aux nuanceurs de fragment, et contiennent des valeurs qui restent les mêmes pour l'ensemble du frame affiché. La position d'une lumière en est un bon exemple.

  2. Les attributs sont des valeurs appliquées à des sommets individuels. Les attributs ne sont disponibles que pour le nuanceur de sommets. Par exemple, chaque sommet peut avoir une couleur distincte. Les attributs ont une relation de un à un avec les sommets.

  3. Les variables variables sont des variables déclarées dans le nuanceur de vertex que nous souhaitons partager avec le nuanceur de fragment. Pour ce faire, nous nous assurons de déclarer une variable variable du même type et du même nom dans le nuanceur de vertex et le nuanceur de fragment. Un cas d'utilisation classique est la normale d'un sommet, car elle peut être utilisée dans les calculs d'éclairage.

Nous utiliserons plus tard les trois types afin que vous puissiez vous faire une idée de leur application concrète.

Maintenant que nous avons parlé des nuanceurs de sommets et de fragments, ainsi que des types de variables qu'ils gèrent, il est intéressant d'examiner les nuanceurs les plus simples que nous pouvons 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 colour 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é, non ?

Dans le nuanceur de sommets, Three.js nous envoie quelques uniformes. Ces deux uniformes sont des matrices 4D, appelées matrice modèle-vue et matrice de projection. Vous n'avez pas besoin de savoir exactement comment ils fonctionnent, mais il est toujours préférable de comprendre comment les choses fonctionnent si possible. En résumé, c'est ainsi que la position 3D du sommet est projetée sur la position 2D finale à l'écran.

Je ne les ai pas inclus dans l'extrait de code ci-dessus, 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 que cela, comme des données de lumière, des couleurs de sommets et des normales de sommets. Si vous le faisiez sans Three.js, vous devriez créer et définir vous-même tous ces uniformes et attributs. Histoire vraie.

6. Utiliser un MeshShaderMaterial

Nous avons configuré un nuanceur, mais comment l'utiliser avec Three.js ? Il s'avère que c'est terriblement simple. Il se présente plutôt comme suit:

/**
* 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. C'est vraiment simple. C'est probablement le cas, mais nous parlons de la 3D exécutée dans votre navigateur. Je pense donc que vous vous attendez à une certaine complexité.

Nous pouvons ajouter deux autres propriétés à notre MeshShaderMaterial: les uniformes 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 uniformes sont les mêmes pour l'ensemble du frame, c'est-à-dire pour tous les sommets. Ils ont donc tendance à être des valeurs uniques. Toutefois, les attributs sont des variables par sommet. Ils doivent donc être un tableau. Il doit y avoir une relation individuelle entre le nombre de valeurs dans le tableau des 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 sommet et une uniforme. Nous allons également ajouter une variable variable afin que le nuanceur de sommet puisse envoyer des données au nuanceur de fragment. Le résultat final est que notre sphère qui était rose va sembler éclairée par le haut et sur le côté, et va pulser. C'est un peu trippy, mais j'espère que cela vous aidera à bien comprendre les trois types de variables, ainsi que leur relation les unes avec les autres et la géométrie sous-jacente.

8. Une lumière factice

Modifions la couleur pour qu'elle ne soit pas plate. Nous pourrions examiner la façon dont Three.js gère l'éclairage, mais comme vous pouvez l'imaginer, c'est plus complexe que nécessaire pour le moment. Nous allons donc simuler. Vous devez absolument consulter les shaders fantastiques qui font partie de Three.js, ainsi que les shaders du récent projet WebGL incroyable de Chris Milk et de Google, Rome. Revenons à nos nuanceurs. Nous allons mettre à jour notre nuanceur de sommets pour fournir la normale de chaque sommet au nuanceur de fragment. Nous utilisons pour cela des éléments variés:

// 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 fragment shader, nous allons définir le même nom de variable, puis utiliser le produit scalaire de la normale du sommet avec un vecteur représentant une lumière qui brille d'en 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 colour
gl_FragColor = vec4(dProd, dProd, dProd, 1.0);

}

Le produit scalaire fonctionne donc de la manière suivante : étant donné deux vecteurs, il renvoie un nombre qui indique le degré de "similitude" entre les deux vecteurs. Avec les vecteurs normalisés, si ils pointent exactement dans la même direction, vous obtenez une valeur de 1. Si elles 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, c'est-à-dire qu'il sera entièrement éclairé, tandis qu'un sommet sur le côté aura une valeur proche de 0 et à l'arrière, -1. Nous limitons la valeur à 0 pour toute valeur négative, mais lorsque vous saisissez les chiffres, vous obtenez l'éclairage de base que nous voyons.

Étape suivante Il serait peut-être intéressant d'essayer de modifier certaines positions de sommets.

9. Attributs

Ce que je voudrais que nous fassions maintenant est d'associer un nombre aléatoire à chaque sommet via un attribut. Nous utiliserons ce nombre pour pousser le sommet vers l'extérieur le long de sa normale. Le résultat net sera une sorte de boule de pics qui changera chaque fois que vous actualiserez la page. Il n'est pas encore animé (cela se produit ensuite), mais quelques actualisations de la page vous montreront qu'il est aléatoire.

Commençons par ajouter l'attribut au nuanceur de sommet:

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

À quoi ressemble-t-il ?

Pas vraiment. En effet, l'attribut n'a pas été configuré dans MeshShaderMaterial. Le nuanceur utilise donc une valeur nulle. C'est un peu comme un espace réservé pour le moment. Nous allons ajouter l'attribut au MeshShaderMaterial dans le code JavaScript, et Three.js les associera automatiquement.

Notez également 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. 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 l'avantage est que tout le déplacement se produit sur le GPU.

11. Animer ce suceur

Nous devrions absolument animer cette animation. Comment procéder ? Il y a deux éléments à mettre en place:

  1. Un uniforme pour animer la quantité de déplacement à appliquer dans chaque frame. Nous pouvons utiliser le sinus ou le cosinus pour cela, car ils vont de -1 à 1.
  2. Boucle d'animation en JavaScript

Nous allons ajouter l'uniforme au MeshShaderMaterial et au nuanceur de sommets. Tout d'abord, le nuanceur de vertex:

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

Nous mettons ensuite à jour 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 l'instant. Mais nous avons l'impression d'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, rien ne change. Nous n'avons pas non plus configuré la boucle d'animation. Nous ne verrons donc jamais ce 0 changer pour un autre élément.

Dans notre code JavaScript, nous devons maintenant encapsuler l'appel de rendu dans une fonction, puis utiliser requestAnimationFrame pour l'appeler. Nous devons également mettre à jour la valeur de l'uniforme.

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);

12. Conclusion

Et voilà ! Vous pouvez maintenant voir qu'il s'anime de manière pulsée étrange (et légèrement psychédélique).

Il y a tellement de choses que nous pourrions aborder sur les nuanceurs, mais 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 vous devriez avoir la confiance nécessaire pour créer vos propres nuanceurs incroyables.