Water Shader
El verano se acerca (al menos cuando estoy escribiendo esta lección), ¡es hora de una fiesta en la piscina! 🩳
En esta lección, crearemos el siguiente efecto de agua usando React Three Fiber y GLSL:
La espuma es más densa alrededor de los bordes de la piscina y alrededor del pato.
Para crear este efecto descubriremos la Biblioteca de shaders Lygia para simplificar la creación de shaders y pondremos en práctica la técnica de render target para crear el efecto de espuma.
Paquete inicial
El paquete inicial para esta lección incluye los siguientes assets:
- Modelo de piscina por Poly by Google CC-BY vía Poly Pizza
- Modelo de pato del mercado de Pmndrs
- Fuente Inter de Google Fonts
El resto es una configuración simple de iluminación y cámara.
Un día soleado en la piscina 🏊
Water shader
El agua es simplemente un plano con un <meshBasicMaterial />
aplicado. Vamos a reemplazar este material con un shader personalizado.
Vamos a crear un nuevo archivo WaterMaterial.jsx
con una plantilla para 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> }` );
Nuestro material tiene dos uniforms: uColor
y uOpacity
.
Para poder usar nuestro material personalizado de manera declarativa, vamos a usar la función extend
de @react-three/fiber
en el archivo main.jsx
:
// ... import { extend } from "@react-three/fiber"; import { WaterMaterial } from "./components/WaterMaterial.jsx"; extend({ WaterMaterial }); // ...
Necesitamos hacer la llamada a
extend
desde un archivo que se importe antes del componente que usa el material personalizado. De esta manera, podremos usar elWaterMaterial
de manera declarativa en nuestros componentes.Es por eso que lo hacemos en el archivo
main.jsx
en lugar del archivoWaterMaterial.jsx
.
Ahora en Water.jsx
, podemos reemplazar el <meshBasicMaterial />
con nuestro material personalizado y ajustar las propiedades con los uniforms correspondientes:
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> ); };
Hemos reemplazado exitosamente el material básico con nuestro shader material personalizado.
Biblioteca de Shaders Lygia
Para crear un efecto de espuma animada, utilizaremos la biblioteca de Shaders Lygia. Esta biblioteca simplifica la creación de shaders proporcionando un conjunto de utilidades y funciones para crear shaders de una manera más declarativa.
La sección que nos interesará es la generativa. Contiene un conjunto de funciones útiles para crear efectos generativos como ruido, curl, fbm.
En la sección generativa, puedes encontrar la lista de funciones disponibles.
Al abrir una de las funciones, puedes ver el fragmento de código para usarla en tu shader y una vista previa del efecto.
pnoise página de función
Este es el efecto que queremos usar. Puedes ver en el ejemplo que para poder usar la función pnoise
, incluyen el archivo shader pnoise
. Nosotros haremos lo mismo.
Resolver Lygia
Para poder usar la biblioteca de Shaders Lygia en nuestro proyecto, tenemos dos opciones:
- Copiar el contenido de la biblioteca en nuestro proyecto e importar los archivos que necesitamos. (Hemos visto cómo importar archivos GLSL en la lección de introducción a shaders)
- Usar una biblioteca llamada
resolve-lygia
que resolverá la biblioteca de Shaders Lygia desde la web y reemplazará automáticamente las directivas#include
relacionadas con lygia con el contenido de los archivos.
Dependiendo de tu proyecto, cuántos efectos quieras usar y si estás usando otras bibliotecas de shaders, puede que prefieras una solución sobre la otra.
En esta lección, utilizaremos la biblioteca resolve-lygia
. Para instalarla, ejecuta el siguiente comando:
yarn add resolve-lygia
Luego, para usarla simplemente necesitamos envolver nuestro código shader fragment y/o vertex con la función 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> }`) );
Solo necesitaremos usar la función resolveLygia
para el shader fragment en esta lección.
Ahora podemos usar la función pnoise
en nuestro 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> }
Multiplicamos el vUv
por 10.0
para hacer el ruido más visible y usamos la función pnoise
para crear el efecto de ruido. Mezclamos el uColor
con negro basado en el valor del ruido para crear el color final.
Vemos el efecto de ruido aplicado al agua.
⚠️ Resolve Lygia parece tener algunos problemas de disponibilidad de vez en cuando. Si encuentras algún problema, puedes usar la biblioteca Glslify o copiar los archivos de la biblioteca de Shaders Lygia que necesitas y usar los archivos glsl como lo hicimos en la lección de introducción a shaders.
Efecto de espuma
El efecto de ruido es la base de nuestro efecto de espuma. Antes de afinar el efecto, creemos los uniformes que necesitaremos para tener control total sobre el efecto de espuma.
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> }`) );
Hemos añadido los siguientes uniformes:
uTime
: para animar el efecto de espumauSpeed
: para controlar la velocidad de la animación del efectouRepeat
: para escalar el efecto de ruidouNoiseType
: para cambiar entre diferentes funciones de ruidouFoam
: para controlar el umbral cuando comienza el efecto de espumauFoamTop
: para controlar el umbral en el que la espuma se vuelve más densa
Ahora necesitamos aplicar estos uniformes en el 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> ); };
Ahora podemos crear la lógica de la espuma en el shader.
Calculamos nuestro tiempo ajustado multiplicando el uTime
por uSpeed
:
float adjustedTime = uTime * uSpeed;
Luego generamos el efecto de ruido usando la función pnoise
:
float noise = pnoise(vec3(vUv * uRepeat, adjustedTime * 0.5), vec3(100.0, 24.0, 112.0));
Aplicamos el efecto de espuma usando la función smoothstep
:
noise = smoothstep(uFoam, uFoamTop, noise);
Luego, creamos colores más brillantes para representar la espuma. Creamos un intermediateColor
y un topColor
:
vec3 intermediateColor = uColor * 1.8; vec3 topColor = intermediateColor * 2.0;
Ajustamos el color basado en el valor del ruido:
vec3 finalColor = uColor; finalColor = mix(uColor, intermediateColor, step(0.01, noise)); finalColor = mix(finalColor, topColor, step(1.0, noise));
Cuando el ruido está entre 0.01
y 1.0
, el color será el color intermedio. Cuando el ruido es mayor o igual a 1.0
, el color será el color superior.
Aquí está el código final del 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; // GENERACIÓN DE RUIDO float noise = pnoise(vec3(vUv * uRepeat, adjustedTime * 0.5), vec3(100.0, 24.0, 112.0)); // ESPUMA 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> }
Ahora tenemos un bonito efecto de espuma en el agua.
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.