Water Shader

Starter pack

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:

El resto es una configuraci贸n simple de iluminaci贸n y c谩mara.

Paquete inicial

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 el WaterMaterial de manera declarativa en nuestros componentes.

Es por eso que lo hacemos en el archivo main.jsx en lugar del archivo WaterMaterial.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.

Lygia Shader library

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.

Lygia Shader library function

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.

Lygia Shader library pnoise

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 espuma
  • uSpeed: para controlar la velocidad de la animaci贸n del efecto
  • uRepeat: para escalar el efecto de ruido
  • uNoiseType: para cambiar entre diferentes funciones de ruido
  • uFoam: para controlar el umbral cuando comienza el efecto de espuma
  • uFoamTop : 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.

End of lesson preview

To get access to the entire lesson, you need to purchase the course.