Wiki · Devlog · Lecciones de Three.js

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

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.