MokaByte Numero 25  -  Dicembre 98
 
Java 3D
di 
Marco Molinari
Le nuove avanzatissime API per la modellazione 3D



Dopo il seminario di Sun, relazionato il mese scorso, ci avventuriamo adesso nella pratica di Java3D. Lo scopo di questo articolo è di iniziare a programmare con Java3D, scopredone i principi fondamentali. Vedremo come visualizzare un mondo, come aggiungere oggetti, come trasformarli (scalandoli, ruotandoli e traslandoli) e come colorarli. Il lettore si rassegni: alla fine dell'articolo non avremo un mondo virtuale in cui passeggiare, ma solo qualche cubo ruotato e una scritta. In compenso nel prossimo articolo vedremo delle caratteristiche un po' più avanzate dell'API, dando agli oggetti delle texture, muovendo gli oggetti e permettendo all'utente di clickare sul cubo per ottenere una forma di interazione.



Il concetto fondamentale con Java3D è quello del "scene graph": un albero a cui si appendono tutti gli oggetti della scena 3D, intendendo per oggetti non solo quelli da visualizzare, ma anche le istruzioni per visualizzarli. Lo scene graph viene percorso da Java3D per trovare i dati per creare l'immagine, perciò qualsiasi cosa che si desideri abbia un'influenza sulla propria scena va messa nel scene graph. In Java3D questo albero ha due rami (branches) principali: il content branch e il view branch. Il primo contiene gli oggetti da visualizzare, il loro colore, dove sono messi nello spazio e altro ancora; il secondo contiene tutto quello che riguarda "il vedere" la scena, ad esempio il punto di vista e i dispositivi di input. In pratica per i progetti più semplici il ramo interessante è il primo, e invece di costruirsi il secondo si può utilizzare una classe di comodo fornita da Sun (SimpleUniverse in com.sun.j3d.utils.universe.*). Tutto quello che sta dentro lo scene graph è un SceneGraphObject; la gerarchia procede così:

 

SceneGraphObject

  Node

    Group - sia per raggruppare gli oggetti che per trasformarli, e altro

    Leaf - gli oggetti, il background, le luci, i suoni e molto altro

  NodeComponent - caratteristiche dei nodi: ad esempio l'apparenza degli oggetti
 
 
 
 
 
 
 
 
 

Alcuni oggetti non sono dentro la gerarchia: un esempio che vedremo in questo articolo è Transform3D.
Non si confonda la gerarchia degli oggetti con la gerarchia del scene graph. Per il nostro primo esempio dichiareremo esplicitamente tutto quanto, compreso il view branch. Il nostro scopo sarà di visualizzare qualche cosa, in particolare un cubo. Creeremo l'albero, il content branch e il view branch; una volta "attaccati" all'albero, i contenuti dei due branch saranno visualizzati. Nell'esempio utilizziamo la classe ColorCube, di com.sun.java.geometry, che definisce appunto un cubo colorato.
 
 
 
 
 
 
 
 

import java.awt.*;

import java.awt.event.*;

import javax.media.j3d.*;

import javax.vecmath.*;

import com.sun.j3d.utils.geometry.*;

public class Cubo1 extends Frame {

  public static void main(String args[]) {

    new Cubo1();

  }

  public Cubo1() {

 // la finestra avrà "Cubo1" come titolo e sarà di 400x300

  super("Cubo1");

    setSize(400,300);

 // Canvas3D estende Component quindi è heavyweight; in esso

 // Java3D disegna la scena; lo piazziamo in mezzo alla finestra

    Canvas3D canvas = new Canvas3D(null);

    add("Center", canvas);

 // da qui in avanti conviene seguire il codice sulla immagine

 // del scene graph.

 // VirtualUniverse è il fondamento di qualsiasi scena di Java3D

 // Una volta creato gli si attacca uno o più oggetti Locale

    VirtualUniverse virtualUniverse = new VirtualUniverse();

  Locale locale = new Locale(virtualUniverse);

 // creiamo il lato sinistro dell'albero, il view branch; è piuttosto

 // complesso e permette di fare cose sofisticate. Però per un

 // scena semplice è solo una scomodità, ecco perchè c'è

 // SimpleUniverse

  BranchGroup viewBranchGroup = new BranchGroup();

  TransformGroup viewTransformGroup = new TransformGroup();

  ViewPlatform viewPlatform = new ViewPlatform();

  viewTransformGroup.addChild(viewPlatform);

  viewBranchGroup.addChild(viewTransformGroup);

  locale.addBranchGraph(viewBranchGroup);

 /* View contiene i parametri per visualizzare la scena da questo

 *  punto di vista

 *  ad esso attacchiamo:

 *    il Canvas3D

 *   un PhysicalBody, che contiene i dati della "testa" dell'osservatore

 *    (la posizione degli occhi e delle orecchie, per esempio)

 *   un setPhysicalEnvironment, che specifica i dispositivi di imput

 *    e di output

 */

  View myView = new View();

  myView.addCanvas3D(canvas);

  myView.setPhysicalBody(new PhysicalBody());

  myView.setPhysicalEnvironment(new PhysicalEnvironment());

  myView.attachViewPlatform(viewPlatform);

 // adesso creiamo l'altro ramo, che contiene il cubo

  BranchGroup contentBranchGroup = new BranchGroup();

  TransformGroup contentTransformGroup = new TransformGroup();

  Shape3D cubo = new ColorCube(0.2);

  contentTransformGroup.addChild(cubo);

  contentBranchGroup.addChild(contentTransformGroup);

  setVisible(true);

  addWindowListener(new WindowAdapter()

      {public void windowClosing(WindowEvent e)

         {dispose(); System.exit(0);}

      }

    );

  }

}
 


 
 
 
 
 
 

Compiliamo, facciamo partire ed ecco che, magicamente, lo schermo rimane nero... 
Il problema è che l'oggetto e il punto di vista sono molto vicini: per l'esattezza il punto di vista è dentro l'oggetto, e in questo caso Java3D non disegna l'oggetto. E' necessario quindi traslare l'oggetto, ma prima di trattare le trasformazioni vediamo come è il codice usando SimpleUniverse; già che ci siamo utilizziamo un metodo predefinito per poter vedere qualcosa. Ecco il sorgente:
 

 
 
 
 
 
 
 

import java.awt.*;

import java.awt.event.*;

import javax.media.j3d.*;

import com.sun.j3d.utils.geometry.*;

import com.sun.j3d.utils.universe.*;

public class Cubo2 extends Frame {

  public static void main(String args[]) {

    new Cubo2();

  }

  public Cubo2() {

  super("Cubo2");

    setSize(400,300);

    Canvas3D c = new Canvas3D(null);

    add("Center",c);

 // SimpleUniverse comprende un Virtual Universe con un view branch

 // completo

  SimpleUniverse u = new SimpleUniverse(c);

 // con questo metodo spostiamo tutto "all'indietro" per poter vedere

 // qualcosa

  u.getViewingPlatform().setNominalViewingTransform();

 // adesso creiamo l'altro ramo, che contiene il cubo

 // è dentro una procedura così in futuro modificheremo solo quella

  BranchGroup contentBranchGroup = createContentBranch();

  u.addBranchGraph(contentBranchGroup);

  setVisible(true);

  addWindowListener(new WindowAdapter()

   {public void windowClosing(WindowEvent e)

    {dispose(); System.exit(0);}

   }

  );

 }

 BranchGroup createContentBranch() {

  BranchGroup contentBranchGroup = new BranchGroup();

  TransformGroup contentTransformGroup = new TransformGroup();

 // il cubo lo istanziamo un po' più piccolo

  Shape3D cubo = new ColorCube(0.2);

  contentTransformGroup.addChild(cubo);

  contentBranchGroup.addChild(contentTransformGroup);

  return contentBranchGroup;

 }

}

Finalmente un bel quadrato rosso appare sullo schermo ! Il cubo non è ruotato, e quindi appare solo una faccia.
 

Le trasformazioni

Per ruotare, traslare o cambiare le dimensioni di un oggetto bisogna appoggiarsi alla classe TrasformGroup; questa permette di costruire un nuovo sistema di coordinate cartesiane rispetto a un'altro sistema, fatto importante perchè le trasformazioni si accumulano andando dalla radice alle foglie dello scene graph. Le trasformazioni applicate al TransformGroup vengono applicate anche a tutti i suoi figli.

Per specificare come trasformare un gruppo, si devono utilizzare delle matrici di 4x4; queste si possono definire a mano oppure utilizzando due oggetti di comodo, Matrix4D e Transform3D. Quest'ultimo è sicuramente il più comodo.

In questo terzo esempio è necessario importare anche javax.vecmath.* che contiene le classi che gestiscono i vettori. Qui c'è il sorgente; vediamo solo il metodo createContentBranch():

 

 BranchGroup createContentBranch() {

  BranchGroup contentBranchGroup = new BranchGroup();

 // Ecco la parte nuova: definiamo un Transform3D

  Transform3D cubeTransform = new Transform3D();

 // Trasliamo verso l'alto

  cubeTransform.setTranslation(new Vector3f(0.0f,0.3f,0.0f));

 // Ruotiamo: si noti come usando rotX e rotY si setti la completa

 // trasformazione, ovvero si cancellano le trasformazioni precedenti.

 // Questo si può risolvere usando una trasformazione temporanea

 // e quindi moltiplicandola (mul) a quella originale, oppure

 // usando setRotation, che però prende come parametro di ingresso

 // una matrice; al contrario setTranslation usato prima non annulla

 // le trasformazioni precedenti

  Transform3D tempTransform = new Transform3D();

  tempTransform.rotY(Math.PI/6.0d);

  cubeTransform.mul(tempTransform);

  tempTransform.rotX(Math.PI/4.0d);

  cubeTransform.mul(tempTransform);

 // nella versione precedente il TransformGroup usato era quello standard

  TransformGroup contentTransformGroup = new TransformGroup(cubeTransform);

  Shape3D cubo = new ColorCube(0.2);

  contentTransformGroup.addChild(cubo);

  contentBranchGroup.addChild(contentTransformGroup);

  return contentBranchGroup;

 }

Adesso il cubo ha un aspetto decisamente più tridimensionale.
 
 
 
 
 
 
 
 
 

Cercasi attori

Stufi del cubo, andiamo alla ricerca di altri oggetti da visualizzare. Si sarà notato che il ColorCube non è compreso nella gerarchia javax.media.j3d.* ma in com.sun.j3d.utils.geometry.*; questo perchè i progettisti hanno preferito lasciare l'API più leggera possibile, e hanno fornito le classi utili in una gerarchia separata. Tra queste ci sono anche, in com.sun.j3d.utils.geometry.*, il cono, il cilindro, la sfera e altre cose utili. Ma per avere oggetti più complessi, dovremo guardare altrove. Ottenere un testo estrudendo una scritta sull'asse Z non è difficile; ad esempio

 

  Font font = new Font("TimesRoman",Font.PLAIN,10);

  Font3D font3D = new Font3D(font,new FontExtrusion());

  Text3D text = new Text3D(font3D, "Mokabyte");

  Shape3D shape = new Shape3D(text, new Appearance());

L'oggetto shape che si ottiene è equivalente al ColorCube, e va "appeso" all'albero (per esempio a un TransformGroup). Un errore comune all'inizio è non mettere bene nel mondo l'oggetto; magari lo scene graph è corretto, ma l'oggetto è fuori dal campo visivo e quindi non si vede nulla.
Per oggetti ottenere oggetti ancora più complessi bisogna appoggiarsi agli oggetti che costruiscono forme basandosi su array di punti e linee. Ce ne sono molti (LineArray, TriangleArray, TriangleFanArray e altri ancora). Si guardi ad esempio come si può costruire un cubo. In realtà nessuno si aspetta che si utilizzino direttamente questi metodi per costruire un oggetto: essi servono per creare dei caricatori di forme dei formati diffusi tra i programmi di rendering 3d. Forniti con Java3D ci sono i loader per gli oggetti di Lightwave e Wavefront, anche questi tra le classi di utilità.
 
 
 
 
 
 
 

L'apparenza delle cose

Capito come ottenere altri oggetti, vediamo come dar loro un certo aspetto. Nel creare il testo alla fine abbiamo specificato, nel costruttore di Shape3D, una new Appearance. E' proprio questa classe che va modificata per ottenere qualche attributo: si può controllare il colore, la trasparenza, il modello da utilizzare (piatto o Gouraud); e ancora il modo in cui l'oggetto viene disegnato, ad esempio con o senza antialiasing. Ad esempio (ecco il sorgente completo):

 

  Appearance look = new Appearance();

  ColoringAttributes ca = new ColoringAttributes();

  ca.setColor(255.0f, 0.0f, 0.0f);

  look.setColoringAttributes(ca);

  PolygonAttributes pa = new PolygonAttributes();

  pa.setPolygonMode(PolygonAttributes.POLYGON_LINE);

  look.setPolygonAttributes(pa);

  Shape3D myShape3D = new Shape3D(myText3D, look);

in questo modo del testo si vedranno solo gli spigoli rossi dei triangoli che lo conpongono. Se invece vogliamo un oggetto con un po' di ombreggiatura, dovremo utilizzare l'attributo Material, che specifica il colore dove la luce punta, la lucentezza e altro ancora. Inoltre è qui che si mette una texture su un oggetto, ovvero si "appiccica" un'immagine all'oggetto.
C'è ancora moltissimo di cui si potrebbe scrivere: luci, collisioni, movimento, interazione; e si potrebbe proseguire trattando dei suoni, dei dispositivi di input e di altre cose del view branch. Nei prossimi articoli descriverò alcune di queste caratteristiche, ma in Java3D l'imperativo, data la sua vastità, è sperimentare.
 

Bibliografia:

Nota: negli esempi che si trovano in rete viene utilizzata spesso la classe MainFrame: è una classe di comodità di com.sun.j3d.utils.* per far sì che un'altra classe in essa contenuta possa essere utilizzata sia come applet che come applicazione. 
Le immagini del view branch e del content branch sono prese da "Introduction to Programming with Java3D": Parts of these tutorial notes are copyright (c) 1998 by Henry A. Sowizral, copyright (c) 1998 by David R. Nadeau, copyright (c) 1998 by Michael J. Bailey, and copyright (c) 1998 by Michael F. Deering. Users and possessors of these tutorial notes are hereby granted a nonexclusive, royalty-free copyright and design patent license to use this material in individual applications. License is not granted for commercial resale, in whole or in part, without prior written permission from the authors. This material is provided "AS IS" without express or implied warranty of any kind.

 
 
 
 
 

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