Java für Fortgeschrittene Proseminar Sommersemester 2009 Grafisches Java - Java OpenGL Ludwig Nägele Technische Universität München 10. Mai 2009 1 Inhaltsverzeichnis 1 Zusammenfassung 3 2 Einleitung 2.1 Vorläufer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.2 Entstehung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.3 Überblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 3 4 4 3 Verwendung 3.1 Installation . . . . . . . . . . . . . . . 3.2 Überblick . . . . . . . . . . . . . . . . 3.3 Klassenaufbau . . . . . . . . . . . . . . 3.3.1 GLCanvas . . . . . . . . . . . . 3.3.2 GL . . . . . . . . . . . . . . . . 3.3.3 GLU . . . . . . . . . . . . . . . 3.3.4 GLEventListener . . . . . . . . 3.4 Vorgehensweise anhand eines Beispiels 3.5 Objektverwaltung . . . . . . . . . . . . 3.6 Perspektive / Kamera . . . . . . . . . 3.7 Animation . . . . . . . . . . . . . . . . 3.8 Beleuchtung . . . . . . . . . . . . . . . 3.9 Materialien . . . . . . . . . . . . . . . 3.10 Eingabeverarbeitung . . . . . . . . . . 3.11 Einlesen von externen 3D-Objekten . . 3.12 JOGLAppletLauncher . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 5 5 6 7 7 8 8 9 11 13 14 15 16 17 17 17 4 Demoprojekt 18 5 Fazit 18 6 Literatur 19 2 1 Zusammenfassung Dieser Artikel gibt einen kurzen Einblick in die wichtigsten Funktionsweisen und Möglichkeiten der Programmbibliothek JOGL. Die Anwendung dieser Programmbibliothek wird anhand eines durchgehenden Beispiels verdeutlicht. Detailliertere und weiterführende Informationen sind unter Anderem unter den im Quellenverzeichnis angegebenen Links nachzulesen. 2 Einleitung Ein Computerspiel der heutigen Zeit hat nur dann eine auf dem Markt Erfolg versprechende Chance, wenn es neben einer spannenden Handlung unter Anderem auch eine ansprechende und atemberaubende Grafik bietet – natürlich in 3D. Ebenso wie CAD-Programme zur Herstellung von dreidimensionalen Werkstücken in der Industrie, benötigen auch Echtzeitsimulationsprogramme eine schnelle und zuverlässige Berechnungsmöglichkeit. Da dies mit dem Prozessor eines Heimrechners nicht ruckelfrei zu bewerkstelligen wäre, nutzt man hierbei zusätzlich die Rechenkapazität der Grafikkarte. In Java wird dem Entwickler mit JOGL eine Schnittstelle zu eben dieser angeboten. Mit JOGL (Java OpenGL) wurde eine externe OpenGL-Programmbibliothek für Java entwickelt, mithilfe derer ein Programmierer auf OpenGL-Funktionen zugreifen kann. Hierzu werden spezielle Java-Wrapperklassen bereitgestellt, die die Schnittstellen zu den nativen Funktionen von OpenGL bereitstellen. Die angebotenen Methoden führen dabei in der Regel einfach den korrespondierenden nativen C-Code aus. OpenGL ist eine plattformunabhängige Bibliothek, wohingegen das bekannte DirectX als Microsoftprodukt nur auf Windowssystemen läuft. Java bietet mit Java3D neben JOGL noch eine alternative Grafik-Entwicklerschnittstelle an, die beide Bibliotheken unterstützt. Ihre Objektstruktur abstrahiert von den zugrunde liegenden Schnittstellenmethoden und ist daher in der Verwendung verschieden von JOGL. 2.1 Vorläufer Mangels Unterstützung von OpenGL konnte sich Java in den ersten Jahren nach Erscheinung 1995 speziell in der Spielindustrie nicht durchsetzen. Einige Entwickler erkannten das Problem, was zu der Entwicklung von sogenannten OpenGL-Bindings führte: gl4java [5] (OpenGL for Java Technology), LWJGL [6] (Lightweight Java Game Library) und Magician [7]. Diese drei Bindings wiesen jedoch einige Probleme auf, die ihre Durchsetzungskraft hemmten. So unterstützt gl4java OpenGL nur bis Version 1.3, die Benutzung neuer 3 I/O-Geräte wird nur beschränkt gewährleistet und die umfangreiche API macht es Programmierern schwer, den Einstieg zu finden. Während LWJGL zwar neueste I/O-Geräte unterstützt und auch für die aktuellste OpenGL Version geeignet ist, liegt der Hauptnachteil in der nicht zulässigen Einbindung von AWT oder Swing Komponenten. Das dritte Binding Magician löst zwar das Problem der AWT und Swing Einbindung, jedoch wird es weder weiterentwickelt, noch unterstützt es neue I/O-Geräte. Ein weiterer Nachteil liegt darin, dass Magician nie OpenSource war. Die erste wirklich vielversprechende Entwicklung stellte das im August 2003 veröffentlichte JOGL dar. 2.2 Entstehung Begonnen wurde die Programmbibliothek ursprünglich von Kenneth Russell (Sun Microsystems) und Chris Kline (Irrational Games) unter dem Namen Jungle. Dabei wurde versucht, die Vorteile der vorhergehenden Bindings zu kombinieren. Mittlerweile wird sie aber von der Game Technology Group, welche zu Sun Microsystems gehört, als Open Source ständig weiterentwickelt und verbessert. Es ist davon auszugehen, dass JOGL in eine der späteren Java-Versionen standardmäßig integriert wird. Die ersten Versionen von JOGL arbeiteten im Package net.java.games.jogl. Dieses hat sich aber nach dem Übergang zu den JSR-231 APIs zu javax.media.jogl geändert. Leider greifen jedoch noch sehr viele Tutorials und Informationsseiten im Internet auf die alte JOGL-Version zurück, die auch in der Klassen- und Methodenstrucktur einige Änderungen aufweist. Beispielsweise wurden einige Klassen (Animator ) aus der neueren Version herausgenommen, oder sogar in ihrer Erzeugung verändert: Die Klasse GLU ist nun nicht mehr über eine Factory zu erzeugen, sondern direkt über new() instanziierbar. 2.3 Überblick In den nachfolgenden Kapiteln wird zunächst auf vier Klassen von JOGL eingegangen. Um einen kleinen Einblick in deren Funktionalitäten und Anwendungsweise zu gewinnen, erzeugen wir anhand eines Beispiels eine einfache JOGL-Ausgabe. Wir werden verstehen, wie man auch komplexere 3D-Strukturen darstellt und diese über eine Kamera aus einer räumlichen Perspektive betrachten kann. Außerdem sind die Verwendung von Lichtquellen und ein kleines Animationsbeispiel Thema dieses Artikels. 4 3 Verwendung 3.1 Installation Da JOGL noch nicht standardmäßig in Java integriert ist, muss man zunächst die Programmbibliothek auf dem Rechner installieren, bevor man sie verwenden kann. 1. Herunterladen der OS-spezifischen Binaries von https://jogl.dev.java.net/ 2. Einbinden der Archive jogl.jar und gluegen-rt.jar in den Buildpath des Projekts 3. Speichern der nativen Dateien in einem der in java.library.path angegebenen Pfade JOGL unterstützt die gängigen Plattformen, auf denen J2SE 1.4 oder höher läuft. 3.2 Überblick OpenGL (Open Graphics Library) ist eine Programmierschnittstelle zur Entwicklung von 3D-Computergrafik unabhängig von Plattform oder Programmiersprache. Sie beinhaltet etwa 250 Befehle, die die Darstellung komplexer 3D-Szenen in Echtzeit erlauben. Zudem können eigene Erweiterungen (beispielsweise von Grafikkartenherstellern) definiert werden. JOGL ermöglicht die Verwendung dieser Befehle in Java. Hierzu wurden sämtliche Methoden direkt übernommen, die lediglich die korrespondierenden C-Implementierungen aufrufen. Aus diesem Grund gibt es auch mehrere ähnlichnamige Funktionen, die sich lediglich im Parameter-Typ unterscheiden (vgl. glVertex3f() und glVertex3d()). Einige Begriffe werden in den folgenden Kapiteln immer wieder fallen und tragen auch zum Verständnis von JOGL bei: Viewport beschreibt den sichtbaren Ausschnitt einer Umgebung. Primitive sind einfache Objekte, die sich oft zu komplexen 3D-Strukturen kombinieren lassen. Matrizen dienen in JOGL als Berechnungsgrundlage. Es wird zwischen mehreren Matrizen unterschieden: Auf der Modelview-Matrix werden alle 3D-Objekte und Szenen abelegt, die Projektions-Matrix bestimmt die räumliche Position des Viewports (Standort des Betrachters), und wird deshalb auch Kamera genannt. Kamera ist eine Umschreibung für eine modifizierte Projektions-Matrix. Licht spielt in 3D-Anwendungen eine große Rolle. Lichtquellen können selbst definiert werden, wodurch der Umgebung ein dreidimensionales Aussehen verliehen werden kann. Standardmäßig sind alle Objekte mit einem gleichmäßigen diffusen Licht beleuchtet. Material bestimmt die Oberflächenbeschaffenheit von Objekten. Dies kann einfach eine Farbe oder auch eine Grafik sein. Zudem können Einstellungen bezüglich der Reflexion von Licht vorgenommen werden. 5 3.3 Klassenaufbau Die Funktionen der verschiedenen OpenGL-Bibliotheken werden in JOGL durch Objekte aufgerufen. Dies geschieht hauptsächlich mit dem Interface GL, das die Erstellung komplexer 3D-Szenen ermöglicht, und der Klasse GLU, die unter Anderem die Möglichkeiten der Kameraeinstellungen bietet. Die Referenz auf das GL-Objekt erhält man über das verwendete GLDrawable, auf dem das Rendering erfolgt (z.B. GLCanvas), wohingegen das GLU -Objekt mit new() gebildet werden muss. Dem Konstruktor von GLCanvas kann optional ein GLCapabilities-Objekt übergeben werden, über das die Parameter für das Rendering festgelegt werden, z.B. das Ein- oder Ausschalten der Hardwarebeschleunigung oder des Double-Bufferings. Bildlich kann man sich die Funktion der Basisklassen wie folgt vorstellen: Über das GLInterface wird die Szene aufgebaut, GLU bestimmt die Sichtweise. Die Verwaltung über diese beiden Klassen übernimmt ein GLEventListener. Wahrgenommen“ werden kann das ” fertige Bild über ein GLDrawable-Objekt (GLCanvas). Diese vier Klassen samt ihrer interessantesten Methoden seien auf den folgenden Seiten kurz erwähnt. Interessierte Leser können weitere Informationen in der Dokumentation [4] nachschlagen. 6 3.3.1 GLCanvas Die Klasse GLCanvas implementiert das Interface GLDrawable und leitet von Canvas ab. Sie ist eine AWT-Komponente, die Hardware-Beschleunigung unterstützt. Auf ihr kann die 3D-Welt angezeigt werden. Über die Methode addGLEventListener() weist man dem GLCanvas eine Klasse zu, die Ereignisse wie Größenänderungen behandelt. Über getGL() bekommt man das GL-Objekt, auf dem man arbeiten kann (vgl. getGraphics() bei Applets). Alternativ kann das Swing-kompatible GLJPanel verwendet werden, falls GLCanvas nicht benutzt werden kann. Allerdings gewährleistet diese Klasse keine Hardwareunterstützung. 3.3.2 GL Das Interface GL stellt die Schnitstelle dar, mit der die 3D-Welt modelliert wird. Über glVertex3f() werden Eckpunkte erstellt, die – wenn mehrere von ihnen innerhalb eines glBegin()/glEnd()-Paares stehen – zu einem Objekt gruppiert werden. glBegin() wird dabei ein einzelnes Argument übergeben, das spezifiziert, wie die Eckpunkte interpretiert werden: Beispielsweise zeichnet GL TRIANGLE STRIP ein ausgefülltes Dreieck. Mit GL QUAD STRIP lässt sich ein ausgefülltes Viereck erzeugen. Mittels glColor3f() kann die aktuelle Zeichenfarbe in RGB (rot-grün-blau) gesetzt werden. glMatrixMode() aktiviert die Matrix, auf der gearbeitet werden soll. Hierbei wird in unserem Beispiel unterschieden in: GL MODELVIEW: Auf dieser Matrix wird die 3D-Welt aufgebaut. GL PROJECTION: Diese Matrix wird für Projektionseigenschaften wie Kameraperspektive verwendet. Darüber hinaus gibt es noch GL TEXTURE und GL COLOR. Über glRotatef() können wir die aktuelle Matrix manipulieren und auf alle nachfolgenden Eingaben eine Rotation bewirken. glTranslatef() hingegen versetzt den Ursprung und hat eine Verschiebung aller nachfolgenden Eingaben zufolge. Mit glLoadIdentity() können alle Modifikationen auf der aktiven Matrix zurückgesetzt und die ursprüngliche Matrix wieder hergestellt werden. glClear() leert den Bitpuffer. 7 Mit glEnable() lassen sich GL-Eigenschaften aktivieren. Ein Beispiel hierfür sehen wir in einem späteren Kapitel. 3.3.3 GLU Die Klasse GLU (GL Utilities) bietet in erster Linie die Verwaltung der Kameraeinstellung und stellt darüber hinaus erweiterte Zeichenfunktionen zur Klasse GL zur Verfügung. In unserem Beispiel gehen wir lediglich auf die beiden Funktionen gluPerspective() und gluLookAt() ein. Hierüber können die Perspektiveigenschaften und der Betrachterstandort festgelegt werden. 3.3.4 GLEventListener Der GLEventListener ist ein Interface, das vom Anwender implementiert werden muss. Es setzt folgende Methoden zur Behandlung der Ereignisse des GLCanvas voraus: Die init()-Methode wird zu Beginn aufgerufen. An dieser Stelle können allgemeine Parameter zum Rendern der Szene eingestellt werden. Die Methoden displayChanged() und reshape() behandeln Änderungen der Bildschirmauflösung oder der Fenstergröße sowie das Verschieben des Fensters. Die Methode display() wird immer dann aufgerufen, wenn tatsächlich gerendert werden soll. In ihr werden u.a. die zu zeichnenden Elemente der Szene beschrieben. Eine Animation erreicht man, indem diese Methode in regelmäßigen Abständen ausgeführt wird, und die Szene jeweils in Abhängigkeit von internen Variablen gerendert wird. Dies kann ein externer Thread übernehmen, der über den Aufruf repaint() des Canvas-Objekts implizit die Ausführung der Methode display() bewirkt. Eine dynamische Änderung dieser Variablen hat dann eine Bewegung in der Darstellung zufolge. 8 3.4 Vorgehensweise anhand eines Beispiels Wir möchten nun eine einfache Ausgabe mittels JOGL erstellen. Hierzu implementieren wir zwei Klassen. Der gesamte Code des hier aufgebauten Beispiels ist unter www. ludwig-naegele.de/tum/jogl/jogl.zip downloadbar. Wir erstellen zunächst ein Frame (MyFrame), auf dem wir eine JOGL-Ausgabe (MyGLEventListener ) darstellen möchten. public c l a s s MyFrame extends Frame { public s t a t i c void main ( S t r i n g [ ] a r g s ) { Frame frame = new Frame ( ”Mein e r s t e s JOGL−P r o j e k t ” ) ; f i n a l GLCanvas c a n v a s = new GLCanvas ( ) ; MyGlEventListener l i s t e n e r = new MyGLEventListener ( ) ; c a n v a s . addGLEventListener ( l i s t e n e r ) ; frame . add ( c a n v a s ) ; frame . s e t S i z e ( 4 0 0 , 3 0 0 ) ; frame . addWindowListener (new WindowAdapter ( ) { public void windowClosing ( WindowEvent e ) { System . e x i t ( 0 ) ; } }); frame . s e t V i s i b l e ( true ) ; } } Mit dem Aufruf new GLCanvas() erzeugen wir eine neue Zeichenfläche, auf der die Ausgabe gezeichnet werden soll. In Java werden Eingaben des Benutzers, z.B. über I/O-Geräte, durch das sogenannte Event-Listener-Modell abgefangen und verarbeitet. Tritt auf der Zeichenfläche ein Ereignis ein, so soll dieses von einem EventListener verarbeitet werden. Die Zuordnung legen wir über den Aufruf von addGLEventListener() fest. Wird nun das Canvas geladen oder in der Größe verändert, reagiert MyGLEventListener darauf und baut die Zeichenfläche neu auf. Letztendlich wollen wir die Zeichenfläche auch sehen können, und fügen sie noch dem Frame hinzu. Beginnen wir mit der Implementierung von MyGLEventListener. public c l a s s MyGLEventListener implements GLEventListener { public void d i s p l a y ( GLAutoDrawable d r a w a b l e ) { f i n a l GL g l = d r a w a b l e . getGL ( ) ; g l . g l B e g i n (GL. GL TRIANGLES ) ; gl . glColor3f (1.0 f , 0.0 f , 0.0 f ) ; gl . glVertex3f (0.0 f , 0.5 f , 0.0 f ) ; g l . g l V e r t e x 3 f ( −0.5 f , −0.5 f , 0 . 0 f ) ; g l . g l V e r t e x 3 f ( 0 . 5 f , −0.5 f , 0 . 0 f ) ; g l . glEnd ( ) ; } ... } 9 Diese Klasse soll auf Ereignisse der Zeichenfläche reagieren können und implementiert daher das Interface GLEventListener. Für unser Beispiel muss zunächst lediglich die Funktion display() angepasst werden, die anderen Methoden werden leer implementiert. Mittels getGL() erhalten wir das GL-Objekt der übergebenen GLAutoDrawable-Instanz. Auf diesem GL-Objekt beginnen wir nun eine neue Gruppe. Der Parameter GL TRIANGLES bedeutet hierbei, dass gezeichnete Punkte zu einem Dreieck zusammengeführt werden. Ebenso wäre beispielsweise GL QUADS für ein Viereck möglich. Mit glColor3f() setzen wir die Zeichenfarbe in RGB. Nun erstellen wir drei Punkte glVertex3f(), die innehrhalb der Gruppierung als ausgefülltes Dreieck abgebildet werden. Als Ergebnis erhalten wir ein Fenster mit einem roten Dreieck. Verändern wir das Fenster in seiner Größe, so fällt uns auf, dass das Dreieck verzerrt dargestellt wird. Das liegt daran, dass JOGL keine Information vorliegt, in welcher Proportion das gerenderte Bild später einmal angezeigt werden soll. Zur Behebung dieses Problems eignet sich die Methode reshape(), in der JOGL über die neue Zeichenflächengröße benachrichtigt wird. Passen wir also die Methode reshape() der Klasse MyGLEventListener an: public void r e s h a p e ( GLAutoDrawable drawable , i n t x , i n t y , i n t width , i n t h e i g h t ) { f i n a l GL g l = d r a w a b l e . getGL ( ) ; int s i z e = height ; i f ( width<h e i g h t ) s i z e = width ; g l . g l V i e w p o r t ( ( width−s i z e ) / 2 , ( h e i g h t −s i z e ) / 2 , s i z e , s i z e ) ; } Mit dem Aufruf von glViewport() kann bestimmt werden, wie das gerenderte Bild in das Canvas gezeichnet wird. In unserem Beispiel stellen wir eine quadratische Ausgabe (grüner Rahmen) mit Seitenlänge size in einem rechteckigen Fenster dar. 10 3.5 Objektverwaltung Wird ein Ereignis ausgelöst - zum Beispiel die Maus bewegt - so soll die 3D-Welt darauf reagieren. Beispielsweise soll ein Würfel in Richtung der Mausbewebung rotieren oder eine Spielfigur einer gedrückten Pfeiltaste nachlaufen. Die klassische Vorstellung eines JavaEntwicklers wäre: Man besitzt eine Referenz auf ein Java-Objekt, das jeweils ein 3D-Objekt repräsentiert. Über spezifische Methoden kann man so die Ausgabe modifizieren. Doch JOGL bietet diese Objektverwaltung nicht. Da die gesamte Ausgabe ausschließlich über parametrisierte Methodenaufrufe der Klasse GL zusammengestellt wird, erhält man keine Referenzen, auf die man später zurückgreifen könnte. Tatsächlich muss für jedes einzelne Bild die gesamte Szenerie neu aufgebaut und durchgerechnet werden, bevor sie gerendert werden kann. Um dennoch das Programm nicht nur möglichst objektorientiert, sondern auch übersichtlich zu gestalten, lagert man üblicherweise die Generierung sämtlicher 3D-Objekte in sogenannte Erzeugermethoden aus. Als Parameter können OffsetWerte übergeben werden, die je nach Anforderung einen Verschiebungs-, Rotations- oder auch Verzerrungsgrad bestimmen. Um nun unser Beispiel konform dieser Richtlinie zu halten, würden wir unsere Klasse MyGLEventListener anpassen und das Erzeugen des Dreiecks in eine neue Methode drawTriangle() auslagern. Damit die nachfolgenden Kapitel leichter nachzuvollziehen sind, werden wir an dieser Stelle das Dreieck durch einen Würfel ersetzen, der in der Methode drawCube() generiert wird. 11 private void drawCube (GL g l ) { g l . g l B e g i n (GL. GL QUAD STRIP ) ; g l . g l N o r m a l 3 f ( −1.0 f , 0 . 0 f , 0 . 0 f ) ; gl . glColor3f (1.0 f , 0.0 f , 1.0 f ) ; g l . g l V e r t e x 3 f ( −0.5 f , −0.5 f , −0.5 f ) ; g l . g l V e r t e x 3 f ( −0.5 f , 0 . 5 f , −0.5 f ) ; g l . g l V e r t e x 3 f ( −0.5 f , −0.5 f , 0 . 5 f ) ; g l . g l V e r t e x 3 f ( −0.5 f , 0 . 5 f , 0 . 5 f ) ; g l . glEnd ( ) ; g l . g l B e g i n (GL. GL QUAD STRIP ) ; g l . g l N o r m a l 3 f ( 0 . 0 f , −1.0 f , 0 . 0 f ) ; gl . glColor3f (0.0 f , 1.0 f , 1.0 f ) ; g l . g l V e r t e x 3 f ( −0.5 f , −0.5 f , −0.5 f ) ; g l . g l V e r t e x 3 f ( 0 . 5 f , −0.5 f , −0.5 f ) ; g l . g l V e r t e x 3 f ( −0.5 f , −0.5 f , 0 . 5 f ) ; g l . g l V e r t e x 3 f ( 0 . 5 f , −0.5 f , 0 . 5 f ) ; g l . glEnd ( ) ; g l . g l B e g i n (GL. GL QUAD STRIP ) ; g l . glNormal3f ( 1 . 0 f , 0.0 f , 0.0 f ) ; gl . glColor3f (0.0 f , 1.0 f , 0.0 f ) ; g l . g l V e r t e x 3 f ( 0 . 5 f , −0.5 f , −0.5 f ) ; g l . g l V e r t e x 3 f ( 0 . 5 f , 0 . 5 f , −0.5 f ) ; g l . g l V e r t e x 3 f ( 0 . 5 f , −0.5 f , 0 . 5 f ) ; gl . glVertex3f (0.5 f , 0.5 f , 0.5 f ) ; g l . glEnd ( ) ; g l . g l B e g i n (GL. GL QUAD STRIP ) ; g l . glNormal3f ( 0 . 0 f , 0.0 f , 1.0 f ) ; gl . glColor3f (1.0 f , 1.0 f , 0.0 f ) ; g l . g l V e r t e x 3 f ( −0.5 f , −0.5 f , 0 . 5 f ) ; g l . g l V e r t e x 3 f ( 0 . 5 f , −0.5 f , 0 . 5 f ) ; g l . g l V e r t e x 3 f ( −0.5 f , 0 . 5 f , 0 . 5 f ) ; gl . glVertex3f (0.5 f , 0.5 f , 0.5 f ) ; g l . glEnd ( ) ; g l . g l B e g i n (GL. GL QUAD STRIP ) ; g l . glNormal3f ( 0 . 0 f , 1.0 f , 0.0 f ) ; gl . glColor3f (1.0 f , 0.0 f , 0.0 f ) ; g l . g l V e r t e x 3 f ( −0.5 f , 0 . 5 f , −0.5 f ) ; g l . g l V e r t e x 3 f ( 0 . 5 f , 0 . 5 f , −0.5 f ) ; g l . g l V e r t e x 3 f ( −0.5 f , 0 . 5 f , 0 . 5 f ) ; gl . glVertex3f (0.5 f , 0.5 f , 0.5 f ) ; g l . glEnd ( ) ; g l . g l B e g i n (GL. GL QUAD STRIP ) ; g l . g l C o l o r 3 f ( 0 . 0 f , 0 . 0 f , −1.0 f ) ; g l . g l V e r t e x 3 f ( −0.5 f , −0.5 f , −0.5 f ) ; g l . g l V e r t e x 3 f ( 0 . 5 f , −0.5 f , −0.5 f ) ; g l . g l V e r t e x 3 f ( −0.5 f , 0 . 5 f , −0.5 f ) ; g l . g l V e r t e x 3 f ( 0 . 5 f , 0 . 5 f , −0.5 f ) ; g l . glEnd ( ) ; } In der display()-Methode rufen wir lediglich noch die Erzeugerfunktion auf: public void d i s p l a y ( GLAutoDrawable d r a w a b l e ) { f i n a l GL g l = d r a w a b l e . getGL ( ) ; drawCube ( g l ) ; } 12 3.6 Perspektive / Kamera Führen wir das Programm nun aus, so sehen wir lediglich ein Viereck. Das liegt daran, dass der Betrachter von vorne senkrecht auf eine Seite des Würfels blickt. Interessanter wird es, wenn man die Perspektive so ändert, dass der Würfel im Raum zu liegen scheint. In JOGL lässt sich dies als sogenannte Kameraeinstellung beliebig anpassen. Erstellen wir also nun eine solche Kameradefinition. public void i n i t ( GLAutoDrawable d r a w a b l e ) { GL g l = d r a w a b l e . getGL ( ) ; GLU g l u = new GLU ( ) ; g l . glMatrixMode (GL. GL PROJECTION ) ; gl . glLoadIdentity ( ) ; glu . gluPerspective (45.0 f , 1.0 f , 0.1 f , 100.0 f ) ; g l u . gluLookAt ( 1 . 0 f , 1 . 5 f , 2 . 0 f , 0.0 f , 0.0 f , 0.0 f , 0.0 f , 1.0 f , 0.0 f ) ; g l . glMatrixMode (GL.GL MODELVIEW) ; gl . glLoadIdentity ( ) ; } Die verschiedenen Matrizen, auf die OpenGL aufbaut, können über glMatrixMode() aktiviert werden. Für die Kameraeinstellungen wird der GL PROJECTION Matrix Stack verwendet. Die GL MODELVIEW Matrix hingegen findet bei der Erstellung der 3D-Welt Anwendung. Die Methode gluPerspective() definiert eine perspektivische Projektion, um sicherzustellen, dass Objekte nach ihrer Position im Raum perspektivisch verzerrt werden. Der Methode wird zuerst der Öffnungswinkel der Kamera übergeben, dann das Verhältnis zwischen Höhe und Breite des Viewing-Volumens, sowie die Punkte, ab wann im Raum Objekte angezeigt werden beziehungsweise ab welcher Entfernung Objekte nicht mehr zu sehen sein sollen. Als Ergebnis erhalten wir eine räumliche Abbildung des farbigen Würfels. Doch scheint hier etwas nicht zu stimmen: JOGL zeigt alle Objekte in der Einfügereihenfolge an. Dies hat zur Folge, dass das zuletzt eingefügte Quadrat ganz oben erscheint, obwohl es eigentlich von den weiter vorne liegenden Seiten des Würfels verdeckt werden müsste. Dieser Fehler lässt sich durch das Einschalten der Tiefenunterstützung beheben, d.h. durch Hinzufügen folgender Zeile in der init()-Methode: g l . g l E n a b l e (GL. GL DEPTH TEST ) ; 13 In der display()-Methode fügen wir hinzu: g l . g l C l e a r (GL. GL COLOR BUFFER BIT | GL. GL DEPTH BUFFER BIT ) ; Der Würfel wird nun richtig angezeigt. Im nächsten Schritt wollen wir den Würfel animieren. 3.7 Animation Unser 3D-Würfel wirkt noch etwas statisch. Wir möchten ihn nun um eine Achse rotieren lassen. Die Idee dabei ist, nach einer bestimmten Zeitspanne jeweils die Methode repaint() des GLCanvas aufzurufen. Dadurch wird die Methode display() aktiviert, die nach jedem Aufruf den Würfel anders darstellt. Implementieren wir also einen Animator-Thread! Am Ende der main()-Methode in der Klasse MyFrame erstellen wir einen neuen Thread, der kontinuierlich die Zeichenfläche auffrischt, und starten ihn anschließend. new Thread ( ) { public void run ( ) { while ( true ) { canvas . r e p a i n t ( ) ; try { Thread . s l e e p ( 2 0 ) ; } catch ( I n t e r r u p t e d E x c e p t i o n e ) {} } } }. start ( ) ; In der Klasse MyGLEventListener definieren wir zwei neue Variablen: private f l o a t r o t a t i o n = 0 . 0 f ; private f l o a t d e l t a = 1 . 0 f ; rotation bestimmt den derzeitigen Rotationswinkel und delta gibt an, um wieviel der Würfel gedreht wird. In der Methode display() fügen wir vor dem Aufruf von drawCube() folgenden Codeschnipsel ein: gl . glLoadIdentity ( ) ; gl . glRotatef ( rotation , 1.0 f , 1.0 f , 0.0 f ) ; r o t a t i o n += d e l t a ; Durch das Manipulieren der Modell-Matrix bewirken wir eine Rotation um den Winkel rotation in x- und y-Richtung. Anschließend wird der Rotationswinkel erhöht. 14 Der Würfel dreht sich! Noch interessanter können wir die Szene darstellen, wenn wir mit eigenen Lichtquellen arbeiten, die den Würfel aus einem bestimmten Winkel beleuchten. Wie man das umsetzen kann, wird im nächsten Kapitel beschrieben. 3.8 Beleuchtung JOGL unterscheidet drei Arten von Licht: • Der ambiente (ungerichtete) Anteil der Lichtquelle ist diffuses Licht, das aus allen Richtungen zu kommen scheint und aufgrund dessen sich die Lichtquelle nicht mehr lokalisieren lässt. Wird ein 3D-Körper ausschließlich mit ambientem Licht beleuchtet, dann erscheint dieser aufgrund der Lichtstreuung zweidimensional. • Der diffuse Anteil kommt aus der Richtung der Lichtquelle und wird von einer angestrahlten Oberfläche in alle Richtungen gleichmäßig reflektiert. Objekte erscheinen unabhängig von der Position des Betrachters gleichmäßig hell. • Der spiegelnde Lichtanteil kommt ebenfalls aus nur einer Richtung, wird aber an der getroffenen Oberfläche in nur eine bestimmte Richtung reflektiert. Bei geeigneter Wahl des Betrachterstandortes erscheint so ein Glanzlicht. Für die Berechnung eben solcher Reflexionen benötigt JOGL Informationen über den Reflexionswinkel. Dieser wird über Normalenvektoren angegeben, die die relative Position der Objekte zur Lichtquelle bestimmen. Diese Angabe geschieht über den Methodenaufruf glNormal3f(), der im Codebeispiel der Erzeugerfunktion drawCube() bereits implementiert wurde. Es gibt drei Arten von Lichtquellen: Richtungslicht kommt von einer unendlich weit entfernten Lichtquelle und strahlt in eine bestimmte Richtung (ideal als Sonne). Punktlicht hingegen beschreibt eine Lichtquelle innerhalb der Szene, die in alle Richtungen gleichmässig strahlt. Spotlicht ist ein Punktlicht, bei dem der Abstrahlwinkel eingeschränkt ist, und somit einen Lichtkegel darstellt. Um nun ein Richtungslicht in JOGL einzufügen, setzen wir in der init()-Methode die Lichtanteile für Licht0 und aktivieren dieses anschließend. Ebenso muss über den Aufruf von glEnable(GL.GL LIGHTING) die Verwendung von Licht aktiviert werden. float float float float [] [] [] [] LightPos LightAmb LightDif LightSpc = = = = {0.0 f {1.0 f {1.0 f {1.0 f , , , , 5.0 f 1.0 f 1.0 f 1.0 f , , , , 10.0 f 1.0 f , 1.0 f , 1.0 f , , 0.0 f }; 1.0 f }; 1.0 f }; 1.0 f }; 15 gl gl gl gl . . . . g l L i g h t f v (GL. GL g l L i g h t f v (GL. GL g l L i g h t f v (GL. GL g l L i g h t f v (GL. GL LIGHT0 , LIGHT0 , LIGHT0 , LIGHT0 , GL. GL POSITION , LightPos GL. GL AMBIENT, LightAmb , GL. GL DIFFUSE , L i g h t D i f , GL. GL SPECULAR, LightSpc , 0); 0); 0); , 0); g l . g l E n a b l e (GL. GL LIGHT0 ) ; g l . g l E n a b l e (GL. GL LIGHTING ) ; Führen wir unseren Quellcode aus, sehen wir das Ergebnis dieses Kapitels: Die Flächen des rotierenden Würfels wechseln abhängig vom Neigungswinkel ihre Helligkeit. Doch man bemerkt auch, dass der Würfel nicht mehr farbig dargestellt wird. Im Licht-Modus ignoriert JOGL alle Angabe von glColor3f(), stattdessen muss man hier Materialien verwenden. 3.9 Materialien Materialien setzen sich ebenso wie Licht aus einem ambienten, einem diffusen und einem spiegelnden Anteil zusammen. Ausserdem kann über einen Spiegelungsexponenten festgelegt werden, wie glänzend das Material dargestellt werden soll. Je größer der Spiegelungsexponent, desto schärfer und kleiner wird das Highlight auf der Oberfläche dargestellt. Daneben kann die Emissionsfarbe der Oberfläche festgelegt werden, d.h. in welcher Farbe die Oberfläche Licht abstrahlt. Definieren wir nun für unseren Würfel Materialeigenschaften, die wir über die Methode glMaterialfv() festlegen. Dabei werden die relevanten Seite des Objekts, der Lichttyp, sowie die Emissionsfarbe als RGBA-Wert übergeben. RGBA ist hierbei ein RGB-Wert, dem noch ein Alpha-Wert (Transparenz) angehängt wird. float float float float gl gl gl gl . . . . mat mat mat mat ambient [ ] = { 0 . 2 4 f , 0 . 1 9 f , 0 . 0 7 f , 1 . 0 f } ; diffuse [ ] = {0.75 f , 0.60 f , 0.22 f , 1.0 f }; specular [ ] = {0.62 f , 0.55 f , 0.36 f , 1.0 f }; shininess = 51.2 f ; g l M a t e r i a l f v (GL. GL FRONT, GL. GL AMBIENT, mat ambient , 0 ) ; g l M a t e r i a l f v (GL. GL FRONT, GL. GL DIFFUSE , m a t d i f f u s e , 0 ) ; g l M a t e r i a l f v (GL. GL FRONT, GL. GL SPECULAR, m a t s p e c u l a r , 0 ) ; g l M a t e r i a l f (GL. GL FRONT, GL. GL SHININESS , m a t s h i n i n e s s ) ; Erweitern wir die Funktion drawCube() zu Beginn mit obigem Codeschnipsel, werden alle nachfolgend erstellten Quadrate mit dieser Materialeigenschaft ausgestattet. Der Würfel erstrahlt in einem spiegelnden Gold. 16 3.10 Eingabeverarbeitung Wichtig für 3D-Spiele ist, dass sie auf Maus- oder Tastatureingaben des Anwenders reagieren können. Wir wollen nun erreichen, dass unser Würfel auf Tastaturereignisse reagiert. Dazu werden wir den MyGLEventListener um das Interface KeyListener erweitern, der die zu implementierenden Methoden vorgibt. In der Klasse MyFrame ordnen wir dem Canvas bereits einen GLEventListener zu, der auf Fensterereignisse reagiert. An dieser Stelle definieren wir analog den KeyListener : canvas . addKeyListener ( l i s t e n e r ) ; In der letzten Zeile der main()-Methode legen wir den Fokus auf die Zeichenfläche, so dass nach dem Laden des Fensters unsere Tastatureingaben sofort verarbeitet werden können: canvas . requestFocus ( ) ; Die Klassendefinition des MyGLEventListener ändern wir wie folgt ab: public c l a s s MyGLEventListener implements GLEventListener , K e y L i s t e n e r { Jetzt müssen wir noch die durch das Interface KeyListener vorgegebenen Methoden implementieren. Hierzu erweitern wir die Klasse MyGLEventListener um folgende Methode: @Override public void k e y P r e s s e d ( KeyEvent a r g 0 ) { i f ( a r g 0 . getKeyCode ()==38) s p e e d += 0 . 1 f ; e l s e i f ( a r g 0 . getKeyCode ()==40) { s p e e d −= 0 . 1 f ; i f ( speed <0) s p e e d = 0 ; } } Beim Drücken der nach-oben/nach-unten Taste erhöht bzw. verringert sich die Rotationsgeschwindigkeit des Würfels. Andere Listener können analog eingebunden werden. 3.11 Einlesen von externen 3D-Objekten Leider ist es nicht ohne Weiteres möglich, 3D-Objekte aus Dateien wie .3ds mittels JOGL einzulesen. Als Workaround kann man aber eine eigene Loader-Klasse schreiben oder einen existierenden Loader verwenden: Da die Java3D-Bibliothek OpenGL unterstützt, kann auch auf deren Loader zurückgegriffen werden. 3.12 JOGLAppletLauncher Natürlich will man JOGL auch innerhalb eines Applets verwenden können, das dann auf einer Website abgelegt ist. Da JOGL jedoch auf nativen Code zurückgreift, ist das nicht ganz so einfach. Die vom Betriebssystem abhängigen Klassen müssen auf dem Rechner installiert werden [8]. 17 Allerdings dürfen die lokalen C-Klassen vom Applet aus Sicherheitsgründen nicht ausgeführt werden. Es ist aber möglich, dem Applet Vollzugriff auf den Rechner zu gestatten, indem man es zertifiziert. Die Zertifizierung kann über einen Anbieter wie Verisign geschehen. Man kann auch ein eigenes Zertifikat erstellen [9]. Der User muss dann allerdings entscheiden, ob er diesem vertrauen will. Die Basisklassen der JOGL-Bibliothek sind standardmäßig bereits zertifiziert. 4 Demoprojekt Als Demoprojekt wurde der Grundaufbau eines 3D-Spiels entwickelt. In Anlehnung an die Bedienung des bekannten Egoshooters CounterStrike“ lässt sich die Spielfigur mit der ” Tastatur bewegen und mit der Maus drehen. Der Spieler kann zwischen verschieden Perspektiven wählen: Ich-Perspektive: Das Geschehen wird aus Sicht der Spielfigur wahrgenommen. Erweiterte Ich-Perspektive: Der Betrachter läuft unmittelbar hinter der Spielfigur. Vogelperspektive: Der Betrachter blickt von oben auf das Spielfeld. Dieses Beispiel zeigt neben der Verarbeitung von Tastatur- und Mausereignissen auch die Verwendung von Kamera, Licht und Texturen. 5 Fazit JOGL ist ein mächtiges Werkzeug, wenn es um die Erstellung von 3D-Applikationen geht. Die Library stellt über die in diesem Artikel erwähnten Funktionen hinaus noch zahlreiche weitere Möglichkeiten zur Verfügung. So kann man Bilder als Texturen an 3D-Objekte binden oder Partikel in Form von Nebel oder Explosionen einsetzen. Der klare Vorteil von JOGL: Durch das direkte Binden der OpenGL-Funktionen kann ein Entwickler mit OpenGL-Kenntnissen in C/C++ analog auch JOGL verwenden. Zudem können 3D-Anwendungen leicht zwischen den beiden Programmiersprachen portiert werden. In Anbetracht der Plattformunabhängigkeit und der leichten Erlernbarkeit von Java ist JOGL für Entwickler der Spieleindustrie wie auch aus anderen Bereichen durchaus eine interessante Alternative, 3D-Anwendungen zu entwickeln. Als Beispiele für Onlinespiele unter Verwengung von JOGL seien PoxNora [10] und Wurm Online [11] erwähnt. 18 6 Literatur [ 1 ] G e t t i n g s t a r t e d with JOGL, Mai 2009 h t t p : / / e i n s t e i n . i n f o r m a t i k . uni−o l d e n b u r g . de / f o r s c h u n g / o p e n g l / j o g l s n a p . html [ 2 ] A l l e s rund um d i e Java B i n d i n g s f ü r OpenGL , Mai 2009 M e l a n i e K l e i n , S t e f a n Jouaux h t t p : / /www. j o g l . i n f o / [ 3 ] J o g l − User ’ s Guide , Mai 2009 h t t p s : / / j o g l . dev . j a v a . n e t / s o u r c e / browse /∗ c h e c k o u t ∗/ j o g l / t r u n k / doc / u s e r g u i d e / i n d e x . html [ 4 ] JOGL API − JSR−231 1 . 1 . 2 S p e c i f i c a t i o n , Mai 2009 Sun Microsystems , I n c . h t t p : / / download . j a v a . n e t / media / j o g l / b u i l d s / n i g h t l y / j a v a d o c p u b l i c / [ 5 ] OpenGL f o r Java ( g l 4 j a v a ) , Mai 2009 http :// g l 4 j a v a . s o u r c e f o r g e . net / [ 6 ] Home o f t h e L i g h t w e i g h t Java Game L i b r a r y , Mai 2009 http :// l w j g l . org / [ 7 ] 3D g r a p h i c s programming i n Java , Part 3 : OpenGL , Mai 2009 h t t p : / /www. r o s e i n d i a . n e t / s o f t w a r e −t u t o r i a l s / d e t a i l /867 [ 8 ] JOGLAppletLauncher (JOGL API −− JSR−231 1 . 1 . 2 S p e c i f i c a t i o n ) , Mai 2009 Sun Microsystems , I n c . h t t p : / / download . j a v a . n e t / media / j o g l / b u i l d s / n i g h t l y / j a v a d o c p u b l i c /com/ sun / o p e n g l / u t i l / JOGLAppletLaunche [ 9 ] Z e r t i f i z i e r u n g von Java−Applets , Mai 2009 Babak Sa yy id H o s s e i n i h t t p : / / t r a n s p o r t b r o k e r . v c e . de / r e s s o u r c e s / documents / E r s t e l l e n v o n Z e r t i f i k a t e n 1 . p d f [ 1 0 ] PoxNora O n l i n e Game , Mai 2009 h t t p : / /www. poxnora . com/ i n f o / media . do [ 1 1 ] Wurm Online , Mai 2009 h t t p : / / wurmonline . com/ 19