Work in progress

The content of this page was not yet updated for Godot 4.2 and may be outdated. If you know how to improve this page or you can confirm that it's up to date, feel free to open a pull request.

Animating thousands of fish with MultiMeshInstance3D

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.

In Godot, this can be accomplished with a custom Shader and a MultiMeshInstance3D. Using the following technique you can render thousands of animated objects, even on low end hardware.

Empezaremos animando a un pez. Luego, veremos cómo extender esa animación a miles de peces.

Animando un objeto

We will start with a single fish. Load your fish model into a MeshInstance3D and add a new ShaderMaterial.

Aquí está el pez que usaremos para las imágenes de ejemplo, puedes usar cualquier modelo de pez que quieras.

../../../_images/fish.png

Nota

The fish model in this tutorial is made by QuaterniusDev and is shared with a creative commons license. CC0 1.0 Universal (CC0 1.0) Public Domain Dedication https://creativecommons.org/publicdomain/zero/1.0/

Typically, you would use bones and a Skeleton3D to animate objects. However, bones are animated on the CPU and so you end having to calculate thousands of operations every frame and it becomes impossible to have thousands of objects. Using vertex animation in a vertex shader, you avoid using bones and can instead calculate the full animation in a few lines of code and completely on the GPU.

La animación se hará con cuatro claves de movimiento:

  1. Un movimiento de lado a lado

  2. Un movimiento pivotante alrededor del centro del pez

  3. Un movimiento de onda de panorámico

  4. 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í:

../../../_images/sidetoside.gif

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:

../../../_images/pivot.gif

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.

../../../_images/wave.gif

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:

../../../_images/twist.gif

Si aplicamos todos estos movimientos uno tras otro, obtenemos un movimiento fluido y como gelatina.

../../../_images/all_motions.gif

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:

../../../_images/mask.png

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.

../../../_images/all_motions_mask.gif

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 makes it easy to render thousands of the same object using a MultiMeshInstance3D node.

A MultiMeshInstance3D node is created and used the same way you would make a MeshInstance3D node. For this tutorial, we will name the MultiMeshInstance3D node School, because it will contain a school of fish.

Once you have a MultiMeshInstance3D add a MultiMesh, and to that MultiMesh add your Mesh with the shader from above.

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.

There are two ways to set per-instance transforms for MultiMeshes. The first is entirely in editor and is described in the MultiMeshInstance3D 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 = Transform3D()
  position = position.translated(Vector3(randf() * 100 - 50, randf() * 50 - 25, randf() * 50 - 25))
  $School.multimesh.set_instance_transform(i, position)

Running this script will place the fish in random positions in a box around the position of the MultiMeshInstance3D.

Nota

If performance is an issue for you, try running the scene with fewer fish.

¿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.

One problem that you will run into at this point is that the fish are animated, but they are not moving. You can move them by updating the per-instance transform for each fish every frame. Although doing so will be faster than moving thousands of MeshInstance3Ds per frame, it'll still likely be slow.

In the next tutorial we will cover how to use GPUParticles3D to take advantage of the GPU and move each fish around individually while still receiving the benefits of instancing.