Fundamentals
Core
Master
Shaders
Water Shader
O verão está chegando (pelo menos quando estou escrevendo esta lição), é hora de criar uma pool party! 🩳
Nesta lição, vamos criar o seguinte efeito de água usando React Three Fiber e GLSL:
A espuma é mais densa ao redor das bordas da piscina e ao redor do pato.
Para criar esse efeito, vamos descobrir a Lygia Shader library para simplificar a criação de shaders e colocaremos em prática a técnica de render target para criar o efeito de espuma.
Pacote inicial
O pacote inicial para esta lição inclui os seguintes assets:
- Modelo de piscina por Poly by Google CC-BY via Poly Pizza
- Modelo de pato do Pmndrs marketplace
- Fonte Inter do Google Fonts
O resto é uma configuração simples de iluminação e câmera.
Um dia ensolarado na piscina 🏊
Shader de água
A água é apenas um plano simples com um <meshBasicMaterial />
aplicado. Vamos substituir esse material com um shader customizado.
Vamos criar um novo arquivo WaterMaterial.jsx
com um boilerplate para um material de shader:
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> }` );
Nosso material tem dois uniforms: uColor
e uOpacity
.
Para podermos usar nosso material customizado de forma declarativa, vamos usar a função extend
de @react-three/fiber
no arquivo main.jsx
:
// ... import { extend } from "@react-three/fiber"; import { WaterMaterial } from "./components/WaterMaterial.jsx"; extend({ WaterMaterial }); // ...
Precisamos fazer a chamada do
extend
de um arquivo que é importado antes do componente que usa o material customizado. Dessa forma, poderemos usar oWaterMaterial
de forma declarativa em nossos componentes.É por isso que fazemos isso no arquivo
main.jsx
em vez do arquivoWaterMaterial.jsx
.
Agora em Water.jsx
, podemos substituir o <meshBasicMaterial />
com nosso material customizado e ajustar as propriedades com os respectivos uniforms:
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> ); };
Substituímos com sucesso o material básico pelo nosso material de shader customizado.
Biblioteca de Shaders Lygia
Para criar um efeito de espuma animada, usaremos a biblioteca de Shaders Lygia. Esta biblioteca simplifica a criação de shaders fornecendo um conjunto de utilitários e funções para criar shaders de uma maneira mais declarativa.
A seção que nos interessará é a generative. Ela contém um conjunto de funções úteis para criar efeitos generativos, como noise, curl, fbm.
Dentro da seção generative, você pode encontrar a lista de funções disponíveis.
Ao abrir uma das funções, você pode ver um trecho de código para usá-la no seu shader e uma pré-visualização do efeito.
Página da função pnoise
Este é o efeito que queremos usar. Você pode ver no exemplo que, para poder usar a função pnoise
, eles incluem o arquivo shader pnoise
. Faremos o mesmo.
Resolver Lygia
Para poder usar a biblioteca de Shaders Lygia em nosso projeto, temos duas opções:
- Copiar o conteúdo da biblioteca para o nosso projeto e importar os arquivos que precisamos. (Vimos como importar arquivos GLSL na lição de introdução a shaders)
- Usar uma biblioteca chamada
resolve-lygia
que resolverá a biblioteca de Shaders Lygia da web e substituirá automaticamente as diretivas#include
relacionadas a lygia pelo conteúdo dos arquivos.
Dependendo do seu projeto, de quantos efeitos você quer usar, e se você está usando outras bibliotecas de shaders, pode preferir uma solução em detrimento da outra.
Nesta lição, usaremos a biblioteca resolve-lygia
. Para instalá-la, execute o seguinte comando:
yarn add resolve-lygia
Em seguida, para utilizá-la, simplesmente precisamos envolver nosso código de shader fragment e/ou vertex com a função 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> }`) );
Só precisaremos usar a função resolveLygia
para o shader fragment nesta lição.
Agora podemos usar a função pnoise
em nosso 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 o vUv
por 10.0
para tornar o noise mais visível e usamos a função pnoise
para criar o efeito de noise. Misturamos a uColor
com preto com base no valor do noise para criar a cor final.
Podemos ver o efeito de noise aplicado à água.
⚠️ O Resolve Lygia parece ter alguns problemas de downtime de tempos em tempos. Se você encontrar algum problema, pode usar a biblioteca Glslify ou copiar os arquivos da biblioteca de Shaders Lygia que você precisa e usar os arquivos glsl como fizemos na lição de introdução a shaders.
Efeito de espuma
O efeito de noise é a base do nosso efeito de espuma. Antes de ajustar finamente o efeito, vamos criar os uniforms que precisaremos para ter total controle sobre o efeito 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> }`) );
Adicionamos os seguintes uniforms:
uTime
: para animar o efeito de espumauSpeed
: para controlar a velocidade da animação do efeitouRepeat
: para escalar o efeito de noiseuNoiseType
: para alternar entre diferentes funções de noiseuFoam
: para controlar o limiar quando o efeito de espuma começauFoamTop
: para controlar o limiar em que a espuma fica mais densa
Agora precisamos aplicar esses uniforms no 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> ); };
Agora podemos criar a lógica da espuma no shader.
Calculamos nosso tempo ajustado multiplicando o uTime
por uSpeed
:
float adjustedTime = uTime * uSpeed;
Então, geramos o efeito de noise usando a função pnoise
:
float noise = pnoise(vec3(vUv * uRepeat, adjustedTime * 0.5), vec3(100.0, 24.0, 112.0));
Aplicamos o efeito de espuma usando a função smoothstep
:
noise = smoothstep(uFoam, uFoamTop, noise);
Em seguida, criamos cores mais brilhantes para representar a espuma. Criamos uma intermediateColor
e uma topColor
:
vec3 intermediateColor = uColor * 1.8; vec3 topColor = intermediateColor * 2.0;
Ajustamos a cor com base no valor do noise:
vec3 finalColor = uColor; finalColor = mix(uColor, intermediateColor, step(0.01, noise)); finalColor = mix(finalColor, topColor, step(1.0, noise));
Quando o noise está entre 0.01
e 1.0
, a cor será a cor intermediária. Quando o noise estiver acima ou igual a 1.0
, a cor será a cor superior.
Aqui está o código final do 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; // GERAÇÃO DE NOISE float noise = pnoise(vec3(vUv * uRepeat, adjustedTime * 0.5), vec3(100.0, 24.0, 112.0)); // ESPUMA noise = smoothstep(uFoam, uFoamTop, noise); // COR 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> }
Agora temos um bom efeito de espuma na água.
End of lesson preview
To get access to the entire lesson, you need to purchase the course.