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

Similar documents