Cómo crear un mundo abierto en el navegador, parte 12: Anillos, niebla del cielo y qué volveríamos a hacer
Por Oleg Sidorkin, CTO y cofundador de Cinevva
¿Acabas de llegar? Consulta la guía de la serie. Explica qué es un prototipo y enlaza todas las partes.
Se suponía que el prototipo 24 consistiría en «añadir anillos de clipmap al terreno». Acabó convirtiéndose en un gran final que abordó a la vez el renderizado, los shaders, la infraestructura de módulos y la integración visual.
La tarea principal del terreno era generar anillos concéntricos de clipmap en el shader de vértices. Cada anillo es una malla de cuadrícula plana centrada en la cámara, cuyos vértices se desplazan según las muestras del mapa de alturas. El anillo interior utiliza la resolución completa. Cada anillo posterior duplica el espaciado entre vértices y cubre un área mayor. La parte complicada es el límite entre anillos: cuando un anillo de alta resolución se encuentra con otro de baja resolución, los vértices del borde de la malla más detallada deben ajustarse al punto medio del borde de la malla más gruesa. Implementamos un morphing de bordes 2:1 detectando los vértices del límite —aquellos cuya coordenada de cuadrícula es impar a lo largo del borde del anillo— y ajustando su altura al punto medio de sus dos vecinos pares,
Abrir el prototipo 24 en una pestaña nueva ↗ · Ver el código fuente
Después llegó la integración de la niebla y el cielo. Queríamos que el terreno lejano se fundiera con el color real del cielo, no con una constante plana. Eso implicaba que el shader de niebla debía saber qué color tendría el cielo en la dirección de cada fragmento. Cargamos una textura HDR equirectangular para el skybox y la muestreamos en el shader de fragmentos mediante la dirección de visión desde la cámara hasta el fragmento, convertida a coordenadas UV equirectangulares a través del nodo equirectUV de TSL. El factor de niebla se basaba en la distancia y utilizaba positionView.z.negate() para obtener la profundidad en el espacio de la cámara, interpolada con smoothstep entre una distancia cercana y otra lejana.
El cableado de los módulos resultó ser más molesto que cualquier aspecto de la geometría. Actualizamos a Three.js 0.183.1, que había reorganizado las salidas de compilación. La importación three/tsl debía resolverse como three.tsl.js, y TSL importaba internamente three/webgpu como un especificador simple. Ambas correspondencias debían indicarse de forma explícita en el mapa de importaciones del HTML. Si faltaba cualquiera de ellas, aparecían errores crípticos como «does not provide an export» o «failed to resolve module specifier», sin ninguna indicación sobre qué correspondencia era incorrecta. Una vez añadidas las dos al mapa de importaciones, el grafo de shaders se cargó correctamente.
También tuvimos un problema de orientación del skybox: la textura se renderizaba al revés. La solución fue establecer flipY = true en la textura equirectangular, que es el valor predeterminado de Three.js para las texturas cargadas, pero que en nuestro código inicial estaba establecido como false.
La implementación original de la niebla muestreaba el cielo en una dirección casi constante, lo que producía una fina franja del color del horizonte en lugar de un gradiente natural. La solución fue calcular por píxel la dirección real en el espacio del mundo desde la cámara hasta el fragmento mediante positionWorld.sub(cameraPosition).normalize() y pasarla a equirectUV para consultar el color de la niebla. Así, los fragmentos del terreno se funden con el color del cielo que realmente tienen detrás, lo que se ve correctamente desde cualquier ángulo de cámara.
Por debajo de todas las correcciones individuales, el resultado principal se mantuvo. Ahora tenemos un sistema de terreno que combina edición volumétrica en el campo cercano —marching cubes con uniones Transvoxel—, fragmentos de mapas de alturas en el campo medio y anillos de clipmap en el campo lejano, todo ello gobernado por una capa de políticas que decide el modo, el LOD y el comportamiento de las transiciones.
Si tuviera que nombrar los patrones que repetiría en el próximo proyecto, serían estos:
Empezar con prototipos de riesgo antes de trabajar en funcionalidades. El prototipo 1 resolvió la pregunta «¿podemos renderizar siquiera con suficiente rapidez?» antes de que invirtiéramos en flujos de contenido.
Congelar líneas base que sepamos que funcionan antes de dar grandes saltos de integración. Los prototipos 13 y 14 nos ahorraron días de búsqueda por bisección de regresiones.
Imponer políticas y observabilidad antes de embarcarse en maratones de optimización. El prototipo 23 convirtió errores misteriosos en condiciones con nombre y reglas de activación.
Probar en movimiento, no mediante capturas de pantalla. Los saltos de los clipmaps, el parpadeo de las uniones y los tirones del streaming se ocultan en los fotogramas estáticos.
Medir el coste en tiempo de fotograma de cada funcionalidad, no los FPS medios. Los promedios ocultan los picos que los usuarios perciben de verdad.
Y publicar las partes caóticas. Los caminos equivocados, la búsqueda de fantasmas en búferes obsoletos, los dos días culpando a la lógica de transición cuando el rango de dibujo era incorrecto. Esas son las partes de las que la gente realmente puede aprender.
Comprobación con la realidad externa: los diarios de desarrollo de Vuntra City
Después de terminar esta serie, revisamos los diarios de desarrollo de @VuntraCity como comprobación de una implementación externa frente a nuestras propias suposiciones sobre los mundos abiertos. Es un proyecto nativo de UE5, no una plataforma para navegador, pero los patrones de sus sistemas se corresponden lo bastante bien como para que la comparación resulte útil.
La primera señal es que la velocidad de desplazamiento debe tratarse como un control del streaming, no solo como una cuestión de jugabilidad. En Vuntra City, el transporte de alta velocidad se encauza de forma intencionada por encima de la mayoría de los interiores, y el alcance del detalle se adapta a la velocidad de movimiento para evitar la creación y destrucción constante de elementos y los bloqueos (sistema de transporte, técnicas de rendimiento). Esto coincide con la dirección de nuestra capa de políticas: el modo de movimiento debe influir directamente en el radio de los fragmentos, la activación de interiores y el trabajo permitido por fotograma.
La segunda señal es la arquitectura. Sus mapas y su sistema de direcciones exigieron separar la topología del mundo de los objetos renderizados para poder ejecutar consultas globales sobre regiones no cargadas (mapas y direcciones). Es la misma separación que necesitamos en el navegador para la búsqueda en el mundo, el trazado de rutas de misiones, los análisis de moderación y la indexación de puntos de interés sin obligarnos a usar rutas de datos ligadas al renderizado.
La tercera señal es la división de la simulación por niveles. Su diseño para un millón de PNJ mantiene barato y global el estado aproximado de sus horarios, y solo destina el costoso presupuesto de comportamiento a los PNJ cercanos al jugador (resumen del millón de PNJ, análisis detallado del sistema). Esto refuerza nuestro propio modelo de simulación centrado en el área de interés, donde la fidelidad en el campo cercano y el determinismo en el campo lejano son aspectos independientes con presupuestos distintos.
Y la cuarta señal es la calidad del diseño, no la escala bruta. Sus mejores momentos de exploración surgen de distribuciones ponderadas, valores atípicos poco frecuentes y pistas diegéticas de navegación, en lugar de superposiciones constantes de la interfaz (notas sobre entornos procedurales, bucle sin minimapa). Para nosotros, esto recuerda que los sistemas técnicos deben ajustarse para producir variaciones que inviten al descubrimiento, no solo para maximizar el rendimiento.
Tecnologías mencionadas en este capítulo
Geometría de anillos de clipmap. Cada anillo es una malla de cuadrícula plana centrada en la cámara, cuyos vértices se desplazan según las muestras del mapa de alturas. El anillo interior utiliza la resolución completa. Cada anillo posterior duplica el espaciado entre vértices y cubre un área mayor. La parte complicada es el límite: cuando un anillo de alta resolución se encuentra con otro de baja resolución, los vértices del borde de la malla más detallada se ajustan al punto medio del borde de la malla más gruesa. La técnica tiene su origen en el artículo de Losasso y Hoppe para SIGGRAPH 2004 (PDF) y se explica con detalle en GPU Gems 2, capítulo 2. Consulta nuestra guía de paisajes sobre clipmaps geométricos.
Morphing de bordes 2:1. En el límite entre dos anillos de clipmap, el anillo más detallado tiene vértices en posiciones que el anillo más grueso no comparte. Se detectan los vértices del límite cuya coordenada de cuadrícula es impar a lo largo del borde del anillo y se interpola su altura entre los dos vértices pares vecinos. Esto produce uniones estancas sin necesidad de geometría de transición específica. La interpolación se ejecuta en el shader de vértices: morphedHeight = mix(heightLeft, heightRight, 0.5) para los vértices del límite, utilizando el mismo sistema de geomorphing descrito en nuestra guía.
Mapeo equirectangular del skybox. Una única imagen 2D que representa la esfera completa de direcciones del cielo mediante una proyección de longitud y latitud. El eje horizontal abarca de 0 a 360 grados y el vertical, de 0 a 180 grados. Una dirección de visión normalizada
En Three.js, establecer texture.mapping = EquirectangularReflectionMapping con SRGBColorSpace permite utilizarla como fondo de la escena. En TSL, equirectUV(direction) aplica esa misma conversión y transforma una dirección de visión 3D en las coordenadas UV 2D necesarias para muestrear la textura.
Color de niebla por fragmento obtenido del cielo. La niebla estándar mezcla los fragmentos con un único color constante. En una escena con un skybox detallado, esto resulta incorrecto porque el color del cielo varía según la dirección. La solución consiste en calcular por píxel la dirección en el espacio del mundo desde la cámara hasta el fragmento (positionWorld.sub(cameraPosition).normalize()) y muestrear el skybox en esa dirección para obtener el color de la niebla. Cada fragmento se desvanece hacia el color del cielo que realmente tiene detrás, lo que produce una mezcla correcta desde cualquier ángulo de cámara. El factor de niebla utiliza smoothstep(nearDist, farDist, viewDepth) con positionView.z.negate() para la profundidad en el espacio de la cámara.
Mapas de importaciones para módulos ES. Un mecanismo nativo del navegador (<script type="importmap">) que asigna especificadores simples de módulos —como three/tsl— a URL reales. Cuando Three.js 0.183.1 reorganizó sus salidas de compilación, three/tsl tuvo que resolverse como three.tsl.js, y TSL importaba internamente three/webgpu como un especificador simple. Ambas correspondencias debían indicarse de forma explícita en el mapa de importaciones; de lo contrario, el navegador producía errores como «does not provide an export» o «failed to resolve module specifier».
Lecturas adicionales
Para profundizar en las tecnologías utilizadas a lo largo de esta serie, consulta nuestras guías complementarias:
- Generación de paisajes con LOD dinámico y streaming para mundos abiertos en el navegador abarca mapas de alturas, SDF, marching cubes, Transvoxel, clipmaps geométricos, geomorphing, arquitectura de streaming, materiales de terreno y renderizado de vegetación.
- Tecnología 3D de mundos abiertos en el navegador para mundos multijugador creados por usuarios abarca plataformas de renderizado, WebGPU, físicas, redes, arquitectura multijugador y lecciones de Skyrim, The Witcher 3, Breath of the Wild y GTA V.
Gracias por acompañarnos en este recorrido de doce partes.
Parte 1: Empezamos intentando romperlo
Parte 2: Físicas en workers y el temor al retardo de entrada
Parte 3: Los prototipos poco vistosos que nos salvaron
Parte 4: Streaming antes que terrenos sofisticados
Parte 5: Presupuestar los elementos visuales
Parte 6: Los clipmaps cambiaron el rumbo
Parte 7: Marching cubes y las primeras cuevas de verdad
Parte 8: Integración sin perder nuestra línea base
Parte 9: Transvoxel comenzó con una estructura básica
Parte 10: El caos de las uniones y la batalla final de las esquinas
Parte 11: Modo basado en políticas, no codificado de forma rígida
Parte 12 de 14.
Anterior: Parte 11 - Modo basado en políticas, no codificado de forma rígida
Siguiente: Parte 13 - Esculpido del terreno y la muerte de la función matemática
Guía de la serie: /es/blog/2026-02-25-open-world-browser-series-guide