wandres.dev
ONTOLOGÍA · El mapa del 3D en la web

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.

⏱ 17 min

Entre el hardware que calcula píxeles y la línea donde escribes scene.add(malla) hay seis capas, cada una con un contrato distinto y un motivo histórico para existir. Saber cuál es cuál no es trivia: cuando un shader se comporta distinto en un móvil, cuando una extensión no está disponible, o cuando un mensaje de error habla de un contexto perdido, el diagnóstico depende por completo de en qué capa ocurrió el problema.

🎯 Al terminar esta lección sabrás
  • Nombrar las seis capas entre el hardware gráfico y el código de aplicación.
  • Explicar qué contrato garantiza cada capa y qué deja indefinido.
  • Situar Three.js con precisión: qué abstrae y qué deja pasar sin filtrar.
  • Reconocer en qué capa vive un problema a partir de sus síntomas.

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.

flowchart TB
app[Capa 6 tu aplicacion] --> lib[Capa 5 biblioteca de render]
lib --> web[Capa 4 API web WebGL2 o WebGPU]
web --> nativa[Capa 3 API nativa Vulkan Metal D3D]
nativa --> driver[Capa 2 driver de la GPU]
driver --> hw[Capa 1 hardware]
style app fill:#a6e3a1,color:#11111b
style lib fill:#89b4fa,color:#11111b
style web fill:#cba6f7,color:#11111b
style nativa fill:#f9e2af,color:#11111b
style driver fill:#f38ba8,color:#11111b
style hw fill:#fab387,color:#11111b

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.

La capa 4 no es una API, es un contrato de portabilidad, y por eso miente un poco

WebGL2 y WebGPU no son solo interfaces: son promesas de que el mismo código produce el mismo resultado en hardware muy distinto. Para poder prometer eso, ambos definen mínimos garantizados y dejan el resto indefinido, y ahí es donde vive el sufrimiento real. La precisión de los flotantes en el fragment shader de un móvil puede ser mediump efectivo aunque pidas highp; el número de unidades de textura, el tamaño máximo de textura o el número de atributos de vértice varían por dispositivo; y el orden en que el driver optimiza tu shader puede cambiar el resultado de una comparación al borde de la precisión. Nada de esto es un fallo del navegador: es el contrato funcionando como se diseñó. La consecuencia práctica es que una escena que se ve bien en tu portátil no está verificada, y que los bugs de precisión aparecen precisamente en los dispositivos donde no puedes conectar un depurador. Consulta límites reales en lugar de asumirlos y prueba en hardware modesto pronto, no al final.

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.

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.