Politecnico di Milano - Facoltà del design

Appunti di VRML per gli studenti del laboratorio di computer grafica C2
Parte 7: animazione

© Prof. Giovanni Gigante - Revisione 1 (28 maggio 2003)


Animazione

Un'animazione, in senso generale, è il succedersi di una serie di immagini progressivamente diverse tra loro (fotogrammi, in inglese frame), in modo da dare l'illusione del movimento. Nei disegni animati tradizionali, veniva ripreso ogni singolo fotogramma. L'animazione al computer invece fa uso dei cosiddetti fotogrammi chiave (keyframe) che, potremmo dire, sono quelli in cui c'è un cambiamento significativo. I fotogrammi intermedi vengono calcolati automaticamente. Per esempio, per animare uno spostamento rettilineo, basterà specificare la posizione iniziale e quella finale. Per uno spostamento lungo una linea spezzata, occorrerà specificare i punti "di svolta", e così via.

Tempo

In VRML esistono due tipi di tempo ai quali può fare riferimento un'animazione. Il tempo assoluto è un tempo di calendario (per esempio, le ore x del giorno y). Il tempo relativo è un tempo fittizio, che va da 0 (all'inizio dell'animazione) fino a 1 (alla fine dell'animazione). Il tempo relativo naturalmente non specifica quanto dura l'animazione; questo sarà stabilito altrove.Vedremo che entrambi i sistemi tornano utili in determinate circostanze.
Le animazioni possono inoltre essere eseguite una volta sola, oppure ricominciare da capo in un ciclo continuo (loop).

Il sistema degli eventi

Il VRML ha un sofisticato sistema di eventi, che servono sia per creare animazioni, sia per creare meccanismi interattivi. Gli eventi sono dei "segnali" che certi nodi VRML emettono in certe circostanze (quando succede qualcosa di stabilito), e che possono essere "instradati" verso altri nodi per provocare in essi dei cambiamenti.

Gran parte dei campi presenti nei vari nodi VRML ha un "connettore" invisibile attraverso il quale esso può essere modificato, e il cui nome inizia con "set_". Per esempio, il campo radius del nodo Sphere ha un corrispondente connettore set_radius, agendo sul quale si può modificare la dimensione della sfera. Questi connettori "in ingresso" si chiamano eventIn.

Altri nodi hanno anche dei connettori in uscita (eventOut), che emettono dei segnali quando si verificano particolari eventi (un'animazione giunta a un certo punto, il bottone del mouse premuto, ecc.).

Utilizzando la parola chiave ROUTE (collegamento) si possono "cablare" questi connettori gli uni agli altri, e provocare effetti visibili quando succedono determinate cose. Il formato del comando route è:

ROUTE nodoorigine.campoorigine TO nododestinazione.campodestinazione

I nodi di origine e destinazione vanno individuati per nome, per cui sarà necessario dargli un nome con DEF.

Creare un'animazione

Per creare una semplice animazione dobbiamo procedere così:

  1. Ci serve un "orologio" che dia la scansione temporale della nostra animazione. Questo orologio è un nodo TimeSensor, il quale emette periodicamente un segnale che ci serve per "dare l'impulso" all'animazione.
  2. Ci serve poi un componente mediante il quale sia possibile associare determinati valori alla scansione temporale. Per esempio, se vogliamo spostare un oggetto, ci serve un nodo nel quale scrivere le posizioni che l'oggetto deve avere in corrispondenza dei vari momenti dell'animazione. A tale scopo usiamo un nodo come PositionInterpolator (per gli spostamenti), OrientationInterpolator (per le rotazioni), ScalarInterpolator (per cambiare ttrasparenze, ecc.) oppure ColorInterpolator (per i cambiamenti di colore).
  3. Dobbiamo poi collegare con una ROUTE l'uscita di TimeSensor all'ingresso dell'interpolatore, in modo da mettere in modo effettivamente l'animazione
  4. Infine, l'uscita dell'interpolatore andrà collegata con una ROUTE al particolare campo che vogliamo cambiare (la posizione, l'orientamento, ecc.)
  5. A questo punto, l'animazione è pronta, e funzionerà così: il nodo TimeSensor emette un segnale temporale, che l'interpolatore trasforma in una posizione (o una rotazione, ecc.) che poi sarà diretta all'oggetto che vogliamo modificare.

Vediamo in particolare il nodo TimeSensor. Qui citiamo tutti i nodi per completezza, ma generalmente per creare una semplice animazione ne bastano molti di meno:

nodo TimeSensor
enabled L'orologio è in funzione? TRUE (in funzione)
startTime L'ora di inizio (tempo assoluto*) 0
stopTime L'ora di arresto (tempo assoluto*) 0
cycleInterval Quanti secondi dura l'animazione nel suo complesso 1.0
loop L'animazione si ripete? FALSE (viene eseguita una volta sola)
isActive eventOut: emette TRUE quando inizia l'animazione, e FALSE quando termina  
time eventOut: emette continuamente un valore tempo assoluto*  
cycleTime eventOut: emette un valore tempo assoluto solamente all'inizio di ogni ciclo dell'animazione  
fraction_changed eventOut: emette un numero decimale, 0 all'inizio dell'animazione, 1 alla fine  

* Lavorando in VRML non ci interessa conoscere effettivamente questo valore (che è espresso in un modo abbastanza complicato), ci basta sapere quali sono i connettori che trattano tempo assoluto.

Il nodo PositionInterpolator ci permette di realizzare animazioni spostando la posizione di oggetti. Eccolo:

nodo PositionInterpolator
key Una serie di valori compresi tra 0 e 1 (0 è l'inizio dell'animazione, 1 è la fine), in corrispondenza dei quali vogliamo far succedere qualcosa. In altre parole, indichiamo quali sono i fotogrammi chiave. [ ] (nessuno)
keyValue Una lista di terne di valori, che rappresentano le posizioni X Y Z nelle quali spostare l'oggetto. Ciascuna posizione corrisponde a un valore di key [ ] (nessuno)
set_fraction eventIn: riceve dall'esterno un valore compreso tra 0 e 1, che viene poi confrontato con i vari key  
value_changed eventOut: la posizione X Y Z valida in questo momento  

Per capire meglio vediamo l'esempio commentato di un cubo che si muove:

Esempio 17: un cubo che si muove

#VRML V2.0 utf8
Group {
	children [

		# il cubetto da spostare. Siccome va spostato,
		# dobbiamo includerlo in un nodo Transform, e
		# dare un nome a questo nodo.

		DEF spostacubetto Transform {
			children Shape {
				geometry Box { size 1 1 1 }
			}
		},

		# l'orologio dell'animazione
		# l'animazione dura 4 secondi e si ripete

		DEF orologio TimeSensor {
			cycleInterval 4.0
			loop TRUE
		},

		# il percorso dell'animazione

		DEF percorso PositionInterpolator {

			# la nostra animazione ha cinque
			# momenti significativi:

			key [
				0, 0.25, 0.5, 0.75, 1
			]

			# a ciascuno di questi momenti associamo
			# una traslazione da inviare al cubetto.
			# il valore iniziale 0 0 0 vuol dire che
			# l'animazione inizia dalla posizione di base
			# del cubo. Il valore finale 0 0 0 vuol dire
			# che alla fine l'animazione torna in quello
			# stesso punto (altrimenti si avrebbe un salto)

			keyValue [
				0 0 0,
				0 2 0,
				1 2 0,
				2 1 2,
				0 0 0
			]
		}
	]
}

# infine, colleghiamo l'uscita dell'orologio all'ingresso
# dell'interpolatore, e l'uscita dell'interpolatore al nodo
# spostacubetto (il nodo Transform che circonda il cubo).
# set_translation è il connettore in ingresso del campo
# translation del nodo Transform.

ROUTE orologio.fraction_changed TO percorso.set_fraction
ROUTE percorso.value_changed TO spostacubetto.set_translation

Animare cambiamenti di scala

PositionInterpolator emette terne di valori X Y Z che in questo esempio abbiamo inviato al campo translation del nodo Transform, ottenendo di animare lo spostamento del cubo. Ma possiamo anche utilizzarlo per animare un cambiamento di dimensioni. Basterà inviarlo, anziché al campo translation, al campo scale del nodo Transform:

Esempio 18: una sfera pulsante

#VRML V2.0 utf8
Group {
	children [
		DEF trasformapalla Transform {
			children Shape {
				appearance Appearance {
					material Material {
						diffuseColor 0.6 0 0
					}
				}
				geometry Sphere { }
			}
		},

		DEF orologio TimeSensor {
			cycleInterval 2.0
			loop TRUE
		},

		DEF pulsazione PositionInterpolator {
			key [ 0, 0.5, 1 ]
			keyValue [
				1 1 1,
				1.2 1.2 1.2,
				1 1 1
			]
		}
	]
}
ROUTE orologio.fraction_changed TO pulsazione.set_fraction
ROUTE pulsazione.value_changed TO trasformapalla.set_scale

Animare rotazioni

Per animare una rotazione non si può usare PositionInterpolator, perché esso emette solo terne di valori, mentre come sappiamo per specificare le rotazioni ne occorrono quattro (tre per l'asse di rotazione più uno per l'angolo). Pertanto per questo tipo di animazioni occorre un nodo diverso, OrientationInterpolator, che come si vede è uguale a PositionInterpolator, salvo che tratta quattro valori alla volta anziché tre::

nodo OrientationInterpolator
key Una serie di valori compresi tra 0 e 1 (0 è l'inizio dell'animazione, 1 è la fine), in corrispondenza dei quali vogliamo far succedere qualcosa. In altre parole, indichiamo quali sono i fotogrammi chiave. [ ] (nessuno)
keyValue Una lista di quaterne di valori, che rappresentano le rotazioni X Y Z A da applicare all'oggetto. Ciascuna orientamento corrisponde a un valore di key [ ] (nessuno)
set_fraction eventIn: riceve dall'esterno un valore compreso tra 0 e 1, che viene poi confrontato con i vari key  
value_changed eventOut: la rotazione X Y Z A valida in questo momento  

Esempio 19: un cubo rotante

#VRML V2.0 utf8
Group {
	children [

		DEF ruotacubo Transform {
			children Shape {
				appearance Appearance {
					material Material {
						diffuseColor 0 0.7 0.7
					}
				}
				geometry Box { }
			}
		},

		DEF orologio TimeSensor {
			cycleInterval 3
			loop TRUE
		},

		DEF percorso OrientationInterpolator {
			key [ 0.0, 0.50, 1.0 ]
			keyValue [

				# una rotazione completa lungo l'asse Z

				0.0 0.0 1.0  0.0,
				0.0 0.0 1.0  3.14,
				0.0 0.0 1.0  6.28
			]
		}
	]
}
ROUTE orologio.fraction_changed TO percorso.set_fraction
ROUTE percorso.value_changed TO ruotacubo.set_rotation

Animare cambiamenti di colore

L'animazione dei colori richiede qualche accorgimento supplementare. Come sappiamo i colori sono definiti con terne RGB (rosso-verde-blu), ma se si fa una semplice interpolazione tra questi valori si ottengono risultati visivamente spiacevoli (per esempio, una transizione dal rosso al blu passa attraverso il grigio, anziché attraverso il viola come ci si aspetterebbe). Pertanto, per animare colori si usa lo speciale nodo ColorInterpolator, che funziona nel solito modo, ma lavora internamente nello spazio di colore HSV (tono-saturazione-intensità), il che vuol dire che produce i colori intermedi che ci aspettiamo intuitivamente.

nodo ColorInterpolator: identico a PositionInterpolator , ma si usa con i colori

Esempio 20: animazione del colore

#VRML V2.0 utf8
Group {
	children [
		Shape {
			appearance Appearance {
				material DEF colorepalla Material {
					diffuseColor 1 0 0
				}
			}
			geometry Sphere { }
		},

		DEF orologio TimeSensor {
			cycleInterval 2.0
			loop TRUE
		},

		DEF animazione ColorInterpolator {
			key [ 0.0, 0.5, 1 ]
			keyValue [
				1 0 0,
				0 0 1,
				1 0 0
			]
		}
	]
}
ROUTE orologio.fraction_changed  TO animazione.set_fraction
ROUTE animazione.value_changed TO colorepalla.set_diffuseColor

Animare altri valori

Infine, il nodo ScalarInterpolator serve ad animare valori singoli:

nodo ScalarInterpolator
key Una serie di valori compresi tra 0 e 1 (0 è l'inizio dell'animazione, 1 è la fine), in corrispondenza dei quali vogliamo far succedere qualcosa. In altre parole, indichiamo quali sono i fotogrammi chiave. [ ] (nessuno)
keyValue Una lista di valori singoli, da emettere. Ciascuna valore corrisponde a un valore di key [ ] (nessuno)
set_fraction eventIn: riceve dall'esterno un valore compreso tra 0 e 1, che viene poi confrontato con i vari key  
value_changed eventOut: il valore in uscita valido in questo momento  

Per esempio, si può usare per regolare la trasparenza di un oggetto, che è un numero singolo:

Esempio 21: animazione della trasparenza

#VRML V2.0 utf8
Group {
	children [
		Shape {
			# il cubo
			appearance Appearance {
				material DEF materialecubo Material {
					# giallo
					diffuseColor 1.0 1.0 0.0
					transparency 0
				}
			}
			geometry Box {
				size 2.0 2.0 2.0
			}
		},

		Shape {
			# il cono
			appearance Appearance {
				material Material {
					diffuseColor 0.0 0.0 1.0
				}
			}
			geometry Cone {
				height 5.0
				bottomRadius 1.0
			}
		},

		DEF orologio TimeSensor {
			cycleInterval 2.0
			loop TRUE
		},

		DEF animazione ScalarInterpolator {
			key [ 0, 0.5, 1 ]
			keyValue [
				1, 0.5, 1
			]
		}

	]
}
ROUTE orologio.fraction_changed  TO animazione.set_fraction
ROUTE animazione.value_changed TO materialecubo.set_transparency

Animazioni multiple

Si possono usare contemporaneamente diversi TimeSensor e diversi interpolatori, e anche collegare con ROUTE un TimeSensor a più interpolatori, o altre configurazioni ancora per creare animazioni complesse, o animare molti oggetti nello stesso modo.