# Three.js et Shadertoy

Shadertoy est un site web célèbre qui héberge des expériences de shaders incroyables. Les gens demandent souvent comment ils peuvent utiliser ces shaders avec Three.js.

Il est important de reconnaître que cela s'appelle ShaderTOY pour une raison. En général, les shaders shadertoy ne sont pas axés sur les meilleures pratiques. Il s'agit plutôt d'un défi amusant, similaire à dwitter (écrire du code en 140 caractères) ou js13kGames (faire un jeu en 13k ou moins).

Dans le cas de Shadertoy, le défi est : écrire une fonction qui, pour une position de pixel donnée, produit une couleur qui dessine quelque chose d'intéressant. C'est un défi amusant et de nombreux résultats sont incroyables. Mais ce n'est pas la meilleure pratique.

Comparez ce shader shadertoy incroyable qui dessine une ville entière

En plein écran sur mon GPU, il tourne à environ 5 images par seconde. Comparez cela à un jeu comme Cities: Skylines

Ce jeu tourne à 30-60 images par seconde sur la même machine car il utilise des techniques plus traditionnelles, dessinant des bâtiments faits de triangles avec des textures, etc...

Néanmoins, passons en revue l'utilisation d'un shader Shadertoy avec three.js.

C'est le shader shadertoy par défaut si vous choisissez "New" sur shadertoy.com, du moins en janvier 2019.

    // Par iq: https://www.shadertoy.com/user/iq
    // licence: Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License.
    void mainImage( out vec4 fragColor, in vec2 fragCoord )
    {
        // Coordonnées normalisées des pixels (de 0 à 1)
        vec2 uv = fragCoord/iResolution.xy;

        // Couleur variable des pixels avec le temps
        vec3 col = 0.5 + 0.5*cos(iTime+uv.xyx+vec3(0,2,4));

        // Sortie à l'écran
        fragColor = vec4(col,1.0);
    }

Une chose importante à comprendre à propos des shaders est qu'ils sont écrits dans un langage appelé GLSL (Graphics Library Shading Language) conçu pour les mathématiques 3D qui inclut des types spéciaux. Ci-dessus, nous voyons `vec4`, `vec2`, `vec3` comme 3 de ces types spéciaux. Un `vec2` a 2 valeurs, un `vec3` 3, un `vec4` 4 valeurs. Ils peuvent être adressés de plusieurs manières. Les manières les plus courantes sont avec `x`, `y`, `z` et `w` comme dans

    vec4 v1 = vec4(1.0, 2.0, 3.0, 4.0);
    float v2 = v1.x + v1.y;  // adds 1.0 + 2.0

Contrairement à JavaScript, GLSL ressemble plus à C/C++ où les variables doivent avoir leur type déclaré, donc au lieu de `var v = 1.2;` c'est `float v = 1.2;` déclarant `v` comme un nombre à virgule flottante.

Expliquer GLSL en détail dépasse le cadre de cet article. Pour un aperçu rapide, consultez cet article et peut-être poursuivez avec cette série.

Il convient de noter que, du moins en janvier 2019, shadertoy.com ne s'occupe que des fragment shaders. La responsabilité d'un fragment shader est, étant donné une position de pixel, de produire une couleur pour ce pixel.

En regardant la fonction ci-dessus, nous pouvons voir que le shader a un paramètre `out` appelé `fragColor`. `out` signifie `output` (sortie). C'est un paramètre pour lequel la fonction est censée fournir une valeur. Nous devons le définir à une certaine couleur.

Il a également un paramètre `in` (pour input, entrée) appelé `fragCoord`. C'est la coordonnée du pixel qui est sur le point d'être dessinée. Nous pouvons utiliser cette coordonnée pour décider d'une couleur. Si le canevas sur lequel nous dessinons est de 400x300 pixels, alors la fonction sera appelée 400x300 fois, soit 120 000 fois. À chaque fois, `fragCoord` sera une coordonnée de pixel différente.

Il y a 2 autres variables utilisées qui ne sont pas définies dans le code. L'une est `iResolution`. Elle est définie à la résolution du canevas. Si le canevas est de 400x300, alors `iResolution` serait 400,300, donc au fur et à mesure que les coordonnées des pixels changent, cela fait passer `uv` de 0.0 à 1.0 en travers et vers le haut de la texture. Travailler avec des valeurs normalisées rend souvent les choses plus faciles, c'est pourquoi la majorité des shaders shadertoy commencent par quelque chose comme ça.

L'autre variable non définie dans le shader est `iTime`. C'est le temps écoulé depuis le chargement de la page en secondes.

Dans le jargon des shaders, ces variables globales sont appelées variables uniformes. Elles sont appelées uniformes car elles ne changent pas, elles restent uniformes d'une itération du shader à la suivante. Il est important de noter qu'elles sont toutes spécifiques à shadertoy. Ce ne sont pas des variables GLSL officielles. Ce sont des variables que les créateurs de shadertoy ont inventées.

La documentation de Shadertoy en définit plusieurs autres. Pour l'instant, écrivons quelque chose qui gère les deux utilisées dans le shader ci-dessus.

La première chose à faire est de créer un simple plan qui remplit le canevas. Si vous ne l'avez pas encore lu, nous l'avons fait dans l'article sur les arrière-plans, alors prenons cet exemple mais retirons les cubes. C'est assez court, voici donc l'intégralité :

    function main() {
      const canvas = document.querySelector('#c');
      const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
      renderer.autoClearColor = false;

      const camera = new THREE.OrthographicCamera(
        -1, // left
         1, // right
         1, // top
        -1, // bottom
        -1, // near,
         1, // far
      );
      const scene = new THREE.Scene();
      const plane = new THREE.PlaneGeometry(2, 2);
      const material = new THREE.MeshBasicMaterial({
          color: 'red',
      });
      scene.add(new THREE.Mesh(plane, material));

      function resizeRendererToDisplaySize(renderer) {
        const canvas = renderer.domElement;
        const width = canvas.clientWidth;
        const height = canvas.clientHeight;
        const needResize = canvas.width !== width || canvas.height !== height;
        if (needResize) {
          renderer.setSize(width, height, false);
        }
        return needResize;
      }

      function render() {
        resizeRendererToDisplaySize(renderer);

        renderer.render(scene, camera);

        requestAnimationFrame(render);
      }

      requestAnimationFrame(render);
    }

    main();

Comme expliqué dans l'article sur les arrière-plans, une `OrthographicCamera` avec ces paramètres et un plan de 2 unités remplira le canevas. Pour l'instant, tout ce que nous obtiendrons est un canevas rouge car notre plan utilise un `MeshBasicMaterial` rouge.

cliquer ici pour ouvrir dans une fenêtre séparée

Maintenant que nous avons quelque chose qui fonctionne, ajoutons le shader shadertoy.

    const fragmentShader = `
    #include <common>

    uniform vec3 iResolution;
    uniform float iTime;

    // Par iq: https://www.shadertoy.com/user/iq
    // licence: Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License.
    void mainImage( out vec4 fragColor, in vec2 fragCoord )
    {
        // Coordonnées normalisées des pixels (de 0 à 1)
        vec2 uv = fragCoord/iResolution.xy;

        // Couleur variable des pixels avec le temps
        vec3 col = 0.5 + 0.5*cos(iTime+uv.xyx+vec3(0,2,4));

        // Sortie à l'écran
        fragColor = vec4(col,1.0);
    }

    void main() {
      mainImage(gl_FragColor, gl_FragCoord.xy);
    }
    `;

Ci-dessus, nous avons déclaré les 2 variables uniformes dont nous avons parlé. Ensuite, nous avons inséré le code GLSL du shader de shadertoy. Enfin, nous avons appelé `mainImage` en lui passant `gl_FragColor` et `gl_FragCoord.xy`. `gl_FragColor` est une variable globale WebGL officielle que le shader est responsable de définir à la couleur qu'il souhaite pour le pixel actuel. `gl_FragCoord` est une autre variable globale WebGL officielle qui nous indique la coordonnée du pixel pour lequel nous choisissons actuellement une couleur.

Nous devons ensuite configurer les uniformes de three.js afin de pouvoir fournir des valeurs au shader.

    const uniforms = {
      iTime: { value: 0 },
      iResolution:  { value: new THREE.Vector3() },
    };

Chaque uniforme dans THREE.js a un paramètre `value`. Cette valeur doit correspondre au type de l'uniforme.

Ensuite, nous passons le fragment shader et les uniformes à un `ShaderMaterial`.

    -const material = new THREE.MeshBasicMaterial({
    -    color: 'red',
    -});
    +const material = new THREE.ShaderMaterial({
    +  fragmentShader,
    +  uniforms,
    +});

et avant de rendre, nous devons définir les valeurs des uniformes

    -function render() {
    +function render(time) {
    +  time *= 0.001;  // convertir en secondes

      resizeRendererToDisplaySize(renderer);

    +  const canvas = renderer.domElement;
    +  uniforms.iResolution.value.set(canvas.width, canvas.height, 1);
    +  uniforms.iTime.value = time;

      renderer.render(scene, camera);

      requestAnimationFrame(render);
    }

> Note : Je n'ai aucune idée pourquoi `iResolution` est un `vec3` et ce que contient la 3ème valeur n'est pas documenté sur shadertoy.com. Elle n'est pas utilisée ci-dessus, donc je la définis juste à 1 pour l'instant. ¯\_(ツ)_/¯

cliquer ici pour ouvrir dans une fenêtre séparée

Cela correspond à ce que nous voyons sur Shadertoy pour un nouveau shader, du moins en janvier 2019 😉. Que fait le shader ci-dessus ?

- `uv` va de 0 à 1.
- `cos(uv.xyx)` nous donne 3 valeurs de cosinus sous forme de `vec3`. Une pour `uv.x`, une autre pour `uv.y` et une autre pour `uv.x` à nouveau.
- L'ajout du temps, `cos(iTime+uv.xyx)`, les rend animés.
- L'ajout de `vec3(0,2,4)` comme dans `cos(iTime+uv.xyx+vec3(0,2,4))` décale les ondes cosinusoïdales
- `cos` va de -1 à 1, donc `0.5 * 0.5 + cos(...)` convertit de -1 <-> 1 à 0.0 <-> 1.0
- les résultats sont ensuite utilisés comme couleur RVB pour le pixel actuel

Un petit changement facilitera la visualisation des ondes cosinusoïdales. Actuellement, `uv` ne va que de 0 à 1. Un cosinus se répète à 2π, alors faisons-le aller de 0 à 40 en multipliant par 40.0. Cela devrait le faire se répéter environ 6,3 fois.

    -vec3 col = 0.5 + 0.5*cos(iTime+uv.xyx+vec3(0,2,4));
    +vec3 col = 0.5 + 0.5*cos(iTime+uv.xyx*40.0+vec3(0,2,4));

En comptant ci-dessous, je vois environ 6,3 répétitions. Nous pouvons voir le bleu entre le rouge car il est décalé de 4 via le `+vec3(0,2,4)`. Sans cela, le bleu et le rouge se chevaucheraient parfaitement, créant du violet.

cliquer ici pour ouvrir dans une fenêtre séparée

Savoir à quel point les entrées sont simples et ensuite voir des résultats comme un canal urbain, une forêt, un escargot, un champignon rend le défi d'autant plus impressionnant. Espérons qu'ils expliquent également clairement pourquoi ce n'est généralement pas la bonne approche par rapport aux méthodes plus traditionnelles de création de scènes à partir de triangles. Le fait qu'il faille faire autant de calculs pour déterminer la couleur de chaque pixel signifie que ces exemples tournent très lentement.

Certains shaders shadertoy prennent des textures en entrée, comme celui-ci.

    // Par Daedelus: https://www.shadertoy.com/user/Daedelus
    // licence: Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License.
    #define TIMESCALE 0.25
    #define TILES 8
    #define COLOR 0.7, 1.6, 2.8

    void mainImage( out vec4 fragColor, in vec2 fragCoord )
    {
        vec2 uv = fragCoord.xy / iResolution.xy;
        uv.x *= iResolution.x / iResolution.y;

        vec4 noise = texture2D(iChannel0, floor(uv * float(TILES)) / float(TILES));
        float p = 1.0 - mod(noise.r + noise.g + noise.b + iTime * float(TIMESCALE), 1.0);
        p = min(max(p * 3.0 - 1.8, 0.1), 2.0);

        vec2 r = mod(uv * float(TILES), 1.0);
        r = vec2(pow(r.x - 0.5, 2.0), pow(r.y - 0.5, 2.0));
        p *= 1.0 - pow(min(1.0, 12.0 * dot(r, r)), 2.0);

        fragColor = vec4(COLOR, 1.0) * p;
    }

Passer une texture à un shader est similaire à en passer une à un matériau normal, mais nous devons configurer la texture sur les uniformes.

Tout d'abord, ajoutons l'uniforme pour la texture au shader. Ils sont appelés `sampler2D` en GLSL.

    const fragmentShader = `
    #include <common>

    uniform vec3 iResolution;
    uniform float iTime;
    +uniform sampler2D iChannel0;

    ...

Ensuite, nous pouvons charger une texture comme nous l'avons vu ici et affecter la valeur de l'uniforme.

    +const loader = new THREE.TextureLoader();
    +const texture = loader.load('resources/images/bayer.png');
    +texture.minFilter = THREE.NearestFilter;
    +texture.magFilter = THREE.NearestFilter;
    +texture.wrapS = THREE.RepeatWrapping;
    +texture.wrapT = THREE.RepeatWrapping;
    const uniforms = {
      iTime: { value: 0 },
      iResolution:  { value: new THREE.Vector3() },
    +  iChannel0: { value: texture },
    };

cliquer ici pour ouvrir dans une fenêtre séparée

Jusqu'à présent, nous avons utilisé les shaders Shadertoy tels qu'ils sont utilisés sur Shadertoy.com, à savoir pour couvrir le canevas. Il n'y a cependant aucune raison de nous limiter à ce cas d'utilisation. L'important à retenir est que les fonctions que les gens écrivent sur shadertoy prennent généralement juste une entrée `fragCoord` et une `iResolution`. `fragCoord` n'a pas à provenir des coordonnées de pixels ; nous pourrions utiliser autre chose, comme des coordonnées de texture, et les utiliser ensuite un peu comme d'autres textures. Cette technique d'utilisation d'une fonction pour générer des textures est souvent appelée une texture procédurale.

Modifions le shader ci-dessus pour faire cela. La chose la plus simple à faire pourrait être de prendre les coordonnées de texture que three.js fournit normalement, de les multiplier par `iResolution` et de les passer pour `fragCoords`.

Pour ce faire, nous ajoutons un varying. Un varying est une valeur passée du vertex shader au fragment shader qui est interpolée (ou varie) entre les sommets. Pour l'utiliser dans notre fragment shader, nous la déclarons. Three.js fait référence à ses coordonnées de texture comme `uv` avec le `v` devant signifiant varying (variable).

    ...

    +varying vec2 vUv;

    void main() {
    -  mainImage(gl_FragColor, gl_FragCoord.xy);
    +  mainImage(gl_FragColor, vUv * iResolution.xy);
    }

Ensuite, nous devons également fournir notre propre vertex shader. Voici un vertex shader three.js minimal assez courant. Three.js déclare et fournira des valeurs pour `uv`, `projectionMatrix`, `modelViewMatrix` et `position`.

    const vertexShader = `
      varying vec2 vUv;
      void main() {
        vUv = uv;
        gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
      }
    `;

Nous devons passer le vertex shader au `ShaderMaterial`

    const material = new THREE.ShaderMaterial({
      vertexShader,
      fragmentShader,
      uniforms,
    });

Nous pouvons définir la valeur de l'uniforme `iResolution` au moment de l'initialisation car elle ne changera plus.

    const uniforms = {
      iTime: { value: 0 },
    -  iResolution:  { value: new THREE.Vector3() },
    +  iResolution:  { value: new THREE.Vector3(1, 1, 1) },
      iChannel0: { value: texture },
    };

et nous n'avons plus besoin de la définir au moment du rendu

    -const canvas = renderer.domElement;
    -uniforms.iResolution.value.set(canvas.width, canvas.height, 1);
    uniforms.iTime.value = time;

Sinon, j'ai copié à nouveau la caméra originale et le code qui configure 3 cubes en rotation de l'article sur la réactivité. Le résultat :

cliquer ici pour ouvrir dans une fenêtre séparée

J'espère que cela vous donnera au moins une base sur la façon d'utiliser un shader shadertoy avec three.js. Encore une fois, il est important de se rappeler que la plupart des shaders shadertoy sont un défi intéressant (tout dessiner avec une seule fonction) plutôt que la méthode recommandée pour réellement afficher des choses de manière performante. Néanmoins, ils sont incroyables, impressionnants, beaux, et vous pouvez apprendre énormément en voyant comment ils fonctionnent.