La implementación del efecto acuarela en los juegos.

imagen

Introducción


Cuando en enero de 2019 comenzamos a discutir nuestro nuevo juego de tinte. , inmediatamente decidimos que el efecto acuarela sería el elemento más importante. Inspirados por este anuncio de Bulgari , nos dimos cuenta de que la implementación de la pintura de acuarela debería ser coherente con la alta calidad de los recursos restantes que planeamos crear. Encontramos un interesante artículo de investigadores de Adobe (1) . La técnica de acuarela descrita en ella se veía maravillosa, y debido a su naturaleza vectorial (en lugar de píxel), podría funcionar incluso en dispositivos móviles débiles. Nuestra implementación se basa en este estudio, cambiamos y / o simplificamos partes del mismo porque nuestros requisitos de rendimiento eran diferentes. tinte .- este es un juego, por lo tanto, además del dibujo en sí, necesitábamos renderizar todo el entorno 3D y ejecutar la lógica del juego en un solo cuadro. También buscamos asegurarnos de que la simulación se realizara en tiempo real y el jugador viera de inmediato lo que se dibujaba.


Simulación de color de agua en tinte.

En este artículo, compartiremos los detalles individuales de la implementación de esta técnica en el motor del juego Unity y hablaremos sobre cómo la adaptamos para que funcione sin problemas en dispositivos móviles de gama baja. Hablaremos más sobre las etapas principales de este algoritmo, pero sin demostrar el código. Esta implementación se creó en Unity 2018.4.2 y luego se actualizó a la versión 2018.4.7.

¿Qué es el tinte?


Tinte . - Este es un juego de rompecabezas que permite al jugador completar los niveles, mezclando los colores de las acuarelas para que coincidan con los colores del origami. El juego fue lanzado en el otoño de 2019 en Apple Arcade para iOS, macOS y tvOS.


Tinte de captura de pantalla.

Requisitos


La técnica descrita en mi artículo se puede dividir en tres etapas principales realizadas en cada cuadro:

  1. Genere nuevos puntos basados ​​en la entrada del jugador y agréguelos a la lista de puntos
  2. Simulación de pintura para todos los puntos en la lista
  3. Representación puntual

A continuación, hablaremos en detalle sobre cómo implementamos cada una de las etapas.

Nuestro objetivo era alcanzar 60 FPS, es decir, estas etapas y toda la lógica que se describe a continuación se realizan 60 veces por segundo.

Obteniendo entrada


En cada cuadro, transformamos la entrada del jugador (dependiendo de la plataforma, puede ser un toque, la posición del mouse o el cursor virtual) en una estructura splatData que contiene la posición, el vector de movimiento, el color y la presión (2). Primero, verificamos la longitud de deslizamiento del jugador en la pantalla y la comparamos con un valor umbral determinado. Con golpes cortos, generamos un punto por cuadro en la posición de entrada. En el caso opuesto, llenamos la distancia entre los puntos de inicio y final del deslizamiento del jugador con nuevos puntos creados con una densidad predeterminada (esto asegura una densidad de pintura constante independientemente de la velocidad del deslizamiento). El color indica la pintura actual utilizada, y la pendiente del movimiento indica la dirección del deslizamiento. Los nuevos puntos creados se agregan a una colección llamada splatList, que también contiene todos los puntos creados previamente. Se utiliza para simular y renderizar pintura en los siguientes pasos. Cada punto individual denota una "gota" de pintura que necesita ser renderizada, el componente principal de la pintura de acuarela. El dibujo de acuarela terminado será el resultado de renderizar decenas / cientos de puntos de intersección. Además, el valor de la vida útil (en cuadros) se asigna al punto recién creado, que determina cuánto tiempo se puede simular el punto.


Un ejemplo de interpolación de puntos largos de deslizamiento. Los círculos huecos indican puntos creados a intervalos regulares.

Lona


Al igual que la pintura real, necesitamos un lienzo. Para implementarlo, creamos un área limitada en el espacio 3D que se parece a una hoja de papel. Las coordenadas de entrada del jugador y todas las demás operaciones, como renderizar una malla, se registran en el espacio del lienzo. Del mismo modo, el tamaño en píxeles de cualquier búfer utilizado para simular el dibujo depende del tamaño del lienzo. El término "lienzo" como se usa en este artículo no está asociado de ninguna manera con la clase Canvas de la Unidad de interfaz de usuario.


El rectángulo verde muestra el área del lienzo en el juego.

Mancha


Visualmente, el punto está representado por una malla redonda, cuyo borde consta de 25 vértices. Puede percibirlo como una "gota" que un pincel húmedo deja en un pedazo de papel si lo toca por un momento muy breve. Agregamos un pequeño desplazamiento aleatorio a la posición de cada vértice, lo que garantiza la irregularidad de los bordes de las manchas de pintura.


Ejemplos de mallas de malla.

Para cada vértice, también almacenamos el vector de velocidad hacia afuera, que luego se usa en la fase de simulación. Generamos varias mallas con pequeñas variaciones entre ellos y almacenamos sus datos en un objeto skriptuemy ( un objeto programable ). Cada vez que un jugador dibuja un lugar en tiempo real, le asignamos una malla seleccionada al azar de este conjunto. Vale la pena mencionar que a diferentes resoluciones de pantalla, el lienzo tiene un tamaño diferente en píxeles. Para que en todos los dispositivos el coeficiente del tamaño de los puntos sea el mismo, cuando comienzas el juego, cambiamos la escala de acuerdo con el tamaño del lienzo.


Un ejemplo de vectores spot almacenados con nuevos datos spot.

Cuando se genera una malla de puntos, también guardamos su "área de humectación", que define un conjunto de píxeles que están dentro de los bordes de puntos originales. El área de humectación se usa para simular la advección . Durante la ejecución de la aplicación en el momento de crear cada nuevo lugar, marcamos el lienzo debajo como húmedo. Al simular el movimiento de la pintura, permitimos que se "extienda" sobre aquellas áreas del lienzo que ya se han mojado. Almacenamos el contenido de humedad del lienzo en el búfer global de mapa húmedo , que se actualiza a medida que se agrega cada nuevo lugar. Además de participar en la mezcla de dos colores, la advección juega un papel importante en la apariencia final del trazo de pintura.


Wetmap de llenado , los píxeles dentro de la forma del punto (círculo verde) marcan el wetmap tampón (rejilla) en forma húmeda (verde). El búfer de mapa húmedo tiene una resolución mucho más alta.

Además, cada punto también contiene un valor de opacidad , que es una función de su área; representa el efecto de almacenar pigmento (una cantidad constante de pigmento en el lugar). Cuando el tamaño de un punto aumenta durante la simulación, su opacidad disminuye y viceversa.


Un ejemplo de pintura sin advección (izquierda) y con ella (derecha).


Ejemplos de advección de pintura.

Ciclo de simulación


Una vez que se recibe la entrada del jugador en el cuadro actual y se convierte en nuevos puntos, el siguiente paso es simular los puntos para simular la propagación de las acuarelas. Al comienzo de esta simulación, tenemos una lista de puntos que deben actualizarse y un mapa húmedo actualizado .

En cada cuadro, recorremos la lista de puntos y cambiamos las posiciones de todos los vértices de los puntos usando la siguiente ecuación:


donde: m es el nuevo vector de movimiento, a es el parámetro de corrección constante (0.33), b es el vector de pendiente de movimiento = dirección normalizada del deslizamiento del jugador multiplicado por 0.3, cr es el valor escalar de la rugosidad del lienzo = Random.Range (1,1 + r), r es el parámetro de rugosidad global, para la pintura estándar lo establecemos en 0.4, v es el vector de velocidad creado de antemano con la malla de puntos, vm es el factor de velocidad, el valor escalar que usamos localmente en algunas situaciones para acelerar la advección, x (t + 1) - posible nueva posición del vértice, x (t) - posición actual del vértice, brEs el vector de rugosidad de ramificación = (Random.Range (-r, r), Random.Range (-r, r)), w (x) es el valor de humectación en el búfer de mapa húmedo.

El resultado de tales ecuaciones se llama caminata aleatoria sesgada , imita el comportamiento de las partículas en la pintura de acuarela real. Estamos tratando de mover cada vértice del punto hacia afuera desde su centro ( v ), agregando aleatoriedad. Luego, la dirección del movimiento cambia ligeramente con la dirección del golpe ( b ) y otra vez se aleatoriza por otro componente de rugosidad ( br ). Entonces, esta nueva posición de vértice se compara con un mapa húmedo . Si el lienzo en la nueva posición ya estaba mojado (valor en el búfer de mapa húmedomayor que 0), entonces le damos al vértice una nueva posición x (t + 1) , de lo contrario no cambiamos su posición. Como resultado, la pintura se extenderá solo en aquellas áreas del lienzo que ya estaban húmedas. En la última etapa, recalculamos el área puntual, que se utiliza en el ciclo de renderizado para cambiar su opacidad.


Ejemplo de microescala de simulación de advección entre dos puntos activos de pintura.

Ciclo de renderizado - Buffer húmedo


Después de volver a contar los puntos, puede comenzar a representarlos. En la salida después de la etapa de emulación, la malla de puntos a menudo se deforma (por ejemplo, ocurren intersecciones), por lo tanto, para su representación correcta sin costos adicionales por triangulación repetida, utilizamos una solución con búfer de plantilla de dos pasadas. La interfaz de dibujo de Unity Graphics se utiliza para representar puntos , y el ciclo de representación se realiza dentro del método Unity OnPostRender . Las mallas puntuales se procesan para representar la textura ( wetBuffer ) usando una cámara separada. Al comienzo del ciclo, wetBuffer se borra y se establece como un objetivo de renderizado mediante Graphics.SetRenderTarget (wetBuffer) . Siguiente para cada punto activo de splatList ejecutamos la secuencia que se muestra en el siguiente diagrama:


Diagrama del ciclo de renderizado.

Comenzamos limpiando el búfer de la plantilla antes de cada punto para que el estado del búfer de la plantilla del punto anterior no afecte al nuevo punto. Luego seleccionamos el material utilizado para dibujar el lugar. Este material es responsable del color del punto, y lo seleccionamos en función del índice de color almacenado en splatData cuando el jugador dibujó el punto. Luego cambiamos la opacidad del color (canal alfa) en función del área de la malla de puntos calculada en el paso anterior. La representación en sí se realiza utilizando un sombreador de búfer de plantilla de dos pasos. En la primera pasada (Material.SetPass (0)) pasamos la malla de puntos original para registrar las coordenadas en las que se llena la malla. Con este pase ColorMaskasignado un valor de 0, por lo que la malla en sí no se representa. En la segunda pasada (Material.SetPass (1)) usamos el cuadrilátero descrito alrededor de la malla de puntos. Verificamos el valor en el búfer de la plantilla para cada píxel del cuadrilátero; si el valor es uno, se representa el píxel; de lo contrario, se omite. Como resultado de esta operación, renderizamos la misma forma que la malla de puntos, pero ciertamente no contendrá artefactos no deseados, por ejemplo, auto intersecciones.


El procedimiento para realizar la técnica de doble búfer de plantilla (de izquierda a derecha). Tenga en cuenta que este búfer de plantilla tiene una resolución mucho más alta que la mostrada, por lo que puede mantener su forma original con gran precisión.


Un ejemplo de tres puntos de intersección representados de la manera tradicional, que condujeron a la aparición de artefactos (izquierda), y al uso de la técnica de búfer de plantilla de dos pasos con la eliminación de todos los artefactos (derecha).

Después de representar todos los puntos en wetBuffer, se muestra en la escena del juego. Nuestro lienzo utiliza un sombreador improvisado que combina un WetBuffer , un mapa de papel difuso y un mapa normal de papel.


Sombreador de lienzo: solo wetBuffer (izquierda), textura de papel agregada (centro), mapa normal agregado (derecha).

El juego admite un modo para personas con daltonismo, en el que se superponen patrones separados en la parte superior de la pintura. Para lograr esto, cambiamos el material de las manchas agregando la textura del patrón con mosaico. Los patrones siguen las reglas de mezclar los colores del juego, por ejemplo, azul (barras) + amarillo (círculos) dan verde (círculos en las barras) en la intersección. Para mezclar patrones sin problemas, deben renderizarse en el mismo espacio UV. Ajustamos las coordenadas UV del cuadrilátero utilizado en la segunda pasada del búfer de plantilla, dividiendo las posiciones x e y (que se especifican en el espacio del lienzo) por el ancho y la altura del lienzo. Como resultado, obtenemos los valores correctos de u, v en el espacio de 0 a 1.


Un ejemplo de patrones de daltonismo.

Optimización - tampón de manchas secas


Como se mencionó anteriormente, una de nuestras tareas era admitir dispositivos móviles de baja potencia. El renderizado puntual resultó ser el cuello de botella de nuestro juego. Cada punto requiere tres llamadas de sorteo (llamar dos pases + borrar el búfer de plantilla), y dado que la línea de pintura contiene decenas o cientos de puntos, el número de llamadas de sorteo aumenta rápidamente y conduce a una caída en la velocidad de cuadros. Para hacer frente a esto, aplicamos dos técnicas de optimización: primero, el dibujo simultáneo de todos los puntos "secos" en dryBuffer , y en segundo lugar, la aceleración local del secado de los puntos después de alcanzar un cierto número de puntos activos.

tampón secoEs una textura de renderizado adicional agregada al ciclo de renderizado. Como se mencionó anteriormente, cada punto tiene una vida útil (en cuadros), que disminuye con cada cuadro. Después de que la vida útil alcanza 0, la mancha se considera "seca". Los puntos secos ya no se simulan, su forma no cambia y, por lo tanto, no es necesario volver a procesarlos en cada fotograma.


DryBuffer en acción; las manchas grises muestran las manchas copiadas en dryBuffer.

Cada punto cuya vida útil llega a 0 se elimina de la lista splatList y se "copia" a dryBuffer . Durante el proceso de copia, el ciclo de renderizado se reutiliza, y esta vez dryBuffer se establece como la textura de renderizado objetivo .

La mezcla adecuada entre wetBuffer y dryBuffer no se puede lograr simplemente superponiendo los tampones en el sombreador de lienzo, porque la textura de renderizado del tampón wetBuffercontiene puntos ya representados con valor alfa (que es equivalente a alfa premultiplicado). Evitamos este problema agregando un paso al comienzo del ciclo de renderizado antes de recorrer iterativamente los puntos. En este punto, representamos un cuadrilátero del tamaño de una pirámide de recorte de cámara que muestra dryBuffer . Gracias a esto, cualquier mancha que se presente en wetBuffer ya se mezclará con manchas secas, previamente pintadas.


Una mezcla de manchas húmedas y secas.

El búfer dryBuffer acumula todos los puntos "secos" y no se borra entre fotogramas. Por lo tanto, toda la memoria asociada con las manchas caducadas se puede borrar después de que se "copien" en el búfer.


Gracias a la optimización con dryBuffer , ya no tenemos límites en la cantidad de pintura que un jugador puede aplicar al lienzo.

El uso de la técnica dryBuffer por separado permite al jugador dibujar con una cantidad casi infinita de pintura, pero no garantiza un rendimiento constante. Como se mencionó anteriormente, el trazo de pintura tiene un grosor constante , que se logra dibujando mediante la interpolación de muchos puntos entre los puntos de inicio y finalización de la pasada. En el caso de muchos golpes rápidos y largos, el jugador puede generar una gran cantidad de puntos activos. Estos puntos se simularán y representarán en el número de fotogramas especificado por su vida útil, lo que en última instancia conduce a velocidades de fotogramas más bajas.

Para garantizar una velocidad de fotogramas estable, cambiamos el algoritmo para que el número de puntos activos estuviera limitado por un valor constante de maxActiveSplats . Todos los puntos que exceden este valor instantáneamente se secan. Esto se logra reduciendo la vida útil de los puntos activos más antiguos a 0, por lo que se copian en el búfer de puntos secos antes. Dado que cuando acortamos la vida, obtenemos un lugar en el estado incompleto de la simulación (que parecerá bastante interesante), al mismo tiempo aumentamos la velocidad de propagación de la pintura. Debido al aumento de la velocidad, el punto alcanza casi el mismo tamaño que a velocidad normal con una vida útil estándar.


Demostración de un máximo de 40 puntos activos (arriba) y 80 (abajo). Las manchas secas copiadas en dryBuffer se muestran en gris. El valor indica la "cantidad" de pintura que se puede simular al mismo tiempo.

El valor de maxActiveSplats es el parámetro de rendimiento más importante, nos permite controlar con precisión el número de llamadas de dibujo que podemos asignar a la representación en acuarela. Lo configuramos al inicio, en función de la plataforma y la potencia del dispositivo. También puede cambiar este valor durante la ejecución de la aplicación si se detecta una disminución en la velocidad de fotogramas.

Conclusión


La implementación de este algoritmo se ha convertido en una tarea interesante y desafiante. Esperamos que los lectores hayan disfrutado el artículo. Puede hacer preguntas en los comentarios al original . Si desea apreciar nuestra acuarela en acción, intente jugar tinte. en el Apple Arcade .


Captura de pantalla de un juego que se ejecuta en Apple TV

(1) S. DiVerdi, A. Krishnaswamy, R. MÄch y D. Ito, "Pintura con polígonos: un motor de procedimiento de acuarela", en IEEE Transactions on Visualization and Computer Graphics, vol. 19, no. 5, pp. 723–735, mayo de 2013. doi: 10.1109 / TVCG.2012.295

(2) La presión solo se tiene en cuenta al dibujar el Apple Pencil en un iPad.

All Articles