MokaByte Numero 14 - Dicembre 1997

Foto

INTERAZIONE TRA VRML E JAVA 

 

di  

Ignazio Locatelli 
Creazione di oggetti vrml a run-time attraverso l'interfaccia Java-Vrml 
quarta parte
 

 
 

Le lezioni precedenti
1.
 
2.
 

 

  

Riassunto dell'articolo precedente

 Il precedente articolo è stato decisamente più pesante del solito. Procedo dunque a riassumere i concetti fondamentali che erano stati introdotti.
L'animazione di oggetti in vrml può essere realizzata banalmente attraverso i meccanismi built-in offerti dalle specifiche vrml 2.0. Le entità che più ci interessano in fase di realizzazione di una animazione sono:

Un nodo TimeSensor, se attivo, emette eventi correlati al trascorrere del tempo. L'evento fraction_changed, in uscita dal sensore di tempo, indica la parte di ciclo temporale trascorsa (la durata del ciclo viene impostata dal campo cycleInterval).
Questo evento viene indirizzato in ingresso ad un nodo interpolatore. A fronte di un evento in ingresso, l'interpolatore reagisce emettendo un evento in uscita. Il valore di tale evento viene calcolato in funzione dei campi key e keyValue (il formato di quest'ultimo dipende dal tipo di interpolatore). Questo evento in uscita può essere indirizzato in ingresso ad un qualsiasi altro campo dello stesso tipo, modificando quindi nel tempo le caratteristiche di un nodo vrml (per esempio la posizione di un certo oggetto).

Sebbene questi meccanismi consentano di ottenre semplicemente delle animazioni, nella maggior parte dei casi non sono sufficienti. Già nell'esempio dell'articolo n.3 abbiamo notato come l'implementazione di un interruttore non può essere realizzata senza fare ricorso ad un linguaggio di programmazione (o di scripting).
La gestione da Java dei nodi vrml che controllano le animazioni non pone ulteriori problemi rispetto ai soliti nodi vrml. E' possibile acquisire una reference ad un campo di tipo eventIn o exposedField e quindi procedere alla sua modifica attraverso il metodo setValue(). Ma a differenza di altri nodi vrml, nodi come TimeSensor e PositionInterpolator possono anche emettere eventi e sono quindi dotati di campi con un tipo di interfaccia eventOut. L'acquisizione di una reference verso questi campi è molto simile a quella per un evento in ingresso (utilizzando il metodo getEventOut("nome_campo")). Non è però possibile procedere alla scrittura del campo, ma solo alla sua lettura (trattandosi appunto di un evento in uscita da un nodo).
Inoltre sorge un altro problema. L'emissione di eventi in uscita è del tutto asincrona rispetto all'esecuzione dell'applet Java e potrebbe rendere difficoltoso recepire il verificarsi di eventi all'interno del mondo vrml.
Il problema viene risolto dalla implementazione dell'interfaccia eventOutObserver, definendo il metodo callback. Tale metodo viene invocato ogni volta che all'interno del mondo vrml si scatena un evento sul quale ci si era posti in ascolto.
Per segnalare la necessità di essere informati del verificarsi di particolari eventi viene utilizzato il metodo advise().
All'interno del metodo callback è possibile identificare l'evento che si è verificato e procedere dunque alla sua gestione.

L'esempio a conclusione del precedente articolo ha mostrato come implementare la gestione di un ascensore attraverso appositi pulsanti di chiamata.
Torneremo brevemente su questo esempio in conclusione del presente articolo, per cercare di risolvere quel problema puntualizzato la volta scorsa. Con Cosmo Player 1.0 infatti, la fase di salita da pianto terra all'ultimo piano presenta tremolii e imperfezioni.

Ho reputato importante ripercorrere velocemente il precedente articolo, in quanto risulta fondamentale per la comprensione dell'interazione tra Java e Vrml attraverso la EAI.
 

Creazione dinamica di oggetti vrml

 

Questo articolo considera un altro aspetto molto interessante nell'interazione tra Java e Vrml: creazione dinamica di oggetti.
Per la precisione, l'aggiunta a run-time di oggetti vrml non è ottenibile solo attraverso Java. In particolare è possibile creare vrml dinamicamente attraverso i seguenti modi:

Qui verrà analizzata soltanto l'ultima tecnica, senz'altro quella più potente e flessibile. In particolare si noti come l'interfaccia CGI consenta di CREARE un nuovo documento, non di modificarlo a run-time; di conseguenza questo approccio è molto meno flessibile.

Abbiamo visto come risulti necessario acquisire una reference al browser html in fase di inizializzazione. Questo consente di accedere a nodi e campi vrml da applet Java.
L'oggetto Browser esporta diversi metodi, uno dei quali, getNode(String), è stato analizzato in precedenza. Un altro metodo molto utile è createVrmlFromString(String), tramite il quale è possibile creare un nuovo nodo a run-time.
Il parametro di tipo String contiene il sorgente Vrml che descrive il nodo da creare. L'oggetto ritornato da questo metodo va assegnato ad un oggetto di tipo Node[] (quindi in realtà si può creare una collezione di nodi).
Per esempio, il seguente frammento di codice crea un nuovo nodo vrml e lo assegna all'oggetto Node[].

public Node[] newNode;
public String stringa;
...
stringa = "Shape { appearance Appearance { material Material { ";
stringa += "diffuseColor 1 0 0 } } geometry Sphere {} }";
newNode = browser.createVrmlFromString(stringa);

Il nodo creato è un semplice nodo Shape contenente la descrizione di una sfera.

La creazione di un nodo attraverso il metodo createVrmlFromString(String) non produce alcun risultato diretto sul mondo vrml. Per poter aggiungere il nodo appena creato alla scena virtuale si deve provvedere a spedirlo come evento in ingresso ad un grouping node.
Un grouping node è un nodo che può contenere al suo interno più nodi, racchiusi all'interno di un campo children. Tale è il caso per esempio del nodo Transform. Tale nodo esporta diversi campi, alcuni dei quali già analizzati in precedenza, come translation, rotation, scale. Per procedere all'aggiunta dinamica di nodi, sono previsti altri due campi:

Si tratta di due eventi in ingresso e quindi possono solo essere scritti dall'esterno. Il nodo appena creato con il metodo createVrmlFromString deve dunque essere assegnato al campo addChildren.
Si noti il tipo di questi campi: MFNode. Un tipo SFNode indica che il campo può contenere un solo nodo. MFNode, di conseguenza, rappresenta un campo che può contenere al suo interno più nodi (tale è il caso per esempio del più volte visto campo children del nodo Transform, al cui interno possiamo posizionare un numero a piacere di nodi vrml).
Per assegnare un nodo appena creato attraverso createVrmlFromString(String), basta passare tale nodo come argomento del metodo setValue associato all'oggetto che punta al campo addChildren del grouping node.

Il seguente esempio mostra più chiaramente il procedimento.

Supponiamo di avere un file vrml contenente un nodo Transform di questo tipo:

DEF Root Transform {}
Vogliamo creare a run-time (per qualche motivo) una sfera da aggiungere al suo campo children. Per prima cosa dobbiamo acquisire una reference al campo addChildren del nodo Root. Solo in questo modo potremo procedere all'aggiunta della sfera al suo campo children (che per il momento è completamente vuoto).
 

    ...
    Node Root;
    EventInMFNode addChildren_root = null;
    ...
    Node newNode[];
    ...
    ...
    // acquisisco una reference al nodo Root
    Root = browser.getNode("Root");
    // acquisisco una reference al campo addChildren
    addChildren_root = (EventInMFNode) Root.getEventIn("addChildren");
    ...
    stringa = "Shape { appearance Appearance { material Material { ";
    stringa += "diffuseColor 1 0 0 } } geometry Sphere {} }";
    newNode = browser.createVrmlFromString(stringa);
    addChildren_root.setValue(newNode);
    ...
Come si può vedere l'aggiunta a run-time di nodi vrml risulta molto semplice. Basta creare un nodo vrml con createVrmlFromString e mandarlo come evento in ingresso al campo addChildren di un grouping node.

In alcune applicazioni potrebbe però nascere la necessità di voler controllare successivvamente i nodi appena creati a run-time.
Per esempio, per la realizzazione di un semplice tool di creazione di scene vrml (che potete trovare presso il sito Vrml Dreamers: http://www.lucia.it/vrml nella sezione Progetti-Builder), oltre alla possibilità di introdurre nuovi oggetti, risulta necessario poterli spostare, ruotare, scalare o cambiare di colore. In tali casi deve essere possibile acquisire dei riferimenti ai campi translation, rotation, scale, ecc. del nodo appena aggiunto.
Questa operazione non presenta particolari problemi, in quanto, dopo la creazione del nuovo nodo, abbiamo a disposizione una reference ad esso. Basta dunque invocare i metodi getEventIn(String) o getEventOut(String) come per un nodo qualsiasi.
C'è un solo particolare su cui fare attenzione. Ricordo infatti che atraverso il metodo createVrmlFromString non creiamo un solo nodo, ma una collezione di nodi (Node[]). Il primo nodo contenuto nella stringa sarà dunque referenziato da newNode[0], il secondo da newNode[1] e così via.
Supponiamo per esempio di avere creato un nuovo nodo Transform e di volere poi traslare il suo contenuto. Per es.

  ...
  stringa = "Transform { children [ Shape { appearance Appearance {";
  stringa += " material Material { diffuseColor 1 0 0 } } ";
  stringa += "geometry Sphere {} ] }";
  newTransf = browser.createVrmlFromString(stringa);
  traslazione = (EventInSFVec3f) newTransf[0].getEventIn("set_translation");
  rotazione = (EventInSFRotation) newTransf[0].getEventIn("set_rotation");
  scala = (EventInSFVec3f) newTransf[0].getEventIn("set_scale");
  addChildren_transf = (EventInMFNode) newTransf[0].getEventIn("addChildren");
  ...


Nel frammento di codice precedente si è provveduto a creare un nuovo nodo Transform, contenente una sfera di colore rosso; successivamente si sono acquisiti i riferimenti ad alcuni campi. Attraverso di essi sarà dunque possibile modificare alcune proprietà del nodo aggiunto.
Si ricorda che il nodo newTransf dovrà essere mandato in ingresso ad un campo addChildren per ottenere l'effettiva aggiunta al mondo vrml.
Si noti infine come sia possibile acquisire riferimenti a campi addChildren di nodi creati a run-time, fornendo la possibilità di aggiungere altri nodi all'interno di quelli creati.

L'aggiunta di oggetti vrml a tempo di esecuzione rappresenta uno strumento molto potente e utile nell'ambito della realizzazione di mondi virtuali sempre più interessanti (anche se si vedono pochi mondi in circolazione che mostrano sfoggio di queste funzionalità... ma questo probabilmente deriva dal fatto che queste operazioni sono scarsamente automatizzabili attraverso tool di authoring appositi; suppongo che la situazione cambierà presto).

Per concludere questo articolo, ci dedichiamo allo sviluppo di una semplice demo di visualizzazione dati. L'utilità di vrml in questo ambito è ancora tutta da scoprire e sperimentare.
La presente applicazione non vuole certo rappresentarne un esempio, ma solo uno spunto per dimostrare come vrml si presti anche ad applicazioni di questo tipo.
Supponiamo di volere raccogliere i dati di vendita di quattro aziende in quattro diversi mesi di attività. Vogliamo realizzare un grafico a barre 3D, ponendo sull'asse x le 4 aziende, sull'asse y il fatturato e sull'asse z i 4 mesi.
Il grafico complessivo sarà dunque costituito da 16 barre 3D, ognuna rappresentante le vendite realizzate da una azienda in un certo mese.

L'applet Java deve gestire tutte le operazioni di input dei dati. Per semplicità, esprimiamo gli importi come numeri interi, assumendo che si tratti di miliardi di lire.
Utilizzeremo una griglia di 16 celle, ognuna delle quali contiene il fatturato in miliardi di una azienda in un certo mese.
Un apposito pulsante sottopone al mondo vrml i dati introdotti e si procede alla creazione a run-time del grafico.

Tralasciando di considerare la parte di creazione del layout dell'applet (nella quale suppongo sarete più esperti del sottoscritto), concentriamoci sugli aspetti di creazione del grafico.
Dobbiamo poter aggiungere a run-time delle barre 3D. Le barre del grafico possono essere realizzate semplicemente attraverso delle Box, la cui altezza è proporzionata al fatturato. La diversa altezza delle Box viene imposta in fase di creazione, adattando il campo size relativo alla dimensione in y; in pratica non si fa altro che agire sulla stringa da sottoporre a createVrmlFromString. Dato poi che la Box viene posizionata con il suo centro nell'origine del sistema di coordinate correnti, si deve provvedere ad incapsulare il nodo Shape contenente la barra 3D all'interno di un nodo Transform. In questo modo possiamo posizionare la barra nella cella opportuna e spostarne il centro in alto o in basso in maniera tale che l'estremo inferiore corrisponda ad una quota pari a 0 miliardi.

I dati introdotti devono poter essere modificati. Questo comporta la necessità di poter cancellare il grafico per ricostruirlo in base ai nuovi dati. La cancellazione di un nodo dal mondo vrml avviene seguendo le stesse modalità previste per la fase di aggiunta. Semplicemente si utilizza il nodo removeChildren anzichè addChildren.

Passiamo ora a considerare i sorgenti. Il mondo vrml è semplicissimo, in quanto è vuoto, tranne che per la definizione degli assi e del nodo Root che verrà utilizzato per procedere all'aggiunta delle barre.
Si faccia attenzione al fatto che è INDISPENSABILE avere un tale nodo. Possiamo infatti aggiungere nodi a run-time solo mandando eventi ad un campo addChildren. Si noti che per acquisire una reference a tale nodo siamo obbligati a fornirgli un nome attraverso il comando DEF.

#VRML V2.0 utf8



Viewpoint {

  description "vista di default"

  position 15 7 30

}



DEF Root Transform {}



DEF asse_x Transform {

  translation 15 0 0

  rotation 0 0 1 -1.57

  children [

    DEF asse Transform {

      children [

        Shape {

          appearance Appearance {

            material Material { diffuseColor 1 0 0 }

          }

          geometry Cylinder {

            radius .1

            height 30

          }

        }

        Transform {

          translation 0 15.5 0 

          children [

            Shape {

              appearance Appearance {

                material Material { diffuseColor 0 1 0 }

              }

              geometry Cone {

                bottomRadius .3

                height 1

              }

            }

          ]

        }

      ]      

    }

  ]

}



DEF asse_y Transform {

  translation 0 10 0

  scale 1 .667 1

  children [

    USE asse

  ]

}



DEF asse_z Transform {

  translation 0 0 -15

  rotation 1 0 0 -1.57

  children [

    USE asse

  ]

}
Qui di seguito riporto invece il sorgente dell'applet Java controllante. Alla luce di quanto spiegato in precedenza non dovrebbero sussistere particolari problemi di comprensione.
import java.awt.*;

import java.applet.*;

import vrml.external.field.*;

import vrml.external.Node;

import vrml.external.Browser;

import vrml.external.exception.*;

import netscape.javascript.JSObject;



public class CreaGraf extends Applet {



        Browser browser;



        Node Root;



        EventInMFNode addChildren_root;

        EventInMFNode removeChildren_root;



        Node barre[][][] = new Node[4][][];



        TextField data[][] = new TextField[4][];



        boolean error = false;





        public void start()

        {

                // reference al browser vrml

                JSObject win = JSObject.getWindow(this);

                JSObject doc = (JSObject) win.getMember("document");

                JSObject embeds = (JSObject) doc.getMember("embeds");

                browser = (Browser) embeds.getSlot(0);



                // piccola attesa prima di accedere ai nodi vrml

                // aspetto che il mondo vrml sia definitivamente caricato

                // in modo da poter accedere ai nodi in esso contenuti



                try {

                  Thread.sleep(2000);

                } catch(InterruptedException e) {

                  showStatus("thread interrupted!");

                  error = true;

                }



                try {



                        Root = browser.getNode("Root");

                        addChildren_root = (EventInMFNode) Root.getEventIn("addChildren");

                        removeChildren_root = (EventInMFNode) Root.getEventIn("removeChildren");

               }

                catch (InvalidNodeException ne) {

                      error = true;

                      showStatus("Errore in fase di reference al nodo "" + ne);

                }

                catch (InvalidEventInException ee) {

                      error = true;

                      showStatus("Errore in fase di reference all'evento "" + ee);

                }

                catch (InvalidEventOutException ee) {

                        error = true;

                        showStatus("Errore in fase di reference all'evento "" + ee);

                }



                barre[0] = new Node[4][];

                barre[1] = new Node[4][];

                barre[2] = new Node[4][];

                barre[3] = new Node[4][];



                data[0] = new TextField[4];

                data[1] = new TextField[4];

                data[2] = new TextField[4];

                data[3] = new TextField[4];



                // layout dell'applet



                setLayout(new GridLayout(6,5,2,2));



                add(new Label(""));

                add(new Label("mese 1"));

                add(new Label("mese 2"));

                add(new Label("mese 3"));

                add(new Label("mese 4"));



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

                  for (int j = 0; j < 4; j++) 

                        data[i][j] = new TextField("0",2);



                add(new Label("ditta 1"));

                add(data[0][0]);

                add(data[0][1]);

                add(data[0][2]);

                add(data[0][3]);



                add(new Label("ditta 2"));

                add(data[1][0]);

                add(data[1][1]);

                add(data[1][2]);

                add(data[1][3]);



                add(new Label("ditta 3"));

                add(data[2][0]);

                add(data[2][1]);

                add(data[2][2]);

                add(data[2][3]);



                add(new Label("ditta 4"));

                add(data[3][0]);

                add(data[3][1]);

                add(data[3][2]);

                add(data[3][3]);



                add(new Label(""));

                add(new Label(""));

                add(new Label(""));

                add(new Label(""));

                add(new Button("Visualizza"));



                visualizza();



        }



        public boolean action(Event evt, Object arg)

        {

                if (evt.target instanceof Button) {

                        String label = (String) arg;



                        if (label.equals("Visualizza")) {

                                pulisci();

                                visualizza();

                        }

                        return true;

                }

                else

                        return false;



        }



        private void visualizza()

        {

            int i,j;

            String stringa = new String();

            float center_x, center_y, center_z;

            float altezza;



            center_x = (float) 4;

            center_z = (float) -4;



            for (i = 0; i < 4; i++) {



              for (j = 0; j < 4; j++) {



                altezza = getValue(i,j);

                if (altezza > 30)

                        altezza = 30;



                center_y = altezza/2;



                stringa = "Transform { translation "" + center_x + " "";

                stringa += center_y + " "" + center_z + " children [ "";

                stringa += "Shape { appearance Appearance { material "";

                stringa += "Material { diffuseColor "";

                switch(i) {

                case 0:

                        stringa += "1 0 0";

                        break;

                case 1:

                        stringa += "0 1 0";

                        break;

                case 2:

                        stringa += ".5 .5 1";

                        break;

                case 3:

                        stringa += ".8 .8 .8";

                        break;

                }



                stringa += " } } geometry Box { size 5 "" + altezza + " 5 }"";

                stringa += " } ] }"";



                barre[i][j] = browser.createVrmlFromString(stringa);

                addChildren_root.setValue(barre[i][j]);



                center_x += 8;

              }

              center_x = (float) 4;

              center_z -= 8;

            }

         }



        private void pulisci()

        {

                int i,j;



                for (i = 0; i < 4; i++)

                  for (j = 0; j < 4; j++)

                        removeChildren_root.setValue(barre[i][j]);

        }



        private float getValue(int i, int j)

        {

                Float num_read = new Float(0);

                String str = new String();



                str = data[i][j].getText();

                num_read = num_read.valueOf(str);

                return num_read.floatValue();

        }



}
Il file html relativo è il seguente:
&ltHTML>

&ltHEAD>&ltTITLE&gtVisualizzazione dati</TITLE></HEAD>



&ltCENTER>

&ltEMBED SRC="grafico.wrl" height = 200 width = 400>

&ltP>

Massimo fatturato = 30

&ltP>

&ltAPPLET CODE="CreaGraf.class"  HEIGHT=150 WIDTH=600 mayscript>

</APPLET>



</HTML>

 

Ricordo che per la visualizzazione dei dati si deve procedere alla pressione del bottone 'Visualizza'; solo allora le modifiche nei campi di input verranno applicate anche al grafico.

L'esempio analizzato nel presente articolo è decisamente esemplificativo delle potenzialità raggiungibili da vrml anche in campi un po' più 'professionali'... in ogni caso è un ottimo esempio di come creare oggetti vrml a run-time.
 

Riflessioni sull'esempio dell'articolo precedente

 In conclusione di articolo torniamo brevemente all'esempio visto la volta scorsa. Avevamo implementato un ascensore virtuale, dotato dei relativi bottoni di chiamata a piano terra e all'ultimo piano. Altri due bottoni sull'ascensore consentivano di controllare l'ascensore dall'interno.

Ci eravamo inbattuti nel problema di spostare l'utente quando questo è posizionato all'interno dell'ascensore in movimento. Lo avevamo risolto affidandoci al meccanismo automatico di collision detection implementato da Cosmo Player (come del resto dalla maggior parte di tutti i browser vrml 2.0). Questa soluzione porta però a risultati accettabili ma non certo esaltanti. Con Cosmo Player 1.0 si ravvisano fastidiosi tremolii e discontinuità nello spostamento.

Una soluzione alternativa potrebbe essere la seguente (anche se in verità presenta una piccola controindicazione)

Nei precedenti articoli abbiamo incontrato più volte il nodo Viewpoint. Tale nodo consente di definire la posizione dell'utente all'interno del mondo virtuale. Il primo nodo Viewpoint definito nel sorgente vrml specifica la posizione iniziale dell'utente. Successive viste predefinite con Viewpoint devono essere attivate mediante i meccanismi forniti dal browser (per esempio, con Cosmo Player 1.0 si clicchi con il pulsante destro del mouse e si selezioni viewpoints).

L'attivazione di una vista predefinita attraverso un nodo Viewpoint può essere effettuata anche da Java (ma anche da vrml direttamente). Il nodo Viewpoint prevede infatti un campo in ingresso di tipo SFBool chiamato set_bind. Mandando un evento TRUE in ingresso a questo campo, si ottiene l'attivazione della vista.

L'idea è dunque la seguente. Definiamo una vista all'interno del campo children nel quale è definita anche la base dell'ascensore. Più precisamente, la vista è sottoposta ad una ulteriore traslazione (mediante un nesting di nodi Transform), in modo che l'utente sia piazzato circa un metro sopra il pavimento dell'ascensore.
Il nodo Viewpoint inoltre è innestato all'interno di un grouping node che contiene anche il pavimento dell'ascensore. Quando il pavimento viene mosso (agendo sul campo translation del nodo Transform che lo contiene), anche la Viewpoint risentirà di un movimento identico. Risultato: se l'utente viene assegnato a questa Viewpoint, il suo posizionamento dovrebbe seguire di pari passo lo spostamento dell'ascensore. E il tutto senza tremolii e imperfezioni.

Attenzione al condizionale... dico dovrebbe in quanto non basta attivare la Viewpoint e poi far partire l'animazione. Infatti, l'utente non risente delle variazioni imposte ad una Viewpoint, se non quando un set_bind viene eseguito. Ciò comporta che si deve procedere a mandare in continuazione eventi set_bind di valore TRUE. In questo modo si realizza uno spostamento fluido e lineare dell'utente.

Il lato negativo? beh... è facile immaginarlo. Dato che continuiamo a mandare eventi set_bind, durante una salita l'utente non potrà muovere un dito! Non potrà scendere dall'ascensore, non potrà voltarsi, guardarsi attorno, ecc. Questo per il semplice fatto che la Viewpoint specifica completamente posizione e orientamento dell'utente.

Non riporto l'esempio modificato dell'ascensore, forse anche perchè, tutto sommato, questa perdita di libertà in fase di movimento dell'ascensore non mi piace poi molto (la sensazione di buttarsi dall'ascensore va pure mantenuta no?).
Ho voluto comunque riportare queste considerazioni in quanto l'animazione della telecamera può risultare molto utile anche in altri contesti.

Prima di chiudere vorrei fare un paio di considerazioni:

Con questo articolo abbiamo completato una buona parte di quanto l'interazione tra Java e Vrml prevede. Nei prossimi cercheremo di realizzare altri esempi a consolidamento di quanto visto sino ad ora, senza tralasciare di approfondire le conoscenze di altri nodi vrml che ancora non sono stati considerati.

 

 

 

MokaByte rivista web su Java

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