WebGPU / TSL

Starter pack

WebGPU est un nouveau standard web qui fournit une API bas niveau pour le rendu graphique et l'exécution de calculs sur le GPU. Il est conçu pour succéder à WebGL, offrant de meilleures performances et des fonctionnalités plus avancées.

Bonne nouvelle, il est désormais possible de l'utiliser avec Three.js avec des modifications minimales de la base de code.

Dans cette leçon, nous allons explorer comment utiliser WebGPU avec Three.js et React Three Fiber, et comment écrire des shaders en utilisant le nouveau Three Shading Language (TSL).

Si vous êtes nouveau dans l'univers des shaders, je vous recommande de d'abord compléter le chapitre Shaders avant de poursuivre avec celui-ci.

WebGPU Renderer

Pour utiliser l'API WebGPU au lieu de l'API WebGL, nous devons utiliser un WebGPURenderer (Pas de section dédiée dans la documentation de Three.js pour l'instant) à la place de WebGLRenderer.

Avec React Three Fiber, lors de la création d'un composant <Canvas>, la configuration du renderer est effectuée automatiquement. Cependant, nous pouvons remplacer le renderer par défaut en passant une fonction à la prop gl du composant <Canvas>.

Dans App.jsx, nous avons un composant <Canvas> qui utilise le WebGLRenderer par défaut. Modifions-le pour utiliser le WebGPURenderer à la place.

Tout d'abord, nous devons arrêter le frameloop jusqu'à ce que le WebGPURenderer soit prêt. Nous pouvons le faire en définissant la prop frameloop sur never.

// ...
import { useState } from "react";

function App() {
  const [frameloop, setFrameloop] = useState("never");

  return (
    <>
      {/* ... */}
      <Canvas
        // ...
        frameloop={frameloop}
      >
        {/* ... */}
      </Canvas>
    </>
  );
}

export default App;

Ensuite, nous devons importer la version WebGPU de Three.js :

import * as THREE from "three/webgpu";

Lors de l'utilisation de WebGPU, nous devons utiliser le module three/webgpu à la place du module par défaut three. Cela est dû au fait que le WebGPURenderer n'est pas inclus dans la build par défaut de Three.js.

Ensuite, nous pouvons utiliser la prop gl pour créer une nouvelle instance de WebGPURenderer :

// ...

function App() {
  const [frameloop, setFrameloop] = useState("never");

  return (
    <>
      {/* ... */}
      <Canvas
        // ...
        gl={(canvas) => {
          const renderer = new THREE.WebGPURenderer({
            canvas,
            powerPreference: "high-performance",
            antialias: true,
            alpha: false,
            stencil: false,
            shadowMap: true,
          });
          renderer.init().then(() => {
            setFrameloop("always");
          });
          return renderer;
        }}
      >
        {/* ... */}
      </Canvas>
    </>
  );
}
// ...

Nous créons une nouvelle instance de WebGPURenderer et lui passons l'élément canvas. Nous définissons également certaines options pour le renderer, telles que powerPreference, antialias, alpha, stencil, et shadowMap. Ces options sont similaires à celles utilisées dans le WebGLRenderer.

Enfin, nous appelons la méthode init() du renderer pour l'initialiser. Une fois l'initialisation terminée, nous définissons l'état frameloop à "always" pour commencer le rendu.

Voyons le résultat dans le navigateur :

Notre cube est désormais rendu en utilisant le WebGPURenderer à la place du WebGLRenderer.

C'est aussi simple que cela ! Nous avons configuré avec succès un WebGPURenderer dans notre application React Three Fiber. Vous pouvez maintenant utiliser la même API Three.js pour créer et manipuler des objets 3D, tout comme vous le feriez avec le WebGLRenderer.

Le plus grand changement concerne l'écriture des shaders. L'API WebGPU utilise un langage de shading différent de WebGL, ce qui signifie que nous devons écrire nos shaders d'une manière différente, en WGSL au lieu de GLSL.

C'est là que le Three Shading Language (TSL) entre en jeu.

Langage de Shading Three

TSL est un nouveau langage de shading conçu pour être utilisé avec Three.js afin d'écrire des shaders de manière plus conviviale grâce à une approche basée sur les nœuds.

Un grand avantage de TSL est qu'il est indépendant du moteur de rendu, ce qui signifie que vous pouvez utiliser les mêmes shaders avec différents moteurs de rendu, tels que WebGL et WebGPU.

Cela simplifie l'écriture et la maintenance des shaders, car vous n'avez pas à vous soucier des différences entre les deux langages de shading.

Il est également à l'épreuve du temps, car si un nouveau moteur de rendu est publié, nous pourrions utiliser les mêmes shaders sans aucune modification tant que TSL le supporte.

Le Three Shading Language est encore en développement, mais il est déjà disponible dans les dernières versions de Three.js. La meilleure façon de l'apprendre, et de suivre les modifications, est de consulter la page wiki du Three Shading Language. Je l'ai utilisé de manière intensive pour apprendre à l'utiliser.

Matériaux basés sur des nœuds

Pour comprendre comment créer des shaders avec TSL, nous devons comprendre ce que signifie être basé sur des nœuds.

Dans une approche basée sur des nœuds, nous créons des shaders en connectant différents nœuds pour créer un graphe. Chaque nœud représente une opération ou fonction spécifique, et les connexions entre les nœuds représentent le flux de données.

Cette approche présente de nombreux avantages, tels que :

  • Représentation visuelle : Il est plus facile de comprendre et de visualiser le flux de données et les opérations dans un shader.
  • Réutilisabilité : Nous pouvons créer des nœuds réutilisables qui peuvent être utilisés dans différents shaders.
  • Flexibilité : Nous pouvons facilement modifier et changer le comportement d'un shader en ajoutant ou retirant des nœuds.
  • Extensibilité : Ajouter/Personnaliser des fonctionnalités à partir de matériaux existants est désormais un jeu d'enfant.
  • Agnostic : TSL générera le code approprié pour le moteur de rendu cible, qu'il s'agisse de WebGL (GLSL) ou de WebGPU (WGSL).

Avant de commencer à coder notre premier matériau basé sur des nœuds, nous pouvons utiliser le playground Three.js en ligne pour expérimenter visuellement avec le système de nœuds.

Ouvrez le playground Three.js et en haut, cliquez sur le bouton Examples, puis choisissez l'exemple basic > fresnel.

Three.js playground

Vous devriez voir un éditeur de matériau basé sur des nœuds avec deux nœuds color et un nœud float attachés à un nœud fresnel. (Color A, Color B, et Fresnel Factor)

Le nœud fresnel est connecté à la couleur du Basic Material, colorant ainsi la théière avec un effet fresnel.

Three.js playground

Utilisez le bouton Splitscreen pour visualiser le résultat à droite.

Disons que nous voulons affecter l'opacité du Basic Material en fonction du temps. Nous pouvons ajouter un nœud Timer et le connecter à un nœud Fract pour remettre le temps à 0 une fois qu'il atteint 1. Puis nous le connectons à l'entrée opacity du Basic Material.

Notre théière apparaît maintenant progressivement avant de disparaître et de réapparaître.

Prenez le temps de jouer avec les différents nœuds et voyez comment ils affectent le matériau.

Maintenant que nous avons une compréhension de base du fonctionnement du matériau basé sur des nœuds, voyons comment utiliser le nouveau matériau basé sur des nœuds de Three.js dans React Three Fiber.

Implémentation de React Three Fiber

Jusqu'à présent, avec WebGL, nous avons utilisé le MeshBasicMaterial, le MeshStandardMaterial ou même le ShaderMaterial personnalisé pour créer nos matériaux.

En utilisant WebGPU, nous devons utiliser de nouveaux matériaux compatibles avec TSL. Leurs noms sont les mêmes que ceux que nous utilisions auparavant, précédés de Node avant Material :

  • MeshBasicMaterial -> MeshBasicNodeMaterial
  • MeshStandardMaterial -> MeshStandardNodeMaterial
  • MeshPhysicalMaterial -> MeshPhysicalNodeMaterial
  • ...

Pour les utiliser de manière déclarative avec React Three Fiber, nous devons les extend. Dans App.jsx :

// ...
import { extend } from "@react-three/fiber";

extend({
  MeshBasicNodeMaterial: THREE.MeshBasicNodeMaterial,
  MeshStandardNodeMaterial: THREE.MeshStandardNodeMaterial,
});
// ...

Dans les futures versions de React Three Fiber, cela pourrait être fait automatiquement.

Nous pouvons maintenant utiliser les nouveaux MeshBasicNodeMaterial et MeshStandardNodeMaterial dans nos composants.

Remplaçons le MeshStandardMaterial du cube dans notre composant Experience par le MeshStandardNodeMaterial :

<mesh>
  <boxGeometry args={[1, 1, 1]} />
  <meshStandardNodeMaterial color="pink" />
</mesh>

WebGPU Pink Cube

Nous pouvons utiliser le MeshStandardNodeMaterial tout comme nous utiliserions le MeshStandardMaterial.

Notre cube repose maintenant sur le MeshStandardNodeMaterial au lieu du MeshStandardMaterial. Nous pouvons désormais utiliser des nodes pour personnaliser le matériau.

Node de Couleur

Apprenons à créer des nœuds personnalisés pour personnaliser nos matériaux avec TSL.

Tout d'abord, créons un nouveau composant nommé PracticeNodeMaterial.jsx dans le dossier src/components.

export const PracticeNodeMaterial = ({
  colorA = "white",
  colorB = "orange",
}) => {
  return <meshStandardNodeMaterial color={colorA} />;
};

Et dans Experience.jsx, remplaçons notre cube par un plan utilisant le PracticeNodeMaterial :

// ...
import { PracticeNodeMaterial } from "./PracticeNodeMaterial";

export const Experience = () => {
  return (
    <>
      {/* ... */}

      <mesh rotation-x={-Math.PI / 2}>
        <planeGeometry args={[2, 2, 200, 200]} />
        <PracticeNodeMaterial />
      </mesh>
    </>
  );
};

WebGPU Plane

Nous avons un plan avec le PracticeNodeMaterial.

Pour personnaliser notre matériau, nous pouvons maintenant modifier les différents nœuds à notre disposition en utilisant différents nœuds. La liste des nœuds disponibles se trouve dans la page wiki.

Commençons simplement avec le nœud colorNode pour changer la couleur de notre matériau. Dans PracticeNodeMaterial.jsx :

import { color } from "three/tsl";

export const PracticeNodeMaterial = ({
  colorA = "white",
  colorB = "orange",
}) => {
  return <meshStandardNodeMaterial colorNode={color(colorA)} />;
};

Nous définissons la prop colorNode en utilisant le nœud color du module three/tsl. Le nœud color prend une couleur comme argument et renvoie un nœud de couleur utilisable dans le matériau.

Cela nous donne le même résultat qu'avant, mais maintenant nous pouvons ajouter plus de nœuds pour personnaliser notre matériau.

Importons les nœuds mix et uv du module three/tsl et utilisons-les pour mélanger deux couleurs basées sur les coordonnées UV du plan.

import { color, mix, uv } from "three/tsl";

export const PracticeNodeMaterial = ({
  colorA = "white",
  colorB = "orange",
}) => {
  return (
    <meshStandardNodeMaterial
      colorNode={mix(color(colorA), color(colorB), uv())}
    />
  );
};

Il exécutera les différents nœuds pour obtenir la couleur finale du matériau. Le nœud mix prend deux couleurs et un facteur (dans ce cas, les coordonnées UV) et renvoie une couleur qui est un mélange des deux couleurs basé sur le facteur.

C'est exactement la même chose que d'utiliser la fonction mix dans GLSL, mais maintenant nous pouvons l'utiliser dans une approche basée sur des nœuds. (Beaucoup plus lisible!)

WebGPU Plane with Mix

Nous pouvons maintenant voir les deux couleurs mélangées en fonction des coordonnées UV du plan.

Ce qui est incroyable, c'est que nous ne partons pas de zéro. Nous utilisons le MeshStandardNodeMaterial existant et lui ajoutons simplement nos nœuds personnalisés. Ce qui signifie que les ombres, les lumières et toutes les autres fonctionnalités du MeshStandardNodeMaterial sont toujours disponibles.

Déclarer des nœuds en ligne est acceptable pour une logique de nœud très simple, mais pour une logique plus complexe, je vous recommande de déclarer les nœuds (et plus tard les uniforms, et plus) dans un hook useMemo :

// ...
import { useMemo } from "react";

export const PracticeNodeMaterial = ({
  colorA = "white",
  colorB = "orange",
}) => {
  const { nodes } = useMemo(() => {
    return {
      nodes: {
        colorNode: mix(color(colorA), color(colorB), uv()),
      },
    };
  }, []);

  return <meshStandardNodeMaterial {...nodes} />;
};

Cela fait exactement la même chose qu'avant, mais maintenant nous pouvons ajouter plus de nœuds à l'objet nodes et les passer au meshStandardNodeMaterial de manière plus organisée/générique.

En changeant les props colorA et colorB, cela n'entraînera pas une recompilation du shader grâce au hook useMemo.

Ajoutons des contrôles pour changer les couleurs du matériau. Dans Experience.jsx :

// ...
import { useControls } from "leva";

export const Experience = () => {
  const { colorA, colorB } = useControls({
    colorA: { value: "skyblue" },
    colorB: { value: "blueviolet" },
  });
  return (
    <>
      {/* ... */}

      <mesh rotation-x={-Math.PI / 2}>
        <planeGeometry args={[2, 2, 200, 200]} />
        <PracticeNodeMaterial colorA={colorA} colorB={colorB} />
      </mesh>
    </>
  );
};

La couleur par défaut fonctionne correctement, mais la mise à jour des couleurs n'a aucun effet.

C'est parce que nous devons passer les couleurs en tant qu'uniforms au meshStandardNodeMaterial.

Uniformes

Pour déclarer des uniformes dans TSL, nous pouvons utiliser le nœud uniform du module three/tsl. Le nœud uniform prend une valeur en argument (elle peut être de différents types comme float, vec3, vec4, etc.) et renvoie un nœud uniforme qui peut être utilisé dans les différents nœuds tout en étant mis à jour depuis le code de notre composant.

Remplaçons les couleurs en dur par des uniformes dans PracticeNodeMaterial.jsx :

// ...
import { uniform } from "three/tsl";

export const PracticeNodeMaterial = ({
  colorA = "white",
  colorB = "orange",
}) => {
  const { nodes, uniforms } = useMemo(() => {
    const uniforms = {
      colorA: uniform(color(colorA)),
      colorB: uniform(color(colorB)),
    };

    return {
      nodes: {
        colorNode: mix(uniforms.colorA, uniforms.colorB, uv()),
      },
      uniforms,
    };
  }, []);

  return <meshStandardNodeMaterial {...nodes} />;
};

Nous déclarons un objet uniforms pour une meilleure organisation du code et nous utilisons les valeurs uniformes à la place de la valeur par défaut que nous avons obtenue lors de la création de nos nœuds.

En les renvoyant dans le useMemo, nous avons maintenant accès aux uniformes dans notre composant.

Dans un useFrame, nous pouvons mettre à jour les uniformes :

// ...
import { useFrame } from "@react-three/fiber";

export const PracticeNodeMaterial = ({
  colorA = "white",
  colorB = "orange",
}) => {
  // ...

  useFrame(() => {
    uniforms.colorA.value.set(colorA);
    uniforms.colorB.value.set(colorB);
  });

  return <meshStandardNodeMaterial {...nodes} />;
};

Utilisez la méthode value.set lorsque vous mettez à jour un uniforme d'objet. Par exemple, les uniformes color ou vec3. Pour les uniformes float, vous devez définir la valeur directement : uniforms.opacity.value = opacity;

Les couleurs se mettent maintenant à jour correctement en temps réel.

Avant d'en faire plus avec la couleur, voyons comment nous pouvons affecter la position des sommets de notre plan en utilisant le positionNode.

Position Node

Le nœud positionNode nous permet d'affecter la position des sommets de notre géométrie.

Three.js logoReact logo

React Three Fiber: The Ultimate Guide to 3D Web Development

✨ You have reached the end of the preview ✨

Go to the next level with Three.js and React Three Fiber!

Get full access to this lesson and the complete course when you enroll:

  • 🔓 Full lesson videos with no limits
  • 💻 Access to the final source code
  • 🎓 Course progress tracking & completion
  • 💬 Invite to our private Discord community
Unlock the Full Course – Just $85

One-time payment. Lifetime updates included.