⚡️ Limited Black Friday Deal
Get 50% off on the React Three Fiber Ultimate Course with the promo code ULTIMATE50
Buy Now
Fundamentals
Core
Master
Shaders
Water Shader
L'été arrive (du moins au moment où j'écris cette leçon), il est temps d'organiser une fête à la piscine ! 🩳
Dans cette leçon, nous allons créer l'effet d'eau suivant en utilisant React Three Fiber et GLSL :
La mousse est plus dense autour des bords de la piscine et autour du canard.
Pour créer cet effet, nous découvrirons la bibliothèque de shaders Lygia pour simplifier la création de shaders et nous mettrons en pratique la technique du render target pour créer l’effet de mousse.
Pack de démarrage
Le pack de démarrage pour cette leçon inclut les ressources suivantes :
- Modèle de piscine par Poly de Google CC-BY via Poly Pizza
- Modèle de canard du marché Pmndrs
- Police d'écriture Inter de Google Fonts
Le reste est une configuration simple d'éclairage et de caméra.
Une journée ensoleillée à la piscine 🏊
Water shader
L'eau est simplement une surface plane avec un <meshBasicMaterial />
appliqué dessus. Nous allons remplacer ce matériau par un shader personnalisé.
Créons un nouveau fichier WaterMaterial.jsx
avec un modèle pour un shader material :
import { shaderMaterial } from "@react-three/drei"; import { Color } from "three"; export const WaterMaterial = shaderMaterial( { uColor: new Color("skyblue"), uOpacity: 0.8, }, /*glsl*/ ` varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`, /*glsl*/ ` varying vec2 vUv; uniform vec3 uColor; uniform float uOpacity; void main() { gl_FragColor = vec4(uColor, uOpacity); #include <tonemapping_fragment> #include <encodings_fragment> }` );
Notre matériau possède deux uniforms: uColor
et uOpacity
.
Pour pouvoir utiliser notre matériau personnalisé de manière déclarative, utilisons la fonction extend
de @react-three/fiber
dans le fichier main.jsx
:
// ... import { extend } from "@react-three/fiber"; import { WaterMaterial } from "./components/WaterMaterial.jsx"; extend({ WaterMaterial }); // ...
Nous devons effectuer l'appel à
extend
depuis un fichier qui est importé avant le composant qui utilise le matériau personnalisé. De cette façon, nous pourrons utiliser leWaterMaterial
de manière déclarative dans nos composants.C'est pourquoi nous le faisons dans le fichier
main.jsx
plutôt que dans le fichierWaterMaterial.jsx
.
Maintenant, dans Water.jsx
, nous pouvons remplacer le <meshBasicMaterial />
par notre matériau personnalisé et ajuster les propriétés avec les uniforms correspondants :
import { useControls } from "leva"; import { Color } from "three"; export const Water = ({ ...props }) => { const { waterColor, waterOpacity } = useControls({ waterOpacity: { value: 0.8, min: 0, max: 1 }, waterColor: "#00c3ff", }); return ( <mesh {...props}> <planeGeometry args={[15, 32, 22, 22]} /> <waterMaterial uColor={new Color(waterColor)} transparent uOpacity={waterOpacity} /> </mesh> ); };
Nous avons réussi à remplacer le matériau de base par notre shader material personnalisé.
Bibliothèque de shaders Lygia
Pour créer un effet de mousse animée, nous allons utiliser la bibliothèque de shaders Lygia. Cette bibliothèque simplifie la création de shaders en fournissant un ensemble d'utilitaires et de fonctions pour créer des shaders de manière plus déclarative.
La section qui nous intéressera est celle sur les génératifs. Elle contient un ensemble de fonctions utiles pour créer des effets génératifs comme le bruit, le curl, le fbm.
Dans la section générative, vous pouvez trouver la liste des fonctions disponibles.
En ouvrant l'une des fonctions, vous pouvez voir l'extrait de code à utiliser dans votre shader et un aperçu de l'effet.
Page de la fonction pnoise
C'est l'effet que nous voulons utiliser. Vous pouvez voir dans l'exemple que, pour pouvoir utiliser la fonction pnoise
, ils incluent le fichier shader pnoise
. Nous allons faire de même.
Resolve Lygia
Pour pouvoir utiliser la bibliothèque de shaders Lygia dans notre projet, nous avons deux options :
- Copier le contenu de la bibliothèque dans notre projet et importer les fichiers dont nous avons besoin. (Nous avons vu comment importer des fichiers GLSL dans la leçon d'introduction aux shaders)
- Utiliser une bibliothèque nommée
resolve-lygia
qui résoudra la bibliothèque de shaders Lygia depuis le web et remplacera automatiquement les directives#include
liées à lygia par le contenu des fichiers.
Selon votre projet, le nombre d'effets que vous souhaitez utiliser, et si vous utilisez d'autres bibliothèques de shaders, vous pouvez préférer l'une ou l'autre solution.
Dans cette leçon, nous allons utiliser la bibliothèque resolve-lygia
. Pour l'installer, exécutez la commande suivante :
yarn add resolve-lygia
Ensuite, pour l'utiliser, nous devons simplement envelopper notre code fragment et/ou vertex shader avec la fonction resolveLygia
:
// ... import { resolveLygia } from "resolve-lygia"; export const WaterMaterial = shaderMaterial( { uColor: new Color("skyblue"), uOpacity: 0.8, }, /*glsl*/ ` varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`, resolveLygia(/*glsl*/ ` varying vec2 vUv; uniform vec3 uColor; uniform float uOpacity; void main() { gl_FragColor = vec4(uColor, uOpacity); #include <tonemapping_fragment> #include <encodings_fragment> }`) );
Nous n'aurons besoin d'utiliser la fonction resolveLygia
que pour le fragment shader dans cette leçon.
Nous pouvons maintenant utiliser la fonction pnoise
dans notre shader :
#include "lygia/generative/pnoise.glsl" varying vec2 vUv; uniform vec3 uColor; uniform float uOpacity; void main() { float noise = pnoise(vec3(vUv * 10.0, 1.0), vec3(100.0, 24.0, 112.0)); vec3 black = vec3(0.0); vec3 finalColor = mix(uColor, black, noise); gl_FragColor = vec4(finalColor, uOpacity); #include <tonemapping_fragment> #include <encodings_fragment> }
Nous multiplions le vUv
par 10.0
pour rendre le bruit plus visible et nous utilisons la fonction pnoise
pour créer l'effet de bruit. Nous mélangeons le uColor
avec du noir en fonction de la valeur du bruit pour créer la couleur finale.
Nous pouvons voir l'effet de bruit appliqué à l'eau.
⚠️ Resolve Lygia semble avoir des problèmes de temps d'arrêt de temps en temps. Si vous rencontrez des problèmes, vous pouvez utiliser la bibliothèque Glslify ou copier les fichiers de la bibliothèque de shaders Lygia dont vous avez besoin et utiliser les fichiers glsl comme nous l'avons fait dans la leçon d'introduction aux shaders.
Effet de mousse
L'effet de noise est la base de notre effet de mousse. Avant de peaufiner l'effet, créons les uniforms nécessaires pour avoir un contrôle total sur l'effet de mousse.
WaterMaterial.jsx
:
import { shaderMaterial } from "@react-three/drei"; import { resolveLygia } from "resolve-lygia"; import { Color } from "three"; export const WaterMaterial = shaderMaterial( { uColor: new Color("skyblue"), uOpacity: 0.8, uTime: 0, uSpeed: 0.5, uRepeat: 20.0, uNoiseType: 0, uFoam: 0.4, uFoamTop: 0.7, }, /*glsl*/ ` varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`, resolveLygia(/*glsl*/ ` #include "lygia/generative/pnoise.glsl" varying vec2 vUv; uniform vec3 uColor; uniform float uOpacity; uniform float uTime; uniform float uSpeed; uniform float uRepeat; uniform int uNoiseType; uniform float uFoam; uniform float uFoamTop; void main() { float noise = pnoise(vec3(vUv * 10.0, 1.0), vec3(100.0, 24.0, 112.0)); vec3 black = vec3(0.0); vec3 finalColor = mix(uColor, black, noise); gl_FragColor = vec4(finalColor, uOpacity); #include <tonemapping_fragment> #include <encodings_fragment> }`) );
Nous avons ajouté les uniforms suivants :
uTime
: pour animer l'effet de mousseuSpeed
: pour contrôler la vitesse de l'animation de l'effetuRepeat
: pour ajuster l'échelle de l'effet de noiseuNoiseType
: pour passer d'une fonction de noise à l'autreuFoam
: pour contrôler le seuil à partir duquel l'effet de mousse commenceuFoamTop
: pour contrôler le seuil à partir duquel la mousse devient plus dense
Nous devons maintenant appliquer ces uniforms sur le material.
Water.jsx
:
import { useFrame } from "@react-three/fiber"; import { useControls } from "leva"; import { useRef } from "react"; import { Color } from "three"; export const Water = ({ ...props }) => { const waterMaterialRef = useRef(); const { waterColor, waterOpacity, speed, noiseType, foam, foamTop, repeat } = useControls({ waterOpacity: { value: 0.8, min: 0, max: 1 }, waterColor: "#00c3ff", speed: { value: 0.5, min: 0, max: 5 }, repeat: { value: 30, min: 1, max: 100, }, foam: { value: 0.4, min: 0, max: 1, }, foamTop: { value: 0.7, min: 0, max: 1, }, noiseType: { value: 0, options: { Perlin: 0, Voronoi: 1, }, }, }); useFrame(({ clock }) => { if (waterMaterialRef.current) { waterMaterialRef.current.uniforms.uTime.value = clock.getElapsedTime(); } }); return ( <mesh {...props}> <planeGeometry args={[15, 32, 22, 22]} /> <waterMaterial ref={waterMaterialRef} uColor={new Color(waterColor)} transparent uOpacity={waterOpacity} uNoiseType={noiseType} uSpeed={speed} uRepeat={repeat} uFoam={foam} uFoamTop={foamTop} /> </mesh> ); };
Nous pouvons maintenant créer la logique de mousse dans le shader.
Nous calculons notre temps ajusté en multipliant uTime
par uSpeed
:
float adjustedTime = uTime * uSpeed;
Ensuite, nous générons l'effet de noise en utilisant la fonction pnoise
:
float noise = pnoise(vec3(vUv * uRepeat, adjustedTime * 0.5), vec3(100.0, 24.0, 112.0));
Nous appliquons l'effet de mousse en utilisant la fonction smoothstep
:
noise = smoothstep(uFoam, uFoamTop, noise);
Ensuite, nous créons des couleurs plus claires pour représenter la mousse. Nous créons une intermediateColor
et une topColor
:
vec3 intermediateColor = uColor * 1.8; vec3 topColor = intermediateColor * 2.0;
Nous ajustons la couleur en fonction de la valeur du noise:
vec3 finalColor = uColor; finalColor = mix(uColor, intermediateColor, step(0.01, noise)); finalColor = mix(finalColor, topColor, step(1.0, noise));
Lorsque le noise est compris entre 0.01
et 1.0
, la couleur sera la couleur intermédiaire. Lorsque le noise est supérieur ou égal à 1.0
, la couleur sera la couleur du haut.
Voici le code final du shader:
#include "lygia/generative/pnoise.glsl" varying vec2 vUv; uniform vec3 uColor; uniform float uOpacity; uniform float uTime; uniform float uSpeed; uniform float uRepeat; uniform int uNoiseType; uniform float uFoam; uniform float uFoamTop; void main() { float adjustedTime = uTime * uSpeed; // NOISE GENERATION float noise = pnoise(vec3(vUv * uRepeat, adjustedTime * 0.5), vec3(100.0, 24.0, 112.0)); // FOAM noise = smoothstep(uFoam, uFoamTop, noise); // COLOR vec3 intermediateColor = uColor * 1.8; vec3 topColor = intermediateColor * 2.0; vec3 finalColor = uColor; finalColor = mix(uColor, intermediateColor, step(0.01, noise)); finalColor = mix(finalColor, topColor, step(1.0, noise)); gl_FragColor = vec4(finalColor, uOpacity); #include <tonemapping_fragment> #include <encodings_fragment> }
Nous avons maintenant un joli effet de mousse sur l'eau.
End of lesson preview
To get access to the entire lesson, you need to purchase the course.