Este artículo tiene demasiada agua.

“Estamos comenzando a desarrollar un nuevo juego, y necesitamos agua fría. ¿Puedes hacer esto?


- me preguntó "Sí, no hay duda! Por supuesto que puedo —respondí, pero mi voz temblaba traidoramente. “¿Y también en Unity?”, Y me quedó claro que había mucho trabajo por delante.

Entonces, un poco de agua. Hasta ese momento no había visto Unity, exactamente como C #, así que decidí hacer un prototipo de las herramientas que conocía: C ++ y DX9. Lo que sabía y podía practicar en la práctica en ese momento eran las texturas de desplazamiento de las normales para formar la superficie y el mapeo de desplazamiento primitivo basado en ellas. Inmediatamente fue necesario cambiar absolutamente todo. Forma realista animada de la superficie del agua. Sombreado (fuertemente) complicado. Generación de espuma. Sistema LOD conectado a la cámara. Comencé a buscar información en Internet sobre cómo hacer todo esto.

El primer punto, por supuesto, fue comprender el Simulando el agua del océano de Jerry Tessendorf .

Los buscapersonas académicos con un montón de fórmulas abstrusas nunca me han dado mucho, así que después de un par de lecturas entendí poco. Los principios generales eran claros: cada cuadro se genera mediante un mapa de altura utilizando la Transformada rápida de Fourier, que, en función del tiempo, cambia suavemente su forma para formar una superficie de agua realista. Pero cómo y qué contar no lo sabía. Poco a poco profundicé en la sabiduría de calcular FFT en sombreadores en D3D9, y el código fuente con un artículo en algún lugar en la naturaleza de Internet, que intenté encontrar durante una hora, pero fue en vano (desafortunadamente), realmente me ayudó. Se obtuvo el primer resultado (aterrador como una guerra nuclear):


Comenzaron los éxitos complacidos, y la transferencia de agua a Unity comenzó con su finalización.

Se presentaron varios requisitos de agua en el juego sobre las batallas navales:

  • Mirada realista. Hermosos como escorzos cercanos y distantes, espuma dinámica, dispersión, etc.
  • Soporte para diversas condiciones climáticas: calma, tormenta y condiciones intermedias. Cambio de hora del día.
  • Física de la flotabilidad de los barcos en una superficie simulada, objetos flotantes.
  • Como el juego es multijugador, el agua debería ser la misma para todos los participantes en la batalla.
  • Dibujo de la superficie: áreas dibujadas del vuelo de los núcleos de volea, espuma de la entrada de núcleos en el agua.

Geometría


Se decidió construir una estructura similar a un árbol cuádruple, con un centro alrededor de la cámara, que se reconstruye discretamente cuando el observador se mueve. ¿Por qué discreto? Si mueve la malla suavemente con la cámara o usa la reproyección del espacio de la pantalla como en el artículo Representación de agua en tiempo real, presentando el concepto de cuadrícula proyectada , entonces, en los planes a largo plazo, debido a la resolución insuficiente de la malla geométrica, los polígonos "saltarán" hacia arriba y abajo Esto es muy llamativo. La imagen es ondulante. Para superar esto, uno debe aumentar considerablemente la resolución del polígono de malla de agua, o "aplanar" la geometría a largas distancias, o construir y mover los polígonos para que estos cambios no sean visibles. Nuestra agua es progresiva (jeje) y elegí el tercer camino. Como en cualquier técnica similar (especialmente familiar para todos los que crearon terreno en los juegos), debe deshacerse de las uniones en T en los bordes de las transiciones de los niveles de detalle. Para resolver este problema, al principio se calculan 3 tipos de quads con parámetros de teselación dados:



El primer tipo es para aquellos quads que no son transiciones a detalles más bajos. Ningún lado tiene un número reducido de vértices 2 veces menor. El segundo tipo es para límite, pero no para quads angulares. El tercer tipo son los cuadrantes de límites angulares. La malla de agua final se construye girando y escalando estos tres tipos de mallas.

Así es como se ve un render con un color diferente de los niveles de agua LOD.


Los primeros cuadros muestran la conexión de dos niveles diferentes de detalle.

El video como marco está lleno de quads de agua:


Déjame recordarte que todo fue hace mucho tiempo (y no es cierto). Ahora se puede hacer de manera más óptima y flexible directamente en la GPU (GPU Pro 5. Quadtrees en la GPU). Y dibujará en una llamada de sorteo, y la teselación puede aumentar los detalles.

Más tarde, el proyecto se trasladó a D3D11, pero las manos no alcanzaron la actualización de esta parte del render del océano.

Generación de forma de onda


Para esto necesitamos la transformación rápida de Fourier. Para la resolución seleccionada (necesaria) de la textura de la ola (llamémosle así por ahora, explicaré qué datos se almacenan allí), preparamos los datos iniciales utilizando los parámetros establecidos por los artistas (fuerza, dirección del viento, dependencia de la dirección del viento y otros). Todo esto debe ser alimentado en las llamadas fórmulas. Espectro de Phillips Modificamos los datos iniciales obtenidos para cada cuadro teniendo en cuenta el tiempo y realizamos FFT en ellos. En la salida, obtenemos un mosaico de textura en todas las direcciones que contiene el desplazamiento de los vértices de la malla plana. ¿Por qué no solo un mapa de altura? Si almacena solo el desplazamiento en altura, el resultado será una masa "burbujeante" poco realista, que solo se parece remotamente al mar:


Si consideramos los desplazamientos para las tres coordenadas, se generarán hermosas ondas realistas "afiladas":


Una textura animada no es suficiente. El mosaico es visible, no hay suficientes detalles en el futuro cercano. Tomamos el algoritmo descrito y hacemos no una, sino 3 texturas generadas por fft. El primero es grandes olas. Establece la forma de onda básica y se usa para la física. El segundo son las olas medianas. Y finalmente, el más pequeño. 3 generadores FFT (la cuarta opción es la mezcla final):


Los parámetros de las capas se establecen independientemente uno del otro, y las texturas resultantes se mezclan en el sombreador de agua en la forma de onda final. Paralelamente a los desplazamientos, también se generan mapas normales de cada capa.

La "uniformidad" del agua para todos los participantes en la batalla está garantizada por la sincronización de los parámetros oceánicos al comienzo de la batalla. El servidor transmite esta información a cada cliente.

Modelo de flotabilidad física


Dado que era necesario hacer no solo una imagen hermosa, sino también el comportamiento realista de los barcos. Y también teniendo en cuenta el hecho de que un mar tormentoso (grandes olas) debería estar presente en el juego, otra tarea que debía resolverse era garantizar la flotabilidad de los objetos en la superficie del mar generado. Primero intenté hacer que la GPU leyera la textura de la onda. Pero, como rápidamente se hizo evidente que toda la física del combate naval debe hacerse en el servidor, el mar, o más bien, su primera capa, que establece la forma de onda, también debe leerse en el servidor (y, lo más probable, no es rápido y / o GPU compatible), se decidió escribir una copia funcional completa del generador GPU FFT en la CPU en forma de un complemento nativo de C ++ para Unity. Yo mismo no implementé el algoritmo FFT y lo usé en la biblioteca Intel Performance Primitives (IPP). Pero todo el enlace y el procesamiento posterior de los resultados lo hice yo, seguido de la optimización en el SSE y la paralelización por hilos. Esto incluyó la preparación de la matriz de datos para el FFT de cada cuadro, y la conversión final de los valores calculados en un mapa de desplazamiento de onda.

Había otra característica interesante del algoritmo, que se basaba en los requisitos para la física del agua. Lo que se necesitaba era la función de obtener rápidamente la altura de las olas en un punto dado del mundo. Es lógico, porque esta es la base para construir la flotabilidad de cualquier objeto. Pero, dado que en la salida del procesador FFT obtenemos un mapa de desplazamiento, no un mapa de altura, la selección habitual de la textura no nos dio la altura de la ola cuando fue necesario. Para simplificar, considere la opción 2D:



Para formar una onda, los texels (elementos de textura mostrados por líneas verticales) contienen un vector (flechas) que establece el desplazamiento del vértice de la malla plana (puntos azules) en la dirección de su posición final (la punta de la flecha). Supongamos que tomamos estos datos e intentamos extraer de ellos la altura del agua en el punto de interés para nosotros. Por ejemplo, necesitamos saber la altura en hB. Si tomamos el vector de texel tB, obtenemos un desplazamiento a un punto cerca de hC, que puede ser muy diferente de lo que necesitamos. Hay dos opciones para resolver este problema: en cada solicitud de altura, verifique el conjunto de texels adyacentes hasta que encuentre uno que tenga un desplazamiento de la posición que nos interesa. En nuestro ejemplo, encontramos que texel tA contiene el desplazamiento más cercano. Pero este enfoque no se puede llamar rápido. Escanear el radio de texel no está claro qué tamaño (y si el mar tempestuoso o la calma, los desplazamientos pueden variar mucho) puede llevar mucho tiempo.

La segunda opción: después de calcular el mapa de desplazamiento, conviértalo en un mapa de altura utilizando el enfoque de dispersión. Esto significa que para cada vector de desplazamiento, escribimos la altura de la onda que establece en el punto donde se desplaza. Esta será una matriz de datos separada, que se utilizará para obtener la altura en el punto de interés. Usando nuestra ilustración, la celda tB contendrá la altura hB obtenida del vector tA → hB. Hay una característica más. La celda tA no contendrá un valor válido, ya que no hay ningún vector que se mueva hacia ella. Para llenar tales "agujeros", se hace un pasaje para llenarlos con valores vecinos.

Así es como se ve si realiza la visualización de los desplazamientos utilizando vectores (rojo - desplazamiento grande, verde - pequeño):


El resto es simple. El plano de la línea de flotación condicional se establece para el barco. En él, se determina una cuadrícula rectangular de puntos de muestra, que define los lugares de aplicación de las fuerzas que empujan fuera del agua hacia el barco. Luego, para cada punto, verificamos si está debajo del agua o no utilizando el mapa de altura del agua descrito anteriormente. Si el punto está debajo del agua, aplique la fuerza vertical hasta el casco físico del cuerpo en este punto, escalado por la distancia desde el punto hasta la superficie del agua. Si está por encima del agua, entonces no hacemos nada, la gravedad hará todo por nosotros. De hecho, las fórmulas allí son un poco más complicadas (todo para el ajuste fino del comportamiento del barco), pero el principio básico es este. En el siguiente video de visualización de flotabilidad, los cubos azules son las ubicaciones de las muestras, y las líneas desde ellos hacia abajo son la magnitud de la fuerza que empuja fuera del agua.


En la implementación del servidor hay otro punto de optimización interesante. No es necesario simular diferentes aguas para diferentes instancias de combate si pasan en las mismas condiciones climáticas (los mismos parámetros del simulador FFT). Entonces, la decisión lógica fue hacer un grupo de simuladores, a los cuales las unidades de combate satisfacen las solicitudes para obtener agua simulada con los parámetros dados. Si los parámetros son los mismos en varias instancias, la misma agua volverá a ellos. Esto se implementa utilizando la API de archivo mapeado de Memor. Cuando se crea el simulador FFT, da acceso a sus datos exportando descriptores de los bloques necesarios. La instancia del servidor, en lugar de lanzar un simulador real, lanza un "dummy" que simplemente regala datos abiertos por estos descriptores. Hubo algunos errores divertidos relacionados con esta funcionalidad. Debido a los errores de recuento de referencias, el simulador se destruyó, pero el archivo mapeado de memoria está vivo mientras al menos un identificador está abierto. Los datos dejaron de actualizarse (no hay simulador) y el agua se "detuvo".

Del lado del cliente, necesitamos información sobre la forma de onda para calcular la penetración de los núcleos en la onda y reproducir los sistemas de partículas y espuma. El daño se calcula en el servidor y allí también es necesario determinar correctamente si el núcleo se ha metido en el agua (la ola puede cerrar el barco, especialmente en tormentas). Aquí ya es necesario hacer un seguimiento del mapa de altura por analogía, como se hace en el mapeo de paralaje o los efectos SSAO.

Sombreado


En principio, como en otros lugares. Las reflexiones, las refracciones, la dispersión del subsuelo se amasan astutamente, teniendo en cuenta la profundidad del fondo, tenemos en cuenta el efecto fresnel, consideramos el especular. Consideramos la dispersión de crestas dependiendo de la posición del sol. La espuma se genera de la siguiente manera: cree un "punto de espuma" en las crestas de las olas (use la altura como una métrica), luego aplique puntos recién creados a los puntos de los cuadros anteriores mientras reduce su intensidad. Por lo tanto, obtenemos una mancha de manchas de espuma en forma de cola a partir de una cresta de ola.


Usamos la textura de "manchas" obtenida como una máscara con la cual mezclamos las texturas de burbujas, manchas, etc. Obtenemos un patrón dinámico de espuma bastante realista en la superficie de las olas. Esta máscara se crea para cada capa FFT (les recuerdo, tenemos 3 de ellas), y en la mezcla final, todas se mezclan.

El video de arriba visualiza una máscara de espuma. La primera y segunda capas. Modifico los parámetros del generador y el resultado es visible en la textura.

Y un video de un mar tormentoso un poco torpe. Aquí puede ver claramente la forma de onda, las capacidades del generador y la espuma:


Dibujo de agua


Imagen de uso:



Utilizado para:

  • Marcadores, visualización de la zona de expansión de núcleos.
  • Dibuja espuma en el punto donde los núcleos golpean el agua.
  • Espumosa estela del barco
  • Exprimir el agua debajo de la nave para eliminar el efecto de las olas que inundan la cubierta y la bodega inundada.

El caso base obvio es la textura proyectiva. Fue implementado. Pero hay requisitos adicionales. Especies cercanas: jabón debido a una resolución insuficiente (puede aumentar, pero no infinitamente), y quiero que estos dibujos proyectivos en el agua sean muy visibles. ¿Dónde se resuelve el mismo problema? Así es, en las sombras (mapa de sombras). ¿Cómo se resuelve allí? Derecha, mapas de sombras en cascada (división paralela). También pondremos en servicio esta tecnología y la aplicaremos a nuestra tarea. Rompemos el tronco de la cámara en N (3-4 usualmente) subfusts. Para cada uno, construimos un rectángulo descriptivo en el plano horizontal. Para cada uno de esos rectángulos, construimos una matriz de proyección ortográfica y dibujamos todos los objetos de interés para cada una de N cámaras ortográficas. Cada una de estas cámaras se dibuja en una textura separada, y luego, en el sombreador del océano, las combinamos en una imagen proyectiva sólida.


Así que puse un enorme avión con una textura de bandera en el mar:



Esto es lo que contienen las divisiones:



Además de las imágenes habituales, es necesario dibujar una máscara adicional de espuma (para los rastros de barcos y lugares de impacto de los núcleos) exactamente de la misma manera, así como una máscara para exprimir el agua debajo de los barcos. Estas son muchas cámaras y muchos pasillos. Al principio funcionó tan frenéticamente, pero luego, después de cambiar a D3D11, usando la "propagación" de la geometría en el sombreador geométrico y dibujando cada copia en un objetivo de renderizado separado a través de SV_RenderTergetArrayIndex, fue posible acelerar mucho este efecto.

Mejoras y actualizaciones


D3D11 es muy manos libres en muchos momentos. Después de cambiar a él y a Unity 5, hice un generador FFT en los sombreadores de cómputo. Visualmente, nada ha cambiado, pero se ha vuelto un poco más rápido. La traducción del error de cálculo de la textura de los reflejos de una cámara separada de renderizado completo a la tecnología Screen Space Planar Reflections dio un buen impulso en el rendimiento. Escribí sobre la optimización de los objetos de la superficie del agua arriba, pero mis manos no alcanzaron la transferencia de la malla a la GPU Quadtree.

Mucho podría hacerse de manera más óptima y sencilla. Por ejemplo, no cerca los jardines con un simulador de CPU, simplemente ejecute la opción GPU en un servidor con un dispositivo WARP (software) d3d. Las matrices de datos allí no son muy grandes.

Bueno, en general, de alguna manera. Cuando comenzó el desarrollo, todo era moderno y genial. Ahora ya está fuera de lugar en algunos lugares. Hay más materiales disponibles, incluso hay un análogo similar a github: Crest . La mayoría de los juegos que tienen mares utilizan un enfoque similar.

Source: https://habr.com/ru/post/451604/


All Articles