Inspeccionar un frame: Spector.js y el Inspector de Three.js
Cómo se captura y se lee la lista completa de comandos de un frame de WebGL, qué preguntas responde que ninguna otra herramienta contesta, y qué aporta el inspector integrado de r184 sobre WebGPU.
Cuando el diagnóstico dice “limitado por GPU” y ya has bajado la resolución, hace falta ver qué está dibujando realmente el motor. Un capturador de frames intercepta todas las llamadas a la API gráfica durante un fotograma y te devuelve la lista completa, con su estado, sus texturas y su resultado visual paso a paso. Es la única herramienta que responde preguntas del tipo “¿por qué hay ochocientos draw calls si solo tengo doscientos objetos?” o “¿esta textura está subida como RGBA de 32 bits?”.
- Capturar un frame de WebGL con Spector.js e interpretar su lista de comandos.
- Localizar draw calls inesperados, cambios de estado redundantes y texturas mal configuradas.
- Usar el
Inspectorde Three.js conWebGPURendererpara ver tiempos por pase. - Elegir la herramienta adecuada según el backend y el tipo de pregunta.
Spector.js
Spector.js es un capturador de frames para WebGL, disponible como extensión de navegador y como biblioteca embebible. Su modelo es simple: se coloca entre tu código y el contexto de WebGL, registra todas las llamadas de un frame, y presenta el resultado en un visor.
<script src="https://spectorcdn.babylonjs.com/spector.bundle.js"></script>
<script>
const spector = new SPECTOR.Spector();
spector.displayUI();
</script>
Con displayUI() aparece una barra flotante con un botón de captura. También se puede disparar por código, que es más práctico cuando el frame problemático solo ocurre en un momento concreto:
const spector = new SPECTOR.Spector();
spector.onCapture.add( ( resultado ) => {
// El resultado es un objeto JSON con todo el frame.
console.log( resultado );
} );
// Capturar el proximo frame de este canvas.
document.addEventListener( 'keydown', ( e ) => {
if ( e.key === 'c' ) spector.captureCanvas( renderer.domElement );
} );
La captura es cara —intercepta y serializa cada llamada— así que el frame capturado tarda muchísimo más de lo normal. Eso es esperado y no invalida nada: Spector no mide tiempos, describe qué se hace.
Qué leer en la captura
El visor muestra la lista de comandos agrupada por pasadas de dibujo, y cada comando lleva el estado completo en ese instante. Estas son las cinco preguntas que responde y que ninguna otra herramienta contesta con la misma precisión.
¿Cuántos draw calls hay de verdad, y de dónde salen? renderer.info te da el número; Spector te dice cuáles. Es donde descubres que el mapa de sombras dibuja la escena entera otra vez, que el post-proceso tiene siete pases en lugar de tres, o que un EffectComposer está copiando entre render targets más veces de las necesarias.
¿Qué se está redibujando encima de qué? Cada draw call muestra una miniatura del framebuffer después de ejecutarse. Recorriendo la secuencia se ve el overdraw literalmente: si la misma zona de pantalla se repinta ocho veces, ahí está tu problema de fragmentos.
¿Cómo están configuradas las texturas? Cada textura muestra su formato, tamaño, mipmaps, filtrado y wrapping. Es la forma más rápida de encontrar una textura de 4096 por 4096 sin comprimir consumiendo 64 MB por sí sola, o una que debería estar en sRGB y está en lineal, o una que se ha subido sin mipmaps y por eso brilla al alejarse.
¿Cuántos cambios de estado hay? Cada useProgram, bindTexture, bindBuffer y enable aparece. Un número desproporcionado de cambios de programa entre draw calls indica que los objetos no están ordenados por material, lo que es un coste de CPU y de driver que no aparece en ningún contador de Three.js.
¿Qué contiene exactamente este uniform? Con el valor de cada uniform en cada draw call se resuelven en minutos los bugs de “no se ve nada”: una matriz mal construida, una luz con intensidad cero, un opacity que quedó a cero de una animación anterior.
Spector es solo para WebGL. No funciona con WebGPU, ni con el WebGPURenderer cuando este ha caído a su backend de WebGL, porque la instrumentación se hace sobre el contexto y hay que activarla antes de crearlo. Para WebGPU, las herramientas de captura equivalentes son las que traen los navegadores en sus canales de desarrollo, todavía menos maduras.
Un frame de una escena en reposo no dice nada. Lo que hay que capturar es el frame en el que ocurre el problema: el que tarda 80 ms, el que aparece cuando la cámara mira hacia cierto sitio, el que sigue a la carga de un trozo. Dispara la captura por código desde el evento que la provoca, o desde un temporizador sincronizado, y no desde el botón, porque para cuando llegas al botón el frame malo ya pasó.
El Inspector de Three.js
La r184 trae un inspector propio en los addons, pensado para WebGPURenderer y con una capacidad que Spector no tiene: medir tiempo de GPU por pase.
import * as THREE from 'three/webgpu';
import { Inspector } from 'three/addons/inspector/Inspector.js';
const renderer = new THREE.WebGPURenderer( { antialias: true } );
renderer.inspector = new Inspector();
document.body.appendChild( renderer.domElement );
await renderer.init();
Con esas dos líneas aparece un panel acoplado al canvas con varias pestañas: rendimiento, memoria, parámetros, consola, línea de tiempo y ajustes. Lo que hace por debajo es lo interesante: al asignarlo, el inspector activa renderer.backend.trackTimestamp y comprueba si el dispositivo soporta timestamp-query; si no lo soporta, lo dice explícitamente en su consola en lugar de mostrar ceros silenciosamente.
La pestaña de rendimiento separa, por cada pase, el tiempo de CPU y el de GPU, y calcula además un residuo llamado miscellaneous que es la diferencia entre la duración real del frame y la suma de lo medido. Ese residuo es información valiosa por sí misma: si es grande, hay trabajo que no está pasando por el renderer, que suele ser tu lógica o el navegador.
La pestaña de memoria muestra los contadores de info.memory con sus tamaños en bytes, que como viste es la información que WebGL no da.
Y hay dos integraciones con TSL que merece la pena conocer. renderer.inspector.createParameters( nombre ) devuelve un grupo donde exponer uniforms con controles, sin necesidad de traer una biblioteca de interfaz aparte:
const gui = renderer.inspector.createParameters( 'Simulacion' );
gui.add( gravedad, 'value', - 0.02, 0, 0.0001 ).name( 'gravedad' );
gui.add( rebote, 'value', 0.1, 1, 0.01 ).name( 'rebote' );
Y .toInspector( nombre ) sobre un nodo de TSL lo hace visible como salida intermedia en el visor, que es la forma más directa de depurar un grafo de shading: puedes ver el resultado de un paso concreto de tu cadena de post-proceso sin desmontar nada.
const escena = pass( scene, camera ).toInspector( 'Escena' );
const desenfocada = gaussianBlur( escena.getTextureNode() ).toInspector( 'Desenfoque' );
Qué herramienta para qué pregunta
| Pregunta | Herramienta |
|---|---|
| Cuánto tarda mi JavaScript | panel de rendimiento del navegador |
| Cuánto trabajo estoy enviando | renderer.info |
| Está el cuello en CPU o en GPU | pruebas de aislamiento |
| Qué comandos exactos se emiten | Spector.js, solo WebGL |
| Cuánto tarda cada pase en la GPU | Inspector, solo WebGPU |
| Qué contiene este uniform ahora mismo | Spector.js |
| Cuánta memoria de GPU consumo | info.memory de WebGPURenderer |
| Por qué esta textura se ve mal | Spector.js |
El error de uso más común con Spector no es técnico sino de propósito: se abre esperando que señale qué optimizar, y no hace eso. Una captura no tiene tiempos —la propia instrumentación los destruye— así que no puede ordenar nada por coste ni decirte qué es caro. Lo que hace es responder a la pregunta “¿qué está pasando realmente?”, que es una pregunta distinta y que se vuelve valiosa exactamente en un momento concreto: cuando lo que mides no coincide con lo que esperas. Los casos donde una captura resuelve en cinco minutos lo que de otro modo son horas tienen todos la misma forma. renderer.info dice 250 draw calls, tu escena tiene 80 objetos, y no entiendes de dónde salen los otros 170: la captura te muestra que hay tres luces con sombra y cada una redibuja la escena entera. Has bajado la resolución y la GPU sigue saturada: la captura te enseña que el EffectComposer está renderizando a resolución completa internamente porque no le pasaste el pixelRatio. Un modelo se ve más oscuro de lo que debería y has revisado el material diez veces: la captura muestra que su textura está subida en espacio lineal en lugar de sRGB. En los tres casos el problema no era de rendimiento sino de modelo mental equivocado, y ninguna herramienta de perfilado los habría encontrado porque las tres estaban midiendo correctamente algo que tú interpretabas mal. De ahí la regla que le da su sitio a esta herramienta: usa el perfilador cuando sabes qué está pasando y quieres saber qué cuesta; usa la captura cuando no sabes qué está pasando. Y hay un corolario que vale para todo el nivel: la mayor parte del tiempo perdido optimizando escenas 3D no se pierde optimizando lo que no toca, sino optimizando un problema que no existía, porque la medida decía una cosa y se leyó como otra. Verificar lo que crees antes de actuar sobre ello es más rentable que cualquier técnica.
- Instala Spector.js y captura un frame de una escena con sombras y post-proceso.
- Cuenta los draw calls de la captura y compáralos con
renderer.info.render.calls. - Recorre las miniaturas del framebuffer e identifica dónde hay overdraw.
- Revisa el formato y el tamaño de todas las texturas y busca la que más memoria consume.
- Monta la misma escena con
WebGPURenderery elInspector, y compara el tiempo de GPU por pase.