Animación de miles de objetos con MultiMeshInstance¶
Este tutorial explora una técnica usada en el juego ABZU para renderizar y animar miles de peces usando animación de vértices e instanciación estática de malla.
En Godot, esto se puede lograr con un personalizado Shader y una MultiMeshInstance. Usando la siguiente técnica puedes renderizar miles de objetos animados, incluso en hardware de gama baja.
Empezaremos animando a un pez. Luego, veremos cómo extender esa animación a miles de peces.
Animando un objeto¶
Empezaremos con un solo pez. Carga tu modelo de pez en un MeshInstance y añade un nuevo ShaderMaterial.
Aquí está el pez que usaremos para las imágenes de ejemplo, puedes usar cualquier modelo de pez que quieras.
Nota
El modelo de pez en este tutorial está hecho por QuaterniusDev y se comparte con una licencia creative commons. CC0 1.0 Universal (CC0 1.0) Public Domain Dedication https://creativecommons.org/publicdomain/zero/1.0/
Típicamente, usarías huesos y un Skeleton para animar objetos. Sin embargo, los huesos se animan en la CPU y así terminas teniendo que calcular miles de operaciones en cada fotograma y se hace imposible tener miles de objetos. Usando la vertex animation en un vertex sharder, evitas usar huesos y puedes en cambio calcular la animación completa en unas pocas líneas de código y completamente en la GPU.
La animación se hará con cuatro claves de movimiento:
Un movimiento de lado a lado
Un movimiento pivotante alrededor del centro del pez
Un movimiento de onda de panorámico
Un movimiento de giro de panorámica
Todo el código de la animación estará en el vertex shader uniforms que controlan la cantidad de movimiento. Usamos uniforms para controlar la fuerza del movimiento para que puedas ajustar la animación en el editor y ver los resultados en tiempo real, sin que el shader tenga que recompilar.
Todos los movimientos se harán usando ondas coseno aplicadas a VERTEX
en el espacio modelo. Queremos que los vértices estén en el espacio modelo para que el movimiento sea siempre relativo a la orientación de los peces. Por ejemplo, de lado a lado siempre moverá al pez de izquierda a derecha, en lugar de en el eje x
en la orientación del mundo.
Para controlar la velocidad de la animación, empezaremos definiendo nuestra propia variable de tiempo usando TIME
.
//time_scale is a uniform float
float time = TIME * time_scale;
El primer movimiento que implementaremos es el movimiento de lado a lado. Puede hacerse compensando VERTEX.x
por cos
de TIME
. Cada vez que la malla se renderice, todos los vértices se moverán de lado por la cantidad de cos(time)
.
//side_to_side is a uniform float
VERTEX.x += cos(time) * side_to_side;
La animación resultante debería ser algo así:
A continuación, añadimos el pivote. Debido a que el pez está centrado en (0, 0), todo lo que tenemos que hacer es multiplicar VERTEX
por una matriz de rotación para que gire alrededor del centro del pez.
Construimos una matriz de rotación así:
//angle is scaled by 0.1 so that the fish only pivots and doesn't rotate all the way around
//pivot is a uniform float
float pivot_angle = cos(time) * 0.1 * pivot;
mat2 rotation_matrix = mat2(vec2(cos(pivot_angle), -sin(pivot_angle)), vec2(sin(pivot_angle), cos(pivot_angle)));
Y luego lo aplicamos en los ejes x
y z
multiplicándolo por VERTEX.xz
.
VERTEX.xz = rotation_matrix * VERTEX.xz;
Con sólo el pivote aplicado, deberías ver algo como esto:
Los próximos dos movimientos necesitan bajar por la espina dorsal del pez. Para eso, necesitamos una nueva variable, body
. body
es un real que está 0
en la cola del pez y 1
en su cabeza."cuerpo" es un flotador que está 0
en la cola del pez y 1
en su cabeza.
float body = (VERTEX.z + 1.0) / 2.0; //for a fish centered at (0, 0) with a length of 2
El siguiente movimiento es una onda coseno que se mueve a lo largo del pez. Para hacer que se mueva a lo largo de la espina dorsal del pez, compensamos la entrada a cos
por la posición a lo largo de la espina dorsal, que es la variable que definimos arriba, body
.
//wave is a uniform float
VERTEX.x += cos(time + body) * wave;
Esto se ve muy similar al movimiento de lado a lado que definimos arriba, pero en este, al usar body
para desplazar cos
cada vertex a lo largo de la espina tienes una posición diferente en la onda haciendo que parezca que una onda se está moviendo a lo largo del pez.
El último movimiento es el giro, que es un rollo de desplazamiento a lo largo de la columna vertebral. De manera similar al pivote, primero construimos una matriz de rotación.
//twist is a uniform float
float twist_angle = cos(time + body) * 0.3 * twist;
mat2 twist_matrix = mat2(vec2(cos(twist_angle), -sin(twist_angle)), vec2(sin(twist_angle), cos(twist_angle)));
Aplicamos la rotación en los ejes xy
para que el pez parezca girar alrededor de su espina. Para que esto funcione, la columna vertebral del pez necesita estar centrada en el eje z
.
VERTEX.xy = twist_matrix * VERTEX.xy;
Aquí está el objeto con el giro aplicado:
Si aplicamos todos estos movimientos uno tras otro, obtenemos un movimiento fluido y como gelatina.
Los peces normales nadan principalmente con la mitad trasera de su cuerpo. Por consiguiente, necesitamos limitar los movimientos de desplazamiento a la mitad trasera del pez. Para hacer esto, creamos una nueva variable, mask
.
mask
es un valor decimal que va desde 0
en la parte delantera del pez hasta 1
en la parte trasera, utilizando la función smoothstep
para controlar el punto en el cual ocurre la transición de 0
a 1
.
//mask_black and mask_white are uniforms
float mask = smoothstep(mask_black, mask_white, 1.0 - body);
Abajo hay una imagen del pez con mask
usada como COLOR
:
Para la ola, multiplicamos el movimiento por la mask
que la limitará a la mitad trasera.
//wave motion with mask
VERTEX.x += cos(time + body) * mask * wave;
Para aplicar la máscara al giro, usamos mix
. mix
nos permite mezclar la vertex position entre un vértice completamente girado y uno que no está girado. Necesitamos usar mix
en lugar de multiplicar la mask
por el VERTEX
girado porque no estamos añadiendo el movimiento al VERTEX
, estamos reemplazando el VERTEX
con la versión girada. Si lo multiplicamos por la mask
, reduciremos el tamaño de los peces.
//twist motion with mask
VERTEX.xy = mix(VERTEX.xy, twist_matrix * VERTEX.xy, mask);
La unión de los cuatro movimientos nos da la animación final.
Adelante, juega con los uniforms para alterar el ciclo de natación de los peces. Verás que puedes crear una gran variedad de estilos de natación usando estos cuatro movimientos.
Hacer un banco de peces¶
Godot facilita la renderización de miles de un mismo objeto utilizando un nodo MultiMeshInstance.
Se crea un nodo MultiMeshInstance y se utiliza de la misma manera que se haría un nodo MeshInstance. Para este tutorial, nombraremos al nodo MultiMeshInstance School
, porque contendrá un banco de peces.
Una vez que tengas una MultiMeshInstance agrega un MultiMesh, y a esa MultiMesh agrega tu Mesh con el shader superior.
MultiMeshes dibuja tu malla con tres propiedades adicionales por sustancia: Transformación (rotación, traslación, escala), Color y Custom. Custom se usa para pasar 4 variables multiusos usando un Color.
"instance_count
especifica cuántas instancias de la malla quieres dibujar. Por ahora, deja instance_count
en 0
porque no puedes cambiar ninguno de los otros parámetros mientras instance_count
sea mayor que 0
. Más tarde pondremos instance_count
en GDScript.
transform_format
especifica si las transformaciones utilizadas son 3D o 2D. Para este tutorial, selecciona 3D.
Tanto para el color_format
como para el custom_data_format
puedes elegir entre None
, Byte
y Float
. None
significa que no pasarás esos datos (ya sea una variable de COLOR
o INSTANCE_CUSTOM
) al shader. Byte
significa que cada número que compone el color que pasas se almacenará con 8 bits mientras que Float
significa que cada número se almacenará en un número de real (32 bits). Float
es más lento pero más preciso, Byte
tomará menos memoria y será más rápido, pero puedes ver algunos artefactos visuales.
Ahora, pon instance_count
al número de peces que quieres tener.
Lo siguiente que necesitamos es establecer las transformaciones por instancia.
Hay dos formas de establecer transformaciones por instancia para MultiMeshes. La primera está completamente en el editor y es descrita en el MultiMeshInstance tutorial.
La segunda es hacer un bucle sobre todas las instancias y establecer sus transformaciones en código. A continuación, usamos GDScript para hacer un bucle sobre todas las instancias y establecer su transformación en una posición aleatoria.
for i in range($School.multimesh.instance_count):
var position = Transform()
position = position.translated(Vector3(randf() * 100 - 50, randf() * 50 - 25, randf() * 50 - 25))
$School.multimesh.set_instance_transform(i, position)
Ejecutar este guión colocará a los peces en posiciones aleatorias en una caja alrededor de la posición de la MultiMeshInstance.
Nota
Si la actuación es un problema para ti, intenta hacer la escena con GLES2 o con menos peces.
¿Nota que todos los peces están en la misma posición en su ciclo de natación? Los hace parecer muy robóticos. El siguiente paso es darle a cada pez una posición diferente en el ciclo de natación para que todo el banco parezca más orgánico.
Animar un banco de peces¶
Uno de los beneficios de animar a los peces usando funciones de cos
es que se animan con un parámetro, time
. Para darle a cada pez una posición única en el ciclo de natación, sólo necesitamos compensar el time
.
Lo hacemos añadiendo el valor personalizado INSTANCE_CUSTOM
a time
.
float time = (TIME * time_scale) + (6.28318 * INSTANCE_CUSTOM.x);
A continuación, tenemos que pasar un valor a INSTANCE_CUSTOM
. Lo hacemos añadiendo una línea en el bucle for
superior. En el bucle for
asignamos a cada instancia un conjunto de cuatro reales aleatorios para usar.
$School.multimesh.set_instance_custom_data(i, Color(randf(), randf(), randf(), randf()))
Ahora todos los peces tienen posiciones únicas en el ciclo de natación. Puedes darles un poco más de individualidad usando INSTANCE_CUSTOM
para hacerlos nadar más rápido o más lento multiplicando por el TIME
.
//set speed from 50% - 150% of regular speed
float time = (TIME * (0.5 + INSTANCE_CUSTOM.y) * time_scale) + (6.28318 * INSTANCE_CUSTOM.x);
Incluso puedes experimentar con el cambio de color por instancia de la misma manera que cambiaste el valor personalizado por instancia.
Un problema con el que te encontrarás en este punto es que los peces están animados, pero no se mueven. Puedes moverlos actualizando la transformación por instancia de cada pez en cada fotograma. Aunque hacerlo será más rápido que mover miles de MeshInstances por fotograma, es probable que siga siendo lento.
En el próximo tutorial cubriremos cómo usar Particles para aprovechar la GPU y mover cada pez individualmente mientras se reciben los beneficios del instanciamiento.