Skin Animation 2962 Visite) DirectX 11
La gestione delle animazioni, presente in tutti i videogiochi, è una tecnologia data ormai per scontata. Tuttavia non è così banale come sembra.
Il primo concetto da introdurre è quello di scheletro. Come per il corpo umano, una volta realizzato il modello viene creato uno scheletro visibile nel cilindro presente nell'immagine sotto.
La maggior parte degli editor 3D forniscono sistemi per la creazione di uno scheletro e l'aggancio di questo al modello. Da questo momento se viene mosso lo scheletro anche il modello si deforma.
Come avviene però questa operazione?
Ogni elemento dello scheletro è a tutti gli effetti un elemento nello spazio e quindi capace di muoversi (principalmente di ruotare attorno alla propria giunzione). Partendo dal primo elemento tutte le trasformazioni andranno a sommarsi. Prendete ad esempio il vostro braccio: se ruotate una mano questa ruoterà attorno al polso ma se muoverete anche il braccio la posizione finale della mano dipenderà sia dalla rotazione della spalla, sia dell’avambraccio sia del polso.
Per un oggetto composto da vertici e triangoli occorrerà calcolare di quanto ogni vertice viene spostato in base al movimento dello scheletro, considerando che ogni vertice può essere influenzato da più ossa.
Prendiamo la formula di interpolazione lineare.
C = A * l + B * (1-l)
Variando l da 0 ad 1 il valore C varierà da A a B. Applichiamo lo stesso concetto con più elementi
Immaginiamo quindi un massimo di 4 ossa, ognuno con una matrice (Mat1… Mat4) e 4 valori che ne indicano quanto ognuna delle ossa influenzerà il movimento da 0 ad 1 (W1..W4) chiamati pesi (in inglese weight)
Matrix result= Mat1 * W1 + Mat2*W2 + Mat3*W3 + Mat4* (1-W1-W2-W3)
Ogni vertice del nostro modello verrà influenzato da un peso in valore da 0 ad 1 (W1+W2+W3 non deve superare il valore 1). Il vertice finale sarà
Vfinal = Vinit*result
Immaginando un modello composto da tantissime articolazioni potremmo caricare tantissime matrice (una per articolazione) e salvare in ogni vertice quali sono le matrici da usare e quanto ognuna influenzerà il movimento
Vertex
{
Float 4 position;
Float 4 weight;
Float4 joint;
}
Il numero 4 per i pesi introdotto prima non è un caso in quanto il Float4 ha appunto 4 valori, ideale per Direct3D (ma anche OpenGL ed altre). Una volta associati correttamente i valori (impossibile a mano per modelli complessi, occorre un editor) il modello è pronto per essere renderizzato.
Essendo ogni articolazione dipendente da quelle a cui è connesso, la gestione delle matrici necessità un certo lavoro. Ogni trasformazione di una giunzione dovrà essere aggiunta a quella proveniente dai genitori. Occorrerà quindi procedere in modo iterativo da articolazione in articolazione fino a raggiungere gli ultimi nodo.
Keyframe
Un'animazione è un movimento continuo nel tempo. In base all'istante in cui avanziamo le articolazioni si troveranno in certe posizioni che sono potenzialmente infinite. Per realizzare qualsiasi animazione però non è necessario salvare ogni singolo istante ma semplicemente un certo numero di posizioni ed aggiornare il movimento tra di esse.
Chi crea le animazioni definisce appunto un certo numero di posizioni dell’oggetto chiamate keyframe (fotogrammi chiave). Un modello 3D avrà un certo numero di posizioni ognuna in un certo istante. Questo significa che il nostro sistema dovrà aggiornare i movimenti dello scheletro variando da una posizione all’altra in funzione del tempo tramite funzioni di interpolazione. Questo non si addice molto alle matrice dato che per loro natura l’interpolazione tra due matrici non dà l’interpolazione della trasformazione. Per questo esistono i quaternioni, una struttura che permette di conservare le rotazioni e che gode di questa proprietà.
Un quaternione è un vettore XYZW che può essere convertito in una matrice di rotazione e viceversa. Il metodo Decompose di una matrice permette di scomporre una matrice nelle matrici di scala, tralazione e nel quaternione di rotazione. Al contrario il metodo Matrix.RotationQuaternion permette di riottenere una matrice di rotazione da un quaternione. Il quaternione ha inoltre già dei metodi per fare l’interpolazione lineare (Slerp). Esisteno però vari metodi di interpolazione, ad esempio l’interpolazione di Hermite e Bezier che richiedono trattamenti differenti. Ecco ad esempio come ottenere un quaternione tramite 4 quaternioni interpolati secondo la formula di Bezier
float p0 = (1 - interpolation) * (1 - interpolation) * (1 - interpolation);
float p1 = 3 * interpolation * (1 - interpolation) * (1 - interpolation);
float p2 = 3 * interpolation * interpolation * (1 - interpolation);
float p3 = interpolation * interpolation * interpolation;
Quaternion q = p0 * start + p1 * tangent_out + p2 * tangent_in + p3 * end;
Dove start, end, tangent_in e tangent_out sono 4 quaternioni. Il valore interporlation è il valore da 0 ad 1 per passare da start ad end.
Per realizzare l'animazione di una qualsiasi articolazione avremo quindi un set di trasformazioni (conservate come matrici o come quaternioni) che rappresentano i fotogrammi chiave dall'istante 0 alla fine dell'animazione.
Dovremmo essere noi a generare in base al tempo di animazione la matrice corrente ed applicarla all’articolazione. Se ad esempio abbiamo 3 posizioni negli intervalli 0, 15 e 60 occorrerà convertirle in 2 intervalli da 0 ad 1 e, prendendo le matrici a due a due, calcolare la matrice da utilizzare. Il tutto moltiplicato per la matrice proveniente dall’articolazione padre.
I calcoli possono spiazzare la maggior parte delle persone (molti di questi argomenti non vengono trattati se non in alcuni corsi universitari) ma con un pò di impegno e pazienza si possono riuscire ad estrarre le formule da utilizzare.
Prendendo come esempio un modello come questo in foto dovremo prevedere di poter gestire in modo ricorsivo le animazioni partendo dai punti iniziali fino alle estremità.
Un'altro concetto importante è la matrice InverseBinding. Gli editor 3D creano in questo processo un ulteriore matrice per ogni articolazione.
Ogni articolazione ha una posizione considerata "a riposo" che tuttavia non è quasi mai la matrice identità. Un modello 3D sia esso in posizione neutrale, sia esso sganciato dallo scheletro, dovrebbe lasciare inalterata la posizione dei vertici. La matrice InverseBinding serve ad annullare la trasformazione dovuta al fatto che ogni articolazione ha già una posizione iniziale. Il braccio ad esempio è già disteso ed ha già una rotazione. Questa posizione deve essere la posizione neutra per il modello.
Gli step saranno quindi i seguenti:
-
Calcolare la matrice per ogni articolazione
-
Partendo dalle articolazioni padre moltiplicare l’inversebinding per la matrice corrente per la matrice proveniente dal nodo padre
-
Preparare un array di matrici e passarlo allo shader
Preparate tutte le matrici queste possono essere passate a Direct3D per la trasformazione.
Se tutti i calcoli sono corretti l’array di matrici chiamato Paletta dovrebbe trasformare ogni vertice
float4x4 mat=palette[input.joint.x] * input.weight.x +
palette[input.joint.y] * input.weight.y +
palette[input.joint.z] * input.weight.z +
palette[input.joint.w] * input.weight.w;
float4 pos = mul(mat , input.position);
Il vettore pos è ora la posizione corretta del vertice. Da qui potremo utilizzarlo a piacimento applicando le matrici world, view, projection e tutto ciò che ci serve.
Cosa uso per caricare le mie animazioni?
In questo Direct3D non ci viene in aiuto. Bandito il vecchio formato .X dovremo essere noi a gestire i caricamenti e l’organizzazione delle mesh. La cosa è tutt’altro che banale essendoci decine di formati, spesso anche chiusi, ed essendo anche gli editor differenti nelle esportazioni.
Nella demo ho utilizzato un formato chiamato Collada.
Collada è un formato xml open source, divenuto standard ISO, che permette di gestire una grande quantità di informazioni. Nell’editor è presente una classe draft per il caricamento ed un sistema di gestione base delle animazioni. Il caricamento non è ancora rifinito quindi non aspettatevi di riuscire a caricare ogni modello, anche perché le regole di caricamento di Collada sono numerose ed ogni editor esporta i modelli utilizzando una regola piuttosto che un altra. Dovrebbe però essere una base per poter comprendere le animazioni in skinning e realizzare i propri sistemi.
Vi rimando al tutorial numero 18 della serie (Github o in fondo dal primo tutorial)