Lo que solo existe con WebGPU
Cómputo, buffers de almacenamiento, características del adaptador y salida HDR: las capacidades que el respaldo de WebGL2 no puede darte.
Si todo funcionara igual en los dos backends, migrar sería puramente estético. No es el caso: hay una lista concreta de capacidades que solo existen con WebGPU, y saber cuáles son es lo que te permite decidir si tu proyecto tiene una razón real para exigirlo o si el respaldo le basta de sobra.
- Enumerar las capacidades exclusivas del backend de WebGPU.
- Consultar las características del adaptador con
hasFeature. - Ramificar el comportamiento de la aplicación según el backend activo.
- Distinguir una diferencia de capacidad de una diferencia de rendimiento.
Cómputo de propósito general
Es la diferencia grande. WebGL2 solo sabe dibujar: para hacer cálculo general hay que disfrazarlo de render, escribiendo a texturas de coma flotante y leyéndolas después, con todas las limitaciones que eso impone. WebGPU tiene compute shaders de verdad, con grupos de trabajo, memoria compartida y barreras de sincronización.
En Three.js eso se expone así:
import { WebGPURenderer } from 'three/webgpu';
import { Fn, instancedArray, instanceIndex, vec3, deltaTime } from 'three/tsl';
const CUENTA = 100000;
const posiciones = instancedArray( CUENTA, 'vec3' );
const velocidades = instancedArray( CUENTA, 'vec3' );
const actualizar = Fn( () => {
const p = posiciones.element( instanceIndex );
const v = velocidades.element( instanceIndex );
v.addAssign( vec3( 0, -9.8, 0 ).mul( deltaTime ) );
p.addAssign( v.mul( deltaTime ) );
} )().compute( CUENTA );
// En el bucle:
renderer.compute( actualizar );
renderer.render( escena, camara );
instancedArray y storage crean buffers que viven en la GPU y persisten entre pasadas. compute() despacha el trabajo. Nada de esto tiene traducción al backend de WebGL2, y es la razón principal por la que un proyecto puede necesitar WebGPU de verdad.
Existen también computeAsync(), que inicializa el renderer si hace falta, y las primitivas atómicas y de grupo de trabajo —atomicAdd, workgroupArray, workgroupBarrier— para algoritmos que necesitan cooperación entre invocaciones.
Buffers y texturas de almacenamiento
Un storage buffer es memoria de GPU de lectura y escritura, de tamaño arbitrario, accesible desde cualquier shader. Es lo que permite estructuras de datos compartidas entre pasadas: listas de partículas, rejillas de aceleración, contadores, colas. WebGL2 solo tiene uniform buffers de solo lectura y con límites de tamaño mucho más estrictos.
Las texturas de almacenamiento son lo mismo aplicado a texturas: escribir a una textura desde un shader sin pasar por un pase de render. Sirven para generar mapas proceduralmente, para algoritmos de imagen en varias etapas, y para todo lo que en WebGL2 exige un pase de dibujado por escritura.
Características del adaptador
WebGPU tiene un sistema de características opcionales que el adaptador puede o no ofrecer. Three.js las expone a través de hasFeature, y sus nombres viven en el enumerado GPUFeatureName:
import { WebGPURenderer } from 'three/webgpu';
const renderer = new WebGPURenderer();
await renderer.init();
// Consultar SIEMPRE despues de init: antes lanza.
if ( renderer.hasFeature( 'timestamp-query' ) ) {
// medicion precisa disponible
}
if ( renderer.hasFeature( 'shader-f16' ) ) {
// media precision en shaders
}
Los nombres exactos del enumerado incluyen core-features-and-limits, depth-clip-control, depth32float-stencil8, texture-compression-bc, texture-compression-bc-sliced-3d, texture-compression-etc2, texture-compression-astc, texture-compression-astc-sliced-3d, timestamp-query, indirect-first-instance, shader-f16, rg11b10ufloat-renderable, bgra8unorm-storage, float32-filterable, float32-blendable, clip-distances, dual-source-blending, subgroups, texture-formats-tier1 y texture-formats-tier2.
Three.js pide todas las que el adaptador soporte al crear el dispositivo, así que si el hardware las tiene, están disponibles. Las tres familias de compresión de texturas son las más útiles en la práctica: saber cuál soporta el dispositivo te permite servir el formato correcto y ahorrar VRAM de verdad.
Con trackTimestamp: true en el constructor y la característica de marcas de tiempo presente, se puede medir el tiempo real de GPU:
const renderer = new WebGPURenderer( { trackTimestamp: true } );
await renderer.init();
// ... tras renderizar
const ms = await renderer.resolveTimestampsAsync( 'render' );
Eso es medición de GPU de verdad, no el tiempo de pared del hilo principal, y no tiene equivalente en el backend de WebGL2.
Salida de alto rango dinámico
Hay un ajuste que solo existe con el backend de WebGPU y que pocos conocen. Cuando outputType es HalfFloatType, el backend configura el lienzo en modo de tone mapping extendido:
const toneMappingMode = parameters.outputType === HalfFloatType ? 'extended' : 'standard';
Ese modo permite que el navegador entregue valores por encima del blanco de referencia a una pantalla HDR. En una pantalla capaz, un emisivo intenso deja de ser blanco recortado y se ve como luz de verdad. No tiene análogo en WebGL2.
Ramificar según el backend
La forma correcta de usar una capacidad exclusiva es preguntar después de inicializar y tener un plan B:
await renderer.init();
const hayComputacion = renderer.backend.constructor.name === 'WebGPUBackend';
if ( hayComputacion ) {
montarParticulasEnGPU( renderer, 200000 );
} else {
montarParticulasEnCPU( 8000 );
}
La segunda rama no tiene por qué ser un mensaje de error: casi siempre puede ser una versión más modesta del mismo efecto. Doscientas mil partículas simuladas en la GPU y ocho mil animadas en la CPU son experiencias distintas, pero las dos son experiencias.
WebGPU es más rápido que WebGL2 en escenas con muchos objetos, y la razón principal no es que dibuje mejor sino que valida menos por llamada. WebGL fue diseñado para validar exhaustivamente cada llamada, y ese coste de CPU es el cuello de botella de la mayoría de escenas complejas. WebGPU mueve esa validación al momento de crear el pipeline, que ocurre una vez. Es una mejora real y a veces grande, pero es una diferencia de grado, no de capacidad: la misma escena funciona en los dos sitios, solo que a distinto ritmo. Conviene no mezclar esa categoría con la de esta lección, que es lo que directamente no existe.
La tentación al leer una lista como la de esta lección es escribir un if sobre el backend y dos versiones de la aplicación. Casi siempre es el enfoque equivocado, por la misma razón que en la web la detección de navegador acabó siendo mala práctica frente a la detección de características: el backend no es lo que te importa, lo que te importa es si puedes hacer X. Preguntar por hasFeature('timestamp-query') es robusto y seguirá siendo correcto dentro de tres años; preguntar por el nombre de la clase del backend es frágil y no distingue entre un adaptador potente y uno limitado, que pueden diferir tanto entre sí como difieren de WebGL2. Ahora bien, hay una excepción importante y conviene reconocerla, porque es justo la que aparece en esta lección: el cómputo no es una característica, es una arquitectura. Cuando tu escena simula doscientas mil partículas en la GPU, la ausencia de compute shaders no se degrada con un ajuste; obliga a un diseño completamente distinto, con otra estructura de datos, otro presupuesto y otro aspecto visual. Ahí la ramificación no es un if alrededor de una llamada, es un if alrededor de dos implementaciones. Y de ahí sale la regla que de verdad organiza este territorio: usa detección de características para todo lo que sea un ajuste, y detección de backend solo para lo que sea una decisión de arquitectura. Si te encuentras ramificando por backend para cambiar un parámetro, casi seguro que había una característica que preguntar. Y si te encuentras ramificando por característica para elegir entre dos motores de simulación distintos, probablemente estabas ocultando una decisión que merecía verse.