Saltar al contenido

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

JS
// 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:

Texto
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í:

Texto
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

JS
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.