Bachelorarbeit im PDF-Format
Transcription
Bachelorarbeit im PDF-Format
Department für Informatik Universität Fribourg, Schweiz http://diuf.unifr.ch/ Einführung in die Echtzeit-3D-Grafik mit Java Bachelorarbeit Daniel Egger September 2005 Unter der Aufsicht von: Prof. Dr. J. PASQUIER -ROCHA und Dr. P. F UHRER Software Engineering Group “Mighty is geometry. When joined with art, resistless.” - Euripides i ii Zusammenfassung Das Ziel dieser Bachelorarbeit ist es, wie der Titel eigentlich schon treffend ausdrückt, eine Einführung in die Echtzeit-3D-Graphik mit Java zu geben. Als erster Schritt wurden dazu die verfügbaren Optionen untersucht und angeschaut. Nach Auswahl der Java 3D-Engine jMonkey Engine auch jME genannt [36] wurden deren Einsaztmöglichkeiten getestet. Im dritten und letzten Teil der Arbeit wurde ein Tutorial geschrieben, um den Lesern zu zeigen, wie man mit Hilfe von jME Echtzeit-3D-Grafik auf den Bildschirm bringen kann. Es ist vor allem das Resultat dieses Tutorials, was man im Rapport finden kann. Schon diese Einleitung beinhaltet einige Wörter, die den einen oder anderen Leser vielleicht bereits verwirren, ich hoffe aber im Verlauf des Textes die meisten Unklarheiten klären zu können. Es wird aber vorausgesetzt das die Leser zumindest einige Grundkenntnisse in Java haben oder zumindest gute Kenntnisse in einer anderen objektorientierten Programmiersprache. Falls sie noch einige Probleme mit Java haben, kann ich das Buch Thinking in Java von Bruce Eckel[Eck02] empfehlen, das mir einen hervorragenden Einstieg in die objektorientierte Programmierung mit Java bereitet hat. Schlüsselwörter: 3D-Graphik, 3D-Engine, Echtzeit-Rendering, Java, jMonkey Engine, OpenGL, Inhaltsverzeichnis I. Einleitung 2 1. Bachelorprojekt 3 1.1. Beschreibung der Aufgabe . . . . . . . . . 1.2. Projektablauf . . . . . . . . . . . . . . . . 1.2.1. 1. Schritt: Auswahl einer 3D-Engine 1.2.2. 2. Schritt: Machbarkeitstest . . . . 1.2.3. 3. Schritt: Tutorial . . . . . . . . . 1.3. Gliederung der Dokumentation . . . . . . . 1.4. Konventionen und Notationen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8 . 10 . 13 . 13 2. 3D-Grafik 2.1. 2.2. 2.3. 2.4. II. Was ist 3D-Grafik? . . . . . . . . Was bedeutet Echtzeit-3D-Grafik? Was ist eine 3D-Engine? . . . . . Zusammenfassung . . . . . . . . 3 3 3 6 7 7 7 8 . . . . . . . . . . . . . . . . . . . . Tutorial 14 3. Erste Schritte mit jME 3.1. Unser erstes jME-Programm . . . . . 3.2. Scenegraph . . . . . . . . . . . . . . 3.2.1. Was ist ein Scenegraph? . . . 3.2.2. Zustände im Kontext von jME 15 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4. Wie funktioniert 3D-Grafik 15 18 18 21 22 4.1. Was macht eine 3D-Engine? . . . . . . . 4.2. Das 3D-Koordinatensystem . . . . . . . . 4.2.1. 2D-Koordinatensystem . . . . . . 4.2.2. 3D-Koordinatensystem . . . . . . 4.2.3. Model-Space vs. World-Space . . 4.3. Transformationen im Raum . . . . . . . . 4.3.1. Verschiebungen (engl. translation) 4.3.2. Rotationen (engl. rotation) . . . . 4.3.3. Skalierungen (engl. scaling) . . . 4.3.4. Alle Bewegungen zusammen . . . 4.4. Perspektive und Projektion . . . . . . . . 4.5. Kamera . . . . . . . . . . . . . . . . . . iii . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22 23 23 23 24 25 25 25 25 26 26 27 iv Inhaltsverzeichnis 4.6. jME, OpenGL, DirectX, 3D-Pipeline: Was ist das? Was machen die? . . . . . . . . . . . 29 4.7. Ressourcen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30 5. Interaktion 31 5.1. Einfache Interaktionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31 5.2. Ein objektorientierter Ansatz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34 6. Simple Formen und Modelle 37 6.1. Einfache Formen und warum die Kugeln nicht rund sind 6.2. Komplexere Modelle . . . . . . . . . . . . . . . . . . . 6.2.1. Modell-Formate . . . . . . . . . . . . . . . . . 6.2.2. Modelle laden in jME . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7. Es werde Licht 37 40 40 41 44 7.1. Lichtquellen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44 7.2. Lichter einsetzen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46 8. Texturen 50 8.1. Was sind Texturen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50 8.2. Texturen einsetzen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50 8.3. Wie geht es weiter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54 9. Landschaften 9.1. 9.2. 9.3. 9.4. 9.5. Einführung . . . . . . . . Heightmaps . . . . . . . . Landschaften darstellen . . Landschaften mit Texturen Skybox . . . . . . . . . . 56 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10. Final Island 56 56 58 61 63 68 10.1. Der Code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68 10.2. Fazit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72 A. CD-ROM 73 B. Ein jME-Demo in der Konsole starten und kompilieren 75 B.1. Demos starten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75 B.2. Demos kompilieren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75 C. Verwendete Software C.1. Entwicklung . . . . . . . . . . C.1.1. Java . . . . . . . . . . C.1.2. Eclipse . . . . . . . . C.2. Dokumentation . . . . . . . . C.2.1. LATEX und Co . . . . . C.2.2. Violet UML-Tool . . . C.2.3. Dia Diagramm-Editor 77 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77 77 77 77 77 77 78 Abbildungsverzeichnis 1.1. Screenshot aus dem finalen Projekt des Gameversity Kurses . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.1. 2.2. 2.3. 2.4. Ein Screenshot von Blender . . . . . . Ein Ausschnitt aus “Geri’s Game” . . Ausschnitt aus dem Film Madagascar Zeitlinie Echtzeit-3D-Spiele . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9 . 10 . 11 . 12 3.1. 3.2. 3.3. 3.4. 3.5. Screenshot aus HelloWorld . . . . . . . . . . . . . . . . . . . . Screenshot des Einstellungsdialogs der bei jedem Start erscheint Eine Hierarchie von einem Haus . . . . . . . . . . . . . . . . . UML-Diagramm der Scenegraph Elemente . . . . . . . . . . . UML Diagramm der RenderStates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16 17 19 20 21 4.1. 4.2. 4.3. 4.4. 4.5. Ein rechthändiges 3D-Koordinatensystem Ein Würfel rotiert, bewegt und skaliert . . Projektionen . . . . . . . . . . . . . . . . 3D-Pipeline . . . . . . . . . . . . . . . . jME und OpenGL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23 27 28 28 29 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6 . . . . . . . . . . 5.1. Die verschiedenen InputActions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36 6.1. Ein Screenshot aus der Demo SimpleGeometry.java . . . . . . . . . . . . . . . . . . . . 38 6.2. Milkshape 3D mit dem Ferrari Modell . . . . . . . . . . . . . . . . . . . . . . . . . . . 40 6.3. Screenshot von ModelLoader.java mit Blick auf das Gittermodell . . . . . . . . . . . . . 41 7.1. Eine Szene mit Licht (links) und ohne Licht (rechts) . . . . . . . . . . . . . . . . . . . . 45 7.2. Ein Punktlicht . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45 7.3. Screenshot aus der Licht Demo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46 8.1. 8.2. 8.3. 8.4. Eine Stadt mit und ohne Textur . . Textur vom Ferrari Modell . . . . Screenshot von der Texturen Demo Beispiel für Bump-Mapping . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51 51 52 54 9.1. 9.2. 9.3. 9.4. 9.5. Screenshot aus Oblivion von Bethesda Softworks . . . . . . . . Eine sehr einfache 3D-Landschaft . . . . . . . . . . . . . . . . Ein Beispiel für eine Heightmap . . . . . . . . . . . . . . . . . Eine Landschaft generiert aus der Heightmap aus Abbildung 9.3 Screenshot aus TerrainDemo.java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57 57 58 59 59 v . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . vi Abbildungsverzeichnis 9.6. 9.7. 9.8. 9.9. Unsere Landschaft mit einigen saftigen, grünen Hügeln Rechts die Grastextur, links die Detailtextur . . . . . . Würfelbild mit der Skybox . . . . . . . . . . . . . . . Screenshot von der Skybox Demo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61 62 64 64 10.1. Final Island: Blick auf das Wasser . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69 10.2. Final Island: Blick auf das Jeep-Modell . . . . . . . . . . . . . . . . . . . . . . . . . . 69 10.3. Beispiel für eine Particle Engine . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72 A.1. CD-Rom Inhalt . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74 Listings 3.1. HelloWorld.java . . . . . . . . . . . . . . . . . . . . . 3.2. HelloWorld.java, simpleInitGame . . . . . . . . . . 4.1. SimpleTransformation.java . . . . . . . . . . . . . . . 5.1. InputDemo.java . . . . . . . . . . . . . . . . . . . . . 5.2. InputActions.java simpleInitGame and simpleUpdate 5.3. InputActions.java KeyNodeUpAction . . . . . . . . . 6.1. SimpleGeometry.java . . . . . . . . . . . . . . . . . . 6.2. ModelLoader.java . . . . . . . . . . . . . . . . . . . . 7.1. SimpleLight.java . . . . . . . . . . . . . . . . . . . . . 7.2. Lichter aktivieren mit LightStates . . . . . . . . . . . 8.1. TextureDemo.java . . . . . . . . . . . . . . . . . . . . 9.1. TerrainDemo.java . . . . . . . . . . . . . . . . . . . . 9.2. TerrainWithTexture.java . . . . . . . . . . . . . . . . . 9.3. SkyBoxTest.java . . . . . . . . . . . . . . . . . . . . . 9.4. SkyBox.java . . . . . . . . . . . . . . . . . . . . . . . 10.1. FinalIsland.java . . . . . . . . . . . . . . . . . . . . . 1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 17 26 31 34 36 37 41 46 48 50 58 62 63 65 68 Teil I. Einleitung 2 1 Bachelorprojekt 1.1. Beschreibung der Aufgabe . . . . . . . . 1.2. Projektablauf . . . . . . . . . . . . . . . . 1.2.1. 1. Schritt: Auswahl einer 3D-Engine 1.2.2. 2. Schritt: Machbarkeitstest . . . . 1.2.3. 3. Schritt: Tutorial . . . . . . . . . 1.3. Gliederung der Dokumentation . . . . . . 1.4. Konventionen und Notationen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 3 3 6 7 7 7 1.1. Beschreibung der Aufgabe Ziel dieser Bachelorarbeit ist es, Leuten mit Programmiererfahrung, die 3D-Grafikprogrammierung näher zu bringen. Es wird sogar angenommen das die Zielgruppe keinerlei spezielle vorherige Kenntnisse der 3D-Grafik hat. Das bringt natürlich einige Einschränkungen mit sich. Einerseits will man zwar schnell einige sehenswerte Ergebnisse haben, deshalb wurde auch eine Engine ausgewählt, mit der man schnell etwas auf den Bildschirm “zaubern” kann. Andererseits sollte man trotzdem ein wenig die Theorie verstehen. Das ist keine leichte Aufgabe, wenn man bedenkt wie breit und zum Teil auch komplex allein der Teilbereich des 3D-Echtzeitrendering ist. Ich selbst bin häufig auf Schwierigkeiten gestossen, ein bestimmtes Teilgebiet dieser Materie zu erklären. Aber ich hoffe trotz allen Problemen und Schwierigkeiten ist das Tutorial immer noch verständlich. 1.2. Projektablauf Das ganze Bachelorprojekt ist in drei Hauptschritten angegangen worden. Im ersten Schritt wurden verschiedene 3D-Engines evaluiert um dann eine von diesen auszuwählen. Im zweiten Schritt wurde eine kleine Machbarkeitsstudie ausgeführt um zu sehen. Es wurde eine grössere Beispielanwendung entwickelt, die auch das Ziel des eigentlichen Tutorials ist. Im dritten und letzten und bei weitem auch umfangreichsten Schritt wurde das Tutorial selbst mit sehr vielen kleinen Beispielanwendungen geschrieben. Aber gehen wir auf die einzelnen Schritte doch noch ein wenig näher ein. 1.2.1. 1. Schritt: Auswahl einer 3D-Engine Ziel des ersten Abschnitts meiner Bachelorarbeit bestand darin einen Überblick über die fast schon unzähligen 3D-Engines zu gewinnen und danach eine 3D-Engine für den weiteren Verlauf des Projektes auszuwählen. 3D-Engines gibt es wie Sand am Meer möchte man fast meinen. Allein eine Suche auf Google mit dem Stichwort “3D Engine” ergibt 469’000 Ergebnisse (Stand: August 2005) und die Zahl 3 1.2. Projektablauf 4 ist steigend. Man kann sich vorstellen, dass unter diesen Bedingungen schon dieser erste Schritt nicht ganz leicht ist. Um die Auswahl etwas zu vereinfachen und zu systematisieren wurden einige Kriterien erstellt. Im einzelnen sind das folgende Kriterien: 1. Die 3D-Engine muss mit Java benutzt werden können und auf verschiedenen Plattformen laufen, nicht nur auf Windows sondern zum Beispiel auch auf Linux oder MacOSX. 2. Die 3D-Engine sollte frei verfügbar und gratis benutzbar sein. Am besten wäre eine Engine die unter einer Open-Source-Lizenz veröffentlicht wurde, die dieses Kriterium langfristig garantiert. 3. Die 3D-Engine sollte ein gutes objektorientiertes Design vorweisen und ein “sauberes” Framework zur Verfügung stellen. 4. Die Engine sollte aktuelle Techniken des Echtzeit-Rendering unterstützen. 5. Die 3D-Engine sollte auch ausreichend dokumentiert sein. Die oben genannten Kriterien ergeben nun einige Konsequenzen, die ich kurz aufgreifen möchte: • Punkt eins und drei verlangen, dass die Engine in einer objektorientierten Sprache geschrieben ist. (Falls möglich in Java, aber dazu weiter unten noch mehr.) Engines die noch in C, oder anderen prozeduralen Sprachen geschrieben sind fallen weg. • Punkt zwei schliesst von vorne Herein viele im Spielesektor bekannte, aber auch sehr teure (die Lizenzkosten belaufen sich zum Teil auf mehrere hunderttausend Dollar) Engines aus wie die Doom3-Engine von id-Software [29], die Source-Engine von Valve [58], die CryEngine [16] von Crytek, die Unreal3-Engine [57] von Epic, etc... • Punkt vier erfordert schlussendlich eine moderne, hardwarebeschleunigte Engine. Das bedeutet, dass die Engine entweder Direct3D [20] oder OpenGL [47] unterstützen muss, wobei Direct3D als Windows-only gleich wieder von der Liste verschwindet. Als Konsequenz davon müssen Software-Renderer gleich wieder von der Liste verschwinden. Der in Java-Kreisen bekannteste Vertreter eines Softwarerenderers dürfte dabei Java3D [33] von Sun sein. Als Randnotiz muss man aber beachten, dass seit der kürzlichen Open-Source Veröffentlichung von Sun auch von einer 3D-Beschleunigung durch OpenGL diskutiert wird. • Punkt fünf filtert schon viele Engines aus, die erst in den Kinderschuhen stecken oder gar nie richtig aus der Konzeptphase herausgekommen sind. Mit Hilfe dieser Kriterien wurde nun eine Internetrecherche durchgeführt und die ersten Engines zur näheren Betrachtung ausgewählt. Eine grosse Hilfe war dabei die 3D Engines Database [1]. Die Datenbank ist sehr umfangreich und man kann die Engines sogar anhand von bestimmtem Kriterien sortieren und filtern. Java gegen C++ In der Spieleindustrie ist immer noch C++, die am weitesten verbreitete Sprache. Das hat zur Folge, dass auch die meisten 3D-Engines in C++ geschrieben sind. Es gibt viele Gründe dafür. Die am weitesten verbreiteten 3D-APIs1 , OpenGL und Direct3D, sind selbst C-APIs. Wie oben bereits erwähnt ist OpenGL-Unterstützung eine Bedingung für die 3D-Engine. Als Java Programmierer können wir nun zwei Wege gehen. Einerseits gibt es OpenGL-Wrapper für Java, wie JOGL von Sun [37] oder die Lightweight Java Game Library LWJGL [40]. Andererseits bieten viele C++-Engines eine Java Unterstützung, die auf dem Java Native Interface (JNI) basiert. 1 API = Aplication Programming Interface. Eine API stellt ein Interface zur Verfügung, dass man als Programmierer benutzen kann. 1.2. Projektablauf 5 C++-Engines Als erstes habe ich einige C++-Engines untersucht. Diese sind: • Irrlicht Engine [30] • OGRE Engine [46] • Crystal Space Engine [17] Diese Engines sind alle in C++ geschrieben. Sie sind alle Open Source und haben eine beeindruckende Feature Liste. Mit der OGRE Engine wurde sogar ein kommerzielles Spiel programmiert. Leider haben aber auch alle drei Engines eine entscheidenden Nachteil. Es existiert zwar bei allen eine Java Unterstützung, der Wrapper ist aber bei allen drei Engines sehr vernachlässigt worden und sogar die Entwickler selbst raten davon ab ihn zu benutzen. Das heisst nun konsequenterweise, das alle diese Engines von der Liste gestrichen werden. Java-Engines Glücklicherweise gibt es aber auch noch einige Engines, die direkt mit Java geschrieben wurden. Leider führt Java im Spielentwicklungsbereich immer noch ein Mauerblümchendasein. Einerseits ist das immer noch auf das alte und eigentlich ausgemerzte Performanceproblem zurückzuführen, das sich in einigen Kreisen aber immer noch sehr hartnäckig weiter vertreten wird. Ehrlicherweise muss man aber auch gestehen, dass sich Java in Sachen Grafikperformance bis vor kurzer Zeit nicht gerade mit Ruhm bekleckert hat. Obwohl Sun das Problem zuerst dementiert und relativiert hat, arbeiten sie aber in letzter Zeit direkt an der bereits angesprochenen Unterstützung von OpenGL. Es gibt auch einige Bücher die sich direkt mit der Spiele- und Grafikprogrammierung unter Java beschäftigen wie [BBV03] und [Dav05]. Ich habe drei Java 3D-Engines in die nähere Betrachtung gezogen: • Aviatrix3D [9] • Espresso3D [24] • jME jMonkey Engine [36] Leider muss man bei Aviatrix3D einige Abstriche machen, da die Dokumentation einiges zu wünschen übrig lässt. Die Installation ist ausserdem sehr aufwändig. Espresso3D ist eine Engine, die noch nicht lange in Entwicklung ist. Es gibt noch nicht sehr viele Features. Sehr überzeugt hat mich hingegen jME. Die Website von jME [36] enthält schon eine ziemlich umfangreiche Dokumentation und auch die Demos sehen sehr gut aus. Entscheidung Nach den Tests habe ich mich schlussendlich für jME entschieden. Sie hat alle Kriterien, die ich am Anfang aufgestellt habe erfüllt. • jME ist komplett in Java geschrieben. • jME steht unter der BSD-Lizenz [13]. Diese Open-Source Lizenz ist sehr liberal und erlaubt sogar den kommerziellen Einsatz. Die Engine ist also frei für jedermann und kann von allen heruntergeladen, benutzt und sogar verändert werden. • Die Engine stellt ein gutes Framework zur Verfügung und ist leicht zu benutzen. • jME verwendet OpenGL [47] mit LWJGL [40]. Die Engine unterstützt also hardwarebasiertes Echtzeit-Rendering. 1.2. Projektablauf 6 Abbildung 1.1.: Screenshot aus dem finalen Projekt des Gameversity Kurses • jME hat bereits jetzt eine umfangreiche Dokumentation, was für ein Open Source Projekt doch sehr positiv bemerkbar ist. In diesem ersten Schritt habe ich also jME ausgewählt. Die weiteren Schritte basieren natürlich auf dieser Entscheidung. 1.2.2. 2. Schritt: Machbarkeitstest Nachdem nun eine 3D-Engine ausgewählt worden ist, hat sich die Frage gestellt, was man damit anfangen soll. Nach einigen Besprechungen mit den verantwortlichen Lehrpersonen wurde beschlossen ein Tutorial zum Thema 3D-Grafik zu schreiben, das auch Anfänger mit Programmierkenntnissen verstehen sollten. Ich musste selbst noch meine 3D-Kenntnisse erweitern und habe dazu online Kurse besucht bei Gameversity [27] und Game Institute [26]. Konkret habe ich bei Game Institute den Kurs Graphics Programming with DirectX 9 - Module I und bei Gameversity den Kurs DirectX Graphic Programming besucht, einige Scripts aus diesen Kursen können Sie auf der CD (siehe Anhang A) im Verzeichnis dateien begutachten. Die Kurse werden sogar in einen amerikanischen Universitäten anerkannt. Das erfolgreiche Bestehen dieser Kurse, ist ungefähr äquivalent mit 10 ECTS Punkten hier in der Schweiz. Diese Kurse sind wirklich sehr empfehlenswert und jeder, der sich noch tiefer und grundlegender mit der Materie in dieser Arbeit beschäftigen will, kann ich diese Kurse sehr ans Herz legen. Diese Erfahrung hat mit gezeigt, dass man sehr wohl in Online basierten Kursen hervorragendes Wissen vermitteln kann. Lange Rede kurzer Sinn. Beim Gameversity Kurs, erlernte man das Anwenden und die Implementation einer 3D-Engine und musste als letzte Aufgabe eine kleine Demo mit einer Insel, Wasser und noch einigem mehr machen. Eine Abbildung dieser Demo sehen Sie auf Screenshot 1.1. Die ganze Demo finden Sie auch auf der CD zum Projekt, sehen Sie sich dazu Anhang A an. Da die Demo Direct3D benutzt, läuft sie nur unter Windows-Betriebssystemen. In dieser Bachelorarbeit geht es hauptsächlich um das Anwenden der 3D-Engine jME, ich habe mir also gedacht, das man die gleiche Demo in jME implementieren könnte. 1.3. Gliederung der Dokumentation 7 Gesagt, getan. In einer Coding-Session von gut einer Woche wurde eine ähnliche Demo in jME programmiert. Es ist also durchaus möglich eine Applikation, die in C++ geschrieben wurde und Direct3D benutzt in Java zu schreiben und eine plattformübergreifende Engine zu benutzen. Der 2. Schritt ist damit abgeschlossen. 1.2.3. 3. Schritt: Tutorial Nachdem nun die Engine ausgwählt und ein Endziel festgelelgt wurde, ging es im dritten und letzten Schritt darum die einzelnen Zwischenschritte zu erstellen, jeweils mit Bespielapplikationen, im Verlauf des Dokuments Demos gennant, und dem jeweiligen Begleittext. Obwohl die Arbeit klar strukturiert und das Resultat klar umschrieben werden konnte, handelte es sich bei diesem Teil, dennoch um den mit Abstand zeitaufwändigsten und arbeitsintensivten Schritt. Einige fast unüberwindbar scheinende Hürden, mussten umschifft werden, bis wir zu diesem Endresultat gelangten. Ich hoffe Sie als Leserinnen und Leser können mit dieser Arbeit nun etwas sinnvolles anfangen. 1.3. Gliederung der Dokumentation Dieses Dokument ist in zwei Hauptteile gegliedert: 1. Im ersten Teil befindet sich eine kurze allegemeine Einführung in die 3D-Computergrafik. Diese Arbeit beschäftigt sich mit einem Unterteil dieses Themas. Desweiteren gibt es einen Überblick über das Bacholorprojekt im allgemeinen. 2. Der zweite Teil dieses Dokumentes beschäftigt sich mit der 3D-Engine jME[36]. In jedem Kapitel wird im Tutorialstil Schritt für Schritt ein weiters Element der Engine jME und der 3DGrafikprogrammierung beschrieben, bis wir am Schluss in der Lage sind die Demo in Kapitel 10 nach zu vollziehen. Am Ende des Dokumentes befinden sich noch einige Anhänge. Sie zeigen unter anderem den Inhalt der mitgelieferten CD auf (siehe Anhang A) und beschreiben wie Sie die jeweiligen Demos starten, bearbeiten und neu kompilieren können (siehe Anhang B). 1.4. Konventionen und Notationen • Dateiname: wird benutzt um Dateinamen, Dateierweiterungen und Pfade anzugeben. • Wichtig wird benutzt um wichtige Namen hervorzuheben. • Variable wird benutzt um Klassennamen und Variablennamen und weiter Quellcodeextrakte im Text anzuzeigen. • Längere Quellcodeabschnitte und Listings werden folgendermassen angezeigt: System . out . println ( " Hallo schöne jME - Welt !" ); • Abbildungen und Listings sind innerhalb eines Kapitels nummeriert. • Referenzen zu einem Objekt innerhalb des Literaturverzeichnisses sieht so aus: [AMH02]. Das komplette Literaturverzeichnis befindet sich am Ende dieses Dokumentes. • Referenzen auf Webseiten sehen folgendermassen aus: [36]. Das gesamte Verzeichnis mit den Web Ressourcen befindet sich ebenfalls am Ende dieser Bachelorarbeit. • Bei den einzelnen Klassendiagrammen in diesem Dokument handelt es sich um herkömmliche UML-Klassendiagramme. Mehr zu UML können Sie im Buch UML Distilled[Fow04] lesen. 2 3D-Grak 2.1. Was ist 3D-Grafik? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8 2.2. Was bedeutet Echtzeit-3D-Grafik? . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 2.3. Was ist eine 3D-Engine? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 2.4. Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 3D-Grafik oder genauer gesagt 3D-Computergrafik ist ein sehr breites Thema und es kann leicht sein, dass man sich zu schnell von den Details erschlagen lässt. Deshalb will ich in den folgenden Unterkapiteln eine kurze allgemeine Einführung in das Thema bereiten. Die Einführung wird sehr kurz sein und sich nur auf das Wesentliche konzentrieren. Aber ich hoffe dennoch, dass Sie damit einen kurzen Überblick über das Thema gewinnen. Ich hoffe vor allem Leute, die sich noch nie mit dieser Thematik beschäftigt haben, werden einiges dazulernen, aber auch alle andern sollte dieser Überblick und die dazugehörige Abgrenzung der Themen helfen. 2.1. Was ist 3D-Grafik? Was ist 3D-Grafik? Das ist vielleicht die grundlegendste Frage, die man im Zusammenhang mit 3DGrafik überhaupt stellen kann. Und die Beantwortung ist gar nicht so einfach. Grundsätzlich gibt es zwei Teilbereiche in der 3D-Grafik: das Modellieren und das Rendern von räumlichen Daten. Mit der Modellierung bezeichnet man das Erstellen von räumlichen Daten, so genannten 3D-Objekten. Diese 3D-Objekte werden weitgehend von Hand mit spezialisierter Software erstellt. Diese Software wird 3D-Modelliersoftware oder auch nur “3D-Modeller” genannt. Einige der bekanntesten unter ihnen sind “3D-Studio Max” von Autodesk [3] , Maya von Alias [5] und SOFTIMAGE|XSI von Softimage [54] . Die meisten dieser Applikationen sind sehr teuer, zum Teil mehrere zehntausend Franken1 . Glücklicherweise gibt es auch einige freie Open-Source Alternativen wie Blender [12] (siehe auch Abbildung 2.1) oder Wings3D [62] . Abschliessend bleiben noch einige günstige Shareware Programme zu erwähnen, die auch ihre Daseinsberechtigung haben, zum Beispiel Milkshape 3D [43] oder AC3D [4] . Die Modellierung von 3D-Objekten ist sehr aufwändig, braucht viel Zeit und Geduld und auch einiges künstlerisches Geschick, wie ich aus eigener missglückter Erfahrung berichten kann. Wir werden im weiteren Verlauf nicht mehr auf das Modellieren eingehen, wer sich dafür interessiert findet unzählige Tutorials und WebSites auf dem Internet dazu, wie zum Beispiel auf 3D Links [2] und den Seiten der einzelnen oben genannten Programme. Die einzelnen 3D-Objekte werden dann unter Umständen noch zusammengefasst zu grösseren Szenen. Man kann sich zum Beispiel eine Stadtszene vorstellen, in der 1 Es gibt aber meistens auch eine gratis Learning- bzw. Personal-Edition für Heimanwender und Studenten, die im Funktions- umfang eingeschränkt ist. Beispiele dafür sind “gmax” von den 3D Studio Max Entwicklern Autodesk [28] und die “Maya Personal Learning Edition” von Alias [42]. 8 9 2.1. Was ist 3D-Grafik? Abbildung 2.1.: Ein Screenshot von Blender 2.2. Was bedeutet Echtzeit-3D-Grafik? 10 Abbildung 2.2.: Ein Ausschnitt aus “Geri’s Game” wir einige Autos sehen, im Hintergrund hat es Geschäfte und Hochhäuser, das sind alles einzelne 3DObjekte die in einer Szene zusammen kommen. Der zweite wichtige Teilbereich in der 3D-Grafik ist das Rendern. Unter dem Rendern verstehen, wir das Erzeugen eines zweidimensionalen Bildes aus den räumlichen 3D-Daten. Es geht also darum nun die erzeugten 3D-Objekte auf dem Bildschirm anzuzeigen. Man kann es vergleichen mit dem Fotografieren oder dem Filmen einer Szene, wir betrachten also unsere 3D-Szene aus einer virtuellen Kamera. Dazu gibt es auch viele verschiedene Methoden und Techniken. In diesem Tutorial beschäftigen wir uns mit dem Echtzeit-Rendern. Wie in der Einleitung erwähnt, wollen wir ja Bilder mit Hilfe von Java in Echtzeit auf den Bildschirm bringen. Auf der Abbildung 2.2 können wir einen Ausschnitt aus dem Pixar Kurzfilm Geri’s Game [51] sehen. Links ist das Drahtmodell (engl. wireframe) zu sehen, das in einem 3D-Modeller modelliert wurde. Rechts ist eine fertig gerenderte Szene mit dem gleichen Kopf aus dem Film zu sehen. 2.2. Was bedeutet Echtzeit-3D-Grafik? Wie der Titel schon andeutet, behandeln wir hier Echtzeit-3D-Grafik. Unter Echtzeit verstehen wir, dass wir eine 3D-Szene in Echtzeit rendern wollen. Viele von ihnen kennen vielleicht die bekannten Animationsfilme von Pixar [50] oder Dreamworks Animation [21], wie “Shrek”, “The Incredibles”, “Findet Nemo” oder den neusten Titel “Madagascar”. Und obwohl diese Filme auch mit Hilfe von 3D-Grafik erstellt wurden (und keineswegs, wie manche vielleicht meinen mit klassischen Trickfilmzeichnungen), handelt es sich eben gerade nicht um Echtzeit 3D-Grafik. In solchen Filmen wird jeder einzelne Frame2 vorher gerendert und später zu einem Film zusammengefügt. Dieses Rendering einer einzelnen Szene kann aufgrund der verwendeten Effekte, Lichteinstellungen und anderen speziellen Einstellungen mitunter Stunden dauern. Das Ziel ist es das Bild möglichst realistisch erscheinen zu lassen, natürlich in einem gewissen Rahmen, der für die Computer noch machbar ist. Ein Oberbegriff für diese Art des Renderns ist Raytracing. Beim Raytracing werden Lichtstrahlen simuliert und die Lichtverteilung errechnet um möglichst realistische Schatten und Farbschattierungen zu erhalten. Die Methoden sind dabei auch für heutige Rechner sehr zeitaufwändig und lassen sich noch nicht in Echtzeit ausführen. 2 Ein Frame ist ein einzelnes Bild aus einem Trickfilm oder eben einer 3D-Animation. 2.2. Was bedeutet Echtzeit-3D-Grafik? 11 Abbildung 2.3.: Ausschnitt aus dem Film Madagascar Wenn wir von Echtzeit sprechen, meinen wir nun, dass eben genau diese Rendering nur Bruchteile von Sekunden dauern darf, damit wir uns auch frei in einer 3D-Welt umher bewegen können. Diese EchtzeitRendering wird in 3D-Simulationen und auch 3D-Spielen verwendet. Um aber das ganze in Echtzeit zu bekommen, muss man auch einige Kompromisse eingehen. Die erzeugten Echtzeit-3D-Welten sind in der Regel nicht so realistisch, wie die vorgerenderten 3D-Welten. Das primäre Ziel ist die erzielbare Geschwindigkeit, mit der ein Frame gerendert werden kann, möglichst zu minimieren. Die Echtzeit-3DGrafik wird aber immer realistischer. Das hängt auch mit der Einführung der so genannten 3D-Karten zusammen, die man mittlerweile als Standardausrüstung, sogar auf Mittelklasse PCs zählen kann. Der Markt für 3D Karten war in den letzten Jahren sehr dynamisch, im Moment gibt es aber nur zwei Firmen, die Karten für den End-User Markt herstellen: ATI [8] und NVidia [44] Auf der Abbildung 2.4 können Sie die Entwicklung der Echtzeit-3D-Grafik in Spielen kurz, aber keinesfalls wirklich repräsentativ, mitverfolgen. Auf dem Teilbild oben links sehen Sie einen Ausschnitt aus dem Spiel Battlezone [10] aus dem Jahre 1980. Das ist das erste Spiel, das eine Art pseudo 3D-Grafik enthielt. Oben rechts ist ein Ausschnitt aus dem Spiel Wolfenstein3D [63] der Firma id Software [29] zu sehen. Das ist eines der ersten 3D-Spiele für den PC und auch der erste “First-Person-Shooter” und somit auch der Vater aller anderen Spiele dieser Art. Es wurde 1992 veröffentlicht. Aus dem Jahr 1999 stammt der dritte Screenshot unten links. Es handelt sich um das Spiel Quake 3 [53], wiederum von id Software. Man kann hier schon gut die ersten eingesetzten Lichteffekte erkennen. Der letzte Screenshot ist von Far Cry [25] . Ein Spiel das 2004 herausgeben wurde. Auf diesem Bild kann man einige interessante Wassereffekte erkennen. Auch die Darstellung der Bäume und Pflanzen ist bemerkenswert und der Detailreichtum der Umwelt. Dieses Echtzeit-Rendering war zuerst nur auf spezieller Hardware möglich, die vor allem militärischen 3D-Simulationen dienten. Im Jahre 1996 hat aber die Firma 3dfx [61] eine erste 3D-Beschleuniger Karte in bezahlbaren Regionen für den Endkundenmarkt hergestellt. Erst mit Hilfe dieser Karten ist das einigermassen realistische 3D-Echtzeit Rendern auf normalen Computern möglich geworden. Die Ent- 12 2.2. Was bedeutet Echtzeit-3D-Grafik? Abbildung 2.4.: Zeitlinie Echtzeit-3D-Spiele oben links oben rechts unten links unten rechts Battlezone [10] , 1980 Wolfenstein 3D [63] , 1992 Quake 3 [53] , 1999 Far Cry [25] , 2004 2.3. Was ist eine 3D-Engine? 13 wicklung der 3D-Grafik-Karten verläuft immer noch rasant. Sie übertrifft sogar das Moorsche Gesetz3 . Die Geschwindigkeit der neuesten 3D-Grafikkarten verdoppelt sich fast alle sechs Monate und ein Ende ist nicht abzusehen. Und das obwohl sich, nach einigen Konkursen und Übernahmen nur zwei grosse Player, ATI [8] und NVidia [44], auf dem Endkundenmarkt tummeln. Es wird also nicht mehr lange dauern, bis wir auch 3D-Echzeitgrafik in der gleichen Qualität, wie die aktuellen Pixar und Dreamworks Animation Filme geniessen können. Was es mit diesen 3D-Grafik-Karten auf sich hat und wie sie intern arbeiten, werden wir ein bisschen genauer im Kapitel 4 unter die Lupe nehmen. 2.3. Was ist eine 3D-Engine? Wir wollen mit Hilfe der 3D-Engine jME [36] eine 3D-Welt aufbauen und schlussendlich auf den Bildschirm bringen. Aber was genau ist eine 3D-Engine? Eine 3D-Engine verwaltet die ganzen 3D-Objekte und die Szenen, die ein 3D-Künstler oder wir selbst vorher angefertigt haben. Sie verwalten also kurz gesagt die ganzen 3D-Daten. Des weiteren ist eine 3D-Engine dafür zuständig, was wir auf den Bildschirm bringen und wie wir das ganze Zeichnen wollen. Diese Entscheidungen werden auf einer unteren und einer oberen Ebene gemacht. Die Entscheidung auf der oberen Ebene werden von unserem Spiel oder unserer Anwendung mit Hilfe eines Scenegraphs auf Softwareebene gemacht, das ganze wird im Abschnitt 3.2 erläutert. Die Entscheidungen der unteren Ebene werden vom Renderer selbst gemacht. Das bedeutet das wir die meiste Arbeit an die Hardware oder genauer gesagt an die Grafik-Karte delegieren können. Wie das genau funktioniert wird im Kapitel 4 näher erklärt. 2.4. Zusammenfassung Ich hoffe, dass Sie als Leser nun ein bisschen mehr Ahnung von der 3D-Grafik haben. Was Sie behalten sollten, ist das wir uns hier im Tutorial auf das Rendern mit der Java 3D-Engine jME beschränken werden. Wir werden uns genauer gesagt sogar auf das interaktive Echtzeit-Rendern beschränken. Wie das abläuft, werden wir im Tutorial sehen. Im Tutorial werden wir uns auf die Handhabung von jME aus Sicht eines Endbenutzers beschränken und nur wo nötig auf implementationstechnische Details eingehen. 3 Als Mooresches Gesetz wird die Beobachtung bezeichnet, dass sich durch den technischen Fortschritt die Komplexität von integrierten Schaltkreisen etwa alle 24 Monate verdoppelt. Teil II. Tutorial 14 3 Erste Schritte mit jME 3.1. Unser erstes jME-Programm . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 3.2. Scenegraph . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18 3.2.1. Was ist ein Scenegraph? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18 3.2.2. Zustände im Kontext von jME . . . . . . . . . . . . . . . . . . . . . . . . . . 21 3.1. Unser erstes jME-Programm Jetzt geht es los liebe Leser! Statt lange um den heissen Brei zu reden, tauchen wir gleich ein in unser erstes jME-Programm. Wie jedes anständige Programmiertutorial beginnt auch dieses Tutorial mit einem HelloWorld Programm. Zu sehen ist aber nicht ein kurzer Textstring sondern ein einfacher Würfel. Sie finden den Quellcode und die Klasse selbst im Verzeichnis demos/firststeps mit dem Namen HelloWorld.java. Wie die CD genau aufgebaut ist, wird in Anhang A erklärt. Im Unterverzeichnis demos der CD finden Sie ausserdem zwei Dateien mit dem Namen firststeps_helloworld.bat und firststeps_helloworld.sh. Es handelt sich dabei um Startdateien, mit denen Sie die Demo unter Windows bzw. unter Linux und MacOSX direkt starten können. Wie sie die Demo direkt in der Konsole starten können, wird im Anhang B erläutert. Listing 3.1: HelloWorld.java 1 package firststeps ; 2 3 import helper . TutorialGame ; 4 5 6 import com . jme . math . Vector3f ; import com . jme . scene . shape . Box ; 7 8 9 10 11 12 13 public class HelloWorld extends TutorialGame { public static void main ( String args [] ) { new HelloWorld () ; } 14 15 16 17 18 protected void simpleInitGame () { // set the title of our application display . setTitle ( " Hello World !!! " ); 19 15 3.1. Unser erstes jME-Programm 16 Abbildung 3.1.: Screenshot aus HelloWorld // create a simple cube Box b = new Box ( " box " , new Vector3f ( 0, 0, 0 ) , new Vector3f ( 1, 1, 1 ) ); 20 21 22 // put the box in the scene graph rootNode . attachChild ( b ); 23 24 } 25 26 } Dieses einfache Programm beinhaltet bereits alles was auch ein grösseres jME-Programm beinhalten muss. Wir erben von einer Klasse namens TutorialGame. In TutorialGame werden verschiedene Dinge aufgesetzt, die in jedem unseres jME-Programme nützlich sind: • Eine einfache Kamera wird erzeugt, die wir mittels der in Spielen üblichen Manier bewegen können. Das heisst konkret: mit der Maus verändern wir unsere Blickrichtung, während wir uns mit der ’W’- und ’S’-Taste vorwärts und rückwärts bewegen. Mit ’A’ und ’D’ bewegen wir und links bzw. rechts seitwärts. Das wird auch für die meisten anderen, kommenden Demos, die Standardmethode sein, mit der wir uns fortbewegen können. Wer bereits einen modernen 3D-Shooter gespielt hat, wird sich gleich zu Hause fühlen. • Ein simples Licht wird aufgesetzt. In Kapitel 7 werden wir genauer darauf eingehen. • Ein Scenegraph wird eingerichtet. Was ein Scenegraph genau ist werden wir am Ende dieses Kapitels genauer erklären. • Einige simple Tastenkommandos werden definiert wie zum Beispiel: – ’T’ und den Wireframe-Modus ein- bzw. auszuschalten – ’C’ um die Position der Kamera in der Konsole auszugeben – ’L’ um die Lichter zu deaktivieren bzw. wieder zu aktivieren 3.1. Unser erstes jME-Programm 17 Abbildung 3.2.: Screenshot des Einstellungsdialogs der bei jedem Start erscheint – ESCAPE um das Programm zu beenden • TutorialGame stellt auch zwei Methoden namens simpleInitGame und simpleUpdate zur Verfügung, die wir in unseren eigenen Programmen überschreiben können, wenn wir etwas initialisieren wollen oder etwas in jedem Frame ändern wollen. Für uns ist es nicht so wichtig zu wissen, wie TutorialGame genau seine Arbeit macht, wichtig zu wissen ist aber, das jedes unserer Programme von TutorialGame erben wird. Beschreiben wir nun unser erstes Programm ein bisschen genauer. Die Dialogbox mit dem Uni-Fribourg Logo, die bei jedem Start gezeigt wird, ist sicher auch schon einigen aufgefallen, auf der Abbildung 3.2 können Sie einen Screenshot davon sehen. Der Konstruktor von TutorialGame, ist dafür verantwortlich, dass dieser Dialog immer erscheint. In diesem Dialog kann man die Bildschirmauflösung des folgenden Programms auswählen und bestimmen, ob die Applikation in einem Fenster oder als Vollbildanwendung ausgeführt wird. Es ist also durchaus sinnvoll diesen Dialog vor jedem Start anzuzeigen. In Zeile 12 beginnt unser Programm dann richtig. Mit dem Aufrufen des Konstruktors wird auch der Konstruktor von TutorialGame aufgerufen, der wiederum eine Endlosschleife ausführt, die erst beendet wird, wenn wir auf ’Fenster schliessen’-Kreuz drücken oder die Taste Escape betätigen. Bevor die Schleife startet wird das System initialisiert und unter anderem auch simpleInitGame aufgerufen. In der Schleife selbst werden danach in jedem Iterationsschritt zwei Dinge gemacht: zunächst erhält jedes Objekt, denn Befehl sich zu bewegen und simpleUpdate wird aufgerufen, als zweites wird alles auf den Bildschirm gerendert. Unser main wird in jeder Demo gleich aussehen, die eigentliche Arbeit werden wir immer in den MethodensimpleInitGame und zum Teil auch in simpleUpdate machen. Besprechen wir also im folgenden Abschnitt simpleInitGame etwas genauer: Listing 3.2: HelloWorld.java, simpleInitGame 15 16 17 protected void simpleInitGame () { // set the title of our application 3.2. Scenegraph 18 display . setTitle ( " Hello World !!! " ); 18 19 // create a simple cube Box b = new Box ( " box " , new Vector3f ( 0, 0, 0 ) , new Vector3f ( 1, 1, 1 ) ); 20 21 22 // put the box in the scene graph rootNode . attachChild ( b ); 23 24 25 } Schon beim betrachten des Codes sehen wir, das genau drei Dinge passieren: 1. Wir geben unserer Demo einen Titel. Der Titel wird auf dem Fenster angezeigt und wird auch der Name sein, mit dem das jeweilige Betriebssystem unsere Applikation kennt. 2. Als nächstes erstellen wir einen Würfel mit Box. Der Würfel ist, dieses Ding, das man auf dem Bildschirm sehen konnte. 3. Wir fügen unseren neu erstellten Würfel der Wurzel unseres Scenegraphen an. Die Wurzel des Scenegraphen heisst rootNode und wird uns von TutorialGame zur Verfügung gestellt. Wie wir oben sehen können, benutzen wir drei Argumente um einen Würfel zu kreieren. Als erstes geben wir dem Objekt mittels eines String Objektes einen Namen. Allen Objekte, die wir irgend einmal einem Scenegraph anhängen wollen müssen wir einen Namen geben. Dieser Würfel wurde in diesem Beispiel box genannt, wir hätten aber auch irgend einen anderen Namen wählen können. Die Nächsten zwei Argumente geben zwei Ecken unseres Würfels an. Der Würfel hat eine Ecke im Ursprung (0; 0; 0) und eine Ecke im Punkt (1; 1; 1) es handelt sich also um einen Einheitswürfel. Jetzt haben wir also einen Würfel erstellt, wir wollen diesen Würfel aber auch sehen. Deshalb müssen wir den Würfel mittels rootNode.attachChild unserem Scenegraph anhängen. Alle Objekte, die gerendert werden sollen, müssen wir dem Scenegraphen anhängen. rootNode ist dabei die Wurzel des Scenegraph, der in TutorialGame definiert wurde. In diesem Beispiel besteht unser ganzer Scenegraph aus der Wurzel rootNode mit dem angehängten Würfel box. Wenn nun von TutorialGame der Befehl kommt rootNode zu zeichnen wird automatisch auch der Würfel mitgerendert. 3.2. Scenegraph 3.2.1. Was ist ein Scenegraph? Kommen wir zurück zu der grundlegenden Frage, was eine 3D-Engine eigentlich macht. Wir haben bereits gesagt, dass es die Hauptaufgabe einer 3D-Engine ist verschiedene 3D-Objekte zu verwalten und auf den Bildschirm zu bringen. Die einfachste Möglichkeit diese Objekte zu verwalten, wäre eine verkette Liste mit allen Objekte, die wir dann eines nach dem anderen auf den Bildschirm zeichnen. Das ist eine einfache aber leider nicht sehr effiziente Methode Objekte zu verwalten. Wenn man sich näher mit einem modernen 3D-Spiel beschäftigt, erkennt man, dass die 3D-Welten aus tausenden von 3D-Objekten bestehen. Diese tausenden von Objekte in einer Liste zu speichern und nacheinander zu verarbeiten würde viele Spiele wohl zu einer eher langweiligen Dia-Show ausarten lassen. Denn selbst Objekte die nicht auf dem Bildschirm erscheinen würde man in diesem Falle einer zeitraubenden Bearbeitung unterziehen müssen und wertvolle Ressourcen rauben. Die 3D-Hardware weiss noch nicht einmal, dass viele Objekte nicht zu zeichnen sind, und schmeisst solche Objekte erst sehr spät aus der Pipeline, deshalb spricht man auch davon, dass die 3D-Beschleuniger auf einem sehr tiefen sogenannten low-level arbeiten. Als Programmierer haben wir aber sehr wohl eine grössere high-level Ahnung von den 3D-Objekten auf dem Bildschirm und wie sie miteinander in Beziehung stehen. Die ganze Welt aus den 3D-Objekten wird, wie wir bereits einmal erwähnt haben als Szene (engl. scene) bezeichnet. Wenn wir nun unser Wissen von den Beziehung dieser 3D-Objekten einbringen erhalten 19 3.2. Scenegraph Abbildung 3.3.: Eine Hierarchie von einem Haus wir einen Scenegraphen1 . Als Scenegraph wird in jME ein Baum verwendet. In einem Baum gibt es eine Wurzel und jedes Element kann mehrere Kindobjekte enthalten, besitzt aber nur ein Elternelement. jME stellt uns mit rootNode bereits die Wurzel eines Scenegraphen zur Verfügung, an den wir weitere Blätter anfügen können. Mit einem Baum können wir als Benutzer einer Engine eine Hierarchie von 3DObjekten aufbauen. Dieser Ansatz gibt uns viele Vorteile, wie Sie in [Ebe00] auch nachlesen können: • Stellen wir uns vor wir haben eine 3D-Welt, die aus vielen verschiedenen Räumen besteht. Wenn wir nun ein Licht einsetzen, wie wir es in Kapitel 7 zeigen, dann wollen wir das dieses Licht nur diesen Raum betrifft und beleuchtet auch aus Performancegründen. Mit einem Scenegraph ist das einfach in dem wir das Licht einfach im Raum einsetzen den wir beleuchten wollen. Das Licht beleuchtet dann automatisch auch alle Kindobjekte von diesem Raum. • Zweitens, in einem Scenegraphen kann man leicht lokale Gruppierung darstellen. Das hilft besonders, weil man mit dieser Methode schnell ganze Objektgruppen eliminieren kann, die nicht auf dem Bildschirm zu sehen sind. Nehmen wir als Beispiel an wir befinden uns im Raum 2 wie auf der Abbildung 3.3 zu sehen. Der Renderer kann den ganzen Unterbaum von Raum 1 direkt von der 1 Für diesen Begriff scheint es leider keine geläufige deutsche Übersetzung zu geben, deshalb werden wir uns im Verlaufe des Dokuments auf den englischen Begriff Scenegraph beschränken. 20 3.2. Scenegraph Abbildung 3.4.: UML-Diagramm der Scenegraph Elemente Bearbeitung ausschliessen, weil dieser Raum nicht zu sehen ist. Wenn man Objekte von der Bearbeitung ausschliesst, die nicht auf dem Bildschirm zu sehen sind spricht man vom sogenannten Frustum Culling. Das ist ein Konzept, das sehr wichtig ist im Echzeitrendern. • Viele 3D-Objekte die wir darstellen wollen sind schon von Natur aus auf hierarchische Weise aufgebaut. Das ist ein dritter Vorteil, den wir mit Scenegraphen haben. Das gilt besonders für humanoide Objekte. Die Lage und die Rotation von einer Hand hängt auf natürliche Weise ab von der Lage und der Rotation des Ellbogens, der Schulter und der Hüfte. Mit einem Scenegraphen ist es leicht solche Abhängigkeiten darzustellen. Wenn wir das ganze von Hand machten müssten, würde es sehr schnell kompliziert werden, wie Sie im Abschnitt 4.2.3 selbst sehen können. Kommen wir nun zum Scenegraphen zurück den jME für uns bereitstellt. In jME gibt es Objekte von drei Klassen, die Elemente eines Scenegraphen sein können, die Klassen Spatial, Geometry und Node. Wie auf der Abbildung 3.4 zu sehen ist, ist die Klasse Spatial die Oberklasse. Man kann Spatial nicht instanzieren, da es sich um eine abstrakte Klasse handelt. In Spatial werden aber die Lage und die Rotation gespeichert, jedes Element des Scenegraphen hat also seine eigenen Standort, der immer relativ zum Elternelement ist. Man kann auch sogenannten RenderStates setzen, der die Lichter und Texturen beschreibt, dazu gibt es im weiteren Verlauf des Tutorials mehr. Die Klasse Geometry und ihre Unterklassen beinhalten all die geometrischen Daten, das heisst die Dreiecke, die ein 3D-Objekt enthalten. Das heisst jedes Objekt, das wir am Schluss auf dem Bildschirm sehen ist ein Geometry-Objekt. Die Box, die wir im ersten Demo benutzt haben (siehe Listing 3.1 und 3.2 Zeile 21), ist auch im UML-Diagramm zu erkennen. Bei den Geometry-Objekten handelt es sich aber nur um die Endblätter unseres Scenegraph-Baumes. Die Knoten werden durch die Klasse Node verwaltet, der 21 3.2. Scenegraph Abbildung 3.5.: UML Diagramm der RenderStates man beliebig viele Kindknoten und Kind Geometry-Objekte anfügen kann, dazu können Sie noch einmal die Abbildung 3.3 betrachten. Für alles das wir in jME auf den Bildschirm sehen existieren also eigene Geometry-Unterklassen oder spezialisierte Node-Klassen. Solche spezialisieren Klassen sind schwierig auf eine gute Art und Weise zu implementieren und das ist eine der eigentlichen Schwierigkeiten, wenn man eine 3D-Engine entwickeln will. Man kann ohne zu übertreiben sagen, dass der Scenegraph das Rückgrat der 3D-Engine von jME ist. Das Design und die Implementierung von jME ist dabei sehr komplex. Der Autor von jME sagt in seiner Internetseite, dass er das Design von jME an die beiden Bücher von David H. Eberly angelehnt hat [Ebe00] und [Ebe04]. In [Ebe00] wird die Implementierung einer Scenegraph basierten Engine beschrieben. Der Text ist dabei sehr mathematisch gehalten. In [Ebe04] wird die Architektur dieser weiterentwickelten Scenegraph-Engine aus einem etwas höheren Level beschrieben. Beide Bücher sind über 500 Seiten dick, das sollte nur ein kleiner Hinweis sein, wie komplex eine moderne 3D-Engine ist. Der ganze Scenegraph entspricht ausserdem dem Composite Pattern, wie er aus dem allseits bekannten Buch der Gang of Four, Design Patterns [GHJV95] bekannt ist. Im Scenegraphen, dessen Wurzel rootNode in unserem Programm verwendet wird, wird einmal pro Frame die Methode draw aufgerufen. Dieser Aufruf bringt die ganze Engine zum Laufen. Im Artikel [BZ] von Avi Bar-Zeev finden Sie noch einige weiter Anmerkungen zu einem Scenegraphen allgemein. Die meisten modernen 3D-Engines benutzen eine Scenegraph-Implementierung, die der von jME ähnlich ist. 3.2.2. Zustände im Kontext von jME Wir werden im Verlauf des Tutorials auch einige Zuständen verwenden. Vor allem Lichter in Kapitel 7 und Texturen in Kapitel 8. Ein Zustand den wir in jME verwenden können ist immer eine Unterklasse von RenderState. Abbildung zeigt ein UML-Diagramm mit einigen der Zustände. Sehen Sie sich nun noch einmal die Abbildung mit den Scenegraph-Elementen (Abbildung 3.4) an. Wie Sie auf dieser Abbildung sehen können, können wir zu jedem Element unseres Scenegraphen, das heisst zu jedem Spatial, einen RenderState also einen Zustand setzen, mit der treffenden Methode setRenderState. Der Zustand, den wir setzen, wirkt sich dann im Scenegraph auf alle Kinder, des jeweiligen Spatials aus dessen Zustand wir erweitern. Betrachten wir dazu noch einmal die Hierarchie auf Abbildung 3.3. Falls wir im Raum1 ein Licht setzen, beleuchtet das Licht alle Elemente der Tischgruppe und auch den Stuhl, Raum2 merkt aber nichts vom Licht. 4 Wie funktioniert 3D-Grak 4.1. Was macht eine 3D-Engine? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22 4.2. Das 3D-Koordinatensystem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23 4.2.1. 2D-Koordinatensystem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23 4.2.2. 3D-Koordinatensystem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23 4.2.3. Model-Space vs. World-Space . . . . . . . . . . . . . . . . . . . . . . . . . . 24 4.3. Transformationen im Raum . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25 4.3.1. Verschiebungen (engl. translation) . . . . . . . . . . . . . . . . . . . . . . . . 25 4.3.2. Rotationen (engl. rotation) . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25 4.3.3. Skalierungen (engl. scaling) . . . . . . . . . . . . . . . . . . . . . . . . . . . 25 4.3.4. Alle Bewegungen zusammen . . . . . . . . . . . . . . . . . . . . . . . . . . . 26 4.4. Perspektive und Projektion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26 4.5. Kamera . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27 4.6. jME, OpenGL, DirectX, 3D-Pipeline: Was ist das? Was machen die? . . . . . . . . 29 4.7. Ressourcen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30 4.1. Was macht eine 3D-Engine? Man kann kurz sagen, eine 3D-Engine ist verantwortlich dafür die 3D-Daten zu verwalten und zu bestimmen, was auf den Bildschirm kommt und wie es zu zeichnen ist. Wir haben oben im Kapitel vor allem das was beschrieben, das wie ist dabei meistens die Aufgabe der sogenannten Rendering-Pipeline. Das Ziel der Rendering-Pipeline ist es ein zweidimensionales Bild auf dem Bildschirm zu erzeugen, aus dreidimensionalen Objekten, einer virtuellen Kamera, Texturen, Lichtern und noch einigem mehr. Als Benutzer einer 3D-Engine haben wir es dabei glücklicherweise um einiges leichter. Die 3D-Engine ist meistens ein Wrapper um eine gegebene 3D-Schnittstelle, wie OpenGL oder Direct3D und abstrahiert dementsprechend die so genannte Rendering-Pipeline. Die Rendering-Pipeline selbst ist heutzutage auch direkt in der Hardware implementiert, in den sogenannten 3D-Beschleunigern. Das sind 3D-Grafikkarten, die mittlerweile wohl jeder Computer enthält. Wir müssen uns also um die meisten Low-Level Angelegenheiten nicht mehr selbst kümmern und können ganz einfach die 3D-Engine walten lassen. Dennoch ist es von Vorteil, zumindest ein grundlegendes Wissen zu haben, was in dieser ominösen Pipeline passiert. Und genau deshalb werden wir uns in diesem Teil des Tutorials ein wenig um die theoretischen Grundlagen kümmern. Ich hoffe, dass ich damit ein wenig Licht in das bereits jetzt von Fachwörtern gespickte Tutorial bringen kann. Gezwungenermassen kann ich hier keinen tiefen Einblick in das Thema geben. Ich verweise die interessierten Leser aber auf die kommentierte Bibliographie am Ende dieses Kapitels in Abschnitt 4.7. 22 4.2. Das 3D-Koordinatensystem 23 Abbildung 4.1.: Ein rechthändiges 3D-Koordinatensystem Trotz allem wird hier ein wenig Vorwissen vorausgesetzt. Der Leser sollte wissen, was ein Vektor und eine Matrix ist. Grundlegende Vektor- und Matrix-Operationen sollten, deshalb auch schon verstanden werden. Wer noch ein wenig Mühe mit dieser Materie hat oder sein mathematisches Wissen ganz allgemein ein bisschen auffrischen will findet im Buch von Fletcher und Parberry 3D Math Primer for Graphics and Game Development [DP02] eine hervorragende Einführung. 4.2. Das 3D-Koordinatensystem 4.2.1. 2D-Koordinatensystem Fast jeder wird wohl schon von einem kartesischen 2D-Koordinatensystem gehört haben und es wohl sogar selbst benutzt haben. Ein Koordinatensystem besteht aus einer oder mehreren Zahlengeraden. Jeder dieser Zahlengeraden heisst Achse. Die Anzahl Achsen in einem System entspricht der Anzahl Dimensionen, die in diesem System repräsentiert werden. In einem 2D-Koordinatensystem sind das normalerweise die x- und die y-Achse. Diese Achsen entspringen dem Ursprung (engl. origin) des Systems. Dieser Ursprung entspricht dem Punkt (0; 0) in einem 2D-System, deshalb wird der Ursprung häufig auch Nullpunkt genannt. 4.2.2. 3D-Koordinatensystem Ein 3D-System fügt nun dem 2D-System eine dritte Tiefendimension hinzu. Diese neue Achse wird normalerweise als z-Achse bezeichnet. Alle drei Achsen in einem 3D-Koordinatensystem stehen im rechten, 90 Grad, Winkel zueinander. Es gibt zwei Versionen des 3D-Koordinatensystem, die häufig benutzt werden. Das linkshändige System (engl: left-handed system) und das rechtshändige System (engl: right-handed system). Der Unterschied zwischen den beiden ist die Richtung in welche die z-Achse zeigt. In einem linkshändigen System zeigt die positive z-Achse gegen vorwärts und negative Zahlen zeigen sozusagen hinten von uns weg. In einem 4.2. Das 3D-Koordinatensystem 24 rechtshändigen System ist das genau umgekehrt. jME benutzt ein rechtshändiges Koordinatensystem, da auch das darunterliegende OpenGL ein rechtshändiges Koordinatensystem benutzt. Das linkshändige Koordinatensystem wird von der anderen bekannten 3D-API Direct3D benutzt. Auf der Abbildung 4.1 sehen Sie ein rechtshändiges Koordinatensystem. 4.2.3. Model-Space vs. World-Space Im Bereich der 3D-Grafik benutzt man oft verschiedene Koordinatensysteme. Im Besonderen unterscheidet man zwischen dem sogenannten World Space, dem Model- oder Object Space und dem Camera Space. Man benötigt verschiedene Koordinatensysteme, weil einige Informationen nur in einem bestimmten Kontext (das bedeutet in unserem Fall in einem bestimmtem Koordinatensystem) von Nutzen sind. Das ganze Konzept ist am Anfang vielleicht ein bisschen schwierig zu verstehen aber die Konzepte sind grundlegend. Am besten wir beginnen mit dem World Space. Das World Space ist ein absolutes Koordinatensystem. Jede Position auf der Welt hat seine eigenen Koordinaten, die unverwechselbar sind. Am besten kann man sich das mit einer normalen Weltkarte verbildlichen. Auf einer Weltkarte ist die Welt in Längen- und Breitengrade aufgeteilt. Jeder Ort auf der Welt lässt sich nun eindeutig mit diesen Koordinaten beschreiben. Die Koordinaten von Freiburg sind zum Beispiel, 46,8◦ nördliche Breite, 7,15◦ östliche Länge. Auch jede 3D-Welt, besitzt nun ein solches absolutes Koordinatensystem, in dem jeder Ort eindeutig durch die x-, y-, z-Koordinaten beschrieben werden kann. Um dieses Konzept besser zu illustrieren, sind in den meisten Demos, die dieses Kapitel begleiten die Achsen des World Space zu sehen. Jedes Objekt in unserer 3D-Welt hat hingegen sein eigenes lokales Koordinatensystem, besitzt seinen eigenen Achsen und hat seinen eigenen Nullpunkt. Der Nullpunkt kann beispielsweise in der Mitte des Objekts liegen. Die Achsen zeigen an, welche Richtungen für das Objekt “oben”, “unten”, “rechts”, “links”, “vorne” und “hinten” sind. Das ist genau so in der “echten” Welt. Für mich bedeutet zum Beispiel “links” etwas anderes als für jemanden, der vis-à-vis von mir sitzt. Diese lokale Koordinatensystem wird Object Space genannt. Ausserdem bewegt sich das lokale Koordinatensystem mit dem Objekt. Wenn sie zum Beispiel den linken Arm ausstrecken, wird dieser linke Arm immer einen Meter links von ihnen sein, egal wie sie sich im Raum umher bewegen. Wenn sie sich aber bewegen, wird sich ihre Position in der Welt verändern. Die Position, die sie innehaben, wenn sie sich umher bewegen wird in Weltkoordinaten ausgedrückt. Natürlich hat auch ihr linker Arm, den sie immer noch ausgestreckt haben eine Position im World Space. Es genügt nun aber, wenn wir nur die Position ihres Nullpunktes kennen (nehmen wir an der Nullpunkt von ihnen befindet sich in der Körpermitte). Die Position ihres linken Armes kann man nun leicht ausrechnen, weil man weiss, dass sich ihr linker Arm ein Meter links von ihrem Nullpunkt entfernt befindet. Weil sich ein 3D-Objekt mitsamt seinem lokalen Koordinatensystem im absoluten globalen Koordinatensystem bewegt, kann es hilfreich sein, das globale Koordinatensystem (World Space) als Eltern-Space und das lokale Koordinatensystem als Kind-Space zu verstehen. Ausserdem ist es sehr nützlich, die 3DObjekte in weiter Subobjekte zu unterteilen. Der Roboter hat zum Beispiel einen Kopf mit einer Nase, zwei Arme und zwei Beine. Falls der Roboter nun nicken will, bewegt sich sein Kopf mitsamt Nase relativ zum Roboterkörper. Um den ganzen Roboter zu bewegen müssen, wie aber den Kopf und die Nase mit bewegen. Wir erhalten also ein Hierarchie von Objekten, die sich alle relativ zueinander bewegen. Genau diese Hierarchie kann man nun mit einem Scenegraph implementieren und das macht es uns leicht in jME1 solche Hierarchien von Objekten zu benutzen. 1 Im Gegensatz zur “herkömmlichen” 3D-Programmierung mit OpenGL und Direct3D, in denen man solche Hierarchien und relative Bewegungen mühsam von Hand selbst verwalten muss. Ausser man implementiert seinen eigenen Scenegraph natürlich. 4.3. Transformationen im Raum 25 4.3. Transformationen im Raum Transformationen ist ganz einfach die englische Bezeichnung für Bewegungen im Raum. Was können wir alles für Bewegungen machen? Trivialerweise können wir Objekte ganz einfach verschieben. Wir können Objekte auch vergrössern oder verkleinern also skalieren. Zu guter Letzt können wir Objekte auch rotieren. Mittels dieser drei Transformationen können wir fast alle Bewegungen, die ein Objekt im dreidimensionalen Raum macht, nachbilden. Wir müssen nun genau definieren, was wir eigentlich verschieben und da sollte uns nun die obige Diskussion mit dem Model Space und dem World Space helfen. Wenn wir ein Objekt bewegen, bewegen wir das Objekt mitsamt seinem relativen Koordinatensystem, dem Model Space, im absoluten Raum, also dem World Space. Normalerweise werden alle diese Transformationen zusammengeführt und in einer einzigen Matrix gespeichert. Fast alle Bücher, die sich mit der Thematik beschäftigen gehen auch näher darauf ein. jME macht uns aber die ganze Aufgabe ein wenig leichter, da für alle Transformationen geeignete Methoden zur Verfügung stehen. Intern wird aber auch jME eine einzige Bewegungsmatrix aus unseren Anweisungen erzeugen. 4.3.1. Verschiebungen (engl. translation) Um ein Objekt im Raum zu verschieben rufen wir folgende Methode auf: void setLocalTranslation ( Vector3f localTranslation ); Beim Parameter localTranslation handelt es sich um den Punkt wohin wir das Objekt im absoluten Raum bewegen möchten. 4.3.2. Rotationen (engl. rotation) Wir können alle Objekte im Raum um auch rotieren und dazu rufen wir diese Methode auf: void setLocalRotation ( Matrix3f rotation ); Vielleicht werden sich jetzt einige wundern, warum wir das ganze nun mit einer Matrix aufrufen müssen. Wie oben bereits erwähnt kann man jede Bewegung im Raum auch wunderbar platz sparend in einer einzigen Matrix speichern, aber zur Illustration ist das natürlich nicht so hilfreich. Deshalb stellt uns jME eine Methode zur Verfügung mit der wir auf einfache Weise eine Rotationsmatrix herstellen können: void fromAngleNormalAxis ( float angle , Vector3f axis ); Diese Methode besitzt zwei Parameter. Mit dem ersten Parameter angle bestimmen wir einen Winkel in Radianten gemessen und mit dem zweiten Parameter axis geben wir einen normalisierten Vektor an um den rotiert werden soll. Wenn wir zum Beispiel unser Objekt box um 45◦ um die x-Achse drehen wollen rufen wir folgenden Code auf: Matrix3f rotMat = new Matrix3f () ; rotMat . fromAngleNormalAxis ( FastMath . DEG_TO_RAD * 45.0 f , new Vector3f ( 1.0f , 0.0f , 0.0 f)); box . setLocalRotation ( rotMat ); Das ganze sieht vielleicht ein wenig kompliziert aus, ist aber sehr flexibel. Wir können unser Objekt zum Beispiel um jeden beliebigen Winkel drehen und sind nicht auf die Weltachsen beschränkt. 4.3.3. Skalierungen (engl. scaling) Falls ein Objekt nicht die gewünschte Grösse hat können wir es auch vergrössern oder verkleinern mit folgenden Methoden: 26 4.4. Perspektive und Projektion void setLocalScale ( float localScale ); void setLocalScale ( Vector3f localScale ); Mit der ersten Methode skalieren wir das ganze Objekt um einen bestimmten Faktor localScale. Die zweite Methode skaliert das Objekt auf jeder Achse um einen anderen Wert, den wir mit einem dreidimensionalen Vektor bestimmen. 4.3.4. Alle Bewegungen zusammen Als abschliessendes Beispiel wollen wir zeigen, wie wir die Grösse eines Würfels verdoppeln, ihn 45◦ um die x-Achse drehen und anschliessend zum Punkt (5.0, 2.0, 3.0) verschieben: Listing 4.1: SimpleTransformation.java 28 29 30 protected void simpleInitGame () { display . setTitle ( " Simple Translation " ); 31 // create a unit - cube which is initially located in the world center Box box = new Box ( " My box " , new Vector3f ( -1, -1, -1 ) , new Vector3f ( 1, 1, 1 ) ); Node n = new Node ( " boxholder " ); Axis . createAxis ( n , 5 ); 32 33 34 35 36 // move our box n. setLocalScale ( 2.0 f ); 37 38 39 // rotate the box Matrix3f rotMat = new Matrix3f () ; rotMat . fromAngleNormalAxis ( FastMath . DEG_TO_RAD * 45.0 f , new Vector3f ( 1.0f , 0.0f , 0.0 f) ); n. setLocalRotation ( rotMat ); 40 41 42 43 44 45 // scale the box n. setLocalTranslation ( new Vector3f ( 5.0f , 2.0f , 3.0 f ) ); 46 47 48 // attach all to our rootNode n. attachChild ( box ); rootNode . attachChild ( n ); Axis . createAxis ( rootNode , 12 ); 49 50 51 52 53 lightState . detachAll () ; 54 55 } Das ganze Beispiel finden Sie auch in Demos unter dem Namen SimpleTransformation.java im Unterordner theory in den Demos (siehe auch Anhang A). Man sollte noch anfügen, dass jME alle Verschiebungen immer in der gleiche Reihenfolge ausführt. Zuerst wird das Objekt skaliert, dann wird es rotiert und schlussendlich verschoben und zwar ganz egal in welcher Reihenfolge wir die oben genannten Methoden aufrufen. Also bitte denken Sie daran, wenn einmal etwas sich aus ihrer Sicht unerwartet bewegt. Die Transformationen wirken sich zudem nicht nur auf das Objekt selbst aus, sondern auf alle Kindobjekte, die mit Vaterobjekt im Scenegraph verbunden sind. Das ist normalerweise auch die gewünschte Eigenschaft von Scenegraph-Hierarchien. 4.4. Perspektive und Projektion Es gibt viele verschiedene Methoden wie man die runde Erde auf ein rechteckiges Papier projizieren kann. Und analog dazu gibt es auch viele verschiedene Methoden, wie man eine dreidimensionale Szene 27 4.5. Kamera Abbildung 4.2.: Ein Würfel rotiert, bewegt und skaliert auf den rechteckigen Bildschirm projiziert. Die bekannteste Methode ist die perspektive Projektion (engl. perspective projection). In dieser Projektion erscheinen Objekte, die weit von der Kamera entfernt sind kleiner auf dem Bildschirm. Zusätzlich können parallele Linien am Horizont konvergieren. Die perspektivische Projektion simuliert also nach, wie wir in der Realität Grössen erfassen. Wenn wir ein Meter vor einem Baum stehen erscheint er grösser, als wenn wir hundert Meter vom selben Baum entfernt sind. Eine weitere sehr verbreitete Projektionsmethode ist die orthogonale Projektion (engl. orthogonal projection). Sie wird auch parallele Projektion genannt. Wenn wir orthogonale Projektion verwenden verändert sich die Grösse der Objekte nicht, egal wie weit entfernt die Kamera vor ihr steht. Ausserdem bleiben parallele Linien auch nach der Projektion parallel. Ich hoffe, die ganzen Vorgänge werden klarer, wenn man sich die Abbildung 4.3 ansieht. Links sieht man eine perspektivische Projektion, rechts ist eine orthogonale Projektion zu sehen. Die Kamera ist immer an der gleichen Stelle positioniert. Man nennt diese Methoden Projektionen, weil vom 3D-Raum das Bild auf eine 2D-Ebene projiziert wird. Wir werden uns im weiteren Verlauf auf die perspektivische Projektion beschränken. Die perspektivische Projektion ist die am meisten benutzte Technik im EchtzeitRendering. Orthogonale Projektion wird vor allem für technische CAD-Anwendungen und 2D-Spiele benutzt. In jME wird zurzeit nur die perspektivische Projektion unterstützt. 4.5. Kamera Zusätzlich zu dem World Space und dem Model Space haben wir noch einen Camera Space. Der Camera Space ist genau der Raum den wir sehen, wenn wir durch unsere virtuelle Kamera die Szene betrachten. Die Kamera hat eine eigen Position im Raum und eine Richtung in die sie schaut. 28 4.5. Kamera Abbildung 4.3.: Projektionen Abbildung 4.4.: 3D-Pipeline 4.6. jME, OpenGL, DirectX, 3D-Pipeline: Was ist das? Was machen die? 29 Abbildung 4.5.: jME und OpenGL 4.6. jME, OpenGL, DirectX, 3D-Pipeline: Was ist das? Was machen die? Wir haben jetzt häufig von der sogenannten 3D-Pipeline geredet ohne dabei zu erwähnen was diese Pipeline genau macht. Der Begriff Pipeline wird ja vor allem im Ölsektor benutzt, was hat jetzt dieser Begriff in der 3D-Grafik zu suchen? In einer 3D-Pipeline geben wir unsere 3D-Daten als Input an, in der Pipeline selbst gehen dann durch Daten durch verschiedene Stufen, in denen die Daten jeweils verändert werden. Als Endresultat erhalten wir am Ende der Pipeline das fertige Bild auf dem Bildschirm. Abbildung 4.4 sollte die ganze Aussage ein wenig verdeutlichen. Sie können auf dem Bild auch erkennen, das ein grosser Teil der Arbeit von der Hardware, das heisst konkret den 3D-Grafikbeschleunigern implementiert, wird. Die Liste, mit all den Sachen, die eine 3D-Karte direkt in der Hardware implementiert wird immer länger. Früher unterstützten diese Karten nur das sogenannte Rasterizing, das Zeichnen der einzelnen Pixel auf den Bildschirm, heute wird selbst das Transformieren und Beleuchten von 3D-Objekten von der Hardware ausgeführt. Die 3D-Karten implementieren dabei meistens einen 3D-API, von denen es heute zwei Bekannte gibt, Direct3D und OpenGL. Das heisst anstatt das wir als Programmierer direkt auf die 3D-Hardware programmieren, benutzen wir die 3D-Schnittstellen Direct3D und OpenGL, die dann die gewünschten Operationen direkt auf der Hardware ausführen. Die genau Schnittstelle zwischen 3D-API und Hardware ist dabei der 3D-Treiber, der von Grafikkartenentwicklern sehr häufig upgedatet wird. Das ist das sogenannte Layering, das in der Informatik allumfassend ist. Man kann mit Direct3D und OpenGL sehr viele Sachen kontrollieren, viele Sachen, die man oft nur selten braucht. Ausserdem ist das ganze Verwalten von 3D-Objekten auf tiefer Ebene auch sehr kompliziert. Deshalb programmiert man mit einer 3D-Engine noch eine zusätzliche Schicht über OpenGL oder Direct3D. Der genau gleiche Weg macht auch jME, wie Sie auf Abbildung 4.5 erkennen können. Wer sich dafür interessiert, was in der 3D-Pipeline noch alles gemacht wird und mit welchen Algorithmen die Konzepte implementiert werden sollte sich das 3D-Pipeline Tutorial [Sal01] durchlesen. In diesem Tutorial werden wir uns nicht mehr weiter mit OpenGL und Direct3D beschäftigen, wer sich aber dafür interessiert, dem werden die Bücher OpenGL Game Programming [AH01] und Introduction to 3D Game Programming[Lun03] wärmstens empfohlen. 4.7. Ressourcen 30 4.7. Ressourcen Wir haben in diesem Kapitel einen Schnelldurchlauf durch die Theorie des 3D-Rendering gemacht. Da ist es kein Wunder, wenn viele Details auf der Strecke bleiben. Viele Standardwerke in diesem Bereich treten das Ganze Thema schliesslich auf mehreren hundert Seiten breit. Ein Standardwerk des 3D-Rendering zweifelsohne Real-Time Rendering [AMH02] von Akenine-Möller und Haines in dem alle theoretischen Konzepte auch von den kommenden Kapiteln befinden. Ein weiteres Standardwerk im Bereich der Computergrafik ist Computer Graphics: Principles and Practice [FvDFH95]. 5 Interaktion 5.1. Einfache Interaktionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31 5.2. Ein objektorientierter Ansatz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34 5.1. Einfache Interaktionen Was macht eine Demo interaktiv könnte man fragen? Eine Antwort liegt auf der Hand, man muss mit der Applikation interagieren können. Interagieren können wir mit unserer Applikation, wenn sie auf unsere Befehle reagiert. In diesem Kapitel werden wir Ihnen zeigen, wie sie in jME eigene Tastaturbefehle erzeugen und diese später dann abfragen können. Legen wir gleich los mit dem Quellcode. Sie finden den Quellcode wie immer auf der CD mit dem Pfad demos/keyinput/InputDemo.java. Im Anhang B wird erklärt, wie Sie die Demo starten und gegebenfalls verändern können. Wir haben hier auf einen Screenshot verzichtet, weil wir in dieser Demo nur einen Würfel sehen. Diese Demo ist interaktiv, weil Sie mit den Tasten ’U’ und ’J’ den Würfel entlang der y-Achse verschieben können, das bedeutet nach oben oder unten, falls Sie die Kamera innerhalb der Demo nicht ändern. Mit den Tasten ’H’ beziehungsweise ’K’ können Sie sich entlang der x-Achse verschieben, was einer Verschiebung nach links oder rechts entspricht, von vorne angesehen. Listing 5.1: InputDemo.java 1 package keyinput ; 2 3 4 5 6 7 import import import import import com . jme . input . KeyBindingManager ; com . jme . input . KeyInput ; com . jme . math . Vector3f ; com . jme . scene . Text ; com . jme . scene . shape . Box ; 8 9 import helper . TutorialGame ; 10 11 12 13 14 15 16 17 18 19 /* * * A demo to present how input handling and textdisplay works * @author Daniel Egger */ public class InputDemo extends TutorialGame { private Box b; private static final float DELTA = 0.005 f; private Text boxPrint ; 20 31 32 5.1. Einfache Interaktionen 21 22 23 24 public static void main ( String [] args ) { new InputDemo () ; } 25 26 27 28 29 protected void simpleInitGame () { // set the title of our demo display . setTitle ( " Input Demo " ); 30 // create and attach a new box b = new Box ( " box " , new Vector3f ( -1, -1, -1) , new Vector3f ( 1, 1, 1 ) ); rootNode . attachChild ( b ); 31 32 33 34 // create some key actions // assign the key ’U ’ to the action boxUp KeyBindingManager . getKeyBindingManager () . set ( ; // assign the key ’J ’ to the action bowDown KeyBindingManager . getKeyBindingManager () . set ( ); // assign the key ’H ’ the the action boxLeft KeyBindingManager . getKeyBindingManager () . set ( ); // assign the key ’L ’ to the action boxRight KeyBindingManager . getKeyBindingManager () . set ( KEY_K ); 35 36 37 38 39 40 41 42 43 " boxUp " , KeyInput . KEY_U ) " boxDown " , KeyInput . KEY_J " boxLeft " , KeyInput . KEY_H " boxRight " , KeyInput . 44 // create the text node Vector3f boxPosition = b. getLocalTranslation () ; boxPrint = new Text ( " boxPrint " , " Box Position : x=" + boxPosition .x + " ; y=" + boxPosition .y + "; z=" + boxPosition .z ); boxPrint . setLocalTranslation ( new Vector3f ( 0, 20 , 0 ) ); textNode . attachChild ( boxPrint ); 45 46 47 48 49 50 51 } 52 53 54 55 protected void simpleUpdate () { Vector3f boxPosition = b. getLocalTranslation () ; 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 // check for the key actions if ( KeyBindingManager . getKeyBindingManager () . isValidCommand ( " boxUp " , true ) ) { boxPosition .y += DELTA ; } if ( KeyBindingManager . getKeyBindingManager () . isValidCommand ( " boxDown " , true ) ) { boxPosition .y -= DELTA ; } if ( KeyBindingManager . getKeyBindingManager () . isValidCommand ( " boxLeft " , true ) ) { boxPosition .x -= DELTA ; } 5.1. Einfache Interaktionen 33 if ( KeyBindingManager . getKeyBindingManager () . isValidCommand ( " boxRight " , true ) ) { boxPosition .x += DELTA ; } 73 74 75 76 77 78 // set the box to its new position b. setLocalTranslation ( boxPosition ); 79 80 81 // update our text string boxPrint . print ( " Box Position : x=" + boxPosition .x + "; y=" + boxPosition .y + "; z=" + boxPosition .z ); 82 83 84 } 85 86 } In den Zeilen 35 bis 43 verbinden wir Tasten mit Aktionen. Tastatureingaben werden in jME, wie viele andere Sachen auch abstrahiert. Der KeyInputManager ist ein Singleton (sehen Sie dazu Design Patterns[GHJV95] auf das wir immer zugreifen können. Sehen wir uns eine Zeile noch etwas genauer an: // assign the key ’U ’ to the action boxUp KeyBindingManager . getKeyBindingManager () . set (" boxUp " , KeyInput . KEY_U ); Hier verbinden wir die Aktion boxUp mit der Taste ’U’ auf der Tastatur. Der Name der Aktion kann dabei ein beliebiger String sein. Alle Tasten, die Ihnen zur Verfügung stehen, sind in der Klasse KeyInput als Konstanten definiert. Für annähernd jede Taste die sich auf einer Tastatur befindet gibt es dabei eine eigene Konstante. Später werden wir nicht testen, ob die Taste ’U’ gedrückt wurde, wir werden testen ob die Aktion boxUp eingetroffen ist. Diese Annäherung hat einige Vorteile: wir können mehrere Tasten der gleichen Aktion zuteilen, ausserdem ist es für einen Anwender leicht seine eigenen Tasten für Aktionen zu definieren beziehungsweise umzustellen. Wenn wir direkt auf Tastatureingaben reagieren würden, wäre das wesentlich komplizierter. In den Zeilen 53 bis 83 benutzen wir zum ersten Mal die Methode simpleUpdate. simpleUpdate wird jeden Frame, das heisst jeden Zwischenschritt, den unsere Demo macht, aufgerufen. Wir müssen schliesslich auch in jedem Zwischenschritt testen, ob eine Taste gedrückt wurde. Um nun abzufragen ob eine Aktion eingetreten ist rufen wir folgende Methode auf: if ( KeyBindingManager . getKeyBindingManager () . isValidCommand ( " boxUp " , true ) ) { boxPosition .y += DELTA ; } Der Name der Aktion muss natürlich mit einer oben definierten Aktion übereinstimmen, sonst passiert nichts. Das kann ein Fehler sein, den man gerne und leicht macht. Als zweites Argument geben wir noch ein true mit. Es bedeutet, dass die Aktion in jedem Frame ausgeführt werden soll. Hätten wir hier ein false geschrieben, würde die Aktion nur einmal pro Tastendruck ausgeführt. Das gleiche Prozedere wiederholen wir mit jeder Aktion die wir oben definiert haben. Am Schluss können wir den Würfel mit setLocalTranslation auf die neue Position verschieben. Neben den Tastenaktionen wird in dieser Demo auch gezeigt, wie man Text auf dem Bildschirm darstellen kann. Dazu haben wir eine Instanz der Klasse Text namens boxPrint erzeugt. In Zeile 47 und 48 sehen wir, wie das abläuft. Wie jedem anderen zu zeichnenden Objekt, müssen wir auch unserem Textobjekt einen Namen geben. Den Text den wir darstellen wollen, ist die Position des Würfels, die in boxPosition gespeichert ist. Danach verschieben wir unser Textobjekt mit setLocalTranslation in den unteren rechten Rand des Bildschirms. Hierbei gilt zu beachten, dass für Textobjekte nicht das gleiche Koordinatensystem verwendet wird wie für die üblichen 3D-Objekte. Dieses Koordinatensystem hat seinen Ursprung im Punkt rechts unten im Bildschirm und erstreckt sich Pixel für Pixel entsprechend der Bildschirmauflösung die wir gewählt haben. Schlussendlich fügen wir unser Textobjekt der textNode 34 5.2. Ein objektorientierter Ansatz an. textNode ist ein spezieller Scenegraph, der dazu optimiert wurde Texte auf dem Bildschirm darzustellen und auch für das andere Koordinatensystem verantwortlich ist. Da sich die Position des Würfels in jedem Frame ändern kann, müssen wir auch den Text von printBox jeden Frame ändern. Dazu benutzen wir die Methode print, wie auf Zeile 83 und 84 zu sehen. 5.2. Ein objektorientierter Ansatz Viele OO-Puristen werden vielleicht leicht die Nase gerümpft haben, als sie das oben gezeigte Verfahren gesehen haben. Das Vorgehen mit der Methode isValidCommand werden vielleicht einige Programmierer als “dirty trick” abwerten und das ist es vielleicht sogar, aber es ist die schnellste Methode um Tastatureingaben zu überprüfen. Einen Anderen Ansatz kann man gehen, indem man einen ähnlichen Weg geht wie im Entwurfsmuster Befehl (engl. Command) geht, dieses Entwurfsmuster ist mit vielen anderen Entfursmuster im Katalog der Gang of Four[GHJV95] beschrieben. Wir kapseln nun unsere Aktionen in sogenannten Actions. Wie das genau geht, wird in diesem Abschnitt erklärt. Diese Demo macht genau das gleiche wie die vorherige Demo, auch die Tastaturbelegung ist die gleiche, das einzige das sich geändert hat ist die Art und Weise, wie wir auf die Tastatureingaben reagieren. Sie finden die Quellcodedatei im Unterverzeichnis demos/keyinput mit dem Namen InputActions.java. Listing 5.2: InputActions.java simpleInitGame and simpleUpdate 28 29 30 31 protected void simpleInitGame () { // set the title of our demo display . setTitle ( " Input Demo " ); 32 33 34 35 // create and attach a new box b = new Box ( " box " , new Vector3f ( -1, -1, -1 ) , new Vector3f ( 1, 1, 1 ) ); rootNode . attachChild ( b ); 36 37 38 39 40 41 42 43 44 45 // create some key actions // assign the key ’U ’ to the action boxUp KeyBindingManager . getKeyBindingManager () . set ( ; // assign the key ’J ’ to the action bowDown KeyBindingManager . getKeyBindingManager () . set ( ); // assign the key ’H ’ the the action boxLeft KeyBindingManager . getKeyBindingManager () . set ( ); // assign the key ’L ’ to the action boxRight KeyBindingManager . getKeyBindingManager () . set ( KEY_K ); " boxUp " , KeyInput . KEY_U ) " boxDown " , KeyInput . KEY_J " boxLeft " , KeyInput . KEY_H " boxRight " , KeyInput . 46 47 48 49 50 // create a new up action and assign it to the " boxUp " call KeyInputAction upAction = new KeyNodeUpAction ( b ); upAction . setKey ( " boxUp " ); input . addAction ( upAction ); 51 52 53 54 55 // create a new down action and assign it to the " boxDown " call KeyInputAction downAction = new KeyNodeDownAction ( b ); downAction . setKey ( " boxDown " ); input . addAction ( downAction ); 56 57 // create a left action and assign it to the " boxLeft " call 5.2. Ein objektorientierter Ansatz 35 KeyInputAction leftAction = new KeyNodeLeftAction ( b ); leftAction . setKey ( " boxLeft " ); input . addAction ( leftAction ); 58 59 60 61 // create a right action and assign it to the " boxRight " call KeyInputAction rightAction = new KeyNodeRightAction ( b ); rightAction . setKey ( " boxRight " ); input . addAction ( rightAction ); 62 63 64 65 66 // create the text node Vector3f boxPosition = b. getLocalTranslation () ; boxPrint = new Text ( " boxPrint " , " Box Position : x=" + boxPosition .x + "; y=" + boxPosition .y + "; z=" + boxPosition .z ); boxPrint . setLocalTranslation ( new Vector3f ( 0, 20 , 0 ) ); textNode . attachChild ( boxPrint ); 67 68 69 70 71 72 73 } 74 75 76 77 78 79 80 81 protected void simpleUpdate () { Vector3f boxPosition = b. getLocalTranslation () ; // update our text string boxPrint . print ( " Box Position : x=" + boxPosition .x + "; y=" + boxPosition .y + "; z=" + boxPosition .z ); } Wie Sie sehen können haben wir in der Methode simpleInitGame etwas hinzugefügt, namentlich die Codezeilen 48 bis 65, in denen wir die verschiedenen KeyNodeActions instanzieren und gleichzeitig haben wir die Methode simpleUpdate entschlankt indem wir alle if’s mit den isValidCommand Aufrufen entfernt haben. Sehen wir uns nun genauer an wie wir eine Action einsetzen: // create a new up action and assign it to the " boxUp " call KeyInputAction upAction = new KeyNodeUpAction ( b ); upAction . setKey ( " boxUp " ); input . addAction ( upAction ); 1. Als erstes erstellen wir eine Instanz einer Action, in diesem konkreten Falle eine Instanz von KeyNodeUpAction. Als Argument geben wir unsere Box an, weil das auch das Objekt ist, das wir während einem Tastendruck bewegen wollen. 2. Im nächsten Schritt bestimmen wir die Taste, die wir zu unserer Action hinzufügen. Wir benutzen dabei die gleichen Strings, die wir auch schon in der vorherigen Demo verwendete haben. Die Strings selbst müssen hier wiederum mir Hilfe des KeyBindingManager einer jeweiligen Taste hinzu gefügt werden. 3. Schlussendlich müssen wir unsere Action einem InputHandler anschliessen. Der InputHandler kümmert sich danach darum, das die Actions auch ausgeführt werden. Als Benutzer der Klasse TutorialGame können wir den InputHandler namens input verwenden, der in dieser Klasse als protected member definiert wurde. Diese drei Schritte bleiben jeweils die gleichen wenn wir Actions definieren und sie bestimmten Tasten hinzufügen. Die Schritte wiederholen sich auch für die nächsten drei Aktionen die wir definieren. In dieser Demo müssen wir natürlich auch die verschiedenen KeyInputActions definieren, die die eigentliche Arbeit machen, wenn wir auf eine bestimmte Taste drücken. Dazu schauen wir uns in der Datei InputActions.java die Zeilen 89 bis 103 genauer an: 5.2. Ein objektorientierter Ansatz 36 Abbildung 5.1.: Die verschiedenen InputActions Listing 5.3: InputActions.java KeyNodeUpAction 84 85 86 87 88 89 90 /* * * This class moves the node up the world y - axis */ class KeyNodeUpAction extends KeyInputAction { // the node to manipulate , our Box is also a spatial private Spatial node ; 91 KeyNodeUpAction ( Spatial node ) { this . node = node ; this . speed = 1; } 92 93 94 95 96 97 // every KeyAction has to implement the performAction method public void performAction ( InputActionEvent evt ) { node . getLocalTranslation () .y += ( speed * evt . getTime () ); } 98 99 100 101 102 103 } Jede Action, die wir deklarieren, muss ein Unterklasse von KeyInputAction sein, deshalb lassen wir auch unsere KeyNodeUpAction von KeyInputAction erben. Unser Konstruktor verlangt einen Spatial und eine float Variable speed als Argument. Wie wir bereits in Abbildung 3.4 gesehen haben ist Spatial die Oberklasse aller möglichen Blätter in unserem Scenegraphen. Im Konstruktor weisen wir dem Spatial node einfach ein gewünschtes Objekt zu. Die Arbeitsmaschine einer InputAction-Klasse ist die Methode performAction, die von uns implementiert werden muss, da sie als abstrakte Methode in InputAction definiert ist. Diese Methode wird jeweils aufgerufen, wenn wir die gewünschte Taste drücken In unserer Klasse KeyNodeUpAction wollen wir unser Objekt entlang der y-Achse nach oben bewegen, wir verändern also die y-Koordinate von unserem Objekt. Mit dem InputActionEvent-Argument evt können wir mit der Methode getTime die Zeit abrufen, die seit dem letzten Frame vergangen ist. Das ist genau die Zeit, die wir auch mit speed multiplizieren wollen um die gewünschte Bewegungsdistanz zu erhalten. Die anderen drei Action Klassen sehen fast gleich aus nur die Implementation der Methode performAction ändert sich ein Bisschen. Um den ganzen Sourcecode noch ein wenig zu visualisieren können Sie auf der Abbildung 5.1 noch ein kleines Klassendiagramm sehen, das die Vererbungssituation der verschiedenen KeyInputActions darstellt. 6 Simple Formen und Modelle 6.1. Einfache Formen und warum die Kugeln nicht rund sind . . . . . . . . . . . . . . 37 6.2. Komplexere Modelle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40 6.2.1. Modell-Formate . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40 6.2.2. Modelle laden in jME . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41 6.1. Einfache Formen und warum die Kugeln nicht rund sind In unseren ersten Programmen haben wir einen simplen Würfel erstellt. Aber jME kann noch andere simple geometrische Formen erzeugen. Zum Beispiel Kreise, Zylinder und noch einige mehr. Im Bereich der 3D-Grafik benutzt man sehr häufig die Worte Model und Mesh, wenn man von Formen spricht. Hier gibt es dazu ein Beispielprogramm. Wenn ich also in Zukunft von einem Modell spreche, ist nichts anderes gemeint als eine dreidimensionale Form. Listing 6.1: SimpleGeometry.java 40 41 42 43 protected void simpleInitGame () { // set a new title display . setTitle ( "A Demo of Some Simple Geometry " ); 44 45 46 47 48 49 50 // create a box Box b = new Box ( " box " , new Vector3f ( -1, -1, -1 ) , new Vector3f ( 1, 1, 1 ) ); b. setLocalTranslation ( new Vector3f ( -2, 2, 0 ) ); b. setModelBound ( new BoundingSphere () ); b. updateModelBound () ; rootNode . attachChild ( b ); 51 52 53 54 55 56 57 // create a sphere Sphere s = new Sphere ( " sphere " , 10 , 15 , 1.5 f ); s. setLocalTranslation ( new Vector3f ( 2, 2, 0 ) ); s. setModelBound ( new BoundingBox () ); s. updateModelBound () ; rootNode . attachChild ( s ); 58 59 60 61 62 // create a torus Torus t = new Torus ( " torus " , 20 , 10 , 0.5f , 1.5 f ); t. setLocalTranslation ( new Vector3f ( -2, -2, 0 ) ); t. setModelBound ( new BoundingBox () ); 37 6.1. Einfache Formen und warum die Kugeln nicht rund sind 38 Abbildung 6.1.: Ein Screenshot aus der Demo SimpleGeometry.java Links sind die normalen Modelle zu sehen, rechts sieht man die Drahtgittermodelle auf denen die einzelnen Punkte und Dreiecke zu erkennen sind. t. updateModelBound () ; rootNode . attachChild ( t ); 63 64 65 // create a cylinder Cylinder c = new Cylinder ( " cylinder " , 10 , 15 , 1.5f , 3 ); c. setLocalTranslation ( new Vector3f ( 2.5f , -2, 0 ) ); // rotate the cylinder 20 degrees around the y - axis Matrix3f m = new Matrix3f () ; m. loadIdentity () ; m. fromAngleNormalAxis ( FastMath . DEG_TO_RAD * 20.0 f , new Vector3f ( 0.0f , 1.0f , 0.0 f ) ); c. setLocalRotation ( m ); c. setModelBound ( new BoundingBox () ); c. updateModelBound () ; rootNode . attachChild ( c ); 66 67 68 69 70 71 72 73 74 75 76 77 78 initText () ; assignKeys () ; 79 80 81 } Hier wurde nur der essentielle Teil des Quellcodes abgedruckt, den Rest finden Sie auf der CD (siehe Anhang A) im Verzeichnis demos/geometry mit dem Dateinamen SimpleGeometry.java. Beim essentiellen Teil handelt es sich vorwiegend um die Methode simpleInitGame. Wie oben bereits erwähnt bleibt der Code in main in fast allen Demos gleich. Was sich vorwiegend von Demo zu Demo ändert sind die importierten Java-Pakete. Ausserdem wurden einige Hilfsanweisungen und dazugehörige Tastaturkommandos programmiert, diese sind aber für das Verständnis des Programmes nicht von Bedeutung. Bevor wir das Programm aber ganz erklären können, müssen wir noch ein wenig Theorie behandeln. jME ist nicht in der Lage Kurven und Rundungen darzustellen, sondern kann nur Dreiecke zeichnen. Das ist nicht nur ein Problem das jME allein betrifft, sondern ein Problem das alle heutigen 3D-Echtzeitrenderer trifft. Die 3D-Beschleuniger, die dafür verantwortlich sind die Grafik zu verarbeiten und auf den Bildschirm zu bringen, können nur Dreiecke (engl. Triangles) verarbeiten und wurden genau für diesen Vorgang optimiert. Diese Dreiecke werden intern als Liste von Punkten (engl. Vertices) gespeichert. jME benutzt die Klasse TriMesh um solche Listen von Punkten und Dreiecken zu verwalten. Es sollte also nicht erstaunen, wenn man erfährt, dass alle simplen geometrischen Formen direkt von TriMesh erben. Anstatt die von jME zur Verfügung gestellten Formen zu verwenden kann man auch von Hand 6.1. Einfache Formen und warum die Kugeln nicht rund sind 39 solche Listen von Punkten erstellen und die jeweiligen Dreiecke angeben indem man direkt eine Instanz von TriMesh erzeugt. Das wäre aber bereits für ein einfachen Würfel mit 8 Punkten und mindestens 12 Dreiecken eine ziemlich mühsame Aufgabe und ist fast ganz undenkbar, wenn man auf diese Weise ein Auto oder eine Person erstellen will. Wie das Programm aussieht, kann man auch auf dem Screenshot in Abbildung 6.1 erkennen. Links sind die jeweiligen Modelle im Standardmodus zu erkennen. Rechts sieht man die Wireframe Modelle und man kann die einzelnen Punkte und Dreiecke betrachten. Den wireframe Modus können Sie mit der Taste ’T’ einschalten und wieder ausschalten. Gehen wir das Programm nun schrittweise durch. Wie sie einen Würfel mit Box erstellen können wissen Sie ja bereits. Wir verschieben den Würfel nun nach links oben mit Hilfe von setLocalTranslation. Was in diesem Programm neu hinzu kommt sind die Zeilen 48 und 49. Wir erstellen hier eine Hülle für unseren Würfel mit dem Befehl setModelBound. Die Hülle um unsere Objekte ist eine einfache Möglichkeit unser Programm zu beschleunigen. Mit Hilfe von diesen Bounding Volumes kann der Scenegraph von jME einen einfachen und schnellen Test machen, ob ein Objekt überhaupt auf dem Bildschirm sichtbar ist und deshalb überhaupt verarbeitet werden muss. Für diese einfachen Objekte ist der Vorteil nicht auf Anhieb erkennbar, aber nehmen wir an wir haben ein sehr komplexes Modell von einem Obergegner, das wir mit einer Kugelhülle (engl. BoundingSphere) überziehen. Falls die Kugel die unser Gegner verhüllt nicht sichtbar ist, ist auch der Gegner selbst nicht sichtbar und jME versucht erst gar nicht ihn auf dem Bildschirm zu zeichnen, was ein sehr wichtiger Perfomancesteigerung unseres Programms bedeutet. Sie können die Hüllen (engl. BoundingVolumes) in der Demo sichtbar machen, indem Sie die Taste ’B’ drücken. In diesem Beispiel haben wir unseren Würfel mit einer Kugel überzogen was nicht sehr sinnvoll ist, aber so kann man den Effekt besser erkennen auf dem Bildschirm. Wenn wir dem Objekt eine Hülle zugewiesen haben, müssen wir die Hülle mit updateModelBound noch auf den neusten Stand bringen, damit die Hülle auch wirklich das ganze Objekt überzieht. Wie bereits erwähnt müssen wir alle Objekte, die wir auf dem Bildschirm sehen wollen an den Scenegraph anhängen In Zeile 52 bis 57 erstellen wir eine Kugel mit Sphere. Wie jedes andere Objekt in jME müssen wir unserer Kugel als erstes Argument einen Namen geben. Da sich unsere Kreativität in Grenzen hält, benennen wir die Kugel mit sphere. Die nächsten beiden Argumente geben an in wieviele Teile und somit Dreiecke unsere Kugel unterteilt wird. Das zweite Argument gibt an in wieviele Teile entlang der z-Achse unsere Kugel unterteilt wird, wir haben 10 genommen. Das dritte Argument gibt in wieviele Teile die Kugel im Radius unterteilt wird, in unserem Beispiel sind es 15. Diese Argumente sind relativ schwer zu beschreiben, am besten probieren Sie selbst einige Zahlen aus. Je grösser diese Zahlen sind desto “runder” wird die Kugel, sie wird in kleinere Dreiecke unterteilt es braucht aber auch mehr Power sie zu rendern. Je kleiner wir diese Zahlen wählen desto eckiger wird die Kugel, bis fast gar nicht mehr als Kugel erkennbar ist. Versuchen sie einmal die Werte 6 und 6 für diese Argumente, sie werden ein eckiges Gebilde erhalten, das nur entfernt einer Kugel ähnelt. Das vierte und letzte Argument ist der Radius der Kugel, wir haben hier 1.5 genommen. Das Zentrum der Kugel ist der Ursprung des Koordinatensystems also der Punkt (0; 0; 0). Wir verschieben die Kugel gegen oben rechts mit setLocalTranslation. Wiederum geben wir unserem Objekt eine Hülle und wir hängen es dem Scenegraphen an. Ein Torus ist ein geometrisches Objekt, das wie ein Donut aussieht. Der Torus wird ähnlich erstellt wie eine Kugel, es enthält aber zwei Radien einen inneren Radius und einen äusseren Radius. Ausserdem gibt es wieder zwei Argumente die, die Zerteilung in Dreiecke bestimmen. Am besten Sie spielen selbst ein bisschen mit den Argumenten herum. Wir verschieben den Torus ein wenig nach rechts unten und erstellen ausserdem eine Hülle. Als viertes und letztes Objekt erstellen wir einen Zylinder in den Zeilen 67 bis 77. Die jeweiligen Argumente schauen Sie sich am besten selbst an in der JavaDoc-Dokumentation [34] von jME, die Sie notfalls auch online finden können. Wir verschieben den Zylinder nach unten rechts und rotieren ihn ausserdem um 20◦ auf der y-Achse. Am Ende von simpleInitGame rufen wir noch die Methoden initText und assignKeys auf. Diese Methoden wurden auch in der Klasse SimpleGeometry definiert und kümmern sich um den anzuzeigenden Text (initText) und stellt ausserdem einige neue Tastenkombinationen zur Verfügung 6.2. Komplexere Modelle 40 Abbildung 6.2.: Milkshape 3D mit dem Ferrari Modell (assignKeys). Diese Methoden werden auch in fast allen unseren Demoprogrammen definiert, sie sind aber für das Verständnis zum jetzigen Zeitpunkt nicht von grosser Bedeutung, wir werden später darauf zurückkommen. 6.2. Komplexere Modelle 6.2.1. Modell-Formate In der Einführung zur 3D-Grafik haben wir bereits von Modellierungsprogrammen gesprochen. Mit diesen Modellierungsprogrammen können 3D-Künstler sehr komplexe Modelle und auch ganze Szenen modellieren. Häufig erstellen 3D-Modellierer Modelle her, die man später in Spielen oder anderen Echtzeitanwendungen verwenden will. Man denke nur an die verschiedenen Gegner in einem 3D-Ballerspiel oder die verschiedensten Waffen, die wir in einem 3D-Rollenspiel antreffen, auch die ganzen Gebäude und überhaupt fast alles, das man in einem 3D-Spiel sehen kann wurde ursprünglich mit einem 3DModellierprogramm erschaffen. Auf dem Screenshot in Abbildung 6.2 sehen wir zum Beispiel das Programm Milkshape 3D. Wir haben bereits ein Modell geöffnet und können direkt die Punkte manipulieren und neue Dinge hinzufügen, es handelt sich um das gleiche Modell, das wir in der nachfolgenden Demo auch in jME darstellen werden. Wie wir bereits im vorherigen Kapitel gesehen haben, besteht jedes Modell im Prinzip aus zusammenhängenden Dreiecken und Listen von Punkten. Die meisten Spiele und Anwendungen erstellen aus- 6.2. Komplexere Modelle 41 Abbildung 6.3.: Screenshot von ModelLoader.java mit Blick auf das Gittermodell serdem ein eigenes meist binäres Dateiformat um diese Modelle, das heisst Listen von Punkten und Dreiecken, zu speichern. Da sich einige dieser Spiele und Anwendungen grösster Beliebtheit erfreuen haben sich einige Quasistandards gebildet, die auch von jME direkt unterstützt werden. Leider ist kein offizieller zertifizierter Standard darunter. Jahrelang hat jeder Hersteller sein eigens Süppchen gekocht. Man scheint jedoch auf dem richtigen Weg zu sein und in letzter Zeit scheint sich ein Standard zu bilden, der von mehreren Herstellern unterstützt wird. Dieser Standard heisst COLLADA [14], er befindet sich aber leider noch in den Kinderschuhen. jME kann unter anderem die Formate ms3d von Milkshape 3D und md2 aus Quake 2 direkt laden. Natürlich unterscheidet sich der Umfang dieser Modell-Formate zum Teil beträchtlich. Einige enthalten ganze Szenen, mit Lichtern, Animation, Kameraführung, Texturen und noch einigem mehr. Wir und auch jME begnügen uns mit einem Unterteil von all diesen Daten und genügt meist, die Form eines Objektes selbst und den Rest machen wir von Hand. Leider können wir die Modelle selbst auch nicht von verändern, wir müssen die Modelle in ein Modellierungsprogramm laden, das auch mit dem Modell-Format umgehen können muss. Die Formate selbst enthalten nur binäre Daten, die Spezifikation dazu kann man aber meistens im Internet finden. Im Internet kann man unzählige von Seiten finden mit Modellen. Leider ist die Qualität zum Teil sehr durchwachsen, aber wenn man genug Zeit investiert kann man durchaus die eine oder andere Perle finden. Auf Psionic’s 3D Resources [52] und Creepy’s Object Downloads [15] können Sie einige frei verwendbare Modelle von hoher Qualität finden. 6.2.2. Modelle laden in jME In der folgenden Demo werden wir einen Ferrari laden und auf dem Bildschirm zeichnen. Der Ferrari ist im Unterordner demos/data mit dem Namen f360.ms3d gespeichert. Einen Screenshot von diesem Programm sehen Sie auf Abbildung 6.3. Fangen wir gleich mit dem Quellcode an: 37 38 39 Listing 6.2: ModelLoader.java protected void simpleInitGame () { display . setTitle ( " Model Loader " ); 40 41 42 43 44 45 46 try { // convert the input file to a jme - binary model FormatConverter converter = new MilkToJme () ; ByteArrayOutputStream modelData = new ByteArrayOutputStream () ; URL modelFile = ModelLoader . class . getClassLoader () . getResource ( 6.2. Komplexere Modelle 42 " data / f360 . ms3d " ); converter . convert ( new BufferedInputStream ( modelFile . openStream () ) , modelData ); 47 48 49 50 // get the model JmeBinaryReader jbr = new JmeBinaryReader () ; Node carModel = jbr . loadBinaryFormat ( new ByteArrayInputStream ( modelData . toByteArray () ) ); 51 52 53 54 55 // load and create a texture for the model URL texLoc ; texLoc = ModelLoader . class . getClassLoader () . getResource ( " data / fskiny . jpg " ); TextureState tsMipMap = display . getRenderer () . createTextureState () ; Texture tex = TextureManager . loadTexture ( texLoc , Texture . MM_LINEAR_LINEAR , Texture . FM_LINEAR ); tsMipMap . setTexture ( tex ); carModel . setRenderState ( tsMipMap ); 56 57 58 59 60 61 62 63 64 65 // make the model a little smaller carModel . setLocalScale ( 0.1 f ); 66 67 68 // attach our model to the scene graph rootNode . attachChild ( carModel ); } catch ( IOException e ) { System . out . println ( " Couldn ’t load the input file :" + e ); e. printStackTrace () ; } 69 70 71 72 73 74 75 76 initText () ; assignKeys () ; 77 78 79 } Wir beschränken uns hier wiederum nur auf die Methode simpleInitGame. Der Rest bleibt fast gleich wie in den vorherigen Demos. Wiederum ist der gesamte Quellcode auf der CD zu finden (siehe Anhang A), konkret ist das die Datei demos/geometry/ModelLoader.java. Man kann in jME nicht eine beliebige Modell-Datei öffnen und dann direkt zeichnen. Man muss den Umweg über das jME-eigene binäre Dateiformat gehen. Dabei erstellen wir eine Instanz der Klasse FormatConverter (Zeile 44). Wie Sie auch auf dem Diagramm sehen können besitzt jedes unterstützte Dateiformat auch eine eigene Unterklasse von FormatConverter. Wir benutzen hier MilkToJme, weil wir ein Milkshape-Modell mit der Dateierweiterung ms3d öffnen wollen. Als nächstes können wir die Datei in das binäre jME-Format konvertieren (Zeile 45 bis 49). Wir machen hier die ganze Arbeit mit Streams. In einer richtigen Anwendung würden wir wohl zuerst alle unsere Modelle am Schluss in das jME eigene Dateiformat konvertieren und dann diese Datei laden. Dabei müssen wir nicht den Umweg über die jeweiligen FormatConverter gehen, unsere Anwendung würde also die Modelle schneller laden können. In den Zeilen 52 bis 54 laden wir endlich unsere Daten mit JmeBinaryReader und erstellen eine Node. Der Knoten kann dann direkt in unseren Scenegraph eingefügt werden. In den Zeilen 57 bis 67 wird ausserdem eine Textur geladen und auf unser Auto “geklebt”, damit es realistischer aussieht. Was es genau auf sich hat mit Texturen werden wir in Kapitel 8 sehen. Sie können diesen Teil also bis auf weiteres ignorieren. Nun haben wir also schon mal ein Demo mit einem Ferrari. Wenn Sie während der Ausführung auf ’T’ drücken können Sie wiederum das Wireframe Modell unseres Ferraris erkennen, wie Sie nun auf dem Bildschirm oder auch auf dem Screenshot sehen, besteht auch der Ferrari im Grunde genommen nur aus Dreiecken. 6.2. Komplexere Modelle 43 Im Ordner demos/data gibt es noch mehr Milkshape-Modelle, wie zum Beispiel das Modell jeep1.ms3d, versuchen Sie doch einmal dieses Modell zu laden. 7 Es werde Licht 7.1. Lichtquellen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44 7.2. Lichter einsetzen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46 Wenn wir dreidimensionale Objekte rendern wollen wir nicht nur die Formen richtig auf den Bildschirm bringen, wir wollen auch, dass die Objekte realistisch aussehen. Das kann man mit verschiedenen Techniken erreichen. Eine der wichtigsten Techniken sind dabei die Lichter und die Beleuchtung. Ohne Licht würden wir in der realen Welt nichts erkennen, ähnlich geht es auch einer 3D Welt ohne Licht. In allen Demos, die wir bis jetzt gesehen haben, war auch schon ein Licht integriert, das wir von unserer Klasse TutorialGame übernommen haben. Wie eine Szene ohne Licht aussehen würde können Sie auf der Abbildung 7.1 sehen. Man sieht zwar im Gegensatz zu der realen Welt noch etwas, auch wenn es kein Licht gibt, die wahre Form unseres Würfels kann man aber nicht mehr richtig erkennen. Deshalb sind Lichter im 3D-Rendering von enormer Bedeutung. 7.1. Lichtquellen Wir können Objekte sehen weil Photonen von Objekten abprallen oder ausgesendet werden, die schlussendlich unsere Augen erreichen. Diese Photonen kommen in der Regel von Lichtquellen. Im Kontext der 3D-Grafik gibt es auch Lichtquellen. Man spricht von Parallelem Licht (engl. directional light), Punkt Lichtern (engl. point light) und Spot Lichtern (engl. spot light). Ein paralleles Licht ist unendlich weit weg positioniert von den Objekten, die beleuchtet werden. Die Lichtstrahlen kommen also alle parallel an. Ein Beispiel für ein solches Licht wäre die Sonne, deren Lichtstrahlen fast parallel auf die Erde eintreffen, da sie so weit weg positioniert ist. Punkt und Spot Lichter nennt man auch Positionslichter, da sie eine Position im Raum besitzen. Man kann sich ein Punktlicht vorstellen, als ein Licht das in einem Punkt Lichtstrahlen in alle Richtungen aussendet. Eine simple Glühbirne wäre ein Beispiel für ein solches Licht. Auf Abbildung 7.2 können Sie sehen, wie ein Punktlicht in jME funktionert. Ein Licht hat ausserdem verschiedene Attribute wie Intensität und Farbe. Man spricht in der Regel von einem Ambient Value und einem Diffuse Value. Der Diffuse Value ist die Farbe des Lichtes, hierbei können wir jede beliebige Farbe übergeben. Die meisten Lichter in unserer Umgebung haben dabei eine weisse Farbe oder gehen leicht ins gelbliche, es gibt aber natürlich auch in der echten Welt Glühbirnen in jeder erdenklichen Farbe, denken Sie nur an Ihre letzte Gartenparty zurück. Der sogenannte Ambient Value ist schwieriger zu erklären. Wir geben hier die Farbe des Streuungslichtes an, das heisst das Licht, das Sie noch sehen können, wenn ein Objekt nicht direkt von der Lichtquelle beschienen wird. Es handelt sich also um das Hintergrundlicht, das die Objekte selber eigentlich abstrahlen. Wir müssen im Echtzeitrendering ein bisschen flunkern und hier einfach einen Wert angeben, da wir momentan nicht die Rechenpower haben um jede einzelne dieser Lichtstrahlen in Echtzeit zu berechnen. 44 45 7.1. Lichtquellen Abbildung 7.1.: Eine Szene mit Licht (links) und ohne Licht (rechts) Abbildung 7.2.: Ein Punktlicht 46 7.2. Lichter einsetzen Abbildung 7.3.: Screenshot aus der Licht Demo Die Lichter, die wir hier in jME einsetzen können entsprechen aber nicht wirklich realen Lichtern, sondern es sind einfache Lichter, die echte Lichter nur auf eine relative Weise nachahmen. Schatten können diese Lichte in jME beispielsweise keine erzeugen. Ausserdem werden nur ganze Dreiecke auf einmal beleuchtet, wie Sie auf dem Screenshot und der Demo erkennen können, sehr gut sieht man dieses Phänomen auf dem Würfel, dessen Oberflächen jeweils nur eine Farbe hat. Die modernen Grafikkarten haben sich aber in letzter Zeit auch in diesem Punkt weiterentwickelt und stellen eine programmierbare Pipeline, die sogenannte Programmable Shader Unit zur Verfügung, in der man mit speziellen Programmen, sogenannten Shader Programmen, die Lichter von Hand genauer programmieren kann. Auch jME stelle und einen ShaderState zur Verfügung mit der wir solche Shader einsetzen können. Wie man Shader einsetzen kann, können wir leider aber hier nicht erklären, da das den Rahmen sprengen würde. In der Tat gibt es schon viele Bücher allein zu diesem Thema wie zum Beispiel das Buch Programming Vertex and Pixel Shaders von Engel [Eng04]. Diese Shader sind momentan der neuste Trend in der Echtzeit Grafikprogrammierung und werden erst von der neusten Generation von 3D-Grafikkarten und den allerneusten Computerspielen unterstützt. 7.2. Lichter einsetzen Nach der kurzen theoretischen Einführung wollen wir nun sehen, wie wir Lichter in jME benutzen und einsetzen können. In der folgenden Demo haben wir zwei Lichter eingesetzt. Ein rotes paralleles Licht und ein blaues Punktlicht, das unsere Objekte beleuchtet. Die Position des blauen Punktlichtes wird durch einen kleinen blauen Würfel dargestellt. Wir können die Position, dieses Lichtes interaktiv verändern, schalten Sie dazu die Hilfe in der Demo ein. Ausserdem gibt es ein globales Umgebungslicht, das in der 3D-Grafik Ambient Light genannt wird. Auf dem Screenshot 7.3 sehen Sie das Endresultat. Sehen wir uns nun den Quellcode an, den Sie wie immer ganz auf der CD (siehe Anhang A) finden können, im Verzeichnis demos/light mit dem Namen SimpleLight.java. 47 7.2. Lichter einsetzen Listing 7.1: SimpleLight.java 47 48 49 protected void simpleInitGame () { display . setTitle ( " Simple Light Test " ); 50 51 52 // our root node Node node = new Node ( " objects " ); 53 54 55 56 57 58 59 // we set a point light pl = new PointLight () ; pl . setLocation ( lightPosition ); pl . setDiffuse ( ColorRGBA . blue ); pl . setAmbient ( new ColorRGBA ( 0, 0, 0.4f , 1 ) ); pl . setEnabled ( true ); 60 61 62 63 64 65 66 // set up the ambient light al = new AmbientLight () ; al . setDiffuse ( new ColorRGBA ( 1.0f , 1.0f , 1.0f , 1.0 f ) ); alValue = 0.2 f; al . setAmbient ( new ColorRGBA ( alValue , alValue , alValue , 1 ) ); al . setEnabled ( true ); 67 68 69 70 71 72 73 74 // set up a new directional light dl = new DirectionalLight () ; dl . setDiffuse ( ColorRGBA . red ); dl . setAmbient ( new ColorRGBA ( 0.4f , 0, 0, 1 ) ); dl . setEnabled ( true ); dl . setDirection ( new Vector3f ( 1, -0.5f , -0.75 f ) ); dl . setEnabled ( true ); 75 76 77 78 79 80 81 // the light box shows the position of the light Box lightBox = new Box ( " lightPosition " , new Vector3f ( -0.1f , -0.1f , -0.1 f ) , new Vector3f ( 0.1f , 0.1f , 0.1 f ) ); lightBoxHolder = new Node ( " lightBoxHolder " ); lightBox . setSolidColor ( new ColorRGBA ( 0.25 f , 0.25 f , 1.0f , 1 ) ); lightBoxHolder . attachChild ( lightBox ); 82 83 84 85 lightPosition = new Vector3f ( 2, 2, 1 ); lightBoxHolder . setLocalTranslation ( lightPosition ); rootNode . attachChild ( lightBoxHolder ); 86 87 88 89 // we create some simple geometry Sphere s = new Sphere ( " sphere " , 15 , 15 , 1 ); s. setLocalTranslation ( new Vector3f ( 2, 0, 0 ) ); 90 91 92 93 Box b = new Box ( " box " , new Vector3f ( -1, -1, -1 ) , new Vector3f ( 1, 1, 1 ) ); b. setLocalTranslation ( new Vector3f ( -2, 0, 0 ) ); 94 95 96 97 98 // attach the simple geometry node . attachChild ( s ); node . attachChild ( b ); rootNode . attachChild ( node ); 99 100 101 // the TutorialGame class already has a default light , we have to detach it // to get the lighting effects we want to get 48 7.2. Lichter einsetzen lightState . detachAll () ; 102 103 // a light LightState ls . attach ( ls . attach ( ls . attach ( 104 105 106 107 108 is ls pl al dl always attached to a light node = display . getRenderer () . createLightState () ; ); ); ); 109 // set the light state to our node we created before node . setRenderState ( ls ); rootNode . updateRenderState () ; 110 111 112 113 // initialize the helper text and the keys assignKeys () ; initText () ; 114 115 116 117 } Auf der Zeile 52 erstellen wir eine Node namens node. An diesen Knoten werden wir den Würfel und die Kugel anhängen und auch die Lichter einsetzen. In den Zeilen 54 bis 74 erstellen wir dann endlich unsere Lichter. Gehen wir noch genauer auf das Punkt Licht ein: // we set a point light pl = new PointLight () ; pl . setLocation ( lightPosition ); pl . setDiffuse ( ColorRGBA . blue ); pl . setAmbient ( new ColorRGBA ( 0, 0, 0.4f , 1 ) ); pl . setEnabled ( true ); Wir haben zuvor pl als privates Feld von der Klasse PointLight definiert. Wir haben im Abschnitt zuvor ebenfalls erwähnt, dass jedes Positionslicht eine Position hat. Deshalb setzen wir die Position mit setLocation. Die Farbe des Lichtes bestimmen wir mit setDiffuse. Wir geben hier dem Licht die Farbe blau. Als nächstes setzen wir den Umgebungslichtfaktor mit setAmbient, dem wir ein dunkleres blau übergeben. Sie können in der Demo alle anderen Lichter ausschalten und falls Sie sich vom Licht entfernen, werden Sie erkennen, dass beide Objekte noch mit einem sehr dunklen Blau zu erkennen sind. Zu guter Letzt müssen wir das Licht noch aktivieren, was wir mit dem Aufruf setEnabled( true ) auch machen. Das Umgebungslicht al und das Parallellicht dl erstellen wir in den darauf folgenden Zeilen auf ähnliche Weise. Beim Umgebungslicht al handelt es sich um ein AmbientLight, das heisst es besitzt weder eine Position noch eine Richtung sondern erleuchte unsere 3D-Szene sozusagen auf globale Art und Weise. In der Demo können Sie die Intensität dieses Umbegungslichtes mit den Tasten + und - steuern. Sie werden sehen, dass Sie damit die Helligkeit der Objekte steuern können. Das Parallellicht dl wird im englischen und in jME selbst DirectionalLight genannt. In Gegensatz zum Punktlicht pl besitzt es keine Position, sondern nur eine Richtungsvektor, die alle Lichtstrahlen besitzen die von diesem Lichte abgestrahlt werden. Auf den Zeilen 76 bis 85 erstellen wir einen kleinen Würfel, den wir benutzen um die Position des Punktlichtes pl anzuzeigen, das wir in der Demo bewegen können. Diese Aufrufe sollten Sie nach der Lektüre der vorherigen Kapitel nun schon verstehen. Ausserdem erstellen wir in den Zeilen 87 bis 98 unsere Objekte, den Würfel und die Kugel, die wir in unserem Knoten node zusammen gruppieren und anschliessend an den rootNode hängen. Die Lichter, die wir erzeugt haben müssen wir aber jetzt noch mit der Szene verbinden, das passiert in den Zeilen 100 bis 111, die wir hier noch einmal genauer anschauen wollen: Listing 7.2: Lichter aktivieren mit LightStates 100 101 // the TutorialGame class already has a default light , we have to detach it // to get the lighting effects we want to get 49 7.2. Lichter einsetzen 102 lightState . detachAll () ; 103 104 105 106 107 108 // a light LightState ls . attach ( ls . attach ( ls . attach ( is ls pl al dl always attached to a light node = display . getRenderer () . createLightState () ; ); ); ); 109 110 111 112 // set the light state to our node we created before node . setRenderState ( ls ); rootNode . updateRenderState () ; Um Lichter in der Szene aktivieren zu können brauchen wir einen Zustand namens LightState. Als erstes müssen wir das Standardlicht, das in TutorialGame definiert wurde abschalten. Wir machen das mit dem Aufruf von lightState.detachAll(), wie es in Zeile 102 zu sehen ist. Anschliessend müssen wir einen LightState erzeugen, was wir in Zeile 105 auch machen. An diesen LightState ls können wir nun alle Lichter, die wir erzeugt haben anhängen (Zeilen 106 bis 108). Als letzten Schritt müssen wir unseren LightState noch an ein Objekt verbinden. Wir setzen den LightState an den Knoten node, der schon die Kugel und den Würfel als Kinder besitzt. Wir rufen am Schluss updateRenderState auf, damit jME erkennt, das wir einen internen Zustand geändert haben. 8 Texturen 8.1. Was sind Texturen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50 8.2. Texturen einsetzen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50 8.3. Wie geht es weiter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54 8.1. Was sind Texturen Texturen sind eines der mit Abstand wichtigsten Mittel um Realismus in eine Szene zu bringen. Einfach ausgedrückt können wir mit Texturen unsere Modelle tapezieren, indem wir Details, meistens in Form eines zweidimensionalen Bildes oder Fotos auf unsere Modelle zeichnen. Sehen wir uns dazu ein Bild an, denn ein Bild spricht ja bekanntlich mehr als tausend Worte. Auf dem Screenshot in Abbildung 8.1 sehen Sie eine kleine Stadtszene, oben ohne und unten mit Textur. Die untere Abbildung erscheint uns auf den ersten Blick realistischer. Unter einer Textur versteht man das Bild, das wir auf unsere Modelle abbilden wollen, den Prozess an sich nennt man Texture Mapping. Als Textur kann man jedes zweidimensionale Bild verwenden. Am meisten verbreitet sind die Formate jpg und png, die auch von jME direkt unterstützt werden. Wir können Texturen auch zur Laufzeit auf prozedurale Art erstellen im Kapitel 9 werden wir darauf zurückkommen. Texturen werden, wie die Modelle auch, von Grafikern erstellt. Im Internet findet man aber tausende frei verfügbare Texturen wie zum Beispiel im Texture Warehouse[56]. Eine Textur muss aber in der Regel speziell auf ein Modell angepasst werden. Wie sie auf der Abbildung 8.2 sehen können kann eine Textur zum Teil etwas komisch aussehen. Wie eine Textur auf ein Modell geklebt wird, wird mit sogenannten u/v-Koordinaten bestimmt. Diese u/v-Koordinaten sind in den Modellen in der Regel schon vom 3D-Grafiker erstellt worden und enthalten, wir werden also hier nicht mehr weiter darauf eingehen. Es kann aber Situationen geben, in denen auch Sie als Programmierer Texturkoordinaten verändern müssen. Jedes Buch zum Thema 3D-Grafik wird sich bestimmt auch mit u/v-Koordinaten auseinander setzen. Schauen Sie sich ein verfügbares Buch aus der ausführlichen Bibliographie an. 8.2. Texturen einsetzen Im folgenden Demo werden wir Ihnen zeigen, wie Sie Texturen laden und mit Modellen verbinden können, das Programm sollte wie auf dem Screenshot 8.3 aussehen. Listing 8.1: TextureDemo.java 40 41 42 protected void simpleInitGame () { display . setTitle ( " Texture Demo " ); 50 51 8.2. Texturen einsetzen Abbildung 8.1.: Eine Stadt mit und ohne Textur Abbildung 8.2.: Textur vom Ferrari Modell 52 8.2. Texturen einsetzen Abbildung 8.3.: Screenshot von der Texturen Demo 43 URL texLoc ; 44 45 46 // 1: create a new box Box box1 = new Box ( " box1 " , new Vector3f ( -1, -1, -1 ) , new Vector3f ( 1, 1, 1 ) ); 47 48 49 50 // 2: get a path to a texture graphic file texLoc = TextureDemo . class . getClassLoader () . getResource ( " data / crate . png " ); 51 52 53 // 3: create a texture state TextureState tsBox1 = display . getRenderer () . createTextureState () ; 54 55 56 57 // 4: load the texture from the system Texture texBox1 = TextureManager . loadTexture ( texLoc , Texture . MM_LINEAR_LINEAR , Texture . FM_LINEAR ); 58 59 60 // 5: assign the texture to the texturestate tsBox1 . setTexture ( texBox1 ); 61 62 63 // 6: set the texture state as a new render state to our box box1 . setRenderState ( tsBox1 ); 64 65 66 // 7: attach the box to the rootNode rootNode . attachChild ( box1 ); 67 68 69 70 // create another box with a texture , we make the // same steps as before , but with a different texture Box box2 = new Box ( " box2 " , new Vector3f ( -1, -1, -1 ) , new Vector3f ( 1, 1, 1 ) ); 8.2. Texturen einsetzen 53 box2 . setLocalTranslation ( new Vector3f ( -3, 0, 0 ) ); texLoc = TextureDemo . class . getClassLoader () . getResource ( " data / xboxes01 . jpg " ); TextureState tsBox2 = display . getRenderer () . createTextureState () ; Texture texBox2 = TextureManager . loadTexture ( texLoc , Texture . MM_LINEAR_LINEAR , Texture . FM_LINEAR ); tsBox2 . setTexture ( texBox2 ); box2 . setRenderState ( tsBox2 ); rootNode . attachChild ( box2 ); 71 72 73 74 75 76 77 78 79 80 // create a third box with a texture , we make the // same steps as before , but with a different texture Box box3 = new Box ( " box3 " , new Vector3f ( -1, -1, -1 ) , new Vector3f ( 1, 1, 1 ) ); box3 . setLocalTranslation ( new Vector3f ( 3, 0, 0 ) ); texLoc = TextureDemo . class . getClassLoader () . getResource ( " data / BumpyMetal . jpg " ); TextureState tsBox3 = display . getRenderer () . createTextureState () ; Texture texBox3 = TextureManager . loadTexture ( texLoc , Texture . MM_LINEAR_LINEAR , Texture . FM_LINEAR ); tsBox3 . setTexture ( texBox3 ); box3 . setRenderState ( tsBox3 ); rootNode . attachChild ( box3 ); 81 82 83 84 85 86 87 88 89 90 91 92 93 AmbientLight al = new AmbientLight () ; al . setAmbient ( new ColorRGBA ( 1.0f , 1.0f , 1.0f , 1.0 f ) ); al . setEnabled ( true ); lightState . attach ( al ); 94 95 96 97 98 initText () ; assignKeys () ; 99 100 101 } Den ganzen Quellcode und das ausführbare Programm finden Sie wie immer auf der CD (siehe Anhang A). Der Quellcode ist im Verzeichnis demos/textures in der Datei TextureDemo.java. In der Demo erstellen wir drei Würfel, die wir mit jeweils drei verschiedenen Texturen bekleben. Obwohl der Würfel jeweils der gleiche ist, haben wir von jedem einzelnen einen völlig anderen Eindruck nur weil die Texturen sich unterscheiden. Der linke Würfel sieht aus, wie eine Metallbox, der mittlere Würfel scheint eine Holzkiste zu sein und im rechten Würfel sehen wir eine Wandstruktur. Die Behauptung im vorherigen Abschnitt, dass Texturen eines der wichtigsten Mittel sind um Realismus in eine Szene zu bringen, scheint also nicht übertrieben zu sein. Gehen wir nun unser Programm zeilenweise durch. Die Schritte, die wir machen müssen um, eine Textur zu laden und mit einer Textur zu verbinden, sind jeweils sehr ähnlich. Deshalb werden wir unseren Fokus vor allem auf den ersten Würfel legen. Den ersten Würfel mit Textur erstellen wir in den Zeilen 45 bis 66. Auch im Quellcode wird jeder Schritt genau kommentiert. 1. Wir erstellen einen Würfel. 2. Wir kreieren ein URL-Objekt mit dem genauen Pfad der Grafikdatei, die wir laden wollen. 3. Ein Zustand namens TextureState wird erstellt. 4. Mit dem TextureManager laden wir unsere Textur. Der TextureManager verwaltet, dabei intern unsere Textur. Mit komisch klingenden Argumente namens Texture.MM_LINEAR_LINEAR und Texture.FM_LINEAR erhalten Sie die besten Resultate. Diese Argumente bestimmen, ob wir MIPMapping wollen oder nicht und wie unsere Texturen gefiltert werden sollen. Auch zu diesem 54 8.3. Wie geht es weiter Abbildung 8.4.: Beispiel für Bump-Mapping Thema finden Sie in Real-Time Rendering [AMH02] weitere Informationen. Sie können ruhigen Gewissens immer die hier genannten Argumente verwenden. Wie die Texturen aussehen, wenn Sie andere Argumente verwenden können Sie im Demo TextureFiltering.java sehen, das Sie auf der CD mit dem Pfad demos/textures/TextureFiltering.java finden können. 5. Als nächsten Schritt weisen wir unserem Texturen-Zustand tsBox1 die soeben geladene Textur zu. 6. Der TextureState wird nun an unseren Würfel mittels setRenderState weitergeleitet. 7. Im letzten Schritt fügen wir, wie immer, unseren Würfel dem rootNode hinzu. Die Schritte wiederholen sich für die nächsten beiden Würfel. Das einzige, das sich jeweils ändert sind die Namen der verschiedenen Variablen und der Name der Textur selbst, wie wir laden wollen. Überhaupt bleiben die sieben Schritte immer gleich, die wir vornehmen müssen um eine Textur zu laden. Sie können das auch überprüfen indem sie in Abschnitt 6.2.2 die Codezeilen anschauen, die wir benötigt haben um die Textur für unseren Ferrari zu laden. 8.3. Wie geht es weiter Wir haben in diesem Kapitel nur die einfachsten Dinge gezeigt, die Sie mit Texturen machen können. Auf viele fortgeschrittene Anwendungen können wir aus Zeit- und Platzmangel hier leider nicht mehr eingehen. Ganz wichtig wäre zum Beispiel noch das Multitexturing, in dem mehrere Texturen aufeinander gereiht werden um interessante Effekte zu erzielen, vor allem auch um bestimmte Beleuchtungseffekte zu simulieren. Eng dazu gehört auch das Blending (deutsch: vermischen) von Texturen und speziell das Alpha Blending. Ein in letzter Zeit auch sehr beliebter Texture Effekt ist das Bump Mapping ein Beispiel dafür ist auf der Abbildung 8.4 zu sehen. In dem eine Textur verwendet wird um die Normalenvektoren einer Fläche zu repräsentieren. 8.3. Wie geht es weiter 55 Zu allen diesen Effekten finden Sie im Buch Real-Time Rendering [AMH02] und der dazugehörigen Webseite weitere Informationen. 9 Landschaften 9.1. Einführung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56 9.2. Heightmaps . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56 9.3. Landschaften darstellen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58 9.4. Landschaften mit Texturen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61 9.5. Skybox . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63 9.1. Einführung Wir haben und in den vorherigen Kapiteln die Grundlagen der 3D-Grafik erarbeitet. In diesem Kapitel gehen wir nun einen Schritt weiter. Wir haben schon früher von 3D-Szenen gesprochen. Solche Szenen sind aber ziemlich langweilig, wenn sie nur aus ein paar Würfeln und einzelnen Modellen bestehen. Um eine realistische 3D-Szene zu erhalten brauchen wir auch eine realistische Umwelt. Deshalb werden wir und in diesem Kapitel mit dem Rendern von Landschaften befassen. Das Rendern von Landschaften wird üblicherweise mit dem englischen Begriff Terrain Rendering bezeichnet. Das Terrain Rendering ist natürlich nicht nur für Spiele wichtig, sondern hat auch eine Vielzahl von “ernsthaften” Anwendungen, wie militärische Simulationen, Wettersimulationen und noch vieles mehr. Auf der Website Virtual Terrain Project [60] finden Sie viele Informationen dazu. Wie Sie sehen das Visualisieren und Rendern von Landschaften ist ein wichtiges Forschungsgebiet aus verschiedenen Gründen. Die Landschaften müssen realistisch wirken und genügend schnell gerendert werden damit eine gute Framerate erzeugt wird. In den letzten Jahren wurden deshalb eine Vielzahl von Algorithmen entwickelt, die diese Ziele erfüllen. Diese Algorithmen werden zum Teil von vielen modernen Spielen implentiert, was zu eindrucksvollen Landschaften führt, wie Sie auf dem Screenshot in Abbildung 9.1 selbst sehen können. Leider können wir uns in dieser Einführung nur auf simple Methoden beschränken. Eine tiefergehende Einführung können Sie aber zum Beispiel aus dem Buch Focus on 3D Terrain Programming [Pol02] entnehmen. Ausserdem werden auf einer Unterseite von VTerrain [60] viele der zum Teil sehr aktuellen Artikel und Papers zum Thema gesammelt. 9.2. Heightmaps Wie wir bereits mehrmals erwähnt haben besteht in der Echtzeit 3D-Grafik alles aus Dreiecken. Das ist auch beim Terrain Rendering nicht anders. Um eine Landschaft darzustellen brauchen wir also eine Reihe von Dreiecken. Die einfachste Landschaft, die man sich vorstellen kann ist ausserdem eine ebene Fläche. Eine einfache 3D-Landschaft ist also nicht anderes als eine Fläche mit einigen Dreiecken, wie auf dem Screenshot 9.2 zu sehen. 56 57 9.2. Heightmaps Abbildung 9.1.: Screenshot aus Oblivion von Bethesda Softworks Abbildung 9.2.: Eine sehr einfache 3D-Landschaft 9.3. Landschaften darstellen 58 Abbildung 9.3.: Ein Beispiel für eine Heightmap Zugegeben, das ist eine ziemlich langweilige Landschaft und lädt nicht so wirklich zum verweilen ein, es fehlt einfach irgend etwas. Was am allermeisten fehlt, sind einige Hügelzüge und Höhenunterschiede. Das lässt sich realisieren indem wir jedem einzelnen Punkt unserer flachen Ebene eine individuelle Höhe geben. Von Hand wäre dieser Vorgang ziemlich mühsam. Aber glücklicherweise gibt es eine weitaus einfachere Methode, nämlich sogenannte Heightmaps. Eine Heightmap ist ein Graustufenbild, ein Beispiel könne Sie auf der Abbildung 9.3 sehen. Jede einzelne Graustufe wird nun eine Zahl zwischen 0 und 255 zugeordnet. Weiss entspricht der Zahl 255 und Schwarz der Zahl 0. Diese Zahlen entsprechen ausserdem einer Höhe. Wenn wir nun diese Heightmap auf unsere Ebene anwenden erhalten wir eine weitaus realistischere Landschaft, sehen sie dazu Abbildung 9.4 an. Je heller ein Abschnitt auf der Heightmap ist, desto höher ist dieser Teil auf der gerenderten Landschaft. Je dunkler die Heightmap, desto tiefer das Tal in der Landschaft. Noch ein weiteres Wort zu Heightmaps. Heightmaps werden in der Regel als raw Dateien gespeichert. Das bedeutet, dass die Höhenwerte direkt ohne Semantik und weitere Angaben in der Datei gespeichert sind. Es handelt ganz genau genommen nicht um Bilddateien. Man kann aber solche raw-Dateien mit den meisten üblichen Bildbearbeitungsprogrammen öffnen, wie zum Beispiel Paint Shop Pro[48]. 9.3. Landschaften darstellen Wir wollen nun das oben erworbene Wissen anwenden. In der folgenden Demo werden wir eine Landschaft generieren mit dem oben genannten Heightmap. Wie in den meisten Demos beschränken wir und hier auf die Methode simpleInitGame. Den gesamten Quellcode und das ausführbare Programm finden auf der CD (siehe Anhang A) im Verzeichnis demos/terrain mit dem Namen TerrainDemo.java. 41 42 43 44 45 Listing 9.1: TerrainDemo.java protected void simpleInitGame () { // set a new title to our application display . setTitle ( " Terrain Demo " ); 9.3. Landschaften darstellen Abbildung 9.4.: Eine Landschaft generiert aus der Heightmap aus Abbildung 9.3 Abbildung 9.5.: Screenshot aus TerrainDemo.java 59 9.3. Landschaften darstellen 60 // load the heightmap URL loc = Terrain . class . getClassLoader () . getResource ( " data / heightmap64 . raw " ); AbstractHeightMap heightMap = new RawHeightMap ( loc . getPath () , 64 ); 46 47 48 49 // create a TerrainBlock out of the heightmap Vector3f terrainScale = new Vector3f ( 4.0f , 0.4f , 4.0 f ); TerrainBlock tb = new TerrainBlock ( " terrain " , heightMap . getSize () , terrainScale , heightMap . getHeightMap () , new Vector3f ( 0, 0, 0 ) , false ); 50 51 52 53 54 // with backface culling we reduce the number of vertices to draw CullState cs = display . getRenderer () . createCullState () ; cs . setCullMode ( CullState . CS_BACK ); cs . setEnabled ( true ); rootNode . setRenderState ( cs ); 55 56 57 58 59 60 // attach the terrainblock to the scenegraph rootNode . attachChild ( tb ); 61 62 63 // set the camera to a new location cam . setLocation ( new Vector3f ( 100 , 60 , 100 ) ); 64 65 66 // disable the default light and create a new directional light // to simulate sunrays lightState . detachAll () ; DirectionalLight light = new DirectionalLight () ; light . setDirection ( new Vector3f ( 0.6f , -0.5f , 0.5 f ) ); float intensity = 1.0 f; light . setDiffuse ( new ColorRGBA ( intensity , intensity , intensity , 1.0 f ) ); light . setAmbient ( new ColorRGBA ( intensity , intensity , intensity , 1.0 f ) ); light . setEnabled ( true ); lightState . attach ( light ); rootNode . updateRenderState () ; 67 68 69 70 71 72 73 74 75 76 77 78 // initialize the text system and the key inputs initText () ; assignKeys () ; 79 80 81 82 } In den Zeilen 47 und 48 laden wir die Heightmap namens heightmap64.raw. Der Name kommt daher, da es sich um eine 64 mal 64 Punkte grosse Heightmap handelt. Wir speichern die Heightmap als ein AbstractHeightMap Objekt. Wir können nämlich Heightmaps auch direkt aus Bildern laden oder sie sogar zur Laufzeit mit einem Algorithmus auf dynamische Art erzeugen. Die Klassen, die diese Funktionalität zur Verfügung stellen heissen MidPointHeightMap, ParticleDepositionHeightMap und FaultFractalHeightMap, die Details dazu können Sie in der Javadoc von jME nachschlagen. Falls Sie also unsere Landschaft langweilt, können Sie auf diese Weise neue Terrains erstellen. Wir erstellen nun mit unserer Heightmap ein TerrainBlock Objekt, wie Sie in den Zeilen 51 bis 53 sehen können. TerrainBlock ist eine Unterklasse von Geometry, die wir ja bekanntlich alle an unseren Scenegraphen anhängen können. Der Konstruktor von TerrainBlock erwartet einige Argumente, unter anderem die Länge und Breite von unserer Heightmap. Ausserdem können wir noch einen Vector3f mitgeben, der die Skalierung der Landschaft angibt. Diesen Skalierungsvektor haben wir in Zeile 51 mit dem Namen terrainScale erstellt. Wir geben in der x- und in der z-Achse an, dass wir unsere Terrain 4 mal vergrössern wollen, das beeinflusst die Länge und die Breite unseres sichtbaren Terrains. In der y-Achse bestimmen wir den Faktor 0.4. Das heisst, dass wir die Höhenunterschiede etwas vermin- 9.4. Landschaften mit Texturen 61 Abbildung 9.6.: Unsere Landschaft mit einigen saftigen, grünen Hügeln dern wollen. Sie können diese Faktoren durchaus verändern um zum Beispiel eine bergige mit grossen Höhenunterschieden zu erstellen. In den Zeilen 56 bis 59 schalten wir das sogenannte Backface Culling ein. Damit reduzieren wir die Anzahl der zu rendernden Dreicke auf dem Bildschirm. Wie der Name Backface schon ausdrückt, eliminieren wir auf diese Weise alle rückseitigen Dreiecke. Genau genommen handelt es sich dabei auf die Rückseite von unserem Terrain, das wir in der Regel ja sowieso nur von oben sehen wollen. Sie können mit der Kamera versuchen unter das Terrain zu steuern und nach oben schauen, Sie werden bemerken, dass Sie von unten fast nichts mehr von unserer Landschaft sehen. Wir schalten in dieser Demo das Backface Culling aus Perfomancegründen ein. Wir könnten sonst unseren Rechner mit der hohen Anzahl von Dreiecken in unserer Landschaft durchaus in die Knie zwingen. In Zeile 65 verstellen wir ausserdem die Position von unserer Kamera, damit wir mittendrin in der Action sind und uns nicht vom Rand noch mühsam in die Mitte von unserer Landschaft kämpfen müssen. In den Zeilen 69 bis 77 schalten wir zu guter Letzt noch unsere Standardmässiges PointLight aus TutorialGame aus und erstellen ein DirectionalLight um auf simple Art und Weise die Sonne zu simulieren. Beachten Sie ausserdem, dass die zu sehenden Graustufen auf der Landschaft nicht von unserer Landschaft stammen, sondern eine Folge der eingesetzten Lichter sind. 9.4. Landschaften mit Texturen Wenn wir uns noch einmal die oben gezeigten Abbildungen ansehen, fällt auf, dass etwas fehlt. Die Landschaften sehen zwar schon aus, sie wirken aber etwas fade mit ihren Grautönen, fast so als ob wir auf dem Mond einen Spaziergang machen würden. Fast fehlt sind einfache saftige, grüne Wiesen. Diese werden wir nun in der folgenden Demo hinzufügen. Auf der Abbildung 9.6 sehen Sie unsere neue, viel realistischere Landschaft. Sie ist zwar noch nicht perfekt, aber fast. Hier sehen Sie die Codezeilen, die wir unserer vorherigen Demo hinzugefügt haben, die beiden Demos sind ansonsten gleich. Wiederum ist der ganze Quellcode auf der CD zu finden. Der genaue Pfad laute: demos/terrain/TerrainWithTexture.java. 62 9.4. Landschaften mit Texturen Abbildung 9.7.: Rechts die Grastextur, links die Detailtextur Listing 9.2: TerrainWithTexture.java 59 60 // create a texture state TextureState ts = DisplaySystem . getDisplaySystem () . getRenderer () . createTextureState () ; 61 62 63 64 65 66 67 68 // add the grass texture URL grass = Terrain . class . getClassLoader () . getResource ( " data / tr_grass . bmp " ); Texture texGrass = TextureManager . loadTexture ( grass , Texture . MM_LINEAR , Texture . FM_LINEAR ); texGrass . setWrap ( Texture . WM_WRAP_S_WRAP_T ); ts . setTexture ( texGrass ); 69 70 71 72 73 74 75 76 // add the detail texture URL detail = Terrain . class . getClassLoader () . getResource ( " data / tr_detail . bmp " ); Texture texDetail = TextureManager . loadTexture ( detail , Texture . MM_LINEAR , Texture . FM_LINEAR ); texDetail . setWrap ( Texture . WM_WRAP_S_WRAP_T ); ts . setTexture ( texDetail , 1 ); 77 78 79 80 81 // set the number of texture repeats tb . setDetailTexture ( 0, 10 ); tb . setDetailTexture ( 1, 4 ); tb . setRenderState ( ts ); Wir haben für das Terrain zwei Texturen benutzt zum eine grüne Grastextur und als zweite Textur eine helle, sogenannte Detailtextur. Die Texturen sind auf der Abbildung 9.7 zu sehen. In den oben abgedruckten Zeilen laden wir die beiden Texturen und setzen sie dem gleichen TextureState zu. Die grundlegende Vorgehensweise bleibt dabei gleich wie wir im Kapitel 8.2 “Texturen einsetzen” beschrieben haben. Auf Zeile 76 können Sie vielleicht sehen, dass wir beim Aufruf von setTexture noch ein weiteres Argument, nämlich eine 1 mitgeben, im Gegensatz zu allen anderen Beispielen. Die 1 gibt an welche Textureinheit wir benutzen wollen. Für jeden Texturplatz stellen uns moderne 3D-Karten mehrere Textureinheiten bereit, die wir mit unterschiedlichen Texturen belegen können. Im Normalfall benutzen wir nur eine Textureinheit, die standardmässig die Nummer 0 trägt, und die wir auch nicht speziell angeben 63 9.5. Skybox müssen wenn wir ein Bild als Textur setzen. Wenn wir nun mehrere Textureinheiten benutzen können wir verschiedene Effekte erzielen, indem wir unsere verschiedenen Texturen miteinander verknüpfen. In diesem Falle, benutzen wir die Standardeinstellung, wo die Farben der beiden Texturen miteinander addiert werden, was eine neue Farbe ergibt. Wir machen das ganze, weil eine einzige Textur auf unserem Terrain verteilt ziemlich langweilig aussieht und man die Wiederholungen, die wir auch verwenden ziemlich schnell erkennt. Neu hinzu kommen unter anderem auch die Zeilen 67 und 75, wo wir den sogenannten Wrap Mode unseres Textur auf Texture.WM_WRAP_S_WRAP_T setzen. Dieser Wrap Mode setzt fest was, wir machen wollen wenn wir unsere Textur mehrmals auf unserem Objekt verwenden wollen. Mit diesem Modus legen wir fest, dass wir unsere Textur kacheln wollen, also das sie einfache mehrere Male nebeneinander gereiht werden soll. Als letzte Neuerung setzen wir in Zeile 79 und 80 den Wiederholungswert unserer Texturen. Wir haben die Grastextur standardmässig auf Textureinheit 0 gestellt und die Detailtextur auf die Textureinheit 1. Hier geben wir nun an, dass wir unsere Grastextur zehn mal und unsere Detailtextur vier mal auf unserer Landschaft wiederholen möchten. 9.5. Skybox Wenn Sie zurück blicken auf das letzte Kapitel, in dem wir eine texturierte Landschaft erstellt, haben, werden Sie immer noch zweifeln, ob diese Landschaft tatsächlich in der Realität vorkommen kann, nehme ich an. Was besonders stört, ist der schwarze Hintergrund. Wenn wir draussen umschauen, haben wir immer einen Horizont um uns und sehen die Sonne, den Himmel, falls wir auf dem Land wohnen vielleicht sogar einige Berge und sonstige Hügellandschaften. Im 3D-Rendering gibt es nun einige Tricks, wie wir einen Horizont simulieren können. Einen der einfachsten dieser Tricks nennt man Skybox und wir direkt von jME zur Verfügung gestellt. Die Idee hinter einer Skybox ist sehr simpel. Die entfernte Landschaft wird auf sechs Texturen aufgeteilt, vier für jede Himmelsrichtung und je eine Textur für oben und unten. Diese sechs Texturen werden auf die Innenseite eines Würfels geklebt. Die Kamera, also wir selbst in der Demo befinden uns in der Mitte dieses Würfels. Wir können uns frei rotieren, unsere Position bleibt aber immer fix in der Mitte des Würfels. Wenn die Szene gerendert wird erzeugen die Texturen, die auf den Würfel projiziert werden, den Eindruck von einem Horizont. Auf der Abbildung 9.8 können Sie die Bilder sehen die wir als Texturen verwenden werden. Wie Sie sehen, passen die Ränder nahtlos aufeinander. Wenn Sie das Bild ausschneiden würden, könnten Sie ausserdem daraus einen Würfel basteln. Dieses Bild wird später in sechs Teilbilder geschnitten, was die sechs oben genannten Texturen gibt. Diese Bilder werden in der Regel von speziellen 3D-Anwendungen erzeugt wie zum Beispiel Terragen[55]. Sie können aber im Internet schon viele freie solche Skybox Bilder finden, zum Beispiel auf der Seite Octane Digital Studios[45], von der auch das hier verwendete Bild stammt. In der folgenden Demo wollen wir zeigen, wie Sie eine Skybox in jME erstellen können. Wir werden in dieser Demo nicht mehr die ganze Arbeit in unserer simpleInitGame Methode machen. Wir haben eine eigene Skybox Klasse erzeugt, die wir in unserer Demo Klasse SkyBoxTest verwenden werden. Auf diese Weise können wir die SkyBox Klasse direkt in unserer Enddemo FinalIsland wieder verwenden. Wir fangen hier mit dem Quellcode von SkyBoxTest an. Den ganzen Quellcode finden Sie auf der CD (siehe Anhang A) im Verzeichnis demos/specialfx in der Datei mit dem Namen SkyBoxTest.java. Listing 9.3: SkyBoxTest.java 36 37 38 39 protected void simpleInitGame () { // set the title of our application display . setTitle ( " SkyBox test " ); 40 41 // create our skybox 64 9.5. Skybox Abbildung 9.8.: Würfelbild mit der Skybox Abbildung 9.9.: Screenshot von der Skybox Demo 65 9.5. Skybox sb = new SkyBox ( rootNode , cam . getLocation () ); 42 43 // create an additional box in our scene Box b = new Box ( " box " , new Vector3f ( -1, -1, -1 ) , new Vector3f ( 1, 1, 1 ) ); rootNode . attachChild ( b ); 44 45 46 47 initText () ; assignKeys () ; 48 49 50 } 51 52 53 54 55 56 57 58 59 /* * * this method is called every frame to eventually update all things */ protected void simpleUpdate () { sb . update () ; checkInput () ; } Das einzig neue das in dieser Demo erscheint, ist die Instanzierung einer Skybox auf Zeile 42. Wir haben das Feld sb in der Klasse als private SkyBox sb definiert. Die Klasse benötigt einen Node als Argument und zusätzlich die Position der Kamera. Wie wir ja bereits erwähnt haben, wird sich die SkyBox immer mit der Kamera bewegen, was wir damit bewerkstelligen wollen. In dieser Demo müssen wir zusätzlich noch die Methode simpleUpdate verwenden, um die SkyBox in jedem Frame bewegen zu können. Somit haben wir unsere Demo erstellt. Schauen wir uns nun die Klasse SkyBox an. Diese Klasse ist ebenfalls auf der CD im Verzeichnis demos/specialfx zu finden. Sie trägt den Namen SkyBox.java. Listing 9.4: SkyBox.java 1 package specialfx ; 2 3 import java . net . URL ; 4 5 6 7 8 9 import import import import import com . jme . image . Texture ; com . jme . math . Vector3f ; com . jme . scene . Node ; com . jme . scene . Skybox ; com . jme . util . TextureManager ; 10 11 12 13 14 15 16 17 18 19 /* * * @author Daniel Egger , daniel . egger@unifr . ch * * this is a skybox helper class */ public class SkyBox { // A sky box for our scene private Skybox skybox ; 20 21 private Vector3f camLocation ; 22 23 24 25 26 27 /* * * a helper class to create our skybox * @param node we will attach our skybox to this node * @param camLocation we want to track the camera so we store its location */ 9.5. Skybox 28 29 30 31 public SkyBox ( Node node , Vector3f camLocation ) { // we have to store the camera location this . camLocation = camLocation ; 32 // initialize the skybox skybox = new Skybox ( " skybox " , 500 , 500 , 500 ); 33 34 35 // load all the 6 side textures for our skybox URL texturelocation ; 36 37 38 // load the west side of the skybox texturelocation = SkyBoxTest . class . getClassLoader () . getResource ( " data / blue_right . jpg "); skybox . setTexture ( Skybox . WEST , TextureManager . loadTexture ( texturelocation , Texture . MM_LINEAR , Texture . FM_LINEAR ) ); 39 40 41 42 43 44 // load the east side of the skybox texturelocation = SkyBoxTest . class . getClassLoader () . getResource ( " data / blue_left . jpg "); skybox . setTexture ( Skybox . EAST , TextureManager . loadTexture ( texturelocation , Texture . MM_LINEAR , Texture . FM_LINEAR ) ); 45 46 47 48 49 50 // load the upper side texturelocation = SkyBoxTest . class . getClassLoader () . getResource ( " data / blue_up . jpg "); skybox . setTexture ( Skybox .UP , TextureManager . loadTexture ( texturelocation , Texture . MM_LINEAR , Texture . FM_LINEAR ) ); 51 52 53 54 55 56 // load the down side texturelocation = SkyBoxTest . class . getClassLoader () . getResource ( " data / blue_down . jpg "); skybox . setTexture ( Skybox . DOWN , TextureManager . loadTexture ( texturelocation , Texture . MM_LINEAR , Texture . FM_LINEAR ) ); 57 58 59 60 61 62 // load the north side texturelocation = SkyBoxTest . class . getClassLoader () . getResource ( " data / blue_front . jpg "); skybox . setTexture ( Skybox . NORTH , TextureManager . loadTexture ( texturelocation , Texture . MM_LINEAR , Texture . FM_LINEAR ) ); 63 64 65 66 67 68 // load the south side texturelocation = SkyBoxTest . class . getClassLoader () . getResource ( " data / blue_back . jpg "); skybox . setTexture ( Skybox . SOUTH , TextureManager . loadTexture ( texturelocation , Texture . MM_LINEAR , Texture . FM_LINEAR ) ); 69 70 71 72 73 74 node . attachChild ( skybox ); 75 76 } 77 78 79 public void update () { 66 9.5. Skybox // we have to move our skybox according to the camera location skybox . setLocalTranslation ( camLocation ); 80 81 } 82 83 67 } Die Klasse ist sehr einfach gehalten und bedarf kaum einer Erklärung. Wir laden unsere sechs Texturen und weisen sie jeweils einer Richtung in der Skybox zu. Wie gesagt stellt uns jME bereits eine Skybox zur Verfügung, die wir auch verwenden. Wir müssen in der Methode update die Skybox jeweils neu mit der Kamera verschieben, damit die Kamera auch immer genau im Zentrum bleibt. Und das ist schon die ganze Hexerei. Bei der Skybox handelt es sich eigentlich um einen Trick Horizonte darzustellen. Sie haben vielleicht bereits erkannt, das der Horizont in dieser Technik fast immer statisch ist. Das kann störend wirken, zum Beispiel bewegen sich die Wolken nicht und auch der Sonnenstand bleibt immer gleich. Es gibt eine Vielzahl von weiteren Techniken um realistische Horizonte und Wolken darzustellen, wie zum Beispiel das Rendern von Skydomes. Im Buch [Pol02] und auf der Webseite [60] können Sie weiter Informationen dazu finden. 10 Final Island 10.1. Der Code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68 10.2. Fazit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72 10.1. Der Code Wir kommen nun zum letzten Abschnitt in unserem Tutorial. Wir haben nun genügend Grundlagen gelernt und können alle zusammen in einer kleinen Demo anwenden. In unserer Abschlussdemo können wir auf einer kleinen Insel laufen, wir können uns nicht mehr frei umher bewegen, wie in den vorherigen Demos. Ausserdem werden wir im Wasser nur langsam vorwärts kommen und können auch nicht tauchen. Auf den Abbildungen 10.1 und 10.2 sehen Sie zwei Screenshots von unserer Final Island Demo. Wir verwenden in dieser Demo alles, was wir in den vorherigen Kapiteln gelernt haben. Wir habe eine schon texturierte Landschaft mit einem Horizont, den wir als Skybox implementiert haben. Das Licht, das einer Sonne empfunden ist scheint aus einem eher tieferen Winkel, was einer schönen Abendsonne entspricht. Die meisten Element der Demo sind in eigenen Klassen definiert, wie Sie später noch sehen werden. Die Demo finden Sie im Unterordner demos/finalisland in der Datei FinalIsland.java auf der CD (siehe Anhang A). Im Listing 10.1 ist der ganze Quellcode abgedruckt: Listing 10.1: FinalIsland.java 1 package finalisland ; 2 3 import helper . TutorialGame ; 4 5 6 7 8 9 import import import import import com . jme . light . DirectionalLight ; com . jme . math . Vector3f ; com . jme . renderer . ColorRGBA ; com . jme . scene . Node ; com . jme . scene . state . LightState ; 10 11 12 13 14 /* * * The Final Island , a simple tech demo of what we learned * @author Daniel Egger */ 15 16 17 18 19 public class FinalIsland extends TutorialGame { // the helper classes from the same package private SkyBox skybox ; 68 69 10.1. Der Code Abbildung 10.1.: Final Island: Blick auf das Wasser Abbildung 10.2.: Final Island: Blick auf das Jeep-Modell 10.1. Der Code 20 21 22 70 private Terrain terrain ; private Water water ; private Jeep jeep ; 23 24 25 26 27 public static void main ( String [] args ) { new FinalIsland () ; } 28 29 30 31 protected void simpleInitGame () { display . setTitle ( " Final Island " ); 32 // set up the scene graph Node terrainScene = new Node ( " terrainScene " ); 33 34 35 // create the skybox skybox = new SkyBox ( terrainScene , cam . getLocation () ); 36 37 38 // create the terrain terrain = new Terrain ( terrainScene ); 39 40 41 // create the water water = new Water ( terrainScene , 39.7147 f ); 42 43 44 // create a jeep jeep = new Jeep ( terrainScene ); jeep . setPosition ( new Vector3f ( 167 , terrain . getHeightAt ( 167 , 190 ) , 190 ) ); 45 46 47 48 // simulate a sun LightState ls = display . getRenderer () . createLightState () ; DirectionalLight light = new DirectionalLight () ; light . setDirection ( new Vector3f ( 0.6f , -0.75f , 0.6 f ) ); float intensity = 1.0 f; light . setDiffuse ( new ColorRGBA ( intensity , intensity , intensity , 1.0 f ) ); light . setAmbient ( new ColorRGBA ( intensity , intensity , intensity , 1.0 f ) ); light . setEnabled ( true ); ls . attach ( light ); terrainScene . setRenderState ( ls ); 49 50 51 52 53 54 55 56 57 58 59 // switch of the standard light lightState . detachAll () ; 60 61 62 // attach the whole scene and update the render states rootNode . attachChild ( terrainScene ); rootNode . updateRenderState () ; 63 64 65 66 // move the camera to the middle of the island cam . setLocation ( new Vector3f ( 150 , 80 , 180 ) ); 67 68 69 } 70 71 72 73 74 protected void simpleUpdate () { Vector3f vec = cam . getLocation () ; 10.2. Fazit 71 // check that we cannot leave the terrain // terrain has a size of about 252 * 252 units if ( vec .x < 2 ) vec .x = 2; if ( vec .x > 250 ) vec .x = 250; if ( vec .z < 2 ) vec .z = 2; if ( vec .z > 250 ) vec .z = 250; 75 76 77 78 79 80 81 // set our camera height to the terrain heigth // to simulate moving on the terrain vec .y = terrain . getHeightAt ( vec .x , vec .z ) + 2; 82 83 84 85 // check that we do not go into the water float waterHeight = water . getHeight () + 1.0 f; if ( vec .y < waterHeight ) { vec .y = waterHeight ; input . setKeySpeed ( 5 ); } else { input . setKeySpeed ( 10 ); } 86 87 88 89 90 91 92 93 94 95 96 97 // update the water height water . update ( tpf ); 98 99 100 // update the skybox skybox . update () ; 101 102 } 103 104 } Wir deklarieren als erstes einige Hilfsobjekte in den Zeilen 18 bis 22. Diese Hilfsobjekte haben wir in eigenen Klassen implementiert, die Sie im gleichen Unterordner demos/finalisland finden können. Die Klassen heissen SkyBox.java, Terrain.java, Water.java und Jeep.java. Diese Klassen implementieren genau das, war wir in der vorherigen Kapiteln gelernt haben. Wir werden Sie also nicht noch einmal hier wiedergeben. Sehen wir uns zuerst die allseits bekannte und beliebte Methode simpleInitGame an. In den Zeilen 33 bis 47 initialisieren wir diese Objekte. Wir geben jeweils den Node terrainScene als Argument an. Unsere Objekte werden dann ihrerseits alles an terrainScene anhängen. Je nach Objekt müssen wir noch weiter Argumente übergeben, was diese aber für eine Bedeutung haben, sehen Sie am besten selbst nach im Quellcode, der jeweiligen Objekte. Als nächstes definieren wir eine Parallellicht in den Zeilen 49 bis 58. Das Licht soll auf einfache Weise einer Sonne entsprechen. Wir haben schon in den vorherigen Kapiteln, das genau gleiche gemacht. Deshalb gehen wir hier nicht weiter darauf ein. Das einzige neue, das wir hier in FinalIsland machen, finden Sie in der Methode simpleUpdate. Wir wollen hier überprüfen, dass wir den Spielfeldrand nicht überschreiten und auch nicht in das Wasser abtauchen können. Die Landschaft erstreckt sich auf 255 * 255 Einheiten. Wir müssen also überprüfen, dass unsere x- und z-Koordinate in den Grenzen 2 bis 250 bleiben, damit wir uns zu jeder Zeit auf dem Terrain befinden. Genau diese Überprüfung nehmen wir in den Zeilen 75 bis 80 vor. Falls wir die Grenzen der erlaubten Zahlen verlassen, setzen wir die Position der Kamera einfach auf den erlaubten Minimal- bzw. Maximalwert. In der Zeile 84 rufen wir die Höhe des Terrains ab auf dem wir stehen und setzen die Kamera um zwei Einheiten höher auf. 72 10.2. Fazit Abbildung 10.3.: Beispiel für eine Particle Engine 10.2. Fazit Wir haben zusammen in vielen kleinen Schritten, die Grundlagen der 3D-Grafik mit Hilfe der 3D-Engine jME gelernt und trotzdem haben wir bis jetzt nur die Spitze des Eisbergs gesehen. Wir haben viele Möglichkeiten von jME noch gar nicht richtig ausgeschöpft. Mit keinem Wort haben wir beispielsweise Particle Engines (siehe Abbildung 10.3 bedacht, mit denen man einige wunderbare Effekte erzielen kann. Ich empfehle Ihnen nun die Demos, die zusammen mit jME ausgeliefert werden genauer zu studieren. Auf der Seite von jME gibt es ausserdem noch weitere Dokumentation. Dort finden Sie auch die pdfDatei Learning jME [Lin] von Jack Lindamood, das zum Teil die gleichen Themen, wie das vorliegende Tutorial behandelt, aber von dem Sie bestimmt noch einiges lernen können. So viel zum grafischen Teil des Tutorials. Was wir nun sträflich vernachlässigt haben sind Dinge wie die Kollisionserkennung und auch Animationen. Dabei handelt es sich um ziemlich komplexe Teilgebiete mit denen man auch Bücher oder weitere Bachelorarbeiten füllen könnte und dementsprechend aus Platzmangel und Zeitgründen keinen Platz in diesem Tutorial fanden. Wer sich mit diesem Bereich der Programmierung noch weiter beschäftigen möchte sollte einen Blick auf das jME Physics System[35] werfen. Diese Library versucht unsere Welt in physikalischer Hinsicht möglichst genau nachzubilden. Es lohnt sich ausserdem immer mal wieder die Webseite von jME[36] zu konsultieren, weil es immer wieder interressante Links auf Projekte gibt und auch die Dokumentation ständig verbessert und weiter aufgebaut wird. A CD-ROM Inhalt der mitgelieferten CD-ROM: • Im Verzeichnis dateien sind weiter Dokumente aufgeführt, die für Sie nützlich sein können. • Den Quellcode und ausführbare Klassen aller Demos finden Sie im Unterordner demos. Die Klassen sind jeweils im Unterordner mit dem jeweiligen Paketnamen zu finden. Ausserdem finden Sie im Ordner demos batch-Dateien und shell-Scripts um die jeweiligen Demos gleich starten zu können. In den jeweiligen Kapiteln wird jeweils erwähnt, wie das Unterverzeichnis heisst, das die Demos enthält. Falls Sie interessiert sind die Demos selbst zu verändern finden Sie in Anhang B eine Anleitung, wie Sie die Demos selbst kompilieren können. • Im Unterordner dokumentation finden Sie eine Kopie dieses Dokumentes im pdf-Format und den Quellcode dieses Dokumentes im LATEX-Format. • Der Unterodner finalisland_directx schlussendlich enthält die Demo des Gameversity-Kurses 73 74 |-| | | |-| | | | | | | | | | | | |-| ‘-- dateien |-- Learning_jME . pdf |-- gameinstitute ‘-- gameversity demos |-- data |-- finalisland |-- firststeps |-- geometry |-- helper |-- keyinput |-- lib |-- light |-- specialfx |-- terrain |-- textures ‘-- theory dokumentation ‘-- source finalisland_directx Abbildung A.1.: CD-Rom Inhalt B Ein jME-Demo in der Konsole starten und kompilieren B.1. Demos starten Es ist nicht ganz einfach eine jME-Demo in der Konsole zu starten. Die Bibliothek jME ist in mehrere Dateien aufgeteilt, ausserdem wird mit dem OpenGL-Wrapper LWJGL eine Systemeigene Library verwendet. Das müssen wir Java speziell mitteilen. Sehen wir us am besten ein Beispiel an. Um die Demo HelloWorld.java zu starten, müssen wir unter Windows folgende Eingabe machen im Verzeichnis demos: java - Djava . library . path =./ lib -cp ./ lib / lwjgl . jar ;./ lib / jme . jar ; firststeps / HelloWorld Mit dem Argument -Djava.library.path=./lib geben wir an, dass das jeweilige Betriebssystem in diesem Unterverzeichnis nach Betriebseigenen Bibliotheken suchen soll. jME benötigt in Windows unter anderem die Datei lwjgl.dll die in diesem Verzeichnis zu finden ist. Entsprechende Dateien für Linux und MacOSX sind in diesem Ordner ebenfalls vorhanden. Mit der Argument -cp geben wir die einzelnen jar-Dateien an, die wir benötigen. Für ein jME-Demo brauchen wir immer zumindest die Dateien lwjgl.jar und jme.jar. Bei mehreren jar-Dateien müssen diese unter Windows immer mit einem Semikolon abgetrennt werden. Unter Linux und MacOSX benötigt man einen Doppelpunkt als Abtrenner. Der Befehl um eine Demo zu starten lautet unter Linux und MacOSX also folgendermassen: java - Djava . library . path =./ lib -cp ./ lib / lwjgl . jar :./ lib / jme . jar : firststeps / HelloWorld Wie in den vorherigen Kapiteln im Text bereits erwähnt, finden Sie im Verzeichnis demos jeweils batchDateien und Shell-Skripts, die diese Befehle bereits enthalten. B.2. Demos kompilieren Ähnlich müssen wir vorgehen wenn wir eine Demo kompilieren wollen. Es reicht wenn wir in diesem Falle den Classpath anpassen. Wenn wir also die Demo HelloWorld.java kompilieren wollen, müssen wir unter Windows folgenden Befehl eingeben: javac -cp ./ lib / lwjgl . jar ;./ lib / jme . jar ; firststeps / HelloWorld . java Achten Sie wiederum darauf, dass Sie sich mit der Kommandozeile im Verzeichnis demos befinden. Um eine Demo zu kompilieren müssen Sie natürlich den ganzen Ordner von der CD zuerst auf die lokale Festplatte kopiert haben. Unter Linux und MacOSX müssen Sie wiederum die Semikolons durch Doppelpunkte ersetzen, hier ein Beispiel: 75 B.2. Demos kompilieren 76 javac -cp ./ lib / lwjgl . jar :./ lib / jme . jar : firststeps / HelloWorld . java Das ganze Vorgehen kann in der Kommandozeile ziemlich mühsam werden. Auf der Seite von [36] finden Sie eine ausführliche Anleitung, wie Sie ein jME-Projekt in Eclipse[22] aufsetzen können. C Verwendete Software C.1. Entwicklung C.1.1. Java Die mitgelieferten jME-Demos und jME selbst wurden mit Java 1.4[32] entwickelt. Es wurde diese Java Version ausgewählt, das Sie zum Startzeitpunkt auf allen drei benutzten Betriebssystemen, WindowsXP, Linux und MacOSX eine lauffähige Version existiert. C.1.2. Eclipse Als Entwicklungsumgebung wurde die freie OpenSource IDE Eclipse[22] ausgewählt. Alle Demos wurden in Eclipse implementiert. Eclipse wurde unter anderem ausgewählt, weil es unter allen benutzten Betriebssystemen einwandfrei läuft. Ausserdem war der Portieraufwand mit Eclipse minimal. Auf der Website von jME[36] wird zusätzlich ausführlich erklärt, wie Sie mit Eclipse eine neue Version von jME direkt mit dem Versionskontrollsystem CVS[18] beziehen können. Diese Vorgehen wir auch von den Autoren von jME vorgeschlagen, da sich jME immer noch in der Entwicklung befindet. C.2. Dokumentation C.2.1. LATEX und Co Der grösste Teil der Dokumentation wurde mit dem Programm LyX[41] geschrieben. LyX ist ein grafisches Frontend für das Textsatzsystem LATEX[39], das nach dem WYSIWYM1 -System arbeitet im Gegensatz zu herkömmlichen Textverarbeitungssystemen, die nach dem WYSIWYG2 -System arbeteien. Am Ende des Projektes wurde der gesamte Text direkt in LATEX formatiert und letzte Änderungen wurden mit dem LATEX-Editor Kile[38] vorgenommen. Für die Erstellung des Literaturverzeichnisses und des Online-Verzeichnisses wurde BibTEX[11] verwendet. BibTEX ist ein Werkzeug zur Erstellung von Literaturverzeichnissen mit LATEX. BibTEX verwendet dazu eine Datebank von Werken in einer Textdatei, die eine bestimmte Syntax verwendet. Diese Textdatenbank wurde mit OpenSource Tool JabRef[31] erstellt. Die meisten Bücher wurden ausserdem mit dem Online-Tool Amatex[6] gefunden. Amatex sucht anhand der ISBN-Nummer oder anderen Angaben eines Werkes alle nötigen BibTEX angaben mit einer Abfrage von Amazon[7] . C.2.2. Violet UML-Tool Die UML-Diagramme wurden mit dem Tool Violet[59] erstellt. Violet ist ein sehr kleiner OpenSource UML-Tool, mit dem sehr leicht, einfache UML-Diagramme erstellen kann. 1 What 2 What you see is what you mean you see is what you get 77 C.2. Dokumentation 78 C.2.3. Dia Diagramm-Editor Einige Abbildungen in diesem Dokument wurden mit dem OpenSource Programm Dia[19] erzeugt. Dia ist laut der Homepage ähnlich aufgebaut wie das Windows Programm Visio. Es eignet sich hervorragend um Diagramme und ähnliche Abbildungen zu erzeugen. Literaturverzeichnis [AH01] Dave Astle and Kevin Hawkins. OpenGL Game Programming. Prima Tech, 2001. [AMH02] Tomas Akenine-Moller and Eric Haines. Real-Time Rendering. AK Peters, Ltd., 2nd edition edition, 2002. http://www.realtimerendering.com/ (letzter Aufruf 29. August, 2005). [BBV03] David Brackeen, Bret Barker, and Laurence Vanhelswue. Developing Games in Java. New Riders Publishing, 2003. [BZ] Avi Bar-Zeev. Scenegraphs: Past, Present and Future. Website. http://www.realityprime.com/ scenegraph.php (letzter Aufruf 29. August, 2005). [Dav05] Andrew Davison. Killer Game Programming in Java. O’Reilly, 2005. http://fivedots.coe. psu.ac.th/~ad/jg/ (letzter Aufruf 1. September, 2005). [DP02] Fletcher Dunn and Ian Parberry. 3D Math Primer for Graphics and Game Development. Wordware Publishing Inc., 2002. [Ebe00] David H. Eberly. 3D Game Engine Design : A Practical Approach to Real-Time Computer Graphics. Morgan Kaufmann, 2000. [Ebe04] David H. Eberly. 3D Game Engine Architecture. Morgan Kaufmann, 2004. [Eck02] Bruce Eckel. Thinking in Java. Prentice Hall PTR, 3rd edition edition, 2002. [Eng04] Wolfgang Engel. Programming Vertex and Pixel Shaders. Charles River Media, 2004. [Fow04] Martin Fowler. UML Distilled. Addison Wesley, 2004. [FvDFH95] James D. Foley, Andries van Dam, Steven K. Feiner, and John F. Hughes. Computer Graphics: Principles and Practice in C. Addison-Wesley Professional, 2nd edition edition, 1995. [GHJV95] Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides. Addison-Wesley Professional, 1995. [Len03] Eric Lengyel. Mathematics for 3D Game Programming and Computer Graphics. Charles River Media, 2nd edition edition, 2003. [Lin] Jack Lindamood. Learning jME. Website. https://www.dev.java.net/files/documents/73/10905/ Starter.pdf (letzter Aufruf 29. August, 2005), auch auf der mitgelieferten CD enthalten. [Lun03] Frank D. Luna. Introduction to 3D Game Programming with DirectX 9.0. Wordware Publishing, Inc., 2003. 79 Design Patterns. Literaturverzeichnis 80 [Pol02] Trent Polack. Focus On 3D Terrain Programming. Muska & Lipman/Premier-Trade, 2002. [Sal01] Dave Salvator. ExtremeTech 3D Pipeline Tutorial. Website, 2001. http://www.extremetech. com/article2/0,1558,9722,00.asp (letzter Aufruf 29. August, 2005). [SWN+ 03] David Shreiner, Mason Woo, Jackie Neider, Tom Davis, and OpenGL Architecture Review Board. Opengl Programming Guide: The Official Guide to Learning Opengl, Version 1.4. Addison Wesley, 2003. http://fly.cc.fer.hr/~unreal/theredbook/ (letzter Aufruf 29. August, 2005). [WP00] Alan Watt and Fabio Policarpo. 3D Games: Real-Time Rendering and Software Technology, Volume 1. Addison Wesley, 2000. [ZDA04] Stefan Zerbst, Oliver Düvel, and Eike Anderson. 3D-Spieleprogrammierung Kompendium. Markt und Technik, 2004. Web Ressourcen [1] 3D Engine Database. http://www.devmaster.net/engines/ (letzter Aufruf 25. August, 2005). [2] 3D Links. http://www.3dlinks.com (letzter Aufruf 25. August, 2005). [3] 3D Studio Max. http://www.discreet.com/3dsmax (letzter Aufruf 25. August, 2005). [4] AC3D. http://www.ac3d.org (letzter Aufruf 25. August, 2005). [5] Alias Maya. http://www.alias.com/maya (letzter Aufruf 25. August, 2005). [6] Amatex. http://www.2ndminute.org:8080/amatex (letzter Aufruf 26. September, 2005). [7] Amazon. http://www.amazon.com (letzter Aufruf 26. September, 2005). [8] Ati. http://www.ati.com (letzter Aufruf 25. August, 2005). [9] Aviatrix 3D Engine. http://aviatrix3d.j3d.org (letzter Aufruf 25. August, 2005). [10] Battlezone Review. http://www.thelogbook.com/phosphor/summer99/batlzone.html (letzter Aufruf 25. August, 2005). [11] BibTeX Wikipedia Eintrag. http://de.wikipedia.org/wiki/BibTeX (letzter Aufruf 26. September, 2005). [12] Blender. http://www.blender.org (letzter Aufruf 25. August, 2005). [13] BSD License. http://www.opensource.org/licenses/bsd-license.php (letzter Aufruf 25. August, 2005). [14] Collada. http://www.collada.org (letzter Aufruf 25. August, 2005). [15] Creepy’s Object Download. http://www.creepyclown.com/downloads.htm (letzter Aufruf 25. August, 2005). [16] CryEngine. http://www.crytek.de/technology/index.php?sx=cryengine (letzter Aufruf 25. August, 2005). [17] Crystal Space Engine. http://www.crystalspace3d.org (letzter Aufruf 25. August, 2005). [18] CVS - Concurrent Versions System. http://www.nongnu.org/cvs (letzter Aufruf 26. September, 2005). [19] Dia. http://www.gnome.org/projects/dia (letzter Aufruf 26. September, 2005). [20] DirectX. http://www.microsoft.com/windows/directx/ (letzter Aufruf 25. August, 2005). [21] Dreamworks Animation. http://www.dreamworksanimation.com (letzter Aufruf 25. August, 2005). [22] Eclipse. http://www.eclipse.org (letzter Aufruf 2. September, 2005). 81 Web Ressourcen 82 [23] Elder Scrolls IV, Oblivion. http://www.elderscrolls.com/games/oblivion_overview.htm (letzter Aufruf 26. September, 2005). [24] Espresso 3D Engine. http://www.espresso3d.com (letzter Aufruf 25. August, 2005). [25] Far Cry. http://www.farcry.de/ (letzter Aufruf 29. August, 2005). [26] Game Institute. http://www.gameinstitute.com (letzter Aufruf 2. September, 2005). [27] Gameversity. http://www.gameversity.com (letzter Aufruf 2. September, 2005). [28] gmax. www.autodesk.com/gmax (letzter Aufruf 25. August, 2005). [29] id Software. http://www.idsoftware.com (letzter Aufruf 25. August, 2005). [30] Irrlicht Engine. http://irrlicht.sourceforge.net/ (letzter Aufruf 25. August, 2005). [31] JabRef. http://jabref.sourceforge.net (letzter Aufruf 26. September, 2005). [32] Java. http://java.sun.com (letzter Aufruf 26. September, 2005). [33] Java 3D. http://java.sun.com/products/java-media/3D/ (letzter Aufruf 24. August, 2005). [34] jME Javadoc Documentation. http://www.jmonkeyengine.com/doc/ (letzter Aufruf 25. August, 2005). [35] jME Physics System. http://jme-physics.sourceforge.net (letzter Aufruf 12. Oktober, 2005). [36] jMonkey Engine. http://jmonkeyengine.com/ (letzter Aufruf 24. August, 2005). [37] Jogl Java bindings for OpenGL. https://jogl.dev.java.net/ (letzter Aufruf 25. August, 2005). [38] Kile. http://kile.sourceforge.net (letzter Aufruf 26. September, 2005). [39] LaTeX. http://www.latex-project.org (letzter Aufruf 26. September, 2005). [40] Lightweight Java Game Library. http://www.lwjgl.org (letzter Aufruf 25. August, 2005). [41] LyX. http://www.lyx.org (letzter Aufruf 26. September, 2005). [42] Maya Personal Learning Edition. http://www.alias.com/glb/eng/products-services/product_details.jsp? productId=1900003 (letzter Aufruf 25. August, 2005). [43] Milkshape 3D. http://www.swissquake.ch/chumbalum-soft/ (letzter Aufruf 25. August, 2005). [44] NVidia. http://www.nvidia.com (letzter Aufruf 25. August, 2005). [45] Octane Digital Studios. http://www.octanedigitalstudios.com/page4 (letzter Aufruf 26. September, 2005). [46] OGRE Engine. http://www.ogre3d.org (letzter Aufruf 25. August, 2005). [47] OpenGL. http://www.opengl.org (letzter Aufruf 25. August, 2005). [48] Paint Shop Pro. http://www.jasc.com (letzter Aufruf 26. September, 2005). [49] Panda3D. http://www.panda3d.org/ (letzter Aufruf 2. September, 2005). [50] Pixar. http://www.pixar.com (letzter Aufruf 25. August, 2005). [51] Pixars Geri’s Game. http://www.pixar.com/shorts/gg/ (letzter Aufruf 25. August, 2005). [52] Psionic’s 3D Game Resources. http://www.psionic3d.co.uk (letzter Aufruf 25. August, 2005). Web Ressourcen [53] Quake 3. http://www.idsoftware.com/games/quake/quake3-arena/ (letzter Aufruf 25. August, 2005). [54] SOFTIMAGE|XSI. http://www.softimage.com/xsi (letzter Aufruf 25. August, 2005). [55] Terragen. http://www.planetside.co.uk/terragen (letzter Aufruf 26. September, 2005). [56] Texture Warehouse. http://www.texturewarehouse.com (letzter Aufruf 26. September, 2005). [57] Unreal 3 Engine. http://www.unrealtechnology.com (letzter Aufruf 25. August, 2005). [58] Valve Software. http://valvesoftware.com (letzter Aufruf 25. August, 2005). [59] Violet UML-Tool. http://www.horstmann.com/violet (letzter Aufruf 26. September, 2005). [60] Virtual Terrain Project. http://www.vterrain.org (letzter Aufruf 2. September, 2005). [61] Wikipedia 3dfx. http://de.wikipedia.org/wiki/3dfx (letzter Aufruf 25. August, 2005). [62] Wings3D. http://www.wings3d.com (letzter Aufruf 25. August, 2005). [63] Wolfenstein 3D. http://www.3drealms.com/wolf3d/ (letzter Aufruf 25. August, 2005). 83