In questo tutorial vedremo cos'è la funzione Slerp di Quaternion, in Unity, per passare da un orientamento iniziale ad uno finale in un determinato arco di tempo, effettuando delle rotazioni con un'interpolazione dolce.
Trascrizione del video
Salve a tutti!
In questo tutorial vedremo cos'è la funzione Slerp di Quaternion, in Unity, per passare da un orientamento iniziale ad uno finale in un determinato arco di tempo, effettuando delle rotazioni con un'interpolazione dolce.
In questo tutorial darò per conosciuto Time.deltaTime, che ho trattato in un videotutorial pubblicato precedentemente sul mio canale Youtube; se non conoscete Time.deltaTime, vi consiglio vivamente di guardare quel video, prima di questo.
Il tutorial è stato registrato con la versione 2022 di Unity, tuttavia la funzione Slerp è disponibile da diverse versioni e, verosimilmente, lo sarà anche nelle prossime.
Per non rendere il tutorial troppo teorico e astratto, mostrerò l'utilizzo di Slerp con un esempio pratico.
Una rotazione per passare da un orientamento A ad un orientamento B (e, più in generale, un'interpolazione tra un valore A ad uno B in un determinato lasso di tempo) può avvenire in vari modi: i valori possono seguire un andamento lineare, oppure un andamento sferico o altri tipi di andamenti, anche "personalizzati".
A video vi sto mostrando proprio alcuni esempi di interpolazione, in particolare facendo ruotare un cubo di 90 gradi intorno al suo asse verticale: un esempio davvero semplice, è vero, ma che rende bene il concetto.
Il tempo della rotazione viene mappato nel range [0,1], dove all'istante 0 abbiamo il valore iniziale e all'istante 1 il valore finale; a valori intermedi del range corrisponderanno i valori dati proprio dall'interpolazione scelta.
I cubi, negli esempi, ruotano tutti di 90 gradi in 5 secondi, tuttavia al quarto secondo dell'animazione (che corrisponde al momento 0.8 nel range [0, 1]), i vari cubi presenteranno orientamenti diversi, proprio perché le loro interpolazioni sono diverse.

Quando è necessario effettuare delle rotazioni, come nel nostro caso, si preferisce l'interpolazione sferica a quella lineare, perché il risultato è più gradevole; in Unity, l'interpolazione sferica viene calcolata dalla funzione Slerp (la S iniziale sta proprio per Spherical), mentre l'interpolazione lineare viene calcolata dalla funzione Lerp (la L iniziale sta proprio per Linear).
Slerp fa parte, come detto, della classe Quaternion, che consente di specificare orientamenti e rotazioni nello spazio 3D con i quaternioni: un concetto matematico che NON spiegheremo in questo tutorial; mi limiterò a dire che servono ad esprimere gli orientamenti (e spero che i matematici mi perdoneranno per questa semplificazione) e a mostrare come utilizzare la funzione Slerp.
Slerp ci serve per passare da un orientamento A ad un orientamento B.
Prende tre parametri: l'orientamento iniziale, l'orientamento al quale si vuole arrivare e l'istante, considerato nell'intervallo [0, 1], del quale si vuole conoscere l'orientamento (detto in altri termini: l'istante in cui si vuole valutare l'interpolazione).
Slerp restituisce un quaternione, che rappresenta l'orientamento valutato nell'istante che abbiamo specificato.
Scrivendo quindi:
Quaternion.Slerp(A, B, 0.2);
chiederemo quindi a Unity di recuperare l'orientamento che l'oggetto deve avere ad un quinto della rotazione che deve effettuare per passare dall'orientamento A a quello B, infatti 0.2 è un quinto di 1.
Se vogliamo, quindi, che l'animazione duri 1 secondo, potremo utilizzare Time.deltaTime in Update, perché il range di interpolazione da 0 a 1 può essere convertito alla perfezione in un'animazione di un secondo utilizzando Time.deltaTime in Update, come ho mostrato nel videotutorial su Time.deltaTime; di contro, per effettuare l'animazione in un numero arbitrario di secondi, dovremo dividere Time.deltaTime per quella quantità, ma di questo ne parlerò meglio con l'esempio pratico, tra poco.
Il più delle volte, l'orientamento iniziale è quello proprio dell'oggetto, ossia transform.rotation, in quanto rotation è espressa proprio in quaternioni; calcolare, invece, l'orientamento di destinazione dell'oggetto, ad esempio volendo sommare un angolo in gradi all'orientamento iniziale, può rivelarsi invece problematico, ma anche di questo parleremo tra pochissimo, nella parte pratica.
Passiamo finalmente ad un esempio pratico.
A video sto mostrando uno scenario nel quale è presente il modello 3D descritto precedentemente.

In particolare, il modello è composto da 4 elementi: la cornice della porta, le due ante e una maniglia.
Anche se nel mio modello originale il punto pivot dell'anta con la maniglia si trovava in corrispondenza dei cardini (e quindi la rotazione avveniva correttamente), per qualche motivo Unity lo ha spostato un po' in avanti.
Per risolvere questo problema, inserisco un oggetto Empty come figlio della cornice e lo posiziono in corrispondenza di uno dei cardini dell'anta che voglio animare, quindi rendo l'anta figlia della Empty.
Sarà quindi questa nuova Empty a dover avere lo script di rotazione, che creiamo subito, chiamandolo ad esempio doorOpening.
Vogliamo, in particolare, che all'avvio del gioco l'anta della porta si apra da sola, ruotando cioè di 90° intorno al suo asse verticale LOCALE (che in questo caso è quello z).

L'orientamento iniziale è quello dato da transform.rotation; per comodità (e per rendere il codice più flessibile), dichiaro comunque una variabile chiamata startRotation, di tipo Quaternion, inizializzandola in Start con:
startRotation = transform.rotation;
Il quaternione che rappresenta l'orientamento finale dovrà quindi essere pari a quello iniziale più una rotazione di 90° intorno all'asse Z locale, ma come definire questa rotazione in Quaternioni?
Ci viene in aiuto la funzione Quaternion.Euler, che prende appunto tre parametri: gli angoli, in gradi, di rotazione intorno ai tre assi XYZ.
Definiamo quindi il quaternione della rotazione da effettuare come:
Quaternion rotationToBePerformed;
e lo inizializzo in Start con:
rotationToBePerformed = Quaternion.Euler(0f, 0f, 90f);
Questo quaternione rappresenta l'operazione da svolgere, ma non è ancora l'orientamento finale, che dobbiamo calcolare aggiungendo questo valore a quello iniziale...
… ed ecco che arriva la parte ingannevole: per sommare la rotazione da effettuare all'orientamento iniziale, i due quaternioni vanno MOLTIPLICATI tra loro.
Definiamo quindi il quaternione finale come:
Quaternion targetRotation;
e lo inizializziamo in Start con:
targetRotation = startRotation * rotationToBePerformed;
Spostiamoci nella funzione Update (che, lo ricordo, viene invocata ad ogni frame del gioco) e iniziamo a scrivere:
transform.rotation = Quaternion.Slerp(startRotation, targetRotation
e qui ci fermiamo, perché non abbiamo ancora definito una variabile per il tempo di valutazione dell'interpolazione!
Questa variabile deve partire da 0, per cui possiamo definirla e inizializzarla fuori da Update con:
float evaluation = 0f;
dopodiché possiamo anche scriverla come terzo parametro della funzione Quaternion.Slerp; inoltre, siccome il suo valore deve aumentare ad ogni frame, in Update scriviamo anche:
evaluation += Time.deltaTime.

Sappiamo che il tempo di valutazione finale della rotazione sarà 1.0, quindi questa animazione durerà esattamente un secondo, perché stiamo incrementando evaluation di Time.deltaTime ad ogni frame.
Avviando ora il gioco, noteremo che la Empty ruota intorno al suo asse verticale, ma l'anta della porta non si apre: ciò è dovuto al fatto che inizialmente avevo impostato questo oggetto come Static, perché non prevedevo di animarlo e, quindi, ne ho fatto calcolare l'illuminazione globale a Unity, tuttavia ora voglio animarlo, per cui deseleziono la casella Static nel suo Inspector e confermo l'operazione anche per gli oggetti figli (ossia la maniglia).
Avviando nuovamente il gioco, questa volta vedremo la porta aprirsi con un'interpolazione dolce in un secondo.

Come fare, adesso, per definire una durata dell'animazione diversa?
La risposta è piuttosto semplice, in realtà: l'incremento di evaluation, ad ogni frame, deve essere uguale a Time.deltaTime diviso per la durata dell'animazione: con il solo Time.deltaTime, infatti, l'animazione dura un secondo, per cui per farla durare ad esempio 5 secondi dovremo far aumentare evaluation di un quinto di Time.deltaTime, ad ogni fotogramma.
Definiamo quindi la variabile
float openingDuration = 5f;
perché vogliamo che l'animazione duri 5 secondi, dopodiché aggiorniamo l'istruzione riguardante evaluation, in Update, come segue:
evaluation += (Time.deltaTime / openingDuration);

Salviamo lo script, torniamo all'editor di Unity ed avviamo il gioco per osservare il risultato: questa volta, l'animazione verrà effettuata in 5 secondi.
Ricapitolando, in questo tutorial abbiamo visto come esprimere la rotazione da effettuare in quaternioni, come ottenere il quaternione di destinazione (dato dal quaternione di partenza moltiplicato per la rotazione da effettuare) e, soprattutto, come utilizzare la funzione Slerp per effettuare delle rotazioni con interpolazione sferica, con durate variabili e velocità di esecuzione costante, indipendentemente dal frame rate, grazie a Time.deltaTime.
Spero che questo tutorial vi sia stato utile! A presto!