WebGPU / TSL
WebGPU é um novo padrão web que fornece uma API de baixo nível para renderizar gráficos e realizar computações na GPU. Ele foi projetado para ser um sucessor do WebGL, oferecendo melhor desempenho e recursos mais avançados.
Ótima notícia, agora é possível usá-lo com Three.js com mudanças mínimas na base de código.
Nesta lição, vamos explorar como usar o WebGPU com Three.js e React Three Fiber e como escrever shaders usando a nova Three Shading Language (TSL).
Se você é novo em shaders, recomendo primeiro completar o capítulo Shaders antes de continuar com este.
WebGPU Renderer
Para usar a WebGPU API em vez da WebGL, precisamos usar um WebGPURenderer
(Ainda não há seção dedicada na documentação do Three.js) em vez do WebGLRenderer.
Com o React Three Fiber, ao criar um componente <Canvas>
, a configuração do renderer é feita automaticamente. Porém, podemos substituir o renderer padrão passando uma função para a prop gl
do componente <Canvas>
.
No App.jsx
, temos um componente <Canvas>
que usa o WebGLRenderer
padrão. Vamos modificá-lo para usar o WebGPURenderer
em vez disso.
Primeiro, precisamos parar o frameloop
até que o WebGPURenderer
esteja pronto. Podemos fazer isso configurando a prop frameloop
para never
.
// ... import { useState } from "react"; function App() { const [frameloop, setFrameloop] = useState("never"); return ( <> {/* ... */} <Canvas // ... frameloop={frameloop} > {/* ... */} </Canvas> </> ); } export default App;
Em seguida, precisamos importar a versão WebGPU do Three.js:
import * as THREE from "three/webgpu";
Ao usar WebGPU, precisamos usar o módulo
three/webgpu
em vez do módulo padrãothree
. Isso ocorre porque oWebGPURenderer
não está incluído na compilação padrão do Three.js.
Então, podemos usar a prop gl
para criar uma nova instância de WebGPURenderer
:
// ... function App() { const [frameloop, setFrameloop] = useState("never"); return ( <> {/* ... */} <Canvas // ... gl={(canvas) => { const renderer = new THREE.WebGPURenderer({ canvas, powerPreference: "high-performance", antialias: true, alpha: false, stencil: false, shadowMap: true, }); renderer.init().then(() => { setFrameloop("always"); }); return renderer; }} > {/* ... */} </Canvas> </> ); } // ...
Criamos uma nova instância de WebGPURenderer
e passamos o elemento canvas
para ela. Também configuramos algumas opções para o renderer, como powerPreference
, antialias
, alpha
, stencil
e shadowMap
. Essas opções são semelhantes às usadas no WebGLRenderer
.
Finalmente, chamamos o método init()
do renderer para inicializá-lo. Assim que a inicialização estiver completa, configuramos o estado frameloop
para "always"
para começar a renderizar.
Vamos ver o resultado no navegador:
Nosso cubo agora está sendo renderizado usando o WebGPURenderer
em vez do WebGLRenderer
.
É simples assim! Configuramos com sucesso um WebGPURenderer
em nossa aplicação React Three Fiber. Você agora pode usar a mesma API do Three.js para criar e manipular objetos 3D, assim como faria com o WebGLRenderer
.
As maiores mudanças ocorrem na escrita de shaders. A API WebGPU usa uma linguagem de sombreamento diferente da WebGL, o que significa que precisamos escrever nossos shaders de uma maneira diferente. Em WGSL em vez de GLSL.
É aí que a Three Shading Language (TSL) entra em cena.
Linguagem de Sombreamento Three
TSL é uma nova linguagem de sombreamento projetada para ser usada com o Three.js para escrever shaders de uma maneira mais amigável para o usuário usando uma abordagem baseada em nós.
Uma grande vantagem do TSL é que ele é agnóstico quanto ao renderizador, o que significa que você pode usar os mesmos shaders com diferentes renderizadores, como WebGL e WebGPU.
Isso facilita a escrita e manutenção dos shaders, pois você não precisa se preocupar com as diferenças entre as duas linguagens de sombreamento.
Também é preparado para o futuro, pois se um novo renderizador for lançado, poderíamos usar os mesmos shaders sem quaisquer alterações, desde que o TSL o suporte.
A Linguagem de Sombreamento Three ainda está em desenvolvimento, mas já está disponível nas versões mais recentes do Three.js. A melhor maneira de aprendê-la e acompanhar as mudanças é verificar a página wiki da Linguagem de Sombreamento Three. Eu a usei extensivamente para aprender a utilizá-la.
Materiais baseados em nós
Para entender como criar shaders com TSL, precisamos entender o que significa ser baseado em nós.
Em uma abordagem baseada em nós, criamos shaders conectando diferentes nós para criar um gráfico. Cada nó representa uma operação ou função específica, e as conexões entre os nós representam o fluxo de dados.
Essa abordagem tem muitas vantagens, tais como:
- Representação visual: É mais fácil entender e visualizar o fluxo de dados e operações em um shader.
- Reutilização: Podemos criar nós reutilizáveis que podem ser usados em diferentes shaders.
- Flexibilidade: Podemos facilmente modificar e alterar o comportamento de um shader adicionando ou removendo nós.
- Extensibilidade: Adicionar/Customizar recursos de materiais existentes é agora muito fácil.
- Agnóstico: TSL gerará o código apropriado para o renderizador de destino, seja ele WebGL (GLSL) ou WebGPU (WGSL).
Antes de começarmos a codificar nosso primeiro material baseado em nós, podemos usar o playground do Three.js online para experimentar o sistema de nós visualmente.
Abra o playground do Three.js e na parte superior, clique no botão Examples e escolha o exemplo basic > fresnel
.
Você deve ver um editor de material baseado em nós com dois nós de color
e um nó de float
conectados a um nó fresnel
. (Color A
, Color B
e Fresnel Factor
)
O nó fresnel
está conectado à cor do Basic Material
, resultando em colorir o Bule com um efeito fresnel.
Use o botão Splitscreen
para visualizar o resultado à direita.
Digamos que queremos afetar a opacidade do Basic Material
com base no tempo. Podemos adicionar um nó Timer
e conectá-lo a um nó Fract
para resetar o tempo para 0 assim que ele chegar a 1. Em seguida, conectamos à entrada de opacity
do Basic Material
.
Nosso bule agora está desvanecendo antes de desaparecer e desvanecer novamente.
Reserve um tempo para brincar com os diferentes nós e ver como eles afetam o material.
Agora que temos uma compreensão básica de como o material baseado em nós funciona, vamos ver como usar o novo material baseado em nós do Three.js no React Three Fiber.
Implementação do React Three Fiber
Até agora, com WebGL, temos usado o MeshBasicMaterial, MeshStandardMaterial, ou até mesmo ShaderMaterial customizados para criar nossos materiais.
Ao utilizar WebGPU, precisamos usar novos materiais que são compatíveis com TSL. Seus nomes são os mesmos que usávamos antes, com Node
antes de Material
:
MeshBasicMaterial
->MeshBasicNodeMaterial
MeshStandardMaterial
->MeshStandardNodeMaterial
MeshPhysicalMaterial
->MeshPhysicalNodeMaterial
- ...
Para usá-los declarativamente com React Three Fiber, precisamos estendê-los
. Em App.jsx
:
// ... import { extend } from "@react-three/fiber"; extend({ MeshBasicNodeMaterial: THREE.MeshBasicNodeMaterial, MeshStandardNodeMaterial: THREE.MeshStandardNodeMaterial, }); // ...
Em futuras versões do React Three Fiber, isso pode ser feito automaticamente.
Agora podemos usar o novo MeshBasicNodeMaterial
e MeshStandardNodeMaterial
em nossos componentes.
Vamos substituir o MeshStandardMaterial
do cubo em nosso componente Experience
pelo MeshStandardNodeMaterial
:
<mesh> <boxGeometry args={[1, 1, 1]} /> <meshStandardNodeMaterial color="pink" /> </mesh>
Podemos usar o MeshStandardNodeMaterial
da mesma forma que usaríamos o MeshStandardMaterial
.
Nosso cubo agora depende do MeshStandardNodeMaterial
em vez do MeshStandardMaterial
. Agora podemos usar nodes para customizar o material.
Node de Cor
Vamos aprender a criar nós personalizados para personalizar nossos materiais com TSL.
Primeiro, vamos criar um novo componente chamado PracticeNodeMaterial.jsx
na pasta src/components
.
export const PracticeNodeMaterial = ({ colorA = "white", colorB = "orange", }) => { return <meshStandardNodeMaterial color={colorA} />; };
E em Experience.jsx
, substituímos nosso cubo por um plano utilizando o PracticeNodeMaterial
:
// ... import { PracticeNodeMaterial } from "./PracticeNodeMaterial"; export const Experience = () => { return ( <> {/* ... */} <mesh rotation-x={-Math.PI / 2}> <planeGeometry args={[2, 2, 200, 200]} /> <PracticeNodeMaterial /> </mesh> </> ); };
Temos um plano com o PracticeNodeMaterial
.
Para personalizar nosso material, podemos agora alterar os diferentes nós à nossa disposição usando diferentes nós. A lista dos disponíveis pode ser encontrada na página do wiki.
Vamos simplesmente começar com o nó colorNode
para mudar a cor do nosso material. Em PracticeNodeMaterial.jsx
:
import { color } from "three/tsl"; export const PracticeNodeMaterial = ({ colorA = "white", colorB = "orange", }) => { return <meshStandardNodeMaterial colorNode={color(colorA)} />; };
Definimos a propriedade colorNode
usando o nó color
do módulo three/tsl
. O nó color
recebe uma cor como argumento e retorna um nó de cor que pode ser usado no material.
Isso nos dá o mesmo resultado de antes, mas agora podemos adicionar mais nós para personalizar nosso material.
Vamos importar os nós mix
e uv
do módulo three/tsl
e usá-los para misturar duas cores com base nas coordenadas UV do plano.
import { color, mix, uv } from "three/tsl"; export const PracticeNodeMaterial = ({ colorA = "white", colorB = "orange", }) => { return ( <meshStandardNodeMaterial colorNode={mix(color(colorA), color(colorB), uv())} /> ); };
Ele executará os diferentes nós para obter a cor final do material. O nó mix
recebe duas cores e um fator (neste caso, as coordenadas UV) e retorna uma cor que é uma mistura das duas cores com base no fator.
É exatamente igual a usar a função mix
em GLSL, mas agora podemos usá-la de maneira baseada em nós. (Muito mais legível!)
Podemos agora ver as duas cores misturadas com base nas coordenadas UV do plano.
O incrível é que não estamos começando do zero. Estamos usando o MeshStandardNodeMaterial
existente e apenas adicionando nossos nós personalizados a ele. O que significa que as sombras, luzes e todos os outros recursos do MeshStandardNodeMaterial
ainda estão disponíveis.
Declarar nós inline é ok para lógica de nó muito simples, mas para lógica mais complexa, recomendo declarar os nós (e mais tarde uniforms, e mais) em um hook useMemo
:
// ... import { useMemo } from "react"; export const PracticeNodeMaterial = ({ colorA = "white", colorB = "orange", }) => { const { nodes } = useMemo(() => { return { nodes: { colorNode: mix(color(colorA), color(colorB), uv()), }, }; }, []); return <meshStandardNodeMaterial {...nodes} />; };
Isso faz exatamente a mesma coisa que antes, mas agora podemos adicionar mais nós ao objeto nodes
e passá-los para o meshStandardNodeMaterial
de uma maneira mais organizada/genérica.
Ao mudar as props colorA
e colorB
, não causará uma recompilação do shader graças ao hook useMemo
.
Vamos adicionar controles para mudar as cores do material. Em Experience.jsx
:
// ... import { useControls } from "leva"; export const Experience = () => { const { colorA, colorB } = useControls({ colorA: { value: "skyblue" }, colorB: { value: "blueviolet" }, }); return ( <> {/* ... */} <mesh rotation-x={-Math.PI / 2}> <planeGeometry args={[2, 2, 200, 200]} /> <PracticeNodeMaterial colorA={colorA} colorB={colorB} /> </mesh> </> ); };
A cor padrão está funcionando corretamente, mas atualizar as cores não tem efeito.
Isso ocorre porque precisamos passar as cores como uniforms
para o meshStandardNodeMaterial
.
Uniformes
Para declarar uniforms em TSL, podemos usar o nó uniform
do módulo three/tsl
. O nó uniform
recebe um valor como argumento (pode ser de diferentes tipos como float
, vec3
, vec4
, etc.) e retorna um nó uniform que pode ser usado nos diferentes nós enquanto é atualizado a partir do nosso código de componente.
Vamos substituir as cores fixas por uniforms em PracticeNodeMaterial.jsx
:
// ... import { uniform } from "three/tsl"; export const PracticeNodeMaterial = ({ colorA = "white", colorB = "orange", }) => { const { nodes, uniforms } = useMemo(() => { const uniforms = { colorA: uniform(color(colorA)), colorB: uniform(color(colorB)), }; return { nodes: { colorNode: mix(uniforms.colorA, uniforms.colorB, uv()), }, uniforms, }; }, []); return <meshStandardNodeMaterial {...nodes} />; };
Declaramos um objeto uniforms
para uma melhor organização do código e usamos os valores uniformes em vez do valor padrão que obtivemos na criação dos nossos nós.
Ao retorná-los no useMemo
, agora temos acesso aos uniforms em nosso componente.
Em um useFrame
, podemos atualizar os uniforms:
// ... import { useFrame } from "@react-three/fiber"; export const PracticeNodeMaterial = ({ colorA = "white", colorB = "orange", }) => { // ... useFrame(() => { uniforms.colorA.value.set(colorA); uniforms.colorB.value.set(colorB); }); return <meshStandardNodeMaterial {...nodes} />; };
Use o método
value.set
quando você atualizar um uniform de objeto. Por exemplo, uniforms decolor
ouvec3
. Para uniforms defloat
, você precisa definir o valor diretamente:uniforms.opacity.value = opacity;
As cores agora estão atualizando corretamente em tempo real.
Antes de fazer mais alterações na cor, vamos ver como podemos afetar a posição dos vértices do nosso plano usando o positionNode
.
Posição do Node
O positionNode
nos permite afetar a posição dos vértices da nossa geometria.
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.