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

Ce tutoriel explore une technique utilisée dans le jeu ABZU pour rendre et animer des milliers de poissons en utilisant des animations de sommets et des instanciations de mesh statiques.

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.

Nous allons commencer par animer un poisson. Puis, nous allons voir comment reproduire celle-ci sur des milliers de poissons.

Animation d'un poisson

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

Voici le poisson que nous utiliserons pour les images d'exemple, mais vous pouvez utiliser n'importe quel modèle de poisson qui vous plaît.

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

Note

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.

L'animation sera constituée de 4 mouvements clés :

  1. Un mouvement latéral

  2. Un mouvement de pivot autour du centre du poisson

  3. Un mouvement d'ondulation

  4. Un mouvement de torsion

Tout le code de l'animation sera dans le vertex shader avec des uniformes pour contrôler la quantité de mouvement. On utilise des uniformes pour contrôler la force des mouvements afin de pouvoir ajuster l'animation dans l'éditeur et ainsi voir les changements en temps réel, sans avoir à recompiler le shader.

Tous les mouvements seront effectués à l'aide d'ondes cosinus appliquées à VERTEX dans l'espace du modèle. Nous voulons que les sommets soient dans l'espace du modèle de sorte que le mouvement soit toujours relatif à l'orientation du poisson. Par exemple, le mouvement latéral devra toujours déplacer le poisson suivant sa gauche et sa droite, et non pas le déplacer sur l'axe x dans l'orientation du monde.

Afin de contrôler la vitesse de l'animation, nous allons commencer par définir notre propre variable de temps en utilisant TIME.

//time_scale is a uniform float
float time = TIME * time_scale;

Le premier mouvement que nous allons implémenter est le mouvement latéral. Cela peut être fait en décalant `` VERTEX.x`` par `` cos`` de `` TIME``. Chaque fois que le mesh est rendu, tous les sommets se déplacent latéralement de la quantité de cos (time).

//side_to_side is a uniform float
VERTEX.x += cos(time) * side_to_side;

L'animation résultante devrait ressembler à ceci :

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

Ensuite, nous ajoutons le pivot. Comme le poisson est centré à (0, 0), il suffit de multiplier VERTEX par une matrice de rotation pour qu'il tourne autour du centre du poisson.

Nous construisons une matrice de rotation de la sorte :

//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)));

Ensuite, nous l'appliquons dans les axes x et z en la multipliant par VERTEX.xz.

VERTEX.xz = rotation_matrix * VERTEX.xz;

Avec seulement le pivot appliqué, vous devriez voir quelque chose comme ceci :

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

Les deux mouvements suivants doivent se déplacer le long de la colonne vertébrale du poisson. Pour cela, nous avons besoin d'une nouvelle variable, body. body est un float qui vaut `` 0 `` à la queue du poisson et 1 à sa tête.

float body = (VERTEX.z + 1.0) / 2.0; //for a fish centered at (0, 0) with a length of 2

Le mouvement suivant est une onde cosinus qui descend le long du poisson. Pour la déplacer le long de la colonne vertébrale du poisson, nous compensons l'entrée dans cos par la position le long de la colonne vertébrale, qui est la variable que nous avons définie ci-dessus, body.

//wave is a uniform float
VERTEX.x += cos(time + body) * wave;

Cela ressemble beaucoup au mouvement latéral que nous avons défini plus haut, mais dans celui-ci, en utilisant body pour décaler cos, chaque sommet le long de la colonne vertébrale a une position différente dans la vague, ce qui donne l'impression qu'une vague se déplace le long du poisson.

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

Le dernier mouvement est la torsion, qui donne un effet de roulement le long de la colonne vertébrale. Comme pour le pivot, nous construisons d'abord une matrice de rotation.

//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)));

Nous appliquons la rotation sur les axes xy pour que le poisson semble rouler autour de sa colonne vertébrale. Pour que cela fonctionne, la colonne vertébrale des poissons doit être centrée sur l'axe z.

VERTEX.xy = twist_matrix * VERTEX.xy;

Voici le poisson avec la torsion appliquée :

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

Si nous appliquons tous ces mouvements l'un après l'autre, nous obtenons un mouvement fluide qui ressemble à de la gelée.

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

Les poissons normaux nagent surtout avec la moitié arrière de leur corps. Par conséquent, nous devons limiter les mouvements de torsion et ondulation à la moitié arrière du poisson. Pour ce faire, nous créons une nouvelle variable, mask.

mask est un float qui va de 0 à l'avant du poisson jusqu'à 1 à la queue en utilisant smoothstep pour contrôler le point où la transition de 0 à 1 se produit.

//mask_black and mask_white are uniforms
float mask = smoothstep(mask_black, mask_white, 1.0 - body);

Ci-dessous, une image du poisson avec mask utilisé comme COLOR :

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

Pour l'ondulation, nous multiplions le mouvement par mask ce qui la limitera à la moitié arrière.

//wave motion with mask
VERTEX.x += cos(time + body) * mask * wave;

Afin d'appliquer le masque à la torsion, nous utilisons mix. mix nous permet de mélanger la position d'un sommet entre un sommet entièrement pivoté et un sommet qui ne l'est pas. Nous devons utiliser mix au lieu de multiplier mask par le VERTEX tourné parce que nous n'ajoutons pas le mouvement au VERTEX, nous remplaçons le VERTEX par la version tournée. Si nous multipliions cela par mask, nous rétrécirions le poisson.

//twist motion with mask
VERTEX.xy = mix(VERTEX.xy, twist_matrix * VERTEX.xy, mask);

L'assemblage des quatre mouvements nous donne l'animation finale.

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

Allez-y et jouez avec les uniformes afin de modifier le cycle de nage des poissons. Vous constaterez que vous pouvez créer une grande variété de styles de nage en utilisant ces quatre mouvements.

Faire un banc de poissons

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.

Les MultiMeshes dessinent votre Mesh avec trois propriétés supplémentaires par instance : Transform (rotation, translation, scale), Color et Custom. Custom (personnalisé) est utilisé pour fournir 4 variables à usage multiple en utilisant une Color.

instance_count définit combien d'instances du mesh vous voulez dessiner. Pour le moment, laissez instance_count à 0 car vous ne pouvez changer aucuns des autres paramètres tant que instance_count est plus grand que 0. Nous définirons instance_count en GDSCript ultérieurement.

transform_format indique si les transformations utilisées sont en 3D ou en 2D. Dans le cadre de ce tutoriel, sélectionnez 3D.

Pour les color_format et les custom_data_format, vous pouvez choisir entre None, Byte et Float. None signifie que vous ne passerez pas de données en paramètres (que ce soit une variable par instante COLOR, ou une INSTANCE_CUSTOM) au shader. Byte signifie que chaque nombre qui constitue la couleur que vous passerez en paramètre sera stockée avec 8 bits tandis que Float signifie que chaque nombre sera stocké dans un nombre flottant (32 bits). Float est plus lent mais plus précis, Byte va prendre moins de mémoire et sera plus rapide mais peut entraîner des artefacts visuels.

Maintenant, réglez instance_count sur le nombre de poissons que vous voulez avoir.

Ensuite, nous devons définir les transformations par instance.

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 seconde est de parcourir toutes les instances et de définir leurs transformations dans le code. En dessous, nous utilisons GDScript pour parcourir toutes les instances et définir leur transformations à une position aléatoire.

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.

Note

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

Remarquez comme tous les poissons sont tous dans la même position dans leur cycle de nage ? Cela les rend très robotique. La prochaine étape est de donner à chaque poissons une position différente dans le cycle de nage de façon à ce que le banc de poisson soit plus organique.

Animer un banc de poisson

Un des bénéfices d'animer le poisson en utilisant les fonctions cos est qu'ils sont animés avec un seul paramètre, time. Afin de donner à chaque poisson une pootition unique dans le cycle de nage, nous allons seulement décaler time.

Nous faisons cela en ajoutant la valeur personnalisé par instance INSTANCE_CUSTOM à time.

float time = (TIME * time_scale) + (6.28318 * INSTANCE_CUSTOM.x);

Ensuite, nous devons passer une valeur INSTANCE_CUSTOM en paramètre. Pour cela, on ajoute une ligne à la boucle for au-dessus. Dans cette boucle for, on assigne à chaque instance un ensemble de 4 flottants aléatoire à utiliser.

$School.multimesh.set_instance_custom_data(i, Color(randf(), randf(), randf(), randf()))

Maintenant, chaque poisson dispose de positions de cycle de nage uniques. Vous pouvez leur donner un peu plus de personnalité en utilisant INSTANCE_CUSTOM pour les faire nager plus ou moins vite en le multipliant par TIME.

//set speed from 50% - 150% of regular speed
float time = (TIME * (0.5 + INSTANCE_CUSTOM.y) * time_scale) + (6.28318 * INSTANCE_CUSTOM.x);

Vous pouvez également expérimenter en changeant la couleur par instante de la même façon que vous avez changé la valeur personnalisé par instance.

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.