Fundamentals
Core
Master
Shaders
Introduction aux Shaders
Il est enfin temps de plonger dans le monde des shaders. Ils sont essentiels pour créer toutes sortes d'effets visuels. Dans ce chapitre, nous allons apprendre ce que sont les shaders, ce que nous pouvons réaliser avec eux, et comment les utiliser dans React Three Fiber.
Prélude
Avant de commencer, je tiens à mentionner que les shaders peuvent prendre un certain temps avant que vous ne vous y habituiez. Ils fonctionnent différemment du reste du code que nous avons écrit jusqu'à présent. C'est une nouvelle façon de penser et de créer des effets visuels. Mais ne vous inquiétez pas, je vais vous guider à travers le processus. Nous commencerons par les bases, puis nous passerons progressivement à des sujets plus avancés.
Ne vous découragez pas si vous ne comprenez pas tout au début. C'est normal. C'est un nouveau concept, et il faut de la pratique pour s'y sentir à l'aise. Je vous recommande de prendre votre temps, d'expérimenter et de pratiquer, mais je vous le promets, cela en vaut la peine ! Les shaders sont incroyablement puissants et vous donneront le contrôle pour créer tout effet visuel que vous pouvez imaginer.
De plus, nous avons tous des styles d'apprentissage différents. Certaines personnes apprennent mieux en lisant, d'autres en regardant des vidéos, et d'autres encore en pratiquant. Pour ce sujet en particulier, il peut être très utile de croiser différentes sources. Je partagerai avec vous des ressources à la fin de ce chapitre pour consolider vos connaissances et aller au-delà de ce que nous couvrons ici.
J'espère ne pas vous avoir fait peur et que vous êtes excité d'apprendre les shaders. Allons-y !
Qu'est-ce qu'un Shader ?
Les shaders sont de petits programmes qui s'exécutent sur le GPU (Graphics Processing Unit). Ils sont écrits dans un langage appelé GLSL (OpenGL Shading Language), qui est similaire au C.
Ils sont utilisés pour positionner les vertices d'une mesh (Vertex Shader) et pour colorier chaque pixel des faces (Fragment Shader).
En fait, nous avons utilisé des shaders tout le long. Lorsque nous créons un material, nous utilisons un shader. Par exemple, lorsque nous créons un MeshBasicMaterial
, nous utilisons un shader qui colorie la mesh avec une seule couleur. Lorsque nous créons un MeshStandardMaterial
, nous utilisons un shader qui simule l'Ă©clairage, les ombres et les reflets.
Vertex Shader
Un vertex shader est un programme exécuté pour chaque vertex d'une géométrie. Sa responsabilité principale est de transformer les vertices de l'espace 3D (notre monde 3D) à l'espace 2D (notre écran ou viewport). Il réalise cette transformation en utilisant plusieurs matrices :
- View Matrix : Cette matrice représente la position et l'orientation de la caméra dans la scène. Elle transforme les vertices de l'espace monde à l'espace caméra.
- Projection Matrix : Cette matrice, soit perspective soit orthographique, convertit les vertices de l'espace caméra aux coordonnées de dispositifs normalisés (NDC), les préparant à la projection finale sur l'écran 2D.
- Model Matrix : Cette matrice encapsule la position, la rotation et l'échelle de chaque objet individuel dans la scène. Elle transforme les vertices de l'espace objet à l'espace monde.
De plus, le vertex shader intègre également la position originale du vertex et tous les attribus qui lui sont associés.
Pour chaque vertex de la géométrie, le vertex shader sera exécuté.
Enfin, la position transformée du vertex dans l'espace 2D est renvoyée via la variable prédéfinie gl_Position
. Après que tous les vertices soient transformés, le GPU interpole les valeurs entre eux pour générer les faces de la géométrie, qui sont ensuite rasterisées et rendues sur l'écran.
Fragment Shader
Un fragment shader, également connu sous le nom de pixel shader, est un programme exécuté pour chaque fragment (ou pixel) généré par le processus de rasterisation. Sa tâche principale est de déterminer la couleur finale de chaque pixel à l'écran.
Pour chaque fragment généré lors de la rasterisation, le fragment shader sera exécuté.
Le fragment shader reçoit des valeurs interpolées du vertex shader, telles que des couleurs, des coordonnées de texture, des normales, et tous les autres attributs associés aux sommets de la géométrie. Ces valeurs interpolées sont appelées varyings et fournissent des informations sur les propriétés de surface à chaque emplacement de fragment.
En plus des valeurs interpolées, le fragment shader peut également échantillonner des textures et accéder à des variables uniformes, qui sont constantes pour tous les fragments. Ces variables uniformes peuvent représenter des paramètres tels que les positions de lumière, les propriétés des matériaux, ou toute autre donnée nécessaire aux calculs de shading.
Nous reviendrons sur les attributs et uniforms plus tard dans cette leçon.
En utilisant les données d'entrée, le fragment shader effectue divers calculs pour déterminer la couleur finale du fragment. Cela peut impliquer des calculs d'éclairage complexes, du texture mapping, des effets de shading, ou tout autre effet visuel souhaité dans la scène.
Une fois le calcul de la couleur terminé, le fragment shader produit la couleur finale du fragment en utilisant la variable prédéfinie gl_FragColor
.
J'ai essayé autant que possible d'expliquer les shaders de manière simple et j'ai volontairement omis certains détails techniques, mais je comprends que cela peut encore être un peu abstrait. Créons un shader simple pour voir comment cela fonctionne en pratique.
Votre Premier Shader
Lançons le starter pack. Vous devriez voir ce frame avec un plan noir au centre de l'écran :
Ouvrez le fichier ShaderPlane.jsx
, il contient un mesh simple avec une géométrie de plan et un material basique. Nous allons remplacer ce material par un shader material personnalisé.
shaderMaterial
Pour créer un shader material, nous utilisons la fonction shaderMaterial
de la Drei library.
Elle prend 3 paramètres :
uniforms
: Un objet contenant les variables uniformes utilisées dans le shader. Laissez-le vide pour le moment.vertexShader
: Une chaîne de caractères contenant le code GLSL pour le vertex shader.fragmentShader
: Une chaîne de caractères contenant le code GLSL pour le fragment shader.
En haut de notre fichier, déclarons un nouveau shader material nommé MyShaderMaterial
:
import { shaderMaterial } from "@react-three/drei"; const MyShaderMaterial = shaderMaterial( {}, ` void main() { gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`, ` void main() { gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0); } ` );
Nous allons plonger dans le code du shader dans un instant.
Pour pouvoir l'utiliser de manière déclarative avec React Three Fiber, nous utilisons la méthode extend
:
import { extend } from "@react-three/fiber"; // ... extend({ MyShaderMaterial });
Nous pouvons maintenant remplacer le <meshBasicMaterial>
par notre nouveau shader material :
import { shaderMaterial } from "@react-three/drei"; import { extend } from "@react-three/fiber"; const MyShaderMaterial = shaderMaterial( {}, ` void main() { gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`, ` void main() { gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0); } ` ); extend({ MyShaderMaterial }); export const ShaderPlane = ({ ...props }) => { return ( <mesh {...props}> <planeGeometry args={[1, 1]} /> <myShaderMaterial /> </mesh> ); };
Vous devriez voir le même plan noir qu'avant. Nous n'avons encore rien changé, mais nous utilisons maintenant un shader material personnalisé.
Pour vérifier que cela fonctionne, changeons la couleur que nous retournons dans le fragment shader. Remplacez la ligne gl_FragColor
par la suivante :
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
gl_FragColor
est une variable prédéfinie qui représente la couleur du fragment. C'est un vec4
(un vecteur à 4 composants) qui représente les canaux rouge, vert, bleu, et alpha de la couleur. Chaque composant est un float entre 0 et 1.
En mettant le premier composant Ă 1.0
, nous réglons le canal rouge à sa valeur maximale, ce qui donnera une couleur rouge.
Vous devriez voir un plan rouge au centre de l'Ă©cran :
Félicitations ! Vous venez de créer votre premier shader material. C'est un shader simple, mais c'est un début.
Code de Shader
Avant de continuer, configurons notre environnement de développement pour écrire des shaders plus confortablement.
Vous avez deux options pour Ă©crire du code de shader :
- Inline : Vous pouvez Ă©crire le code du shader directement dans le fichier JavaScript.
- Externe : Vous pouvez écrire le code du shader dans un fichier séparé avec l'extension
.glsl
et l'importer dans votre fichier JavaScript.
Je préfère généralement l'approche inline à l'intérieur des fichiers de material appropriés, de cette manière, le code du shader est proche de la déclaration du material.
Mais pour rendre l'écriture et la lecture plus faciles, je recommande d'utiliser un surligneur de syntaxe pour GLSL. Vous pouvez utiliser l'extension Comment tagget templates pour Visual Studio Code. Elle surlignera le code GLSL à l'intérieur des chaînes de caractères du template.
Une fois installée, pour activer le surligneur de syntaxe, vous devez ajouter le commentaire suivant au début du code du shader :
const MyShaderMaterial = shaderMaterial( {}, /* glsl */ ` void main() { gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`, /* glsl */ ` void main() { gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); } ` );
Vous devriez voir le code GLSL surligné dans les chaînes de caractères du template :
Le vertex shader en haut a maintenant le surlignage syntaxique correct et est plus facile Ă lire.
C'est tout ce dont vous avez besoin pour du code de shader inline. Vous pouvez toujours décider d'utiliser un fichier externe si vous préférez garder votre code de shader séparé. Voyons comment faire.
Importation de fichiers GLSL
Tout d'abord, créez un nouveau dossier nommé shaders
dans le dossier src
. À l'intérieur de ce dossier, créez deux fichiers: myshader.vertex.glsl
et myshader.fragment.glsl
et copiez le code shader respectif dans chaque fichier.
myshader.vertex.glsl
:
void main() { gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }
myshader.fragment.glsl
:
void main() { gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); }
N'hésitez pas à utiliser la convention de nommage que vous préférez et à regrouper vos shaders dans des sous-dossiers si vous en avez beaucoup.
Ensuite, pour pouvoir importer ces fichiers dans notre fichier JavaScript, nous devons installer le plugin vite-plugin-glsl en tant que dépendance de développement :
yarn add vite-plugin-glsl --dev
Ensuite, dans votre fichier vite.config.js
, importez le plugin et ajoutez-le au tableau plugins
:
import react from "@vitejs/plugin-react"; import { defineConfig } from "vite"; import glsl from "vite-plugin-glsl"; // https://vitejs.dev/config/ export default defineConfig({ plugins: [react(), glsl()], });
Vous pouvez maintenant importer les fichiers GLSL dans votre fichier JavaScript et les utiliser comme code shader:
import myShaderFragment from "./shaders/myshader.fragment.glsl"; import myShaderVertex from "./shaders/myshader.vertex.glsl"; const MyShaderMaterial = shaderMaterial({}, myShaderVertex, myShaderFragment);
Maintenant que nous avons une manière confortable d'écrire et d'importer du code shader, nous pouvons commencer à explorer les différentes parties du code shader.
GLSL
Le code du shader est Ă©crit en GLSL (OpenGL Shading Language). C'est un langage ressemblant au C, voyons les bases.
Types
GLSL possède plusieurs types, mais les plus courants sont :
- bool : Une valeur booléenne (
true
oufalse
). - int : Un nombre entier.
- float : Un nombre Ă virgule flottante.
- vectors : Une collection de nombres.
vec2
est une collection de 2 nombres Ă virgule flottante (x
ety
),vec3
est une collection de 3 nombres Ă virgule flottante (x
,y
etz
), etvec4
est une collection de 4 nombres Ă virgule flottante (x
,y
,z
etw
). Au lieu d'utiliserx
,y
,z
etw
, vous pouvez aussi utiliserr
,g
,b
eta
pour les couleurs, ils sont interchangeables. - matrices : Une collection de vecteurs. Par exemple,
mat2
est une collection de 2 vecteurs,mat3
est une collection de 3 vecteurs, etmat4
est une collection de 4 vecteurs.
Swizzling et manipulations
Vous pouvez accéder aux composants d'un vecteur en utilisant le swizzling. Par exemple, vous pouvez créer un nouveau vecteur en utilisant les composants d'un autre vecteur :
vec3 a = vec3(1.0, 2.0, 3.0); vec2 b = a.xy;
Dans cet exemple, b
sera un vecteur avec les composants x
et y
de a
.
Vous pouvez Ă©galement utiliser le swizzling pour changer l'ordre des composants :
vec3 a = vec3(1.0, 2.0, 3.0); vec3 b = a.zyx;
Dans cet exemple, b
sera Ă©gal Ă vec3(3.0, 2.0, 1.0)
.
Pour créer un nouveau vecteur avec tous les mêmes composants, vous pouvez utiliser le constructeur :
vec3 a = vec3(1.0);
Dans cet exemple, a
sera Ă©gal Ă vec3(1.0, 1.0, 1.0)
.
Opérateurs
GLSL possède les opérateurs arithmétiques classiques : +
, -
, *
, /
, +=
, /=
, *=
et les opérateurs de comparaison classiques : ==
, !=
, >
, <
, >=
, <=
.
Ils doivent être utilisés avec les types corrects. Par exemple, vous ne pouvez pas ajouter un entier à un float, vous devez d'abord convertir l'entier en float :
int a = 1; float b = 2.0; float c = float(a) + b;
Vous pouvez également effectuer des opérations sur les vecteurs et les matrices :
vec3 a = vec3(1.0, 2.0, 3.0); vec3 b = vec3(4.0, 5.0, 6.0); vec3 c = a + b;
Ce qui Ă©quivaut Ă :
vec3 c = vec3(a.x + b.x, a.y + b.y, a.z + b.z);
Fonctions
Le point d'entrée des vertex et fragment shaders est la fonction main
. C'est la fonction qui sera exécutée lorsque le shader sera appelé.
void main() { // Votre code ici }
void est le type de retour de la fonction. Cela signifie que la fonction ne retourne rien.
Vous pouvez également définir vos propres fonctions :
float add(float a, float b) { return a + b; }
Vous pouvez ensuite appeler cette fonction dans la fonction main
:
void main() { float result = add(1.0, 2.0); // ... }
GLSL fournit de nombreuses fonctions intégrées pour des opérations courantes comme sin
, cos
, max
, min
, abs
, round
, floor
, ceil
, et bien d'autres utiles comme mix
, step
, length
, distance
, et plus encore.
Nous découvrirons les fonctions essentielles et nous les pratiquerons dans la prochaine leçon.
Boucles et Conditions
GLSL supporte les boucles for
et les instructions if
. Elles fonctionnent de manière similaire à JavaScript :
for (int i = 0; i < 10; i++) { // Votre code ici } if (condition) { // Votre code ici } else { // Votre code ici }
Journalisation / DĂ©bogage
Comme les programmes de shader s'exécutent en parallèle pour chaque vertex et fragment, il n'est pas possible d'utiliser console.log
pour déboguer votre code ni d'ajouter des points d'arrêt. C'est ce qui rend difficile le débogage des shaders.
Une manière courante de déboguer les shaders est d'utiliser le gl_FragColor
pour visualiser les valeurs de vos variables.
Erreurs de compilation
Si vous faites une erreur dans votre code de shader, vous verrez une erreur de compilation dans la console. Elle vous indiquera la ligne et le type d'erreur. Ce n'est pas toujours facile à comprendre, mais c'est un bon moyen de savoir où chercher le problème.
Supprimons le canal alpha du gl_FragColor
et voyons ce qui se passe :
void main() { gl_FragColor = vec4(1.0, 0.0, 0.0); }
Vous devriez voir une erreur de compilation dans la console :
Nous indiquant que le gl_FragColor
attend 4 composants, mais nous n'en avons fourni que 3.
N'oubliez pas de restaurer le canal alpha Ă 1.0
pour supprimer l'erreur.
Uniforms
Pour transmettre des données du code JavaScript au shader, nous utilisons les uniforms. Ils sont constants pour tous les vertices et fragments.
Le projectionMatrix
, modelViewMatrix
et la position
sont des exemples d'uniforms intégrés qui sont automatiquement transmis au shader.
Créons un uniform personnalisé pour transmettre une couleur au shader. Nous l'utiliserons pour colorier le plane. Nous allons l'appeler uColor
. C'est une bonne pratique de préfixer le nom de l'uniform avec un u
pour indiquer clairement qu'il s'agit d'un uniform dans notre code.
D'abord, déclarons-le dans l'objet uniforms de shaderMaterial
:
import { Color } from "three"; // ... const MyShaderMaterial = shaderMaterial( { uColor: new Color("pink"), } // ... ); // ...
Ensuite, nous pouvons l'utiliser dans le fragment shader :
uniform vec3 uColor; void main() { gl_FragColor = vec4(uColor, 1.0); }
Vous devriez voir le plane colorié en rose :
Ici, la couleur rose est la valeur par défaut de l'uniform. Nous pouvons la changer directement sur le material :
<MyShaderMaterial uColor={"lightblue"} />
Le plane est maintenant colorié en bleu clair.
Les shaders de vertex et de fragment peuvent accéder aux uniforms. Ajoutons le temps comme deuxième uniform au vertex shader pour déplacer le plane de haut en bas :
End of lesson preview
To get access to the entire lesson, you need to purchase the course.