⏱️ Lectura: 17 min
Cargar 526.000 objetos en una escena 3D debería colapsar cualquier navegador: esa cifra multiplicada por una llamada de dibujo por objeto satura a cualquier GPU en el primer fotograma. Space Now, el visualizador de sistema solar del proyecto Belle Lune 2, anuncia ese catálogo (526.000 asteroides y satélites, al 30 de septiembre de 2026) y lo muestra con cámara libre y sin caída de cuadros gracias a una técnica de la GPU conocida como instancing en WebGL.
📑 En este artículo
- TL;DR
- ¿Qué es el instancing en WebGL?
- Por qué importa renderizar así en el navegador
- Cómo funciona el renderizado por instancias
- Ejemplos prácticos y cómo empezar
- El caso de Space Now: un catálogo completo del sistema solar en el navegador
- Casos de uso reales del instancing más allá de la astronomía
- Errores comunes y buenas prácticas
- Comparativa con alternativas de renderizado masivo
- Profundizando: qué pasa dentro de la GPU
- Preguntas frecuentes
- ¿Qué es el instancing en WebGL y en qué se diferencia de dibujar cada objeto por separado?
- ¿Cuántas instancias soporta InstancedMesh de Three.js sin perder fluidez?
- ¿Necesito WebGL2 para usar geometría instanciada?
- ¿Puedo animar cada asteroide de forma independiente con renderizado por instancias?
- ¿Sirve el renderizado por instancias para satélites en órbita real, no solo para asteroides estáticos?
- ¿Qué motor de renderizado usa Space Now para mostrar 526.000 objetos?
- Referencias
La misma técnica sostiene bosques de miles de árboles en videojuegos y sistemas de partículas, y ahora catálogos astronómicos enteros dentro de una pestaña del navegador. Entender cómo funciona explica por qué una laptop con GPU integrada mueve más objetos en pantalla que un servidor dedicado a fuerza bruta.
TL;DR
- El instancing en WebGL agrupa miles de objetos idénticos en una sola llamada de dibujo en vez de una por objeto.
- Three.js expone la técnica con InstancedMesh: una geometría, un material y una matriz por instancia.
- Space Now anuncia 526.000 asteroides y satélites al 30 de septiembre de 2026 en un único canvas WebGL.
- renderer.info.render.calls confirma en consola cuántas llamadas de dibujo emite tu escena.
- Una textura de datos reemplaza al buffer de matrices cuando las instancias superan el límite práctico de un atributo.
¿Qué es el instancing en WebGL?
El instancing en WebGL es una técnica de renderizado que pide a la tarjeta gráfica dibujar múltiples copias de una misma geometría, como un asteroide o un satélite, en una sola llamada de dibujo, diferenciando cada copia solo por una matriz de posición, rotación y escala.
La idea central es simple. La CPU sube una sola vez la forma del objeto, por ejemplo los vértices de una esfera o de un icosaedro irregular, y después sube aparte una lista de transformaciones con la posición, el tamaño y la orientación de cada copia. La GPU combina ambas cosas en paralelo y dibuja todo de una vez.
WebGL hereda esta capacidad de OpenGL ES. En WebGL 1 llega vía la extensión ANGLE_instanced_arrays; en WebGL 2 ya es parte del núcleo de la API, sin pedir permiso extra al navegador para usarla.
Por qué importa renderizar así en el navegador
Cada llamada de dibujo (draw call) tiene un costo fijo que no depende del tamaño del objeto. El motor de JavaScript arma el comando, el driver de la GPU lo valida, y el navegador cruza la frontera entre el hilo de JavaScript y el proceso de composición gráfica, y ese costo se paga aunque el objeto sea un cubo de un solo triángulo.
Con un asteroide por llamada, un catálogo de algunas decenas de miles de objetos ya empieza a comerse el presupuesto de un fotograma completo, que en un navegador a 60 cuadros por segundo dura apenas 16,6 milisegundos. El cuello de botella no es la GPU dibujando triángulos: es la comunicación CPU-GPU repetida miles de veces por segundo.
El instancing convierte el problema en uno solo. Reduce todo a una llamada y un cruce de frontera, y deja que la carga de posicionar cientos de miles de copias se resuelva dentro de la GPU, que está diseñada para paralelizar exactamente ese tipo de trabajo.
💭 Clave: subir 526.000 matrices completas en cada fotograma sería casi tan costoso como el problema original; la optimización real está en actualizar solo las matrices que cambiaron, no todo el buffer de golpe.
Cómo funciona el renderizado por instancias
En un render normal, cada objeto recorre el pipeline de la GPU con su propia llamada, activando su geometría, subiendo su matriz de transformación y disparando drawArrays o drawElements. Repetir esto 526.000 veces por fotograma es, en la práctica, imposible a 60 cuadros por segundo.
El renderizado por instancias cambia el flujo: la geometría y el material se activan una sola vez, y junto a los atributos normales de cada vértice (posición, normal, UV) se agrega un atributo extra que varía por instancia y no por vértice. Ese atributo suele ser una matriz 4×4 de transformación, guardada en un InstancedBufferAttribute.
Dentro del vertex shader, la GPU multiplica la posición de cada vértice por la matriz de la instancia que le toca, identificada con la variable interna gl_InstanceID. Como resultado, la misma malla de un asteroide se repite 526.000 veces, cada una en su propio lugar, tamaño y orientación, todo dentro de una sola llamada drawArraysInstanced o drawElementsInstanced.
flowchart LR
subgraph SinInstancing["Sin instancing"]
A1["Objeto 1"] --> D1["Draw call 1"]
A2["Objeto 2"] --> D2["Draw call 2"]
A3["Objeto N"] --> D3["Draw call N"]
end
subgraph ConInstancing["Con instancing"]
B1["Geometria + N matrices"] --> D4["1 draw call"]
end
sequenceDiagram
participant JS as JavaScript
participant Buf as Buffer de instancias
participant GPU as GPU
JS->>Buf: escribe una matriz 4x4 por asteroide
Buf->>GPU: sube el buffer completo una vez
JS->>GPU: drawElementsInstanced(count)
GPU->>GPU: multiplica cada vertice por su matriz via gl_InstanceID
GPU-->>JS: un solo draw call renderiza todas las instancias
Ejemplos prácticos y cómo empezar
El ejemplo más simple posible: cinco cubos idénticos en cinco posiciones distintas, todos en una sola llamada de dibujo.
import * as THREE from 'three';
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({ color: 0x8899ff });
const count = 5;
const mesh = new THREE.InstancedMesh(geometry, material, count);
const dummy = new THREE.Object3D();
for (let i = 0; i < count; i++) {
dummy.position.set(i * 2, 0, 0);
dummy.updateMatrix();
mesh.setMatrixAt(i, dummy.matrix);
}
mesh.instanceMatrix.needsUpdate = true;
scene.add(mesh);
renderer.render(scene, camera);
console.log(renderer.info.render.calls);
La salida en consola confirma que, pese a cinco objetos separados en el espacio, el costo para la GPU fue de una sola llamada:
1
El paso de cinco cubos a un catálogo real es el mismo código; solo cambia el conteo y cómo se calcula cada matriz. Este ejemplo distribuye 50.000 asteroides en una capa esférica, imitando el cinturón principal:
const asteroidGeo = new THREE.IcosahedronGeometry(0.05, 0);
const asteroidMat = new THREE.MeshStandardMaterial({ color: 0xaaaaaa });
const count = 50000;
const asteroids = new THREE.InstancedMesh(asteroidGeo, asteroidMat, count);
const dummy = new THREE.Object3D();
for (let i = 0; i < count; i++) {
const radius = 2.2 + Math.random() * 0.6;
const theta = Math.random() * Math.PI * 2;
const phi = Math.acos((Math.random() * 2) - 1);
dummy.position.set(
radius * Math.sin(phi) * Math.cos(theta),
radius * Math.cos(phi) * 0.05,
radius * Math.sin(phi) * Math.sin(theta)
);
dummy.scale.setScalar(0.5 + Math.random());
dummy.updateMatrix();
asteroids.setMatrixAt(i, dummy.matrix);
}
asteroids.instanceMatrix.needsUpdate = true;
scene.add(asteroids);
Para distinguir asteroides de satélites sin duplicar geometría, alcanza con un color por instancia en vez de un material completo por instancia:
const colors = new Float32Array(count * 3);
const color = new THREE.Color();
for (let i = 0; i < count; i++) {
color.setHSL(Math.random() * 0.1 + 0.05, 0.3, 0.5);
color.toArray(colors, i * 3);
}
asteroids.instanceColor = new THREE.InstancedBufferAttribute(colors, 3);
asteroidMat.vertexColors = true;
Para levantar este ejemplo en una máquina limpia, primero hace falta tener Node.js y un gestor de paquetes instalados. Con eso resuelto, los comandos son los mismos en Windows, macOS y Linux porque corren sobre Node.js, no sobre el sistema operativo:
npm create vite@latest orrery-demo -- --template vanilla
cd orrery-demo
npm install three
npm run dev
El servidor de desarrollo queda escuchando y muestra en la terminal la URL local, normalmente http://localhost:5173; ahí se pega el código de arriba dentro de main.js.
El caso de Space Now: un catálogo completo del sistema solar en el navegador
Space Now, publicado en space.bl2.net como parte del proyecto Belle Lune 2, es un visualizador 3D del sistema solar que corre entero dentro de una pestaña: Sol, planetas, lunas y un catálogo que el propio sitio anuncia, al 30 de septiembre de 2026, en 526.000 asteroides y satélites. La cámara se controla con mouse y teclado: arrastrar rota, la rueda acerca, un clic muestra la ficha y la órbita de un objeto, y doble clic vuela directo hacia él. WASD mueve, R y F suben o bajan, Q, E y las flechas rotan, y Shift acelera el desplazamiento.
Ese nivel de interacción, cámara libre en tiempo real sobre cientos de miles de cuerpos con datos que se actualizan solos, es exactamente el escenario donde el renderizado por instancias deja de ser una optimización opcional y pasa a ser la única forma viable de construir el producto. Dibujar cada asteroide como una malla independiente, con su propia llamada de dibujo, no llegaría a un fotograma estable ni de lejos.
El sitio no publica su código fuente, así que no hay forma de confirmar desde afuera qué motor usa exactamente. Lo que sí puede afirmarse con certeza técnica es el patrón general, no el detalle de implementación. Cualquier proyecto que mueva esa cantidad de objetos en un canvas con WebGL, con cámara fluida y sin caída de cuadros, necesita algún esquema de geometría instanciada, agrupamiento por nivel de detalle y descarte de lo que queda fuera de cámara.
Casos de uso reales del instancing más allá de la astronomía
Los motores de videojuegos usan GPU instancing para poblar bosques, multitudes y campos de batalla: miles de árboles o soldados comparten la misma malla base y solo cambian su posición, orientación y a veces un tinte de color.
Los rastreadores de constelaciones de satélites, cada vez más comunes a medida que crecen las megaconstelaciones en órbita baja, enfrentan el mismo problema que Space Now a menor escala. Miles de puntos que se mueven en tiempo real necesitan actualizarse cuadro a cuadro sin recrear geometría.
En CAD y visualización arquitectónica, el instancing resuelve escenas con componentes repetidos, como tornillos, sillas o paneles de una fachada, sin que el archivo 3D crezca de forma lineal con cada copia.
En ciencia de datos, algunas herramientas de visualización 3D usan la misma técnica para dibujar scatter plots con decenas de miles de puntos representados como glifos geométricos en vez de píxeles planos, ganando profundidad e iluminación real.
Errores comunes y buenas prácticas
Olvidar marcar instanceMatrix.needsUpdate = true después de modificar las matrices es el error más frecuente. Sin ese flag, la GPU sigue usando el buffer viejo y los objetos no se mueven en pantalla aunque el código de posición sea correcto.
Un solo draw call no significa gratis. El vertex shader y el fragment shader igual se ejecutan por cada vértice y cada píxel de cada instancia, así que una geometría con demasiados triángulos por asteroide, multiplicada por 526.000, sigue pesando aunque el conteo de llamadas de dibujo sea uno. La solución habitual es un nivel de detalle bajo para los objetos lejanos o pequeños.
InstancedMesh no aplica frustum culling por instancia de forma automática. Three.js calcula un bounding box que cubre todas las instancias juntas, así que si la cámara mira una fracción del catálogo, igual se procesan todas salvo que el desarrollador implemente su propio descarte, por ejemplo dividiendo el catálogo en una grilla u octree y actualizando solo el buffer de la región visible.
Mezclar materiales transparentes con instancing sin ordenar por profundidad genera artefactos visuales. Los objetos translúcidos se dibujan en el orden del buffer, no en el orden de distancia a la cámara.
Cada matriz de instancia ocupa cuatro vectores de cuatro componentes, 16 floats en total. A partir de cierto volumen, ese atributo empieza a competir por el límite de memoria de vértice de la tarjeta gráfica, y ahí conviene mover las transformaciones a una textura de datos que el shader lee por índice en vez de por atributo fijo.
Comparativa con alternativas de renderizado masivo
Elegir la técnica correcta depende del volumen de objetos, de si necesitan geometría real con iluminación y de si sus posiciones cambian todo el tiempo o una sola vez al cargar la escena.
| Técnica | Cuándo usarla | Ventaja | Limitación |
|---|---|---|---|
| InstancedMesh (buffer attribute) | Hasta cientos de miles de instancias con geometría simple | Un solo draw call, API directa en Three.js | Cada matriz consume memoria de vértice; sin culling individual por defecto |
| Points / Sprites | Millones de puntos sin volumen 3D real | Memoria mínima por partícula | Sin geometría real: no sirve para objetos con forma o iluminación propia |
| Geometría fusionada (merge) estática | Objetos que nunca cambian de posición entre sí | Un solo draw call sin overhead de atributos por instancia | Mover una sola copia implica reconstruir toda la geometría fusionada |
| Textura de datos (DataTexture) + instancing | Millones de instancias con transformaciones calculadas en GPU | Evita el límite de atributos de vértice, mueve el cálculo a la tarjeta gráfica | Requiere shaders personalizados; mayor complejidad de código |
Profundizando: qué pasa dentro de la GPU
Cuando el driver recibe drawElementsInstanced, reparte el trabajo entre los miles de núcleos paralelos de la GPU en vez de lanzar 526.000 hilos de ejecución secuenciales, procesando al mismo tiempo un vértice de una instancia distinta en cada núcleo. Es el mismo modelo de paralelismo masivo que usa cualquier tarjeta gráfica para sombrear millones de píxeles por fotograma, aplicado ahora a repetir geometría en vez de calcular color.
El vertex shader recibe, además de los atributos normales de cada vértice, la matriz de instancia correspondiente a través de gl_InstanceID. Three.js genera este código automáticamente cuando detecta un InstancedMesh, agregando una línea equivalente a multiplicar la posición del vértice por instanceMatrix antes de aplicar la matriz de cámara.
Para catálogos que superan el millón de objetos, algunos proyectos mueven directamente el cálculo de posición al shader. En vez de que la CPU calcule la órbita de cada asteroide cuadro a cuadro y suba una matriz nueva, el vertex shader recibe los elementos orbitales (semieje mayor, excentricidad, inclinación) como datos por instancia y calcula la posición en tiempo real dentro de la GPU.
WebGPU, el sucesor de WebGL que ya corre en navegadores basados en Chromium y en Firefox, añade compute shaders de propósito general: código que corre en la GPU fuera del pipeline de dibujo, útil para calcular qué instancias son visibles antes de que el pipeline de render las toque siquiera. Es el paso natural después del instancing para quien busca escalar de cientos de miles a millones de objetos.
flowchart TD
F["Nuevo fotograma"] --> C["Calcular que instancias entran en el frustum de camara"]
C --> U["Actualizar solo esas matrices en el buffer"]
U --> D["1 draw call instanciado"]
D --> R["Frame renderizado"]
R --> F
📖 Resumen en Telegram: Ver resumen
Tu próximo paso: cloná el ejemplo de 50.000 asteroides de este artículo, subí el conteo a 500.000 cambiando una sola línea (const count = 500000) y mirá en la consola cuánto tarda renderer.info.render.calls en seguir devolviendo 1.
Preguntas frecuentes
¿Qué es el instancing en WebGL y en qué se diferencia de dibujar cada objeto por separado?
Es una técnica que dibuja múltiples copias de la misma geometría en una sola llamada a la GPU, en vez de emitir una llamada de dibujo distinta por cada objeto. La diferencia entre uno y 526.000 draw calls es, en la práctica, la diferencia entre un fotograma fluido y un navegador congelado.
¿Cuántas instancias soporta InstancedMesh de Three.js sin perder fluidez?
Depende de la cantidad de triángulos por instancia, de si hay culling implementado y de la GPU del usuario. No hay un número universal; la forma correcta de saberlo es medir con renderer.info.render.calls y renderer.info.render.triangles en el hardware objetivo, no asumir una cifra de otro proyecto.
¿Necesito WebGL2 para usar geometría instanciada?
No. WebGL1 la soporta a través de la extensión ANGLE_instanced_arrays; WebGL2 la incluye de forma nativa en el núcleo de la API, sin pedir la extensión aparte.
¿Puedo animar cada asteroide de forma independiente con renderizado por instancias?
Sí, actualizando la matriz de cada instancia cuadro a cuadro y marcando instanceMatrix.needsUpdate = true. A partir de cierto volumen conviene mover ese cálculo al propio shader en vez de recalcularlo en JavaScript cada fotograma.
¿Sirve el renderizado por instancias para satélites en órbita real, no solo para asteroides estáticos?
Sí, es el mismo patrón: cada satélite es una instancia con su propia matriz de transformación, recalculada según sus elementos orbitales en cada actualización de tiempo.
¿Qué motor de renderizado usa Space Now para mostrar 526.000 objetos?
El sitio no publica su código fuente, así que no puede confirmarse el motor exacto desde afuera. Lo verificable es el comportamiento: cámara fluida sobre un catálogo de esa escala en un único canvas WebGL, algo que en la práctica exige instancing o una técnica equivalente basada en GPU.
Referencias
- Space Now, Belle Lune 2: visualizador 3D del sistema solar que sirve de caso de estudio para este artículo.
- Documentación oficial de InstancedMesh en Three.js: API completa de la clase usada en los ejemplos de código.
- MDN: extensión ANGLE_instanced_arrays: referencia de la extensión que habilita instancing en WebGL1.
- Repositorio de Three.js en GitHub: código fuente de la librería usada en los ejemplos.
📱 ¿Te gusta este contenido? Únete a nuestro canal de Telegram @programacion donde publicamos a diario lo más relevante de tecnología, IA y desarrollo. Resúmenes rápidos, contenido fresco todos los días.
Imagen destacada: Foto de Trnava University en Unsplash
¿Te sirvió? ¿Te dio otro error? Contalo abajo: las preguntas se responden y le sirven al siguiente que llegue.
Dejar un comentario
0 Comentarios