Politecnico di Milano - Facoltà del design

Appunti di VRML per gli studenti del laboratorio di computer grafica C2
Parte 8: interattività

© Prof. Giovanni Gigante - Revisione 2 (12 maggio 2004)


Sensori di tocco

Nel capitolo precedente si è visto come utilizzare il sistema degli eventi VRML per realizzare semplici animazioni. E' però anche possibile fare in modo che il mondo reagisca alle azioni dell'utente. In particolare, usando il dispositivo di puntamento (mouse, nel nostro caso) l'utente può "avvicinarsi" (passare sopra un oggetto) "toccare" (fare clic su un oggetto) o trascinare (fare clic su un oggetto e, tenendo premuto, spostare). Ci sono una serie di nodi VRML che possono essere utilizzati per reagire a questi eventi.

Il più importante è TouchSensor, che reagisce al passaggio e al clic del mouse:

nodo TouchSensor
enabled Il sensore è in funzione? TRUE (in funzione)
isOver eventOut: emette TRUE quando l'utente muove il cursore sopra l'oggetto, FALSE quando se ne distacca  
isActive eventOut: emette TRUE quando l'utente fa clic sopra l'oggetto, FALSE quando l'utente rilascia il pulsante  
touchTime eventOut: emette un valore tempo assoluto* relativo al momento in cui l'utente scatena l'evento  
hitPoint_changed eventOut: le coordinate del punto toccato  

Esempio 22: attivare l'animazione con la prossimità

Creiamo un cubo che ruota quando viene sfiorato. Per realizzare questo effetto creiamo la consueta animazione del cubo, lasciandola però inizialmente inattiva. Creiamo un nodo TouchSensor, e colleghiamo la sua uscita isOver all'ingresso set_enabled del TimeSensor. In questo modo, quando l'utente sfiora il cubo il connettore isOver di TouchSensor emette TRUE, che viene instradato in set_enabled di TimeSensor, attivando, l'animazione. Quando poi l'utente si allontana, si ripete lo schema ma con l'invio di un segnale FALSE, che ferma l'animazione.

#VRML V2.0 utf8
Group {
	children [
		DEF trasformacubetto Transform {
			children Shape {
				appearance Appearance {
					material Material { }
				}
				geometry Box { }
			}
		},


		DEF tocco TouchSensor { },
		# non ci serve specificare nulla di particolare per il sensore


		DEF orologio TimeSensor {
			# l'orologio inizialmente è disattivato
			enabled FALSE
			cycleInterval 4.0
			loop TRUE
		},

		DEF animazione OrientationInterpolator {
			key [ 0, 0.5, 1 ]
			keyValue [
				0 0 1  0.0,
				0 0 1  3.14,
				0 0 0  6.28
			]
		}
	]
}
ROUTE tocco.isOver TO orologio.set_enabled
ROUTE orologio.fraction_changed TO animazione.set_fraction
ROUTE animazione.value_changed TO trasformacubetto.set_rotation

Esempio 23: attivare l'animazione con il tocco

Qui usiamo una tecnica diversa per creare un'animazione che si attiva soltanto quando l'oggetto viene cliccato. In quel momento, il campo touchTime del nodo TouchSensor emette il tempo assoluto dell'evento (il clic del mouse). Ma inviando questo dato al campo startTime del TimeSensor, si attiva effettivamente l'animazione in quello stesso momento.
L'animazione si ferma dopo un ciclo perché non abbiamo richiesto il loop dell'animazione.

#VRML V2.0 utf8
Group {
	children [
		DEF trasformacubetto Transform {
			children Shape {
				appearance Appearance {
					material Material {
						diffuseColor 1 0.8 0.4
					}
				}
				geometry Box { }
			}
		},

		DEF tocco TouchSensor { },
		# anche qui, il sensore non richede altro

		DEF orologio TimeSensor {
			cycleInterval 1.0
		},

		DEF animazione OrientationInterpolator {
			key [ 0.0, 0.5, 1.0 ]
			keyValue [
				1.0 1.0 1.0  0.0,
				1.0 1.0 1.0  3.14,
				1.0 1.0 1.0  6.28			
			]
		}
	]
}
ROUTE tocco.touchTime TO orologio.set_startTime
ROUTE orologio.fraction_changed TO animazione.set_fraction
ROUTE animazione.value_changed TO trasformacubetto.set_rotation

Sensori di spostamento lungo un piano

Esistono poi tre tipi di sensori che servono a rendere mobili gli oggetti, cioè a fare in modo che si possano trascinare con il mouse.

Il primo di essi è PlaneSensor, che rileva le azioni di trascinamento eseguite con il mouse, calcolando le distanze di traslazione e riportandole in uscita:

nodo PlaneSensor
enabled Il sensore è in funzione? TRUE (in funzione)
autoOffset Il sensore deve "ricordare" la posizione in cui è stato lasciato l'ultima volta? TRUE
offset "Scostamento": un valore opzionale che viene sommato alle coordinate prodotte in uscita 0 0 0
maxPosition
minPosition
Coordinate X Y massime e minime oltre alle quali non è consentita la traslazione. Con i valori di default, la traslazione non è vincolata. -1 -1
0 0
isActive eventOut: emette TRUE quando viene attivato il sensore, FALSE quando viene rilasciato  
translation_changed eventOut: le coordinate di traslazione prodotte dallo spostamento del mouse  

Per rendere effettivamente traslabile un oggetto, si deve circondare l'oggetto con un nodo Transform, e instradare l'uscita del nodo PlaneSensor (translation_changed) alla proprietà translation del nodo Transform, in modo da causare effettivamente lo spostamento.
Ricordiamo che è necessario assegnare un nome (con DEF) sia al sensore che al nodo Transform, altrimenti non possiamo specificare i collegamenti in ROUTE.

Esempio 24: un cubo che si può traslare con il mouse

#VRML V2.0 utf8
Group {
children [
DEF trasformacubetto Transform { # il cubetto viene circondato da un nodo Transform che inizialmente # non specifica una traslazione, quindi a prima vista non fa nulla
children Shape {
appearance Appearance {
material Material { diffuseColor 1 0.8 0.4 }
}
geometry Box { }
}
}, # il sensore planare viene applicato al cubetto
DEF sensore PlaneSensor { }
]
} # instradando l'uscita del planesensore al nodo transform, il cubetto subisce # una traslazione in funzione di come viene trascinato con il mouse sullo schermo.
ROUTE sensore.translation_changed TO trasformacubetto.set_translation

PlaneSensor rileva ed emette i trascinamenti lungo il piano XY, e quindi, se viene usato così com'è, rende un oggetto traslabile lungo il piano XY (cioè, rispetto alla posizione iniziale sullo schermo, destra-sinistra e alto-basso). Così nell'esempio precedente.
Se si vuole invece che il piano di riferimento sia un altro, occorre applicare una ulteriore rotazione al sistema di coordinate, a valle di quanto abbiamo appena visto. Nell'esempio seguente, vogliamo che il cubetto sia traslabile lungo il piano XZ (cioè, rispetto alla posizione iniziale sullo schermo, destra-sinistra e avanti-indietro). Per ottenere questo effetto, agiamo come nell'esempio precedente, e poi circondiamo tutto il risultato con un ulteriore nodo Transform, che "ribalta" il risultato di 90 gradi in modo che il cubo si sposti lungo il piano XZ anziché XY:

Esempio 25: un cubo che si può traslare con il mouse lungo il piano XZ

#VRML V2.0 utf8
Group {
children [
Transform {
# questo ulteriore nodo transform ruota tutto quanto di
# 90 gradi (1.57 radianti) attorno all'asse X, in modo che lo
# spostamento XY prodotto dal nodo transform interno diventi
# in realta' uno spostamento XZ.
rotation 1.0 0.0 0.0 -1.57
children DEF trasformacubetto Transform {
# questo e' il nodo transform, inizialmente vuoto,
# che riceve il controllo da parte del sensore
children Shape {
appearance Appearance {
material Material { }
}
geometry Box { }
}
}
},
DEF sensore PlaneSensor {
# questa volta proviamo anche a specificare un limite
# massimo di spostamento: 2 unità in tutte le direzioni
minPosition -2 -2
maxPosition 2 2
}
]
}
ROUTE sensore.translation_changed TO trasformacubetto.set_translation

Sensori di spostamento attorno a un centro

Il nodo SphereSensor funziona in modo concettualmente simile, ma converte il moto di trascinamento del mouse in una rotazione attorno a un centro, come se si facesse ruotare una sfera:

nodo SphereSensor
enabled Il sensore è in funzione? TRUE (in funzione)
autoOffset Il sensore deve "ricordare" la posizione in cui è stato lasciato l'ultima volta? TRUE
offset "Scostamento": un valore opzionale che viene sommato alle coordinate di rotazione prodotte in uscita 0 1 0 0 (nessuna rotazione aggiuntiva)
isActive eventOut: emette TRUE quando viene attivato il sensore, FALSE quando viene rilasciato  
rotation_changed eventOut: le coordinate di rotazione prodotte dallo spostamento del mouse  

Dal momento che il sensore sferico produce una rotazione, anziché una traslazione, esso emetterà un insieme di 4 valori (come tutte le rotazioni), che questa volta andrà diretto alla proprietà rotation (e non translation!) del nodo Transform:

Esempio 26: un cono che si può far ruotare attorno al proprio centro con il mouse

#VRML V2.0 utf8
Group {
children [
DEF trasformacono Transform {
# questo nodo transform, inizialmente inerte,
# ricevera' il valore di rotation dal sensore
children Shape {
appearance Appearance {
material Material {
diffuseColor 1 0 0
}
}
geometry Cone { }
}
},
# il sensore sferico. Non ci serve specificare
# alcun valore particolare al suo interno
DEF sensore SphereSensor { }
]
}
# instradando l'uscita del sensore alla proprieta' rotation
# del nodo transform che circonda il cono, otteniamo di rendere
# il cono "ruotabile" con il mouse
ROUTE sensore.rotation_changed TO trasformacono.set_rotation

Sensori di spostamento attorno a un asse

Infine, il nodo CylinderSensor converte il moto di trascinamento del puntatore (mouse) in una rotazione attorno a un asse, l'asse di un ipotetico cilindro per l'appunto. Usando il sensore nel solito modo diventa possibile creare un oggetto che si può far ruotare attorno a un asse con il mouse:

nodo CylinderSensor
enabled Il sensore è in funzione? TRUE (in funzione)
autoOffset Il sensore deve "ricordare" la posizione in cui è stato lasciato l'ultima volta? TRUE
offset "Scostamento": un valore opzionale, espresso come angolo in radianti, che viene sommato al valore di rotazione prodotto in uscita 0 (nessuna rotazione aggiuntiva)
minAngle
maxAngle
Rotazione minima e massima, espressa in radianti. I valori di default non pongono alcun limite alla rotazione -1
0
isActive eventOut: emette TRUE quando viene attivato il sensore, FALSE quando viene rilasciato  
rotation_changed eventOut: le coordinate di rotazione prodotte dallo spostamento del mouse  

Anche il sensore cilindrico emette una rotazione, come quello sferico, e quindi si instraderà la sua uscita alla proprietà rotation del nodo Transform:

Esempio 27: un cono che si può far ruotare attorno all'asse verticale con il mouse

(N.B. la rotazione non è molto evidente, bisogna osservare le sfumature del cono. Si noti anche che il codice di questo esempio è identico al precedente, salvo che usa un CylinderSensor anziché uno SphereSensor)
#VRML V2.0 utf8
Group {
children [
DEF trasformacono Transform {
# questo nodo transform, inizialmente inerte,
# ricevera' il valore di rotation dal sensore
children Shape {
appearance Appearance {
material Material {
diffuseColor 1 0 0
}
}
geometry Cone { }
}
},
# il sensore cilindrico. Non ci serve specificare
# alcun valore particolare al suo interno
DEF sensore CylinderSensor { }
]
}
# instradando l'uscita del sensore alla proprieta' rotation
# del nodo transform che circonda il cono, otteniamo di rendere
# il cono "ruotabile" con il mouse
ROUTE sensore.rotation_changed TO trasformacono.set_rotation

Il sensore cilindrico converte i movimenti del mouse in una rotazione attorno all'asse Y (verticale). Se si vuole far ruotare un oggetto attorno a un altro asse, occorre circondare tutto quanto con un ulteriore nodo Transform che effettui la rotazione degli assi (e quindi anche dell'asse di rotazione del sensore). Questo però otterrà anche l'effetto indesiderato di far ruotare l'oggetto: dovremo dunque inserire un terzo nodo Transform, internamente, per raddrizzare l'oggetto.
Ecco come modificare in questo modo l'esempio precedente in modo che il cono ruoti attorno all'asse Z anziché all'asse Y:

Esempio 28: un cono che si può far ruotare attorno all'asse Z con il mouse

#VRML V2.0 utf8
Group {
children [
Transform {
# questo nodo transform esterno ci serve a far ruotare il
# sistema di riferimento, in modo che la rotazione indotta
# dal sensore avvenga attorno all'asse Z anziché attorno
# all'asse Y. Per cui dobbiamo ribaltare gli assi di 90 gradi
# attorno all'asse X:
rotation 1 0 0 1.57
children [
DEF trasformacono Transform {
# questo e' il nodo transform, inizialmente inerte,
# che ricevera' il valore di rotation dal sensore
children [
Transform {
# questo nodo transform interno serve a voltare il
# cono in senso INVERSO a quello del nodo transform
# piu' esterno: in questo modo il cono torna nella
# posizione dritta di partenza
rotation 1 0 0 -1.57
children [
Shape {
appearance Appearance {
material Material {
diffuseColor 1 0 0
}
}
geometry Cone { }
}
]
}
]
},
DEF sensore CylinderSensor { }
]
}
]
}
ROUTE sensore.rotation_changed TO trasformacono.set_rotation

Ambito di attivazione dei sensori

Per scegliere dove posizionare i sensori all'interno del codice VRML, basta sapere due regole:

  1. Il sensore va posizionato all'interno di un nodo di gruppo (nodo Group oppure Transform), e si lega a tutti gli oggetti compresi in quel gruppo
  2. Se un oggetto rientra nel dominio di più sensori, ha effetto su di lui soltanto il sensore più specifico, ossia il sensore del gruppo più interno.

L'esempio seguente, un po' complesso, mostra come funzionano queste regole. Si tratta di un braccio snodato, formato da due bracci (cilindri slanciati) e una base (un cubo). I due bracci insieme formano un gruppo al quale viene legato un sensore (qui chiamato "sensoregenerale"), che permette di far ruotare l'intero sistema trascinandolo. Viene poi posizionato un secondo sensore (qui chiamato "sensorealto") sul solo braccio superiore, che permette di muoverlo indipendentemente dal resto.
Si utilizzano sensori sferici, ipotizzando che ci siano delle giunture sferiche tra le parti dell'oggetto.

Esempio 29: un braccio snodato

#VRML V2.0 utf8
# l'intero oggetto e' un gruppo
Group {
	children [

		# la base e' un cubo rosso
		# in questo gruppo esterno non ci sono sensori, e quindi il cubo non e'
		# sensibile
		Shape {
			geometry Box { size 0.5 0.5 0.5 }
			appearance Appearance { material Material { diffuseColor 1 0 0 }}
		},

		# il gruppo del braccio snodato, che comprende entrambi i segmenti
		DEF tuttoilbraccio Transform {
			# lo posizionamo sopra il cubo
			translation 0 1 0
			# spostiamo il centro di rotazione in modo da farlo coincidere
			# con il punto alla sua base
			center 0 -1 0
			children [

				# il braccio inferiore e' un cilindro verde
				Shape {
					geometry Cylinder { radius 0.2 height 2 }
					appearance Appearance { material Material { diffuseColor 0 1 0 }}
				},

				# questo sensore si applica all'intero braccio snodato
				DEF sensoregenerale SphereSensor { },

				# il braccio superiore e' un sottogruppo
				DEF bracciosuperiore Transform {
					# lo posizioniamo sopra l'altro cilindro
					translation 0 2 0
					# spostiamo il centro di rotazione in modo da farlo coincidere
					# con il punto alla sua base
					center 0 -1 0
					children [

						# il braccio superiore e' un cilindro blu
						Shape {
							geometry Cylinder { radius 0.2 height 2 }
							appearance Appearance { material Material { diffuseColor 0 0 1 }}
						},

						# questo sensore si applica SOLO a questo gruppo, cioe'
						# al braccio superiore. Ottengo quindi che manipolando il
						# braccio superiore spostero' solo lui
						DEF sensorealto SphereSensor { },

					]
				}

			]
		}

	]
}
ROUTE sensoregenerale.rotation_changed TO tuttoilbraccio.set_rotation
ROUTE sensorealto.rotation_changed TO bracciosuperiore.set_rotation