Java 3D pro začátečníky
Úvod
Java 3D je přídavkem ke knihovnám jazyka Java a slouží k zobrazování trojrozměrné grafiky. Programy napsané v Java 3D mohou běžet na mnoha odlišných typech počítačů a přes internet.
Knihovna tříd Java 3D poskytuje rozhraní které je jednodušší než rozhraní mnoha jiných grafických knihoven, ale zároveň je dostačující k tvorbě dobrých her a animací. Java 3D staví na existujících technologiích jako je DirectX a OpenGL takže programy neběží tak pomalu jak bychom mohli čekat. Java 3D také umožňuje začlenit objekty vytvořené 3D modelovacími nástroji jako je TrueSpace nebo dokonce VRML modely.
Tento tutoriál je úvodem do knihovny Java 3D. Příklady vás provedou základními metodami tvorby 3D grafiky a animací. Nemusíte mít žádnou znalost 3D grafiky nebo samotné knihovny Java 3D, ale hodně vám pomůže pokud budete rozumět jazyku Java alespoň na základní úrovni. Programování ve třech rozměrech se může zdát komplikované kvůli množství cizích pojmů a matematiky, ale v tomto tutoriálu se budeme snažit zachovat věci co nejjednoduššími.
Instalace a spuštění Java 3D
Java 3D software je zdarma k dispozici od Sun Microsystems na j3d.java.net. Nejdříve budeme muset nainstalovat verzi Java 3D 1.3.1 a teprve poté aktuální verzi (v době překladu 1.5.0).
Po instalaci Java 3D můžete přeložit vaše programy tímto příkazem:
javac ClassFileName.java
A spustit je:
java ClassFileName
ClassFileName by mělo být shodné s názvem třídy definované v tomto souboru.
Začínáme - Náš první program
Následující program obsahuje základní kroky které potřebujeme k zobrazení 3D scény.
- Vytvoří virtuální vesmír obsahující naši scénu.
- Vytvoří datovou strukturu obsahující skupinu s objekty.
- Přidá objekt do skupiny.
- Umístí pozorovatele tak aby mohl vidět objekt na scéně.
- Přídá skupinu objektu do vesmíru.
Podívejme se na konstruktor třídy Hello3D a můžeme vidět pět řádků které dělají to co jsme si řekli. Program zobrazí kostku, přičemž pozorovatel se dívá přímo na červenou stěnu této kostky takže můžeme vidět červený čtverec na černém pozadí.
import com.sun.j3d.utils.universe.SimpleUniverse; import com.sun.j3d.utils.geometry.ColorCube; import com.sun.j3d.utils.geometry.Sphere; import javax.media.j3d.BranchGroup; public class Hello3D { public Hello3D() { SimpleUniverse universe = new SimpleUniverse(); BranchGroup group = new BranchGroup(); group.addChild(new ColorCube(0.3)); universe.getViewingPlatform().setNominalViewingTransform(); universe.addBranchGraph(group); } public static void main(String args[]) { new Hello3D(); } }
Příkazy import na začátku této třídy importují různé části knihovny Java 3D, takže přeložení a spuštění tohoto programu je dobrým důkazem toho že je vše v pořádku instalováno.
Posviťme si na svět
Dobře, náš první příklad byl dobrým začátkem, ale byl opravdu 3D? Pokud si myslíte že čtverec nepatří do 3D, tak si na to budeme muset v našem vesmíru posvítit. Způsob jakým světlo dopadá na objekty vytváří stíny a to nám pomáhá vidět objekty ve třech rozměrech.
Další příklad ilustruje jak zobrazit kouli osvícenou červeným světlem:
import com.sun.j3d.utils.geometry.*; import com.sun.j3d.utils.universe.*; import javax.media.j3d.*; import javax.vecmath.*; public class Ball { public Ball() { // Vytvoříme vesmír SimpleUniverse universe = new SimpleUniverse(); // Vytvoříme strukturu obsahující objekty BranchGroup group = new BranchGroup(); // Vytvoříme kouli a přidáme ji do skupiny objektů Sphere sphere = new Sphere(0.5f); group.addChild(sphere); // Vytvoříme červené světlo svítící 100m od počátku Color3f light1Color = new Color3f(1.8f, 0.1f, 0.1f); BoundingSphere bounds = new BoundingSphere(new Point3d(0.0,0.0,0.0), 100.0); Vector3f light1Direction = new Vector3f(4.0f, -7.0f, -12.0f); DirectionalLight light1 = new DirectionalLight(light1Color, light1Direction); light1.setInfluencingBounds(bounds); group.addChild(light1); // Nastavíme pohled na kouli universe.getViewingPlatform().setNominalViewingTransform(); // Vložíme skupinu s objekty do vesmíru universe.addBranchGraph(group); } public static void main(String args[]) { new Ball(); } }
Koule kterou jsme vytvořili je bílá (výchozí nastavení), ale vypadá jako červená díky světlu. Protože je to směrové světlo (objekt třídy DirectionalLight), musíme také určit jak daleko bude světlo svítit a v jakém směru. V našem programu světlo svítí na vzdálenost 100 metrů směrem doprava, dolů a do obrazovky, jak je definováno vektorem (4.0 doprava, -7.0 dolů a -12.0 do obrazovky).
Také můžeme vytvořit ambientní světlo (objekt třídy AmbientLight), které vlastně nesvítí žádným směrem, nebo kruhové (lampičkové) světlo (objekt třídy SpotLight) pokud si chceme posvítit jen na určité místo na scéně. Kombinace silného směrového světla a slabšího ambientního osvětlení dává scéně přiřozený vzhled. Světla v Java 3D nevrhají stíny (ale je možné je nastavit).
Rozmísťování objektů
Dosud jsme vytvářeli objekty na jednom stejném místě ve středu vesmíru. V Java 3D lze jednotlivá umístění popsat souřadnicemi x, y a z. Souřadnice se zvětšují směrem doprava na ose x, směrem nahoru na ose y a směrem do obrazovky na ose z.
Tomuto se říká pravoruký systém souřadnic protože můžeme použít palec a dva první prsty pravé ruky jako jednotlivé osy souřadnic. Všechny vzdálenosti jsou měřeny v metrech.
Při rozmísťování objektů na scéně začínáme v bodě (0, 0, 0) a poté je můžeme posouvat kam chceme. Pohybování objekty se říká transformace, takže třídy které budeme používat se jmenují TransformGroup a Transform3D. Objekty na scéně a objekty Transform3D přidáváme do TransformGroup předtím než přidáme samotný objekt TransformGroup na scénu.
- Vytvoříme transformaci, skupinu transformací a objekt:
- Určíme polohu objektu:
- Nastavíme transformaci aby posunula objekt na dané místo:
- Přidáme transformaci do skupiny transformací:
- Přidáme objekt do skupiny transformací:
Transform t = new Transform3D(); TransformGroup tg = new TransformGroup(); Cone c = new Cone(0.5f, 0.5f);
Vector3f v = new Vector3f(-2.f, 1.f, -4.f);
t.setTranslation(v);
tg.setTransform(t);
tg.addChild(c);
Může to vypadat komplikovaně, ale skupina transformací nám umožní seskupit objekty a pracovat s nimi jako s jedním velkým objektem. Například bychom mohli vytvořit stůl ze čtyř válců sloužících jako nohy a hranolu jako deska stolu. Když vložíme všechny tyto části do jedné skupiny transformací, můžeme posouvat celým stolem.
Třída Transform3D však zvládne mnohem víc než jen umístění objektu na dané souřadnice. Její metody včetně setScale() umožňující změnu velikosti objektu a rotX(), rotY() a rotZ() sloužící pro rotaci kolem os (proti směru hodinových ručiček).
Následující příklad zobrazuje na každé ose jiný objekt.
import com.sun.j3d.utils.geometry.*; import com.sun.j3d.utils.universe.*; import javax.media.j3d.*; import javax.vecmath.*; public class Position { public Position() { SimpleUniverse universe = new SimpleUniverse(); BranchGroup group = new BranchGroup(); // Osa x vyrobená z koulí for (float x = -1.0f; x <= 1.0f; x = x + 0.1f) { Sphere sphere = new Sphere(0.05f); TransformGroup tg = new TransformGroup(); Transform3D transform = new Transform3D(); Vector3f vector = new Vector3f( x, .0f, .0f); transform.setTranslation(vector); tg.setTransform(transform); tg.addChild(sphere); group.addChild(tg); } // Osa Y vyrobená z kuželů for (float y = -1.0f; y <= 1.0f; y = y + 0.1f) { TransformGroup tg = new TransformGroup(); Transform3D transform = new Transform3D(); Cone cone = new Cone(0.05f, 0.1f); Vector3f vector = new Vector3f(.0f, y, .0f); transform.setTranslation(vector); tg.setTransform(transform); tg.addChild(cone); group.addChild(tg); } // Osa Z vyrobená z válců for (float z = -1.0f; z <= 1.0f; z = z+ 0.1f) { TransformGroup tg = new TransformGroup(); Transform3D transform = new Transform3D(); Cylinder cylinder = new Cylinder(0.05f, 0.1f); Vector3f vector = new Vector3f(.0f, .0f, z); transform.setTranslation(vector); tg.setTransform(transform); tg.addChild(cylinder); group.addChild(tg); } Color3f light1Color = new Color3f(.1f, 1.4f, .1f); BoundingSphere bounds = new BoundingSphere(new Point3d(0.0,0.0,0.0), 100.0); Vector3f light1Direction = new Vector3f(4.0f, -7.0f, -12.0f); DirectionalLight light1 = new DirectionalLight(light1Color, light1Direction); light1.setInfluencingBounds(bounds); group.addChild(light1); universe.getViewingPlatform().setNominalViewingTransform(); // Vložíme skupinu transformací do vesmíru universe.addBranchGraph(group); } public static void main(String args[]) { new Position(); } }
Vzhled znamená vše
Existuje mnoho způsobů jak změnit vzhled objektů na scéně. Můžeme změnit jejich barvu, kolik světla odrážejí, můžeme na ně aplikovat dvojrozměrné obrázky nebo aplikovat textury na jejich povrch. Třída Appearance obsahuje metody které umožňují dělat takové změny. V této části si ukážeme jak tyto metody použít.
Nejjednodušším způsobem nastavení vzhledu je určit barvu objektu a metodu stínování. To funguje pro jednoduše, ale aby objekt vypadal realističtěji, musíme určit jak se bude chovat na světle. To uděláme vytvořením materiálu.
- Vytvoříme objekt:
- Vytvoříme vzhled:
- vytvoříme barvu:
- Určíme metodu obarvení:
- Přidáme atributy ke vzhledu:
- Nastavíme vzhled objektu:
Sphere s = new Sphere();
Appearance a = new Appearance();
Color3f c = new Color3f(0.f, 0.f, 1.f);
ColoringAttributes ca = new ColoringAttributes(c, ColoringAttributes.NICEST);
a.setColoringAttributes(ca);
s.setAppearance(a);
Materiály
Materiály mají pět vlastností kterými můžeme definovat vzhled objektu. Čtyři z nich jsou barvy: ambientní, emisivní, difuzní a barva odlesku. Pátou vlastností je lesk.
- Ambientní barva odráží světlo které bylo rozptýleno v prostředí tak, že není možné určit směr ze kterého přichází. Vytváří se pomocí AmbientLight.
- Emisivní barva je vidět i ve tmě. Lze ji použít například pro neonová světla.
- Difuzní barva odráží světlo určité barvy které přichází z jednoho směru, takže je jasnější čím blíž je objekt. Používá se s objektem třídy DirectionalLight.
- Odražené světlo přichází z určitého směru, a má tendenci se také tak odrážet. Lesklý kov nebo plast mají výrazný podíl této složky. Množství odraženého světla které uvidí pozorovatel zleží na pozici pozorovatele a úhlu odrazu světla.
Změna faktoru lesku ovlivní nejen lesk objektu, ale i velikost plochy odlesku.
Pro většinu objektů můžeme použít jednu barvu jak pro ambientní tak i difuzní složku a černou barvu pro emisivní složku (většina věcí ve tmě nesvítí). Pokud je to lesklý objekt, mohli bychom použít světlejší barvu pro odlesk. Například defince materiálu pro biliárovou kouli by mohla vypadat takto:
// biliárová koule // amb emi dif odl zář // Material mat = new Material(red, black, red, white, 70f);
Pro hopskuli bychom mohli použít černé nebo červené světlo k odrazu což by snížilo lesk koule. Snížení lesku ze 70 na 0 by nemuselo fungovat způsobem jaký bychom očekávali, jen by se rozšířila plocha odrazu po celém objektu místo toho aby se koncentrovala jen na jedno místo.
Textury
Materiály způsobí změnu celého objektu, ale občas i ten nejzářivější objekt může vypadat divně. Přidáním textury můžeme vyrobit zajímavější efekty jako třeba mramor nebo obalit celý objekt do dvourozměrného obrázku.
Třída TexturLoader nám umožnuje použít obrázek jako texturu. Rozměry obrázku musí být mocninami čísla 2, například 128 na 256 pixelů. Když načítáme texturu, můžeme také určit jak chceme obrázek použít. Například RGB načte obrázek barevně, zatímco LUMINANCE ho zobrazí černobíle.
Po načtení textury můžeme změnit vlastnosti objektu třídy TextureAttributes a říci zda chceme objekt nahradit tímto obrázkem nebo zda jej budeme modulovat barvou. Můžeme jej také aplikovat jako otisk nebo jej prolnout s jakoukoli barvou.
Pokud použijeme jednoduchý objekt jako je koule pak musíme také umožnit texturování nastavením "primitvních konstant". Ty můžeme nastavit jako Primitive.GENERATE_NORMALS + Primitive.GENERATE_TEXTURE_COORDS při vytváření objektu.
Teď to asi začíná znít trochu komplikovaně, takže si uvedeme příklad. Můžeme experimentovat s nastaveními v tomto příkladu a pak porovnávat výsledky. Obrázek použitý v příkladu lze nahradit svým vlastním:
import com.sun.j3d.utils.geometry.*; import com.sun.j3d.utils.universe.*; import com.sun.j3d.utils.image.*; import javax.media.j3d.*; import javax.vecmath.*; import java.awt.Container; public class TexturedBall { public TexturedBall() { // Vytvoříme vesmír SimpleUniverse universe = new SimpleUniverse(); // Vytvoříme "galaxii" BranchGroup group = new BranchGroup(); // Nastavíme barvy Color3f black = new Color3f(0.0f, 0.0f, 0.0f); Color3f white = new Color3f(1.0f, 1.0f, 1.0f); Color3f red = new Color3f(0.7f, .15f, .15f); // Nastavíme mapování textury TextureLoader loader = new TextureLoader("your_picture.jpg", "LUMINANCE", new Container()); Texture texture = loader.getTexture(); texture.setBoundaryModeS(Texture.WRAP); texture.setBoundaryModeT(Texture.WRAP); texture.setBoundaryColor(new Color4f(0.0f, 1.0f, 0.0f, 0.0f)); // Nastavíme atributy textury // Může být REPLACE, BLEND nebo DECAL místo MODULATE TextureAttributes texAttr = new TextureAttributes(); texAttr.setTextureMode(TextureAttributes.MODULATE); Appearance ap = new Appearance(); ap.setTexture(texture); ap.setTextureAttributes(texAttr); // Nastavíme materiál ap.setMaterial(new Material(red, black, red, black, 1.0f)); // Vytvoříme kouli na které demonstrujeme textury int primflags = Primitive.GENERATE_NORMALS + Primitive.GENERATE_TEXTURE_COORDS; Sphere sphere = new Sphere(0.5f, primflags, ap); group.addChild(sphere); // Vytvoříme světla Color3f light1Color = new Color3f(1f, 1f, 1f); BoundingSphere bounds = new BoundingSphere(new Point3d(0.0,0.0,0.0), 100.0); Vector3f light1Direction = new Vector3f(4.0f, -7.0f, -12.0f); DirectionalLight light1 = new DirectionalLight(light1Color, light1Direction); light1.setInfluencingBounds(bounds); group.addChild(light1); AmbientLight ambientLight = new AmbientLight(new Color3f(.5f,.5f,.5f)); ambientLight.setInfluencingBounds(bounds); group.addChild(ambientLight); // Pozorovatele umístíme přímo naproti kouli universe.getViewingPlatform().setNominalViewingTransform(); // Vložíme skupinu objektů do vesmíru universe.addBranchGraph(group); } public static void main(String args[]) { new TexturedBall(); } }
Můžeme také použít trojrozměrné textury použitím tvarů místo obrázků.
Speciální efekty
Inspiraci můžeme najít v příkladu který je v distribuci Java 3D a jmenuje se AppearanceTest. Například můžeme zobrazit objekty jako jako drátěné modely, zobrazit pouze rohy objektu atd. Také můžeme objekt i zprůhlednit pomocí následujících nastavení:
TransparencyAttributes t_attr = new TransparencyAttributes(TransparencyAttributes.BLENDED,0.5,
TransparencyAttributes.BLEND_SRC_ALPHA,
TransparencyAttributes.BLEND_ONE);
ap.setTransparencyAttributes(t_attr);
Java 3D a uživatelské rozhraní
Většina skutečných aplikací používá směsici troj a dvojrozměrných prvků. Tato sekce popisuje jak zkombinovat Java 3D se zbytkem našeho programu.
Canvas3D
Každá oblast kde lze vykreslovat torjrozměrnou grafiku se nazývá Canvas3D. To je vlastně obdélníkový pohled na objekty v našem vesmíru. Plátno se vloží do rámu, a pak se vytvoří vesmír který se zobrazí na plátně.
Následující příklad ukazuje jak vytvořit plátno v rámu s popisky nahoře a dole. Program můžeme spustit buď jako applet nebo aplikaci:
import com.sun.j3d.utils.universe.SimpleUniverse; import com.sun.j3d.utils.geometry.ColorCube; import javax.media.j3d.BranchGroup; import javax.media.j3d.Canvas3D; import java.awt.GraphicsConfiguration; import java.awt.BorderLayout; import java.awt.Label; import java.applet.Applet; import com.sun.j3d.utils.applet.MainFrame; public class CanvasDemo extends Applet { public CanvasDemo() { setLayout(new BorderLayout()); GraphicsConfiguration config = SimpleUniverse.getPreferredConfiguration(); Canvas3D canvas = new Canvas3D(config); add("North",new Label("This is the top")); add("Center", canvas); add("South",new Label("This is the bottom")); BranchGroup contents = new BranchGroup(); contents.addChild(new ColorCube(0.3)); SimpleUniverse universe = new SimpleUniverse(canvas); universe.getViewingPlatform().setNominalViewingTransform(); universe.addBranchGraph(contents); } public static void main(String args[]) { CanvasDemo demo = new CanvasDemo(); new MainFrame(demo,400,400); } }
Java 3D a Swing
Canvas3D využívá výhod naší grafické karty ke zvýšení výkonu. Naneštěstí to znamená že to nefunguje příliš dobře se Swing GUI (situace se mohla změnit protože došlo k vývoji Swingu a zejména zlepšení výkonu grafického potrubí). Tyto komponenty se nazývají "odlehčené". Odlehčené komponenty mohou být skryty Canvas3D i když by měly být viditelné.
Pro tento problém existuje několik řešení:
- Můžeme použít jak odlehčené tak těžkotonážní komponenty na jedné obrazovce pokud budou v oddělených kontajnerech.
- Pokud použijeme vyskakovací menu, pak statická metoda JPopupMenu řeší tento problém: setDefaultLightWeightPopupEnabled(false);
- Můžeme použít starší komponenty AWT místo komponent Swing.
Animace a interakce - odrážející se kulička
K vytvoření animace potřebujeme pohybovat objekty mezi každým snímkem animace. Můžeme použít časovač a pohybovat 3D objekty v malých časových rozmezích. Také můžeme objekty upravovat jinými způsoby, další příklad mění velikost kuličky takže se deformuje při dopadu.
Pro interakci s uživatelem můžeme zpracovávat stisky kláves na tlačítcích a jiných komponentách.
Jedna věc které bychom si měli všimnout je že musíme Java 3D oznámit že budeme něčím hýbat pomocí nastavení schopnosti. Jinak ničím nepohneme.
objTrans = new TransformGroup();
objTrans.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE);
Následující příklad tyto techniky kombinuje. Začínáme kliknutím na tlačítko, pak kulička začne skákat nahoru a dolů a můžeme stisknout klávesu a nebo s abychom usměrnili pohyb kuličky doleva nebo doprava.
import java.applet.Applet; import java.awt.*; import java.awt.event.*; import java.awt.event.WindowAdapter; import com.sun.j3d.utils.applet.MainFrame; import com.sun.j3d.utils.universe.*; import javax.media.j3d.*; import javax.vecmath.*; import com.sun.j3d.utils.geometry.Sphere; import javax.swing.Timer; public class BouncingBall extends Applet implements ActionListener, KeyListener { private Button go = new Button("Go"); private TransformGroup objTrans; private Transform3D trans = new Transform3D(); private float height = 0.0f; private float sign = 1.0f; private Timer timer; private float xloc = 0.0f; public BranchGroup createSceneGraph() { // Vytvoříme kořen grafu větve BranchGroup objRoot = new BranchGroup(); objTrans = new TransformGroup(); objTrans.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE); objRoot.addChild(objTrans); // Vytvoříme graf listu a vložíme ho do grafu větve Sphere sphere = new Sphere(0.25f); objTrans = new TransformGroup(); objTrans.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE); Transform3D pos1 = new Transform3D(); pos1.setTranslation(new Vector3f(0.0f,0.0f,0.0f)); objTrans.setTransform(pos1); objTrans.addChild(sphere); objRoot.addChild(objTrans); BoundingSphere bounds = new BoundingSphere(new Point3d(0.0,0.0,0.0), 100.0); Color3f light1Color = new Color3f(1.0f, 0.0f, 0.2f); Vector3f light1Direction = new Vector3f(4.0f, -7.0f, -12.0f); DirectionalLight light1 = new DirectionalLight(light1Color, light1Direction); light1.setInfluencingBounds(bounds); objRoot.addChild(light1); // Nastavíme ambientní světlo Color3f ambientColor = new Color3f(1.0f, 1.0f, 1.0f); AmbientLight ambientLightNode = new AmbientLight(ambientColor); ambientLightNode.setInfluencingBounds(bounds); objRoot.addChild(ambientLightNode); return objRoot; } public BouncingBall() { setLayout(new BorderLayout()); GraphicsConfiguration config = SimpleUniverse.getPreferredConfiguration(); Canvas3D c = new Canvas3D(config); add("Center", c); c.addKeyListener(this); timer = new Timer(100, this); Panel p = new Panel(); p.add(go); add("North", p); go.addActionListener(this); go.addKeyListener(this); // Vytvoříme jednoduchou scénu a vložíme ji do vesmíru BranchGroup scene = createSceneGraph(); SimpleUniverse u = new SimpleUniverse(c); u.getViewingPlatform().setNominalViewingTransform(); u.addBranchGraph(scene); } public void keyPressed(KeyEvent e) { if (e.getKeyChar() == 's') { xloc = xloc + .1f; } if (e.getKeyChar()== 'a') { xloc = xloc - .1f; } } public void keyReleased(KeyEvent e) { } public void keyTyped(KeyEvent e){ } public void actionPerformed(ActionEvent e ) { // Spustíme časovač if (e.getSource() == go) { if (!timer.isRunning()) { timer.start(); } } else { height += .1 * sign; if (Math.abs(height *2) >= 1) sign = -1.0f * sign; if (height<-0.4f) { trans.setScale(new Vector3d(1.0, .8, 1.0)); } else { trans.setScale(new Vector3d(1.0, 1.0, 1.0)); } trans.setTranslation(new Vector3f(xloc,height,0.0f)); objTrans.setTransform(trans); } } public static void main(String args[]) { System.out.println("Program Started"); BouncingBall bb = new BouncingBall(); bb.addKeyListener(bb); MainFrame mf = new MainFrame(bb, 256, 256); } }
Více informací
Tento tutoriál byl přeložen ze stránek java3d.org patřící Gregu Hopkinsovi. Stránky autora tohoto překladu jsou na dredwerkz.