WebGPU / TSL

Starter pack

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ódulo three predeterminado. Esto se debe a que el WebGPURenderer 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.

Three.js playground

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.

Three.js playground

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>

WebGPU Pink Cube

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>
    </>
  );
};

WebGPU Plane

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!)

WebGPU Plane with Mix

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, uniformes color o vec3. Para uniformes float, 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.

Three.js logoReact logo

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
Unlock the Full Course – Just $85

One-time payment. Lifetime updates included.