El fallback automático a WebGL
Cómo funciona por dentro la caída al backend de WebGL2, en qué momento exacto ocurre, y qué implica de verdad para tu código.
Que WebGPURenderer funcione sin WebGPU suena a truco de marketing y es una pieza de ingeniería concreta con un mecanismo que se puede leer en cuarenta líneas de código. Merece la pena entenderlo, porque decide qué puedes asumir de tu escena y en qué momento puedes preguntarlo, y porque su existencia es el argumento central de la estrategia de despliegue del último tema de este nivel.
- Describir el mecanismo de respaldo paso a paso desde el código real.
- Determinar en qué momento se puede saber qué backend está activo.
- Enumerar qué se pierde al caer al backend de WebGL2.
- Forzar el backend de WebGL2 para probar la ruta de respaldo.
El mecanismo, paso a paso
Todo empieza en el constructor de WebGPURenderer:
constructor( parameters = {} ) {
let BackendClass;
if ( parameters.forceWebGL ) {
BackendClass = WebGLBackend;
} else {
BackendClass = WebGPUBackend;
parameters.getFallback = () => {
warn( 'WebGPURenderer: WebGPU is not available, running under WebGL2 backend.' );
return new WebGLBackend( parameters );
};
}
const backend = new BackendClass( parameters );
super( backend, parameters );
// ...
}
Fíjate en lo que no pasa aquí: no se comprueba si WebGPU existe. Salvo que pidas forceWebGL, el backend construido es siempre WebGPUBackend, y lo único que se hace es dejar preparada una función de respaldo en los parámetros.
La decisión se toma en init(), en la clase base Renderer:
this._initPromise = new Promise( async ( resolve, reject ) => {
let backend = this.backend;
try {
await backend.init( this );
} catch ( error ) {
if ( this._getFallback !== null ) {
// try the fallback
try {
this.backend = backend = this._getFallback( error );
await backend.init( this );
} catch ( error ) {
reject( error );
return;
}
} else {
reject( error );
return;
}
}
// ...
Es un try/catch alrededor de la inicialización del backend, con un segundo intento con el de respaldo. Y lo que dispara el catch es el error que ya viste:
if ( adapter === null ) {
throw new Error( 'WebGPUBackend: Unable to create WebGPU adapter.' );
}
Cualquier fallo del camino de WebGPU acaba ahí: que navigator.gpu no exista, que el adaptador venga nulo, que la creación del dispositivo falle.
Y el detalle que hay que retener: this.backend se sustituye en el sitio. La propiedad pública del renderer cambia de objeto durante la inicialización. Por eso preguntar qué backend tienes antes de que init() resuelva no solo es inútil, es engañoso: siempre dirá WebGPUBackend.
Qué cambia realmente al caer
Lo que sigue funcionando es casi todo, y esa es la gracia.
Los materiales de nodos y todo tu TSL funcionan, porque el generador emite GLSL ES 3.0 en lugar de WGSL. Las geometrías, las luces, las sombras, el instancing, el skinning y los cargadores funcionan. El grafo de escena es el mismo. La pila de post-procesado basada en nodos funciona. En una escena típica no notarás la diferencia mirando la imagen.
Lo que no sigue funcionando es lo que depende de capacidades que WebGL2 no tiene:
| Característica | Con WebGPU | Con el respaldo WebGL2 |
|---|---|---|
| Compute shaders | sí | no |
| Storage buffers | sí | no |
| Texturas de almacenamiento | sí | no |
| Índices de dibujo indirecto | sí | no |
| Sistema de coordenadas de profundidad | 0 a 1 | -1 a 1 |
| Consultas de marca de tiempo | sí, si el adaptador lo permite | no |
| Coste de validación por draw call | bajo | alto |
Esa última fila es la que más se nota en escenas con muchos objetos, y es la diferencia estructural entre las dos APIs más que una característica concreta.
La fila del sistema de coordenadas es la que puede morderte en código propio. renderer.coordinateSystem devuelve WebGLCoordinateSystem o WebGPUCoordinateSystem según el backend activo, y si haces matemáticas con profundidad a mano tienes que consultarlo. TSL lo hace por ti en los nodos integrados.
Forzar el respaldo para probarlo
La ruta de respaldo es código que se ejecuta en las máquinas de tus usuarios y no en la tuya, así que hay que probarla a propósito:
const params = new URLSearchParams( location.search );
const renderer = new THREE.WebGPURenderer( {
antialias: true,
forceWebGL: params.has( 'webgl' )
} );
Con eso, añadir ?webgl a la URL ejecuta toda la aplicación sobre el backend de WebGL2. Es la comprobación más barata que existe y conviene dejarla puesta durante todo el desarrollo.
Ten en cuenta que forceWebGL no es lo mismo que el respaldo automático: construye WebGLBackend directamente y no instala ninguna función de recuperación. Sirve para probar el destino, no el camino.
Si tu escena usa compute() con storage buffers para simular partículas, el respaldo automático te dará un renderer que funciona y una simulación que no. El mecanismo garantiza que el renderer arranca, no que tu escena tenga sentido sin las características que le faltan. Cuando uses algo exclusivo de WebGPU, tienes que ramificar tú, y para eso está renderer.backend después de init().
Detectar antes de construir
Si quieres saber si hay WebGPU antes de montar nada, Three.js trae una utilidad en los addons:
import WebGPU from 'three/addons/capabilities/WebGPU.js';
if ( WebGPU.isAvailable() ) {
// camino completo
} else {
document.body.appendChild( WebGPU.getErrorMessage() );
}
Es un export por defecto con exactamente dos métodos estáticos, isAvailable() y getErrorMessage(). Y tiene una peculiaridad importante: el módulo hace una comprobación con await de nivel superior al importarse, pidiendo un adaptador de verdad. Es decir, importar el módulo bloquea el grafo de módulos hasta que el adaptador responde. Es una comprobación fiable, no una simple mirada a navigator.gpu, pero conviene saber que tiene ese coste al cargar.
El mecanismo de esta lección tiene una propiedad que es a la vez su mayor virtud y su mayor peligro: es invisible. Funciona sin que hagas nada, no rompe nada, y avisa por consola con un warn que en producción nadie lee. Eso significa que puedes desarrollar durante meses en un portátil con WebGPU, desplegar, y no enterarte nunca de que el treinta por ciento de tus usuarios está corriendo por una ruta de código que jamás has ejecutado. Y esa ruta no es exótica: es un backend distinto, con un generador de shaders distinto, un sistema de coordenadas distinto y un perfil de rendimiento distinto. La probabilidad de que todo funcione idéntico sin haberlo comprobado es exactamente la que le darías a cualquier otro camino de código sin probar. Es el mismo fenómeno que los manejadores de error que nadie ejerce, los reintentos que nunca se disparan en desarrollo y los planes de recuperación que no se ensayan: la degradación elegante que no se ejercita se convierte, con el tiempo, en degradación a secas. La disciplina que lo evita es corta y hay que adoptarla desde el primer día, no cuando aparezca el primer informe de error. Deja un interruptor en la URL y úsalo. Ejecuta el conjunto de pruebas visuales en los dos backends. Muestra en algún rincón de tu interfaz de depuración cuál está activo. Y cuando llegue el momento de medir rendimiento, mide los dos, porque el objetivo real de tu presupuesto de frame no es tu máquina de desarrollo: es la peor combinación de dispositivo y backend que estés dispuesto a soportar.