En septiembre de este año, se lanzaría el juego móvil Titan World de Unstoppable, la oficina de Minsk de Glu mobile. El proyecto fue cancelado justo antes del lanzamiento mundial. Pero los logros permanecieron, y el más interesante de ellos, con el amable permiso de los jefes del estudio Dennis Zdonov y Alex Paley, me gustaría compartir con el público.En marzo de 2018, el líder del equipo y yo mantuvimos una reunión en la que discutimos qué hacer a continuación: se completó el código de representación y no había nuevas características y efectos especiales en los planes. Parecía una elección lógica reescribir el sistema de partículas desde cero: de acuerdo con todas las pruebas, proporcionó los mayores inconvenientes en productividad, además de enloquecer a los diseñadores con su interfaz (archivo de configuración de texto) y capacidades extremadamente escasas.
Cabe señalar que la mayoría de las veces el equipo trabajó en el juego en el modo "lanzamiento de mañana", así que escribí todos los subsistemas, en primer lugar, tratando de no romper lo que ya funciona, y en segundo lugar, con un ciclo de desarrollo corto. En particular, la mayoría de los efectos de los que el sistema estándar no era capaz se realizaron en el sombreador de fragmentos sin afectar el código principal.
La restricción en el número de partículas (las matrices de transformación para cada partícula se formaron en la CPU, la conclusión fue a través del instalador del ios extensible gl), por ejemplo, era necesario escribir un sombreador que "emulara" una gran variedad de partículas en función de una representación analítica de la forma de los objetos y combinada con el espacio palma de datos falsos en el búfer de profundidad.
La coordenada z del fragmento se calculó para una partícula plana, como si estuviéramos dibujando una esfera, y el radio de esta esfera fue modulado por el seno del ruido de Perlin teniendo en cuenta el tiempo:
r=.5+.5*sin(perlin(specialUV)+time)
Se puede encontrar una descripción completa de la reconstrucción de la profundidad de la esfera en
Íñigo Quílez , pero utilicé un código simplificado y más rápido. Por supuesto, era una aproximación aproximada, pero en formas geométricas complejas (humo, explosiones) dio una imagen bastante decente.
Captura de pantalla del juego. La "falda" de humo se hizo en una pequeña parte, varias más quedaron en el cuerpo principal de la explosión. Por supuesto, se veía espectacularmente "desde el suelo", cuando el humo envolvió suavemente edificios y unidades, sin embargo, las propuestas para cambiar la posición de la cámara durante la explosión no entraron en producción.Declaración del problema.
¿Qué querías sacar? Más bien, pasamos de las limitaciones con las que nos atormentaron el sistema de partículas anterior. La situación empeoró por el hecho de que el presupuesto del marco estaba casi agotado, y en dispositivos débiles (como el ipad air), las tuberías de píxeles y vértices estaban completamente cargadas. Por lo tanto, quería obtener el sistema más productivo como resultado, incluso si limitaba un poco la funcionalidad.
Los diseñadores compilaron una lista de características y dibujaron un boceto de la interfaz de usuario basada en su propia experiencia y práctica con unidad, irreal y efectos posteriores.
Tecnología disponible
Debido al legado y las restricciones impuestas por la oficina central, estábamos limitados a las operaciones 2. Por lo tanto, las tecnologías como la retroalimentación de transformación utilizada en los sistemas de partículas modernos no estaban disponibles.
Lo que quedaba? ¿Usar la búsqueda de textura de vértice y almacenar posiciones / aceleraciones en texturas? Una opción de trabajo, pero la memoria también está casi terminada, el rendimiento de una solución de este tipo no es el más óptimo y el resultado no es diferente en belleza arquitectónica.
En este momento, había leído muchos artículos sobre la implementación de sistemas de partículas en gpu. La gran mayoría contenía un título brillante ("millones de partículas en gpu móvil, con preferencia y poetas"), sin embargo, la implementación se redujo a ejemplos de emisores / atractores simples, aunque divertidos, y en general fue casi inútil para un uso real en el juego.
Este artículo trajo el máximo beneficio: el autor resolvió el problema real y no hizo "partículas esféricas en el vacío". Los números de referencia de este artículo y los resultados de la creación de perfiles ahorraron mucho tiempo en la etapa de diseño.
Buscar enfoques
Comencé clasificando los problemas resueltos por el sistema de partículas y buscando casos particulares. Resultó aproximadamente lo siguiente (una parte de los muelles reales del concepto de la correspondencia con el líder del equipo):
"- Conjuntos de partículas / mallas con movimiento cíclico. Sin posición de procesamiento, todo a través de la ecuación de movimiento. Aplicaciones: es posible el humo de las tuberías, el vapor sobre el agua, la nieve / lluvia, la niebla volumétrica, los árboles que se balancean, el uso parcial de los efectos no cíclicos de las explosiones aka.
- Cintas Formación de vb por evento, procesando solo en la GPU (disparos por rayos, vuelos a lo largo de una trayectoria fija (?) Con un rastro). Tal vez la variante con la transferencia de las coordenadas de inicio-fin a los uniformes y la construcción de la cinta por vertexID despegue. con t.z. renderizar cruz con fresnel como en directlights + uvscroll.
- Generación de partículas y procesamiento de velocidad. La opción más versátil y más difícil / más lenta, vea el procesamiento de movimiento tecnológico ”.
En resumen: hay diferentes efectos de partículas, y algunos de ellos pueden implementarse más fácilmente que otros.
Decidimos dividir la tarea en varias iteraciones, de simples a complejas. La creación de prototipos se realizó en mi motor / editor en windows / directx11 debido al hecho de que la velocidad de dicho desarrollo fue varias veces mayor. El proyecto se compiló en un par de segundos, y los sombreadores se editaron completamente "sobre la marcha" y se compilaron en segundo plano, mostrando el resultado en tiempo real y sin requerir gestos adicionales como presionar botones. Creo que cualquiera que haya construido grandes proyectos con un montón de macbook / xcode entenderá los motivos de esta decisión.
Todos los ejemplos de código se tomarán del prototipo de Windows.
Entorno de desarrollo para ventanas.Implementación
La primera etapa es la salida estática de una matriz de partículas. Nada complicado: inicie la protección de vértices, rellene con quads (escriba el uv correcto para cada quad) y cosa la identificación del vértice en el uv "adicional". Después de eso, en el sombreador, mediante la identificación del vértice en función de la configuración del emisor, formamos las posiciones de las partículas y, por medio de UV, restauramos las coordenadas de la pantalla.
Si vertex_id está disponible de forma nativa, puede prescindir completamente de un búfer y sin uv para restaurar las coordenadas de la pantalla (como resultado de lo cual se hizo en la versión de Windows).
Shader:
struct VS_INPUT { … uint v_id:SV_VertexID; … } //float index = input.uv2.x/6.0;// vertex_id index = floor(input.v_id/6.0);// vertex_id float2 map[6]={0,0,1,0,1,1,0,0,1,1,0,1}; float2 quaduv=map[frac(input.v_id/6.0)*6];
Después de eso, puede implementar escenarios simples con una cantidad muy pequeña de código, por ejemplo, el movimiento cíclico con pequeñas desviaciones es adecuado para el efecto de nieve. Sin embargo, nuestro objetivo era dar el control del comportamiento de las partículas al lado de los artistas, y ellos, como saben, rara vez saben cómo sombrear. La opción con valores predeterminados de comportamiento y parámetros de edición a través de los controles deslizantes tampoco resultó atractiva: cambiar los sombreadores o bifurcarse en el interior, multiplicar las opciones predeterminadas, falta de control total.
La siguiente tarea fue implementar la aparición y desaparición gradual de este sistema. Las partículas no deberían aparecer de la nada y desaparecer en la nada. En la implementación clásica de un sistema de partículas, procesamos el búfer mediante programación utilizando CPU, creando nuevas partículas y eliminando las antiguas. De hecho, para obtener un buen rendimiento, debe escribir un administrador de memoria inteligente. Pero, ¿qué sucede si simplemente no dibujas las partículas "muertas"?
Supongamos (para empezar) que el intervalo de tiempo de emisión de partículas y la vida útil de una partícula es una constante dentro de un solo emisor.

Luego podemos presentar especulativamente nuestro búfer (que contiene solo la identificación del vértice) como circular y determinar su tamaño máximo de la siguiente manera:
pCount = round (prtPerSec * LifeTime / 60.0); pCountT = floor (prtPerSec * EmissionEndTime / 60.0); pCount=min (pCount, pCountT);
y en el sombreador, calcule el tiempo en función del índice y el tiempo (tiempo transcurrido desde el inicio del efecto)
pTime=time-index/prtPerSec;
Si el emisor está en una fase cíclica (todas las partículas se emiten y ahora mueren y nacen sincrónicamente), hacemos fractura desde el momento de la partícula y, por lo tanto, obtenemos un bucle.
No necesitamos dibujar partículas con pTime menor que cero; aún no han nacido. Lo mismo se aplica a las partículas en las que la suma de la vida útil y el tiempo actual excede el tiempo de finalización de la emisión. En ambos casos, no dibujaremos nada anulando el tamaño de partícula y / o colocándolo detrás de la pantalla. Este enfoque proporcionará una pequeña sobrecarga en las fases de desvanecimiento / desvanecimiento, al tiempo que mantiene el máximo rendimiento en la fase de sostenido.
El algoritmo se puede mejorar ligeramente enviando solo esa parte del búfer de vértices que contiene partículas vivas para renderizar. Debido al hecho de que la emisión se produce secuencialmente, las partículas vivas se segmentarán como máximo una vez, es decir. Se requieren dos llamadas.
Ahora, conociendo el tiempo actual de cada partícula, puede establecer la velocidad, la aceleración (y, en general, cualquier otro parámetro) para escribir la ecuación de movimiento, obteniendo finalmente las coordenadas en el espacio mundial.
Usando restablecido desde vertex_id uv, ya obtendremos cuatro puntos (más precisamente, moveremos cada uno de los puntos cuádruples en la dirección que necesitemos), sobre los cuales el sombreador de vértices, después de completar la proyección, completará su trabajo.
p.xy+=(quaduv-.5);
Con la bonificación gratuita, tuvimos la oportunidad no solo de pausar el emisor, sino también de rebobinar el tiempo de un lado a otro con precisión en el cuadro. Esta característica resultó ser muy útil en el diseño de efectos complejos.
Aumentamos la funcionalidad
La siguiente iteración en el desarrollo fue la solución al problema de un emisor en movimiento. Nuestro sistema en particular no sabía nada acerca de su posición, y cuando el emisor se movió, todo el efecto se movió sincrónicamente detrás de él. Para el humo del tubo de escape y efectos similares, parecía más que extraño.
La idea era registrar la posición del emisor en un buffer de vértices cuando nació una nueva partícula. Como el número de tales partículas es pequeño, la sobrecarga debería haber sido mínima.
Un colega sugirió que al desarrollar su propia interfaz de usuario, usaba map / unmap solo parte del buffer de vértices y estaba bastante satisfecho con el rendimiento de esta solución. Hice pruebas, y resultó que este enfoque realmente funciona bien tanto en plataformas de escritorio como móviles.
La dificultad surgió con la sincronización del tiempo en CPU y GPU. Era necesario asegurarse de que la actualización del búfer se hiciera exactamente cuando la "nueva" partícula en bucle estaba en su posición inicial. Es decir, en relación con el búfer de anillo, es necesario sincronizar los límites de la región de actualización con el tiempo de funcionamiento del emisor.
Transferí el código hlsl a C ++, para la prueba escribí el emisor moviéndose alrededor de Lissajous, y todo esto funcionó de repente. Sin embargo, de vez en cuando, el sistema "escupió" una o más partículas, disparándolas en una dirección arbitraria, sin eliminarlas a tiempo o creando otras nuevas en lugares arbitrarios.
El problema se resolvió auditando la precisión del cálculo del tiempo en el motor y verificando simultáneamente el delta de tiempo al registrar la nueva posición del emisor, de modo que se actualizara toda la sección del búfer que no se vio afectada por la iteración anterior. También era necesario que el sistema funcionara en condiciones de desincronización forzada: una reducción repentina de fps no debería romper el efecto, especialmente porque para diferentes dispositivos nuestro juego registró diferentes fps de acuerdo con el rendimiento: 60/30/20.
El código del método ha crecido bastante (el buffer de anillo es difícil de procesar con elegancia), sin embargo, después de tener en cuenta todas las condiciones, el sistema funcionó de manera correcta y estable.
Alrededor de este tiempo, el socio ya había hecho el "pez" del editor, suficiente para probar el sistema, y escribió las plantillas / api para integrar el sistema en nuestro motor.
Porté todo el código a ios / opengl, lo integré y finalmente realicé pruebas de efectos reales en un dispositivo real. Quedó claro que el sistema no solo funciona, sino que también es adecuado para la producción. Quedaba por terminar el editor de IU y pulir el código al estado "no da miedo darlo para que se publique mañana".
Incluso nos preparamos para escribir un administrador de memoria para no asignar / destruir un búfer (que finalmente almacenaba vertex_id, uv, posición y vector de partículas inicial) para cada nuevo efecto con un emisor dinámico, ya que se me ocurrió otra idea.
El hecho de la existencia del búfer de vértices en este sistema me perseguía. Él claramente miró en su arcaísmo, "el legado de las edades oscuras del transportador fijo". Al realizar efectos de prueba en un prototipo de Windows, pensé que el movimiento del emisor siempre es suave y mucho más lento que el movimiento de la partícula. Además, con una gran cantidad de partículas, la actualización de la posición lleva al hecho de que cientos de partículas registran los mismos datos. La solución resultó ser simple: presentamos una matriz fija en la que caerá el "historial" de la posición del emisor, normalizado por la vida útil de la partícula. Y en gpu interpolaremos los datos. Después de eso, la necesidad de búferes dinámicos desapareció en la versión ios / gles2 (solo quedaba la estática general para implementar vertex_id), y en las versiones de windows / dx11 los búferes desaparecieron por completo debido al vértice_id nativo y la capacidad de d3d api para aceptar nulo en lugar de vincularse al búfer de vértices.
Por lo tanto, la versión ganadora del sistema, según los estándares modernos, no consume memoria en absoluto, sin importar cuántas partículas queramos mostrar. Solo un pequeño buffer constante con parámetros, un buffer de posiciones / bases (60 pares de vectores resultaron ser suficientes, con un margen, en cualquier caso) y, si es necesario, textura. Las mediciones de rendimiento muestran una velocidad cercana a las pruebas sintéticas.
Además, la "cola" en efectos como chispas comenzó a verse mucho más natural, ya que la interpolación permitió eliminar la discretización por cuadros y, por lo tanto, el emisor cambió su posición suavemente, como si las llamadas de dibujo se realizaran a una frecuencia de cientos de hercios.
Caracteristicas
Además de la funcionalidad básica del vuelo de la partícula (velocidad, aceleración, gravedad, resistencia del medio), necesitábamos una cierta cantidad de "grasa" funcional.
Como resultado, el desenfoque de movimiento (estirando una partícula a lo largo de un vector de movimiento), la orientación de las partículas a través del vector de movimiento (esto permite, por ejemplo, hacer una esfera de partículas), redimensionar la partícula de acuerdo con el momento actual de su vida, y se implementaron docenas de otras pequeñas cosas.
La complejidad surgió con los campos vectoriales: dado que el sistema no almacena su estado (posición, aceleración, etc.) para cada partícula, sino que los calcula cada vez a través de la ecuación de movimiento, una serie de efectos (como el movimiento de la espuma cuando se agita el café) eran imposibles en principio. Sin embargo, una simple modulación de la velocidad y la aceleración por el ruido de perlin dio resultados que parecen bastante modernos. El cálculo de ruido en tiempo real para tantas partículas resultó ser demasiado costoso (incluso con un límite de cinco octavas), por lo que se generó una textura a partir de la cual el sombreador de vértices luego tomaría muestras. Para mejorar el efecto de un campo de vector falso, se agregó un pequeño desplazamiento de las coordenadas de la muestra dependiendo del tiempo actual del emisor.
La prueba de humo de cigarrillo funciona mediante la distribución de la velocidad inicial y la aceleración sobre el ruido perlin.Transportador de píxeles
Inicialmente, solo planeamos cambiar el color / transparencia de la partícula dependiendo de su tiempo. Agregué varios algoritmos al sombreador de píxeles.
Rotación de color de textura: simplificado, sin (color + tiempo). Permite en cierta medida imitar el efecto de permutación de AfterEffects.
Iluminación falsa: modulación del color de una partícula por un gradiente en coordenadas mundiales, independientemente del ángulo de rotación de la partícula.
Evolución de los bordes: cuando una partícula se mueve en el espacio, sus bordes (canal alfa) se modulan mediante una combinación de reflector y ruido perlin, lo que proporciona una dinámica de flujo muy similar a las nubes, el humo y otros efectos de fluidos.
Pseudocódigo de sombreador:
b=perlin(uv)
En una versión un poco complicada, este sombreador puede dibujar bordes con suavidad arbitraria y con un resaltado de contorno, lo que agrega efectos "explosivos" al realismo.
Los primeros experimentos con la evolución de los límites.Que sigue
A pesar del editor, ya listo para trabajar e integrado en el motor, los diseñadores no tuvieron tiempo de hacer un solo efecto: el proyecto se cerró. Sin embargo, no existen obstáculos para utilizar estas prácticas en otros lugares, por ejemplo, para trabajar en la revisión de demostración.
Desde un punto de vista tecnológico, también hay espacio para moverse; ahora, por ejemplo, están en funcionamiento varios efectos de destrucción de objetos con estructura de alambre:

La cuestión de clasificar las partículas para la mezcla alfa sigue abierta hasta ahora: dado que todo se considera analíticamente en el sombreador, en realidad no hay datos de entrada para la clasificación. ¡Pero hay un gran campo para la experimentación!
Durante el desarrollo de Titan World, se aplicaron muchos trucos en la parte gráfica del juego, pero más sobre eso la próxima vez.
PD: puede profundizar en el motor alfa de origen
aquí . Los ejemplos están en la carpeta release / samples, las teclas de control principales son espacio, alt | control + mouse. Los sombreadores se encuentran directamente en los archivos fxp, su código está disponible a través de la ventana del editor.