Animer des milliers de poissons avec MultiMeshInstance

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.

Dans Godot, ceci peut être accompli avec un Shader personnalisé et un MultiMeshInstance. En utilisant cette technique, vous pourrez rendre des milliers d'objets animés sur du matériel bas ou haut de gamme.

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

Animation d'un poisson

Nous allons commencer avec un seul poisson. Chargez votre modèle de poisson dans une MeshInstance et ajoutez un nouveau 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

Le modèle de poisson de ce tutoriel a été créé par QuaterniusDev et est partagé sous licence creative commons. CC0 1.0 Universal (CC0 1.0) Public Domain Dedication https://creativecommons.org/publicdomain/zero/1.0/

En général, on utilise des os (bones) et un squelette (Skeleton) pour animer des objets. Cependant, les os sont animés par le CPU et vous vous retrouvez avec des milliers d'opérations à calculer à chaque image et il devient impossible d'avoir des milliers d'objets. En utilisant l'animation de points dans un shader de sommets (vertex shader), vous évitez l'utilisation des os et pouvez donc calculer l'animation complète en quelques lignes de code et entièrement sur le 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 facilite le rendu de milliers d'objets identiques à l'aide d'un nœud MultiMeshInstance.

Un nœud MultiMeshInstance est créé et utilisé de la même manière que vous le feriez pour un nœud MeshInstance. Pour ce tutoriel, nous appellerons le nœud MultiMeshInstance School, car il contiendra un banc de poissons.

Une fois que vous avez un MultiMeshInstance ajoutez-y un MultiMesh, et ajoutez à ce MultiMesh votre Mesh avec le shader précédemment créé.

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.

Il existe deux manières de définir ces transformations par instance pour les MultiMeshes. La première est uniquement dans l'éditeur et est décrite dans le tutoriel MultiMeshInstance.

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 = Transform()
  position = position.translated(Vector3(randf() * 100 - 50, randf() * 50 - 25, randf() * 50 - 25))
  $School.multimesh.set_instance_transform(i, position)

Exécuter ce script va placer le poisson dans une position aléatoire de la boite autour de la position du MultiMeshInstance.

Note

Si les performances sont importantes pour vous, essayez de lancer la scène avec GLES2 ou avec moins de poissons.

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.

Un problème que vous allez rencontrer à un moment donné est que les poissons ont une animation, mais ils ne bougent pas. Vous pouvez les déplacer en mettant à jour la transformation par instance pour chaque poisson. Cependant, même si faire cela est plus rapide que déplacer des milliers de MeshInstances par image, il est probable que ça soit tout de même lent.

Dans le prochain tutoriel, nous allons voir comment utiliser les Particles pour tirer parti du GPU afin de déplacer chaque poisson individuellement tout en gardant les bénéfices de l'instanciation.