Post-traitement

Le post-traitement fait généralement référence à l'application d'une sorte d'effet ou de filtre à une image 2D. Dans le cas de THREE.js, nous avons une scène avec un ensemble de maillages. Nous rendons cette scène en une image 2D. Normalement, cette image est rendue directement dans le canvas et affichée dans le navigateur, mais nous pouvons au lieu de cela la rendre sur une cible de rendu (render target) et ensuite appliquer des effets de post-traitement au résultat avant de le dessiner sur le canvas. On appelle cela post-traitement parce que cela se produit après (post) le traitement principal de la scène.

Des exemples de post-traitement sont les filtres de type Instagram, les filtres Photoshop, etc...

THREE.js propose des classes d'exemple pour aider à mettre en place un pipeline de post-traitement. La manière dont cela fonctionne est de créer un EffectComposer et d'y ajouter plusieurs objets Pass. Ensuite, vous appelez EffectComposer.render et cela rend votre scène sur une cible de rendu puis applique chaque Pass.

Chaque Pass peut être un effet de post-traitement comme l'ajout d'une vignette, le flou, l'application d'un effet de lumière (bloom), l'application d'un grain de film, le réglage de la teinte, de la saturation, du contraste, etc... et enfin le rendu du résultat sur le canvas.

Il est un peu important de comprendre comment fonctionne EffectComposer. Il crée deux cibles de rendu. Appelons-les rtA et rtB.

Ensuite, vous appelez EffectComposer.addPass pour ajouter chaque pass dans l'ordre où vous voulez les appliquer. Les passes sont ensuite appliquées à peu près comme ceci.

D'abord, la scène que vous avez passée à RenderPass est rendue sur rtA, puis rtA est passée à la passe suivante, quelle qu'elle soit. Cette passe utilise rtA comme entrée pour faire ce qu'elle a à faire et écrit les résultats sur rtB. rtB est ensuite passé à la passe suivante qui utilise rtB comme entrée et écrit de nouveau sur rtA. Cela continue à travers toutes les passes.

Chaque Pass a 4 options de base

enabled

Indique si cette passe doit être utilisée ou non

needsSwap

Indique s'il faut échanger rtA et rtB après avoir terminé cette passe

clear

Indique s'il faut effacer avant de rendre cette passe

renderToScreen

Indique s'il faut rendre sur le canvas au lieu de la cible de rendu de destination actuelle. Dans la plupart des cas d'utilisation, vous ne définissez pas explicitement ce drapeau car la dernière passe de la chaîne est automatiquement rendue sur l'écran.

Mettons en place un exemple de base. Nous allons commencer avec l'exemple de l'article sur la réactivité.

Pour cela, nous créons d'abord un EffectComposer.

const composer = new EffectComposer(renderer);

Ensuite, comme première passe, nous ajoutons un RenderPass qui rendra notre scène avec notre caméra dans la première cible de rendu.

composer.addPass(new RenderPass(scene, camera));

Ensuite, nous ajoutons un BloomPass. Un BloomPass rend son entrée sur une cible de rendu généralement plus petite et floute le résultat. Il ajoute ensuite ce résultat flouté par-dessus l'entrée originale. Cela fait fleurir (bloom) la scène.

const bloomPass = new BloomPass(
    1,    // strength
    25,   // kernel size
    4,    // sigma ?
    256,  // blur render target resolution
);
composer.addPass(bloomPass);

Ensuite, nous ajoutons un FilmPass qui dessine du bruit et des lignes de balayage par-dessus son entrée.

const filmPass = new FilmPass(
    0.5,   // intensity
    false,  // grayscale
);
composer.addPass(filmPass);

Enfin, nous ajoutons un OutputPass qui effectue la conversion de l'espace couleur en sRGB et un mappage tonal (tone mapping) optionnel. Cette passe est généralement la dernière de la chaîne.

const outputPass = new OutputPass();
composer.addPass(outputPass);

Pour utiliser ces classes, nous devons importer un certain nombre de scripts.

import {EffectComposer} from 'three/addons/postprocessing/EffectComposer.js';
import {RenderPass} from 'three/addons/postprocessing/RenderPass.js';
import {BloomPass} from 'three/addons/postprocessing/BloomPass.js';
import {FilmPass} from 'three/addons/postprocessing/FilmPass.js';
import {OutputPass} from 'three/addons/postprocessing/OutputPass.js';

Pour pratiquement n'importe quel post-traitement, EffectComposer.js, RenderPass.js et OutputPass.js sont requis.

Les dernières choses que nous devons faire sont d'utiliser EffectComposer.render au lieu de WebGLRenderer.render et de dire à l'EffectComposer de correspondre à la taille du canvas.

-function render(now) {
-  time *= 0.001;
+let then = 0;
+function render(now) {
+  now *= 0.001;  // convertir en secondes
+  const deltaTime = now - then;
+  then = now;

  if (resizeRendererToDisplaySize(renderer)) {
    const canvas = renderer.domElement;
    camera.aspect = canvas.clientWidth / canvas.clientHeight;
    camera.updateProjectionMatrix();
+    composer.setSize(canvas.width, canvas.height);
  }

  cubes.forEach((cube, ndx) => {
    const speed = 1 + ndx * .1;
-    const rot = time * speed;
+    const rot = now * speed;
    cube.rotation.x = rot;
    cube.rotation.y = rot;
  });

-  renderer.render(scene, camera);
+  composer.render(deltaTime);

  requestAnimationFrame(render);
}

EffectComposer.render prend un deltaTime qui est le temps en secondes depuis le rendu de la dernière frame. Il passe cela aux différents effets au cas où certains d'entre eux seraient animés. Dans ce cas, le FilmPass est animé.

Pour changer les paramètres d'effet à l'exécution, il faut généralement définir des valeurs d'uniformes. Ajoutons une interface graphique (GUI) pour ajuster certains paramètres. Déterminer quelles valeurs vous pouvez facilement ajuster et comment les ajuster nécessite de fouiller dans le code de cet effet.

En regardant à l'intérieur de BloomPass.js, j'ai trouvé cette ligne :

this.combineUniforms[ 'strength' ].value = strength;

Nous pouvons donc définir la force (strength) en définissant

bloomPass.combineUniforms.strength.value = someValue;

De même, en regardant dans FilmPass.js, j'ai trouvé ces lignes :

this.uniforms.intensity.value = intensity;
this.uniforms.grayscale.value = grayscale;

Ce qui indique assez clairement comment les définir.

Faisons une petite interface graphique rapide pour définir ces valeurs

import {GUI} from 'three/addons/libs/lil-gui.module.min.js';

et

const gui = new GUI();
{
  const folder = gui.addFolder('BloomPass');
  folder.add(bloomPass.combineUniforms.strength, 'value', 0, 2).name('strength');
  folder.open();
}
{
  const folder = gui.addFolder('FilmPass');
  folder.add(filmPass.uniforms.grayscale, 'value').name('grayscale');
  folder.add(filmPass.uniforms.intensity, 'value', 0, 1).name('intensity');
  folder.open();
}

et maintenant nous pouvons ajuster ces paramètres

Ce fut une petite étape pour créer notre propre effet.

Les effets de post-traitement utilisent des shaders. Les shaders sont écrits dans un langage appelé GLSL (Graphics Library Shading Language). Passer en revue l'intégralité du langage est un sujet beaucoup trop vaste pour ces articles. Quelques ressources pour commencer seraient peut-être cet article et peut-être le Livre des Shaders.

Je pense qu'un exemple pour vous aider à démarrer serait utile, alors créons un simple shader de post-traitement GLSL. Nous en créerons un qui nous permette de multiplier l'image par une couleur.

Pour le post-traitement, THREE.js fournit un outil utile appelé ShaderPass. Il prend un objet avec des informations définissant un shader de vertex, un shader de fragment, et les entrées par défaut. Il gérera la configuration de la texture à lire pour obtenir les résultats de la passe précédente et l'endroit où rendre, soit sur une des cibles de rendu de l'EffectComposer, soit sur le canvas.

Voici un simple shader de post-traitement qui multiplie le résultat de la passe précédente par une couleur.

const colorShader = {
  uniforms: {
    tDiffuse: { value: null },
    color:    { value: new THREE.Color(0x88CCFF) },
  },
  vertexShader: `
    varying vec2 vUv;
    void main() {
      vUv = uv;
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1);
    }
  `,
  fragmentShader: `
    varying vec2 vUv;
    uniform sampler2D tDiffuse;
    uniform vec3 color;
    void main() {
      vec4 previousPassColor = texture2D(tDiffuse, vUv);
      gl_FragColor = vec4(
          previousPassColor.rgb * color,
          previousPassColor.a);
    }
  `,
};

Ci-dessus, tDiffuse est le nom que ShaderPass utilise pour passer la texture résultat de la passe précédente, donc nous en avons pratiquement toujours besoin. Nous déclarons ensuite color comme une Color de THREE.js.

Ensuite, nous avons besoin d'un shader de vertex. Pour le post-traitement, le shader de vertex montré ici est à peu près standard et n'a que rarement besoin d'être modifié. Sans entrer dans trop de détails (voir les articles liés ci-dessus), les variables uv, projectionMatrix, modelViewMatrix et position sont toutes ajoutées comme par magie par THREE.js.

Enfin, nous créons un shader de fragment. Dans celui-ci, nous obtenons une couleur de pixel de la passe précédente avec cette ligne

vec4 previousPassColor = texture2D(tDiffuse, vUv);

nous la multiplions par notre couleur et définissons gl_FragColor au résultat

gl_FragColor = vec4(
    previousPassColor.rgb * color,
    previousPassColor.a);

Ajoutons une simple interface graphique (GUI) pour définir les 3 valeurs de la couleur

const gui = new GUI();
gui.add(colorPass.uniforms.color.value, 'r', 0, 4).name('red');
gui.add(colorPass.uniforms.color.value, 'g', 0, 4).name('green');
gui.add(colorPass.uniforms.color.value, 'b', 0, 4).name('blue');

Ce qui nous donne un simple effet de post-traitement qui multiplie par une couleur.

Comme mentionné précédemment, tous les détails sur la manière d'écrire du GLSL et des shaders personnalisés sont trop complexes pour ces articles. Si vous voulez vraiment savoir comment fonctionne WebGL lui-même, consultez ces articles. Une autre excellente ressource est simplement de lire les shaders de post-traitement existants dans le dépôt THREE.js. Certains sont plus compliqués que d'autres, mais si vous commencez par les plus petits, vous pourrez, je l'espère, vous faire une idée de leur fonctionnement.

La plupart des effets de post-traitement dans le dépôt THREE.js ne sont malheureusement pas documentés, donc pour les utiliser, vous devrez lire les exemples ou le code des effets eux-mêmes. J'espère que ces simples exemples et l'article sur les cibles de rendu vous fourniront suffisamment de contexte pour commencer.