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.

Animare migliaia di pesci con MultiMeshInstance3D

Questo tutorial esplora una tecnica utilizzata nel gioco ABZU per renderizzare e animare migliaia di pesci tramite animazioni dei vertici e l'istanziamento di mesh statiche.

In Godot, si può fare con uno Shader personalizzato e un MultiMeshInstance3D. Attraverso la seguente tecnica è possibile renderizzare migliaia di oggetti animati, anche su hardware di fascia bassa.

Cominceremo animando un singolo pesce. Poi vedremo come estendere questa animazione a migliaia di pesci.

Animazione di un pesce

Cominceremo con un singolo pesce. Carica il modello del pesce in un MeshInstance3D e aggiungi un nuovo ShaderMaterial.

Ecco il pesce che useremo per le immagini di esempio; puoi usare qualsiasi modello di pesce che ti piace.

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

Nota

Il modello di pesce in questo tutorial è stato creato da QuaterniusDev ed è condiviso con una licenza Creative Commons. CC0 1.0 Universal (CC0 1.0) Public Domain Dedication https://creativecommons.org/publicdomain/zero/1.0/

In genere, si userebbero ossa e uno Skeleton3D per animare gli oggetti. Tuttavia, le ossa sono animate dalla CPU, il che porterà a calcolare migliaia di operazioni per ogni frame, rendendo impossibile gestire migliaia di oggetti. Utilizzando l'animazione dei vertici in uno shader di vertici, si evita di usare ossa e si può calcolare l'intera animazione in poche righe di codice, interamente sulla GPU.

L'animazione sarà composta da quattro movimenti chiave:

  1. Un movimento laterale

  2. Un movimento rotatorio attorno al centro del pesce

  3. Un movimento ondulatorio

  4. Un movimento di torsione

Tutto il codice per l'animazione si troverà nello shader dei vertici, con le uniformi che controllano l'intensità del movimento. Utilizziamo le uniformi per controllare l'intensità del movimento, così da poter aggiustare l'animazione nell'editor e vedere i risultati in tempo reale, senza dover ricompilare lo shader.

Tutti i movimenti saranno generati tramite onde cosinusoidali applicate a VERTEX nello spazio modello. Vogliamo che i vertici si trovino nello spazio modello in modo che il movimento sia sempre relativo all'orientamento del pesce. Ad esempio, il movimento laterale farà sempre muovere il pesce avanti e indietro nella sua direzione da sinistra a destra, anziché lungo l'asse x nell'orientamento del mondo.

Per controllare la velocità dell'animazione, cominceremo definendo la nostra variabile di tempo usando TIME.

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

Il primo movimento che implementeremo è il movimento laterale. Si può fare compensando VERTEX.x per cos di TIME. Ogni volta che la mesh viene renderizzata, tutti i vertici si sposteranno lateralmente di una quantità pari a cos(time).

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

L'animazione risultante dovrebbe assomigliare a questa:

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

Poi aggiungiamo il perno. Poiché il pesce è centrato in (0, 0), tutto ciò che dobbiamo fare è moltiplicare VERTEX per una matrice di rotazione affinché ruoti attorno al centro del pesce.

Costruiamo una matrice di rotazione così:

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

E poi la applichiamo agli assi x e z moltiplicandola per VERTEX.xz.

VERTEX.xz = rotation_matrix * VERTEX.xz;

Applicando solo il perno dovresti vedere qualcosa di simile a questo:

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

I prossimi due movimenti devono seguire la colonna vertebrale del pesce. Per farlo, abbiamo bisogno di una nuova variabile, body. body è un float che ha valore 0 sulla coda del pesce e 1 sulla testa.

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

Il prossimo movimento è un'onda cosinusoidale che si muove lungo la lunghezza del pesce. Per farla muovere lungo la spina dorsale del pesce, compensiamo l'input di cos in base alla posizione lungo la spina dorsale, che è la variabile che abbiamo definito sopra, body.

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

Sembra molto simile al movimento laterale che abbiamo definito sopra, ma in questo caso, utilizzando body per compensare cos, ogni vertice lungo la spina dorsale ha una posizione diversa nell'onda, facendo sembrare che un'onda si muova lungo il pesce.

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

L'ultimo movimento è la torsione, che consiste in un movimento di rotazione lungo la colonna vertebrale. Analogamente al perno, costruiamo prima una matrice di rotazione.

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

Applichiamo la rotazione sugli assi xy in modo che il pesce sembri rotolare attorno alla sua spina dorsale. Affinché ciò funzioni, la spina dorsale del pesce deve essere centrata sull'asse z.

VERTEX.xy = twist_matrix * VERTEX.xy;

Ecco il pesce con la torsione applicata:

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

Se applichiamo tutti questi movimenti uno dopo l'altro, otteniamo un movimento fluido, simile a quello di una gelatina.

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

I pesci normali nuotano principalmente con la metà posteriore del corpo. Di conseguenza, dobbiamo limitare i movimenti di torsione alla metà posteriore del pesce. Per fare ciò, creiamo una nuova variabile, mask.

mask è un float che va da 0 nella parte anteriore del pesce a 1 nella parte finale utilizzando smoothstep per controllare il punto in cui avviene la transizione da 0 a 1.

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

Di seguito è riportata un'immagine del pesce con mask utilizzata come COLOR:

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

Per l'onda, moltiplichiamo il movimento per mask, il che lo limiterà alla metà posteriore.

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

Per applicare la maschera alla torsione, usiamo mix. mix ci permette di combinare la posizione del vertice tra un vertice completamente ruotato e uno non ruotato. Dobbiamo usare mix invece di moltiplicare mask per il VERTEX ruotato, perché non stiamo aggiungendo il movimento al VERTEX, ma stiamo sostituendo il VERTEX con la versione ruotata. Se moltiplicassimo questo per mask, rimpiccioliremmo il pesce.

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

Mettendo insieme i quattro movimenti otteniamo l'animazione finale.

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

Prova a smanettare con le uniformi per alterare il ciclo di nuoto dei pesci. Scoprirai che potrai creare un'ampia varietà di stili di nuoto usando questi quattro movimenti.

Creare un banco di pesci

Godot rende facile renderizzare migliaia di oggetti uguali tramite un nodo MultiMeshInstance3D.

Un nodo MultiMeshInstance3D è creato e utilizzato nello stesso modo in cui si creerebbe un nodo MeshInstance3D. In questo tutorial, denomineremo il nodo MultiMeshInstance3D School, perché conterrà un banco di pesci.

Una volta ottenuto un MultiMeshInstance3D, aggiungi un MultiMesh e a quel MultiMesh aggiungi il tuo Mesh con lo shader creato in precedenza.

Le MultiMesh disegnano la Mesh con tre proprietà aggiuntive per istanza: Transform (rotazione, traslazione, scala), Color e Custom. Custom serve per passare 4 variabili multiuso attraverso un Color.

instance_count specifica quante istanze della mesh si desidera disegnare. Per ora, lascia instance_count a 0 perché non puoi modificare nessuno degli altri parametri finché instance_count è maggiore di 0. Imposteremo instance_count in GDScript più avanti.

transform_format specifica se le trasformazioni utilizzate sono 3D o 2D. Per questo tutorial, seleziona 3D.

Per color_format e custom_data_format puoi scegliere tra None, Byte e Float. None significa che non passerai quei dati (né una variabile COLOR per istanza, né INSTANCE_CUSTOM) allo shader. Byte significa che ogni numero che compone il colore passato sarà memorizzato con 8 bit, mentre Float significa che ogni numero sarà memorizzato in un numero in virgola mobile (32 bit). Float è più lento ma più preciso, Byte occuperà meno memoria e sarà più veloce, ma potresti notare alcuni artefatti visivi.

Ora imposta instance_count sul numero di pesci che vuoi avere.

Adesso dobbiamo impostare le trasformazioni per istanza.

Esistono due modi per impostare le trasformazioni per istanza per le MultiMesh. Il primo è interamente tramite l'editor ed è descritto nel tutorial di MultiMeshInstance3D.

Il secondo è di iterare su tutte le istanze e impostare le loro trasformazioni in codice. Di seguito, utilizziamo GDScript per effettuare un ciclo su tutte le istanze e impostare la loro trasformazione su una posizione casuale.

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)

Eseguire questo script posizionerà i pesci in posizioni casuali in un riquadro attorno alla posizione del MultiMeshInstance3D.

Nota

Se le prestazioni sono un problema per te, prova a eseguire la scena con meno pesci.

Nota come tutti i pesci siano nella stessa posizione durante il loro ciclo di nuoto? Li fa sembrare molto robotici. Il prossimo passo è dare a ogni pesce una posizione diversa nel ciclo di nuoto, così che l'intero banco sembri più organico.

Animare un banco di pesci

Uno dei vantaggi di animare i pesci con le funzioni cos è che sono animati con un solo parametro, time. Per assegnare a ogni pesce una posizione unica nel ciclo di nuoto, dobbiamo solo compensare time.

Lo facciamo aggiungiamo il valore personalizzato per istanza INSTANCE_CUSTOM a time.

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

Successivamente, dobbiamo passare un valore a INSTANCE_CUSTOM. Lo facciamo aggiungiamo una riga al ciclo for descritto sopra. Nel ciclo for assegniamo a ciascuna istanza un insieme di quattro float casuali da utilizzare.

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

Ora i pesci hanno tutti posizioni uniche nel ciclo di nuoto. Puoi dare loro un po' più di individualità usando INSTANCE_CUSTOM per farli nuotare più velocemente o più lentamente moltiplicando per TIME.

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

Puoi anche sperimentare cambiando il colore per ogni istanza nello stesso modo in cui hai modificato il valore personalizzato per ogni istanza.

Un problema che incontrai a questo punto è che i pesci sono animati, ma non si muovono. Potresti muoverli aggiornando la trasformazione per istanza, per ogni pesce, a ogni frame. Sebbene ciò sia più veloce che muovere migliaia di MeshInstance3D per frame, probabilmente sarà comunque lento.

Nel prossimo tutorial spiegheremo come utilizzare GPUParticles3D per sfruttare la GPU e muovere ogni pesce individualmente, continuando a trarre vantaggio dell'istanzazione.