Présentation des nuanceurs

Introduction

Je vous ai déjà présenté une présentation de Three.js. Si vous n'avez pas lu ce message, il s'agit de la base sur laquelle je m'appuierai au cours de cet article.

J'aimerais parler des nuanceurs. WebGL est un excellent outil, et comme je l'ai déjà dit Three.js (et d'autres bibliothèques) est un excellent moyen d'éliminer les difficultés pour vous. Toutefois, vous aurez parfois besoin d'obtenir un effet spécifique, ou vous voudrez peut-être approfondir un peu la façon dont ces éléments étonnants sont apparus à l'écran. Les nuanceurs feront très certainement partie de cette équation. De plus, si vous êtes comme moi, vous voudrez peut-être passer des choses de base du dernier tutoriel à quelque chose un peu plus délicat. Je vais me baser sur l'utilisation de Three.js, car il effectue une grande partie des tâches fastidieuses pour mettre en œuvre le nuanceur. J'expliquerai d'emblée le contexte des nuanceurs, et la dernière partie de ce tutoriel nous permettra d'aborder des aspects légèrement plus avancés. La raison à cela est que les nuanceurs sont inhabituels à première vue et demandent un peu d’explication.

1. Nos deux nuanceurs

WebGL ne permet pas d'utiliser le pipeline fixe, ce qui est un moyen court de dire qu'il ne vous donne aucun moyen de rendre vos éléments prêts à l'emploi. Cependant, ce qu'il fait, c'est le pipeline programmable, qui est plus puissant, mais également plus difficile à comprendre et à utiliser. En résumé, le pipeline programmable signifie, en tant que programmeur, que vous assumez la responsabilité de l'affichage des sommets et autres éléments à l'écran. Les nuanceurs font partie de ce pipeline. Il en existe deux types:

  1. Nuanceurs de sommets
  2. Nuanceurs de fragments

Ces deux points, je suis sûr que vous serez d'accord, ne signifient absolument rien en eux-mêmes. Ce que vous devez savoir à leur sujet, c'est qu'ils s'exécutent tous deux 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 se charger d'autres tâches. Un GPU moderne est fortement optimisé pour les fonctions requises par les nuanceurs. Il est donc idéal de pouvoir l'utiliser.

2. Nuanceurs de sommets

Prenez une forme primitive standard, comme une sphère. Il est constitué de sommets, n'est-ce pas ? Un nuanceur de sommets reçoit chacun de ces sommets l'un après l'autre, et il peut perturber leur fonctionnement. C'est au nuanceur de sommets de déterminer ce qu'il fait avec chacun d'eux, mais il a une responsabilité: à un moment donné, il doit définir gl_Position, un vecteur flottant 4D, qui est la position finale du sommet à l'écran. C'est en soi un processus assez intéressant, car il s'agit d'obtenir une position en 3D (un sommet avec x,y,z) sur un écran 2D, ou de la projeter. Heureusement pour nous, si nous utilisons quelque chose comme Three.js, nous disposons d'un moyen abrégé de définir gl_Position sans que les choses 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 des textures et de l'éclairage ? C'est à cela que sert le nuanceur de fragments. Tout comme le nuanceur de sommets, le nuanceur de fragments ne dispose que d'une tâche obligatoire: il doit définir ou supprimer 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 à l'intérieur de ce triangle doit être dessiné. Un fragment correspond aux données fournies par ces trois sommets pour dessiner chaque pixel de ce triangle. C'est pourquoi les fragments reçoivent des valeurs interpolées à partir de leurs sommets constitutifs. Si un sommet est rouge et que son voisin est bleu, les valeurs de couleur interpoleront du rouge au bleu en passant par le violet.

4. Variables du nuanceur

Lorsque vous parlez de variables, vous pouvez effectuer trois déclarations: Uniforms, Attributes et Varyings. La première fois que j'ai entendu parler de ces trois-là, j'ai été très confus car ils ne correspondaient à aucun autre élément avec lequel j'avais déjà travaillé. Voici comment les envisager:

  1. Les uniformes sont envoyées à la fois aux nuanceurs de sommets et aux nuanceurs de fragments. Elles contiennent des valeurs identiques sur l'ensemble du frame affiché. La position d'une lampe 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. Cela pourrait être quelque chose comme chaque sommet ayant une couleur distincte. Les attributs ont une relation de type un à un avec les sommets.

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

Plus tard, nous utiliserons les trois types afin que vous puissiez avoir une idée de la façon dont ils sont appliqués concrètement.

Maintenant que nous avons parlé des nuanceurs de sommets et de fragments, ainsi que des types de variables qu'ils traitent, il est utile 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);
}   

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é cependant, non ?

Dans le nuanceur de sommets, Three.js nous envoie deux variables uniformes. Ces deux variables uniformes sont des matrices 4D, appelées matrices modèles-vues et matrices de projection. Vous n'avez pas désespérément besoin de savoir exactement comment ils fonctionnent, bien qu'il soit toujours préférable de comprendre comment les choses font ce qu'elles font si vous le pouvez. Dans la version courte, il s'agit de la façon dont la position 3D du sommet est réellement projetée sur la position 2D finale à l'écran.

Je les ai omis de l'extrait ci-dessus, car Three.js les ajoute en haut du code de votre nuanceur. Vous n'avez donc pas à vous en soucier. En vérité, cela ajoute bien plus que cela, comme des données de lumière, des couleurs et des normales de sommet. Si vous le faisiez sans Three.js, vous devriez créer et définir vous-même tous ces uniformes et attributs. Une 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 facile. La procédure est 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()
});

À partir de là, Three.js compile et exécute vos nuanceurs associés au maillage auquel vous donnez ce matériau. Rien de plus simple. C'est probablement le cas, mais nous parlons de l'exécution de la 3D dans votre navigateur, donc je pense que vous vous attendez à un certain degré de complexité.

Nous pouvons ajouter deux autres propriétés à MeshShaderMaterial: uniforms et attributs. Elles peuvent toutes les deux accepter des vecteurs, des entiers ou des nombres à virgule flottante, mais comme je l'ai mentionné avant que les variables uniformes soient identiques pour l'ensemble de la trame, c'est-à-dire pour tous les sommets, elles ont tendance à être des valeurs uniques. En revanche, les attributs sont des variables par sommet. Ils sont donc censés être un tableau. Il doit y avoir une relation de type 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 consacrer un peu de temps à ajouter une boucle d'animation, des attributs de sommet et une variable uniforme. Nous allons également ajouter une variable variable afin que le nuanceur de sommets puisse envoyer des données au nuanceur de fragments. Au final, notre sphère rose semble être éclairée d'en haut et de côté, et elle va pulser. C'est un peu fou, mais il devrait vous aider à bien comprendre les trois types de variables, ainsi que leurs relations les uns avec les autres et la géométrie sous-jacente.

8. Une fausse lumière

Mettons à jour la couleur pour qu'il ne s'agisse pas d'un objet de couleur plate. Nous pourrions voir comment Three.js gère l'éclairage, mais je suis sûr que vous pouvez comprendre qu'il est plus complexe que nous n'en avons besoin actuellement, nous allons donc le simuler. Nous vous invitons à examiner en détail les nuanceurs fantastiques qui font partie de Three.js, ainsi que ceux tirés de l'incroyable projet WebGL de Chris Milk et Google, Rome. Revenons à nos nuanceurs. Nous mettrons à jour Vertex Shader pour lui fournir chaque sommet normal. Nous utilisons pour cela des variables:

// 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 de la normale de sommet avec un vecteur représentant une lumière brillante au-dessus et à droite de la sphère. Le résultat net de cela nous donne un effet semblable à une lumière directionnelle dans un pack 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 car, pour deux vecteurs, il donne un nombre qui vous indique à quel point les deux vecteurs sont "similaires". Avec les vecteurs normalisés, s'ils pointent exactement dans la même direction, vous obtenez la valeur 1. S'ils pointent dans des directions opposées, vous obtenez un -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 entièrement éclairé, tandis qu'un sommet du côté aurait une valeur proche de 0 et un sommet arrondi serait -1. Nous limitons la valeur à 0 pour tout ce qui est négatif, mais lorsque vous insérez les chiffres, vous vous retrouvez avec l'éclairage de base que nous voyons.

Étape suivante Eh bien, ce serait bien d'essayer de jouer avec quelques positions de sommet.

9. Attributs

J'aimerais maintenant associer un nombre aléatoire à chaque sommet via un attribut. Nous l'utiliserons pour pousser le sommet le long de sa normale. Le résultat net est une sorte de boule de pic bizarre qui change à chaque fois que vous actualisez la page. Elle ne sera pas encore animée (ce qui se produit ensuite), mais quelques actualisations de la page vous montreront qu'elle est aléatoire.

Commençons 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);
}

À quoi cela ressemble-t-il ?

Pas vraiment différent ! En effet, l'attribut n'a pas été configuré dans MeshShaderMaterial, si bien que le nuanceur utilise une valeur zéro à la place. Pour l’instant, c’est un peu comme un espace réservé. Dans une seconde, nous allons ajouter l'attribut à "MeshShaderMaterial" dans JavaScript, et Three.js les liera automatiquement pour nous.

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 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 endommagée, mais l'avantage est que tous les déplacements se produisent sur le GPU.

11. Animer une ventouse

Nous devrions créer une animation. Comment faisons-nous ? Eh bien, il y a deux choses que nous devons mettre en place:

  1. Uniforme pour animer le déplacement à appliquer dans chaque image. Nous pouvons utiliser le sinus ou le cosinus pour cela car ils vont de -1 à 1
  2. Une boucle d'animation dans le code JavaScript

Nous allons ajouter l'uniforme à la fois à MeshShaderMaterial et à Vertex Shader. Commençons par Vertex Shader:

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 l'élément 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 il semblerait que nous ayons fait un pas en arrière. Cela est en grande partie dû au fait que notre valeur d'amplitude est égale à 0 et que, comme nous la multiplions par le déplacement, nous ne constatons rien de changement. Nous n'avons pas non plus configuré la boucle d'animation. Ainsi, nous ne voyons jamais que 0 passe à autre chose.

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

Voilà, c'est terminé ! Vous pouvez maintenant voir qu'elle s'anime d'une manière étrange (et légèrement triplée).

Nous pouvons aborder bien d'autres sujets, mais j'espère que cette présentation vous a été utile. Vous devriez maintenant être en mesure de comprendre les nuanceurs lorsque vous les voyez et d'être capable de créer vos propres nuanceurs incroyables.