Skip to content

Construir un mundo abierto en el navegador, parte 26: Tres formas de crear agua

Por Oleg Sidorkin, CTO y cofundador de Cinevva

¿Acabas de llegar? Consulta la guía de la serie. Explica qué es un experimento y contiene enlaces a todas las partes.

La parte 25 vistió a los avatares. Esta parte trata sobre el agua, y consta de tres experimentos porque el agua es la superficie en la que un atajo barato y la solución correcta parecen idénticos en una captura de pantalla, pero completamente distintos en movimiento. El experimento 51 genera reflejos mediante el espacio de pantalla, la opción tentadora, y choca de frente con su limitación inherente. El experimento 52 cambia al método que utilizan realmente todos los juegos publicados. El experimento 53 incorpora una biblioteca de agua terminada para ver lo lejos que estamos de poder considerarlo «listo». Los tres comparten una misma capa de refracción, por lo que la única variable que cambia entre los dos primeros es cómo se calcula el reflejo.

Reflejos a partir de la pantalla que ya tienes

Abrir el experimento 51 en una pestaña nueva ↗ · Ver el código fuente

Los reflejos en espacio de pantalla reutilizan el fotograma que ya has renderizado. Para cada píxel del agua, reflejas el rayo de visión sobre la superficie, haces avanzar ese rayo reflejado por el búfer de profundidad y, cuando el rayo pasa por detrás de una superficie registrada, has encontrado lo que refleja el agua, muestreado directamente desde el búfer de color. La adaptación sigue línea por línea el SSRNode de three.js, que a su vez sigue la introducción a SSR de lettier. El avance es un recorrido DDA en espacio de pantalla: se proyectan el inicio y el final del rayo a coordenadas de píxel y se avanza por el eje que sea más largo, con una muestra por píxel. La profundidad del rayo reflejado en cada paso necesita una interpolación con corrección de perspectiva, 11/z0+s(1/z11/z0), porque interpolar linealmente la Z de vista es sencillamente incorrecto y produce intersecciones en lugares equivocados.

Dos cosas hacen que esto sea utilizable en lugar de una presentación de diapositivas. El recorrido grueso está limitado a 64 pasos, porque, de lo contrario, un rayo largo proyectado a lo largo de mil píxeles ejecutaría cientos de iteraciones por fragmento, y un plano de agua con un millón de fragmentos multiplicado por cientos de iteraciones y por varias muestras de textura da como resultado una escena a 30 fps. La calidad controla la zancada efectiva dentro de ese límite, en lugar del número de iteraciones. Y como un recorrido grueso limitado deja bandas visibles en forma de escalones, un refinamiento binario de seis iteraciones biseca el intervalo entre el último fallo y la intersección, lo que proporciona una precisión de subpaso 64 veces mayor, suficiente para que los fragmentos de agua vecinos dejen de fijarse en la misma posición de intersección gruesa. Una comprobación final de distancia entre punto y línea confirma que el candidato está realmente sobre el rayo de reflexión y no solo a la misma profundidad, con una tolerancia de grosor que se ajusta automáticamente a la anchura en espacio de vista de un píxel a esa profundidad: más estricta de cerca y más permisiva de lejos.

La parte honesta de este experimento está escrita en sus propios comentarios: SSR no puede reflejar lo que la cámara principal nunca muestreó. La parte inferior de un árbol, cualquier cosa que esté fuera de pantalla o cualquier cosa ocluida no existe en los búferes, así que no puede aparecer en el reflejo. Esa es la «pérdida de información del lado incorrecto» que ninguna mejora de calidad del recorrido puede solucionar, y es exactamente la razón por la que existe el siguiente experimento.

El espejo que no puede mentir

Abrir el experimento 52 en una pestaña nueva ↗ · Ver el código fuente

Un reflejo planar renderiza la escena una segunda vez desde una cámara reflejada respecto al plano del agua, hacia un destino fuera de pantalla que el shader del agua muestrea. Este es el patrón canónico, el que utilizan UE5 Water, el propio WaterMesh de three.js, ABZÛ y Sea of Thieves, porque dispone de todos los píxeles de la escena, incluida la geometría que SSR nunca puede ver. En TSL resulta casi anticlimático: reflector() asigna la cámara auxiliar reflejada y su destino de renderizado, añades su destino a la malla para que se actualice en cada fotograma y muestreas su color. Cuando incorporemos las olas, el reflejo ondulará al añadir un desplazamiento de distorsión al nodo UV del reflector, que es exactamente la línea que utiliza WaterMesh.

El error que merece la pena registrar estaba en la red de seguridad, no en el espejo. Una versión anterior mezclaba la salida del reflector con un cielo procedimental como alternativa, ponderada según la magnitud del color reflejado, basándose en la teoría de que un reflejo cercano a cero significaba que el destino no contenía nada allí. Pero una sombra oscura bajo el dosel también tiene una magnitud baja, por lo que la limitación nunca alcanzaba su intensidad máxima y esos píxeles realmente oscuros acababan mezclados con el cielo luminoso. El síntoma detectado por el usuario era preciso: en modo de depuración, el destino del espejo sin procesar mostraba árboles oscuros perfectos, mientras que el renderizado compuesto presentaba reflejos desvaídos, y el diagnóstico fue que el shader «hace desaparecer los colores oscuros». La solución fue eliminar código. El destino del reflector es fiable después del primer fotograma, así que no hace falta ninguna alternativa. Ambos experimentos comparten la misma refracción subyacente: muestrear la escena detrás de la superficie, reconstruir a qué profundidad bajo la línea de flotación se encuentra cada píxel y aplicar una extinción de Beer-Lambert por canal para que el rojo desaparezca tras unos pocos metros mientras el azul persiste, con una máscara de cielo para que la imagen de fondo del plano lejano no reciba niebla. El efecto Fresnel de Schlick mezcla refracción cuando miras directamente hacia abajo dentro del agua y reflejo cuando miras a través de ella.

Qué aspecto tiene un resultado terminado

Abrir el experimento 53 en una pestaña nueva ↗ · Ver el código fuente

El experimento 53 es la comprobación entre desarrollar o comprar. Incorpora threejs-water-pro tal como se distribuye, con su preajuste tropical, su clipmap oceánico predeterminado, seguimiento de cámara y cielo de Rayleigh, y carga la misma isla glTF que utiliza la demostración de la propia biblioteca. El objetivo es ver la distancia entre un shader de agua plana creado a mano y un sistema oceánico completo, y esa distancia es grande: este tiene un sistema de flotabilidad que muestrea la altura de las olas en varios puntos bajo el casco de un barco para que cabecee y se balancee en lugar de limitarse a subir y bajar, un generador de estelas, espuma en la costa y la superficie, y una pasada de máscara que impide renderizar agua dentro del casco para que las ondas no atraviesen la cubierta.

La integración reveló el tipo de detalle que solo se aprende usando una biblioteca en lugar de leyendo su README. El preajuste hace referencia a las texturas de espuma por nombre de archivo, pero cargarlas es responsabilidad de quien usa la biblioteca; sin ellas, la costa parece un borde duro en la línea de flotación en lugar de olas rompiendo. La isla está situada de modo que su geometría submarina desciende más allá del fondo oceánico, permitiendo que la malla del fondo de la biblioteca oculte el anillo exterior del modelo sin dejar visible el borde del plano. Este es el patrón previsto por la biblioteca: aportar un modelo 3D, no sintetizar el terreno. Además, el antialiasing está desactivado deliberadamente en el renderizador a favor de una pasada FXAA de posprocesado, porque MSAA mezcla los fragmentos de borde con el fondo antes de que se aplique la niebla atmosférica basada en profundidad, lo que deja una franja oscura y fina allí donde la geometría se encuentra con la niebla. Resolver el aliasing después de la niebla, en lugar de antes, elimina esa franja. Lo que esto reduce es el riesgo de la propia decisión: un océano para producción es un sistema grande y especializado y, en los casos en que necesitamos uno, adoptar una biblioteca mantenida es mejor que reconstruir desde cero las estelas, la flotabilidad y la espuma, mientras que el shader de espejo planar del experimento 52 sigue siendo la solución adecuada para las masas de agua interiores más pequeñas que un creador coloca en su propio mundo.

Tecnología mencionada en este capítulo

Agua con reflejos en espacio de pantalla. Un rayo de visión reflejado recorre el búfer de profundidad en espacio de pantalla mediante DDA, utilizando interpolación 1/z con corrección de perspectiva, un límite estricto de pasos para acotar el coste por fragmento y una pasada de refinamiento binario para eliminar las bandas que deja un recorrido limitado. Una confirmación de punto a línea con un grosor ajustado según la profundidad rechaza intersecciones falsas. El límite absoluto del método es que solo puede reflejar la geometría que la cámara principal ya ha muestreado, por lo que las superficies fuera de pantalla y del lado incorrecto nunca aparecen. Consulta materiales del terreno.

Reflejo de espejo planar. Una cámara auxiliar reflejada respecto al plano del agua renderiza la escena en un destino fuera de pantalla que muestrea el shader del agua, lo que proporciona reflejos perfectos a nivel de píxel, incluida la geometría que SSR no puede ver. Este es el patrón que utilizan UE5 Water y WaterMesh de three.js, con la distorsión de las olas aplicada como desplazamiento al nodo UV del reflector. Una alternativa de cielo ponderada por magnitud borraba erróneamente los píxeles oscuros del reflejo; el destino del reflector es fiable después del primer fotograma, así que la solución fue eliminar esa alternativa.

Refracción con tinte de profundidad de Beer-Lambert. Ambos shaders muestrean la escena detrás de la superficie, reconstruyen la profundidad de cada píxel del fondo bajo la línea de flotación y aplican extinción exponencial por canal —el rojo desaparece en cuestión de metros, mientras que el azul persiste— compuesta hacia un color de niebla acuática, con una máscara de cielo para que el plano lejano no reciba niebla. El efecto Fresnel de Schlick mezcla refracción en incidencia normal con reflejo en ángulos rasantes.

Adopción de una biblioteca de agua para producción. threejs-water-pro incluye un clipmap oceánico, cielo de Rayleigh, flotabilidad multipunto para el cabeceo y balanceo de barcos, estelas, espuma y una pasada de máscara para el casco. Los detalles del lado del consumidor importan: las texturas de espuma deben cargarse explícitamente, la geometría submarina de la isla debe descender más allá del fondo oceánico para que este oculte sus bordes y el antialiasing debe ejecutarse como una pasada FXAA de posprocesado, en lugar de utilizar MSAA, para evitar una franja oscura de niebla en los bordes de la geometría.


Parte 26 de 29. Anterior: Parte 25 - Un esqueleto, todos los atuendos Siguiente: Parte 27 - Una isla creada con ruido y un suelo que parece suelo Guía de la serie: /es/blog/2026-02-25-open-world-browser-series-guide