Présentation des nuanceurs

Introduction

Je vous ai déjà présenté Three.js. Si vous ne l'avez pas lu, nous vous conseillons de 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 que vous souhaitiez en savoir plus sur 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 travailler en partant du principe que vous utilisez Three.js, car il effectue une grande partie du travail pour nous en ce qui concerne l'exécution du nuanceur. Je dirai d'emblée que j'expliquerai d'abord le contexte des nuanceurs, et que la dernière partie de ce tutoriel sera l'occasion d'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 rendre vos éléments prêts à l'emploi. Il offre cependant, Programmable Pipeline, un pipeline plus puissant, mais également 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. Nuanceurs de sommets
  2. Nuancheurs de fragments

Vous serez certainement d'accord avec ces deux termes. Ils ne signifient absolument rien en soi. 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, en 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. Vertex Shaders

Elles prennent 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. 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 texturation et de l'éclairage ? C'est à cela que sert le nuanceur de fragments. Tout comme le nuanceur de sommets, le nuanceur de fragments n'a qu'une seule tâche incontournable: il doit définir ou supprimer la variable gl_FragColor, un autre vecteur float 4D correspondant à 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

Lorsque vous parlez de variables, vous pouvez effectuer trois déclarations: Uniforms, Attributes et Varyings. 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 sommet. 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 variables uniformes. Ces deux variables uniformes sont des matrices 4D, appelées matrices Model-View et matrice de projection. Vous n'avez pas nécessairement 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 réellement 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. Elle se présente plutôt de la façon suivante:

/**
* 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. Vous devez donc vous attendre à 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. Les attributs, quant à eux, sont des variables par sommet. Ils doivent donc être des tableaux. Il doit y avoir une relation de type un à un entre le nombre de valeurs du tableau d'attributs et le nombre de sommets du 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 sommets puisse lui envoyer des données. 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 déroutant, 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'il ne s'agisse pas d'un objet plat et coloré. 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. Examinez en détail les nuanceurs qui font partie de Three.js, ainsi que ceux issus du récent projet WebGL de Chris Milk et de Google, Rome. Revenons à nos nuanceurs. Nous mettrons à jour notre nuanceur Vertex pour fournir chaque sommet normal au nuanceur de fragments. Pour cela, nous utilisons différents:

// 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 définir le même nom de variable, puis utiliser le produit scalaire du sommet normal avec un vecteur représentant une lumière brillante au-dessus 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);

}

La raison pour laquelle le produit scalaire fonctionne est qu'avec deux vecteurs, il génère 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 bien d'essayer de modifier certaines positions de sommets.

9. Attributs

Ce que je voudrais que nous fassions maintenant, c'est associer un nombre aléatoire à chaque sommet via un attribut. Ce nombre nous permet de pousser le sommet selon sa normale. Le résultat net sera une sorte de balle rebondissante qui change chaque fois que vous actualisez 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. Dans un instant, nous ajouterons l'attribut à MeshShaderMaterial dans 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 tronquée. Ce qui est intéressant, c'est que tout le déplacement se produit sur le GPU.

11. Animer ce suceur

Nous devrions vraiment l'animer. Comment parvenons-nous à faire ? Il y a deux choses que nous devons mettre en place:

  1. Uniforme pour animer l'intensité du déplacement à appliquer à chaque image. Pour ce faire, nous pouvons utiliser le sinus ou le cosinus, car ils vont de -1 à 1.
  2. Une boucle d'animation dans le code JS

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 principalement dû au fait que la valeur de notre amplitude est égale à 0 et que, comme nous multiplions cette valeur par le déplacement, nous ne constatons aucun changement. 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 aussi mettre à jour la valeur de la variable 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&#39;il s&#39;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.