WebGPU / TSL
WebGPU es un nuevo estándar web que proporciona una API de bajo nivel para renderizar gráficos y realizar cálculos en la GPU. Está diseñado para ser el sucesor de WebGL, ofreciendo un mejor rendimiento y características más avanzadas.
Buenas noticias, ahora es posible usarlo con Three.js con cambios mínimos en la base del código.
En esta lección, exploraremos cómo usar WebGPU con Three.js y React Three Fiber, y cómo escribir shaders utilizando el nuevo Three Shading Language (TSL).
Si eres nuevo en shaders, te recomiendo completar primero el capítulo de Shaders antes de continuar con este.
WebGPU Renderer
Para usar WebGPU API en lugar de la de WebGL, necesitamos usar un WebGPURenderer
(No hay sección dedicada en la documentación de Three.js aún) en lugar del WebGLRenderer.
Con React Three Fiber, al crear un componente <Canvas>
, la configuración del renderer se realiza automáticamente. Sin embargo, podemos sobrescribir el renderer predeterminado pasando una función al prop gl
del componente <Canvas>
.
En App.jsx
, tenemos un componente <Canvas>
que usa el WebGLRenderer
predeterminado. Vamos a modificarlo para que use el WebGPURenderer
en su lugar.
Primero, necesitamos detener el frameloop
hasta que el WebGPURenderer
esté listo. Podemos hacer esto configurando el prop frameloop
en never
.
// ... import { useState } from "react"; function App() { const [frameloop, setFrameloop] = useState("never"); return ( <> {/* ... */} <Canvas // ... frameloop={frameloop} > {/* ... */} </Canvas> </> ); } export default App;
A continuación, necesitamos importar la versión WebGPU de Three.js:
import * as THREE from "three/webgpu";
Al usar WebGPU, necesitamos usar el módulo
three/webgpu
en lugar del módulothree
predeterminado. Esto se debe a que elWebGPURenderer
no está incluido en la build predeterminada de Three.js.
Luego, podemos usar el prop gl
para crear una nueva instancia 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> </> ); } // ...
Creamos una nueva instancia de WebGPURenderer
y le pasamos el elemento canvas
. También configuramos algunas opciones para el renderer, como powerPreference
, antialias
, alpha
, stencil
y shadowMap
. Estas opciones son similares a las utilizadas en el WebGLRenderer
.
Finalmente, llamamos al método init()
del renderer para inicializarlo. Una vez que la inicialización esté completa, configuramos el estado frameloop
a "always"
para comenzar a renderizar.
Veamos el resultado en el navegador:
Nuestro cubo ahora se renderiza utilizando el WebGPURenderer
en lugar del WebGLRenderer
.
¡Es así de simple! Hemos configurado con éxito un WebGPURenderer
en nuestra aplicación de React Three Fiber. Ahora puedes usar la misma API de Three.js para crear y manipular objetos 3D, tal como lo harías con el WebGLRenderer
.
Los cambios más grandes vienen a la hora de escribir shaders. La API de WebGPU utiliza un lenguaje de sombreado diferente al de WebGL, lo que significa que necesitamos escribir nuestros shaders de una manera diferente. En WGSL en lugar de GLSL.
Aquí es donde entra en juego el Three Shading Language (TSL).
Lenguaje de Sombreado Three
TSL es un nuevo lenguaje de sombreado diseñado para ser utilizado con Three.js para escribir shaders de una forma más amigable usando un enfoque basado en nodos.
Una gran ventaja de TSL es que es independiente del renderizador, lo que significa que puedes usar los mismos shaders con diferentes renderizadores, como WebGL y WebGPU.
Esto facilita la escritura y el mantenimiento de shaders, ya que no tienes que preocuparte por las diferencias entre los dos lenguajes de sombreado.
También está preparado para el futuro, ya que si se lanza un nuevo renderizador, podríamos usar los mismos shaders sin ningún cambio siempre que TSL lo soporte.
El Lenguaje de Sombreado Three sigue en desarrollo, pero ya está disponible en las últimas versiones de Three.js. La mejor manera de aprenderlo y de seguir los cambios es consultar la página wiki de Three Shading Language. La utilicé extensamente para aprender a usarlo.
Materiales basados en nodos
Para entender cómo crear shaders con TSL, necesitamos entender qué significa ser basado en nodos.
En un enfoque basado en nodos, creamos shaders conectando diferentes nodos entre sí para crear un grafo. Cada nodo representa una operación o función específica, y las conexiones entre los nodos representan el flujo de datos.
Este enfoque tiene muchas ventajas, como:
- Representación visual: Es más fácil entender y visualizar el flujo de datos y operaciones en un shader.
- Reusabilidad: Podemos crear nodos reutilizables que pueden ser usados en diferentes shaders.
- Flexibilidad: Podemos modificar y cambiar fácilmente el comportamiento de un shader añadiendo o eliminando nodos.
- Extensibilidad: Añadir/Personalizar características de materiales existentes ahora es muy sencillo.
- Agnóstico: TSL generará el código apropiado para el renderizador objetivo, ya sea WebGL (GLSL) o WebGPU (WGSL).
Antes de empezar a codificar nuestro primer material basado en nodos, podemos usar el Three.js playground online para experimentar visualmente con el sistema de nodos.
Abre el Three.js playground y en la parte superior, haz clic en el botón Examples y elige el basic > fresnel
.
Deberías ver un editor de materiales basado en nodos con dos nodos color
y un nodo float
adjunto a un nodo fresnel
. (Color A
, Color B
y Fresnel Factor
)
El nodo fresnel
está conectado al color del Basic Material
, resultando en un efecto fresnel en la tetera.
Usa el botón Splitscreen
para previsualizar el resultado a la derecha.
Digamos que queremos afectar la opacidad del Basic Material
basado en el tiempo. Podemos añadir un nodo Timer
y conectarlo a un nodo Fract
para reiniciar el tiempo a 0 una vez que alcanza 1. Luego lo conectamos a la entrada opacity
del Basic Material
.
Nuestra tetera ahora se desvanece antes de desaparecer y volverse a desvanecer.
Tómate el tiempo para jugar con los diferentes nodos y ver cómo afectan al material.
Ahora que tenemos una comprensión básica de cómo funciona el material basado en nodos, veamos cómo usar el nuevo material basado en nodos de Three.js en React Three Fiber.
Implementación de React Three Fiber
Hasta ahora, con WebGL, hemos estado utilizando MeshBasicMaterial, MeshStandardMaterial, o incluso ShaderMaterial personalizado para crear nuestros materiales.
Cuando usamos WebGPU, necesitamos usar nuevos materiales que sean compatibles con TSL. Sus nombres son los mismos que usábamos antes, solo que con Node
antes de Material
:
MeshBasicMaterial
->MeshBasicNodeMaterial
MeshStandardMaterial
->MeshStandardNodeMaterial
MeshPhysicalMaterial
->MeshPhysicalNodeMaterial
- ...
Para usarlos de manera declarativa con React Three Fiber, necesitamos extend
ellos. En App.jsx
:
// ... import { extend } from "@react-three/fiber"; extend({ MeshBasicNodeMaterial: THREE.MeshBasicNodeMaterial, MeshStandardNodeMaterial: THREE.MeshStandardNodeMaterial, }); // ...
En futuras versiones de React Three Fiber, esto podría hacerse automáticamente.
Ahora podemos usar los nuevos MeshBasicNodeMaterial
y MeshStandardNodeMaterial
en nuestros componentes.
Reemplacemos el MeshStandardMaterial
del cubo en nuestro componente Experience
con el MeshStandardNodeMaterial
:
<mesh> <boxGeometry args={[1, 1, 1]} /> <meshStandardNodeMaterial color="pink" /> </mesh>
Podemos usar el MeshStandardNodeMaterial
igual que usaríamos el MeshStandardMaterial
.
Nuestro cubo ahora se basa en el MeshStandardNodeMaterial
en lugar del MeshStandardMaterial
. Ahora podemos usar nodes para personalizar el material.
Nodo de Color
Vamos a aprender cómo crear nodos personalizados para personalizar nuestros materiales con TSL.
Primero, vamos a crear un nuevo componente llamado PracticeNodeMaterial.jsx
en la carpeta src/components
.
export const PracticeNodeMaterial = ({ colorA = "white", colorB = "orange", }) => { return <meshStandardNodeMaterial color={colorA} />; };
Y en Experience.jsx
, reemplazamos nuestro cubo con un plano usando el PracticeNodeMaterial
:
// ... import { PracticeNodeMaterial } from "./PracticeNodeMaterial"; export const Experience = () => { return ( <> {/* ... */} <mesh rotation-x={-Math.PI / 2}> <planeGeometry args={[2, 2, 200, 200]} /> <PracticeNodeMaterial /> </mesh> </> ); };
Tenemos un plano con el PracticeNodeMaterial
.
Para personalizar nuestro material, ahora podemos alterar los diferentes nodos que tenemos a nuestra disposición utilizando diferentes nodos. La lista de los disponibles se puede encontrar en la página wiki.
Comencemos simplemente con el nodo colorNode
para cambiar el color de nuestro material. En PracticeNodeMaterial.jsx
:
import { color } from "three/tsl"; export const PracticeNodeMaterial = ({ colorA = "white", colorB = "orange", }) => { return <meshStandardNodeMaterial colorNode={color(colorA)} />; };
Definimos la propiedad colorNode
usando el nodo color
del módulo three/tsl
. El nodo color
toma un color como argumento y devuelve un nodo de color que se puede usar en el material.
Esto nos da el mismo resultado que antes, pero ahora podemos agregar más nodos para personalizar nuestro material.
Vamos a importar los nodos mix
y uv
del módulo three/tsl
y usarlos para mezclar dos colores basados en las coordenadas UV del plano.
import { color, mix, uv } from "three/tsl"; export const PracticeNodeMaterial = ({ colorA = "white", colorB = "orange", }) => { return ( <meshStandardNodeMaterial colorNode={mix(color(colorA), color(colorB), uv())} /> ); };
Esto ejecutará los diferentes nodos para obtener el color final del material. El nodo mix
toma dos colores y un factor (en este caso, las coordenadas UV) y devuelve un color que es una mezcla de los dos colores basado en el factor.
Es exactamente lo mismo que usar la función mix
en GLSL, pero ahora podemos usarlo en un enfoque basado en nodos. (¡Mucho más legible!)
Ahora podemos ver los dos colores mezclados basados en las coordenadas UV del plano.
Lo increíble, es que no estamos comenzando desde cero. Estamos utilizando el MeshStandardNodeMaterial
existente y simplemente agregando nuestros nodos personalizados a él. Lo que significa que las sombras, luces y todas las otras características del MeshStandardNodeMaterial
todavía están disponibles.
Declarar nodos en línea está bien para lógica de nodos muy simple, pero para una lógica más compleja, te recomiendo declarar los nodos (y más tarde uniformes, y más) en un 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} />; };
Esto hace exactamente lo mismo que antes, pero ahora podemos agregar más nodos al objeto nodes
y pasarlos al meshStandardNodeMaterial
de una manera más organizada/genérica.
Al cambiar las props colorA
y colorB
, no causará una recompilación del shader gracias al hook useMemo
.
Vamos a agregar controles para cambiar los colores del material. En 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> </> ); };
El color predeterminado está funcionando correctamente, pero actualizar los colores no tiene efecto.
Esto se debe a que necesitamos pasar los colores como uniforms
al meshStandardNodeMaterial
.
Uniformes
Para declarar uniformes en TSL, podemos usar el nodo uniform
del módulo three/tsl
. El nodo uniform
toma un valor como argumento (puede ser de diferentes tipos como float
, vec3
, vec4
, etc.) y devuelve un nodo uniforme que puede ser utilizado en los diferentes nodos mientras se actualiza desde nuestro código de componente.
Cambiemos los colores codificados por uniformes en 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 un objeto uniforms
para una mejor organización del código, y usamos los valores de uniforme en lugar del valor por defecto que obtuvimos al crear nuestros nodos.
Al devolverlos en el useMemo
ahora tenemos acceso a los uniformes en nuestro componente.
En un useFrame
podemos actualizar los uniformes:
// ... 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} />; };
Usa el método
value.set
cuando actualices un uniforme objeto. Por ejemplo, uniformescolor
ovec3
. Para uniformesfloat
, necesitas establecer el valor directamente:uniforms.opacity.value = opacity;
Los colores ahora se actualizan correctamente en tiempo real.
Antes de hacer más con el color, veamos cómo podemos afectar la posición de los vértices de nuestro plano usando el positionNode
.
Nodo de Posición
El nodo positionNode
nos permite afectar la posición de los vértices de nuestra geometría.
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.