Lecciones de Three.js
Three.js es el renderizador del combate espacial 3D de CONTRABAND. Es poderoso, maduro y tiene curva de aprendizaje. Aquí está lo que aprendí durante el desarrollo — tanto las victorias como los callejones sin salida.
Por qué Three.js
La experiencia de combate 3D en CONTRABAND necesitaba renderizado en tiempo real de naves, efectos de armas y vistas de galaxia. Las opciones para 3D en navegador son esencialmente: Three.js, Babylon.js o WebGL puro. Elegí Three.js porque su API es la más accesible, su documentación es extensa y su comunidad ha respondido la mayoría de las preguntas con las que me topé. Babylon.js es discutiblemente más poderoso; WebGL puro es discutiblemente más minimalista. Three.js es el medio productivo.
Carga de modelos GLB
Cada nave en CONTRABAND es un archivo GLB cargado en tiempo de ejecución. GLB es la variante binaria de glTF — es el formato 3D estándar para la web. El GLTFLoader en Three.js maneja el parsing; después tienes un objeto de grafo de escena que puedes manipular.
Lo no obvio sobre los archivos GLB es que embeben sus texturas. Esto significa que un archivo de nave es el único asset necesario para esa nave — sin archivos de textura separados que gestionar. También significa que los tamaños de archivo son mayores de lo que esperarías. Un casco de nave GLB promedio en CONTRABAND es de 400-800KB. Para 16 naves, eso es aproximadamente 10MB de assets 3D totales. Esta es la mayor contribución al tamaño de descarga del juego.
El lazy-loading ayuda. Cargo el GLB de cada nave solo cuando el jugador realmente la vuela. El Scout MK-I inicial carga inmediatamente. Las otras naves cargan a demanda desde el preview del astillero. Esto mantiene la descarga inicial en aproximadamente 3MB incluso con todas las naves disponibles.
Rendimiento en móvil
La mayor lección de Three.js fue: el rendimiento móvil no se parece al rendimiento de escritorio. Mi juego corría a 60fps en mi portátil y 15fps en un móvil de gama media. Perfilando revelé que el culpable era la complejidad de shader — el material por defecto de Three.js hace múltiples pases de iluminación por pixel, lo cual es caro en GPUs móviles.
El arreglo fue usar materiales más simples. Reemplacé MeshStandardMaterial con MeshBasicMaterial para la mayoría de las naves, aceptando que las naves no responderían dinámicamente a la iluminación. Esto está bien para un juego espacial donde la iluminación es ya mínima. Las tasas de frames móviles saltaron a 45-60fps tras el cambio. Los jugadores en móviles más viejos ahora pueden realmente jugar.
La lección: los materiales "buenos" de Three.js están diseñados para escritorio y móvil de gama alta. Enviar a móvil significa entender qué características puedes permitirte.
Gestión de escena
Una escena de Three.js es un árbol de objetos. Añadir y quitar objetos tiene coste de rendimiento si lo haces por frame. Aprendí esto a la mala — la implementación original removía y re-añadía disparos de láser cada frame, lo que causaba stutters notables. El arreglo fue poolear objetos de disparo: crear 20 meshes de disparo al arranque y reusarlos, cambiando flags de visibilidad en lugar de añadir/quitar de la escena.
Este patrón — object pooling — es estándar en desarrollo de juegos. En Three.js específicamente, es crítico porque las operaciones de grafo de escena son más caras que en motores que manejan pooling automáticamente.
Comportamiento de cámara
Lograr que la cámara de combate espacial se sintiera bien tomó más iteraciones que cualquier otro sistema. Las versiones tempranas tenían la cámara siguiendo rígidamente a la nave del jugador, lo que producía mareo cuando las naves maniobraban. El arreglo fue una cámara soft-follow que queda detrás de la rotación del jugador y suaviza curvas de aceleración. Este es diseño estándar de cámara de juegos AAA; implementarlo en Three.js tomó aproximadamente un día.
La cámara final usa un modelo de física de resorte: la cámara quiere estar en un offset específico de la nave, pero se mueve hacia ese offset con dinámicas de resorte (fuerza proporcional + derivativa). Ajustar las constantes del resorte importa — demasiado rígido se siente tieso, demasiado suelto se siente flotante.
Campo de estrellas y fondos
El campo de estrellas de la vista galáctica son 2000 objetos Points renderizados en una sola llamada de dibujado vía un PointsMaterial. Esto es suficientemente barato que no necesité optimizar. Donde sí necesité optimizar fue el fondo parallax durante el combate — originalmente implementado como tres capas separadas de sprites, lo que causaba problemas de z-sorting. Combinado en una sola textura cubemap, que también sirve como mapa de entorno de la escena para materiales de nave reflectantes.
Lo que haría distinto
- Empezar con renderizado mobile-first. Retrofittée el soporte móvil. Habría sido más rápido diseñar el renderizador para móvil desde el inicio.
- Invertir en un sistema de shaders antes. Eventualmente escribí shaders personalizados para efectos de armas. Hacer esto antes habría mejorado la calidad visual.
- Usar InstancedMesh para objetos repetidos. Al renderizar 30 naves idénticas en una vista de flota, el instancing es la respuesta correcta. Three.js lo soporta bien.
- Aprender las herramientas de debugging. Three.js tiene un inspector de escena (SceneUtils.detach, etc.). Úsalas temprano.
Lo que me sorprendió
Three.js es más estable de lo que esperaba. Durante 6 meses de desarrollo, la librería tuvo cero bugs que me afectaran. Deprecaciones menores de API, sí. Pero nada que rompiera mi juego en una actualización de librería. Para un proyecto tan activo (releases semanales) esto es impresionante.
La comunidad también es inusualmente útil. Las preguntas específicas de Three.js en Stack Overflow casi siempre tienen respuestas. Problemas complejos de animación GLTF — que tuve brevemente — fueron resueltos leyendo Q&A existente. Esto no es verdad de toda librería JavaScript.
¿La usaría de nuevo?
Para cualquier juego 3D de navegador: sí. Three.js es la respuesta por defecto por el futuro previsible. Las únicas alternativas que consideraría son Babylon.js (para juegos con demandas gráficas más pesadas) o WebGL puro (para juegos donde el overhead de Three.js se vuelve un problema — raro). Para CONTRABAND específicamente, Three.js fue la elección correcta.