Scripting

Tutorial de VRML97

Tot i que el VRML97 dóna molts mecanismes per a definir interacció i comportaments, hi ha coses que no es poden fer diréctament i llavors cal utilitzar la potència d'un llenguatge de programació extern. Això s'aconsegueix a través del node Script i els llenguatges que es poden utilitzar són el Java i el JavaScript.

En aquest tutorial, tan sols entrarem a veure la utilització del JavaScript dins del node Script degut a que és molt més senzill, directe i comú d'utilitzar.

Nota: Partirem del supòsit que el lector ja té un coneixement prèvi del JavaScript com a llenguatge i que el sap utilitzar a nivell bàsic per a realitzar petites utilitats en pantalles de HTML.


El node Script

El concepte bàsic darrere el node Script és que és un node que permet els següents passos:

Un node Script esta format per dues parts principals: les definicions de camps i esdeveniments, i el codi en el llenguatge que hem triat.

És important el fet que en aquest node hi podem definir camps i esdeveniments segons les nostres necessitats, en contrast amb els altres nodes de VRML que ja tenen predefinits tots els seus components.

El lloc on es posa el codi del llenguatge és en el field url. Aquest field permet escriure tot el codi entre cometes dobles (") o bé referenciar un arxiu extern on hi figuri tot el codi.

A continuació veiem un esquema de l'estructura del node:

Eesquema: Estructura del node Script.

	Script {
		field ...
		. . .
		. . .
		field ...
		eventIn ...
		. . .
		. . .
		eventIn ...
		eventOut ...
		. . .
		. . .
		eventOut ...

		url "javascript:
			function X (v,t){
				. . .
				. . .
			}
			. . .
			. . .
			function Z (v,t){
				. . .
				. . .
			}
		"
	}

Aquí es poden observar les dues parts que definiem a dalt. Primer es troben les definicions de camps i esdeveniments. Aquests no han d'estar ordenats de cap manera concreta. En segon lloc trobem el field url on, entre cometes, es defineix el tipus de codi que s'utilitza (en el nostre cas javascript:) i a continuació totes les funcions que configuren els nostres processos.


Els eventIns i les functions de JavaScript

Hi ha una relació dirécta entre els noms dels eventIn i els noms de les funcions del codi. Aquesta relació s'estableix per tal que quan arribi un esdeveniment d'entrada al node Script en qüestió, ell cridi la funció que té el mateix nom que l'eventIn referenciat i així es pugui capturar el valor que ha rebut i processar-lo.

El mecanisme implementat pel node Script fa que tota funció de JavaScript associada a un eventIn tingui dos paràmetres per defecte: el valor rebut per l'eventIn i l'instant de temps en que s'ha generat aquell esdeveniment. D'aquesta manera, la funció pot llavors utilitzar el valor de l'esdeveniment que ha rebut i inclus discernir-lo d'altres gràcies al fet que també es disposa del temps en que aquell valor s'ha generat.

A continuació donem un exemple on un ProximitySensor envía un esdeveniment de canvi de posició del punt de vista a un Script que mira si la coordenada X d'aquest punt de vista és més gran que 5 i no fa res més (de moment).

Exemple1: Definim un ProximitySensor enomenat SensorPuntVista que va detectant el moviment del punt de vista de l'usuari per dins seu. Aquest ProximitySensor va generant eventOuts de nom position_changed, els quals estan encaminats mitjançant un ROUTE al eventIn de nom novaPosicio del Script enomenat SegueixPuntVista.

DEF SensorPuntVista ProximitySensor {
	size 100 100 100
}

DEF SegueixPuntVista Script {
	eventIn SFVec3f novaPosicio

	url "javascript:
		function novaPosicio (v,t) {
			if (v[0] > 5);
		}
	"
}

ROUTE SensorPuntVista.position_changed TO SegueixPuntVista.novaPosicio

Analitzem aquest codi part per part:

(1) Aixi com en VRML no s'accedeix mai a les components de les dades que pertanyen a tipus no escalars com SFVec3f, SFRotation, SFColor, SFVec2f i tots els MFs, quan hem de programar pot interessar-nos accedir a aquestes components. En aquest cas, un valor d'un d'aquests tipus es comporta com si fos un array de JavaScript i per tant s'accedeix a les components indexant des de 0 (zero) fins al nombre de components menys u. Per exemple: a un field SFColor amb nom colorMeu, se li podrien assignar els valors RGB (1, 0.5, 0.3) des d'un Script indexant de la següent manera: colorMeu[0]=1; colorMeu[1]=0.5; colorMeu[2]=0.3;


Els eventOut

Els eventOut es defineixen en la primera part del node Script de forma similar als eventIn. Per tal de poder generar l'esdeveniment de sortida a través de l'eventOut que hem definit, només cal asignar-li un valor des de dintre de la funció que ha de generar l'esdeveniment.

Ampliem el nostre exemple anterior. Ara volem que soni una alarma quan detectem que el punt de vista ha sobrepassat el llindar de 5 unitats en l'eix X. Els elements necessaris són els següents:

Exemple2: Definició d'uns esdeveniments de sortida per activar un so d'alarma (afegim a l'exemple anterior).

DEF SensorPuntVista ProximitySensor {
	size 100 100 100
}

DEF SegueixPuntVista Script {
	eventIn SFVec3f novaPosicio
	eventOut SFTime engegaAlarma
	eventOut SFTime apagaAlarma

	url "javascript:
		function novaPosicio (v,t) {
			if (v[0] > 5) engegaAlarma = t;
			else apagaAlarma = t;
		}
	"
}

DEF ClipAlarma AudioClip {
	url "alarma.wav"
	startTime -1
	loop TRUE
}

Sound {
	source USE ClipAlarma
	maxFront 200
	maxBack 200
}

ROUTE SensorPuntVista.position_changed TO SegueixPuntVista.novaPosicio
ROUTE SegueixPuntVista.engegaAlarma TO ClipAlarma.startTime
ROUTE SegueixPuntVista.apagaAlarma TO ClipAlarma.stopTime

Els elements nous són els següents:


Els fields

Els field també es defineixen en la primera part del node Script de forma similar als eventIn i als eventOut. Els field serveixen com a variables globals pel Script i per a guardar valors al llarg de tota l'execució de l'entorn. Amb això podem comparar valors d'esdeveniments nous amb valors antics que haguem guardat en fields.

De nou ampliem el nostre exemple. El que farem ara és que soni l'alarma només el primer cop que l'usuari passa el llindar. Si llavors l'usuari torna enrera i després torna a passar el llindar, l'alarma ja no ha de sonar. Els elements necessaris són els següents:

Exemple3: Definició d'un field booleà per saber si l'usuari ja havia trespassat el llindar amb anterioritat.

DEF SensorPuntVista ProximitySensor {
	size 100 100 100
}

DEF SegueixPuntVista Script {
	eventIn SFVec3f novaPosicio
	field SFBool haEntrat FALSE
	eventOut SFTime engegaAlarma
	eventOut SFTime apagaAlarma

	url "javascript:
		function novaPosicio (v,t) {
			if (v[0] > 5)
				if(!haEntrat){
					engegaAlarma = t;
					haEntrat = TRUE;
				}
			else	apagaAlarma = t;
		}
	"
}

DEF ClipAlarma AudioClip {
	url "alarma.wav"
	startTime -1
	loop TRUE
}

Sound {
	source USE ClipAlarma
	maxFront 200
	maxBack 200
}

ROUTE SensorPuntVista.position_changed TO SegueixPuntVista.novaPosicio
ROUTE SegueixPuntVista.engegaAlarma TO ClipAlarma.startTime
ROUTE SegueixPuntVista.apagaAlarma TO ClipAlarma.stopTime

Només hem afegit els elements següents:


El field mustEvaluate

Inicialment hem dit que, tret del field url, el node Script no tenia cap altre camp predefinit. Això ho hem dit per tal d'evitar complicar l'explicació, però no és cert. De fet el node Script disposa de dos camps predefinits: mustEvaluate i directOutput.

Durant l'execució d'un entorn de VRML, el browser té l'autorització (per especificació) de gestionar els esdeveniments en el moment que li sigui més idoni. Això pot provocar que s'acumulin una sèrie d'esdeveniments durant un lapse de temps i de cop siguin tots avaluats de cop (evidentment en l'ordre en que s'han generat).

Això vol dir que quan programem un Script per gestionar uns esdeveniments, pot passar que no se'ns avalui l'Script cada cop que es genera l'esdeveniment d'entrada que necessitem. En aquest cas passaria que al cap d'una estona s'avaluaria el nostre Script tantes vegades com esdeveniments d'entrada s'haguessin acumulat.

Per tal de controlar això, el node Script disposa del camp predefinit field SFBool mustEvaluate. Per defecte, aquest camp té valor FALSE, cosa que significa que l'avaluació de l'Script pot ser posposada. Si volem que el nostre Script s'avalúi cada cop que es generi un esdeveniment d'entrada dels que gestionem, llavors cal posar el camp mustEvaluate TRUE.

Nota: Cal tenir en compte que posar mustEvaluate TRUE implica imposar un control més exhaustiu al browser i per tant es perd eficiència. Cal fer-ho només quan sigui estríctament necessari.


El Mecanisme d'Accés a altres Nodes i el field directOutput

A vegades és pràctic poder accedir diréctament als exposedFields, eventOuts i eventIns de nodes externs al Script, sense haver de definir tot un conjunt de ROUTEs. Això dóna més control sobre l'entorn ja que no es depén de la gestió d'esdeveniments que fa el browser. Així doncs, podriem tenir definit un node de transformació amb un cub com a geometria, del qual volem saber el valor del camp de translació desde dintre l'Script sense necessitat de que es generi un esdeveniment. Llavors el que fariem seria:

Exemple4: Definim un accés directe a un node extern a un Script.

DEF TransfCub Transform {
	translation 0 0 0
	children [
		Shape {
			geometry Box { size 1 1 1 }
		}
		DEF TS TouchSensor {}
	]
}

DEF MeuScript Script {
	eventIn SFTime click
	field SFNode TCub USE TransfCub

	url "javascript:
		function click(v,t) {
			if(TCub.translation[0] > 5) ...
		}
	"
}

ROUTE TS.touchTime TO MeuScript.click

Mirem pas a pas el que s'ha fet en aquest exemple:

Amb aquest exemple veiem que podem accedir a informació de nodes externs sense haver d'establir ROUTEs que ens encaminin esdeveniments. Però tal i com està definit, només podem accedir als exposedFields i als eventOuts per llegir-los.

Si volguessim accedir als exposedFields o als eventIns per modificar-los, no podriem a menys que utilitzem el camp que subministren els Scripts, field SFBool directOutput. Aquest camp per defecte té el valor FALSE. Així, per tal de poder tenir accés directe als camps de nodes externs i poder-los modificar, cal posar-lo a TRUE.

Mirem-ho en el següent exemple:

Exemple5: Definim un accés directe a un node extern a un Script amb possibilitat de modificació.

DEF TransfCub Transform {
	translation 0 0 0
	children [
		Shape {
			geometry Box { size 1 1 1 }
		}
		DEF TS TouchSensor {}
	]
}

DEF MeuScript Script {
	directOutput TRUE
	eventIn SFTime click
	field SFNode TCub USE TransfCub

	url "javascript:
		function click(v,t) {
			if(TCub.translation[0] > 5) TCub.translation[0] = 0;
			else TCub.translation[0] = TCub.translation[0] + 1;
		}
	"
}

ROUTE TS.touchTime TO MeuScript.click

El que fa l'exemple anterior és que cada cop que s'entra a l'Script, es mou el cub una posició en l'eix de les X fins arribar a 5, moment en el que es torna al cub a l'origen.

Exercicis proposats:

Dissenyeu un pont llevadís que pugi i baixi en prémer un botó que es trobi en un panell. El pont ha de fer tot un recorregut de pujada i de baixada sense que es pugui interrompre a mig camí i sense que se'l pugui fer pujar quan ja és dalt ni baixar quan ja és a baix.
Comentaris
  • Utilitzeu tan sols primitives per a modelar els objectes.
  • Utilitzeu camps de Script de tipus SFBool (booleans o lògics) per a saber l'estat del pont en cada moment.
  • Utilitzeu OrientationInterpolators per animar el pont.
Solució proposada: pont.wrl.




< Anterior | Menú ^ | Següent >