Scripting

Tutorial de VRML97

Aunque el VRML da muchos mecanismos para definir interacción y comportamientos, hay cosas que no se pueden hacer diréctamente y entonces se debe utilizar la poténcia de un lenguaje de programación externo. Esto se consigue a través del node Script y los lenguajes que se pueden utilizar son el Java y el JavaScript (o compatible JavaScript).

En este tutorial, tan solo entraremos a ver la utilización del JavaScript dentro del node Script debido a que es mucho más sencillo, directo y común de utilizar.

Nota: Partiremos del supósito que el lector ya cuenta con un conocimiento previo del JavaScript como lenguaje y que lo sabe utilizar a nivel básico para realizar pequeñas utilidades en pantallas de HTML.


El node Script

El concepto básico detrás del node Script es que es un node que permite los siguientes pasos:

Un node Script está formado por dos partes principales: las definiciones de campos y eventos, y el código en el lenguaje que hemos escogido.

Es importante el hecho que en este node podemos definir campos y eventos según nuestras necesidades, en contraste con los otros nodes de VRML que ya tienen predefinidos todos sus componentes.

El sitio donde se pone el código del lenguaje es en el field url. Este field permite escribir todo el código entre comillas dobles (") o bién referenciar un archivo externo donde figure todo el código.

A continuación vemos un esquema de la estructura del node:

Esquema: Estructura del node Script.

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

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

Aquí se pueden observar las dos partes que definíamos arriba. Primero se encuentran las definiciones de campos y eventos. Estos no requieren estar ordenados de ningúna forma concreta. En segundo lugar, encontramos el field url donde, entre comillas, se define el tipo de código que se utiliza (en nuestro caso javascript:) y a continuación todas las funciones que configuran nuestros procesos.


Los eventIns y las functions de JavaScript

Existe una relación directa entre los nombres de los eventIn y los nombres de las funciones del código. Esta relación se establece para que cuando llegue un evento de entrada al node Script en custión, él llame a la función que tiene el mismo nombre que el eventIn referenciadi y así se pueda capturar el valor que se ha recibido y se pueda procesar.

El mecanismo implementado por el node Script hace que toda función de JavaScript asociada a un eventIn tenga dos parámetros por defecto: el valor recibido por el eventIn y el instante de tiempo en que se ha generado aquel evento. De esta forma, la función puede utilizar el valor del evento que ha recibido e incluso discernirlo de otros eventos gracias al hecho de que también se dispone de su sello de tiempo.

A continuación damos un ejemplo donde un ProximitySensor envía un evento de cambio de posición del punto de vista a un Script que mira si la coordenada X de este punto de vista es mayor que 5 (y no hace nada más de momento).

Ejemplo1: Definimos un ProximitySensor llamado SensorPuntoVista que va detectando el movimiento del punto de vista del usuario por dentro suyo. Este ProximitySensor va generando eventOuts de nombre position_changed, los cuales son encaminados mediante un ROUTE al eventIn de nombre nuevaPosicion del Script llamado SiguePuntoVista.

DEF SensorPuntoVista ProximitySensor {
	size 100 100 100
}

DEF SiguePuntoVista Script {
	eventIn SFVec3f nuevaPosicion

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

ROUTE SensorPuntoVista.position_changed TO SiguePuntoVista.nuevaPosicion

Analicemos este código parte por parte:

(1) Al contrario de en VRML donde no se accede nunca a las componentes de los datos que pertenecen a tipos no escalares como SFVec3f, SFRotation, SFColor, SFVec2f y todos los MFs, cuando hemos de programar puede interesarnos acceder a estas componentes. En un caso como este, un valor de estos tipos se comporta como si fuera un array de JavaScript y por lo tanto se accede a las componentes indexando desde 0 (cero) hasta el número de componentes menos uno. Por ejemplo: a un field SFColor con nombre miColor, se le podrían asignar los valores RGB (1, 0.5, 0.3) desde un Script indexando de la siguiente forma: miColor[0]=1; miColor[1]=0.5; miColor[2]=0.3;


Los eventOut

Los eventOut se definen en la primera parte del node Script de forma similar a los eventIn. Para poder generar el evento de salida a través del eventOut que hemos definido, tan solo tenemos que asignarle un valor desde dentro de la función que ha de generar el evento.

Ampliemos nuestro ejemplo anterior. Ahora queremos que suene una alarma cuando detectemos que el punto de vista ha sobrepasado el límite de 5 unidades en el eje X. Los elementos necesarios son los siguientes:

Ejemplo2: Definición de unos eventos de salida para activar un sonido de alarma (añadimos al ejemplo anterior).

DEF SensorPuntoVista ProximitySensor {
	size 100 100 100
}

DEF SiguePuntoVista Script {
	eventIn SFVec3f nuevaPosicion
	eventOut SFTime activaAlarma
	eventOut SFTime apagaAlarma

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

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

Sound {
	source USE ClipAlarma
	maxFront 200
	maxBack 200
}

ROUTE SensorPuntoVista.position_changed TO SiguePuntoVista.nuevaPosicion
ROUTE SiguePuntoVista.activaAlarma TO ClipAlarma.startTime
ROUTE SiguePuntoVista.apagaAlarma TO ClipAlarma.stopTime

Los elementos nuevos son los siguientes:


Los fields

Los field también se definen en la primera parte del node Script de forma similar a los eventIn y a los eventOut. Los field sirven como variables globales para el Script y para guardar valores a lo largo de toda la ejecución del entorno. Con esto podemos comparar valores de eventos nuevos con valores antiguos que hayamos guardado previamente en fields.

De nuevo ampliemos nuestro ejemplo. Lo que haremos ahora es que suene la alarma solo la primer vez que el usuario pase el límite. Si el usuario vuelve atrás y después de nuevo sobrepasa el límite, la alarma ya no deberá sonar. Los elementos necesarios son los siguientes:

Ejemplo3: Definición de un field booleano para saber si el usuario ya había traspasado el límite préviamente.

DEF SensorPuntoVista ProximitySensor {
	size 100 100 100
}

DEF SiguePuntoVista Script {
	eventIn SFVec3f nuevaPosicion
	field SFBool haEntrado FALSE
	eventOut SFTime activaAlarma
	eventOut SFTime apagaAlarma

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

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

Sound {
	source USE ClipAlarma
	maxFront 200
	maxBack 200
}

ROUTE SensorPuntoVista.position_changed TO SiguePuntoVista.nuevaPosicion
ROUTE SiguePuntoVista.activaAlarma TO ClipAlarma.startTime
ROUTE SiguePuntoVista.apagaAlarma TO ClipAlarma.stopTime

Solo hemos añadido los elementos siguientes:


El field mustEvaluate

Inicialmente hemos dicho que, excepto por el field url, el node Script no tenía ningún otro campo predefinido. Esto lo hemos dicho para simplificar las explicaciones anteriores, pero no es exáctamente cierto. De hecho el node Script dispone de dos campos predefinidos: mustEvaluate y directOutput.

Durante la ejecución de un entorno de VRML, el browser tiene la autorización (por especificación) de gestionar los eventos en el momento que le sea más idóneo. Esto puede provocar que se acumulen una série de eventos durante un lapso de tiempo, para después ser evaluados todos de golpe (evidentemente en el orden en que se han generado).

Esto significa que cuando programemos un Script para gestionar unos eventos, podría pasar que no se nos evalúe el Script cada vez que se genera el evento de entrada que necesitamos. En este caso pasaría que al cabo de un rato se evaluaría nuestro Script tantas veces como eventos de entrada se hayan acumulado.

Para poder evitar o controlar esto, el node Script dispone del campo prodefinido field SFBool mustEvaluate. Por defecto, este campo tiene valor FALSE, cosa que significa que la evaluación del Script puede ser postpuesta. Si queremos que nuestro Script se evalúe cada vez que se reciba un evento de entrada de los que gestionamos, entonces debemos poner el campo mustEvaluate TRUE.

Nota: Es necesario tener en cuenta que poner mustEvaluate TRUE implica imponer un control más exhaustivo al browser y por lo tanto se pierde eficiencia. Sólo debe hacerse cuando sea estríctamente necesario.


El Mecanismo de Acceso a otros Nodes y el field directOutput

A veces es práctico poder acceder diréctamente a los exposedFields, eventOuts y eventIns de nodes externos al Script, sin tener que definir todo un conjunto de ROUTEs. Esto da más control sobre el entorno ya que no se depende de la gestión de eventos que hace el browser. Así pués, podríamos tener definido un node de transformación con un cubo como geometría, del cual queremos saber el valor del campo de traslación desde dentro del Script sin necesidad de que se genere un evento. Entonces lo que haríamos sería:

Ejemplo4: Definimos un acceso directo a un node externo a un Script.

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

DEF MiScript Script {
	eventIn SFTime click
	field SFNode TCubo USE TransfCubo

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

ROUTE TS.touchTime TO MiScript.click

Miremos paso a paso lo que se ha hecho en este ejemplo:

Con este ejemplo vemos que podemos acceder a información de nodes externos sin tener que establecer ROUTEs que nos encaminen eventos. Pero tal y como está definido, sólo podemos acceder a los exposedFields o a los eventOuts para leer su valor.

Si quisiéramos acceder a los exposedFields o a los eventIns para modificarlos, no podríamos a menos que utilizáramos el campo que subministra el VRML en los Scripts, que es el field SFBool directOutput. Este campo, por defecto tiene el valor FALSE. Así, para poder tener acceso a los campos de nodes externos y poderlos modificar, debemos cambiarlo a TRUE.

Veamoslo en el siguiente ejemplo:

Ejemplo5: Definimos un acceso directo a un node externo a un Script con posibilidad de modificación.

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

DEF MiScript Script {
	directOutput TRUE
	eventIn SFTime click
	field SFNode TCubo USE TransfCubo

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

ROUTE TS.touchTime TO MiScript.click

Lo que hace el ejemplo es que cada vez que se entra al Script, se mueve el cubo una posición en el eje de las X hasta llegar a 5, momento en el que se devuelve el cubo al origen.

Ejercicos propuestos:

Diseñar un puente levadizo que suba y baje al apretar un botón que se encuentre en un panel. El puente debe realizar todo el recorrido de subida y de bajada sin que se pueda interrumpir a medio camino y sin que se le pueda pedir subir ciando ya esté arriba ni bajar cuando esté abajo.
Comentarios
  • Utilizar tan solo primitivas para modelar los objetos.
  • Utilitzar campos de Script de tipo SFBool (booleanos o lógicos) para saber el estado del puente en cada momento.
  • Utilitzar OrientationInterpolators para animar el puente.
Solución propuesta: pont.wrl.




< Anterior | Menú ^ | Siguiente >