Moteur VFX
Jusqu'à présent, nous avons créé des composants personnalisés pour créer des particules dans nos scènes 3D. La plupart du temps, nous voulons faire presque la même chose : émettre des particules à partir d'un point dans l'espace et les animer au fil du temps. (Couleur, taille, position, etc.)
Au lieu de dupliquer le même code encore et encore, nous pouvons créer un moteur VFX relativement générique qui peut être utilisé pour créer différents types d'effets de particules.
Cela présente de nombreux avantages :
- Réutilisabilité : Vous pouvez utiliser le même moteur pour créer différents types d'effets de particules dans vos projets.
- Performance : Le moteur peut être optimisé pour gérer efficacement un grand nombre de particules et pour fusionner plusieurs systèmes de particules en un seul.
- Flexibilité : Vous pouvez facilement personnaliser le comportement des particules en modifiant les paramètres du moteur.
- Facilité d'utilisation : Vous pouvez créer des effets de particules complexes avec seulement quelques lignes de code.
- Éviter la duplication de code : Vous n'avez pas à écrire le même code plusieurs fois.
Nous utiliserons ce moteur VFX dans les prochaines leçons pour créer divers effets. Bien que vous puissiez passer cette leçon et utiliser directement le moteur, comprendre comment il fonctionne vous aidera à maîtriser plus en profondeur la performance et la flexibilité dans vos projets 3D.
Prêt à construire votre moteur VFX ? Commençons !
Particules GPU
Nous avons vu dans les leçons précédentes comment nous pouvons utiliser le composant <Instances />
de drei pour créer des particules contrôlées dans nos scènes 3D.
Mais cette approche a une principale limitation : le nombre de particules que nous pouvons gérer est limité par le CPU. Plus nous avons de particules, plus le CPU doit les gérer, ce qui peut entraîner des problèmes de performance.
Cela est dĂ» au fait que sous le capot, le composant <Instances />
effectue des calculs pour obtenir la position, la couleur et la taille de chaque <Instance />
dans sa boucle useFrame
. Vous pouvez voir le code ici.
Pour notre Moteur VFX, nous voulons pouvoir générer bien plus de particules que ce que nous pouvons gérer avec le composant <Instances />
. Nous utiliserons le GPU pour gérer la position, la couleur et la taille des particules. Cela nous permettra de gérer des centaines de milliers de particules (millions ? 👀) sans aucun problème de performance.
Maillage Instancié
Bien que nous puissions utiliser Sprite ou Points pour créer des particules, nous allons utiliser InstancedMesh.
Cela nous permet de rendre non seulement des formes simples comme des points ou des sprites, mais aussi des formes 3D comme des cubes, des sphères et des géométries personnalisées.
Créons un composant dans un nouveau dossier vfxs
appelé VFXParticles.jsx
 :
import { useMemo, useRef } from "react"; import { PlaneGeometry } from "three"; export const VFXParticles = ({ settings = {} }) => { const { nbParticles = 1000 } = settings; const mesh = useRef(); const defaultGeometry = useMemo(() => new PlaneGeometry(0.5, 0.5), []); return ( <> <instancedMesh args={[defaultGeometry, null, nbParticles]} ref={mesh}> <meshBasicMaterial color="orange" /> </instancedMesh> </> ); };
Nous créons la géométrie qui sera utilisée pour chaque particule. Dans ce cas, nous utilisons une simple géométrie plane de taille 0.5
sur les deux axes. Plus tard, nous ajouterons une prop pour passer n'importe quelle géométrie que nous souhaitons.
Le composant instancedMesh
prend trois arguments :
- La géométrie des particules.
- Le material des particules. Nous avons passé
null
pour le définir de manière déclarative à l'intérieur du composant. - Le nombre d'instances que le composant pourra gérer. Pour nous, il représente le nombre maximum de particules pouvant être affichées en même temps.
Remplaçons le cube orange par notre composant VFXParticles
dans le fichier Experience.jsx
:
// ... import { VFXParticles } from "./vfxs/VFXParticles"; export const Experience = () => { return ( <> {/* ... */} <VFXParticles /> </> ); };
Vous pouvez voir une particule orange au milieu de la scène. C'est notre composant VFXParticles
.
Notre nombre de particules est défini sur 1000
mais nous ne pouvons en voir qu'une. C'est parce qu'elles sont toutes rendues Ă la mĂŞme position (0, 0, 0)
. Changeons cela.
Matrice d'Instances
Le maillage instancié utilise une matrice pour définir la position, la rotation et l'échelle de chaque instance. En mettant à jour la propriété instanceMatrix de notre maillage, nous pouvons déplacer, faire pivoter et mettre à l'échelle chaque particule individuellement.
Pour chaque instance, la matrice est une matrice 4x4 qui représente la transformation de la particule. La classe Matrix4 de Three.js nous permet de composer
et décomposer
la matrice pour définir/obtenir la position, la rotation et l'échelle de la particule d'une manière plus compréhensible.
En haut de la déclaration de VFXParticles
, déclarons quelques variables temporaires pour manipuler les particules sans recréer trop souvent des Vectors et des Matrices :
// ... import { Euler, Matrix4, PlaneGeometry, Quaternion, Vector3 } from "three"; const tmpPosition = new Vector3(); const tmpRotationEuler = new Euler(); const tmpRotation = new Quaternion(); const tmpScale = new Vector3(1, 1, 1); const tmpMatrix = new Matrix4();
Créons maintenant une fonction emit
pour configurer nos particules :
// ... import { useEffect } from "react"; // ... export const VFXParticles = ({ settings = {} }) => { // ... const emit = (count) => { for (let i = 0; i < count; i++) { const position = [ randFloatSpread(5), randFloatSpread(5), randFloatSpread(5), ]; const scale = [ randFloatSpread(1), randFloatSpread(1), randFloatSpread(1), ]; const rotation = [ randFloatSpread(Math.PI), randFloatSpread(Math.PI), randFloatSpread(Math.PI), ]; tmpPosition.set(...position); tmpRotationEuler.set(...rotation); tmpRotation.setFromEuler(tmpRotationEuler); tmpScale.set(...scale); tmpMatrix.compose(tmpPosition, tmpRotation, tmpScale); mesh.current.setMatrixAt(i, tmpMatrix); } }; useEffect(() => { emit(nbParticles); }, []); // ... };
La fonction emit
boucle sur le nombre de particules que nous voulons émettre et définit une position, une rotation et une échelle aléatoires pour chaque particule. Nous composons ensuite la matrice avec ces valeurs et l'attribuons à l'instance à l'index actuel.
Vous pouvez voir des particules aléatoires dans la scène. Chaque particule a une position, une rotation et une échelle aléatoires.
Pour animer nos particules, nous définirons des attributs tels que lifetime (durée de vie), speed (vitesse), direction afin que le calcul puisse être effectué sur le GPU.
Avant de faire cela, nous devons passer à un shader material personnalisé pour gérer ces attributs, car nous n'avons pas accès et contrôle sur les attributs du meshBasicMaterial
.
Matériau Particules
Notre premier objectif sera de ne constater aucun changement entre le meshBasicMaterial
et notre nouveau shaderMaterial
. Nous allons créer un matériau shader simple qui affichera les particules de la même manière que le meshBasicMaterial
le fait actuellement.
Dans le composant VFXParticles
, créons un nouveau matériau shader :
// ... import { shaderMaterial } from "@react-three/drei"; import { extend } from "@react-three/fiber"; import { Color } from "three"; const ParticlesMaterial = shaderMaterial( { color: new Color("white"), }, /* glsl */ ` varying vec2 vUv; void main() { gl_Position = projectionMatrix * modelViewMatrix * vec4(instanceMatrix * vec4(position, 1.0)); vUv = uv; } `, /* glsl */ ` uniform vec3 color; varying vec2 vUv; void main() { gl_FragColor = vec4(color, 1.0); }` ); extend({ ParticlesMaterial });
Ceci est un matériau shader très simple qui prend une uniforme color
et affiche les particules avec cette couleur. La seule nouvelle chose ici est le instanceMatrix
que nous utilisons pour obtenir la position
, la rotation
, et l'échelle
de chaque particule.
Notez que nous n'avions pas besoin de déclarer l'attribut
instanceMatrix
car c'est l'un des attributs intégrés duWebGLProgram
lors de l'utilisation de l'instancing. Plus d'informations peuvent être trouvées ici.
Remplaçons le meshBasicMaterial
par notre nouveau ParticlesMaterial
:
<instancedMesh args={[defaultGeometry, null, nbParticles]} ref={mesh}> <particlesMaterial color="orange" /> </instancedMesh>
Parfait ! Notre position
, rotation
, et échelle
fonctionnent toujours comme prévu. Les particules sont rendues avec une couleur orange légèrement différente. C'est parce que nous ne prenons pas en compte l'environnement dans notre matériau shader. Pour simplifier les choses, nous allons le laisser ainsi.
Nous sommes maintenant prêts à ajouter des attributs personnalisés à nos particules pour les animer.
Attributs de Buffer Instanciés
Jusqu'à présent, nous avons uniquement utilisé l'attribut instanceMatrix
, nous allons maintenant ajouter des attributs personnalisés pour avoir plus de contrôle sur chaque particule.
Pour cela, nous allons utiliser le InstancedBufferAttribute de Three.js.
Nous ajouterons les attributs suivants Ă nos particules :
instanceColor
: Un vector3 représentant la couleur de la particule.instanceColorEnd
: Un vector3 représentant la couleur que prendra la particule au fil du temps.instanceDirection
: Un vector3 représentant la direction dans laquelle la particule se déplacera.instanceSpeed
: Un float pour définir la vitesse à laquelle la particule se déplacera dans sa direction.instanceRotationSpeed
: Un vector3 pour déterminer la vitesse de rotation de la particule par axe.instanceLifetime
: Un vector2 pour définir la durée de vie de la particule. La première valeur (x
) est le temps de début, et la seconde valeur (y
) est la durée de vie/la durée. Combiné avec un uniforme de temps, nous pouvons calculer l'âge, le progrès, et si une particule est vivante ou morte.
Créons les différents buffers pour nos attributs :
// ... import { useState } from "react"; // ... export const VFXParticles = ({ settings = {} }) => { // ... const [attributeArrays] = useState({ instanceColor: new Float32Array(nbParticles * 3), instanceColorEnd: new Float32Array(nbParticles * 3), instanceDirection: new Float32Array(nbParticles * 3), instanceLifetime: new Float32Array(nbParticles * 2), instanceSpeed: new Float32Array(nbParticles * 1), instanceRotationSpeed: new Float32Array(nbParticles * 3), }); // ... }; // ...
J'utilise un useState
pour créer les différents buffers pour nos attributs afin d'éviter de les recréer à chaque rendu. J'ai choisi de ne pas utiliser le hook useMemo
car changer le nombre maximal de particules pendant le cycle de vie du composant n'est pas quelque chose que nous souhaitons gérer.
Le Float32Array
est utilisé pour stocker les valeurs des attributs. Nous multiplions le nombre de particules par le nombre de composants de l'attribut pour obtenir le nombre total de valeurs dans le tableau.
Dans l'attribut instanceColor
, les 3 premières valeurs représenteront la couleur de la première particule, les 3 valeurs suivantes représenteront la couleur de la deuxième particule, et ainsi de suite.
Commençons par nous familiariser avec le InstancedBufferAttribute
et comment l'utiliser. Pour cela, nous allons implémenter l'attribut instanceColor
:
// ... import { DynamicDrawUsage } from "three"; export const VFXParticles = ({ settings = {} }) => { // ... return ( <> <instancedMesh args={[defaultGeometry, null, nbParticles]} ref={mesh}> <particlesMaterial color="orange" /> <instancedBufferAttribute attach={"geometry-attributes-instanceColor"} args={[attributeArrays.instanceColor]} itemSize={3} count={nbParticles} usage={DynamicDrawUsage} /> </instancedMesh> </> ); };
Dans notre <instancedMesh />
, nous ajoutons un composant <instancedBufferAttribute />
pour définir l'attribut instanceColor
. Nous le joignons Ă l'attribut geometry-attributes-instanceColor
du mesh. Nous passons le tableau attributeArrays.instanceColor
comme source de données, configurons le itemSize
Ă 3
car nous avons un vector3, et le count
Ă nbParticles
.
L'attribut usage
est configuré à DynamicDrawUsage
pour indiquer au rendu que les données seront fréquemment mises à jour. Les autres valeurs possibles et plus de détails peuvent être trouvés ici.
Nous ne les mettrons pas à jour à chaque frame, mais chaque fois que nous émettons de nouvelles particules, les données seront mises à jour. Suffisamment pour être considéré comme DynamicDrawUsage
.
Parfait, créons une variable fictive tmpColor
en haut de notre fichier pour manipuler les couleurs des particules :
// ... const tmpColor = new Color();
Mettons maintenant Ă jour la fonction emit
pour définir l'attribut instanceColor
:
const emit = (count) => { const instanceColor = mesh.current.geometry.getAttribute("instanceColor"); for (let i = 0; i < count; i++) { // ... tmpColor.setRGB(Math.random(), Math.random(), Math.random()); instanceColor.set([tmpColor.r, tmpColor.g, tmpColor.b], i * 3); } };
Nous commençons par obtenir l'attribut instanceColor
de la géométrie du mesh. Nous parcourons ensuite le nombre de particules que nous voulons émettre et définissons une couleur aléatoire pour chaque particule.
Mettons Ă jour le particlesMaterial
pour utiliser l'attribut instanceColor
au lieu de l'uniforme de couleur :
const ParticlesMaterial = shaderMaterial( { // color: new Color("white"), }, /* glsl */ ` varying vec2 vUv; varying vec3 vColor; attribute vec3 instanceColor; void main() { gl_Position = projectionMatrix * modelViewMatrix * vec4(instanceMatrix * vec4(position, 1.0)); vUv = uv; vColor = instanceColor; } `, /* glsl */ ` varying vec3 vColor; varying vec2 vUv; void main() { gl_FragColor = vec4(vColor, 1.0); }` ); // ...
Nous avons ajouté un attribute vec3 instanceColor;
au vertex shader et configuré le vColor
variant pour passer la couleur au fragment shader. Nous définissons ensuite le gl_FragColor
au vColor
pour rendre les particules avec leur couleur.
Nous avons réussi à définir une couleur aléatoire pour chaque particule. Les particules sont rendues avec leur couleur.
Parfait, ajoutons les autres attributs Ă nos particules. D'abord, mettons Ă jour notre fonction emit
pour définir les attributs instanceColorEnd
, instanceDirection
, instanceLifetime
, instanceSpeed
, et instanceRotationSpeed
avec des valeurs aléatoires :
const emit = (count) => { const instanceColor = mesh.current.geometry.getAttribute("instanceColor"); const instanceColorEnd = mesh.current.geometry.getAttribute("instanceColorEnd"); const instanceDirection = mesh.current.geometry.getAttribute("instanceDirection"); const instanceLifetime = mesh.current.geometry.getAttribute("instanceLifetime"); const instanceSpeed = mesh.current.geometry.getAttribute("instanceSpeed"); const instanceRotationSpeed = mesh.current.geometry.getAttribute( "instanceRotationSpeed" ); for (let i = 0; i < count; i++) { // ... tmpColor.setRGB(Math.random(), Math.random(), Math.random()); instanceColor.set([tmpColor.r, tmpColor.g, tmpColor.b], i * 3); tmpColor.setRGB(Math.random(), Math.random(), Math.random()); instanceColorEnd.set([tmpColor.r, tmpColor.g, tmpColor.b], i * 3); const direction = [ randFloatSpread(1), randFloatSpread(1), randFloatSpread(1), ]; instanceDirection.set(direction, i * 3); const lifetime = [randFloat(0, 5), randFloat(0.1, 5)]; instanceLifetime.set(lifetime, i * 2); const speed = randFloat(5, 20); instanceSpeed.set([speed], i); const rotationSpeed = [ randFloatSpread(1), randFloatSpread(1), randFloatSpread(1), ]; instanceRotationSpeed.set(rotationSpeed, i * 3); } };
Et créons les composants instancedBufferAttribute
pour chaque attribut :
<instancedMesh args={[defaultGeometry, null, nbParticles]} ref={mesh}> <particlesMaterial color="orange" /> <instancedBufferAttribute attach={"geometry-attributes-instanceColor"} args={[attributeArrays.instanceColor]} itemSize={3} count={nbParticles} usage={DynamicDrawUsage} /> <instancedBufferAttribute attach={"geometry-attributes-instanceColorEnd"} args={[attributeArrays.instanceColorEnd]} itemSize={3} count={nbParticles} usage={DynamicDrawUsage} /> <instancedBufferAttribute attach={"geometry-attributes-instanceDirection"} args={[attributeArrays.instanceDirection]} itemSize={3} count={nbParticles} usage={DynamicDrawUsage} /> <instancedBufferAttribute attach={"geometry-attributes-instanceLifetime"} args={[attributeArrays.instanceLifetime]} itemSize={2} count={nbParticles} usage={DynamicDrawUsage} /> <instancedBufferAttribute attach={"geometry-attributes-instanceSpeed"} args={[attributeArrays.instanceSpeed]} itemSize={1} count={nbParticles} usage={DynamicDrawUsage} /> <instancedBufferAttribute attach={"geometry-attributes-instanceRotationSpeed"} args={[attributeArrays.instanceRotationSpeed]} itemSize={3} count={nbParticles} usage={DynamicDrawUsage} /> </instancedMesh>
Il est maintenant temps d'ajouter de la vie à nos particules en implémentant leur mouvement, leur couleur et leur logique de durée de vie.
Durée de vie des particules
Pour calculer le comportement de nos particules, nous devons passer le temps écoulé à notre shader. Nous utiliserons la propriété uniforms
du shaderMaterial
pour lui passer le temps.
Mettons Ă jour notre ParticlesMaterial
pour ajouter une uniforme uTime
:
const ParticlesMaterial = shaderMaterial( { uTime: 0, }, /* glsl */ ` uniform float uTime; // ... `, /* glsl */ ` // ... ` );
Et dans une boucle useFrame
, nous mettrons Ă jour l'uniforme uTime
:
// ... import { useFrame } from "@react-three/fiber"; // ... export const VFXParticles = ({ settings = {} }) => { // ... useFrame(({ clock }) => { if (!mesh.current) { return; } mesh.current.material.uniforms.uTime.value = clock.elapsedTime; }); // ... };
Dans le vertex shader, nous allons calculer l'âge et le progrès de chaque particule en fonction de l'uniforme uTime
et de l'attribut instanceLifetime
. Nous transmettrons le progrès dans le fragment shader pour animer les particules à l'aide d'un varying nommé vProgress
.
uniform float uTime; varying vec2 vUv; varying vec3 vColor; varying vec3 vColorEnd; varying float vProgress; attribute float instanceSpeed; attribute vec3 instanceRotationSpeed; attribute vec3 instanceDirection; attribute vec3 instanceColor; attribute vec3 instanceColorEnd; attribute vec2 instanceLifetime; // x: startTime, y: duration void main() { float startTime = instanceLifetime.x; float duration = instanceLifetime.y; float age = uTime - startTime; vProgress = age / duration; gl_Position = projectionMatrix * modelViewMatrix * vec4(instanceMatrix * vec4(position, 1.0)); vUv = uv; vColor = instanceColor; vColorEnd = instanceColorEnd; }
L'âge est calculé en soustrayant le startTime
de uTime
. Le progrès est ensuite calculé en divisant l'âge par la duration
.
Maintenant, dans le fragment shader, nous allons interpoler la couleur des particules entre instanceColor
et instanceColorEnd
en fonction du progrès :
varying vec3 vColor; varying vec3 vColorEnd; varying float vProgress; varying vec2 vUv; void main() { vec3 finalColor = mix(vColor, vColorEnd, vProgress); gl_FragColor = vec4(finalColor, 1.0); }
Nous pouvons voir les particules changer de couleur au fil du temps, mais nous faisons face à un problème. Toutes les particules sont visibles au début alors que leur temps de départ est aléatoire. Nous devons cacher les particules qui ne sont pas encore nées.
Pour empêcher le rendu des particules non nées et mortes, nous utiliserons le mot-clé discard
dans le fragment shader :
// ... void main() { if (vProgress < 0.0 || vProgress > 1.0) { discard; } // ... }
Le mot-clé discard
indique au renderer de supprimer le fragment actuel et de ne pas le rendre.
Parfait, nos particules naissent maintenant, vivent et meurent au fil du temps. Nous pouvons maintenant ajouter la logique de mouvement et de rotation.
Mouvement des particules
En utilisant la direction, la vitesse et l'âge des particules, nous pouvons calculer leur position au fil du temps.
Dans le vertex shader, ajustons le gl_Position
pour prendre en compte la direction et la vitesse des particules.
Tout d'abord, nous normalisons la direction pour éviter que les particules se déplacent plus vite lorsque la direction n'est pas un vecteur unitaire :
vec3 normalizedDirection = length(instanceDirection) > 0.0 ? normalize(instanceDirection) : vec3(0.0);
Ensuite, nous calculons le offset des particules basé sur la vitesse et l'âge :
vec3 offset = normalizedDirection * age * instanceSpeed;
Obtenons la position de l'instance :
vec4 startPosition = modelMatrix * instanceMatrix * vec4(position, 1.0); vec3 instancePosition = startPosition.xyz;
Et appliquons l'offset :
vec3 finalPosition = instancePosition + offset;
Enfin, nous obtenons la position en vue modèle mvPosition
en appliquant la modelViewMatrix Ă la finalPosition pour transformer la position en espace monde :
vec4 mvPosition = modelViewMatrix * vec4(finalPosition, 1.0);
Et appliquons la projectionMatrix pour transformer la position du monde en espace caméra :
gl_Position = projectionMatrix * mvPosition;
Voici notre vertex shader complet jusqu'à présent :
uniform float uTime; varying vec2 vUv; varying vec3 vColor; varying vec3 vColorEnd; varying float vProgress; attribute float instanceSpeed; attribute vec3 instanceRotationSpeed; attribute vec3 instanceDirection; attribute vec3 instanceColor; attribute vec3 instanceColorEnd; attribute vec2 instanceLifetime; // x: startTime, y: duration void main() { float startTime = instanceLifetime.x; float duration = instanceLifetime.y; float age = uTime - startTime; vProgress = age / duration; vec3 normalizedDirection = length(instanceDirection) > 0.0 ? normalize(instanceDirection) : vec3(0.0); vec3 offset = normalizedDirection * age * instanceSpeed; vec4 startPosition = modelMatrix * instanceMatrix * vec4(position, 1.0); vec3 instancePosition = startPosition.xyz; vec3 finalPosition = instancePosition + offset; vec4 mvPosition = modelViewMatrix * vec4(finalPosition, 1.0); gl_Position = projectionMatrix * mvPosition; vUv = uv; vColor = instanceColor; vColorEnd = instanceColorEnd; }
Les particules se déplacent maintenant dans différentes directions à différentes vitesses. C'est chaotique, mais c'est à cause de nos valeurs aléatoires.
Remédions à cela en ajustant nos valeurs aléatoires dans la fonction emit
pour avoir une vue plus claire du mouvement des particules :
for (let i = 0; i < count; i++) { const position = [ randFloatSpread(0.1), randFloatSpread(0.1), randFloatSpread(0.1), ]; const scale = [randFloatSpread(1), randFloatSpread(1), randFloatSpread(1)]; const rotation = [ randFloatSpread(Math.PI), randFloatSpread(Math.PI), randFloatSpread(Math.PI), ]; tmpPosition.set(...position); tmpRotationEuler.set(...rotation); tmpRotation.setFromEuler(tmpRotationEuler); tmpScale.set(...scale); tmpMatrix.compose(tmpPosition, tmpRotation, tmpScale); mesh.current.setMatrixAt(i, tmpMatrix); tmpColor.setRGB(1, 1, 1); instanceColor.set([tmpColor.r, tmpColor.g, tmpColor.b], i * 3); tmpColor.setRGB(0, 0, 0); instanceColorEnd.set([tmpColor.r, tmpColor.g, tmpColor.b], i * 3); const direction = [randFloatSpread(0.5), 1, randFloatSpread(0.5)]; instanceDirection.set(direction, i * 3); const lifetime = [randFloat(0, 5), randFloat(0.1, 5)]; instanceLifetime.set(lifetime, i * 2); const speed = randFloat(1, 5); instanceSpeed.set([speed], i); const rotationSpeed = [ randFloatSpread(1), randFloatSpread(1), randFloatSpread(1), ]; instanceRotationSpeed.set(rotationSpeed, i * 3); }
Ça commence à prendre forme !
Nous ajouterons plus tard des contrĂ´les d'interface utilisateur simples pour ajuster les variables. Maintenant, finalisons les particules en ajoutant la logique de rotation.
Bien que nous ayons séparé la direction et la vitesse pour le mouvement, pour la rotation, nous utiliserons un attribut instanceRotationSpeed
unique pour définir la vitesse de rotation par axe.
Dans le vertex shader, nous pouvons calculer la rotation de la particule en fonction de la vitesse de rotation et de l'âge :
vec3 rotationSpeed = instanceRotationSpeed * age;
Ensuite, pour pouvoir appliquer cette "rotation de décalage" à la particule, nous devons la convertir en une matrice de rotation :
mat4 rotX = rotationX(rotationSpeed.x); mat4 rotY = rotationY(rotationSpeed.y); mat4 rotZ = rotationZ(rotationSpeed.z); mat4 rotationMatrix = rotZ * rotY * rotX;
rotationX
, rotationY
, et rotationZ
sont des fonctions qui renvoient une matrice de rotation autour des axes X, Y, et Z respectivement. Nous les définirons dans la fonction main
dans le vertex shader :
mat4 rotationX(float angle) { float s = sin(angle); float c = cos(angle); return mat4( 1, 0, 0, 0, 0, c, -s, 0, 0, s, c, 0, 0, 0, 0, 1 ); } mat4 rotationY(float angle) { float s = sin(angle); float c = cos(angle); return mat4( c, 0, s, 0, 0, 1, 0, 0, -s, 0, c, 0, 0, 0, 0, 1 ); } mat4 rotationZ(float angle) { float s = sin(angle); float c = cos(angle); return mat4( c, -s, 0, 0, s, c, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ); }
Pour en savoir plus sur les matrices de rotation, vous pouvez consulter cet article Wikipédia, l'incroyable Game Math Explained Simply par Simon Dev, ou la section Matrice du Livre des Shaders.
Enfin, nous pouvons appliquer la matrice de rotation à la position de départ de la particule :
vec4 startPosition = modelMatrix * instanceMatrix * rotationMatrix * vec4(position, 1.0);
Essayons-le :
Les particules se déplacent désormais, changent de couleur et tournent ! ✨
Parfait, nous avons une base solide pour notre moteur VFX. Avant d'ajouter plus de fonctionnalités et de contrôles, préparons une deuxième partie importante du moteur : l'émetteur.
Émetteurs
Dans les exemples et tutoriels, c'est souvent une partie négligée du système de particules. Mais c'est une partie cruciale pour intégrer les particules dans vos projets de manière simple et efficace :
- Simplement parce que votre composant
<VFXParticles />
sera au sommet de votre hiérarchie et votre émetteur pourra les faire apparaître depuis n'importe quel sous-composant de votre scène. Ce qui permet de les faire apparaître facilement depuis un point spécifique, de les attacher à un objet en mouvement ou à un os en mouvement. - Efficacement parce qu'au lieu de recréer des meshes instanciées, de compiler des shader materials, et de définir des attributs chaque fois que vous voulez faire apparaître des particules, vous pouvez réutiliser le même composant VFXParticles et simplement appeler une fonction pour faire apparaître des particules avec les paramètres souhaités.
useVFX
Nous souhaitons pouvoir appeler la fonction emit
de notre composant VFXParticles
depuis n'importe où dans notre projet. Pour cela, nous allons créer un hook personnalisé appelé useVFX
qui s'occupera de l'enregistrement et du désenregistrement des émetteurs du composant VFXParticles.
Nous allons utiliser Zustand car c'est un moyen simple et efficace de gérer l'état global dans React avec de bonnes performances.
Ajoutons-le Ă notre projet :
yarn add zustand
Dans notre dossier vfxs
, créons un fichier VFXStore.js
:
import { create } from "zustand"; export const useVFX = create((set, get) => ({ emitters: {}, registerEmitter: (name, emitter) => { if (get().emitters[name]) { console.warn(`Emitter ${name} already exists`); return; } set((state) => { state.emitters[name] = emitter; return state; }); }, unregisterEmitter: (name) => { set((state) => { delete state.emitters[name]; return state; }); }, emit: (name, ...params) => { const emitter = get().emitters[name]; if (!emitter) { console.warn(`Emitter ${name} not found`); return; } emitter(...params); }, }));
Ce qu'il contient :
- emitters : Un objet qui stockera tous les émetteurs de nos composants
VFXParticles
. - registerEmitter : Une fonction pour enregistrer un émetteur avec un nom donné.
- unregisterEmitter : Une fonction pour désenregistrer un émetteur avec un nom donné.
- emit : Une fonction pour appeler l'émetteur avec un nom et des paramètres donnés depuis n'importe où dans notre projet.
Branchons-le dans notre composant VFXParticles
:
// ... import { useVFX } from "./VFXStore"; // ... export const VFXParticles = ({ name, settings = {} }) => { // ... const registerEmitter = useVFX((state) => state.registerEmitter); const unregisterEmitter = useVFX((state) => state.unregisterEmitter); useEffect(() => { // emit(nbParticles); registerEmitter(name, emit); return () => { unregisterEmitter(name); }; }, []); // ... }; // ...
Nous ajoutons une prop name
Ă notre composant VFXParticles
pour identifier l'émetteur. Nous utilisons ensuite le hook useVFX
pour obtenir les fonctions registerEmitter
et unregisterEmitter
.
Nous appelons registerEmitter
avec le name
et la fonction emit
à l'intérieur du hook useEffect
pour enregistrer l'émetteur lorsque le composant est monté et le désenregistrer lorsqu'il est démonté.
Dans le composant Experience
, ajoutons la prop name
Ă notre composant VFXParticles
:
// ... import { VFXParticles } from "./vfxs/VFXParticles"; export const Experience = () => { return ( <> {/* ... */} <VFXParticles name="sparks" /> </> ); };
VFXEmitter
Maintenant que nous avons notre hook useVFX
, nous pouvons créer un composant VFXEmitter
qui sera responsable de l'émission de particules depuis notre composant VFXParticles
.
Dans le dossier vfxs
, créons un fichier VFXEmitter.jsx
:
import { forwardRef, useImperativeHandle, useRef } from "react"; import { useVFX } from "./VFXStore"; export const VFXEmitter = forwardRef( ({ emitter, settings = {}, ...props }, forwardedRef) => { const { duration = 1, nbParticles = 1000, spawnMode = "time", // time, burst loop = false, delay = 0, } = settings; const emit = useVFX((state) => state.emit); const ref = useRef(); useImperativeHandle(forwardedRef, () => ref.current); return ( <> <object3D {...props} ref={ref} /> </> ); } );
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
One-time payment. Lifetime updates included.