Three.js — Animazioni

Gianluca Lomarco
8 min readMay 26, 2023

--

Animare le nostre scene 3D è sicuramente la parte più interessante e divertente. Nell'articolo precedente abbiamo visto come muovere e ruotare gli oggetti nello spazio:

sfruttando queste conoscenze aggiungiamo un pizzico di magia alle nostre pagine web.

Introduzione

In three.js le animazioni funzionano esattamente come gli stop motion. Dopo il primo render vengono spostati gli oggetti e si fa un nuovo render, poi li spostiamo ancora e facciamo un altro render, e così via. A seconda di quanto spostiamo gli oggetti questi appariranno con una maggiore o minore velocità.

La frequenza con cui aggiorneremo il render dipenderà dal frame rate del monitor ma anche dalle caratteristiche del computer che potrebbe limitare le performance. La maggior parte dei monitor supportano un frame rate di 60fps, quindi si aggiornano 60 volte ogni secondo, circa ogni 16ms (millisecondi). Non è raro però trovare schermi con frame rate più alti.

Quindi noi vogliamo eseguire la funzione renderer.render(…) ad ogni frame, a seconda del frame rate con cui riuscirà a girare il nostro progetto e per farlo useremo una funzione nativa di JavaScript, requestAnimationFrame.

window.requestAnimationFrame( ... )

Questa funzione accetta come parametro un altra funzione che verrà eseguita in maniera asincrona, come avviene per le altre timing function. A differenza però, di setInterval o setTimeout, non dobbiamo specificare dopo quanto tempo deve essere eseguita la funzione, perché sarà eseguita al prossimo frame.

Di conseguenza il tempo viene gestito automaticamente dal browser, permettendoci di adeguarci al miglior frame rate che il computer riesce a gestire.

Aggiungiamo al nostro codice una funzione animate().

function animate() {

mesh.rotation.y = += 0.01
mesh.rotation.x += 0.01

renderer.render(scene, camera)

requestAnimationFrame( animate )

}

requestAnimationFrame( animate )

Questa è una funzione ricorsiva che quando viene invocata incrementa le rotazioni del nostro cubo e poi esegue il render, dopo di chè invoca nuovamente se stessa al prossimo frame.

In questo modo vedremo il cubo ruotare su se stesso attorno agli assi X e Y.

Congratulazioni! Hai appena creato la tua prima animazione in three.js.

Uniformare le animazioni al frame rate

Sarà necessario però fare una piccola modifica. Infatti il codice che abbiamo scritto ha un piccolo problema.

Gli utenti potrebbero vedere il cubo ruotare a velocità diverse, a seconda del loro frame rate. Gli utenti che vedono il cubo a 60fps, lo vedranno ruotare di 0.60 radianti ogni secondo, mentre chi lo vede a 30fps lo vedrà ruotare di 0.30 radianti al secondo, mentre chi dovesse avere avere 144fps vedrebbe il cubo ruotare di 1.44 radianti al secondo.

Come facciamo ad uniformare le animazioni a prescindere dagli fps?

Siccome stiamo parlando di velocità, dobbiamo tenere a mente che la velocità si misura come spazio / tempo, quindi per sapere di quanto si è spostato un oggetto in un certo intervallo di tempo dobbiamo moltiplicare la velocità per il tempo.

Facciamo un esempio

Se abbiamo un oggetto che si muove ad una velocità di cento metri al secondo (100 m/s) vuol dire che:

  • in 1 secondo il nostro oggetto si sposta di 100 m/s * 1s = 100 metri
  • in 0.5 secondi si sposta di 100 m/s * 0.5 s = 50 metri
  • in un sessantesimo di secondo si sposta 100 m/s * 1/60 s = 1.6667 metri

Nel nostro codice invece stiamo sempre incrementando la rotazione della stessa quantità a prescindere da quanto tempo è passando tra un frame e l'altro.

Per correggere il problema ci serve sapere quanto tempo è trascorso tra un frame e l'altro e moltiplicarlo per la velocità ottenendo come risultato quindi lo spazio da incrementare.

Misuriamo il tempo

Utilizzando l'oggetto Date di JavaScript

Dato che dobbiamo misurare il tempo e usarlo per fare dei calcoli per prima cosa dobbiamo decidere che unità di misura utilizzare: secondi, millisecondi, minuti, ore, ecc. E di conseguenza anche le velocità dovranno essere espresse con la stessa unità di tempo.

In questo esempio useremo i secondi. Calcoleremo quanti secondi sono trascorsi tra un frame e l'altro e li moltiplicheremo per le velocità che saranno espresse in secondi.

Dato che stiamo animando la rotazione del cubo la velocità sarà radianti al secondo, ovvero lo spazio sarà in realtà un angolo e dicidiamo che il cudo routerà con una velocità di 1 radiante al secondo, circa 57 gradi ogni secondo.

// current timestamp in millisecondi
let time = Date.now()

// impostiamo la velocità di rotazione uguale a 1 radiante al secondo
let vel = 1

function animate() {

// current timestamp in millisecondi
const currentTime = Date.now()
// calcoliamo quanto tempo è trascorso dal frame precendente
const deltaTime = ( currentTime - time ) / 1000
// aggiorno la variabile time
time = currentTime

mesh.rotation.y = += vel * deltaTime
mesh.rotation.x += vel * deltaTime

renderer.render(scene, camera)

requestAnimationFrame( animate )

}

requestAnimationFrame( animate )

Prima di eseguire la funzione animate() abbiamo salvato in una variabile time il timestamp corrente, che rappresenta il tempo in millisecondi passato dal 1 gennaio 1970.

Ad ogni frame viene eseguita la funzione animate(). In questa funzione riprendiamo il tempo corrente nella variabile currentTime e ci calcoliamo la differenza con la variabile time ottenendo i millisecondi che sono trascorsi. Dividendo per 1000 convertiamo questo valori in secondi che salviamo nella variabile deltaTime.

Moltiplicando deltaTime per la velocità otteniamo il valore con cui incrementare la rotazione del cubo.

Aggiorniamo la variabile time con il valore di currentTime per fare in modo che al prossimo frame deltaTime venga calcolato correttamente.

Utilizzando il Clock di three.js

Three.js ci mette a disposizione un orologio con cui possiamo ottenere lo stesso risultato. Da questo orologio possiamo ottenere diverse informazioni tra cui il tempo totale trascorso, e l'intervallo di tempo dall'ultimo aggiornamento. Quindi possiamo riscrivere il nostro codice in questo modo:

// creo il clock
const clock = new THREE.Clock()

// impostiamo la velocità di rotazione uguale a 1 radiante al secondo
let vel = 1

function animate() {

// tempo trascorso in secondi
const deltaTime = clock.getDelta()

mesh.rotation.y = += vel * deltaTime
mesh.rotation.x += vel * deltaTime

renderer.render(scene, camera)

requestAnimationFrame( animate )

}

requestAnimationFrame( animate )

Il clock di three.js ci permette di avere un codice un po' più pulito, ma bisogna fare attenzione a non invocare getDelta più di una volta per ogni frame, altrimenti il tempo trascorso non verra calcolato correttamente e potremmo avere degli effetti spiacevoli.

Entrambi i codici ci permettono di avere animazioni uniformi indipendentemente dal frame rate.

Funzioni trigonometriche

Oltre a getDelta potrebbe servirci sapere quanto tempo è trascorso dell'inizio del nostro frame loop.

Spesso questo parametro viene utilizzato per animazioni molto particolari che utilizzano funzioni trigonometriche come seno e coseno. Queste sono funzioni che a partire da un parametro che cresce continuamente nel tempo ci restituiscono sempre un valore compreso tra -1 e +1 con un andamento ad onda.

Vediamo come possiamo utilizzarle per muovere il nostro cubo facendolo fluttuare su e giu.

// creo il clock
const clock = new THREE.Clock()

// impostiamo la velocità di rotazione uguale a 1 radiante al secondo
let vel = 1

function animate() {

// tempo trascorso in secondi
// const deltaTime = clock.getDelta()
const time = clock.getElapsedTime()

// mesh.rotation.y = += vel * deltaTime
// mesh.rotation.x += vel * deltaTime

mesh.position.y = Math.sin( time )

renderer.render(scene, camera)

requestAnimationFrame( animate )

}

requestAnimationFrame( animate )

Il metodo getElapsedTime() ci restituisce il tempo in secondi trascorso da quando è stato avviato il clock.

Alla funzione seno, che in javascript troviamo dentro all'oggetto Math, passiamo come parametro il tempo trascorso dall'inizio della nostra animazione. Così tale funzione ci restituisce un valore che partendo da 0 diventerà +1, poi torna a 0 per proseguire verso -1 ed infine ritornare a 0 per poi ripersi all'infinito.

Ampienza

Possiamo aumentare l'ampiezza dell'animazione moltiplicando per 2 per esempio, per far oscillare il cubo tra +2 e -2.

mesh.position.y = Math.sin( time ) * 2

Frequenza

Se invece volessimo velocizzare la frequenza dell'oscillazione possiamo moltiplicare la variabile time che passiamo come parametro alla funzione seno.

// raddoppia la frequenza
mesh.position.y = Math.sin( time * 2 )

// dimezza la frequenza
mesh.position.y = Math.sin( time * 0.5 )

Combinando in vari modi il tempo con le funzioni trigonometriche si possono ottenere risultati davvero sorprendenti e ti consiglio di sperimentare con il tuo codice varie soluzioni.

Animazioni con GSAP e altre librerie esterne

Nessuno ci vieta di utilizzare librerie esterne per animare le nostre scene 3D. GSAP è sicuramente la più famosa ed utilizzata, soprattuto dai creative developer e in three.js. È davvero completa e ci permette di gestire le animazioni in modo molto semplice. Vediamo come utilizzarla.

Utilizzando un bundler come Vite o Webpack è possibile installarla molto facilmente con npm, in alternativa è possibile scaricare la build ed includerla nel tuo progetto. Installiamola!

npm install --save gsap

Importiamola all'inizio del file main.js.

import './style.css'
import * as THREE from 'three'
import gsap from 'gsap'

Aggiungiamo un pulsante nella pagina index.html e facciamo in modo che al click il cubo si sposti alla coordinata x = 2 nell'arco di 1 secondo.

...
<body>
<button id="move">Vai</button>
<script type="module" src="/main.js"></script>
</body>
...

E in fondo al file main.js aggiungiamo queste righe di codice:

const btn = document.getElementById('move')

btn.addEventListener('click', function() {

gsap.to( mesh.position, { duration: 1, x: 2 })

})

Per il funzionamento di GSAP ti consiglio di andare sulla documentazione ufficiale.

In breve, con il metodo gsap.to(…) stiamo impostando il punto finale della nostra animazione. Il primo parametro è l'elemento che vogliamo animare, quindi la proprietà mesh.position del cubo. Il secondo parametro invece contiene le informazioni su come deve essere fatta l'animazione. Nel nostro caso gli abbiamo detto che la durata deve essere di 1 secondo e che alla fine della animazione la proprietà x della mesh.position deve essere uguale a 2, a prescindere dal suo valore iniziale.

GSAP ha un suo requestAnimationFrame interno con cui gestisce le animazioni, quindi aggiornerà la posizione del cubo in base al frame rate supportato, senza che debba farlo tu manualmente, rispettando i parametri dell'animazione. L'unica cosa che dovrai continuare a fare è invocare il metodo renderer.render(…) nella funzione animate().

Potremmo aggiungere un ritardo, modificare durata e velocità oltre a modificare anche le proprietà y e z di position. Sentiti libero di sperimentare con diversi parametri.

Conclusioni

Abbiamo visto le basi su come poter creare le animazioni sui nostri oggetti 3D. Scegli tu la soluzione che più fa al caso tuo. Tieni presente che non sempre serve una libreria esterna come GSAP.

A questo punto esercitati con alcune animazioni semplici, sperimenta con diversi metodi animando proprietà diverse come position, rotation, scale. Prova anche animando più oggetti contemporaneamente sincronizzando i movimenti e creando coreografie accattivanti.

Col tempo e la pratica riuscirai a padroneggiare animazioni sempre più complesse ed articolate.

Next step

Nel prossimo articolo vedremo come adattare il canvas alla viewport e come renderlo responsive aggiornando il render al resize della della viewport.

--

--

Gianluca Lomarco

I am a creative developer working with webGL and fullstack Main teacher at Boolean Academy. Now I’am starting a new experience as content creator in YouTube