Migrar desde WebGLRenderer
Qué cambia exactamente en la API al sustituir el renderer, qué parámetros de constructor son distintos, y qué métodos han desaparecido o cambiado de nombre.
El cambio mínimo
// Antesimport * as THREE from 'three';const renderer = new THREE.WebGLRenderer( { antialias: true } );// Despuesimport * as THREE from 'three/webgpu';const renderer = new THREE.WebGPURenderer( { antialias: true } );Dos líneas. El resto de la escena —Scene, PerspectiveCamera, BufferGeometry, Mesh, las luces, GLTFLoader, OrbitControls— sigue igual, porque three/webgpu incluye el núcleo completo de Three.js.
Lo que sí cambia es que a partir de aquí hay una inicialización asíncrona de la que ocuparse. Con setAnimationLoop no hace falta hacer nada, porque el método la espera por dentro; con render bajo demanda sí, y eso es el tema de la lección siguiente.
Parámetros de constructor
WebGPURenderer documenta estos, con sus valores por defecto:
| Parámetro | Por defecto | Nota |
|---|---|---|
antialias |
false |
activa MSAA con 4 muestras |
samples |
0 |
número de muestras si quieres otro que 4 |
alpha |
true |
en WebGLRenderer el defecto es false |
depth |
true |
|
stencil |
false |
|
logarithmicDepthBuffer |
false |
|
reversedDepthBuffer |
false |
no existe en WebGLRenderer |
forceWebGL |
false |
fuerza el backend de WebGL2 |
multiview |
false |
para renderizado en WebXR |
outputType |
sin definir | tipo de textura de salida al canvas |
outputBufferType |
HalfFloatType |
tipo de los buffers intermedios |
Tres diferencias merecen atención.
alpha vale true por defecto, al contrario que en WebGLRenderer. Si tu escena asumía un lienzo opaco, puede que notes la diferencia al componer sobre la página.
outputBufferType vale HalfFloatType por defecto, según su propia documentación por calidad. Ponerlo a UnsignedByteType ahorra memoria y ancho de banda a costa de la calidad, y es un ajuste razonable en móviles de gama baja.
El antialiasing se pide igual pero se resuelve distinto. samples permite fijar un número de muestras que no sea el predeterminado, algo que en WebGLRenderer no está expuesto de esta forma.
Además, el backend acepta parámetros propios que se pasan por el mismo objeto: canvas para usar un lienzo existente, device para reutilizar un GPUDevice que ya tengas, requiredLimits para pedir límites concretos al adaptador, powerPreference, y trackTimestamp para habilitar la medición por consultas de marca de tiempo.
Métodos: lo que cambia
| Método | Estado en r184 |
|---|---|
render( scene, camera ) |
existe, lanza si el renderer no está inicializado |
init() |
nuevo, asíncrono, devuelve una promesa |
renderAsync( scene, camera ) |
obsoleto desde r181 |
hasFeature( name ) |
existe, lanza si no está inicializado |
hasFeatureAsync( name ) |
obsoleto desde r181 |
compute( nodes, dispatchSize ) |
nuevo |
computeAsync( nodes, dispatchSize ) |
existe y no está obsoleto |
waitForGPU() |
eliminado, su cuerpo solo emite un error |
resolveTimestampsAsync( tipo ) |
nuevo |
getArrayBufferAsync( atributo ) |
nuevo, lee de vuelta datos de la GPU |
setAnimationLoop( cb ) |
existe, ahora es asíncrono e inicializa |
setSize, setPixelRatio, setDrawingBufferSize |
igual |
getContext() |
devuelve GPUCanvasContext o WebGL2RenderingContext |
coordinateSystem |
getter nuevo, depende del backend |
initialized / hasInitialized() |
nuevos |
Los mensajes de obsolescencia de r181 son explícitos sobre qué hacer:
Renderer: "renderAsync()" has been deprecated. Use "render()" and"await renderer.init();" when creating the renderer.Es decir, el patrón recomendado ha pasado de esperar en cada frame a esperar una sola vez al arrancar. Si has visto código con await renderer.renderAsync( escena, camara ) dentro del bucle, está desactualizado y además era más lento de lo necesario.
Tone mapping y espacio de color
Las propiedades son las mismas —toneMapping, toneMappingExposure, outputColorSpace— y sus valores por defecto también: sin tone mapping y salida en sRGB. Lo que cambia es dónde se aplican, y eso tiene consecuencias visibles.
En WebGLRenderer, el tone mapping y la conversión de espacio de color se inyectan dentro del fragment shader de cada material, como los dos últimos chunks del main(). Por eso material.toneMapped = false funciona: es una decisión por material que entra incluso en la clave de caché del programa.
En WebGPURenderer se aplican en un pase de render aparte. La documentación interna lo justifica así:
Returns an internal render target which is used when computing the outputtone mapping and color space conversion. Unlike in WebGLRenderer, this isdone in a separate render pass and not inline to achieve more correct results.Y la consecuencia práctica está escrita en la documentación de Material.toneMapped: esa propiedad se ignora cuando se usa WebGPURenderer. Todos los materiales pasan por el tone mapping, sin excepciones.
Si tu escena dependía de excluir un material concreto —un elemento de interfaz superpuesto, un fondo que debía llegar intacto— ese truco deja de funcionar y hay que resolverlo de otra manera, normalmente compensando el valor para que el tone mapping lo devuelva donde querías.
Un ejemplo migrado completo
import * as THREE from 'three/webgpu';import { OrbitControls } from 'three/addons/controls/OrbitControls.js';import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';const renderer = new THREE.WebGPURenderer( { antialias: true } );renderer.setPixelRatio( Math.min( devicePixelRatio, 2 ) );renderer.setSize( innerWidth, innerHeight );renderer.toneMapping = THREE.ACESFilmicToneMapping;renderer.shadowMap.enabled = true;document.body.appendChild( renderer.domElement );const escena = new THREE.Scene();const camara = new THREE.PerspectiveCamera( 50, innerWidth / innerHeight, 0.1, 200 );camara.position.set( 4, 2, 6 );const controles = new OrbitControls( camara, renderer.domElement );controles.enableDamping = true;const sol = new THREE.DirectionalLight( 0xffffff, 3 );sol.position.set( 5, 8, 3 );sol.castShadow = true;escena.add( sol, new THREE.HemisphereLight( 0x99bbff, 0x221100, 0.5 ) );new GLTFLoader().load( '/modelo.glb', ( gltf ) => { gltf.scene.traverse( ( o ) => { if ( o.isMesh ) o.castShadow = o.receiveShadow = true; } ); escena.add( gltf.scene );} );addEventListener( 'resize', () => { renderer.setSize( innerWidth, innerHeight ); camara.aspect = innerWidth / innerHeight; camara.updateProjectionMatrix();} );renderer.setAnimationLoop( () => { controles.update(); renderer.render( escena, camara );} );Comparado con la versión de WebGLRenderer, las diferencias son las dos primeras líneas. Todo lo demás es idéntico, incluidas las sombras, el cargador y los controles.