MokaByte Numero 15 - Gennaio 1998
Foto
   
Interfaccia Java-Vrml 
di
Ignazio Locatelli
Esempi di programmazione dell'interfaccia tra java e vrml

 



Riassunto delle lezioni precedenti

 In questo articolo vedremo di consolidare le nozioni acquisite nei precedenti, cercando di introdurre semplici novità dal punto di vista dei nodi resi disponibili dalle specifiche vrml 2.0. Procederemo ad analizzare un paio di esempi: il primo consente di introdurre attrazione gravitazionale sugli oggetti all'interno del mondo; il secondo consiste invece in un semplice gioco vrml, incentrato sull'utilizzo della stessa legge gravitazionale spiegata nell'esempio precedente.

Prima però vale la pena ripercorrere brevemente il percorso seguito sino a questo punto.
Abbiamo notato come sia banale modificare un mondo vrml da una applet Java. Attraverso semplici metodi è infatti possibile acquisire un riferimento alla scena virtuale e procedere alla modifica di nodi e campi vrml.
Abbiamo poi visto come sia spesso indispensabile poter sentire il verificarsi di eventi all'interno della scena virtuale: capire in pratica quando si verifica un evento e di quale evento si tratta. Per conseguire questo risultato si deve implementare l'interfaccia EventOutObserver e definire il metodo callback. Sfruttando questa tecnica è pertanto possibile gestire gli eventi generati all'interno del mondo vrml, consentendo un controllo interattivo della scena.
Infine, la volta scorsa abbiamo visto come sia relativamente semplice procedere ad una modifica sostanziale dell'ambiente virtuale aggiungendo o rimuovendo nodi a tempo di esecuzione.

Ricapitolando, i passi fondamentali seguiti nell'esposizione dell'interfaccia Java-Vrml sono stati i seguenti:
- modifica del valore di campi e nodi vrml
- monitoraggio di eventi e relativa gestione
- creazione a run-time di oggetti vrml

Gli argomenti sino a questo punto trattati sono le basi portanti dell'interfaccia Java-Vrml (sia attraverso l'utilizzo della EAI che dal nodo Script).

Ritengo a questo punto molto utile soffermarsi sull'esame di un paio di esempi, in modo da rafforzare le conoscenze acquisite e chiarirne l'utilizzo. Nel prossimo articolo si vedrà brevemente l'approccio all'interazione tra Java e Vrml dal nodo Script, nonchè l'utilizzo di JavaScript all'interno di mondi vrml. I successivi saranno invece incentrati su esempi più complessi e, a mio avviso, più interessanti.
 

Introdurre attrazione gravitazionale sugli oggetti

 Subito è necessaria una precisazione. Vrml implementa già una forma di attrazione gravitazionale (l'opzione gravity del browser). Questa agisce nei confronti del visitatore, forzandolo a rimanere ancorato al terreno (se esistente).
Non esiste però un concetto analogo per quanto riguarda gli oggetti presenti all'interno del mondo. Nulla vieta che esistano dei blocchi sospesi magicamente nel vuoto. Non voglio dire che questo non debba accadere: per esempio potremmo volutamente decidere di implementare un ambiente in assenza di gravità, oppure anche un mondo nel quale le leggi fisiche sono completamente sovvertite. Ma se l'obiettivo che ci poniamo è quello di ricreare un ambiente il più possibile realistico dobbiamo preoccuparci della gestione di questo aspetto.

Ma perchè si dovrebbero avere degli oggetti sospesi nel vuoto? A parte il caso banale di un errore nel posizionamento da parte del programmatore (contro il quale non si vuole ovviamente agire), si potrebbero avere oggetti non ancorati al suolo in quanto vogliamo fornire all'utente la possibilità di spostarli. In un mondo 'reale' è infatti possibile sollevare oggetti non troppo pesanti... così come poi è naturale aspettarsi che questi oggetti cadano al suolo in conseguenza del loro rilascio.

L'esempio che procediamo ora ad analizzare considera appunto questi due fattori: possibilità di sollevare oggetti e loro caduta in conseguenza dell'abbandono.
Il sollevamento deve essere tale da risultare più difficoltoso per oggetti molto pesanti, mentre molto più semplice per oggetti di peso inferiore. La caduta deve seguire la nota legge di Newton, subendo una accelerazione pari a g.

L'esempio riportato introduce alcune semplificazioni:
- lo 'sforzo' da effettuare per il sollevamento non dipende da g ma direttamente da un coefficiente di peso
- lo spostamento consentito sugli oggetti riguarda solo un moto traslatorio lungo l'asse y

Entrambe queste limitazioni possono essere rimosse senza eccessiva fatica.

Sollevamento dell'oggetto
Il modo più naturale per consentire all'utente di sollevare un oggetto è quello di utilizzare un meccanismo di drag and drop. Effettuando un drag verso l'alto è possibile trascinare l'oggetto lungo la stessa direzione. Ma come è possibile implementarlo dal punto di vista pratico?
Per prima cosa si deve notare che si tratta di eventi vrml. La pressione e il trascinamento del mouse avvengono all'interno della scena. Sarà dunque necessario implementare l'interfaccia EventOutObserver per monitorare il verificarsi di tali eventi.
Sino ad ora abbiamo analizzato solo un nodo che genera eventi in conseguenza della pressione del mouse: TouchSensor. Tale nodo non si presta però alla risoluzione del nostro problema (sebbene contenga un campo la cui funzione è quella di segnalare lo spostamento del mouse a pulsante premuto).
Utilizzeremo un nodo PlaneSensor. Un PlaneSensor, come un nodo TouchSensor, deve essere inserito all'interno del campo children nel quale l'oggetto è definito. Esso definisce un piano immaginario che è in grado di registrare i movimenti del mouse a pulsante premuto.

 

DEF oggetto Transform {

  children [

    Shape {

      appearance Appearance { material Material { diffuseColor 1 0 0 } }

      geometry Sphere {}

    }

    DEF sensore PlaneSensor {}

  ]

}

Il sensore manda eventi solo quando il mouse viene premuto con il cursore posizionato sopra l'oggetto definito all'interno del campo children (stesso meccanismo del nodo TouchSensor).
In occasione della pressione del mouse sopra l'oggetto viene emesso un evento isActive di valore TRUE. Spostamenti del cursore a pulsante premuto generano in continuazione eventi translation_changed di tipo SFVec3f. Noi siamo particolarmente interessati alla seconda componente del vettore emesso (quella relativa all'asse y, che indica quindi la traslazione verso l'alto imposta dal movimento del mouse).

Attraverso l'interfaccia EventOutObserver, possiamo fare in modo che l'applet java venga notificata del verificarsi di tali eventi (sfruttando il metodo advise visto nel terzo articolo). All'interno del metodo callback è dunque possibile accedere alla componente di traslazione rispetto all'asse y e quindi mapparla sulla posizione effettiva dell'oggetto.

Come simulare il diverso peso di un oggetto? L'idea l'ho presa da un libro di realtà virtuale di Joseph Gradecky, il quale implementava lo stesso meccanismo utilizzando il mitico Rend386. La maggiore forza richiesta nello spostamento di un oggetto pesante può essere simulata facendo in modo che a pari spostamento del mouse lo spostamento effettivo dell'oggetto sia minore. In pratica un oggetto leggero richiederà spostamenti minimi del cursore; viceversa uno pesante richiederà operazioni di drag molto più consistenti.
Per conseguire questo risultato non si fa altro che dividere la componente di traslazione lungo y per un coefficiente di peso. Pesi maggiori implicano minori spostamenti a pari traslazioni del cursore.

Perchè limitare gli spostamenti alla sola componente y? Perchè non considerare anche gli spostamenti lungo x e z e mapparli nello stesso modo?
Vi sono diversi motivi. In primo luogo perchè il nodo PlaneSensor nasconde un subdolo problema quando viene utilizzato per spostare oggetti. Se infatti ci si posiziona sul lato opposto del piano, spostamenti positivi lungo l'asse x sono in realtà negativi per l'oggetto. Questo richiede pertanto un certo processing da parte dell'applet java per gestire in modo corretto il segno. Inoltre si deve considerare la possibilità di collisione con altri oggetti posizionati sul piano, complicando di molto la gestione dell'ambiente (nell'esempio riportato si gestisce solo la collisione con il pavimento).
 

Caduta dell'oggetto
Una volta sollevato un oggetto, questo deve ricadere al suolo se l'utente rilascia il pulsante del mouse. L'evento di rilascio può essere rilevato semplicemente monitorando il campo isActive del PlaneSensor. In tale occasione infatti viene generato un evento di valore FALSE.
Una volta determinato che l'oggetto è stato rilasciato, si deve procedere ad attivare la fase di caduta. Questa deve seguire la legge di Newton, applicando all'oggetto una accelerazione pari a g (trascuriamo, per ovvi motivi, l'attrito esercitato dall'aria). Rispolverando qualche antica nozione di fisica imparata nel biennio, l'equazione per determinare lo spazio percorso dopo un certo tempo t dovrebbe essere la seguente: spazio = g * t * t.
La gestione del posizionamento dell'oggetto viene effettuata sempre da applet. Serve però un meccanismo per poter tenere traccia del trascorrere del tempo (il parametro t della formula). A tal fine è stato introdotto all'interno del mondo vrml un nodo TimeSensor, con campo loop posto a FALSE e un campo cycleInterval sufficientemente alto (poi vedremo perchè).
In occasione del rilascio del mouse (campo isActive del PlaneSensor a FALSE), si imposta il campo startTime del nodo TimeSensor all'etichetta temporale corrente (questo forza il sensore ad iniziare il ciclo temporale in questo preciso istante). Subito dopo si procede ad attivare il nodo TimeSensor settando a TRUE il campo enabled.
Dal momento dell'attivazione, il nodo TimeSensor inizia ad emettere eventi fraction_changed. Questi vengono recepiti dal metodo callback (ci si era posti in ascolto invocando il metodo advise sulla reference al relativo campo). All'interno del metodo callback è dunque possibile determinare la nuova posizione dell'oggetto mediante la seguente equazione:
posizione = altezza di partenza - g * t * t
Un semplice test è in grado di determinare l'avvenuta collisione dell'oggetto con il terreno. In tale occasione si deve disattivare il sensore di tempo. Si può dunque comprendere il motivo del settaggio del campo cycleInterval con un valore sufficientemente alto. Infatti si deve evitare il ritorno a zero del campo fraction_changed, situazione che porterebbe l'oggetto a ripartire dalla posizione iniziale.

A questo punto dovrebbe risultare abbastanza semplice comprendere il sorgente del file vrml e della classe Java che lo controlla. La demo prevede la presenza di due blocchi cubici posizionati sopra un piano. Quello di colore rosso risulta essere molto leggero (peso 1), mentre quello di colore blu ha un coefficiente di peso maggiore (peso 5).
 

Sorgente Vrml:

 

#VRML V2.0 utf8
 
 

# semplice mondo per dimostrare l'utilizzo di attrazione gravitazionale
 
 

#posizione iniziale dell'utente

Viewpoint {

  description "entrance"

  position 0 1 0

}
 
 

#definizione del background

Background {

        skyAngle [1 1.57 3.14]

        skyColor [0 0 1, 0 0 1, .6 .6 1, 0 0 1]

}
 
 
 
 

# piano a quota 0

Shape {

  appearance Appearance {

    material Material { diffuseColor 0 .5 0 }

  }

  geometry IndexedFaceSet {

    coord Coordinate {

      point [

        -100 0 100, 100 0 100, 100 0 -100, -100 0 -100

      ]

    }

    coordIndex [ 0,1,2,3,-1 ]

  }

}
 
 

# prima scatola (di colore rosso e leggera)

DEF Box1 Transform {

  translation 2 .5 -8

  children [

    Shape {

      appearance Appearance {

        material Material { diffuseColor 1 0 0 }

      }

      geometry Box { size 1 1 1 }

    }

    DEF sensore1 PlaneSensor {}

  ]

}
 
 

# seconda scatola (di colore blu e pesante)

DEF Box2 Transform {

  translation -2 .5 -8

  children [

    Shape {

      appearance Appearance {

        material Material { diffuseColor 0 0 1 }

      }

      geometry Box { size 1 1 1 }

    }

    DEF sensore2 PlaneSensor {}

  ]

}
 
 

# sensore di tempo per il controllo della caduta della prima scatola

DEF orologio1 TimeSensor {

        enabled FALSE

        loop FALSE

        cycleInterval 100

}
 
 

# sensore di tempo per il controllo della caduta della seconda scatola

DEF orologio2 TimeSensor {

        enabled FALSE

        loop FALSE

        cycleInterval 100

}

Il sorgente dell'applet Java è il seguente:
 

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 Gravita extends Applet implements EventOutObserver {
 
 

        // riferimento al browser vrml

        Browser browser;
 
 

        // riferimenti ai nodi delle box

        Node box1 = null;

        Node box2 = null;
 
 

        // campi per il loro posizionamento

        EventInSFVec3f translation_box1 = null;

        EventInSFVec3f translation_box2 = null;
 
 

        // riferimenti ai nodi che definiscono i sensori

        Node sensore1 = null;

        Node sensore2 = null;
 
 

        // eventi per il controllo delle azioni dell'utente

        EventInSFVec3f offset_sensor_1 = null;

        EventInSFVec3f offset_sensor_2 = null;

        EventOutSFVec3f hit_changed_1 = null;

        EventOutSFVec3f hit_changed_2 = null;

        EventOutSFBool isActive_1 = null;

        EventOutSFBool isActive_2 = null;
 
 

        // nodi per il sensore di tempo

        Node time_sensor1 = null;

        Node time_sensor2 = null;
 
 
 
 

        // eventi per la gestione dei nodi temporali (animazione di caduta)

        EventInSFBool enable_time1 = null;

        EventInSFTime startTime1 = null;

        EventOutSFFloat fraction_changed1 = null;

        EventInSFBool enable_time2 = null;

        EventInSFTime startTime2 = null;

        EventOutSFFloat fraction_changed2 = null;
 
 

        // altezze dei due blocchi

        float height1 = 0;
 
 

        float height2 = 0;
 
 

        // array contenenti le posizioni dei blocchi

        float value_changed1[] = new float[3];

        float value_changed2[] = new float[3];
 
 

        boolean error = false;
 
 

        // flag che indica se l'oggetto Š in fase di caduta.

        boolean inCaduta1 = false;

        boolean inCaduta2 = 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 {

                        // riferimenti a nodi e campi vrml
 
 

                        box1 = browser.getNode("Box1");

                        box2 = browser.getNode("Box2");
 
 

                        translation_box1 = (EventInSFVec3f) box1.getEventIn("set_translation");

                        translation_box2 = (EventInSFVec3f) box2.getEventIn("set_translation");
 
 

                        sensore1 = browser.getNode("sensore1");

                        sensore2 = browser.getNode("sensore2");
 
 

                        offset_sensor_1 = (EventInSFVec3f) sensore1.getEventIn("set_offset");

                        offset_sensor_2 = (EventInSFVec3f) sensore2.getEventIn("set_offset");

                        hit_changed_1 = (EventOutSFVec3f) sensore1.getEventOut("translation_changed");

                        hit_changed_2 = (EventOutSFVec3f) sensore2.getEventOut("translation_changed");

                        hit_changed_1.advise(this, new Integer(1));

                        hit_changed_2.advise(this, new Integer(2));

                        isActive_1 = (EventOutSFBool) sensore1.getEventOut("isActive");

                        isActive_2 = (EventOutSFBool) sensore2.getEventOut("isActive");

                        isActive_1.advise(this, new Integer(3));

                        isActive_2.advise(this, new Integer(4));
 
 

                        // sensori di tempo

                        time_sensor1 = browser.getNode("orologio1");

                        enable_time1 = (EventInSFBool) time_sensor1.getEventIn("enabled");

                        startTime1 = (EventInSFTime) time_sensor1.getEventIn("set_startTime");

                        fraction_changed1 = (EventOutSFFloat) time_sensor1.getEventOut("fraction_changed");

                        fraction_changed1.advise(this, new Integer(5));
 
 

                        time_sensor2 = browser.getNode("orologio2");

                        enable_time2 = (EventInSFBool) time_sensor2.getEventIn("enabled");

                        startTime2 = (EventInSFTime) time_sensor2.getEventIn("set_startTime");

                        fraction_changed2 = (EventOutSFFloat) time_sensor2.getEventOut("fraction_changed");

                        fraction_changed2.advise(this, new Integer(6));
 
 

               }

                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);

                }
 
 

                value_changed1[0] = 2;

                value_changed1[1] = .5f;

                value_changed1[2] = -8;

                value_changed2[0] = -2;

                value_changed2[1] = .5f;

                value_changed2[2] = -8;

        }
 
 

        // metodo invocato automaticamente al verificarsi di un evento vrml

        public void callback(EventOut evt, double when, Object which)

        {

                Integer whichNum = (Integer) which;

                float vector[] = new float[3];

                float time;
 
 

                // gestione dell'evento translation_changed dal nodo PlaneSensor

                // blocco rosso

                if (whichNum.intValue() == 1) {

                        if (inCaduta1) {

                                enable_time1.setValue(false);

                                offset_sensor_1.setValue(value_changed1);

                                height1 = value_changed1[1];

                                inCaduta1 = false;

                        }

                        vector = hit_changed_1.getValue();

                        value_changed1[1] = .5f + vector[1];

                        if (value_changed1[1] < .5f) value_changed1[1] = .5f;

                        height1 = value_changed1[1];

                        translation_box1.setValue(value_changed1);

                }
 
 

                // gestione dell'evento translation_changed dal nodo PlaneSensor

                // blocco blu

                if (whichNum.intValue() == 2) {

                        if (inCaduta2) {

                                enable_time2.setValue(false);

                                offset_sensor_2.setValue(value_changed2);

                                height2 = value_changed2[1];

                                inCaduta2 = false;

                        }

                        vector = hit_changed_2.getValue();

                        value_changed2[1] = .5f + vector[1] / 5;

                        if (value_changed2[1] < .5f) value_changed2[1] = .5f;

                        height2 = value_changed2[1];

                        translation_box2.setValue(value_changed2);

                }
 
 

                // isActive = false --> blocco 1 rilasciato

                if (whichNum.intValue() == 3) {

                        startTime1.setValue(when);

                        enable_time1.setValue(true);

                        inCaduta1 = true;

                }
 
 

                // isActive = false --> blocco 2 rilasciato

                if (whichNum.intValue() == 4) {

                        startTime2.setValue(when);

                        enable_time2.setValue(true);

                        inCaduta2 = true;

                }
 
 

                // evento fraction_changed per la caduta del blocco rosso

                if (whichNum.intValue() == 5) {

                   if (inCaduta1) {

                        time = fraction_changed1.getValue() * 100;

                        value_changed1[1] = height1 - 9.81f * time * time;

                        if (value_changed1[1] < .5f) {

                           //   enable_time1.setValue(false);

                                value_changed1[1] = .5f;

                                offset_sensor_1.setValue(value_changed1);

                                inCaduta1 = false;

                        }

                        translation_box1.setValue(value_changed1);

                   }

                }
 
 

                // evento fraction_changed per la caduta del blocco blu

                if (whichNum.intValue() == 6) {

                   if (inCaduta2) {

                        time = fraction_changed2.getValue() * 100;

                        value_changed2[1] = height2 - 9.81f * time * time;

                        if (value_changed2[1] < .5f) {

                             // enable_time2.setValue(false);

                                value_changed2[1] = .5f;

                                offset_sensor_2.setValue(value_changed2);

                                inCaduta2 = false;

                        }

                        translation_box2.setValue(value_changed2);

                   }

                }

        }

}

Come sempre, un file html dovrà includere il file vrml e l'applet all'interno della stessa pagina:
 

&ltHTML>

&ltHEAD>&ltTITLE&gtTest accelerazione gravitazionale</TITLE></HEAD>

&ltCENTER>

&ltEMBED="testgrav.wrl" WIDTH=600 HEIGHT=600>

&ltP>

&ltAPPLET CODE="Gravita.class" HEIGHT=10 WIDTH=400 mayscript>

</APPLET>

</HTML>

 

Questo esempio vuole mostrare come sia possibile programmare il comportamento degli oggetti all'interno di un mondo vrml in modo da rispecchiare precise proprietà fisiche: un passo avanti verso la creazione di mondi sempre più realistici e interattivi.
 

Semplice gioco: colpire un bersaglio con un cannone

 Il semplice gioco riportato a conclusione del presente articolo è una applicazione di quanto visto in precedenza.
L'utente può controllare un cannone virtuale, posizionato sopra un piano. Ad una certa distanza si trova un obiettivo da colpire. In mezzo è stato piazzato un muro come ostacolo. L'utente può controllare il cannone attraverso due parametri:
- inclinazione della canna
- forza di emissione della palla

La palla nel corso della sua traiettoria subisce attrazione gravitazionale (la legge è sempre la stessa): si tratta pertanto di trovare la giusta inclinazione e forza per potere ottenere una parabola vincente.

L'utente ha a disposizione infinite munizioni; un semplice messaggio in una TextArea comunica l'avvenuto centro o meno.

Questa demo è molto semplice e non richiede particolari spiegazioni aggiuntive.
Il controllo dell'inclinazione della canna del cannone è realizzato attraverso due pulsanti contenuti all'interno dell'area di visualizzazione java. All'interno del metodo action non si fa altro che settare nel modo corretto il campo rotation del nodo Transform che racchiude la canna.
Un ulteriore pulsante consente di esplodere un colpo. In funzione dell'orientamento correntemente assunto dalla canna del cannone, si procede a calcolare la velocità iniziale, scomponendola nelle sue componenti x e y. Mentre la componente x rimane invariata nel tempo (spazio = vel * t), la componente in y subisce una attrazione gravitazionale verso il basso; questo porterà la traiettoria ad assumere un provvidenziale andamento a parabola. Particolari controlli vengono effettuati in modo da considerare i casi di collisione con il terreno, con l'ostacolo o con l'obiettivo.
L'animazione della palla di cannone viene implementata sfruttando un meccanismo identico a quello visto per la caduta dei blocchi nell'esempio precedente: un sensore di tempo con campo cycleInterval sufficientemente alto e monitoraggio del campo fraction_changed per determinare il tempo trascorso (dato che tale campo è normalizzato a uno si deve provvedere a moltiplicarlo per il valore di fondo scala del campo cycleInterval).

Un aspetto interessante è rappresentato dalla creazione a run-time delle palle sparate. Quelle sparate in precedenza rimangono ferme sul terreno (non mi pare plausibile che scompaiano). Ogni nuova palla creata viene aggiunta ad un nodo ad hoc presente all'interno della scena vrml (nodo 'Palle').
 

Qui di seguito riporto i sorgenti del file vrml, della classe Java e del file html che li compone sulla stessa pagina.

 

#VRML V2.0 utf8
 
 

# posizione iniziale dell'utente

Viewpoint {

  position -10 1 40

  description "entrance"

}
 
 

# piano

DEF Piano Shape {

  appearance Appearance {

    material Material { diffuseColor 0 .5 0 }

  }

  geometry Box { size 1000, .1, 1000 }

}
 
 

# ostacolo

DEF Ostacolo Transform {

  translation 0 5 0

  children [

    Shape {

      appearance Appearance {

        material Material { diffuseColor 0 0 .5 }

      }

      geometry Box { size 1 10 10 }

    }

  ]

}
 
 

# cannone

DEF Cannone Transform {

  translation -35 0 0

  children [

    DEF ruota1 Transform {

      translation 0 1 -.75

      rotation 1 0 0 1.57

      children [

        DEF Ruota Shape {

          appearance Appearance {

            material Material { diffuseColor 1 0 0 }

          }

          geometry Cylinder {

            radius 1

            height .2

          }

        }

      ]

    }

    DEF ruota2 Transform {

      translation 0 1 .75

      rotation 1 0 0 1.57

      children [

        USE Ruota

      ]

    }

    DEF sostegno Transform {

      translation 0 1 0

      children [

        Shape {

          appearance Appearance {

            material Material { diffuseColor .3 .3 .3 }

          }

          geometry Box { size .3 .3 2 }

        }

      ]

    }

    DEF Canna Transform {

      translation .5 1.3 0

      rotation 0 0 1 1.57

      children [

        Shape {

          appearance Appearance {

            material Material { diffuseColor .5 .5 .5 }

          }

          geometry Cylinder {

                radius .35

                height 3.5

          }

        }

      ]

    }

  ]

}
 
 

# obiettivo

DEF Obiettivo Transform {

  translation 20 .5 0

  children [

    Shape {

      appearance Appearance {

        material Material { diffuseColor 1 1 0 }

      }

      geometry Box { size 2 1 2 }

    }

  ]

}
 
 

# nodo che conterr… tutte le palle sparate

DEF Palle Group {}
 
 

# sensore di tempo per controllare lo spostamento dei colpi

DEF Orologio TimeSensor {

        enabled FALSE

        loop TRUE

        startTime 1

        cycleInterval 100

}
 
 

# orologio di appoggio, utilizzato per catturare il tempo di attivazione

# questo tempo di attivazione viene usato come etichetta temporale

# per settare il campo startTime di Orologio.

DEF Orologio_appo TimeSensor {

        enabled FALSE

        loop TRUE

        startTime 1

        cycleInterval 1

}

Il sorgente della classe Java controllante è il seguente:
 

import java.awt.*;

import java.applet.*;

import java.lang.Math;

import vrml.external.field.*;

import vrml.external.Node;

import vrml.external.Browser;

import vrml.external.exception.*;

import netscape.javascript.JSObject;
 
 

public class Shoot extends Applet implements EventOutObserver {
 
 

        final int MAX_COLPI = 100;

        final float massa = 1;
 
 

        // riferimento al browser vrml

        Browser browser;
 
 

        // nodo deputato a contenere tutte le palle sparate

        Node Root;
 
 

        // nodo che controlla l'orientamento del cannone

        Node cannone;
 
 

        // nodo che controlla il trascorrere del tempo

        Node tempo;

        Node tempo_appo;
 
 

        // nodi rappresentanti le varie palle sparate

        Node palle[][] = new Node[MAX_COLPI][];
 
 

        // traslazione delle palle

        EventInSFVec3f[] trasl_palle = new EventInSFVec3f[MAX_COLPI];
 
 

        // campo controllante la rotazione del cannone

        EventInSFRotation rot_cannone = null;
 
 

        // campi per aggiunta e rimozione delle palle

        EventInMFNode addChildren_root = null;

        EventInMFNode removeChildren_root = null;
 
 

        // campi ed eventi di controllo del tempo

        EventInSFBool enable_tempo = null;

        EventOutSFFloat tempo_fraction = null;

        EventInSFTime startTime_tempo = null;
 
 

        EventInSFBool enable_tempo_appo = null;

        EventOutSFBool isActive_tempo_appo = null;
 
 

        float orient = 1.57f;
 
 

        // flag che indica se il colpo non Š ancora terminato (palla non ferma)

        boolean shooting = false;
 
 

        // array contenente la rotazione del cannone

        float orient_cannone[] = new float[4];
 
 

        TextField forza;
 
 

        float velx = 0;

        float vely = 0;

        float posx = 0;

        float posy = 0;

        float startx = 0;

        float starty = 0;

        float g = 9.81f;
 
 

        // numero di tentativi

        int tentativi = 0;
 
 

        // tempo inziale associato ad un colpo

        float tempo_iniziale = 0;
 
 

        // vettore di traslazione per una palla

        float trasl_palla[] = new float[3];
 
 

        // flag utilizzato per sapere se si ha colpito l'ostacolo

        boolean stopx = false;
 
 
 
 

        boolean error = false;
 
 

        TextArea output;
 
 
 
 
 
 

        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 {

                        // acquisizione di references relative a nodi e campi vrml
 
 

                        Root = browser.getNode("Palle");

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

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

                        cannone = browser.getNode("Canna");

                        rot_cannone = (EventInSFRotation) cannone.getEventIn("set_rotation");
 
 

                        tempo = browser.getNode("Orologio");

                        enable_tempo = (EventInSFBool) tempo.getEventIn("enabled");

                        startTime_tempo = (EventInSFTime) tempo.getEventIn("set_startTime");

                        tempo_fraction = (EventOutSFFloat) tempo.getEventOut("fraction_changed");

                        tempo_fraction.advise(this, new Integer(1));
 
 

                        tempo_appo = browser.getNode("Orologio_appo");

                        enable_tempo_appo = (EventInSFBool) tempo_appo.getEventIn("enabled");

                        isActive_tempo_appo = (EventOutSFBool) tempo_appo.getEventOut("isActive");

                        isActive_tempo_appo.advise(this, new Integer(2));
 
 

               }

                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);

                }
 
 

                orient_cannone[0] = orient_cannone[1] = 0;

                orient_cannone[2] = 1;

                orient_cannone[3] = orient;
 
 

                // layout dell'applet

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

                add(new Button("Cannon Up"));

                add(new Label("Force"));

                forza = new TextField("30.5");

                add(forza);
 
 

                add(new Button("Cannon Down"));

                output = new TextArea(4,50);

                add(output);

                add(new Button("Shoot!"));

        }
 
 

        // invocato automaticamente in conseguenza di un evento AWT

        public boolean action(Event evt, Object arg)

        {

                if (evt.target instanceof Button) {

                        String label = (String) arg;
 
 

                        if (label.equals("Cannon Up")) {

                                if (orient < 3.14f) {

                                        orient += 0.05f;

                                        orient_cannone[3] = orient;

                                        rot_cannone.setValue(orient_cannone);

                                }

                        }
 
 

                        if (label.equals("Cannon Down")) {

                                if (orient > 1.57f) {

                                        orient -= 0.05f;

                                        orient_cannone[3] = orient;

                                        rot_cannone.setValue(orient_cannone);

                                }

                        }
 
 

                        if (label.equals("Shoot!")) {

                                if (! shooting) {

                                        shooting = true;

                                        stopx = false;

                                        tentativi++;

                                        Spara();

                                }

                        }
 
 
 
 

                        return true;

                }

                else

                        return false;
 
 

        }
 
 
 
 

        // attiva l'animazione della palla sparata

        private void Spara()

        {

                String stringa = new String();

                float acc;
 
 

                posx = -34.5f + 1.75f * (float) Math.cos(orient-1.57);

                posy = 1f + 1.75f * (float) Math.sin(orient-1.57);

                startx = posx;

                starty = posy;

                trasl_palla[0] = posx;

                trasl_palla[1] = posy;

                trasl_palla[2] = 0;

                acc = getForza()/massa;

                if (acc > 200)  acc = 200;

                velx = acc * (float) Math.cos(orient-1.57);

                vely = acc * (float) Math.sin(orient-1.57);
 
 

                stringa = "Transform { children [ Shape { appearance "";

                stringa += "Appearance { material Material { diffuseColor 1 0 0 } } "";
 
 

                stringa += "geometry Sphere { radius .2 } } ] }"";

                palle[tentativi] = browser.createVrmlFromString(stringa);

                trasl_palle[tentativi] = (EventInSFVec3f) palle[tentativi][0].getEventIn("set_translation");

                trasl_palle[tentativi].setValue(trasl_palla);
 
 

                addChildren_root.setValue(palle[tentativi]);
 
 

                enable_tempo_appo.setValue(true);

        }
 
 

        // invocato automaticamente al verificarsi di un evento vrml

        public void callback(EventOut event, double when, Object which)

        {

                Integer whichNum = (Integer) which;

                float time_passed;
 
 

                // evento fraction_changed -> consente di determinare il trascorrere del tempo

                if (whichNum.intValue() == 1 && shooting) {

                                time_passed = tempo_fraction.getValue() * 100f;

                                if (! stopx)

                                        trasl_palla[0] = startx + velx * time_passed;

                                trasl_palla[1] = starty + (vely - g * time_passed) * time_passed;

                                trasl_palla[2] = 0;

                                if (trasl_palla[1] < 0 && shooting) {

                                        shooting = false;

                                        enable_tempo.setValue(false);

                                        trasl_palla[1] = 0;

                                        if (trasl_palla[0] > 19 && trasl_palla[0] < 21)

                                                output.appendText("\nColpo "" + tentativi + ":\nColpito!!!");

                                        else

                                                output.appendText("\nColpo "" + tentativi + ":\nMancato!");

                                }

                                if (trasl_palla[0] < .5f && trasl_palla[0] > -.5f) {

                                        if (trasl_palla[1] < 10.2f) {

                                                stopx = true;

                                        }

                                }

                                trasl_palle[tentativi].setValue(trasl_palla);

                }
 
 

                // rileva il tempo di sparo.

                if (whichNum.intValue() == 2 && isActive_tempo_appo.getValue() == true) {

                        enable_tempo_appo.setValue(false);

                        startTime_tempo.setValue(when);

                        enable_tempo.setValue(true);

                }

        }
 
 

        // lettura del campo relativo alla forza

        public float getForza()

        {

                Float value_read = new Float(0);

                String str = new String();
 
 

                str = forza.getText();

                return value_read.valueOf(str).floatValue();

        }
 
 

}

Infine, il file html risulta il seguente:
 

&ltHTML>

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

&ltCENTER>

&ltEMBED SRC="gravgame.wrl" height = 300 width = 600>

&ltP>

&ltAPPLET CODE="Shoot.class"  HEIGHT=100 WIDTH=400 mayscript>

&ltAPPLET>

</HTML>

 
 

Con questo esempio si chiude il quinto articolo della serie.
Come accennato in precedenza, il prossimo articolo sarà incentrato sulla spiegazione di come utilizzare il nodo Script per introdurre controllo da Java o Javascript.
I successivi articoli si focalizzeranno invece su alcuni progetti più complessi, tramite i quali si cercherà di evidenziare alcune delle caratteristiche del linguaggio vrml non ancora analizzate. Per quanto rigurda gli aspetti di interazione tra Java e Vrml sfruttando l'interfaccia EAI, direi che gli aspetti principali sono stati abbondantemente trattati. Ritengo pertanto che a questo punto dovreste essere in grado di realizzare applicazioni JAVA-VRML mediamente complesse. In tal caso, fatemelo sapere: ci conto!

Prima di chiudere, vi ricordo come sempre che presso il gruppo VRML DREAMERS (http://www.lucia.it/vrml) è disponibile una gran quantità di risorse su vrml. Tra le altre cose è presente un tutorial per esempi tenuto da Emiliano Pecis, incentrato sull'interfaccia EAI. Inoltre, ho da poco iniziato a scrivere brevi articoli su come introdurre particolari effetti all'interno di un mondo vrml (vedere 'effetti speciali vrml'): l'accelerazione gravitazionale era appunto il primo (i prossimi argomenti vanno dal morphing alla simulazione di nuvole, ecc.)
 


MokaByte Web  1998 - www.mokabyte.it

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