Saltar al contenido

Las capas: del silicio a tu código, y dónde entra Three.js

Las seis capas que separan un transistor de una línea de JavaScript, qué responsabilidad tiene cada una y qué se rompe cuando intentas saltarte una.

Seis capas y un contrato por capa

El hardware. Una GPU es un procesador con miles de unidades aritméticas organizadas en grupos que ejecutan la misma instrucción sobre datos distintos. No es un procesador lento con muchos núcleos: es una máquina con una filosofía opuesta a la de una CPU. La CPU minimiza la latencia de un hilo; la GPU maximiza el rendimiento agregado y esconde la latencia cambiando de hilo. Todo lo que viene por encima existe para alimentar esa máquina sin dejarla ociosa.

El driver. Traduce las llamadas de una API gráfica a los comandos del hardware concreto, compila los shaders al lenguaje máquina de esa GPU y gestiona la memoria de vídeo. Es la capa donde vive la mayor parte de la variabilidad del mundo real: dos móviles con la misma versión de WebGL pueden comportarse distinto porque sus drivers optimizan distinto o tienen fallos distintos.

La API nativa. Vulkan, Metal, Direct3D. Es la interfaz que expone el sistema operativo. El navegador no implementa gráficos: los delega aquí.

La API web. WebGL2 y WebGPU. Es la frontera de seguridad y de portabilidad. Un navegador no puede pasar tus comandos al driver tal cual, porque eso permitiría leer memoria de otros procesos o colgar el sistema; los valida, los traduce y a menudo los reescribe. En Chrome, esa traducción la hace un proceso separado con su propia capa de compatibilidad. Ese salto de proceso es la razón de que las llamadas de dibujo tengan un coste de CPU que no tienen en una aplicación nativa.

La biblioteca de render. Three.js, Babylon, PlayCanvas. Convierte una descripción de escena en llamadas a la API web.

La aplicación. Tu código: el modelo del mundo, la interacción, la interfaz.

Cada flecha del diagrama es una frontera con coste. La más cara con diferencia es la que va de la capa 5 a la capa 4, y no por el trabajo gráfico, sino porque cada llamada cruza la validación del navegador. Esa es la razón profunda de que en la web «reducir draw calls» sea un consejo aún más importante que en el escritorio.

Qué abstrae Three.js exactamente

La respuesta corta es: abstrae la gestión, no el modelo. Y la distinción es la clave del track entero.

Three.js te libera de crear y actualizar búferes, de compilar y enlazar programas, de recordar qué textura está en qué unidad, de escribir el mismo shader de iluminación por décima vez, de calcular la matriz de proyección a mano y de implementar shadow maps desde cero. Todo eso es fontanería que no cambia de un proyecto a otro.

Three.js no te libera del modelo mental de la GPU. Sigues teniendo vértices que se transforman en un shader y fragmentos que se colorean en otro. Sigues teniendo un búfer de profundidad con sus artefactos. Sigue habiendo estados que hacen que la transparencia sea dura de ordenar. La API tiene nombres amables, pero cada nombre amable mapea a un concepto de hardware, y cuando algo falla, falla en términos del hardware.

Esta es la razón por la que este track dedica cuatro niveles a la teoría antes del primer cubo. No es purismo académico: es que la abstracción de Three.js filtra, en el sentido de la ley de las abstracciones con fugas. Puedes escribir mesh.rotation.y += 0.01 sin saber nada de cuaterniones, y funciona; pero el día que dos rotaciones se comporten de forma extraña, la explicación no está en la documentación de rotation, está en la representación interna. Todo lo que Three.js oculta acaba emergiendo en forma de bug.

Diagnosticar por capas

El valor operativo del modelo es que cada capa produce síntomas reconocibles.

Síntoma Capa probable Qué mirar
El objeto no aparece pero no hay error 5 o 6 Escala, posición, cámara, material, frustum
Error de compilación de shader en consola 4 y 5 El GLSL generado, con el mensaje del driver
Se ve bien en escritorio y mal en móvil 2 y 4 Precisión, límites del dispositivo, memoria
Rendimiento cae al añadir objetos distintos 5 Número de órdenes de dibujo y de cambios de estado
Rendimiento cae al ampliar la ventana 1 Coste por píxel, resolución efectiva, sombreado
Pantalla en negro tras un rato 2 y 4 Contexto perdido, memoria de vídeo agotada

Merece la pena detenerse en las dos filas de rendimiento, porque separan los dos únicos cuellos de botella que existen. Si el coste sube con el número de objetos distintos, el cuello está en la CPU emitiendo órdenes: la GPU está esperando. Si el coste sube con la cantidad de píxeles o con la complejidad del sombreado, el cuello está en la GPU: la CPU está esperando. Son problemas opuestos con soluciones opuestas, y la prueba para distinguirlos es tan simple como reducir la ventana a la mitad y ver si mejora.

La última fila también merece una nota. El contexto de WebGL se puede perder: si el sistema recupera la GPU, si otra pestaña consume demasiado, si el driver se reinicia. Cuando ocurre, todos los recursos de GPU dejan de existir. Una aplicación seria escucha webglcontextlost en el canvas y reacciona; la mayoría no lo hace y se queda en negro.

JS
import * as THREE from 'three';const canvas = document.createElement('canvas');document.body.appendChild(canvas);const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });renderer.setSize(640, 480);canvas.addEventListener('webglcontextlost', (evento) => {  // Sin preventDefault el navegador no intentara restaurar el contexto.  evento.preventDefault();  console.warn('contexto perdido: todos los recursos de GPU han desaparecido');});canvas.addEventListener('webglcontextrestored', () => {  console.info('contexto restaurado: hay que volver a subirlo todo');});// Limites reales del dispositivo, no los que asumes.const gl = renderer.getContext();console.log('tamano maximo de textura:', gl.getParameter(gl.MAX_TEXTURE_SIZE));console.log('unidades de textura:', gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS));console.log('atributos de vertice:', gl.getParameter(gl.MAX_VERTEX_ATTRIBS));console.log('revision de three:', THREE.REVISION);

renderer.getContext() devuelve el contexto real, y es la puerta de escape legítima hacia la capa 4 cuando necesitas saber algo que la biblioteca no expone. Usarla para consultar límites es sano; usarla para emitir comandos por tu cuenta rompe la caché de estado del motor y produce bugs imposibles de reproducir.

Con las capas colocadas, la comparación entre WebGL y WebGPU deja de ser una lista de características y pasa a ser lo que realmente es: dos contratos distintos en la capa 4.