La inicialización asíncrona
Por qué WebGPU obliga a esperar antes de dibujar, qué hace init exactamente, y los dos patrones válidos según cómo dispares el render.
Por qué hace falta esperar
En WebGL el navegador te entrega un contexto de inmediato. En WebGPU hay dos pasos, y los dos devuelven promesas.
Primero se pide un adaptador, que representa una implementación concreta de la GPU disponible en el sistema. El navegador puede tener que consultar al sistema operativo, decidir entre una GPU integrada y una discreta según la preferencia de energía, y comprobar listas de bloqueo por controlador. Después se pide un dispositivo a ese adaptador, declarando qué características y qué límites necesitas, y el navegador crea el objeto con la configuración negociada.
Nada de eso puede resolverse sin ceder el hilo, así que la API es asíncrona por diseño y no hay forma de esquivarlo.
El código de Three.js que lo hace es directo:
const adapter = ( typeof navigator !== 'undefined' ) ? await navigator.gpu.requestAdapter( adapterOptions ) : null;if ( adapter === null ) { throw new Error( 'WebGPUBackend: Unable to create WebGPU adapter.' );}Ese throw es la puerta por la que entra el mecanismo de respaldo, que es el tema de la lección siguiente.
Qué hace init
async init() { if ( this._initPromise !== null ) { return this._initPromise; } // ... crea el backend, gestiona el respaldo, monta los gestores internos}Tres propiedades que conviene saber.
Es idempotente. La promesa se memoriza, así que llamar a init() diez veces devuelve la misma promesa y solo inicializa una vez. No hace falta protegerlo con banderas.
Resuelve con el propio renderer, así que const r = await renderer.init() devuelve el mismo objeto y permite encadenar.
Cuando resuelve, todo está listo: el backend definitivo está elegido, los gestores internos construidos, el bucle de animación arrancado y renderer.initialized vale true.
Los dos patrones
Patrón uno: bucle continuo
Si tu aplicación dibuja en cada frame, no tienes que hacer nada. setAnimationLoop es asíncrono y espera la inicialización por dentro:
async setAnimationLoop( callback ) { if ( this._initialized === false ) await this.init(); this._animation.setAnimationLoop( callback );}Así que esto es correcto tal cual:
const renderer = new THREE.WebGPURenderer( { antialias: true } );renderer.setAnimationLoop( () => { controles.update(); renderer.render( escena, camara );} );El callback no se ejecutará hasta que el backend esté vivo, así que el render() de dentro nunca encuentra un renderer sin inicializar. Ni siquiera hace falta poner await delante de setAnimationLoop.
Patrón dos: render bajo demanda
Si dibujas solo cuando algo cambia —un configurador de producto, un visor que solo se actualiza al mover la cámara— tienes que esperar tú:
const renderer = new THREE.WebGPURenderer( { antialias: true } );document.body.appendChild( renderer.domElement );await renderer.init();function dibujar() { renderer.render( escena, camara );}controles.addEventListener( 'change', dibujar );addEventListener( 'resize', () => { renderer.setSize( innerWidth, innerHeight ); camara.aspect = innerWidth / innerHeight; camara.updateProjectionMatrix(); dibujar();} );dibujar();Si te lo saltas, el error es explícito:
Renderer: .render() called before the backend is initialized.Use "await renderer.init();" before rendering.Y la documentación del propio método resume las dos situaciones:
This method can only be called if the renderer has been initialized. Whenusing render() inside an animation loop, it's guaranteed the renderer willbe initialized. The animation loop must be defined with setAnimationLoopthough. For all other use cases (like when using on-demand rendering), youmust call init() before rendering.Qué hacer mientras se espera
En la práctica la inicialización tarda unas decenas de milisegundos, no es un problema de rendimiento. Pero sí es un punto donde algo puede fallar, y merece un manejo explícito:
async function arrancar() { const renderer = new THREE.WebGPURenderer( { antialias: true } ); try { await renderer.init(); } catch ( error ) { // Ni WebGPU ni el respaldo de WebGL2 han funcionado. mostrarMensajeSinSoporte( error ); return; } document.body.appendChild( renderer.domElement ); montarEscena( renderer );}Que init() rechace es raro pero real: significa que ni WebGPU ni el respaldo de WebGL2 han conseguido arrancar. Ocurre en entornos virtualizados sin aceleración, con controladores en lista de bloqueo, o en máquinas muy antiguas. Un mensaje decente en ese caso vale más que una pantalla en blanco.
Aprovecha también ese punto para consultar qué has conseguido de verdad:
await renderer.init();console.log( renderer.backend.constructor.name ); // WebGPUBackend o WebGLBackendconsole.log( renderer.coordinateSystem === THREE.WebGPUCoordinateSystem );Esa consulta solo dice la verdad después de init(), porque el backend puede haberse sustituido durante la inicialización.
Otros métodos que esperan por ti
El mismo patrón de auto-inicialización aparece en varios sitios:
async computeAsync( computeNodes, dispatchSize = null ) { if ( this._initialized === false ) await this.init(); this.compute( computeNodes, dispatchSize );}computeAsync, resolveTimestampsAsync e initTextureAsync llevan esa misma guarda. Nota que computeAsync no está obsoleto, al contrario que renderAsync: ahí la variante asíncrona sigue siendo la forma normal de trabajar.