MokaByte Numero 27  -  Febbraio 1999
marco.gif (11435 bytes)
Java 3D API
III parte
  di 
Marco Molinari
Terza e ultima parte della serie su Java 3D
I behaviour e alcune considerazioni


 

Prosegue la descrizione delle basi di Java 3D; dopo aver visto come inserire in una scena un oggetto, come trasformarlo, come cambiarne le caratteristiche e come farlo muovere, vediamo adesso come interagire con esso


I behaviour

Come promesso in questo articolo vedremo in particolare i behaviour, le classi di Java 3D per gestire alcuni sofisticati processi in una scena 3D. Tutti i behaviour devono essere definiti dentro una regione, per gli stessi motivi delle luci: performance e comodità. I metodi fondamentali di un behaviour sono initialize() e processStimulus(); il primo viene chiamato solo una volta, all'inizio; di solito contiene le definizioni delle condizioni di WakeUp (lett. svegliarsi) e la specifica del primo criterio per il quale svegliarsi (tramite la funzione wakeupOn). Invece processStimulus viene chiamato unn volta che avviene il criterio specificato da un wakeupOn: di solito contiene quello che bisogna fare in quel caso e un nuovo wakeupOn. Senza il wakeupOn processStimulus verrebbe chiamato solo una volta (questo differenzia il meccanismo dei behaviour dagli event del jdk1.1, i cui metodi invece vengono attivati sempre senza bisogno di rispecificarlo ogni volta). Ci sono vari WakeUpCriterion:
    WakeupOnAWTEvent, quando avviene un evento dell'AWT
    WakeupOnBehaviourPost, quando un behaviour specificato lancia un preciso evento
    WakeupOnActivation e WakeupOnDeactivation, quando un behaviour entra o esce dai limiti di attivazione
    WakeupOnElapsedFrames, dopo che sono stati disegnati un certo numero di frames
    WakeupOnElapsedTime, dopo che è passato un certo tempo
    WakeupOnSensorEntry e WakeupOnSensorExit, il centro di un Sensor entra o esce una certa regione
    WakeupOnViewplatformEntry e WakeupOnViewplatformExit, il centro di una ViewPlatform entra o esce una certa regione
    WakeupOnTransformChange, quando un certo nodo viene sottoposto a una trasformazione
    WakeupOnCollisionEntry, WakeupOnCollisionExit e WakeupOnCollisionMovement, per la gestione delle collisioni 
I behaviour sono piuttosto flessibili e possono essere combinati con degli AND e OR logici, tramite quattro classi:
    WakeupAnd: una serie di criteri legati da un AND (WakeupCriterion && WakeupCriterion && ...)
    WakeupOr: una seria di criteri legati da un OR (WakeupCriterion || WakeupCriterion || ...)
    WakeupAndOfOrs: una serie di WakeupOr legati da un AND (WakeupOr && WakeupOr && ...)
    WakeupOrOfAnds: una serie di WakeupAnd legati da un OR (WakeupAnd || WakeupAnd || ...) 
Un esempio di behaviour potrebbe essere una tipica situazione da videogame: il giocatore deve schiacciare due pulsanti entro un certo tempo, una porta si apre e il giocatore deve correre prima che la porta si chiuda. In questo caso il criterio iniziale è la vicinanza al primo bottone; dopo che questo è scattato i criteri diventano una certa quantità di tempo o la vicinanza al secondo bottone; se finisce prima il quanto di tempo viene mandato un evento al primo bottone, che torna al suo posto, e si riprende dall'inizio; se invece viene premuto in tempo il secondo bottone si manda un evento alla porta (per farla aprire) e il criterio diventa solo una certa quantità di tempo, dopo la quale la porta si chiude.
Nell'esempio riportato qua sotto riprendiamo l'esempio della terra della volta scorsa e lo modifichiamo: aggiungiamo un'altra sfera (che vorrebbe essere la luna); inoltre facciamo in modo che con il tasto sinistro possiamo ruotare il sistema terra-luna (per poterlo vedere meglio) e con il tasto destro possiamo traslare solo la luna. Infine quando la luna toccherà la terra registreremo il tremendo impatto scrivendo in stardard output "boom". Qui a fianco si può vedere l'albero della scena, che andrebbe sempre prima costruito e che riassume bene quello che si vuole ottenere.albero.gif (4793 bytes)
Per ruotare e traslare degli oggetti bisognerebbe usare il criterio WakeupOnAWTEvent; quando avviene un evento di drag bisognerebbe calcolare lo spostamento avvenuto, mapparlo in un angolo di rotazione e applicare la rotazione al sistema interessato. Qualcosa di molto simile bisognerebbe fare con la traslazione, ma fortunatamente per entrambi i behaviour ci sono delle classi di comodo per ottenerli facilmente. Per la gestione della collisione invece è necessario scrivere un po' di codice (qui c'è il sorgente completo di tutto l'esempio).
 
class CollisionDetector extends Behavior {
  private boolean collisione = false;
  private Sphere sfera;
  private WakeupOnCollisionEntry entra;
  private WakeupOnCollisionExit esci;
  public CollisionDetector(Sphere s) {
    sfera = s;
    collisione = false;
  }
  public void initialize() {
    entra = new WakeupOnCollisionEntry(sfera);
    esci = new WakeupOnCollisionExit(sfera);
    wakeupOn(entra);
  }
  public void processStimulus(java.util.Enumeration criteria) {
    collisione = !collisione;
    if (collisione) {
      System.out.println("boom");
      wakeupOn(esci);
    }
    else {
      System.out.println("out");
      wakeupOn(entra);
    }
  }
}


Vediamo adesso il codice del metodo che modifichiamo rispetto agli altri articoli, createContentBranch.
 

BranchGroup createContentBranch() {
  BranchGroup objRoot = new BranchGroup();
  // il gruppo a cui attacchiamo sia la terra che la luna (ma non la luce)
  TransformGroup globalTrans = new TransformGroup();
  globalTrans.setCapability(TransformGroup.ALLOW_TRANSFORM_READ);
  globalTrans.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE);
  Transform3D t3d = new Transform3D();
  t3d.setScale(0.5);
  globalTrans.setTransform(t3d);
  objRoot.addChild(globalTrans);
  // i limiti di tutta la scena, usati sia dalla luce che dal gestore delle collisioni
  BoundingSphere worldBounds = new BoundingSphere(
     new Point3d(0.0, 0.0, 0.0), // Centro
                        1000.0); // Estensione
  // la terra
  Appearance app = new Appearance();
  app = new Appearance();
  Material material = new Material();
  app.setMaterial(material);
  Texture tex = new TextureLoader("earth.jpg", this).getTexture();
  app.setTexture(tex);
  Sphere earth = new Sphere(0.4f,Sphere.GENERATE_TEXTURE_COORDS | 
                            Sphere.GENERATE_NORMALS, 15, app);
  globalTrans.addChild(earth);
  // la luna
  Transform3D t = new Transform3D();
  t.setTranslation(new Vector3d(0.8, 0.0, 0.0));
  TransformGroup moonTrans = new TransformGroup(t);
  moonTrans.setCapability(TransformGroup.ALLOW_TRANSFORM_READ);
  moonTrans.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE);
  Sphere moon = new Sphere(0.1f,Sphere.GENERATE_NORMALS, 15, null);
  moonTrans.addChild(moon);
  globalTrans.addChild(moonTrans);
  // la luce
  PointLight light = new PointLight(
  new Color3f(1.0f, 1.0f, 1.0f),
  new Point3f(1.0f, 1.0f, 1.0f),
  new Point3f(1.0f, 0.0f, 0.0f) );
  light.setEnable(true);
  light.setInfluencingBounds(worldBounds);
  objRoot.addChild(light);
  // per ruotare il sistema
  MouseRotate behavior = new MouseRotate();
  behavior.setTransformGroup(globalTrans);
  globalTrans.addChild(behavior);
  behavior.setSchedulingBounds(worldBounds);
  // per muovere la luna
  MouseTranslate behavior3 = new MouseTranslate();
  behavior3.setTransformGroup(moonTrans);
  moonTrans.addChild(behavior3);
  behavior3.setSchedulingBounds(worldBounds);
  // per gestire la collisione
  CollisionDetector cd = new CollisionDetector(moon);
  cd.setSchedulingBounds(worldBounds);
  objRoot.addChild(cd);
  // per migliorare le prestazioni
 

  objRoot.compile();
  return objRoot;
}


Può essere interessante giocare con i behaviour e attaccarli ad altri oggetti, per esempio ruotando la luce (che adesso è di tipo Point), oppure solo la terra. Testando questo codice si nota presto una cosa: la collisione è estremamente imprecisa. Questo si può notare in tutti gli esempi che si trovano in rete. Ho cercato di minimizzare il problema passando l’oggetto da controllare come Node, in modo che si guardi semplicemente il centro dell’oggetto; in questo caso è plausibile farlo perchè la "luna" è abbastanza piccola. Passando al behaviour la Shape dell’oggetto (in modo che vengano utilizzati i limiti dell’oggetto, un’approssimazione della forma geometrica) si ottiene un effetto molto meno preciso, penso a causa di una non corretta implementazione del metodo getShape nella classe di comodo Sphere; in altri esempi che si trovano utilizzando getShape si ottengono risultati discreti (ma sempre chiaramente imprecisi). Si potrebbe utilizzare un metodo ancora più preciso, basato sull’effettivageometria, ma esso è estremamente lento.
 
 
 

Conclusione

C’è ancora molto da esplorare in Java3D, anche se in questi tre articoli abbiamo visto le cose fondamentali. 
Innanzitutto il Viewing Model: abbiamo sempre utilizzato SimpleUniverse, però il modello di Java 3D è sufficientemente flessibile da supportare sistemi di input sofisticati (per esempio i caschi per la realtà virtuale, anche con la visione in 3D separando le viste degli occhi) tramite la separazione tra il mondo virtuale, dove posizioniamo i nostri oggetti, e il mondo fisico, dove esistono i nostri occhi e lo strumento di output (che sia lo schermo del computer o qualcos’altro). A questo si legano i dispositivi di input, anch’essi in teoria supportati da Java3D, che possono essere qualcosa di più sofisticato di un mouse.
Un’altro aspetto di Java3D che non abbiamo visto è la gestione del suono. Come ci si può aspettare è supportato il suono in 3D che finalmente sta emergendo nei computer di massa, anche se non c’è ancora uno standard. L’API per gestirlo fa parte di Java3D e non di JavaSound proprio perchè pensata per i mondi virtuali.
E ancora si potrebbe parlare del depth cueing, che può servire a dare un effetto di nebbia ma anche ad accentuare l’effetto tridimensionale di una scena; delle (limitate) capacità di morphing, della gestione del background e di molto altro ancora.
Risulta penso chiaro che Java3D è un’API piuttosto estesa, flessibile e complessa. Proprio per aiutare chi non vuole realizzare scene troppo complesse sono apparse le classi di comodo che abbiamo visto spesso in questi articoli e che in effetti semplificano la vita. 
Java3D è adatta per progetti sofisticati, in cui strumenti principalmente descrittivi come il VRML non sono sufficienti (anche se bisogna tenere a mente che il VRML si può integrare con Java) e in cui si desidera utilizzare un’API a più alto livello rispetto a OpenGL e Direct3D. Però Java3D presenta un importante problema: la velocità di resa. 
Mentre scrivo è fuori il jdk1.2 finale (Java 2) e la versione finale di Java3D 1.1, e chiunque provi gli esempi più complessi su un computer "normale" si renderà conto che la velocità è davvero un problema. Questo è un peccato, proprio perchè i vantaggi architetturali di Java3D si sentono solo nei progetti più complessi. Il motto "write once, run anywhere" mi lascia perplesso, dato che attualmente Java 2 è disponibile solo per Win32 e per Solaris, e Java3D dipende da Java 2, mentre ad esempio il VRML è visualizzabile su praticamente tutte le piattaforme; Java 2 deve ancora uscire per molte piattaforme, e anche quando sarà fuori per avere Java3D bisognerà ancora aspettare. E’ solamente una mia opinione, sia ben chiaro, ma temo che Java3D risulti agli sviluppatori troppo complessa rispetto al VRML e troppo lenta rispetto alle API come OpenGL. 
Potrà HotSpot, la nuova virtual machine così promettente, così attesa ma così in ritardo, risolvere questo problema ?

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