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. |