MokaByte Numero 16 - Febbraio 1998
Foto
 

 

Interazione Java-Vrml
di
Ignazio Locatelli
Altre tecniche di interazione tra Java e Vrml: Script node e JavaScript 






Le lezioni precedenti
1.
2.
3.
4.
5.
 

Interfaccia esterna EAI e alternative

 Sino ad ora è stata analizzata l'interfaccia esterna EAI. Abbiamo visto come questa soluzione rappresenti un ottimo approccio al problema di controllare un mondo vrml da Java.
In particolare, la external authoring interface è l'unica a consentire un controllo direttamente da una applet.

Esistono tecniche alternative che consentono di programmare comportamenti all'interno di un mondo vrml. Nella prima lezione di questo corso (numero di settembre) sono state analizzate le differenze tra la EAI e l'utilizzo di Java all'interno di un nodo Script. Non intendo qui ripercorrere in dettaglio le differenze tra questi due approcci; basti dire che entrambi forniscono le stesse potenzialità a livello di controllo della scena virtuale.

Una ulteriore possibilità a livello della programmazione dei comportamenti all'interno di un mondo virtuale è rappresentata dall'utilizzo di JavaScript. Quest'ultima alternativa è particolarmente semplice da utilizzare e esente da particolari problemi di configurazione. Non è però in grado di offrire la potenza di elaborazione propria di Java e pertanto si presta maggiormente per la realizzazione di semplici scripts.
 

Utilizzo di Java dal nodo Script

 Le specifiche vrml 2.0 prevedono soltanto questa tecnica di interfacciamento tra Java e Vrml (l'interfaccia EAI rappresenta una proposta di aggiunta allo standard).

Le modalità di interazione con il mondo vrml sono molto simili a quelle analizzate sino ad ora e quindi verranno spiegate velocemente in questo articolo.

Come visto nella prima lezione di questo corso, l'approccio dal nodo Script prevede che all'interno del sorgente vrml esista un nodo che indica quali siano gli elementi che la classe Java dovrà sottoporre a controllo.
La specifica del nodo Script è la seguente:

 
 
 

Script {
 
 

  exposedField MFString url []
 
 

  field SFBool directOutput FALSE
 
 

  field SFBool mustEvaluate FALSE
 
 

  # and any number of:
 
 

  eventIn eventTypeName eventName
 
 

  eventOut eventTypeName eventName
 
 

  field fieldTypeName fieldName initialValue
 
 

}

Il nodo Script prevede un campo url che contiene la reference alla classe Java controllante il mondo vrml. Tralasciando i campi directOutput e mustEvaluate, il nodo può contenere un qualsiasi numero di campi (field), eventi in ingresso (eventIn) e eventi in uscita (eventOut).
La classe Java può modificare il valore dei soli campi indicati con un tipo di interfaccia eventOut. Gli unici eventi che possono essere recepiti dalla classe sono quelli indicati nel nodo Script con il tipo di interfaccia eventIn.
Già da ora si nota pertanto una grossa differenza rispetto alla EAI. In quel caso infatti, il mondo vrml non contiene alcuna indicazione circa un controllo esterno da parte di una classe Java (a parte la denominazione dei nodi sfruttando il comando DEF); in questo caso invece, il nodo Script limita l'azione della parte programmativa Java, evidenziando quali sono gli eventi che la classe può ricevere e corrispondentemente quali saranno gli eventi che può generare.
Non è infatti possibile modificare un campo che non risulti elencato all'interno del nodo Script con un tipo di interfaccia pari ad eventOut.

Si deve poi fare attenzione ad un particolare. La classe Java può generare eventi in uscita (quelli indicati dal nodo Script con tipo di interfaccia eventOut), ma questi poi vanno rediretti all'opportuno campo di destinazione mediante un comando di ROUTE (vedere terza lezione).

Vediamo un semplice esempio per chiarire le idee.

 
 
 

DEF Cubo Transform {
 
 

  translation 10 1 0
 
 

  children [
 
 

    Shape {
 
 

      appearance Appearance {
 
 

        material Material { diffuseColor 1 0 0 }
 
 

      }
 
 

      geometry Box { size 2 2 2 }
 
 

    }
 
 

    DEF Touch TouchSensor {}
 
 

  ]
 
 

}
 
 
 
 
 
 

DEF cambia_posizione Script {
 
 

  eventIn SFBool clicked
 
 

  eventOut SFVec3f new_position
 
 

  url "CambiaPosizione.class"
 
 

}
 
 
 
 
 
 

ROUTE Touch.isActive TO cambia_posizione.clicked
 
 

ROUTE cambia_posizione.new_position TO Cubo.set_translation

L'evento isActive sul sensore di tempo viene inviato al nodo Script. Questo evento potrà essere recepito dalla classe Java in quanto è stato rediretto al campo clicked che è indicato con un tipo di interfaccia eventIn. La classe Java effettuerà delle elaborazioni e produce in uscita un assegnamento al campo new_position (l'assegnamento avviene in modo del tutto analogo al caso della EAI). L'assegnamento ad un campo di tipo eventOut produce un evento in uscita dal nodo Script. Questo viene rediretto dal comando ROUTE al campo translation del cubo modificandone la posizione.

Vediamo ora come deve essere strutturata la classe Java per poter interagire con il mondo Vrml.

Utilizzando Java dal nodo Script non è possibile definire la classe come estensione di Applet. La si deve invece definire come estensione della classe Script.

Un opportuno metodo initialize() viene invocato prima della generazione di qualsiasi evento. Si tratta di un metodo nel quale effettuare delle operazioni preliminari. Tipicamente viene utilizzato per acquisire le references ai campi elencati nel nodo Script o per inizializzare opportuni elementi di interfaccia (per esempio sfruttando la AWT).

L'acquisizione delle references ai campi avviene in un modo molto simile a quanto visto per l'interfaccia esterna.
Dapprima si devono definire delle variabili adatte a contenere tali references. Per esempio le seguenti dichiarazioni potrebbero essere impiegate per la classe CambiaPosizione.java:

import vrml.*;
 
 

import vrml.field.*;
 
 

import vrml.node.*;
 
 
 
 
 
 

class CambiaPosizione extends Script {
 
 

  private SFBool clicked;
 
 

  private SFVec3f position;
 
 
 
 
 
 

  initialize() {
 
 

    clicked = (SFBool) getEventIn("clicked");
 
 

    position = (SFVec3f) getEventOut("new_position");
 
 

  }
 
 

  ...
 
 

  ...
 
 

}

Si può notare come sia molto semplice acquisire una reference ad un campo elencato nel nodo Script. Dato che i campi sono relativi al solo nodo Script, non vi è alcuna necessità di acquisire prima delle references a nodi.
Si noti inoltre l'importing dei 3 pacchetti vrml.*, vrml.field.* e vrml.node.*.
L'acquisizione di una reference ad un campo avrebbe richiesto l'utilizzo del metodo getField(String).

Il settaggio di un evento in uscita avviene semplicemente invocando il metodo setValue, come per esempio:

position.setValue(array_di_3_float);
Se si vogliono recepire eventi che si verificano all'interno del mondo vrml esistono due metodi progettati appositamente: ProcessEvent e ProcessEvents.

Il metodo ProcessEvent viene invocato ogni volta che il nodo Script riceve un evento in ingresso. Viceversa ProcessEvents viene invocato nel momento in cui siano sopraggiunti più eventi contemporaneamente. Solitamente tale metodo non fa altro che eseguire un ciclo nel quale si scandiscono i vari eventi e li si inviano singolarmente al metodo ProcessEvent. Per esempio:

public void processEvents(int count, Event events[]) {
 
 

    for (int i=0; i < count; i++)
 
 

      processEvent(events[i]);
 
 

}

Passiamo dunque ad analizzare il funzionamento del metodo processEvent.
Tale metodo riceve un evento. Per quanto detto in precedenza, tale evento si sarà scatenato su uno dei campi definiti con tipo di interfaccia eventIn (nel caso in esame pertanto non potrà che trattarsi del campo booleano clicked).

Il tipo di dato Event ha la seguente struttura:

class Event {
 
 

  public String getName();
 
 

  public ConstField getValue();
 
 

  public double getTimeStamp();
 
 

}

Passando a processEvent un oggetto di tipo Event è quindi possibile risalire al campo sul quale l'evento si è generato, al suo valore e al relativo timestamp.
Il campo processEvent(Event) non fa quindi altro che identificare l'evento (controllando il valore ritornato dal metodo getName()) e ad ottenerne il valore attraverso il metodo getValue(). Per esempio:
public void processEvent(Event evt) {
 
 

  if (evt.getName().equals("clicked))
 
 

    position.setValue(new_pos);
 
 

}

Esistono altri metodi che possono risultare utili in fase di controllo di un mondo vrml (per es. i metodi shutdown() e eventsProcessed()), ma quelli sino ad ora spiegati sono i più importanti. Per ulteriori approfondimenti vi rimando direttamente alle specifiche vrml 2.0 (o vrml97).

Chiudo qui la trattazione del controllo di un mondo vrml mediante una classe Java instanziata dal nodo Script. Non riporto un esempio esplicito a causa del già citato problema relativo a Cosmo Player 1.0. In realtà è sembrato molto strano che la Silicon Graphics abbia rilasciato la prima versione (la finale, non una beta!) del proprio browser vrml supportando la EAI (non standard) e tralasciando l'approccio previsto dalle specifiche (peraltro supportato benissimo dalle beta precedenti di Cosmo Player). Questa mancanza, unitamente a molti bug, fanno di Cosmo Player 1.0 un browser non molto apprezzato. Perchè allora usarlo in questo corso? semplice: a settembre la versione 2.0 di Cosmo Player ancora non era uscita e CP 1.0 sembrava il browser vrml più diffuso (e forse lo è tuttora).

Sarebbe però bene che piano piano ognuno di noi cerchi di aggiornarsi alla nuova versione (la 2.0): il pacchetto è scaricabile direttamente dal sito SGI (http://vrml.sgi.com). Il passaggio non sarà certo indolore, specie se si è cercato di programmare mondi abbastanza complessi (un paio di esempi di questo corso presentano alcuni problemi con CP2.0). Se viceversa state utilizzando con soddisfazione altri browsers, allora non avrete particolari necessità di passare a Cosmo Player.
 

Javascript

 Abbiamo più volte notato come sia necessario disporre di strumenti programmativi per procedere alla creazione di comportamenti complessi. Java rappresenta la soluzione più ricca e potente a questo problema, consentendo di espandere notevolmente il numero di applicazioni nelle quali vrml può essere utilizzato con successo.

In molti casi è però possibile evitare il ricorso a Java, soprattutto quando ci si ritrova a dover gestire elaborazioni non molto complese o che comunque non richiedono particolari funzionalità a livello di interfaccia o comunicazione. Le specifiche vrml 2.0 prevedono l'interfacciamento verso Javascript, inserendo direttamente il sorgente all'interno del nodo Script oppure indicando il file sorgente all'interno del campo url.

Le prime versioni dei browser vrml 2.0 prevedevano l'utilizzo di un linguaggio di scripting denominato vrmlscript. Si trattava di un sottoinsieme di javascript, ormai non più utilizzato.

L'utilizzo di uno script in Javascript si presta in particolar modo per svolgere semplici elaborazioni, evitando quindi di far ricorso ad un vero e proprio linguaggio di programmazione. Questo approccio potrebbe risultare quindi molto più semplice per persone a digiuno di esperienza nel campo della programmazione.

Il funzionamento dell'interfaccia verso Javascript segue di pari passo quanto visto nella prima parte di questo articolo. Il nodo Script elenca tutti i campi in ingresso e in uscita al nodo. Le funzioni Javascript possono settare il valore di un campo in uscita (generando così un eventOut) e recepire eventuali eventi in ingresso.
Consideriamo brevemente lo stesso esempio analizzato per l'utilizzo di Java dal nodo Script.

DEF cambia_posizione Script {
 
 

  eventIn SFBool clicked
 
 

  eventOut SFVec3f posizione
 
 

  field SFVec3f new_posizione 100 0 0
 
 
 
 
 
 

  url "javascript:
 
 
 
 
 
 

      function clicked(value,ts)
 
 

      {
 
 

         if (value == 0)
 
 

          posizione = new_posizione;
 
 

      }
 
 

  "
 
 

}

Analizzando il sorgente precedente, non si può fare a meno di notare come l'utilizzo di javascript per piccoli calcoli risulti molto più veloce e immediato della corrispondente soluzione Java.
In primo luogo, non vi è necessità di acquisire dei riferimenti ai campi in quanto questi sono direttamente disponibili.
La generazione di un eventOut viene effettuata semplicemente assegnando (con =) il nuovo valore al campo in uscita.
Il nodo Script ha un comportamento reattivo. Acquisisce eventi in ingresso, li elabora e genera eventi in uscita. Per ogni evento in ingresso si deve definire una opportuna funzione di gestione avente lo stesso nome del campo in input. Quindi, a differenza dell'interfaccia Java, non vi è alcuna necessità di procedere ad una fase di riconoscimento dell'evento: la relativa funzione di risposta viene invocata automaticamente.
Ogni funzione riceve due parametri: valore e timestamp. Il primo contiene il valore dell'evento ricevuto, mentre il secondo indica il timestamp (tempo di generazione dell'evento).
Nell'esempio considerato si testa il valore dell'evento ricevuto e si procede successivamente a generare l'evento in uscita (ricordo infatti che un TouchSensor genera due eventi distinti, uno alla pressione, con valore 1, e uno in occasione del rilascio, con valore 0)

Non voglio procedere con una descrizione dettagliata dell'utilizzo di Javascript. Ci si dovrebbe infatti occupare dei vari metodi esportati dall'oggetto Browser (come per esempio createVRMLFromString) e delle modalità tramite le quali gestire i vari tipi di dati (per esempio come definire array). Per approfondimenti di questo tipo vi rimando direttamente alle specifiche vrml.

Per chiudere questo articolo, riporto ora un esempio che fa ampio utilizzo delle tecniche di javascript.

Si tratta di un semplice puzzle, simile a quelli che usualmente utilizzano i bambini (in questo caso però il soggetto è più adatto ai grandi che ai piccini ;) ). L'utente ha a disposizione una serie di blocchi, ognuno dei quali riportante un pezzo dell'immagine da costruire. Effettuando delle operazioni di drag and drop con il mouse è possibile accostare i blocchi in modo da ricostruire l'immagine.

L'idea di costruire questo semplice esempio mi è venuta vedendo un mondo sul sito della Silicon Graphics che utilizzava la stessa tecnica. Mi sono chiesto come si potesse implementare: la soluzione seguente è la prima che mi è venuta in mente.

Per prima cosa mi sono cercato un ottimo soggetto da raffigurare. Una foto della Schiffer mi sembrava più che indicata. Ho tagliato l'immagine in più parti; ognuna di queste textures viene poi applicata ad un blocco all'interno del mondo vrml.

Il sorgente vrml (che qui non riporto per esteso) consiste di una sequenza di blocchi disposti su una griglia. Ad ogni blocco viene associato un pezzo dell'immagine di partenza. Inoltre, ho provveduto ad inserire un PlaneSensor per ogni blocco.

Il funzionamento del PlaneSensor è molto semplice (lo abbiamo già visto la lezione scorsa). Quando l'utente clicca con il mouse sull'oggetto a cui è associato, emette in uscita degli eventi che consentono di recepire lo spostamento del cursore a tasto premuto (è in grado cioè di rilevare l'operazione di drag). Questo evento viene utilizzato per spostare il blocco in conseguenza dell'azione dell'utente.
E' immediato capire che serve un PlaneSensor per ogni blocco da spostare, onde evitare conflitti nella gestione degli spostamenti.

Qui sotto riporto il codice vrml relativo ad un solo blocco. Gli altri blocchi saranno trattati in maniera analoga.

DEF B1 Transform {
 
 

  translation -2 0 0
 
 

  children [
 
 

    Shape {
 
 

      appearance Appearance {
 
 

        material Material { diffuseColor .5 .5 1 }
 
 

        texture ImageTexture { url "img12.jpg" }
 
 

      }
 
 

      geometry Box { size 1 1 1 }
 
 

    }
 
 

    DEF P1 PlaneSensor {}
 
 

  ]
 
 

}

Il PlaneSensor P1 opera su tutti gli oggetti contenuti nello stesso campo children nel quale si trova: quindi sul cubo a cui è associato.

Nell'esempio implementato ho fatto ricorso ad una griglia di 6 righe e 5 colonne (inizialmente avevo optato per una 5x5, ma poi mi sono accorto che avrei dovuto tagliare un pezzo dell'immagine che senz'altro attira molto ;) ). Di conseguenza si dovranno inserire 30 blocchi, con i relativi sensori.

L'evento translation_changed in uscita dal nodo PlaneSensor viene intercettato da un nodo Script, il quale provvede ad elaborarlo e a muovere il blocchetto corrispondente.
Il nodo Script sfrutta Javascript per implementare le semplici elaborazioni richieste. Per questo tipo di applicazione sarebbe del tutto inutile passare a Java.

Il nodo Script riceve in ingresso gli eventi translation_changed. Vi saranno quindi 30 campi di tipo eventIn. Dato che deve poter modificare la posizione di ciascun blocco, dovranno essere presenti anche 30 campi di tipo eventOut. Infine, vengono inseriti anche dei campi di tipo field, utilizzati per effettuare un corretto calcolo degli spostamenti.
Per capire a fondo il funzionamento dei campi di tipo field, si deve porre particolare attenzione al funzionamento del nodo PlaneSensor. Questo nodo emette eventi translation_changed quando il mouse viene spostato (a pulsante premuto) sopra l'oggetto a cui è associato. Gli eventi translation_changed indicano lo spostamento rispetto al punto iniziale di pressione (quindi in ogni momento ritorna la distanza rispetto al punto di pressione iniziale). Si può ben capire come sia dunque necessario ricordarsi della precedente posizione del blocco, in modo da poterla sottrarre dal valore ritornato dall'evento translation_changed. Il risultato della sottrazione indica di quanto si è spostato il mouse dall'ultima operazione di spostamento. A questo punto basterà sommare tale step (che in realtà è un array di 3 elementi x,y,z) al valore di posizione precedente.

Come si può notare le elaborazioni svolte da javascript sono molto semplici. Un utilizzo di Java in questo caso sarebbe stato del tutto fuori luogo.

Qui sotto riporto alcuni pezzi del nodo Script:

DEF muovi_puzzle Script {
 
 

  eventIn SFVec3f new_pos1
 
 

  eventIn SFVec3f new_pos2
 
 

  ...
 
 

  eventIn SFVec3f new_pos30
 
 
 
 
 
 

  # campi di appoggio utilizzati per ricordarsi della posizione
 
 

  # attuale del blocco
 
 

  field SFVec3f pos_1 -2 0 0
 
 

  field SFVec3f pos_2 -1 0 0
 
 

  ...
 
 

  field SFVec3f pos_30 2 -1 0
 
 
 
 
 
 

  eventOut SFVec3f out_pos1
 
 

  eventOut SFVec3f out_pos2
 
 

  ...
 
 

  eventOut SFVec3f out_pos30
 
 
 
 
 
 

  field SFVec3f stepx 0
 
 

  field SFVec3f stepy 0
 
 
 
 
 
 

  url "javascript:
 
 
 
 
 
 

     function new_pos1(value) {
 
 

       stepx = value[0] - pos_1[0];
 
 

       stepy = value[1] - pos_1[1];
 
 

       pos_1[0] += (stepx - 2);
 
 

       pos_1[1] += stepy;
 
 

       out_pos1 = pos_1;
 
 

     }
 
 
 
 
 
 

     ...
 
 
 
 
 
 

     function new_pos30(value) {
 
 

      ...
 
 

     }
 
 

  "
 
 

}
 
 
 
 
 
 

#sequenza di ROUTE
 
 
 
 
 
 

ROUTE P1.translation_changed TO muovi_puzzle.new_pos1
 
 

ROUTE P2.translation_changed TO muovi_puzzle.new_pos2
 
 

...
 
 

ROUTE P30.translation_changed TO muovi_puzzle.new_pos30
 
 
 
 
 
 

ROUTE muovi_puzzle.out_pos1 TO B1.set_translation
 
 

ROUTE muovi_puzzle.out_pos2 TO B2.set_translation
 
 

...
 
 

ROUTE muovi_puzzle.out_pos30 TO B30.set_translation


L'esempio è abbastanza semplice. Penso comunque che sia più che adatto per comprendere il funzionamento dell'interfaccia verso Javascript.

Il prossimo articolo sarà dedicato all'analisi di un progetto abbastanza complesso: come realizzare animazione umana in vrml. Questo argomento sta assumendo sempre maggiore importanza sulla scena vrml. Cercherò di restare sul semplice, analizzando i meccanismi fondamentali e fornendo un esempio pratico (molto semplificato). Una applicazione molto più complessa in quest'ambito può essere trovata presso il gruppo VRML DREAMERS (http://www.lucia.it/vrml), sezione progetti - Frank (e costituisce anche la base della tesi che dovrò dare al Politecnico di Milano il 19 febbraio).
L'analisi di questo progetto ci fornirà comunque l'occasione per approfondire le tecniche di programmazione vrml e dell'interfaccia verso Java (torneremo ad utilizzare l'interfaccia EAI).

Giunti a questo punto, con alle spalle tutta la trattazione sulla EAI e sul nodo Script, sarebbe per me molto importante sapere se gli esempi che via via fornisco sono pienamente accessibili e chiari. Se pertanto qualcuno reputa che siano troppo complicati o duri da digerire, fatemelo sapere, in modo che mi possa regolare. Come avrete notato, gli esempi sono andati via via complicandosi con il passare delle lezioni (ma questo è naturale). Il mio indirizzo email è: ignazio@logicom.it.
 
 
 
 


MokaByte Web  1998 - www.mokabyte.it

MokaByte ricerca nuovi collaboratori. 
Chi volesse mettersi in contatto con noi può farlo scrivendo a mokainfo@mokabyte.it