Attention: Here be dragons
This is the latest
(unstable) version of this documentation, which may document features
not available in or compatible with released stable versions of Godot.
Checking the stable version of the documentation...
Animer des milliers de poissons avec 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
Nous allons commencer avec un seul poisson. Chargez votre modèle de poisson dans une MeshInstance3D 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.

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 :
Un mouvement latéral
Un mouvement de pivot autour du centre du poisson
Un mouvement d'ondulation
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 :

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 :

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.

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 :

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

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
:

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.

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 MultiMeshInstance3D.
Un nœud MultiMeshInstance3D est créé et utilisé de la même manière que vous le feriez pour un nœud MeshInstance3D . Pour ce tutoriel, nous appellerons le nœud MultiMeshInstance3D Banc
, car il contiendra un banc de poissons.
Une fois que vous avez un MultiMeshInstance3D 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 MultiMeshInstance3D.
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)
Exécuter ce script va placer le poisson dans une position aléatoire de la boite autour de la position du 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.
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 MeshInstance3D par image, il est probable que ça soit tout de même lent.
Dans le prochain tutoriel, nous allons voir comment utiliser les GPUParticles3D pour tirer parti du GPU afin de déplacer chaque poisson individuellement tout en gardant les bénéfices de l'instanciation.