Masterarbeit Entwicklung und Einbindung von 3D
Transcription
Masterarbeit Entwicklung und Einbindung von 3D
Masterarbeit Entwicklung und Einbindung von 3D-Grafikanimationen in ein Car-Multimedia-Framework Tsz Kwong YUNG Matrikel-Nr.: 702251 Datum: 08.10.2005 Fachhochschule Darmstadt Fachbereich Informatik Referent: Prof. Dr. Joachim Wietzke Korreferent: Prof. Dr. Thomas Horsch Inhaltsverzeichnis 1 2 Einleitung .................................................................................................. 7 1.1 Motivation .......................................................................................................... 7 1.2 Aufgabenstellung ............................................................................................... 7 HMI mit 3D-GUI ...................................................................................... 9 2.1 2.1.1 Automotive Framework Integration............................................................... 9 2.1.2 OpenGL........................................................................................................ 14 2.1.2.1 OpenGL Konventionen ............................................................................ 16 2.1.2.2 Programmstruktur..................................................................................... 17 2.1.2.3 Grafik-Primitive: Lines, Polygon… ......................................................... 18 2.1.2.4 Vertex Array............................................................................................. 18 2.1.2.5 Koordinatensystem und Transformation .................................................. 19 2.1.2.6 Modell- und View-Transformation .......................................................... 21 2.1.2.7 Projektion Transformation ....................................................................... 22 2.1.2.8 Texture Mapping ...................................................................................... 24 2.2 Implementierung .............................................................................................. 25 2.2.1 Mesa ............................................................................................................. 26 2.2.2 OpenGL Photon Anbindung......................................................................... 27 2.2.3 3D GUI......................................................................................................... 30 2.3 3 Grundlagen ......................................................................................................... 9 Diskussion ........................................................................................................ 31 Geländedarstellung.................................................................................. 33 3.1 Grundlagen ....................................................................................................... 33 3.1.1 3.2 Implementierung .............................................................................................. 38 3.2.1 Funktionsbeschreibung................................................................................. 39 3.2.2 Probleme bei der Implementierung .............................................................. 42 3.3 4 Der SOAR (Stateless One-pass Adaptive Refinement) Algorithmus .......... 35 Evaluierung und Diskussion............................................................................. 43 OpenGL Java-Binding ............................................................................ 47 4.1 Grundlage ......................................................................................................... 48 4.1.1 4.1.1.1 4.1.2 JOGL ............................................................................................................ 48 Native-Anbindung.................................................................................... 51 JAVA3D....................................................................................................... 55 2 4.1.2.1 4.1.3 SWTOGL ..................................................................................................... 63 4.1.3.1 4.1.4 5 Native-Anbindung.................................................................................... 64 JNI und NIO ................................................................................................. 67 4.2 Implementierung .............................................................................................. 73 4.3 Evaluierung und Diskussion............................................................................. 78 3D-Modelle und Fisheye View ............................................................... 80 5.1 Grundlagen ....................................................................................................... 80 5.1.1 3D-Formate .................................................................................................. 80 5.1.1.1 Das OBJ-Format....................................................................................... 82 5.1.1.2 Das 3DS-Format....................................................................................... 82 5.1.2 6 Native-Anbindung.................................................................................... 61 Fisheye View................................................................................................ 83 5.2 Implementierung .............................................................................................. 87 5.3 Diskussion ........................................................................................................ 88 Zusammenfassung und Ausblick ............................................................ 91 Literatur ............................................................................................................... 93 Anhang A: Technische Daten der Target-Hardware........................................... 95 Selbständigkeitserklärung ................................................................................... 97 3 Abbildungsverzeichnis Abbildung 1 Automotive Framework Architektur. Quelle [Wie05]........................................ 10 Abbildung 2 Aufbau der HMI-Komponente ............................................................................ 11 Abbildung 3 Raw-Widget mit Photon GUI-Builder ................................................................ 12 Abbildung 4 Integration von 3D-GUI mit Grafik-Widget ....................................................... 12 Abbildung 5 (a) Die einzelne Layer. (b) Die Überlagerung der Layer. (c) Das Ergebnis. ...... 13 Abbildung 6 Framework Integration Multi-Layer-Technik..................................................... 14 Abbildung 7 OpenGL Namenskonvention............................................................................... 16 Abbildung 8 Geometrische Objekte......................................................................................... 18 Abbildung 9 Würfel mit acht Eckpunkte ................................................................................. 19 Abbildung 10 Das OpenGL Koordinatensystem ..................................................................... 20 Abbildung 11 Transformationsstufen....................................................................................... 20 Abbildung 12 (a) Translation, (b) Rotation und (c) Skalierung............................................... 21 Abbildung 13 View-Transformation........................................................................................ 22 Abbildung 14 Orthografische Projektion ................................................................................. 23 Abbildung 15 Perspektivische Projektion. (a) glFrustum(), (a) gluPerspective() 24 Abbildung 16 Texture-mapping ............................................................................................... 24 Abbildung 17 (a) GUI in der normalen Stellung. (b) GUI in der aufgeklappten Stellung....... 31 Abbildung 18 Darstellung eines Geländes mit einem quadratischen Dreieck-Gitter .............. 33 Abbildung 19 (a) Darstellung als vollständiges Gitter. (b) Darstellung mit LOD.................. 34 Abbildung 20 Quadtree Hierarchie .......................................................................................... 34 Abbildung 21 Höhenunterschied zwischen Original-Punkt und der approximierten Stelle (weißer Punkt). ......................................................................................................................... 35 Abbildung 22 Longest edge bisection ...................................................................................... 35 Abbildung 23 Rekursive Verfeinerung .................................................................................... 36 Abbildung 24 Schematischer Ablauf des Verfeinerungsalgorithmus ...................................... 37 Abbildung 25 Für die Geländedarstellung: (a) Textur. (b) Heightmap.................................... 38 Abbildung 26 Ablaufdiagramm des Gelände-Darstellungsprogramms ................................... 39 Abbildung 27 Blick auf das Gelände mit einem Symbol für die Navigation und ................... 42 Abbildung 28 Screenshot aus unterschiedlichen Winkeln....................................................... 42 Abbildung 29 Fehlerhaft dargestelltes Gelände ....................................................................... 43 Abbildung 30 Vektorisierte Strasse auf einem Gelände-Gitter. Die neu entstandenen Punkte sind in blau gekennzeichnet. .................................................................................................... 45 Abbildung 31 Klassendiagramm der wichtigsten JOGL Klasse.............................................. 50 4 Abbildung 32 Sequenz-Diagramm beim Aufruf der Methode GLCanvas.reshape()............... 53 Abbildung 33 Klassendiagramm GLContext Hierarchie ......................................................... 53 Abbildung 34 Aufbau eines Szenen-Graphen in Java3D aus [Sun01]..................................... 57 Abbildung 35 Hierarchie der wichtigsten Szenen-Graphen-Klassen....................................... 58 Abbildung 36 Beispiel HelloUniverse Szenen-Graph ............................................................. 59 Abbildung 37 Screenshot Java3D-Beispielanwendung ........................................................... 61 Abbildung 38 Sequenz-Diagramm beim Erzeugen des GLContext Objektes ......................... 65 Abbildung 39 Namenskonvention der Native-Funktion bei JNI ............................................. 68 Abbildung 40 Vordefinierte C-Typen auf Java-Objekte.......................................................... 69 Abbildung 41 Klassen-Hierarchie von Java-NIO-Buffer. Quelle [Hit02]. .............................. 71 Abbildung 42 Schematische Aufbau eines Buffer-Objektes. Quelle [Hit02]. ......................... 71 Abbildung 43 Klassendiagramm SWT Widget........................................................................ 73 Abbildung 44 Sequenz-Diagramm beim Instantiieren des GLContext Objektes .................... 74 Abbildung 45 Sequenz-Diagramm CreateContext................................................................... 75 Abbildung 46 Gears Demo in Java mit SWTOGL .................................................................. 78 Abbildung 47 Schematische Darstellung des 3DS-Formats .................................................... 83 Abbildung 48 (a) Eine klassische Fisheye-Projektion. (b) Eine normale 2D-Karte Darstellung. .................................................................................................................................................. 84 Abbildung 49 Hervorhebung durch 3D-Fisheye Zoom bei der Visualisierung ....................... 84 Abbildung 50 (a) Objekte/Häuser auf der Ebene. .................................................................... 85 Abbildung 51 Winkel θ und φ in Beziehung zu Punkt p ......................................................... 86 Abbildung 52 Herleitung der Projektion von p nach p’ bei φ = 90° oder 270° ....................... 86 Abbildung 53 Bestimmung von θ und φ .................................................................................. 87 Abbildung 54 Ein gerendertes POI Objekt. ............................................................................. 88 Abbildung 55 Stadt-Modell als normale Perspektivendarstellung........................................... 88 Abbildung 56 Stadt-Modell in Fisheye-View aus verschiedenen Blick-Richtung .................. 89 Abbildung 57 Darstellung einer künstlichen 3D-Straßen-Szene ............................................. 90 Abbildung 58 Fotografische Abbildung der Target-Hardware ................................................ 95 Abbildung 59 Schematische Darstellung der Hardware. Quelle [ICM04]. ............................. 96 5 Tabellenverzeichnis Tabelle 1 Gegenüberstellung der Applikationsgröße mit und ohne 3D-Grafik ....................... 32 Tabelle 2 Messergebnisse bei einem 256x256 Gitter............................................................... 44 Tabelle 3 Messergebnisse bei einem 512x512 Gitter............................................................... 44 Tabelle 4 Zuordnung der Java- und OpenGL-Datentypen....................................................... 49 Tabelle 5 Zuordnung der Java- und C-Datentypen .................................................................. 69 Tabelle 6 Messergebnisse Gears-Demo mit Java/SWTOGL und C auf dem Target............... 79 Tabelle 7 Technische Daten der Target-Hardware. Quelle [ICM04]....................................... 95 Listingverzeichnis Listing 1 Ein OpenGL Beispielprogramm ............................................................................... 17 Listing 2 Implementierung der glICM Funktionen .................................................................. 30 Listing 3 OpenGL Code in (a) Java/JOGL, (b) C/OpenGL ..................................................... 49 Listing 4 JOGL „HelloWorld“-Programm ............................................................................... 51 Listing 5 Code-Beispiele (a) Java/SWTOGL, (b) C/GLX...................................................... 63 6 1 Einleitung 1.1 Motivation Echtzeit-3D-Computergrafik ist längst kein Privileg mehr der Highend-Workstation oder der Spielkonsolen. Sei es auf dem PC oder im Bereich der Unterhaltungselektronik, überall sind 3D-Grafik garnierte Applikationen zu sehen. Deshalb ist es nicht verwunderlich, dass immer mehr Hersteller im Automobilbereich1 ihre Produkte mit 3D-Applikationen ausstatten. 3Dbasierte Infotainment- und Navigationssysteme sind entweder in der Planung oder bereits etabliert. Derartige Systeme gehören nicht nur zum aktuellen Trend, sondern sie werden oft vom Kunden als Innovation angesehen. Vor der Realisierung eines solchen Systems sind allerdings viele Fragen zu klären, die die technische Umsetzung betreffen, wie z.B.: • • • • • • Welche 3D-Bibliothek2 kann eingesetzt werden? Wie ist der Verbrauch von Speicher- und CPU-Kapazitäten? Können bestehende 3D-Programme portiert werden? Wie kann eine 3D-Bedienoberfläche/-Anwendung in ein bestehendes AutomotiveFramework integriert werden? Welche Programmiersprache kann eingesetzt werden? Und wie ist das „Look-and-Feel“ einer solchen Anwendung? 1.2 Aufgabenstellung Anhand von ausgewählten Themen sollen Einsatzmöglichkeit, Durchführbarkeit und Portierbarkeit von Echtzeit 3D-Applikationen auf einer Automotive-Embedded-Hardware untersucht werden. Die Aufgaben sind im Einzelnen folgende: • HMI (Human Machine Interface) mit 3D GUI (Graphical User Interface) Eine mit 3D-Grafik animierte Benutzeroberfläche3 soll prototypisch implementiert werden. Neben dem Demonstrieren des „Look-and-Feel“ soll hier insbesondere der einwandfreie Einsatz von OpenGL sowie die Einbindung in ein AutomotiveFramework auf dem Target4 untersucht werden. • Geländedarstellung Für echte 3D-Navigation ist eine Kartendarstellung mit Geländeinformationen unabdingbar. Daher soll hier ein geeignetes Verfahren zur Geländevisualisierung auf dem Target portiert werden. Ziel ist es, die Durchführbarkeit und die Portierbarkeit eines solchen Algorithmus auf dem Target zu untersuchen und die Problemgrösse zu erfassen. • OpenGL Java-Binding Im Rahmen dieser Untersuchung soll eine geeignete freiverfügbare5 OpenGL JavaBinding-Implementierung auf dem Target unter QNX portiert werden. Es soll der 1 engl. automotive engl. library 3 engl. GUI (Graphical User Interface) 4 siehe Anhang A 5 engl. Open Source 2 7 Frage nachgegangen werden, inwieweit die Entwicklung von 3D-Applikation auf Java-Basis auf dem Target möglich und sinnvoll ist. • 3D-Modelle und Fisheye-View Eine weitere wichtige Anwendung im 3D-Bereich ist die Visualisierung von 3DModellen. Ein prototypischer 3D-Viewer ist bei dieser Untersuchung zu entwickeln. Der Viewer soll ein 3D-Modell aus einem gängigen 3D-Dateiformat einlesen und mit OpenGL auf dem Target darstellen. Diese Modelle können sowohl einzelne Gebäude als auch eine ganze Stadt beinhalten. Als zusätzliche Funktion soll das Programm ein eingelesenes 3D-Stadt-Modell mit einer „Fisheye-Projektion“ visualisieren. Ziel dieser Untersuchung ist die Realisierbarkeit eines 3D-Viewers sowie die Durchführbarkeit von Echtzeit-Berechnung von visuellen Effekten mit umfangreicheren 3D-Modellen zu evaluieren. Als 3D-Library soll OpenGL eingesetzt werden. Die Anwendungen sollen auf der im Multimedia-Labor der FH-Darmstadt installierten Target-Hardware prototypisch implementiert werden. Technische Daten des Targetsystems sind im Anhang A angeführt. 8 2 HMI mit 3D-GUI Will man heutzutage eine Anwendung mit 3D-Grafik Unterstützung im Desktop-Bereich entwickeln, so ist die Entscheidung für die geeignete 3D-Library sowie auf das GUIFramework relativ einfach zu treffen. Je nach Hardware, Betriebssystem, Programmiersprache und finanziellen Rahmenbedingungen findet man hier schnell das passende Produkt. Für Embedded-Anwendungen trifft diese Situation nicht zu. Selbstverständlich existieren auch hier eine Fülle von guten Produkten, aber die Anforderungen und Eigenschaften der eingesetzten Hard- und Software sind sehr speziell und sehr verschieden, sodass man nicht nach einem „Einheitsrezept“ vorgehen kann. Außerdem ist ein ausgefeiltes GUI-Framework für einfache Anwendungen, wie z.B. zur Steuerung von Waschmaschinen, nicht unbedingt erforderlich sind, auch wenn es bereits solche Produkte mit voll grafikfähigem Display, vergleichbar mit einem Navigationssystem, auf dem Markt gibt. Im Rahmen einer prototypischen Implementierung einer mit 3D-Grafik animierten GUI soll in dieser Ausarbeitung untersucht werden, wie sich die Integration von 3D-Grafik in ein GUIund Automotive-Framework realisieren lässt. In Kapitel 2.1.1 wird zunächst auf mögliche Vorgehensweisen und Techniken für die Realisierung einer 3D-GUI sowie die Integration der GUI bzw. HMI in ein AutomotiveFramework eingegangen. Dabei werden grundlegende Konzepte der Software-Architektur erörtert. Einen Überblick zu den Grundlagen von OpenGL wird schließlich in Kapitel 2.1.2 gegeben. Im Implementierungsteil (Kapitel 2.2) wird die prototypische Realisierung vorgestellt. Auf die konkrete Target-Portierung der OpenGL-Library und die Anbindung von OpenGL zum QNX-Window-System Photon wird in diesem Abschnitt ebenfalls eingegangen. 2.1 Grundlagen 2.1.1 Automotive Framework Integration In einem modernen Auto werden Geräte im Infotainmentbereich über den MOST-Bus (Media Oriented System Transport6) angeschlossen. Die Steuerung des gesamten Systems erfolgt über ein Softwaresystem, welches in der Regel auf der Headunit betrieben wird. Das klassische Software-Design im Embedded-Bereich beschränkt sich auf das Strukturieren der Funktionalitäten in Unterprogrammen und auf das Aufteilen der Aufgaben in statische Schichten und Module. Ein Multi-Tasking/-Processing fähiges Betriebsystem ist je nach Einsatzgebiet nicht immer vorhanden. Zur Laufzeit befindet sich die Steuerungssoftware allein im System und hat jederzeit die volle Kontrolle. Diese einfache Vorgehensweise ist für ein modernes Infotainmentsystem zwar auch möglich, aber längst nicht optimal. Um die Geräte und den Kommunikationsbus besser abstrahieren zu können, ist eine modernere Architektur erforderlich. Abbildung 1 zeigt die schematische Darstellung einer Software-Architektur für Automotive-Systeme7. 6 7 vgl. [MOST01] vgl. [Wie05] 9 Abbildung 1 Automotive Framework Architektur. Quelle [Wie05] Die Architektur wird durch ein Framework8 geregelt. Logische und physikalische Geräte werden in Software-Komponenten gekapselt und zur Laufzeit als eigenständiger Thread oder Prozess in das Gesamtsystem integriert. Die Kommunikation zwischen Komponenten erfolgt über Austausch von Nachrichten9. Events werden in der Regel asynchron verarbeitet und in Shared-Memory-Queues abgelegt. Jede Komponente verfügt über eigene Queues (Intern-, Extern- und System-Queue) und einen Dispatcher für den Empfang und die Verarbeitung von Events. Neben den Geräte-Komponenten (Tuner, CD usw.), die jeweils ein logisches oder physikalisches Gerät repräsentieren, gibt es weitere Komponenten wie z.B. die AdminKomponente, die spezifische Aufgaben im System erledigen. Eine zentrale Rolle spielt dabei die Main-Dispatcher-Komponente. Hier erfolgt nämlich die Steuerung des MOST-Treibers. Wie bereits oben erwähnt, werden die Geräte physikalisch über den MOST-Bus angeschlossen. Zum Ansteuern eines Gerätes werden MOST-Events gemäß dem MOST-Protokoll über den MOST-Treiber zum Gerät gesendet. Auch ankommende Daten von den Geräten sind über den MOST-Treiber zu empfangen. Außerdem ist diese Komponente auch für die interne Kommunikation zuständig. Alle internen Events werden zunächst zum Main-Dispatcher gesendet und von dort aus zum Empfänger weiter geleitet. 8 9 Die genaue Definition und Aufgabe von einem Framework ist in [Wie05] ausführlich beschrieben. engl. Events 10 Für die grafische Darstellung der Bedienoberfläche sowie Bearbeitung der BenutzerInteraktion ist die HMI-Komponente zuständig. Diese Komponente besteht aus zwei logischen Schichten10. Die GUI-Schicht und die „Main Desktop Manager“-Schicht (siehe Abbildung 2). Aufgabe der GUI-Schicht ist die Darstellung der grafischen Bedien-Elemente sowie das Anzeigen von Meldungen und Informationen. Der Main-Desktop-Manager kümmert sich um das Event-Handling, also die Kommunikation mit anderen Komponenten, und steuert je nach Kontext und Zustand die Ausgabe der GUI. Benutzer-Eingaben werden in der Regel als Event gekapselt, vom Main-Dispatcher an die HMI weitergeleitet und vom Main-Desktop-Manager ausgewertet. Abbildung 2 Aufbau der HMI-Komponente Die Anbindung von 3D-Animationen erfolgt logischerweise in der GUI-Schicht. Die GUI kann mit verschiedenen Techniken realisiert werden. Einige Möglichkeiten werden nachfolgend vorgestellt: • Window-System und Widget-Toolkit Die klassische Art und Weise eine GUI zu erstellen ist die Verwendung eines Window-Systems und der dazu passenden Widget-Toolkit-Library. Bei den meisten Toolkits sind in der Regel komfortable GUI-Builder verfügbar. Damit kann man in kürzester Zeit eine GUI per visuellen Editor zusammenstellen und den entsprechenden Programmcode generieren. Soll die Anwendung vollständig in das GUI-Framework integriert werden, so kann man je nach Ablauflogik Callback-Funktionen den einzelnen Bedienelementen (Button, Slider) zuweisen. Diese Callback-Funktionen werden bei einer Aktivierung des GUI-Elements („anklicken“ eines Buttons) entsprechend aufgerufen. Eine derartige Integration setzt aber voraus, dass die Eingabegeräte (TouchscreenDisplay, Commander) auch in das Betriebssystem integriert sein müssen, damit das Event-Handling des GUI-Framework funktioniert. Im Embedded-Bereich ist das jedoch aus diversen Gründen nicht immer möglich. Bei einem Touchscreen-Display ist die Integration noch relativ einfach, das Drücken auf dem Display kann direkt als eine Interaktion mit x- und y-Koordinaten an das GUI-Event-Framework weitergeleitet werden. Bei einem komplexen Eingabegerät, etwa dem Commander eines modernen InCar-Multimedia-Systems, ist eine einfache Zuordnung nicht mehr möglich. Für die Darstellung von 3D-Grafik werden von den Toolkit-Libraries spezielle Grafik- oder Raw-Widget zur Verfügung gestellt. Die von der 3D-Engine gerenderte Grafik kann durch entsprechende Funktionen auf diesen Widgets angezeigt werden. Abbildung 3 zeigt ein Beispiel mit QNX-Photon. 10 Die „Virtuelle GUI“ nach [Wie05], die die tatsächliche GUI softwaretechnisch abstrahiert, wird hier nicht behandelt. 11 Abbildung 3 Raw-Widget mit Photon GUI-Builder Für die Animation kann beispielsweise die Timer-Funktion des GUI-Frameworks verwendet werden. D.h. es wird jeweils nach einem festgelegten Zeit-Intervall eine Callback-Funktion aufgerufen, die ein neues Bild rendert und anzeigt. Alternativ kann die 3D-Engine auch in einer Schleife explizit aufgerufen werden. In diesem Fall ist die Ausführung der GUI getrennt als ein separater Thread zu empfehlen, damit die HMI nicht unnötig durch das Rendern der Grafik geblockt wird. Eine mögliche Integration in ein Automotive Framework ist schematisch in Abbildung 4 dargestellt. GUI Loop GUI Thread HMI Prozess Events Disp. Main Desktop Manager Main Thread Abbildung 4 Integration von 3D-GUI mit Grafik-Widget Die GUI-Schicht wird als getrennter Thread ausgeführt und ist für die grafische Darstellung und Animation der Bedienoberfläche zuständig und übernimmt auch die Initialisierung des Window-Systems. So kann sich der Main-Desktop-Manager auf das Framework-Event-Handling und die Steuerung der Ablauflogik konzentrieren. Der Vorteil dieser Methode ist die einfache Integration in die bestehende GUI. 3DGrafik-Animationen können dadurch auch mit High-Level-Widgets wie z. B. Browser oder Text-Editor kombiniert werden. Der Nachteil ist die im Vergleich zu anderen Methoden schlechtere Performance. 12 • Multi-Layer Unterstützt die Grafik-Hardware Multi-Layer Technik, so kann die GUI auch entsprechend ausgelegt werden. Beispielsweise kann ein Layer im Hintergrund eine 3D-Navigationskarte darstellen, während im Vordergrund die 2D-Bedienknöpfe angezeigt werden. Abbildung 5 zeigt das Konzept schematisch. Die einzelnen Layer der Beispiel-GUI sind in Abbildung 5(a) abgebildet. Diese können wie in Abbildung 5(b) hardwaremäßig aufeinander gelegt werden. Abbildung 5(c) zeigt das für den Anwender sichtbare Ergebnis. (a) (b) (c) Abbildung 5 (a) Die einzelne Layer. (b) Die Überlagerung der Layer. (c) Das Ergebnis. Die 2D Elemente können dabei mit einem klassischen GUI-Toolkit erstellt und angezeigt werden. Der bei der Überlagerung sichtbare Bereich des VordergrundLayers wird als transparent markiert. Die 3D-Karte im Hintergrund-Layer kann mit der OpenGL-Engine unabhängig von den anderen Layern gerendert werden. Eine mögliche Integration in das Framework zeigt Abbildung 6. Bei diesem Modell bietet sich an für jede Layer einen separaten Thread einzusetzen. Die Steuerung der gesamten GUI über alle Layer hinweg wird wie bereits erwähnt vom Main-DesktopManager übernommen. 13 GUI-2D GUI-Info GUI-3D Loop GUI Thread HMI Prozess Events Disp. Main Desktop Manager Main Thread Abbildung 6 Framework Integration Multi-Layer-Technik Das Verfahren bietet einige Vorteile gegenüber anderen Verfahren. Zum einen wird eine modulare GUI-Entwicklung besser unterstützt. Zum anderen ist eine bessere Performance zu erzielen; bei der Aktualisierung der 3D-Karte beispielsweise müssen die anderen Layer nicht erneut gerendert werden. Der Nachteil bei dieser Methode ist, dass die Steuerung im Desktop-Manager komplexer wird. • 3D im Direct-Mode Obwohl OpenGL als 3D-Engine eingesetzt wird, kann man diese Library auch hervorragend für das 2D-Rendering benutzen. Die Idee bei diesem Modell ist, die 2D-Bedienelemente mit der 3D-Szene zusammen in OpenGL zu erzeugen und zu rendern. Dadurch ist es möglich, auf das Window-System zu verzichten; die Grafikausgabe kann direkt zum Grafiktreiber weitergeleitet werden. Diese Vorgehensweise wird überwiegend bei der Spieleprogrammierung verwendet. Die Integration in das Framework erfolgt wie beim oben genannten ersten Modell. Das Rendern im Direct-Mode bringt in erster Linie Performance-Vorteile. Nachteile sind dagegen die aufwendigen Programmierungen, da OpenGL standardmäßig keine (2D/3D) Widget-Library bietet. 2.1.2 OpenGL Im Bereich der 3D-Computergrafik hat sich OpenGL in den letzten Jahren als IndustrieStandard durchgesetzt. OpenGL (Open Graphics Library) wurde von der Firma SGI (Silicon Graphics) entwickelt und 1992 der Öffentlichkeit vorgestellt. Die aktuelle Version vom Stand September 2004 ist 2.0. Das Entwicklungsziel war eine effiziente, plattformunabhängige und einfach benutzbare API für 2D- und 3D-Grafik bereit zu stellen. Wesentliche Eigenschaften von OpenGL zusätzlich zu den gängigen 2D-Grafik-APIs sind: • Transparenz und Alpha-Blending Objekte und Farben können als transparent dargestellt werden. Realisiert wird es durch eine zusätzliche Farbinformation im Alpha-Kanal, d.h. eine Farbe wird durch vier Werte (rot, grün, blau und alpha) beschrieben. Der Alpha-Wert gibt den Grad der Transparenz an. 14 • Anti-Aliasing Bei der Darstellung von schräg verlaufender Linien und Kurven entstehen unter normalen Umständen sog. Treppen-Effekte. Bei Anti-Aliasing wird die Farbe der umliegenden Punkte so angepasst, dass diese Linien insgesamt weicher erscheinen. • 3D-Projektion 3D-Szenen können sowohl in Perspektiven als auch in orthogonalen Ansichten dargestellt werden. Bei der Perspektivenansicht erscheinen entfernte Objekte mit zunehmender Entfernung kleiner. Bei der orthogonalen Projektion werden Objekte wie bei einer CAD-Zeichnung ohne räumliche Verzerrung dargestellt. • Texturen 2D-Bilder können als Oberfläche auf Polygone projiziert werden. Verändert sich die Größe der Polygone, so wird die erforderliche Anpassung der Textur-Größe automatisch durchgeführt. • Licht- und Schatten-Effekte Vielfältige Licht- und Schatten-Effekte sind mit OpenGL realisierbar. Eigenschaften wie Lichtquellen, Materialien und Beleuchtungsmodelle können beliebig eingestellt werden. • Nebel und atmosphärische Effekte Durch Nebel und atmosphärische Effekte können sehr realitätsnahe Grafiken erstellt werden. OpenGL setzt sich aus drei Hauptkomponenten zusammen: • OpenGL Library Die OpenGL Library (z.B. GL.a unter Linux) enthält alle Implementierungen der Basis-Funktionen. Alle Basis-Funktionen beginnen mit dem Präfix „gl“. OpenGLBefehle sind leistungsfähige, aber primitive, Rendering-Funktionen. Komplexe 3DModelle müssen durch eine Menge von einfachen geometrischen Objekten, wie Punkte, Linien und Polygone, aufgebaut werden. • OpenGL Utility Library Die GLU Library (z.B. GLU.a unter Linux) enthält eine Menge von Hilfsfunktionen, um komplexere 3D-Objekte wie Ellipsoide oder Splines zu erstellen. Nach der OpenGL-Konvention erhalten alle GLU-Funktionen den Präfix „glu“. • Plattform/Window-System abhängige Library Da OpenGL plattformunabhängig ist, wird für die Darstellung der gerenderten Grafik eine Plattform- und Window-System abhängige Library verwendet. Die wichtigsten sind: o GLX: Für Unix / X-Window-Systeme o WGL: Für Microsoft Windows o GlPh: Für QNX-Photon 15 Außerdem existieren noch weitere Plattform- und Window-System unabhängigen Toolkits, die mit OpenGL zusammenarbeiten. Die bekanntesten sind: o GLUT: Das OpenGL Utility Toolkit [GLUT] ist eine Open-Source Library und wird oft in der Literatur als Beispiel verwendet. o SDL: Die Simple DirectMedia Layer Library [SDL] ist ebenfalls Open-Source, wird überwiegend bei Computerspiele eingesetzt. Im Folgenden werden einige Grundbegriffe von OpenGL erläutert, die für das Verständnis dieser Ausarbeitung wichtig sind. Eine umfassende Erläuterung dieses Kontexts ist in der weiterführenden Literatur [Shr04] verfasst. 2.1.2.1 OpenGL Konventionen Die Benennung der Funktionen und Konstanten bei OpenGL erfolgt über bestimmten Konventionen. Diese sind in Abbildung 7 zusammengefasst dargestellt. glVertex3fx(...) Wenn v, Vektor oder Array Version Suffix OpenGL-Typ Daten-Typ f d b s i ub us uI GLfloat 4-byte float GLdouble 8-byte float GLbyte signed 1-byte integer GLshort signed 2-byte integer GLint signed 4-byte integer GLubyte unsigned 1-byte integer GLushort unsigned 2-byte integer GLuint unsigned 4-byte integer GLenum GLbitfield Anzahl der Komponenten (2,3 oder 4) Funktionsname Prefix für OpenGL-Befehle Abbildung 7 OpenGL Namenskonvention Zum Beispiel setzt sich die Funktion glVertex3f()aus den folgenden Argumenten zusammen: - Präfix: gl - Name: Vertex - Anzahl der Komponenten: 3 - OpenGL Typ der Operanden: f Konstanten beginnen bei OpenGL immer mit dem Präfix „GL_“ und werden immer in großen Buchstaben geschrieben, z.B. GL_POINTS. 16 2.1.2.2 Programmstruktur Anhand eines einfachen Beispiels soll die Struktur eines OpenGL Programm gezeigt werden. Display *display = NULL; Window window; XVisualInfo *visualInfo = NULL; GLXContext glxContext; void main() { // X11-Code display = XOpenDisplay( NULL ); visualInfo = glXChooseVisual( display, DefaultScreen(display), ...); window = XCreateWindow( pDisplay, ...); XMapWindow( display, window ); // GLX-Code glxContext = glXCreateContext( display, visualInfo, NULL, GL_TRUE ); glXMakeCurrent( display, window, glxContext ); // OpenGL-Init-Code glClearColor( 0.0f, 0.0f, 0.0f, 1.0f ); glMatrixMode(GL_PROJECTION); glLoadIdentity(); glOrtho(-1.0f, 1.0f, 1.0f, 5.0f, 0.0f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // 3D-Objekt-Code glColor3f(1.0f, 0.3f, 0.3f); glBegin(GL_QUADS); glVertex3f(0.25f, 0.25f, 0.0f); glVertex3f(0.75f, 0.25f, 0.0f); glVertex3f(0.75f, 0.75f, 0.0f); glVertex3f(0.25f, 0.75f, 0.0f); glEnd(); // GLX-SwapBuffer glXSwapBuffers( display, window ); ... } Listing 1 Ein OpenGL Beispielprogramm Für die X11 Anbindung verwendet das Beispielprogramm GLX. Die wesentlichen Schritte sind: • Window-System initialisieren (X11-Code) Mit den Funktionen XOpenDisplay() und XCreateWindow() wird ein Fenster unter X11 initialisiert. • OpenGL-Context aus dem Window-System-Context erstellen (GLX-Code) Der Handler bzw. Context des erstellten Fensters wird mit den Funktionen glXCreateContext() und glXMakeCurrent() an OpenGL übergeben. Damit wird die gerenderte Grafik auf diesem Fenster ausgegeben. • OpenGL Grundeinstellung durchführen (OpenGL-Init-Code) 17 Grundeinstellungen, wie z.B. Projektionsmodus glOrtho () oder Hintergrundfarbe glClearColor() werden getätigt. • 3D-Objekt erstellen (3D-Objekt-Code) Beliebige Grafik-Objekte können an dieser Stelle mit den entsprechenden OpenGLFunktionen erstellt werden. Ein geometrisches Objekt, z.B. GL_QUADS, wird durch eine Menge von Punkten (Vertices) beschrieben. Ein Punkt wird im OpenGL 3DRaum wiederum durch die Funktion glVertex()definiert. • Das gerenderte Ergebnis darstellen (GLX-SwapBuffer) Das Ergebnis wird mit der Funktion glXSwapBuffers() auf dem Fenster gezeigt. In der Regel verwendet man bei OpenGL für das Rendering den Double-Buffer-Modus, d.h. das Rendering erfolgt zunächst auf dem Back-Buffer und erst nach Fertigstellung wird der Buffer umgeschaltet und damit sichtbar gemacht. Damit ist ein flüssiger Framewechsel möglich. Wird dagegen der Single-Buffer-Modus verwendet, so genügt ein glFlush() Aufruf um das Ergebnis anzuzeigen. Es ist zu beachten, dass OpenGL eine State-Machine ist. D.h. die Zustände bzw. getätigten Einstellungen, z.B. die Hintergrundfarbe, bleiben so lange gültig bis zur nächsten Änderung. 2.1.2.3 Grafik-Primitive: Lines, Polygon… Wie bereits in Listing 1 gezeigt, erfolgt die Spezifikation eines geometrischen Objektes mit Hilfe des Funktionspaares glBegin()und glEnd(). D.h. alle Vertices innerhalb der glBeginn() und glEnd() Klammer gehören zu diesem Objekt. In der Funktion glBeginn() ist der Typ des geometrischen Objektes einzutragen. Es gibt zehn primitive Grafik-Typen in OpenGL. Diese sind in Abbildung 8 angeführt. V1 V0 V1 V3 V0 V2 V4 V4 V5 V1 V3 GL_TRIANGLE_STRIP V2 V0 V5 V4 GL_LINE_STRIP GL_LINES V1 V5 V1 V2 V2 V4 V1 V0 V3 GL_TRIANGLE_FAN V2 V5 V3 V4 GL_QUADS V2 V4 V6 V1 V0 V7 V3 V5 GL_QUADS_STRIPS V4 V3 V5 GL_TRIANGLES GL_LINE_LOOP V6 V1 V0 V2 V3 V5 V2 V4 V4 V3 V0 V3 V0 V6 V7 GL_POINTS V0 V1 V2 V7 V0 V4 V1 V2 V3 GL_POLYGON Abbildung 8 Geometrische Objekte 2.1.2.4 Vertex Array Ein Vertex Array wird benutzt, um die Anzahl der Aufrufe von OpenGL-Befehlen zum Erstellen eines 3D-Objektes zu minimieren. Zum Beispiel benötigt das Programm in Listing 1 zum Zeichnen eines Viereckes sechs Aufrufe. Je einen Aufruf für glBegin() und glEnd() bzw. vier für die Vertex-Definitionen. Bei komplexeren Objekten steigt die Anzahl der OpenGL-Aufrufe entsprechend an. Ein weiteres Problem ist die redundante Angabe von gemeinsam genutzten Vertices. Das Problem wird im folgenden Beispiel erläutert: Der Würfel in Abbildung 9 soll z.B. gerendert werden. 18 2 6 3 7 0 4 1 5 Abbildung 9 Würfel mit acht Eckpunkte Ein Würfel kann durch acht Eckpunkte und sechs Flächen beschrieben werden. Da OpenGL keinen Würfel als 3D-Objekt kennt, muss dieser durch einfache geometrische Objekte zusammengesetzt werden. Verwendet man dafür als Grafik-Primitive ein Viereck (GL_QUADS), so werden sechs GL_QUADS Objekte mit je vier Vertices benötigt. Das sind insgesamt 24 glVertex()-Aufrufe. Die Summe der Aufrufe ist nicht effizient. Die Lösung dieses Problem ist ein Vertex-Array. Dabei werden die Koordinaten der Vertices eines 3D-Objektes in einem Array gespeichert und mit einem einzigen Befehl an OpenGL weitergeleitet. Darüber hinaus können auf gemeinsam genutzte Vertices mit Hilfe von IndexListen zugegriffen werden. Das folgende Beispiel zeigt die Verwendung eines Vertex-Arrays. static GLfloat myVertices[] = { 0.25f, 0.25f, 0.0f, 0.75f, 0.25f, 0.0f, 0.75f, 0.75f, 0.0f, 0.25f, 0.75f, 0.0f }; static Glubyte triangleIndices[] = { 0, 1, 2 }; glEnableClientState(GL_VERTEX_ARRAY); glVertexPointer(3, GL_FLOAT, 0, myVertices); glDrawElements(GL_TRIANGLES, 3, GL_UNSIGNED_BYTE, triangleIndices); Beispiel mit Vertex Array, ein Dreieck wird gezeichnet Als erstes muss das Array mit der Funktion glEnableClientState() aktiviert werden. Dann wird das Array mit glVertexPointer() an OpenGL übergeben. Schließlich wird das Objekt durch ein Vertex-Index-Array dereferenziert und mit der Funktion glDrawElements() gerendert. In der Praxis ist die Verwendung von Vertex-Array bei komplexeren 3D-Objekte unabdingbar. 2.1.2.5 Koordinatensystem und Transformation OpenGL verwendet ein 3-dimensionales Euklidisches Koordinatensystem. Die Ausrichtungen der x, y und z-Achsen ist in Abbildung 10 abgebildet. 19 +y -z -x +x +z -y Abbildung 10 Das OpenGL Koordinatensystem Ein 3D-Objekt wird durch seine Vertices spezifiziert. Für die Visualisierung muss das 3DObjekt zunächst im Koordinatensystem positioniert werden. Anschließend sind eine Reihe von weiteren Transformationen bis zum fertigen Ergebnis notwendig. Abbildung 11 zeigt die erforderlichen Transformationsstufen. Abbildung 11 Transformationsstufen • Modell-View Transformation In dieser Stufe wird das 3D-Objekt im 3D-Raum auf die richtige Position (Translation, Rotation) gebracht. Außerdem wird die Kamera-Position eingestellt. • Projektions-Transformation Je nach Projektionsmodus (perspektivisch oder orthogonal) und je nach Parameter werden die Koordinaten entsprechend transformiert. • Normierung und Viewport Transformation Zum Schluss werden die Koordinaten in die Ausgabegeräte spezifische Koordination transformiert. 3D-Transformationen werden in der Computergrafik durch Matrix-Multiplikation realisiert. Eine Transformationsmatrix ist eine 4 x 4 Matrix. Bei der Transformation werden die Koordinaten eines jeden Punktes in der 3D-Welt mit dieser Matrix multipliziert. Matrix-Multiplikation in OpenGL wird immer auf den Matrix-Stack angewendet, d.h. die als Operand übergebene Transformationsmatrix wird mit der „aktuellen“ (Top of Stack) Matrix 20 multipliziert. Das Ergebnis davon bleibt als aktuelle Matrix auf dem Stack. Wenn also C die aktuelle Matrix und M die Transformationsmatrix ist, dann ist das Ergebnis nach der Multiplikation immer C·M. OpenGL bietet folgende Funktionen für die Matrix-Multiplikation an. • glLoadMatrixf(const GLfloat *M) Eine 4 x 4 Matrix wird als die aktuelle Matrix geladen. • glLoadIdentity(GLvoid) Die Einheitsmatrix wird geladen, die Einheitsmatrix wird zum Initialisieren benötigt. • glMultMatrix(const GLfloat *M) Die eigentliche Multiplikationsfunktion. Die als Parameter übergegebene Matrix wird mit der Aktuellen Matrix multipliziert. • glPushMatrix(GLvoid), glPopMatrix(GLvoid) Matrizen auf dem Matrix-Stack werden mit glPushMatrix()eine Stufe nach verschoben und mit glPopMatrix() aus dem Stack gelöscht. unten Diese Funktionen beziehen sich immer auf den aktuell ausgewählten Matrix-Modus. Der Matrix-Modus kann mit der Funktion glMatrixMode() eingestellt werden. Mögliche Werte sind: GL_MODELVIEW, GL_PROJECTION und GL_TEXTURE. 2.1.2.6 Modell- und View-Transformation • Modell Transformation Translation, Rotation und Skalierung von 3D-Objekten sind grundlegende Operationen bei der Modell-Transformation (siehe Abbildung 12). +y +y +y +x +z +z (a) +x +x +z (b) (c) Abbildung 12 (a) Translation, (b) Rotation und (c) Skalierung Bei OpenGL sind folgende Funktionen vorgesehen: o o o • glTranslate() glRotate() glScale() View Transformation Der Ansichtspunkt (View Point) oder die Kamera-Position werden mit der Funktion 21 gluLookAt( eyeX, eyeY, eyeZ, centerX, centerY, centerZ, upX, upY, upZ) eingestellt. Dabei spezifizieren die eye* Parameter die Position der Kamera und die center* Parameter den Mittelpunkt auf den die Kamera gerichtet ist. Die up* Parameter geben den sog. Up-Vektor an, also die „Orientierung“ des Ansichtspunktes (siehe Abbildung 13) Abbildung 13 View-Transformation 2.1.2.7 Projektion Transformation Nach der Modell-View-Transformation sind alle Punkte der 3D-Objekte an der richtigen Position im 3D-Raum. Für die Darstellung auf einem 2D-Display müssen die Punkte jedoch entsprechend der Projektionsregeln erneut transformiert werden. In OpenGL sind zwei Projektionstransformationen möglich: die orthogonale (parallel-) und die perspektivische Transformation. • Orthogonale Projektion In der orthogonalen Projektion werden alle Punkte durch parallele Strahlen auf die Projektionsfläche abgebildet. Die Größen und Winkel der Objekte bleiben konstant, unabhängig von der Entfernung der Kamera. Diese Projektionsart wird vor allem bei CAD-Systemen verwendet. Eine orthogonale Projektion kann in OpenGL mit der folgenden Funktion eingestellt werden: glOrtho( left, right, bottom, top, near, far ) Die Bedeutung der Parameter ist in Abbildung 14 dargestellt. Für eine 2DDarstellung mit OpenGL kann ebenso die Funktion glOrth2D() verwendet werden. 22 top far left right near bottom Abbildung 14 Orthografische Projektion • Perspektivische Projektion Objekte erscheinen bei der perspektivischen Projektion zunehmend kleiner, je weiter sie von der Kamera entfernt sind. Die perspektivische Projektion kann in OpenGL auf zwei Arten definiert werden. o glFrustum( left, right, bottom, top, near, far) o gluPerspective( fovy, aspect, near, far) Die Parameter der beiden Funktionen sind in Abbildung 15 dargestellt. Der Parameter fovy ist der vertikale (y- / z-Achse) Blickwinkel mit einem Wertebereich von 0° bis 180°. Der Parameter aspect ist das Verhältnis zwischen dem horizontalen und vertikalen Blickwinkel. 23 top Aspect=w/h right left near w far fovy h θ near far bottom (a) (b) Abbildung 15 Perspektivische Projektion. (a) glFrustum(), (a) gluPerspective() 2.1.2.8 Texture Mapping Mit Texture-Mapping kann man eine Textur als Oberfläche für ein grafisches Objekt zuweisen. Eine Textur ist in der Regel ein zwei dimensionales Bild11. Ein Datenelement in der Textur wird Texel genannt. Das Prinzip des Texture-Mappings ist in Abbildung 16 gezeigt. t1 t2 p1 p2 p4 t4 t3 p3 3D Objekt Texture-Image Abbildung 16 Texture-mapping Jeder Punkt des 3D-Objektes bekommt als zusätzliche Information die Koordinaten des Texels zugeordnet. Dabei beziehen sich die Texel-Koordinaten immer auf das aktuelle Textur-Objekt. Die benötigte Funktion glTextCoord2f() wird wie folgt in Verbindung mit glVertex3f() verwendet. 11 engl. image 24 glBegin(GL_QUADS); glTextCoord2f(0.0f, glTextCoord2f(1.0f, glTextCoord2f(1.0f, glTextCoord2f(0.0f, glEnd(); 0.0f); 0.0f); 1.0f); 1.0f); glVertex3f(0.25f, glVertex3f(0.75f, glVertex3f(0.75f, glVertex3f(0.25f, 0.25f, 0.25f, 0.75f, 0.75f, 0.0f); 0.0f); 0.0f); 0.0f); Beispiel mit glTextCoord Eine moderne 3D-Grafikkarte verwendet eine spezielle Texture-Engine und einen TextureSpeicher, um das Rendering zu beschleunigen. Es ist nicht sinnvoll Texturen bei jedem Frame an die 3D-Engine zu schicken. Texturen werden daher in OpenGL als Textur-Objekte verwaltet und je nach Implementierung im Video-Speicher abgelegt. Ein 3D-Objekt kann ein Textur-Objekt über ein von OpenGL generiertes Index (Name) referenzieren. Das folgende Beispiel zeigt diesen Mechanismus. glGenTextures(1, &texName); glBindTexture(GL_TEXTURE_2D, texName); glTexParameteri(GL_TEXTURE_2D, ...); glTexImage2D(GL_TEXTURE_2D, 0, GL_RAGBA, width, high, 0, GL_RGBA, GL_UNSIGNED_BYTE, myImage); ... glBindTexture(GL_TEXTURE_2D, texName); glBegin(GL_QUADS); glTextCoord2f(0.0f, 0.0f); glVertex3f(0.25f, 0.25f, 0.0f); ... glEnd(); Beispiel mit glGenTextures Mit der Funktion glGenTextures() wird ein freier Index für ein neues Textur-Objekt ermittelt und mit glBindTexture() initialisiert. Weitere Eigenschaften können mit der Funktion glTexParameteri() eingestellt werden. Die Funktion glTexImage2D() lädt schließlich das Image zu dem Textur-Objekt. Ein mit glBindTexture() aktiviertes TexturObjekt hat bis zum erneuten Aufruf Gültigkeit für alle folgenden Objekt-Definitionen. 2.2 Implementierung Für die Entwicklung von 3D-Appliaktionen mit OpenGL bietet die Firma QNX ein 3DGraphic Technology Development Kit (TDK)12 an. Das Paket enthält neben den Basis OpenGL-Libraries auch ein Framework für die QNX-Photon-Anbindung13. Ausserdem werden für einige unterstützte Grafik-Chipsätze OpenGL-Treiber mit Hardwarebeschleunigung zur Verfügung gestellt. Für die Implementierung der Aufgabe stand das TDK nicht zur Verfügung, was jedoch keine negativen Auswirkungen auf die angestrebten Untersuchungen hatte. Eine hardwarebeschleunigte OpenGL-Implementierung für den im Target verwendeten GrafikChip Lynx3DM von Silicon Motion14 war ohnehin nicht verfügbar, obwohl der Chip 12 vgl. [QNX3D] Details zum GLPh-Framework sind in [Photon04] beschrieben. 14 vgl. [SMLynx] 13 25 hardwareseitige 3D-Beschleunigungsfunktionen kennt. Des Weiteren existiert eine OpenSource OpenGL-Implementierung namens Mesa15, die 3D-Rendering auf Softwarebasis anbietet. Die Mesa-Libraries sind feste Bestandteile der gängigen Linux-Distribution und somit bei fast allen Linux-Rechnern ohne spezielle 3D-Grafikkarte im Einsatz. Aus diesen Gründen wurde entschieden für die Durchführung der vorliegenden Arbeit die Mesa-Libraries auf QNX-X86 und QNX-SH4 zu portieren. Details über die Portierung sind in Kapitel 2.2.1 beschrieben. Ein weiteres Problem ist das fehlende GLPh-Framework. Schließlich muss die gerenderte Grafik auch angezeigt werden. Die fehlende Verbindung zum Window-System Photon musste daher selbst entwickelt werden. Diese wurde prototypisch implementiert und ist in Kapitel 2.2.2 aufgeführt. Die für Demonstrationszwecke zu entwickelnde 3D-GUI wird in Kapitel 2.2.3 beschrieben. 2.2.1 Mesa Für die Portierung auf QNX wurde die Mesa-Library in der Version 6.0 benutzt. Folgende Änderungen sind dafür nötig gewesen: • Das Buildscript ./Make-config # QNX V4 & Watcom Compiler #qnx: # $(MAKE) $(MFLAGS) -f Makefile.X11 targets \ # "GL_LIB = libGL.a" \ # "GLU_LIB = libGLU.a" \ # "GLUT_LIB = libglut.a" \ # "GLW_LIB = libGLw.a" \ # "OSMESA_LIB = libOSMesa.a" \ # "CC = qcc" \ # "CFLAGS = -O" \ # "APP_LIB_DEPS = -L/usr/X11/lib -lX11 -lm" qnx: $(MAKE) $(MFLAGS) -f Makefile.X11 targets \ "GL_LIB = libGL.a" \ "GLU_LIB = libGLU.a" \ "GLUT_LIB = libglut.a" \ "GLW_LIB = libGLw.a" \ "OSMESA_LIB = libOSMesa.a" \ "CC = qcc" \ "CFLAGS = -O -Y_ecpp-ne -I/usr/X11R6/include - \ DDEFAULT_SOFTWARE_DEPTH_BITS=31" \ "APP_LIB_DEPS = -L/usr/X11R6/lib -lglut -lGLU -lGL -lX11 -lm" Änderungen in der Datei Make-config • Das Shellscript ./bin/mklib 'QNX') 15 vgl. [Mesa] 26 LIBNAME="lib${LIBNAME}.a" echo "mklib: Making QNX library: " ${LIBNAME} ar -ruv ${LIBNAME} ${OBJECTS} FINAL_LIBS=${LIBNAME} ;; Änderungen in der Datei ./bin/mklib Da es keine offizielle QNX-Photon Unterstützung von Mesa gibt, wird die Library als eine Unix-Variante erstellt. Hierbei ist das Problem, dass Mesa bei einem Unix-Build die CHeader-Dateien von X11 benötigt. Diese Header-Dateien können, falls nicht bereits vorhanden, aus dem Open-Source XFree86-Distribution für QNX nachträglich installiert werden. Alternativ kann die Mesa-Library auch als Offscreen-Version „gebaut“ werden. In diesem Fall sind die X11-Header nicht mehr erforderlich. Für die SH4-Offscreen-Version sind folgende Änderungen nötig. qnx: $(MAKE) $(MFLAGS) -f Makefile.X11 targets \ "GL_LIB = libGL.so" \ "GLU_LIB = libGLU.so" \ "GLUT_LIB = libglut.so" \ "GLW_LIB = libGLw.so" \ "OSMESA_LIB = libOSMesa.so" \ "CC = qcc" \ "CFLAGS = -O -Y_ecpp-ne -DDEFAULT_SOFTWARE_DEPTH_BITS=31 \ -Vgcc_ntoshle" \ "APP_LIB_DEPS = -lGLU -lGL -lm" Änderungen in der Datei Make-config für OS-Mesa und SH4 2.2.2 OpenGL Photon Anbindung Die Library für die OpenGL-X-Window Anbindung, GLX, verwendet für die Darstellung einer von OpenGL gerenderten Grafik drei Funktionen (vgl. Listing 1): • • • CreateContext MakeCurrent SwapBuffer Für die Problemlösung ist es nahe liegend, aus dem Sourcecode von GLX die entsprechenden Funktionen zu extrahieren und zu portieren. Ein Blick in den Sourcecode von Mesa sowie in die GLX Library zeigt jedoch, dass die Implementierung recht komplex und umfangreich ist. Eine Portierung in einem vernünftigen Zeitrahmen war daher nicht möglich. Alternativ kann man die Offscreen-Version von Mesa verwenden. Die Idee dabei ist anstatt die Grafik direkt auf einem Ausgabefenster anzuzeigen, das Ergebnis im Offscreen-Modus in einen Image-Buffer zu rendern. Dieser Buffer wird anschließend über normale PhotonFunktionen auf dem Bildschirm angezeigt. Diese Methode ist sicherlich nicht besonders effizient, aber portabler und einfacher zu realisieren. Für die Implementierung des Prototyps wurde daher diese Methode verwendet. Liegt eine Grafik in einem Image-Buffer vor, so gibt es mehrere Möglichkeiten die Grafik auf einem QNX-Photon Fenster anzuzeigen. 27 • Label-Widget Die einfachste Möglichkeit, ein Image auf einem Photon-Fenster anzuzeigen, ist die Verwendung einees Label-Widget mit einem Image als Inhalt. Solch ein Widget kann mit dem Photon-GUI-Builder erstellt werden. Im Programm wird eine neue Image-Struktur mit dem Inhalt der gerenderten Grafik dem Label-Widget zugewiesen. Der folgende Code zeigt ein Beispiel. PhImage_t *img; img = PhCreateImage(NULL,width,height,Pg_IMAGE_DIRECT_8888,NULL,0,0); ... img->image = (char*) myOpenGLBuffer; PtSetResource( ABW_3Dlabel, Pt_ARG_LABEL_IMAGE, img, 0); PtFlush(); ... Label-Widget Beispiel • Memory und Shared Memory Blit16 Bei dieser Methode wird die Funktion PgDrawImage verwendet. Im Gegensatz zu der ersten Methode wird das Image nicht über die Widget-Library, sondern direkt zum Photon-Window-Manager geschickt. Ein Image kann beispielsweise wie folgt angezeigt werden: unsigned short *image; // 16 bit RGB565 Data ... image = (unsigned short*) malloc(width * height * 2); ... PhPoint_t pos = {0, 0}; PhDim_t size = {width, height}; PgDrawImage(image, 0, &pos, &size, pitch, 0); pgFlush(); Memory Blit Beispiel Unter QNX läuft das Window-System und die Applikation in unterschiedlichen Prozessen. Für die Anzeige muss das Image daher komplett von der Applikation zum Window-System/Grafik-Treiber in Form von Messages übertragen werden. Um eine bessere Performance zu erzielen, bietet Photon u.a. die Funktion PgDrawImagemx() an, die Shared-Memory als Image-Buffer verwendet, Dadurch entfällt die zeitaufwendige Übertragung der Image-Daten. Ein Shared-Memory-Buffer kann mit der Funktion PgShmemCreate() erstellt werden, Details dazu siehe [Photon]. unsigned short *image; // 16 bit RGB565 Data ... image = (unsigned short*) PgShmemCreate(width * height * 2, NULL); ... PhPoint_t pos = {0, 0}; PhDim_t size = {width, height}; PgDrawImagemx(image, 0, &pos, &size, pitch, 0); pgFlush(); 16 vgl . [Feh04] 28 Shared Memory Blit Beispiel mit PdDrawImagemx Prinzipiell wird das Image immer noch zum Window-Manager kopiert. Der Vorteil ist allerdings, dass das Kopieren des Images aus dem Shared-Memory schneller vonstatten geht. Zu beachten ist dabei, dass der Shared-Memory nach der Benutzung wieder freigegeben wird, um „Shared-Memory-Leak“ zu vermeiden. Die aktuell im System belegten Shared-Memories sind im Device-Verzeichnis /dev/shmem zu sehen. • Video Memory Blit Eine weitere Methode um die Performace zu steigen, ist die Verwendung von VideoSpeicher als Image-Buffer. Dadurch entfällt der externe Kopier-Vorgang des ImageBuffers. Der folgende Code zeigt die benötigten Funktionen. unsigned short *image; // 16 bit RGB565 Data PdOffscreenContext_t *ctx; ... ctx = PdCreateOffscreenContext(0, width, height, Pg_OSC_MEM_PAGE_ALIGN); image = PdGetOffscreenContextPtr(ctx); ... PhRect_t rect = { {0, 0}, {width, height} }; PgContextBlit(ctx, &rect, NULL, &rect); pgFlush(); Video Memory Blit Beispiel Die Verwendung von Video-Speicher hat jedoch einige Einschränkungen. Zum Beispiel ist der Video-Speicher in der Regel auf einer relativ kleine Größe beschränkt. Eine Art „virtueller Video-Speicher“ um den Speicher zu vergrößern existiert nicht. Außerdem ist das Anlegen bzw. Initialisieren von Video-Speicher sehr zeitaufwendig. • Video Memory Blit im Direct-Mode Eine weitere, wenn auch nur minimale Performance-Steigerung kann durch die Grafik-Ausgabe im Direct-Mode erzielt werden. unsigned short *image; // 16 bit RGB565 Data PdOffscreenContext_t *ctx; ... PdDirectContext_t *direct_ctx; direct_ctx = PdCreateDirectContext(); PdDirectStart(direct_ctx); ... ctx = PdCreateOffscreenContext(0, width, height, Pg_OSC_MEM_PAGE_ALIGN); image = PdGetOffscreenContextPtr(ctx); ... PhRect_t rect = { {0, 0}, {width, height} }; PgContextBlit(ctx, &rect, NULL, &rect); pgFlush(); Direct-Mode Beispiel Der Window-Manager wird in diesem Fall deaktiviert und das Anwendungsprogramm hat die alleinige Kontrolle über den Grafik-Device. Die 29 Konsequenz davon ist, dass die Maus sowie alle vom Window-Manager verwalteten Eingabe-Geräte ebenfalls deaktiviert werden. Für die Implementierung der glICM-Funktionen wurden die letzten beiden Methoden gewählt. Die Auswahl von Direct-Mode erfolgt über ein Parameter beim Aufruf der createContext() Funktion. Listing 2 zeigt ein Auszug davon. ... int glICMCreateContext(int width, int height, int directMode) { ... // Start Direct Mode if (directMode) { // Create the direct context direct_context = PdCreateDirectContext(); PdDirectStart(direct_context); } else { glICMInitPhotonWindow(); } ... // Create the offscreen context buff=PdCreateOffscreenContext(0, screenWidth, screenHeight+1, Pg_OSC_MEM_PAGE_ALIGN | Pg_OSC_MEM_2D_WRITABLE); ... OSMesaBuffer = (unsigned char *) PdGetOffscreenContextPtr(buff); glICMOSMesaInit(); return; } int glICMOSMesaInit(void) { OSMesaCtx = OSMesaCreateContextExt( OSMESA_RGB_565, 8, 8, 8, NULL ); ... } int glICMMakeCurrent() { if (!OSMesaMakeCurrent( OSMesaCtx, OSMesaBuffer, GL_UNSIGNED_SHORT_5_6_5, screenWidth, screenHeight )) { printf("OSMesaMakeCurrent failed!\n"); return 0; } return 1; } void glICMSwapBuffer(void) { glICMSwapImage(OSMesaBuffer, screenHeight, screenWidth, 2); PgContextBlit(buff,&rect, NULL, &rect); PgFlush(); } Listing 2 Implementierung der glICM Funktionen 2.2.3 3D GUI Als Animation soll eine aufklappbare Bedientafel realisiert werden. Nach dem Aufklappen soll eine Instrumententafel erscheinen. Abbildung 17 zeigt den schematischen Aufbau. 30 +y +y +x -x +x +z +z (a) (b) Abbildung 17 (a) GUI in der normalen Stellung. (b) GUI in der aufgeklappten Stellung Die Bedien- und Instrumententafel werden in OpenGL als GL_QUAD mit jeweils vier Punkten definiert und mit den dazugehörigen Texturen überzogen. 2.3 Diskussion Die implementierte GUI ist eine einfache OpenGL-Anwendung und zeigt praktische Beispiele der OpenGL-Programmierung. Das definierte Ziel, eine 3D-GUI mit OpenGL zum Demonstrationszweck zu entwickeln und in ein Automotive-Framework zu integrieren, wurde erreicht. Die erfolgreiche Portierung von Mesa war ausschließlich auf Anpassungen des Buildscripts beschränkt. Das zeigt, dass der C-Code von Mesa plattformunabhängig geschrieben wurde. Auch die ausgereifte Cross-Compile-Technologie von QNX / GNU wird hier eindrucksvoll bestätigt. Zwischen der x86 und SH4 Version wurden selbst bei einer derart komplexen Library keine Unterschiede festgestellt. Die selbstentwickelte glICM-Funktion hat nur prototypischen Charakter und dient hier lediglich als Ersatz für das fehlende GlPh-Framework. Durch diese Realisierung wurden gute Einblicke in das Window-System Photon gewonnen. Die im Target eingesetzte Grafik-Chip, ein Silicon Motion Lynx3DM, ist hardwareseitig bereits mit multimedia- und 3D-Beschleunigungsfunktionen ausgestattet. Leider ist eine hardwarebeschleunigte OpenGL-Implementierung nur als kommerzielle Individuallösung erhältlich. Ein Blick in deren Datenblätter lassen interessante Möglichkeiten vermuten. Eine Gegenüberstellung der Applikationsgröße mit und ohne 3D-Grafik zeigt Tabelle 1. Auf dem Target beträgt der Mehrverbauch an Speicher etwa 1,8 MB. 31 SH4 SH4 mit Debug-Option X86 X86 mit Debug-Option Mit statisch gelinken Mesa-Libraries 3,7 MB 6,8 MB 3,5 MB 7,1 MB Ohne 3D-Grafik 1,9 MB 5,0 MB 1,9 MB 5,7 MB Tabelle 1 Gegenüberstellung der Applikationsgröße mit und ohne 3D-Grafik Die in diesem Kaptitel portierten und implementierten OpenGL-Funktionen dienen als Basis für die Untersuchungen in den Kapiteln 3, 4 und 5. Insbesondere in Kapitel 3 wird eine komplexerer OpenGL-Anwendung, die die 3D-Engine wesentlich mehr belastet, auf das Target portiert und untersucht. 32 3 Geländedarstellung Haupteinsatzgebiet von Echtzeit 3D-Grafik in einem InCar-Multimedia-System ist die Darstellung von Navigationskarten. Bisherige Systemen haben meistens einen 3D-Modus, der jedoch nur 2D-Karten in einer Perspektivendarstellung anzeigt. Das liegt einerseits daran, dass das kommerziell verfügbare Kartenmaterial nur als 2D-Information vorliegt, andererseits hat die Grafikfähigkeit der Hardware Grenzen. Eine Perspektiven-2D-Darstellung trägt zwar zu einer besserer Orientierung des Benutzers bei, jedoch erhöht sich der Informationsgewinn für ihn, wenn die Straßenkarte in einem 3D-Gelände integriert visualisiert wird. Im Rahmen dieser Arbeit soll daher ein geeigneter Gelände-Visualisierungsalgorithmus prototypisch auf dem Target implementiert werden. Ziel der Untersuchung ist zu evaluieren, inwieweit ein solches Visualisierungsverfahren, das in der Regel für High-End-Workstation konzipiert ist, auf dem Target angewendet werden kann. In Kapitel 3.1 wird zunächst auf die Probleme und Grundlagen der Gelände-Visualisierung eingegangen. Die Implementierung des Verfahrens wird in Kapitel 3.2 erläutert. Die Ergebnisse der Evaluierung werden schließlich in Kapitel 3.3 zusammengefasst und diskutiert. 3.1 Grundlagen Als Beispiel wird ein Gelände mit der Größe von 100.000m x 100.000m, das in 1m Auflösung dargestellt werden soll, betrachtet. Die Höhendaten liegen, wie im Bereich der Gelände-Visualisierung üblich, in einer sog. Heightmap vor. Eine Heightmap ist ein zweidimensionales Array, in dem die Höhendaten als Wert gespeichert sind. Dabei entsprechen die Indizes den x und y Koordinaten des Geländes. Die einfachste Möglichkeit, das Gelände darzustellen, ist ein quadratisches Dreieck-Gitter aufzubauen (Abbildung 18). Abbildung 18 Darstellung eines Geländes mit einem quadratischen Dreieck-Gitter Zwei Probleme treten bei diesem Beispiel auf. Erstens, das Gitter ist mit einer Größe von ca. 120GB17 ziemlich groß. Zweitens, für die Darstellung des Geländes werden ca. 20 Milliarden18 Dreiecke benötigt, die dann von der 3D-Engine gerendert werden. 17 18 100.000 · 100.000 · 3 Punkte · 4 Byte für Float-Variable pro Punkt Formel: (n -2)2 · 2 mit n = Anzahl der Punkte 33 Für das erste Problem hilft nur, eine niedrigere Auflösung zu nehmen. Ein 1k x 1k Gitter ist für die heutige Hardware eine gängige Größe. Das zweite Problem kann durch den Einsatz von einem LOD (Level of Detail) Algorithmus effektiv gelöst werden. Das Grundprinzip eines LOD Algorithmus ist, das Originalgelände (repräsentiert durch eine Heightmap oder eine vergleichbare Struktur) durch ein entsprechend vereinfachtes Gitter zu approximieren. Das generierte Gitter soll dabei so wenig Punkte/Dreiecke wie nötig enthalten und gleichzeitig so viele Details wie möglich darstellen. Abbildung 19 zeigt den Idealfall; eine ebene Fläche kann durch einige wenige Dreiecke dargestellt werden. (a) (b) Abbildung 19 (a) Darstellung als vollständiges Gitter. (b) Darstellung mit LOD LOD Verfahren werden in zwei Gruppen eingeteilt: diskrete und kontinuierliche LOD Verfahren. Diskrete Verfahren arbeiten mit einer festen Anzahl von statischen Modellen/Gittern mit unterschiedlichen Auflösungen. Werden zur Laufzeit mehr Details gefordert, so wird das Gitter mit der nächst höheren Auflösung eingeblendet. Solche Verfahren kommen meistens zum Einsatz, wenn LOD auf 3D-Objekte angewendet werden. Für die Darstellung von Flächen und Geländen sind die CLOD (continuous LOD) Verfahren besser geeignet. Bei CLOD gibt es keine festen Detailstufen, die Detaillierung des Gitters wird dynamisch generiert. Eine typische Vorgehensweise ist die Darstellung des Gitters als Quadtree, um darauf eine rekursive Verfeinerung auf die einzelnen Teil-Quadranten des Gitters vorzunehmen (siehe Abbildung 20). Abbildung 20 Quadtree Hierarchie Um zu entscheiden, ob ein betroffener Quadrant weiter verfeinert wird, wird ein sog. „Vertex Interpolation Error“19 berechnet. Überschreitet der Vertex-Interpolation-Error einen bestimmten Schwellenwert, so wird dieser Quadrant rekursiv verfeinert. Je nach Algorithmus werden für den Vertex-Interpolation-Error unterschiedliche Aspekte berücksichtigt. Im einfachsten Fall ist es der Höhenunterschied des Punktes zwischen den einzelnen DetailLevels (siehe Abbildung 21). Auch die Entfernung des Punktes zum Betrachter wird häufig in die Berechnung einbezogen; je weiter die Punkte entfernt liegen, desto weniger Details sind erforderlich. 19 vgl [Ulr00] 34 Abbildung 21 Höhenunterschied zwischen Original-Punkt und der approximierten Stelle (weißer Punkt). Für eine flüssige Animation, wie etwa bei einem Flugsimulator, ist eine Frame-Rate von mindesten 25 Frames pro Sekunde erforderlich. Um solche Anforderung zu erfüllen, arbeiten die meisten LOD-Verfahren mit einem dynamischen Schwellenwert für den VertexInterpolation-Error. Diese Fehlerschranke steuert direkt den Detaillierungsgrad der Darstellung. Dauert die Berechnung von einem Frame zu lange, so kann man für den nächsten Frame die Detaillierung verringern. Damit ist eine ruckfreie Animation gewährleistet. 3.1.1 Der SOAR (Stateless One-pass Adaptive Refinement) Algorithmus Für die Evaluierung wurde eine auf dem SOAR-Algorithmus basierte Implementierung auf dem Target adaptiert. Das SOAR-Verfahren wurde von Peter Lindstrom und Valerio Pascucci an der Lawrence Livermore National Laboratory20 entwickelt und zählt zu den am häufigsten eingesetzten LOD-Verfahren. Im Gegensatz zu anderen vergleichbaren Verfahren wird bei SOAR das Gitter für jeden Frame komplett neu generiert. Das Kernstück des Verfahrens ist der Verfeinerungs-(Refinement)Algorithmus. Dieser basiert auf der „longest edge bisection“, wobei ein Dreieck beim Verfeinern mittig durch seine Hypotenuse in zwei gleich große, rechtwinklige Teil-Dreiecke aufgeteilt wird (Abbildung 22). Abbildung 22 Longest edge bisection Diese Teilung wird sehr häufig bei LOD-Verfahren angewendet und hat zwei entscheidende Vorteile. Erstens, die Teilung kann völlig lokal und unabhängig vom gesamten Gitter durchgeführt werden. Zweitens, die durch die Teilung entstandenen Punkte ergeben sich wieder zu einem Quadrat; das Texture-Mapping beim Rendern wird dadurch vereinfacht. Ausgangpunkt der Verfeinerung ist ein Gitter mit vier Dreiecken bestehend aus den vier Eckpunkten und einem Mittelpunkt (siehe Abbildung 23a), welches die gesamte Geländefläche repräsentiert. Ausgehend von dieser Stufe wird das Gitter rekursiv per EdgeBisection in kleineren Dreiecken aufgeteilt (Abbildung 23b,c). Die Eckpunkte der so entstandenen Dreiecke werden als neue Punkte in das Gitter aufgenommen. Die Entscheidung über die Teilung/Verfeinerung eines Teil-Quadranten erfolgt, wie bereits erwähnt, über die 20 vgl. [Lin01] 35 Auswertung des Vertex-Interpolation-Errors. Eine ausführliche Darstellung des Verfahrens ist in [Lin01] beschrieben. (a) (b) (c) Abbildung 23 Rekursive Verfeinerung Eine Besonderheit der eingesetzten Implementierung ist, dass die Heightmap-Daten direkt als Vertex-Array (Array von 3D-Koordinaten) an OpenGL mit der Funktion glArrayPointer() übergeben werden. Das Aufnehmen eines neuen Punktes bei der Verfeinerung in dem generierten Gitter bedeutet einfach das Hinzufügen des Indizes dieses Punktes in die IndexListe. Diese Liste wird dann zum Rendern des Geländes an OpenGL übergeben21. Damit ist eine Speicherung der Gitterpunkte etwa in Form von Quadtree nicht mehr erforderlich. Der Ablauf des Verfeinerungsalgorithmus ist in Abbildung 24 schematisch dargestellt. 21 vgl. Kapitel 2.1.2 36 Abbildung 24 Schematischer Ablauf des Verfeinerungsalgorithmus Die Schritte sind folgendermaßen: • Starten der Verfeinerung mit einem Gitter wie in Abbildung 23a. • In der Verfeinerungsfunktion wird zuerst der Vertex-Interpolation-Error des Mittelpunktes r, siehe (1), ermittelt. Ist der Error kleiner als der Schwellenwert, dann ist eine Verfeinerung nicht erforderlich; die Rekursion kann abgebrochen werden. • Beginn der Verfeinerung. • Ist eine Verfeinerung am Punkt p1 erforderlich? D.h. Fehler am Punkt p1 > Schwellwert. • Ist eine Verfeinerung am Punkt p2 erforderlich (3)? • Wenn ja, Quadrant A rekursiv verfeinern (4). Nach der Verfeinerung können hier zwei Fälle entstehen: • Wenn Quadrant A verfeinert wurde, dann sind alle darin enthaltenen Punkte bereits in das Gitter aufgenommen (5). Keine weitere Aktion ist erforderlich. 37 • • • • Wurde aber die Verfeinerung nicht durchgeführt, weil der Fehler zu klein ist, so ist es auf jeden Fall sicher, dass der Quadrant A, wie in (6) dargestellt, gerendert wird. Ist Verfeinierung am Punkt p3 erforderlich (7)? • Quadrant B rekursiv verfeinern (11). Nach der Verfeinerung können hier zwei Fälle entstehen ... Wenn am Punkt p2 nicht verfeinert wird (3), dann erfolgt die Abbildung der Dreiecke wie in (10). • Verfeinerung am Punkt p3 ... Am Punkt p1 wird nicht verfeinert. D.h. die Darstellung erfolgt wie in (8). • Fortfahren mit der Prüfung am Punkt p2...(9) Es gibt somit insgesamt 16 Fall-Unterscheidungen für das Verfeinern eines Quadranten. 3.2 Implementierung Die als Basis herangezogene Experiment-Implementierung liegt als lauffähiges C/C++Programm unter Windows vor. Für die Anbindung von OpenGL an das Window-System wurde SDL eingesetzt. Die Heightmap liegt in der Auflösung von 1025 x 1025 in BMP Format vor. Eine Texture-Grafik für das Gelände mit einer Auflösung von 1024 x 1024 (siehe Abbildung 25) sowie Bilder für den Hintergrund (Skybox) sind ebenfalls vorhanden. Eine Skybox ist ein überdimensionierter Würfel, welcher mit sechs vordefinierten Texturen um die gesamten 3D-Szene herum gezeichnet wird und bildet damit einen nahtlosen Panoramahintergrund für die 3D-Szene. (a) (b) Abbildung 25 Für die Geländedarstellung: (a) Textur. (b) Heightmap Bei der Portierung eines OpenGL Programm auf das Target-System ist grundsätzlich zu beachten: • Sicherstellen, dass der Code unter QNX syntaktisch und vor allem inhaltlich korrekt arbeitet. • Die Window-System abhängigen Funktionen auf QNX-Photon umstellen. In diesem Fall sind es die Funktionen aus der SDL-Library, die nicht auf dem Target verfügbar sind. Diese Funktionen sind durch die in Kapitel 2.2.2 entwickelten Funktionen zu ersetzen. 38 Abbildung 26 zeigt den Ablauf des Programms in der Initialisierungsphase (a) und in der Renderingsphase (b). soar soa init() OpenGL display() initOpenGL() drawTerrain() refineTerrain() loadTexture() calculateTree() loadHeightMap() rekursiver Aufruf glDrawElements() processTerrain() drawSkybox() glVertex3f() (a) Initialisierungsphase (b) Darstellungsphase Abbildung 26 Ablaufdiagramm des Gelände-Darstellungsprogramms 3.2.1 Funktionsbeschreibung Im Folgenden werden die wichtigsten Funktionen beschrieben. • Funktionen display(void) ist die Haupt-Display-Funktion, hier werden die „draw“-Funktionen für Skybox, Terrain usw. der Reihe nach aufgerufen. • Funktionen drawTerrain(void) Hier wird zuerst die Funktion refineTerrain() zum Generieren des Gitters aufgerufen. Das Ergebnis wird in dem Array guiTerrainIndexList gespeichert. Die zu zeichnenden Dreiecke werden jeweils durch drei Einträge (drei Eckpunkte) aus diesem Array dargestellt. Ein Element dieses Arrays enthält nicht die 39 Koordinaten eines Punktes, sondern einen Index auf das eigentliche Vertex-Array. Mit dem OpenGL-Befehl glDrawElements(GL_TRIANGLES, iNmbPrimitiveNodes, GL_UNSIGNED_INT, guiTerrainIndexList); wird das Gitter schließlich gerendert. Durch das Flag GL_TRIANGLES wird OpenGL mitgeteilt, dass es sich hier um Dreiecke handelt. Die Anzahl der Dreiecke wird in dem Variable iNmbPrimitiveNodes gespeichert. • Funktionen refineTerrain(void) Die Verfeinerungsfunktion calculateTree() wird hier mit den Initialwerten gestartet. Für cI ist der Initialwert der Mittelpunkt des gesamten Geländes. • Funktionen calculateTree(int cI, int deltaX, int deltaY) Der bereits erläuterte Verfeinerungsalgorithmus ist hier implementiert. Dabei ist cI der Index des Mittelpunktes R, deltaX und deltaY der Index-Offset bis zum Mittelpunkt der Teil-Quadranten. Durch das Programmieren der 16 Fallunterscheidungen ist diese Funktion ziemlich groß und unübersichtlich geraten. • Funktionen preprocessTerrain(void) Die für die Fehler-Ermittlung zum Teil benötigten Daten werden hier in der Initialisierungsphase einmalig vorgerechnet und in den folgenden Arrays float *fErrorMatrix; float *fRadiiMatrix; abgespeichert. • Funktionen loadHeihtMap(void) In dieser Funktion wird das Heightmap eingelesen. Die zum Lesen der BMP-Datei verwendete SDL-Funktion SDL_LoadBMP wurde durch eine selbst entwickelte Version ersetzt (SDL war auf dem Target-System nicht verfügbar). Nach dem Einlesen der Daten werden Speicher für die globalen Arrays GLfloat *gfTerrainVertexList; GLfloat *gfTerrainColorList; GLfloat *gfTerrainTexCoordList; reserviert. Das Array gfTerrainVertexList enthält die x, y und z Koordinaten des Originalgitters. Während die y Werte direkt aus dem Heighmap abgegriffen werden können, muss für die x und z Werte eine Skalierung mit einem konstanten Faktor TERRAIN_SCALE_FLAT durchgeführt werden. Das Array gfTerrainColorList bekommt die Farbinformation zugewiesen und wird sichtbar wenn das TextureMapping ausgeschaltet ist. Im Array gfTerrainTexCoordList sind schließlich die 2D-Koordinaten für das Texture-Mapping gespeichert22. • 22 Funktionen loadTextures(void) vgl. Kapitel 2.1.2.8 40 Die Bilder für die Textures werden hier eingelesen (txtImg[])und mit den folgenden OpenGL Befehlen aktiviert. glTexImage2D(GL_TEXTURE_2D, ... GL_RGBA, GL_UNSIGNED_BYTE, txtImg[run1]->pixels); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); • Funktionen drawSkybox(void) Die Panorama-Hintergrundbilder werden hiermit gezeichnet. Eine Skybox wird in OpenGL als ein Würfel um das darzustellende Objekt (hier das Gelände) herum modelliert. Die Hintergrundbilder werden als Texture auf die sechs Innenseite des Würfels projiziert. Die Skybox dient hier nur der besseren Optik und kann optional abgeschaltet werden. • Funktionen main() Im Hauptprogramm kann schließlich ein Flug oder eine Fahrt durch das Gelände simuliert werden. Dabei werden in einer Schleife die Koordinaten der KameraPosition frameweise verändert. Eine typische Steuerung sieht wie folgt aus. for(i=0; i<15; i++) { moveCamera(0.0, 0.0, -10.0, 0.0, 0.0, 0.0); display(); glICMSwapBuffer(); } void moveCamera(float X, float Y, float Z, float angle_X, float angle_Y, float angle_Z) { sCamera.angle_X += angle_X; sCamera.angle_Y += angle_Y; sCamera.angle_Z += angle_Z; sCamera.coord_X_fly += X; sCamera.coord_Y_fly += Y; sCamera.coord_Z_fly += Z; } display() { ... glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluPerspective(90.0f, ratio, 0.1f, 100000.0f); // Rotete and translate the View glRotatef(sCamera.angle_X, 1.0f, 0.0f, 0.0f); glRotatef(sCamera.angle_Y, 0.0f, 1.0f, 0.0f); glRotatef(sCamera.angle_Z, 0.0f, 0.0f, 1.0f); glTranslatef(-sCamera.coord_X_fly, -sCamera.coord_Y_fly, sCamera.coord_Z_fly); ... } C-Code für die Kamera-Steuerung 41 Die folgende Abbildungen zeigen die Ergebnisse des implementierten Programms. Abbildung 27 Blick auf das Gelände mit einem Symbol für die Navigation und einer eingezeichneten virtuellen Strasse. (a) (b) Abbildung 28 Screenshot aus unterschiedlichen Winkeln. Mit Texturen und Skybox. (b) Als Gittermodell 3.2.2 Probleme bei der Implementierung Eine Portierung verläuft selten ohne Probleme. Insgesamt ist es schwierig bei einem derart komplexen Algorithmus den Überblick zu behalten. Stößt man auf einen Fehler, so stellt sich als erstes die Frage, welche Komponente diesen Fehler verursacht hat. Die Mesa-Library ist zwar ausgereift, hat aber offiziell keine QNX-Unterstützung. Die selbstentwickelten glICM – Funktionen ist auch eine mögliche Fehlerquelle, ebenso der LOD-Algorithmus, der hier nur in einer experimentellen Form vorliegt. Selbst wenn alles auf einem X86-basierten QNX-Rechner fehlerfrei läuft, muss das Programm noch auf dem Target getestet werden. Auch hier treten oft Probleme auf. Das Fehlen von bestimmten Runtime-Libraries beispielsweise ist noch einfach zu beheben. 42 Komplizierter ist es, wenn sich die Grafikhardware anders als auf einem X86-PC verhält. Im Folgenden wird auf einige dieser Probleme eingegangen. • Einlesen des Heightmap Auch wenn ein Heighmap in einem Bild-Format vorliegt (.BMP), ist das nicht mit der Existenz eines Bildes gleichzusetzen. Auf der Suche nach der passenden Gittergröße werden Heightmaps mit Hilfe eines Zeichenprogramms vergrößert, verkleinert oder bearbeitet. Hat man beim Abspeichern ein anderes Format oder eine andere Farbauflösung (Graustufe, RGB, RGBA, 16, 24, 32 Bit) aus Versehen gewählt, entstehen „verfälschte“ Landschaften. • Falsche Skalierung des Geländes Das in Abbildung 29 gezeigte Bild war das erste sichtbare Ergebnis. Lange wurde gerätselt, ob das nun ein Gelände ist oder doch nur ein zufälliges Bit-Muster im Framebuffer. Nach einer langen Debug-Session hat sich herausgestellt, dass die Skalierung auf der X-Achse bei diesem Bild verstellt war. Abbildung 29 Fehlerhaft dargestelltes Gelände • Verhalten der Target-Grafikhardware Auf dem Target muss der Offscreen-Context23 im Direct-Mode mindestens um eine Pixel-Zeile größer als der Image-Buffer definiert werden. D.h. bei einer ImageBuffer-Größe von 320 x 96 Pixel muss der Offscreen-Context mit 320 x 97 Pixel initialisiert werden. Dieses Verhalten ist auf der QNX-X86 Plattform nicht zu beobachten. Die Ursache dafür ist bisher unbekannt. 3.3 Evaluierung und Diskussion Um die Leistungsfähigkeit des Algorithmus zu überprüfen, wurden einige Tests durchgeführt. • Laufzeitverhalten Die Laufzeiten bezüglich der Größe des Gitters und des eingestellten Fehlers werden bei diesem Test gemessen. Dabei wird das Gelände 30 mal gerendet und jeweils der Mittelwert für Frames pro Sekunden (FPS), Anzahl der benötigten Dreiecke (Triangles) und Anzahl der verfeinerten Quadranten (Nodes) gebildet. Die Kamera wird mittig im Gelände positioniert und bei jedem Frame um 5 Grad nach links gedreht. Diese Messung wird jeweils für den Fehler 8, 16, 32 und 64 durchgeführt. Die Ergebnisse für ein 256x256 Gitter sind in Tabelle 2 und für ein 512x512 Gitter in Tabelle 3 aufgeführt. 23 Vgl. Kapitel 2.2.2 43 Error 8 16 32 64 Gitter: 256 x 256 FPS Triangles 4,274 1647 4,295 1055 4,407 824 4,467 630 Nodes 508 326 255 197 Tabelle 2 Messergebnisse bei einem 256x256 Gitter Error 8 16 32 64 Gitter: 512 x 512 FPS Triangles 3,863 2056 4,087 1396 4,278 1007 4,377 761 Nodes 644 438 313 235 Tabelle 3 Messergebnisse bei einem 512x512 Gitter Die Ergebnisse zeigen, dass durch das Erhöhen der Fehler-Schranke die Anzahl der Dreiecke und Rekursionen verringert und gleichzeitig die Frame-Rate erhöht wird. Das war zu erwarten. Der Performance-Gewinn an Frame-Rate fällt aber verhältnismäßig gering aus. Vergleicht man in Tabelle 3 die Werte aus der ersten und vierten Zeile, so stellt man Folgendes fest: Obwohl das System 63% weniger Dreiecke zu rendern hat, ist die Frame-Rate nur um 14% gestiegen. D.h. die GrafikAusgabe auf dem Display verursacht prinzipiell eine hohe Grundlast. • Speicherverbrauch Der Speicherverbrauch des Algorithmus ist nicht von der Laufzeit-Komplexität (Anzahl der Rekursionen und Verfeinerungsstufen) abhängig. Daher kann eine Abschätzung analytisch erfolgen. Bei einer n x n Gitter werden benötigt: // xyz pro vertex VertexList: (n+1)*(n+1)*3 * sizeof(GLfloat) // rgb pro vertex ColorList: (n+1)*(n+1)*3 * sizeof(GLfloat) // uv pro vertex TexCoordList: (n+1)*(n+1)*2 * sizeof(GLfloat) // 1 pro vertex ErrorMatrix: (n+1)*(n+1) * sizeof(float) // 1 pro vertex RadiiMatrix: (n+1)*(n+1) * sizeof(float) // max Quads * 2 * 3, (2 tri pro quad, 3 vertices pro tri) IndexList: n*n*2*3 * sizeof(GLuint) Zusammengefasst ergibt sich: 44 = ( 10 · (n + 1)2 + 6 · n2 ) · sizeof(float) = ( 16 · n2 + 20 · n + 10 ) · sizeof(float) Der Speicherbedarf beträgt bei einer Float-Variable-Größe von 4 Byte: Gitter-Größe 512 x 512 ≈ 16 MB. Gitter-Größe 1024 x 1024 ≈ 67 MB . Ein Test mit einem 1024x1024 Gitter auf dem Target ist daher aufgrund von Speicherproblemen gescheitert. Zusammenfassend lässt sich sagen, dass das Programm erfolgreich auf dem Target portiert wurde und zufrieden stellend arbeitet. Die Ergebnisse bestätigen die eingangs erwähnte Notwendigkeit des LOD-Verfahrens. Die Feineinstellung der Detaillierungsgenauigkeit durch den Vertex-Interpolation-Error führt zu den erwarteten Veränderungen der zu verarbeiteten Dreiecke. Diese tragen allerdings nicht entscheidend zum Gesamtergebnis bei, da die GrafikAusgabe auf dem Bildschirm verhältnismäßig lang dauert. Ein vollwertiges 3D-Navigationssystem lässt sich dadurch aber noch nicht realisieren. Soll eine 2D-Strassenkarte (3D-Strassenkarte sind im Moment noch nicht erhältlich) auf einem derartig gerenderten Gelände abgebildet werden, so treten einige Probleme auf. Abbildung 30 zeigt eine vektorisierte Strasse auf einem Geländegitter. Hier wird deutlich, dass neue Punkte auf dem Gelände entstehen. Die Höhe (y-Koordinaten in OpenGL) an diesen Punkten lässt sich bei einem statischen Gitter noch verhältnismäßig einfach mathematisch bestimmen. Aber bei einem dynamischen LOD-Verfahren ändert sich die Geometrie des Gitters ständig. Außerdem müssen die neuen Punkte in dem Verfeinerungsalgorithmus berücksichtigt werden. Vektorisierte Strasse Neue Punkte Abbildung 30 Vektorisierte Strasse auf einem Gelände-Gitter. Die neu entstandenen Punkte sind in blau gekennzeichnet. Generell müssen 3D-Objekte, wie Städte und Gebäude bei der Visualisierung berücksichtigt werden. Bei zunehmender Anzahl und Komplexität von Objekten ist der Einsatz von SzenenGraphen sinnvoll und erforderlich. Ob diese Techniken auf dem Target anwendbar sind, ist allerdings ungeklärt. 45 Diese und einige andere Probleme müssen noch untersucht werden. Geeignete oder spezialisierte Algorithmen sind zu evaluieren oder müssen noch entwickelt werden. 46 4 OpenGL Java-Binding Bisherige Systeme im Automotive-Bereich werden in der Regel in C/C++ entwickelt. Mit zunehmender Leistungsfähigkeit der eingesetzten Hardware hat die Programmiersprache Java auch im Embedded-Bereich Einzug gehalten. So schreiben manche Automobil-Hersteller beispielsweise vor, dass die HMI komplett in Java zu entwickeln ist. Es existieren einige OpenGL Java-Binding Implementierungen; sie sind aber bisher nur auf Windows- und UnixPlattformen verfügbar. Für die Evaluierung auf der SH4-QNX Plattform ist daher eine Portierung erforderlich. Die wichtigsten OpenGL Java-Binding sind: • JOGL24 (JSR25 231) JOGL wurde ursprünglich von Ken Russel und Chris Kline entwickelt und im August 2003 veröffentlich. Die Firma Sun und SGI haben durch eine Zusammenarbeit JOGL als die offizielle Referenz-Implementierung für den Standard JSR231 aufgenommen. Bis zu endgültiger Verabschiedung von JSR231 wird JOGL wohl als quasi Standard angesehen. • Java3D26 (JSR 189) Java3D ist sei Jahren die von SUN offiziell favorisierte 3D-API. Im Gegensatz zu anderen „Eins zu Eins“ OpenGL-Binding bietet Java3D ein vollständig objektorientiertes Programmiermodell für 3D-Objekte mit umfangreichen HighLevel Funktionen. Seit 2004 ist Java3D Open-Source, damit ist unklar, ob SUN die Entwicklung wie bisher weiter vorantreiben wird. • SWT Experimental OpenGL Plug-in (SWTOGL)27 Speziell für das Java GUI-Framework SWT wurde dieses OpenGL-Binding als Unterprojekt der Entwicklungsumgebung Eclipse entwickelt. SWTOGL ist sowohl als Eclipse-Plugin als auch für den Standalone-Einsatz konzipiert. Der Entwicklungsstand wird aber noch als experimentell eingestuft. Weitere Implementierungen wie LWJGL (Lightweight Java Game Library), JavaGL, GL4Java (OpenGL for Java Technology) und Magician haben sich aufgrund unterschiedlicher Probleme nicht durchgesetzt. Hinzu kommt, dass diese Implementierungen teilweise nicht frei verfügbar sind oder nicht weiter entwickelt werden. Im Bereich Java und 3D-Grafik gibt es daneben noch einige interessanten Alternativen, wie z.B. Java Bindings for OpenGL ES (JSR 239), Mobile 3D Graphics API for J2ME (JSR 184), die allerdings in der vorliegenden Arbeit aus Zeitgründen nicht behandelt wurden. In Kapitel 4.1 werden die Prinzipien und Techniken der einzelnen Java-BindingImplementierungen sowie die Grundlage der Java-Native-Schnittstelle JNI erläutert. Insbesondere zwischen JOGL und Java3D gibt es große konzeptionellen Unterschiede. Die konkrete Portierung der ausgesuchten Java-Binding, SWTOGL, wird in Kapitel 4.2 beschrieben. Die Evaluierungsergebnisse werden in Kapitel 4.3 zusammenfassend aufgeführt und diskutiert. 24 vgl [JOGL] JSR Java Specification Request, vgl [JCP] 26 vgl [Java3D] 27 vgl [SWTOGL] 25 47 4.1 Grundlage Vereinfacht ausgedrückt muss man für ein OpenGL Java-Binding nichts anderes tun, als eine Wrapper-Klasse in Java zu schreiben, die die C-Funktionen von OpenGL eins zu eins abbilden. Auf den ersten Blick eine einfache Aufgabe; Native-Libraries kann man von Java heraus über die JNI-API aufrufen. Nun stellt sich aber die Frage, wohin mit dem gerenderten Bild? Die Grafik-Ausgabe soll logischerweise wieder auf einem Fenster (Canvas, Widget) der Java-Anwendung erscheinen. Das bedeutet, dass das Java-Binding die Verbindung zwischen dem zugrunde liegenden Java-GUI-Framework und dem plattformabhängigen WindowSystem herstellen muss. Bekannte Java-GUI-Frameworks sind: • AWT (Abstract Window Toolkit) AWT ist das erste Java-GUI-Framework von SUN und ist fester Bestandteil der Java-Foundation-Classes (JFC). Für die Darstellung der GUI-Komponenten (Widgets) werden jeweils die Window-System eigenen Komponenten verwendet. Für den Zugriff auf die nativen Window-System-Elemente dienen sog. Peer-Codes, d.h. zu jeder Widget-Klasse existiert auch eine entsprechende Native-Peer-Funktion, die das entsprechende Window-System-Widget ansteuert. Jede Java-RuntimeEnvironment-Distribution (JRE) einer bestimmten Plattform enthält jeweils einen Satz von Window-System abhängigen Peer-Libraries. Da nicht alle Plattformen den gleichen Umfang an GUI-Elementen besitzt, bildet AWT nur eine gemeinsame Basis der unterstützten Plattformen ab. Aus diesem Grund werden moderne JavaApplikation nur ganz selten AWT als GUI-Framework verwenden. • Swing Seit Java 1.2 (1998) ist Swing zusätzlich zu AWT als GUI-Framework verfügbar. Bei Swing werden die GUI-Elemente selbst in Java gerendert und sind somit nicht von Window-System abhängig. Das Ergebnis ist ein einheitliches „Look and Feel“ auf allen Plattformen. Das Framework selbst ist modular und objektorientiert aufgebaut. Als reine Java-Implementierung sind Swing-Applikationen in der Regel langsamer als AWT oder SWT. • SWT (Standard Widget Toolkit) SWT wurde ursprünglich von IBM entwickelt und ist inzwischen zusammen mit dem Eclipse-Projekt als Open-Source freigegeben. Ziel der Entwicklung ist es einen effizienten Zugriff auf plattformabhängige Widget-Funktionalitäten durch eine plattformunabhängige API zu ermöglichen. Wie bei AWT werden bei SWT auch die Window-System spezifischen GUI-Komponenten verwendet. Der Zugriff auf die Native-GUI-Funktionen erfolgt über JNI. Für jede unterstützte Plattform werden jeweils spezifischen Java- und C-Funktionen implementiert, dadurch ist eine optimale Umsetzung möglich. Gegenüber AWT gilt SWT als umfangreicher und leistungsfähiger. Die Virtual-Machine J9 von IBM verwendet SWT als Basis GUIFramework; so bauen sich bei J9 die AWT Klassen auf SWT auf, um damit die für die Zertifizierung erforderliche AWT Klassen-Kompatibilität zu erreichen. In den folgenden Abschnitten werden die Konzepte der anfangs erwähnten Java-Bindings näher erläutert. 4.1.1 JOGL JOGL, aktuell in der Version 1.1, ist eine auf AWT und Swing basierte OpenGL JavaBinding und bietet Unterstützung von OpenGL bis zu der aktuellen Version 2.0. Eine Java48 VM in der Version 1.4 (oder höher) wird von JOGL strikt vorausgesetzt. Vom Konzept her entspricht JOGL einer Eins-zu-Eins-Abbildung der Native-OpenGL-Funktionen. Zusätzlich werden Klassen für die Anbindung an das AWT-Event-Konzept zur Verfügung gestellt. Die Namenskonventionen der OpenGL-Funktionen28 werden auch in JOGL beibehalten, obwohl das Problem bei einer objektorientierten Programmiersprache durch Überladen der Funktionen mit unterschiedlichen Signaturen gelöst werden kann. Um Plattformunabhängigkeit zu erreichen, verwendet OpenGL eigene Datentypen. Das ist bei Java nicht erforderlich. Deshalb werden bei JOGL nur die vorgegebene Java-Standardtypen wie in Tabelle 4 aufgelistet benutzt. Java-Typ OpenGL-Type byte GLbyte short GLshort int GLint float GLfloat double GLdouble Größe in Bit 8 16 32 32 64 Tabelle 4 Zuordnung der Java- und OpenGL-Datentypen Das Beispiel in Listing 3 zeigt den Unterschied zu einem C-Programm. (a) Java Code public void draw() { gl.glClear( GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT); gl.glColor3f(1.0f, 0.3f, 0.3f); gl.glBegin(GL.GL_QUADS); gl.glVertex3f(0.2f, 0.2f, 0.0f); gl.glVertex3f(0.7f, 0.2f, 0.0f); gl.glVertex3f(0.7f, 0.7f, 0.0f); gl.glVertex3f(0.2f, 0.7f, 0.0f); gl.glEnd(); } (b) C Code void draw() { glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glColor3f(1.0f, 0.3f, 0.3f); glBegin(GL_QUADS); glVertex3f(0.25f, 0.25f, 0.0f); glVertex3f(0.75f, 0.25f, 0.0f); glVertex3f(0.75f, 0.75f, 0.0f); glVertex3f(0.25f, 0.75f, 0.0f); glEnd(); } Listing 3 OpenGL Code in (a) Java/JOGL, (b) C/OpenGL Die wichtigsten Klassen der JOGL-API sind GL bzw. GLU. Diese Klassen sind die zentralen Wrapper-Klassen der Native-OpenGL-Funktionen. Diese Klassen sind als Interface ausgelegt und werden durch plattformabhängige Implementierungen ergänzt. Sämtliche OpenGLFunktionen werden in den beiden Implementierungsklassen als native deklariert. In der aktuelle JOGL Version ist die GLU-Funktionalitäten komplett als Java-Klassen ausgeführt, die aber per Einstellung auf die „gewrappte“ Native-Version umgeschaltet werden kann. Eine Besonderheit von JOGL ist, dass die Sourcecodes (Java und C) zum Großteil durch ein eigens entwickeltes Tool zur Buildzeit generiert werden. Bei der Generierung werden die HeaderDateien der auf dem System installierten OpenGL-Libraries gescannt und die Codes entsprechend erstellt. Damit ist eine optimale OpenGL-Unterstützung gewährleistet. Abbildung 31 zeigt die Beziehung der wichtigsten Klassen von JOGL. 28 vgl. Kapitel 2.1.1 49 JPanel DrawableFactory GLCapabilities <<use>> <<create>> GLJpanel «interface» Drawable X11GLImpl «interface» GLEventListener Canvas X11GLUImpl «interface» GL GLCanvas myGLEventListener <<has>> > e> us < < «interface» GLU <<use>> Abbildung 31 Klassendiagramm der wichtigsten JOGL Klasse Weitere Basisklassen sind GLCanvas, GLJpanel, GLCapabilities, GLDrawableFactory, GLDrawable und GLEventListener. Die Klasse GLCanvas ist eine Spezialisierung der AWT-Klasse Canvas und repräsentiert einen Grafik-Ausgabebereich mit garantierter Hardware-Beschleunigungsunterstützung. Durch die Vererbung bleibt das AWT-Event-Konzept vollständig erhalten. Die Klasse GLJpanel ist die Swing-Version davon, die zwar Swing-Kompatibilität bietet, jedoch Performance-Nachteile bringt. Beide Klassen implementieren das Interface GLDrawable und können mit der Klasse GLDrawableFactory instantiiert werden. Ein Objekt der Klasse GLCapabilities kann dabei als Parameter übergeben werden. Damit werden Grundeinstellungen wie Farbtiefe oder Anzahl der Framebuffer von OpenGL konfiguriert. Das Listing 4 zeigt ein lauffähiges Beispielprogramm. import java.awt.*; import java.awt.event.*; import net.java.games.jogl.*; public class HelloWorld { public static void main(String[] args) { Frame frame = new Frame("Hello World"); GLCanvas canvas = GLDrawableFactory.getFactory().createGLCanvas(new GLCapabilities()); canvas.addGLEventListener(new MyRenderer()); frame.add(canvas); frame.setSize(300, 300); canvas.display(); frame.show(); } static class MyRenderer implements GLEventListener { public void init(GLDrawable drawable) { GL gl = drawable.getGL(); 50 gl.glMatrixMode(gl.GL_PROJECTION); gl.glLoadIdentity(); gl.glOrtho(0.0, 1.0, 0.0, 1.0, -1.0, 1.0); } public void display(GLDrawable drawable) { GL gl = drawable.getGL(); gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT); gl.glColor3f(1.0f, 0.3f, 0.3f); gl.glBegin(GL.GL_QUADS); gl.glVertex3f(0.25f, 0.25f, 0.0f); gl.glVertex3f(0.75f, 0.25f, 0.0f); gl.glVertex3f(0.75f, 0.75f, 0.0f); gl.glVertex3f(0.25f, 0.75f, 0.0f); gl.glEnd(); gl.glFlush(); } public void displayChanged(GLDrawable drawable, boolean modeChanged, boolean deviceChanged) { } public void reshape(GLDrawable drawable, int x, int y, int width, int height) { } } } Listing 4 JOGL „HelloWorld“-Programm In diesem Beispiel ist das Event-Konzept deutlich erkennbar. Das Objekt MyRenderer implementiert die Interface-Klasse GLEventListener und wird an dem Event-Listener des GLCanvas Objektes übergeben. canvas.addGLEventListener(new MyRenderer()); Je nach Event und Situation werden die entsprechende Methode init (), display(), und displayChanged() aufgerufen. reshape() JOGL bietet keine Unterstützung für die High-Level 3D-Programmierung. Soll eine Anwendung objektorientiert modelliert werden, so ist es Aufgabe des Entwicklers, das System entsprechend zu gestalten. 4.1.1.1 Native-Anbindung Von höherer Bedeutung für die Portierung und Performance ist die Art und Weise, wie OpenGL Native angebunden wird. Die Funktion glColor3f() ist beispielsweise wie folgt implementiert. package net.java.games.jogl.impl.x11 public class X11GLImpl implements GL { ... native public void glColor3f(float red, float green, float blue); JNIEXPORT void JNICALL Java_net_java_games_jogl_impl_x11_X11GLImpl_glColor3f( JNIEnv *env, jobject _unused, jfloat red, jfloat green, jfloat blue) { 51 glColor3f((GLfloat) red, (GLfloat) green, (GLfloat) blue); } Java- und C-Code aus X11GLImpl.java und X11GLImpl_JNI.c Bei dieser einfachen Funktion können die Parameter ohne Umwandlung direkt von jfloat auf GLfloat „gecastet“ und weiter verwendet werden. Komplexe Anwendungen wie die in Kaptitel 3 vorgestellte Geländevisualisierung arbeiten aus Performance-Gründen mit Vertex-Array29. Eine effiziente Implementierung der entsprechenden Funktionen ist daher für eine gute Gesamt-Performance unerlässlich. Die folgenden Code-Fragmente zeigen die Implementierung der Funktion glVertexPointer. public void glVertexPointer(int size, int type, int stride, java.nio.Buffer ptr) { if (!BufferFactory.isDirect(ptr)) throw new GLException("Argument \"ptr\" was not a direct buffer"); glVertexPointer0(size, type, stride, ptr); } native private static void glVertexPointer0(int size, int type, int stride, java.nio.Buffer ptr); JNIEXPORT void JNICALL Java_net_java_games_jogl_impl_x11_X11GLImpl_glVertexPointer0( JNIEnv *env, jobject _unused, jint size, jint type, jint stride, jobject ptr) { GLvoid * _ptr3 = NULL; if (ptr != NULL) { _ptr3 = (GLvoid *) (*env)->GetDirectBufferAddress(env, ptr); } else { _ptr3 = NULL; } glVertexPointer((GLint) size, (GLenum) type, (GLsizei) stride, (GLvoid *) _ptr3); } Java- und C-Code aus X11GLImpl.java und X11GLImpl_JNI.c Hier ist zu erkennen, dass JOGL mit dem seit J2SE 1.4 eingeführten NIO (Native IO)30 Konzept arbeitet. Das Vertex-Array java.nio.Buffer ptr wird auf der Java-Seite als Direct-Buffer deklariert und an die korrespondierte C-Funktion übergeben. Die C-Funktion leitet diesen Pointer direkt an OpenGL als GLvoid „gecastet“ weiter. OpenGL hat damit ohne Zeitverzögerung den vollen Zugriff auf das gesamte Array. Die Anbindung von OpenGL an AWT soll exemplarisch auf einer X-Window/LinuxPlattform gezeigt werden. Abbildung 32 zeigt den Ablauf beim Aufruf der Methode reshap(). Ein Objekt der Klasse GLCanvas hat u.a. ein GLContex Objekt, in diesem Fall eine konkrete X11-Implementierung X11OnscreenGLContext. In dieser Klasse sind alle WindowSystem/Plattform abhängige Funktionen gekapselt (siehe Abbildung 33). 29 30 vgl. Kapitel 2.1.1 vgl. Kapitel 4.1.4 52 Abbildung 32 Sequenz-Diagramm beim Aufruf der Methode GLCanvas.reshape() GLContext X11GLContext WindowsGLContext ... X11OnscreenGLContext X11OffscreenGLContext ... ... Abbildung 33 Klassendiagramm GLContext Hierarchie Wie bereits in Kapitel 2.1.2 erläutert, benötigt OpenGL für die Initialisierung einen GrafikContext, damit OpenGL das fertig gerenderte Bild anzeigen kann. Das geschieht bei GLX mit der Funktion glXMakeCurrent(Display* display, XID drawable, GLXContext ctx). Die benötigten Parameter display und drawable müssen aus dem AWT Grafik-Context abgeleitet werden. Diese können entweder aus der GraphicDevice-Klasse abgefragt werden31 31 vgl. Kapitel 4.1.2.1 53 oder native wie bei JOGL praktiziert über die AWT-C API. Diese Abfrage wird bei JOGL in der Klasse JAWT realisiert. Die Methode GetDrawingSurface zeigt die Funktionsweise. Der Parameter target ist dabei ein Objekt der AWT Component-Klasse. public JAWT_DrawingSurface GetDrawingSurface(java.lang.Object target) { ByteBuffer _res; _res = GetDrawingSurface0(getBuffer(), target); if (_res == null) return null; return new JAWT_DrawingSurface(_res.order(ByteOrder.nativeOrder())); } native private java.nio.ByteBuffer GetDrawingSurface0( java.nio.Buffer jthis0, java.lang.Object target); #include <jawt.h> ... JNIEXPORT jobject JNICALL Java_net_java_games_jogl_impl_JAWT_GetDrawingSurface0( JNIEnv *env, jobject _unused, jobject jthis0, jobject target) { JAWT * this0 = NULL; JAWT_DrawingSurface * _res; if (jthis0 != NULL) { this0 = (JAWT *) (*env)->GetDirectBufferAddress(env, jthis0); } else { this0 = NULL; } _res = this0->GetDrawingSurface(env, (jobject) target); if (_res == NULL) return NULL; return (*env)->NewDirectByteBuffer(env, _res, 24); } Java- und C-Code aus JAWT.java und JAWT_JNI.c Das JAWT-Objekt wird hier mit der Funktion (*env)->GetDirectBufferAddress()ermittelt. Anschließend wird die Funktion GetDrawingSurface() aufgerufen, um einen Zeiger auf die JAWT_DrawingSurface Struktur zu bekommen. Interessant ist hier zu sehen, dass diese Struktur mit der Funktion NewDirectByteBuffer in Direct-ByteBuffer umgewandelt und zurückgeliefert wird32. Als Direct-ByteBuffer wird diese Struktur nicht mehr vom GarbageCollector beeinflusst, wodurch ein konstant schneller Zugriff gewährleistet wird. In der Methode lockSurface des X11OnscreenContext Objektes werden für die X11 wichtigen Informationen wie display, drawable und visualID aus dem JAWT_DrawingSurface Objekt extrahiert. Anschließend werden diese Informationen als Parameter von der Methode makeCurrent() an die Wrapper-Klasse GLX.glXMakeCurrent() weitergeleitet. 32 vgl. Kapitel 4.1.4 54 private boolean lockSurface() throws GLException { ... ds = getJAWT().GetDrawingSurface(component); ... dsi = ds.GetDrawingSurfaceInfo(); ... x11dsi = (JAWT_X11DrawingSurfaceInfo) dsi.platformInfo(); display = x11dsi.display(); drawable = x11dsi.drawable(); visualID = x11dsi.visualID(); ... } protected synchronized boolean makeCurrent(Runnable initAction) throws GLException { ... if (!lockSurface()) { return false; } ... if (!GLX.glXMakeCurrent(display, (int) drawable, context)) { throw new GLException("Error making context current"); } ... } Java -Code aus X11OnscreenContext.java Die Methode GLX.glXMakeCurrent() wird Java- und C-seitig wie folgt implementiert: native static public boolean glXMakeCurrent( long dpy, long drawable, long ctx); JNIEXPORT jboolean JNICALL Java_net_java_games_jogl_impl_x11_GLX_glXMakeCurrent( JNIEnv *env, jclass _unused, jlong dpy, jlong drawable, jlong ctx) { Bool _res; _res = glXMakeCurrent((Display *) (intptr_t) dpy, (XID) (intptr_t) drawable, (GLXContext) (intptr_t) ctx); return _res; } Java- und C-Code aus GLX.java und GLX_JNI.c 4.1.2 JAVA3D Java3D bietet eine vollständig objektorientierte Klassenbibliothek um 3D-Applikationen zu erstellen. Dabei werden die 3D-Objekte als sog. Szenen-Graph dargestellt. Ziel der Entwicklung ist High-Level-Konzepte eines Szenen-Graphen basierten Systems mit einer Low-Level-3D-Engine wie OpenGL zu kombinieren. Die API bietet vielfältige Möglichkeiten zum Erstellen und Manipulieren von 3D- und Szenen-Graph-Objekten. Loader-Klassen zum Einlesen von 3D Objekten in den gängigsten Formaten (VRML, 3DS, usw.) sind ebenfalls 55 vorhanden. Aktuell liegt Java3D in der Version 1.4 vor. Eine JVM in der Version 1.4 wird vorausgesetzt. Die Funktionalitäten von Java3D sind in drei Packages aufgeteilt: • Java3D-Core Classes (javax.media.j3d) Basis Klassen von Java3D, um das „virtual universe“ aufzubauen und zu bearbeiten. • Mathematik Klassen für (javax.vecmath) Mathematik-Klassen für Vektoren, Matrizen und sonstige 3D Berechnungen. • Utility-Klassen (com.sun.j3d.utils) Die Utility-Klassen bieten zusätzliche Hilfsfunktionen, beispielsweise für das Einlesen von 3D Objekte oder eine vereinfachte Handhabung des Szenen-Graphen Objektes (z.B. SimpleUniverse). Das zentrale Konstrukt in Java3D ist der Szenen-Graph33. Eine 3D-Welt wird als eine BaumStruktur in einem sog. Virtual Universe dargestellt. 3D-(Szenen-Graph)-Objekte in diesem 3D-Raum werden als Knoten in diesem Baum angehängt. Die Kanten dieses Baumes repräsentieren Beziehungen zwischen den Knoten. Zwei Arten von Beziehungen sind definiert: • Vater-Kind-Beziehung (Parent-Child-Relationship) Die Knoten-Objekte Virtual Universe, Locale, Group und Leaf stehen in einer VaterKind-Beziehung zueinander. D.h. die Hierarchie der Knoten wird durch diese Art von Beziehung festgelegt. • Referenz-Beziehung (Reference-Relationship) Knoten-Objekte von Typ Node Component und „Weitere Objekte“ (vgl. weiter unten) werden von den anderen Knoten-Objekten referenziert und stehen somit in einer Referenz-Beziehung zu denen. Ein Vater-Knoten kann viele Kind-Knoten haben, umgekehrt darf aber ein Kind-Knoten nur einen Vater-Knoten haben. Außerdem sind Zyklen nicht erlaubt. Jeder Szene-Graph entspricht somit einen Directed Acyclic Graph (DAG). Abbildung 34 zeigt den Aufbau eines Szenen-Graphen in Java3D. 33 engl. scene graph 56 VirtualUniverse Parent-Child link Reference Locale Content branch graph View branch graph BG BG Group (BranchGroup) TG TG Group (TransformGroup) Leaf (Shape3D) S TG V View Canvas3D Screen3D Leaf (ViewPlatform) Appearance NodeComponent Physical Body Physical Env. Abbildung 34 Aufbau eines Szenen-Graphen in Java3D aus [Sun01] Die Klassen des Szenen-Graphen sind: • Virtual Universe Das Virtual Universe ist die Wurzel des gesamten Szenen-Graphen und enthält Locale-Objekte als Kind-Knoten. Eine typische Java3D-Applikation hat normalerweise nur ein Virtual Universe. Bei einem sehr großen Virtual Universe kann der Szenen-Graph in unterschiedlichen Locale aufgeteilt werden. • Locale In einem Locale-Objekt sind u.a. die „High-Resolution-Coordinates“ des TeilBaumes definiert. Die Position der 3D-Objekte innerhalb des Teil-Szenen-Graphen sind immer relativ zu der Koordinaten des Locale-Objektes definiert. • Group Objekte der Klasse Group werden an dem Locale Objekt als Kind-Konten zugeordnet und repräsentieren Teilbäume im Szenen-Graph-Modell. Es sind zwei Group-Typen definiert: TransformGroup und BranchGroup. TransformGroup ist für die Positionierung der 3D-Objekte im Raum zuständig. BranchGroup enthält Referenzen auf weitere Objekte (Group oder Leaf). Es gibt unterschiedliche Typen von Teil-Graphen, die jeweils mit einem Group-Objekt eingeleitet wird. Der „Content Branch Graph“ repräsentiert den Inhalt wie z.B. Geometrie des 3DObjektes und der „View Branch Graph“ enthält die Ansichtsparameter. • Leaf Leaf-Objekte bilden die Blatt-Knoten des Baumes. Sie referenzieren die 3DElemente, die gerendert werden sollen. 57 • Node Component und dessen Subklassen Appearance und Geometry enthalten letztendlich Darstellungsattribute, wie Geometrie und Lichtverhältnisse. NodeComponent • Weitere Objekte Weitere Objekte sind Canvas3D-, Screen3D- und View-Objekte. Sie sind alle für die Ausgabe der Grafik zuständig. Abbildung 35 zeigt die Hierarchie der wichtigsten Klassen. BranchGroup Canvas3D Group TransformGroup SharedGroup ... Node Shape3D Leaf ViewPlatform SceneGraphObject Behavior ... Apperance Geometry NodeComponent Material ... Texture Abbildung 35 Hierarchie der wichtigsten Szenen-Graphen-Klassen Anhand eines aus [Sun01] modifizierten HelloUniverse-Beispiels soll gezeigt werden, wie die Programmierung konkret aussieht. Dabei soll der Szenen-Graph aus Abbildung 36 implementiert werden. 58 Abbildung 36 Beispiel HelloUniverse Szenen-Graph Um das Beispiel möglichst einfach zu halten, wird die Utility-Klasse SimpleUniverse zum Erzeugen des Szenen-Graphen verwendet. Der Vorteil von SimpleUniverse ist, dass der komplette „View Branch Graph“ Teilbaum automatisch erzeugt wird34. Damit kann der Entwickler sich auf die Inhalte des Teilbaums „Content Branch Graph“ konzentrieren. public HelloMyUniverse() { SimpleUniverse simpleUniverse = null; setLayout(new BorderLayout()); GraphicsConfiguration config = SimpleUniverse.getPreferredConfiguration(); Canvas3D canvas3D = new Canvas3D(config); add("Center", canvas3D); simpleUniverse = new SimpleUniverse(canvas3D); simpleUniverse.getViewingPlatform().setNominalViewingTransform(); BranchGroup sceneCube = createCubeSceneGraph(); BranchGroup sceneText = createTextSceneGraph(); simpleUniverse.addBranchGraph(sceneCube); simpleUniverse.addBranchGraph(sceneText); } public static void main(String[] args) { MainFrame frame = new MainFrame(new HelloMyUniverse(), 600, 400); } Hauptprogramm von HelloMyUniverse 34 vgl. Abbildung 36 59 public BranchGroup createCubeSceneGraph() { BranchGroup objRoot = new BranchGroup(); ColorCube colorCube = new ColorCube(0.4); Transform3D xRotation = new Transform3D(); Transform3D yRotation = new Transform3D(); xRotation.rotX(Math.PI/4.0d); yRotation.rotY(Math.PI/5.0d); xRotation.mul(yRotation); TransformGroup objRotation = new TransformGroup(xRotation); objRoot.addChild(objRotation); objRotation.addChild(colorCube); objRoot.compile(); return objRoot; } public BranchGroup createTextSceneGraph() { BranchGroup objRoot = new BranchGroup(); Text2D text2D = new Text2D("Hello Universe", new Color3f(0.2f, 0.2f, 1.0f), "Helvetica", 40, 0); Transform3D translation = new Transform3D(); translation.setTranslation(new Vector3d(-0.5, 0.1, 0.5)); TransformGroup objTranslation = new TransformGroup(translation); objRoot.addChild(objTranslation); objTranslation.addChild(text2D); objRoot.compile(); return objRoot; } Methode zum Erstellen der Teil-Szenen-Graphen Im Konstruktor der Klasse HelloMyUniverse()wird das Canvas3D Objekt mit einer DefaultKonfiguration erzeugt. Das Canvas3D-Objekt stellt hier den Kontext für die Grafikausgabe dar. Mit dem Canvas3D-Objekt als Parameter wird anschließend das SimpleUniverse Objekt, den eigentlichen Szenen-Graphen, erstellt. Ab hier kann man die zwei Teil-Bäume (ContentBranch-Graph), wie in Abbildung 36 dargestellt, dem Szenen-Graph mit der Methode addBranchGraph() hinzufügen. Diese BranchGroup-Objekte werden in den Methoden createCubeSceneGraph() und createTextSceneGraph() erzeugt. Das BranchGroupObjekt von der Methode createCubeSceneGraph enthält einen Würfel (ColorCube) sowie entsprechende Transformationsobjekte (TransformGroup mit Rotationen auf der X- und YAchse). Die Methode createTextSceneGraph() liefert dagegen ein BranchGroup-Objekt mit einem Text-Objekt (Text2D) inklusive einer einfachen Translation als Knoten zurück. Das Ergebnis des Beispielprogramms ist in Abbildung 37 abgebildet. 60 Abbildung 37 Screenshot Java3D-Beispielanwendung 4.1.2.1 Native-Anbindung Bei Java3D ist es aufgrund der höheren Komplexität insgesamt schwierig, die NativeAnbindung zu bewerten. Dennoch findet man bei der Analyse der Sourcecodes einige bekannte Muster, die auf eine effiziente Implementierung schließen. • GeometrieArray Ein Objekt der Klasse GeometrieArray sowie Objekte der Subklassen enthalten Geometrie-Daten von 3D-Objekten. Der Sourcecode zeigt, dass diese Klasse für die Speicherung der Vertex-Koordinaten die Klasse J3DBuffer verwendet. Wie das folgende Code-Fragment zeigt, benutzt J3DBuffer für die eigentliche Datenhaltung NIO-Buffer. public class J3DBuffer { ... private java.nio.Buffer originalBuffer = null; ... public J3DBuffer(java.nio.Buffer buffer) { ... setBuffer(buffer); } } Java-Code aus javax.media.j3d.J3DBuffer.java Die Methode execute der GeometryArray Klasse wird aufgerufen, um u.a. die verschiedene Vertex-Arrays an OpenGL zu übergeben. Die Übergabe geschieht per JNI, wie es der nächste Programmabschnitt zeigt. abstract class GeometryArrayRetained extends GeometryRetained{ // Used for NIO buffer geometry J3DBuffer coordRefBuffer = null; FloatBufferWrapper floatBufferRefCoords = null; ... void execute(...) { Object vcoord = null; vcoord = floatBufferRefCoords.getBufferAsObject(); 61 ... executeVABuffer(cv.ctx, this, ..., vcoord, ...); ... } JNIEXPORT void JNICALL Java_javax_media_j3d_GeometryArrayRetained_executeVABuffer( JNIEnv *env, jobject obj, ..., jobject vcoords, ... ) { ... fverts= (jfloat *)(*(table->GetDirectBufferAddress))(env, vcoords ); ... coordoff = 3 * initialCoordIndex; glVertexPointer(3, GL_FLOAT, 0, &(fverts[coordoff])); ... } Java- und C-Code aus javax.media.j3d.GeometryArrayRetained.java bzw. …/native/d3d/ GeometryArrayRetained.cpp • Canvas3D Das Konfigurieren der OpenGL-Engine erfolgt hauptsächlich in der Klasse Canvas3D. In der Methode createNewContext() wird beispielsweise den OpenGLContext per JNI mit der glXCreateNewContext() Funktion erstellt. Hier sind die bekannte GLX-Funktionen gut zu erkennen. native long createNewContext(long display, int window, int vid, long fbConfig, long shareCtx, boolean isSharedCtx, boolean offScreen); JNIEXPORT jlong JNICALL Java_javax_media_j3d_Canvas3D_createNewContext( JNIEnv *env, jobject obj, jlong display, jint window, jint vid, jlong fbConfigListPtr, jlong sharedCtxInfo, jboolean isSharedCtx, jboolean offScreen) { jlong gctx; jlong sharedCtx; ... ctx = glXCreateNewContext((Display *)display, fbConfigList[0], GLX_RGBA_TYPE, (GLXContext)sharedCtx, True); ... if (!glXMakeCurrent((Display *)display, (GLXDrawable)window,(GLXContext)ctx)) { ... } ... } Java- und C-Code aus javax.media.j3d.Canvas3D.java bzw. …/native/ogl/Canvas3D.c 62 Im Gegensatz zu JOGL wird die JAWT-API bei Java3D nicht verwendet. Die für die GLXFunktionen erforderlichen Parameter, wie display und screen35, werden aus der konkreten AWT –GraphicsDevice-Implementierungsklasse X11GraphicsDevice abgefragt. NativeScreenInfo(GraphicsDevice graphicsDevice) { // Open a new static display connection if one is not already opened getStaticDisplay(); // Get the screen number screen = ((X11GraphicsDevice)graphicsDevice).getScreen(); } Java-Code aus javax.media.j3d.NativeScreenInfo.java Interessant ist auch die Feststellung, dass Java3D für die Grafik-Ausgabe teilweise XlibFunktionen direkt verwendet. Der folgende Codeausschnitt zeigt ein Beispiel dafür. JNIEXPORT jlong JNICALL Java_javax_media_j3d_NativeScreenInfo_openDisplay( JNIEnv *env, jclass cls) { Display* dpy; dpy = XOpenDisplay(NULL); return (jlong)dpy; } C-Code aus .../native/ogl/NativeScreenInfo.c 4.1.3 SWTOGL Ähnlich wie bei JOGL ist SWTOGL ein klassisches Eins-zu-Eins-OpenGL-Binding. Das Konzept bezüglich Namenskonvention und Datentypen sind bei beiden Bindings fast gleich. Das Listing 5 zeigt eine Gegenüberstellung zwischen Java/SWTOGL und C/GLX. (a) Java Code public void draw() { GL.glClear( GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT); GL.glColor3f(1.0f, 0.3f, 0.3f); GL.glBegin(GL.GL_QUADS); gl.glVertex3f(0.2f, 0.2f, 0.0f); gl.glVertex3f(0.7f, 0.2f, 0.0f); gl.glVertex3f(0.7f, 0.7f, 0.0f); gl.glVertex3f(0.2f, 0.7f, 0.0f); GL.glEnd(); (b) C Code void draw() { glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glColor3f(1.0f, 0.3f, 0.3f); glBegin(GL.GL_QUADS); glVertex3f(0.25f, 0.25f, 0.0f); glVertex3f(0.75f, 0.25f, 0.0f); glVertex3f(0.75f, 0.75f, 0.0f); glVertex3f(0.25f, 0.75f, 0.0f); glEnd(); glContext.swapBuffers(); } glXSwapBuffers(display, drawable); } Listing 5 Code-Beispiele (a) Java/SWTOGL, (b) C/GLX 35 vgl. Kapitel 2.1.2.2 63 Die Basis-Klassen von SWTOGL sind GL, GLU und GLContext. Im Gegensatz zu JOGL werden die OpenGL Methode in der GL- und GLU Klassen als static final native deklariert. Eine Applikation kann die gerenderte Grafik direkt durch den Aufruf der Methode swapBuffer() des GLContext-Objektes auf dem Bildschirm darstellen. Eine feste Integration in das Event-Framework, wie z.B. der GLEventListener von JOGL, ist nicht zwingend erforderlich. Diese einfache Vorgehensweise entspricht am ehesten der klassischen OpenGLProgrammierung, beispielsweise mit GLX. 4.1.3.1 Native-Anbindung Analog zu JOGL wird die Anbindung von OpenGL in diesem Kapitel anhand der Implementierung an einigen signifikanten Stellen näher erläutert. • Funktion glColor3f() public static final native void glColor3f ( float red, float green, float blue); JNIEXPORT void JNICALL GL_NATIVE(glColor3f) (JNIEnv *env, jclass that, jfloat arg0, jfloat arg1, jfloat arg2) { GL_NATIVE_ENTER(env, that, glColor3f_FUNC); glColor3f(arg0, arg1, arg2); GL_NATIVE_EXIT(env, that, glColor3f_FUNC); } Java- und C-Code aus GL.java bzw. gl.c Die verwendeten Makros sind wie folgt definiert (die Marcos GLU_NATIVE_ENTER werden nur für Statistik-Zwecke benutzt und sind per Default ausgeschaltet): #define GL_NATIVE(func) Java_org_eclipse_swt_opengl_GL_##func extern int GLU_nativeFunctionCallCount[]; #define GLU_NATIVE_ENTER(env, that, func) GLU_nativeFunctionCallCount[func]++; #define GLU_NATIVE_EXIT(env, that, func) Macro Definition aus gl.h Wie zu erwarten ist bei dieser einfachen Funktion kein Unterschied festzustellen. • Funktion glVertexPointer(): public static final native void glVertexPointer ( int size, int type, int stride, int[] pointer); JNIEXPORT void JNICALL GL_NATIVE(glVertexPointer) (JNIEnv *env, jclass that, jint arg0, jint arg1, jint arg2, jintArray arg3) { jint *lparg3=NULL; 64 GL_NATIVE_ENTER(env, that, glVertexPointer_FUNC); #ifdef JNI_VERSION_1_2 if (IS_JNI_1_2) { if (arg3) lparg3 = (*env)->GetPrimitiveArrayCritical(env, arg3, NULL); } else #endif { if (arg3) lparg3 = (*env)->GetIntArrayElements(env, arg3, NULL); } glVertexPointer(arg0, arg1, arg2, lparg3); #ifdef JNI_VERSION_1_2 if (IS_JNI_1_2) { if (arg3) (*env)->ReleasePrimitiveArrayCritical(env, arg3, lparg3, 0); } else #endif { if (arg3) (*env)->ReleaseIntArrayElements(env, arg3, lparg3, 0); } GL_NATIVE_EXIT(env, that, glVertexPointer_FUNC); } Java- und C-Code aus GL.java bzw. gl.c Hier ist deutlich zu erkennen, dass SWTOGL für die Native-Anbindung JNI bis zu Version 1.2 unterstützt. Das von JOGL eingesetzte NIO-Package findet hier keine Verwendung. Das ist gut für die Portierbarkeit, aber hinsichtlich der Geschwindigkeit nicht optimal. • Anbindung SWT-OpenGL Die Anbindung von SWT an OpenGL soll am Beispiel der GLContext-Klasse analysiert werden. Die Abbildung 38 verdeutlicht den Verlauf. Abbildung 38 Sequenz-Diagramm beim Erzeugen des GLContext Objektes 65 Im Konstruktor der GLContext-Klasse sind die erforderlichen Schritte zum Erstellen des OpenGL-Context ersichtlich. Es wird zunächst ein SWT-Grafik-Context (gc) mit der Methode drawable.internal_new_GC() erzeugt. Diese Methode des drawable-Objektes ist in diesem Fall in der Klasse Control definiert. Verfolgt man den Aufruf, so kann man feststellen, dass schließlich die C-Funktion gdk_gc_new() aufgerufen wird. gdk_gc_new() ist eine Funktion aus der GTK-Toolkit Library. GTK (Gimp Toolkit) ist ein weit verbreitetes Open-Source GUI-Framework. Die XWindow Implementierung von SWT basiert darauf. Die Klasse OS ist aus dem Standard-Package von SWT, in der plattformabhängige Native-Funktionen gekapselt sind. Offensichtlich liefert die OS -Klasse standardmäßig nur den GTK-Grafik-Context zurück. Um die erforderlichen XLib-Parameter, wie xDisplay und info, für die Funktion glXCreateContext() zu ermitteln, wurde eine weitere Native-Klasse XGTK entwickelt. Die Methode XGTK.gdk_x11_gc_get_xdisplay() ruft die entsprechende GTK C-Funktion gdk_x11_gc_get_xdisplay() auf, um den XWindow-Handler xDisplay zu referenzieren. public GLContext (Drawable drawable) { if (drawable == null) SWT.error (SWT.ERROR_NULL_ARGUMENT); this.drawable = drawable; data = new GCData (); gc = drawable.internal_new_GC (data); if (gc == 0) SWT.error (SWT.ERROR_NO_HANDLES); int xDisplay = XGTK.gdk_x11_gc_get_xdisplay (gc); int screen = XGTK.XDefaultScreen (xDisplay); ... handle = XGL.glXCreateContext (xDisplay, info, 0, false); } GLContext.java public int internal_new_GC (GCData data) { checkWidget (); int window = paintWindow (); if (window == 0) SWT.error (SWT.ERROR_NO_HANDLES); int gdkGC = OS.gdk_gc_new (window); if (gdkGC == 0) error (SWT.ERROR_NO_HANDLES); ... return gdkGC; } Control.java public static final native int _gdk_gc_new(int window); JNIEXPORT jint JNICALL OS_NATIVE(_1gdk_1gc_1new) (JNIEnv *env, jclass that, jint arg0) { jint rc = 0; OS_NATIVE_ENTER(env, that, _1gdk_1gc_1new_FUNC); 66 rc = (jint)gdk_gc_new((GdkDrawable *)arg0); OS_NATIVE_EXIT(env, that, _1gdk_1gc_1new_FUNC); return rc; } OS.java und os.c public static final synchronized native int gdk_x11_gc_get_xdisplay(int gc); JNIEXPORT int JNICALL XGTK_NATIVE(gdk_1x11_1gc_1get_1xdisplay) (JNIEnv *env, jclass that, jint arg0) { jint rc; NATIVE_ENTER(env, that, "gdk_1x11_1gc_1get_1xdisplay\n") rc = (jint)gdk_x11_gc_get_xdisplay((GdkGC *)arg0); NATIVE_EXIT(env, that, "gdk_1x11_1gc_1get_1xdisplay\n") return rc; } XGTK.java und xgtk.c 4.1.4 JNI und NIO Im folgenden Abschnitt werden die grundlegende Technik und Funktionsweise von JNI und NIO hinsichtlich für OpenGL-Binding relevante Aspekte erläutert. Eine ausführliche Beschreibung befindet sich in [Sun02] und [Hit02]. Um von einem Java-Programm heraus Native-Library Funktionen aufzurufen, hat die Firma SUN das Java Native Interface (JNI) entwickelt. JNI stellt damit die Brücke zwischen der Native- und Java-Seite dar. Anhand eines Beispiels soll die Vorgehensweise verdeutlicht werden. Soll die statische Methode glColor3f () der Klasse GL im net.java.games.jogl Package als Native-Funktion realisiert werden, muss auf der Java-Seite die Methode als native deklariert werden. package net.java.games.jogl; public class GL { ... native public void glColor3f(float red, float green, float blue); Native Methode Definition in Java Dann muss eine C-Funktion als „Gegenstück“ entwickelt werden. Dabei sind die definierten Datentypen und Makros zu verwenden und die JNI-Namenkonventionen einzuhalten. Der Programmcode dazu sieht folgendermaßen aus: JNIEXPORT void JNICALL Java_net_java_games_jogl_GL_glColor3f( JNIEnv *env, jobject _this, jfloat red, jfloat green, jfloat blue) { glColor3f((GLfloat) red, (GLfloat) green, (GLfloat) blue); } Native Methode Definition in C 67 Die Parameter JNIEnv *env und jobject _this sind bei JNI obligatorisch. JNIEnv *env ist die Referenz auf die VM-Umgebung. Über diesen Pointer können weitere JNI-Funktionen aufgerufen werden. jobject _this repräsentiert das aufrufende Objekt selbst. Der Header dieser C-Funktion kann auch mit dem Java-Tool javah > javah –jni GL.java generiert werden. Die Makros JNIEXPORT und JNICALL sorgen für die nötige Portierbarkeit zwischen den Plattformen. Für die Benennung der C-Funktion gilt folgende Regel (Abbildung 39): Der voll-qualifizierte Klassenname, getrennt durch _ statt . Prefix, ist immer Java_ Java_ + net_java_games_jogl_GL_ Methodename + glColor3f Abbildung 39 Namenskonvention der Native-Funktion bei JNI Die erstellte C-Funktion wird anschließend kompiliert und ggf. mit anderen C-Objekt-Dateien zu einer Shared-Library zusammengefasst. Auf QNX kann eine Shared-Library namens libmylib.so beispielsweise wie folgt erzeugt werden. > cc –o libmylib.so –shared –lGL –lGLU –lm gl.o Vor der Benutzung der Native-Funktion muss die Shared-Library noch geladen werden. Dies geschieht mit der Methode System.loadLibrary () und soll typischerweise im staticBlock der Java-Klasse aufgerufen werden. package net.java.games.jogl; public class GL { static { System.loadLibrary(“mylib”); } ... native public void glColor3f(float red, float green, float blue); Beispiel zum Laden der externen Library mit der loadLibrary() Methode Im oben gezeigten Beispiel werden float Variablen als Parameter an die C-Funktion übergeben. Prinzipiell können alle Java-Primitive-Datentypen (Tabelle 5) sowie -Objekte (Abbildung 40) als Parameter und Return-Wert übergeben werden. 68 Java-Typ C-Type boolean byte char short int long float double void jboolean jbyte jchar jshort jint jlong jfloat Jdouble Void Grösse in Bit 8, unsigned 8 16 unsigned 16 32 64 32 64 - Tabelle 5 Zuordnung der Java- und C-Datentypen jobject ... jstring jarray jintArray – int arrays jclass jobjectArray – arrays of objects jfloatArray – float arrays ... Abbildung 40 Vordefinierte C-Typen auf Java-Objekte Java-Objekte werden über „passed by reference“ übergeben. D.h. auf der Native-Seite wird ein Java-Objekt über einen Pointer referenziert. JNI hat dazu einige Pointer-Typen definiert (Abbildung 40). Obwohl es ausreichen würde, alle Java-Objekte über den jobject-Pointer zu referenzieren, sollen laut Konvention stattdessen die dazugehörigen Sub-Typen der Objekte verwendet werden. Bei einer typischen OpenGL-Applikation werden große Datenmengen in einem Vertex-Array abgespeichert und an die Native-OpenGL-Funktionen weitergeleitet. Es ist daher wichtig zu wissen, wie JNI Arrays verwaltet. Hierzu ein Beispiel mit einem Float-Array als Parameter: native public float sum(float[] aFloatArray); JNIEXPORT jfloat JNICALL Java_net_java_games_jogl_GL_sum( JNIEnv *env, jobject _this, jfloatArray aFloatArray) { ... } Java- und C-Code für ein Beispiel JNI Aufruf Der Zugriff auf die Java-Objekte erfolgt über Funktionen, die von JNI zur Verfügung gestellt werden. Auf ein Float-Array-Objekt kann beispielsweise wie folgt zugegriffen werden. 69 JNIEXPORT jfloat JNICALL Java_net_java_games_jogl_GL_sum( JNIEnv *env, jobject _this, jfloatArray aFloatArray) { int i; float sum = 0; jsize len = (*env)->GetArrayLength(env, aFloatArray); jfloat *arr = (*env)->GetFloatArrayElements(env, aFloatArray, 0); for (i=0; i<len; i++) { sum += arr[i]; } ... (*env)->ReleaseFloatArrayElements (env, aFloatArray, arr, 0); Return sum; } C-Code für ein Beispiel mit Array bei JNI Die Länge eines Arrays muss immer getrennt mit der Funktion GetArrayLength() ermittelt werden. Beim Aufruf der Funktion GetFloatArrayElements() wird das Array je nach JVM entweder als nicht verschiebbar markiert (pin) oder in einen nicht verschiebbaren Speicherbereich (Nonmoveable Memory) der JVM kopiert, damit der Speicher nicht vom Garbage-Collector zufällig während der Ausführung der Native-Funktion umorganisiert wird. Es ist daher wichtig, nach der Ausführung diesen Speicherbereich mit der Funktion ReleaseFloatArrayElements() wieder freizugeben. Die JVM kopiert dann das Array aus dem Nonmoveable Memory zurück und gibt den Speicherbereich frei. Bei nicht Beachtung dieser Regel kann es zu einem Memory-Leak und sogar zum Absturz der JVM führen. Insbesondere das Kopieren der Arrays, obwohl es im Speicher erfolgt, kann bei großen Datenmengen zu Performance-Verlust führen. Um hier Abhilfe zu schaffen, hat die Firma SUN mit Java 1.2 zwei neue Funktionen eingeführt: GetPrimitiveArrayCritical() und ReleasePrimitiveArrayCritical(). Die Semantik der beiden neuen Funktionen sind vergleichbar mit den bisherigen Get/ReleaseArrayElements() Funktionen. Wenn möglich liefert die JVM einen Pointer auf die „uncopied“ Version des Arrays, unabhängig davon, ob die JVM das „pinning“ (Festnageln) von Speicher unterstützt. Der Name „...Critical“ deutet aber schon an, dass die Verwendung dieser Funktion einige Einschränkungen hat. Um einen konsistenten Zugriff auf den Speicher zu gewährleisten, muss die JVM je nach Implementierung den Garbage-Collector vorübergehend abschalten oder sogar alle anderen Threads stoppen. D.h. der Code zwischen Get/ReleasePrimitiveArrayCritical() bildet eine Art „Critical-Region“. Die Ausführung der Code-Zeilen innerhalb dieser Critical-Region soll deshalb nicht zu lange dauern. Außerdem sind weitere JNI- und System-Calls, die zu Blockaden führen könnten, dringend zu vermeiden. Aufgrund dieser Problematik werden diese Funktionen eher als eine Not-Lösung für akute Performance-Probleme angesehen. Um Abhilfe zu schaffen, hat die Firma SUN mit der Einführung von Java 1.4 das neue NIODirekt-Buffer Konzept sowie entsprechende neue JNI-Funktionen eingeführt. Ein Direkt-Buffer repräsentiert einen von der Speicherverwaltung der JVM nicht betroffenen Speicherbereich. Dieser Speicherbereich kann sowohl innerhalb der JVM als auch außerhalb der JVM vom Native-Code erzeugt werden. Der Zugriff auf diesen Speicherbereich ist 70 sowohl auf der Java-Seite (gekapselt als Direct-Buffer-Objekt) als auch auf der Native-Seite transparent. So ist es beispielsweise möglich, Video-Memory oder Register-Mapping-Bereich eines Device-Controllers als Direct-Buffer zu deklarieren und nach Java zu exportieren. In Verbindung mit dem ebenfalls in NIO neu eingeführten Channel-Konzept ist es zum ersten mal möglich, hoch performante oder Hardware nahe Anwendungen mit Java zu realisieren. Die Klassen-Hierarchie des NIO-Buffers ist in Abbildung 41 dargestellt. Abbildung 41 Klassen-Hierarchie von Java-NIO-Buffer. Quelle [Hit02]. Ein Buffer-Objekt ist ein Array von primitiven Datentypen und hat zusätzlich vier Eigenschaften: • Capacity Die maximale Kapazität des Buffers wird beim Erzeugen des Buffer-Objektes festgelegt und kann nicht verändert werden. • Limit Das erste Element, das nicht gelesen oder geschrieben werden kann. • Position Der Index des nächsten Elements, das gelesen oder geschrieben wird. Die Position wird automatisch beim Aufruf der relativen get() und put() Methoden aktualisiert. • Mark Eine Markierung der Index-Position: beim Aufruf der Methode mark() wird die Markierung auf die aktuelle Position gesetzt. Bei reset() dagegen wird die aktuelle Position auf den Wert der Markierung zurückgesetzt. Abbildung 42 zeigt den schematischen Aufbau eines Buffer-Objektes. Abbildung 42 Schematische Aufbau eines Buffer-Objektes. Quelle [Hit02]. 71 Für den Zugriff und die Bearbeitung von Buffer-Objekten stehen effiziente Methoden zur Verfügung. Hardwarenahe Aspekte wie Byte-Order (Big- / Little-Endian) werden ebenfalls berücksichtigt. Ausführliche Beschreibungen zu dieser Thematik sind in [Hit02] beschrieben. Für schnelle I/O-Operationen oder für direkten Speicher-Zugriff ist der Direct-Buffer vorgesehen. Ein Direct-Buffer ist keine eigene Java-Klasse, sondern eine spezielle Form von ByteBuffer. Das folgende Beispiel zeigt einen typischen Ablauf. Public ByteBuffer getString() throws Exception { CharBuffer chars = CharBuffer.allocate(100); chars.put(“Hello World\n”); chars.put(“bye bye\n”); chars.flip(); Charset charset = Charset.forName(“US-ASCII”); ByteBuffer buffer = charset.newEncoder().encode(chars); ByteBuffer directBuffer = ByteBuffer.allocateDirect(buffer.limit()); directBuffer.put(buffer); return directBuffer; } ... Beispiel Direct ByteBuffer Es wird zunächst ein CharBuffer-Objekt erzeugt und mit Daten gefüllt. Mit der Methode flip() wird der Buffer auf die Übertragung vorbereitet. Dabei werden die internen Zeiger wie Limit und Position auf die richtigen Stellen gesetzt. Anschließend wird ein ByteBuffer-Objekt erzeugt, welches als Inhalt den ASCII-Code des CharBuffer-Objektes (Java-String enthält Unicode Zeichen) enthält. Das eigentliche Direct-Buffer Objekt wird mit ByteBuffer.allocateDirect() erzeugt und bekommt als Inhalt den ByteBuffer mit den ASCII-Zeichen. Das Direct-Buffer-Objekt kann nun beispielsweise über JNI direkt auf ein Low-Level-Device geschrieben werden. Wie bereits oben erwähnt, sind seit Java 1.4 folgende neue JNI Funktionen für das Handling mit Direct-Buffer hinzugekommen: • NewDirectByteBuffer() Diese Funktion konstruiert ein Direct-Buffer-Objekt aus einem (evtl. außerhalb der JVM reservierten) Speicherbereich. Das Direct-Buffer Objekt wird in der Regel an die Jave-Seite für die Weiterverarbeitung zurückgeliefert. • GetDirectBufferAddress() Der Zugriff auf ein Direct-Buffer Objekt, das innerhalb der JVM erzeugt wurde, kann mit Hilfe dieser Funktion erfolgen. • GetDirectBufferCapacity() Die Größe des Direct-Buffer Objektes wird mit dieser Funktion ermittelt. Das folgende Beispiel zeigt, wie der Zugriff auf einen Direct-Buffer in JNI-Native-Code erfolgen kann. native private java.nio.ByteBuffer getBuffer( java.nio.Buffer ptr); 72 JNIEXPORT jobject JNICALL example_Example_GetBuffer( JNIEnv *env, jobject _unused, jobject ptr) { ... buffer = (*env)->GetDirectBufferAddress(env, ptr); ... return (*env)->NewDirectByteBuffer(env, mem, 100); } Beispiel JNI Funktionen für Direct ByteBuffer 4.2 Implementierung Auf dem Target liegt eine IBM J9 Java-Virtual-Machine in der Version 1.3 vor. D.h. das von JOGL eingesetzte NIO Package ist nicht vorhanden. Ein weiteres Problem ist die fehlende Native-AWT-Kompatibilität von IBM J9. Diese Kompatibilität wird bei der Zertifizierung nicht gefordert. Beide Faktoren zusammen führen dazu, dass eine Portierung von JOGL in einen vernünftigen Zeitrahmen ausgeschlossen ist. Des Weiteren ist SWTOGL auch für Java 1.3 ausgelegt und SWT ist auf QNX-Photon verfügbar. Aus diesen Gründen wurde für die Portierung SWTOGL ausgewählt. Wie bereits in Kapitel 4.1.3.1 gezeigt, besteht die Hauptaufgabe bei der Portierung darin, den Grafik-Context des SWT-Fensters zu ermitteln und an die Native-OpenGL-Funktionen weiterzuleiten. Analog wie bei der X-Window/GTK Version von SWT wird der GrafikContext unter Photon durch entsprechende Methoden exportiert. Ein Blick in den Sourcecode von SWT-Photon verdeutlicht das Konzept. Der Grafik-Context wird von der Methode internal_new_GC der Interface-Klasse Drawable zurückgeliefert. In der Klassenhierarchie der GUI-Komponente wird diese Methode von der Widget-Klasse Control implementiert: Widget Control +internal_new_GC () «interface» Drawable +internal_new_GC () Abbildung 43 Klassendiagramm SWT Widget 73 aGLContext aControl OS Photon Lib new internal_new_GC pgCreateGC pgCreateGC Abbildung 44 Sequenz-Diagramm beim Instantiieren des GLContext Objektes Die Klassen Control und OS sehen folgendermaßen aus: public int internal_new_GC (GCData data) { checkWidget(); int phGC = OS.PgCreateGC(0); if (phGC == 0) SWT.error(SWT.ERROR_NO_HANDLES); ... return phGC; } Control.java public static final native int PgCreateGC(int size); JNIEXPORT jint JNICALL OS_NATIVE(PgCreateGC) (JNIEnv *env, jclass that, jint arg0) { jint rc = 0; OS_NATIVE_ENTER(env, that, PgCreateGC_FUNC); rc = (jint)PgCreateGC(arg0); OS_NATIVE_EXIT(env, that, PgCreateGC_FUNC); return rc; } Java- und C-Code aus OS.java und os.c Aus den oben gezeigten Sourcecode-Teilen wird ersichtlich, dass der Photon-Grafik-Context von SWT fehlerfrei exportiert wird. Für eine prototypische Portierung sind mindesten drei Funktionen anzupassen: • • • CreateContext() MakeCurrent() SwapBuffers() 74 Es ist also eine neue Java-Klasse sowie die dazugehörige C-Library mit den drei Funktionen zu realisieren. Auf der Java-Seite wird dazu ein neues Package parallel zu der X11/GTK Version erstellt. aGLContext aControl OS Native Photon Lib PhotonGL Native OSMesa Lib new internal_new_GC pgCreateGC pgCreateGC glPhotonCreateContext createOSContext Abbildung 45 Sequenz-Diagramm CreateContext Die neue PhotonGL-Klasse sieht wie folgt aus: package org.eclipse.swt.opengl.internal.photon; import org.eclipse.swt.opengl.Library; public class PhotonGL { static { Library.loadLibrary("photonGL"); } public static final native int glPhotonCreateContext(int gc); public static final native boolean glPhotonMakeCurrent(); public static final native void glPhotonSwapBuffers(); } PhotonGL.java Der äquivalente C-Code ist nachfolgend aufgelistet: JNIEXPORT jint JNICALL GL_NATIVE(glPhotonCreateContext) (JNIEnv *env, jclass that, jint gc) { DEBUG_CALL("glPhotonCreateContext\n") return glICMCreateContext(SCREEN_WIDTH_3D, SCREEN_HEIGHT_3D, 0, gc); } JNIEXPORT jboolean JNICALL GL_NATIVE(glPhotonMakeCurrent) (JNIEnv *env, jclass that) { 75 DEBUG_CALL("glPhotonMakeCurrent \n") return glICMMakeCurrent(); } JNIEXPORT void JNICALL GL_NATIVE(glPhotonSwapBuffers) (JNIEnv *env, jclass that) { DEBUG_CALL("glPhotonSwapBuffer \n") glICMSwapBuffer(); } PhotonGL.c Aus dem C-Code wird anschließend eine Shared-Library mit dem Name libphotonGL.so erstellt. Diese Shared-Library muss zur Laufzeit vom System auffindbar sein, z.B. durch Setzen der System-Variable LD_LIBRARY_PATH. In der Klasse GLContext sind die drei Methoden wie folgt implementiert36. public GLContext (Drawable drawable) { if (drawable == null) SWT.error (SWT.ERROR_NULL_ARGUMENT); this.drawable = drawable; data = new GCData (); gc = drawable.internal_new_GC (data); PhotonGL.glPhotonCreateContext(gc); } public void setCurrent () { PhotonGL.glPhotonMakeCurrent(); } public void swapBuffers () { PhotonGL.glPhotonSwapBuffers(); } GLContext.java Die neuen Methoden sind völlig transparent und können von jedem SWTOGL-Programm ohne Code-Änderungen verwendet werden. Der folgende Code zeigt ein lauffähiges Beispiel. public class HelloWorld { private Canvas glCanvas; private GLContext context; public void run() { // init SWT Display display = new Display(); Shell shell = new Shell(display); shell.setLayout( new GridLayout()); shell.setLocation(1,1); 36 Die Klasse GLContext ist in der vorliegenden Version von SWTOGL nicht als Interface oder abstrakte Klasse ausgelegt, d.h. alle plattformabhängigen Codes werden hier fest kodiert. Für unterschiedliche Plattform werden entsprechend angepasste Versionen ausgeliefert. 76 GridData gridData = new GridData(); gridData.heightHint = 97; // hack gridData.widthHint = 320; gridData.verticalAlignment = GridData.BEGINNING; glCanvas = new Canvas(shell, SWT.NONE); glCanvas.setLayout(new GridLayout()); glCanvas.setLayoutData(gridData); glCanvas.setSize(320, 96); // needed for windows shell.pack(); shell.open(); context = new GLContext(glCanvas); context.setCurrent(); GL.glMatrixMode(GL.GL_PROJECTION); GL.glLoadIdentity(); GL.glOrtho(0.0, 1.0, 0.0, 1.0, -1.0, 100.0); GL.glClearColor(0.7f, 0.7f, 0.7f, 0.7f); GL.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT); GL.glLoadIdentity(); GL.glBegin(GL.GL_POLYGON); GL.glVertex3f(0.25f, 0.25f, 0.0f); GL.glVertex3f(0.75f, 0.25f, 0.0f); GL.glVertex3f(0.75f, 0.75f, 0.0f); GL.glVertex3f(0.25f, 0.75f, 0.0f); GL.glEnd(); GL.glFlush(); context.swapBuffers(); while( !shell.isDisposed()) { } display.dispose(); } Beispiel Programm mit SWTOGL/Photon auf dem Target Zum Ausführen des Programms auf dem Target sind für unterschiedliche Komponenten einige Java- und C-Libraries erforderlich: • JRE Die Basis-Klassen der Java Runtime Environment von J9 sind: o JclMax/classes.jar o runtime.jar o jface.jar • SWT Die Basis SWT-Klassen sind in den folgenden Libraries zusammengefasst: o Java: swt.jar o C: libswt-photon-3138.so • SWTOGL Das potierte OpenGL-Binding sowie die Shared-Libraries dazu sind hier zu finden: o Java: swtopengl.jar o C: libgl-photon.so o C: libphotonGL-photon.so 77 • OpenGL Die OpenGL-Mesa-Libraries: o OSMesa.so o GL.so o GLU.so • Anwendung Die Java-Klassen der eigentlichen Anwendung: o Gears.class All diese Libraries müssen zur Laufzeit von der JVM auffindbar sein. Ein möglicher JVMAufruf kann wie folgt aussehen: export JAVAROOT=/usr/qnx630/host/qnx6/x86/usr/qde/eclipse/jre/ export CLASSPATH=.:$JAVAROOT/lib/jclMax/classes.zip:\ $JAVAROOT/lib/eclipse/swt.jar:\ $JAVAROOT/lib/eclipse/jface.jar:\ $JAVAROOT/lib/eclipse/runtime.jar:\ /root/java/swt-opengl-photon/swtopengl.jar export LD_LIBRARY_PATH=$JAVAROOT/bin:\ /root/Mesa-6.0-OS/lib:/root/java/swt-opengl-photon $JAVAROOT/bin/j9 Gears Beispiel-Aufruf von Java-Gears-Demo 4.3 Evaluierung und Diskussion Um die Funktionsfähigkeit der durchgeführten Portierung zu prüfen, wurde die Java-Version des Gears-Demoprogramm aus dem JOGL-Beispiel für Tests auf dem Target adaptiert. Abbildung 46 zeigt einen Screenshot des lauffähigen Gears-Demoprogramms. Aus dem Blickwinkel der Grafikdarstellung ist kein Unterschied zu der C-Implementierung festzustellen. Abbildung 46 Gears Demo in Java mit SWTOGL Zur Beurteilung der Ergebnisse wurde ein einfacher Performance-Test durchgeführt, bei dem jeweils die Framerate (FPS) der Java-Version und einer C-Version auf dem Target gemessen wurde. Und zwar jeweils mit und ohne Bildschirm-Ausgabe. Tabelle 6 zeigt die Ergebnisse 78 Mit Ausgabe Ohne Ausgabe FPS mit Java 32 44 FPS mit C 50 59 Tabelle 6 Messergebnisse Gears-Demo mit Java/SWTOGL und C auf dem Target Im direkten Vergleich ist die Framerate der Java-Version erheblich schlechter. Ziel der Portierung war allerdings nicht primär die Performance zu beurteilen, sondern viel mehr einen Einblick über die Machbarkeit zu gewinnen. Die erfolgreich durchgeführte Portierung von SWTOGL bestätigt, dass die Entwicklung von 3D-Applikationen für das Target mit Java möglich ist. Eine fundierte Leistungsbewertung ist bei dem derzeitigen Software-Stand nicht möglich. Die schlechte Performance bei den durchgeführten Tests ist auf die noch nicht optimal implementierten OpenGL-Binding zurückzuführen. Für eine gute Gesamt-Performance sind folgende Punkte erforderlich: • eine leistungsfähige JVM, • ein effizientes Java-Binding und • eine (evtl. hardwarebeschleunigte) OpenGL-Library. Nur durch das optimale Zusammenwirken dieser Komponenten wird Java im Bereich 3DGrafik auf Embedded-Systemen konkurrenzfähig sein. Die lange Startzeit der JVM beim Hochfahren des Systems ist ein Hauptargument gegen den Einsatz von Java im Automotive-Bereich. Dieser Nachteil fällt hier nicht so sehr ins Gewicht. Ein Navigationssystem, das 3D-Karte in Java rendert, muss nicht sofort beim Start verfügbar sein. Auch eine kleine Verzögerung bis zum Erscheinen des ersten Bildes kann durchaus akzeptiert werden. JOGL in Verbindung mit Java 1.4 stellt im Moment das beste OpenGL-Binding dar. Insbesondere der konsequente Einsatz von NIO-Buffer führt zur performanten Verarbeitung auch mit komplexen und großen 3D-Modellen. Sollte die IBM J9 endgültig in der Version 1.4 vorliegen, so ist es sicherlich eine interessante Aufgabe, JOGL auf die J9- / SWT-Plattform zu portieren. Der Aufwand dürfte sich in Grenzen halten, da die Sourcecodes zum Großteil durch intelligente Generator-Tools erstellt werden. SWTOGL ist ein schlankes OpenGL-Binding, welches noch in der Entwicklungsphase befindet und noch keine Produktions-Qualität erreicht hat. Gerade durch das einfache Design ist es für eine Portierung ideal geeignet. Fällt die Design-Entscheidung für einen objektorientierten Ansatz nach dem Szenen-GraphModell, so ist der Einsatz von Java3D aus softwaretechnischen Sicht die beste Alternative. Laufzeitzeitverhalten und Portierbarkeit auf dem Target müssen aber noch eingehender untersucht werden. Dank der Open-Source-Politik der oben evaluierten Produkte ist es möglich, Einblicke in die interne Funktionsweise zu gewinnen und eine erste Bewertung vorzunehmen. Das Analysieren des Sourcecodes war sehr lehrreich; es weckt Interesse auf weiterführende Untersuchungen. 79 5 3D-Modelle und Fisheye View Echtzeit-3D-Darstellung von POI-(Point of Interest)-Objekten oder z.B. eine Navigationsfahrt durch Städte mit realitätstreuer 3D-Darstellung der Umgebungsgebäude sind Funktionen, die die Navigationssysteme der nächsten Generation anbieten werden. Bereits heute sind einige Großstädte in Deutschland komplett digitalisiert. Auch die POI-Datenbank der Kartenzulieferfirmen wächst von Tag zu Tag. Für die Weiterverarbeitung werden die eingescannten Daten in 3D-Modellierungsprogramme importiert und in entsprechende Formaten abgespeichert. Ein Navigationssystem muss also in der Lage sein, 3D-Modelle aus der Datei zu lesen und zu visualisieren. 3D-Viewer sind im PC-Bereich für verschiedene 3D-Formate verfügbar. Ob sich allerdings ein solcher 3D-Viewer auch auf dem Target mit OpenGL realisieren lässt, ist noch ungeklärt. Im Rahmen dieser Arbeit soll daher ein 3D-Viewer implementiert werden, der sowohl kleinere POI-Objekte als auch größere 3D-Stadt-Modelle visualisieren kann. Als weiteres Feature soll eine Fisheye-View realisiert werden. Dabei soll ein 3D-Stadt-Modell ähnlich wie bei einem Globus auf eine Halb-Kugel projiziert werden. 5.1 Grundlagen 5.1.1 3D-Formate In den Kapitel 2, 3 und 4 wurden bereits mehrfach gezeigt, wie ein 3D-Objekt oder gar ein komplettes Gelände aus Vertices und Texturen mit einem OpenGL-Programm erzeugt wird. In der Praxis werden 3D-Objekte jedoch mit einem Modellierungsprogramm erstellt und anschließend in einer Datei eines bestimmten 3D-Formates abgespeichert. Sowohl ein einzelnes Objekt als auch eine komplette 3D-Szene können so wieder eingelesen und dargestellt werden. Wie in anderen Gebieten der Informatik existieren auch hier eine Menge von proprietären, wie offenen, 3D-Formate. Grundsätzlich gibt es verschiedene Möglichkeiten, 3D-Objekte zu beschreiben. Neben den von OpenGL verwendeten Gitter- und Flächen-Modellen, bei denen ein 3D-Objekt durch Kanten zwischen einzelnen Punkten sowie von Kanten eingeschlossene Flächen (Polygonzügen und –Flächen37) repräsentiert werden, gibt es noch weitere Modelle. Auch wenn diese alternativen Modelle nicht von den gängigen 3D-Modellierungstools verwendet werden, werden diese nachfolgend kurz erwähnt: • Punktwolke Eine sehr einfache Form der Darstellung. Dabei werden 3D-Objekte durch eine Menge von einzelnen Punkten im Raum beschrieben. Es gibt keine Informationen über Kanten, Flächen oder Volumen des Objektes. • Volumenmodell Bei diesem Modell wird ein 3D-Objekt durch geometrische Körper beschrieben. Ein Objekt kann entweder aus einfachen Grund-Objekten wie Würfel, Kugel, Pyramide usw. oder aus komplexeren Grund-Objekten wie Freiformkörper oder Fraktale 37 vgl. Kapitel 2.1.2 80 zusammengesetzt werden. Bekannter Vertreter dieses Modells ist das Voxel-Modell. Dabei wird ein Objekt aus einer Menge von kleineren Objekten gleicher Größe (in der Regel Würfel) approximiert. Weitere Vertreter des Volumen-Modells sind beispielsweise Octree, Bintree oder CSG-Tree. Ausführliche Informationen darüber sind in [Wat02] verfasst. Neben dem zugrunde gelegten Darstellungsmodell lassen sich 3D-Formate je nach Einsatzgebiet und Anforderungen in verschiedene Kategorien unterteilen. Die wichtigsten sind Folgende: • CAD/CAM Formate CAD-Systeme werden typischerweise in der Konstruktion und Fertigung eingesetzt. Nicht die realistisch gerenderte Darstellung, sondern die präzise und detaillierte Beschreibung des Objektes steht hier im Vordergrund. Gängige Formate sind: o DXF: Ein von der Firma Autodesk (AutoCAD) spezifiziertes Format zum Austausch von CAD-Daten. o IGES: ANSI Standard • Modellierung- und Animationsformate Im Gegensatz zu CAD-Formate enthalten Modellierungsformate zusätzlich zu den Objekt-Daten auch die komplette Beschreibung der Szene inklusiv Effekte, Lichter, Kamera-Einstellung usw. Einsatzgebiete von Modellierungsprogrammen reichen von High-End-Simulatoren über die Film-Industrie bis zu der Computerspiel-Branche. Beispiele dieses Formates sind: o 3DS: Dieses Format wird von dem Modellierungstool 3D Studio der Firma Autodesk verwendet. Obwohl die Spezifikation nicht offen gelegt ist, wird das Format aufgrund der weiten Verbreitung des Programms sehr häufig als Austausch-Format eingesetzt. o OBJ: Das Format von dem Programm Advance-Visualizer der Firma Alias Wavefront, in der Praxis auch als Austausch-Format sehr beliebt. o MB: Das Format des High-End Modellierungsprogrammes Maya von Alias. o BLEND: Das Open-Source Modellierungsprogramm Blender erfreut sich immer größerer Beliebtheit. Obwohl es durch die offene Architektur praktisch mit allen gängigen 3D-Formaten umgehen kann, hat das Programm auch ein eigenes Format. o U3D: Das Universal-3D Format wurde von der ECMA als ECMA-363 standardisiert und dient als herstellerübergreifendes 3D-Austausch-Format. • Internet-basierte Formate Diese Formate wurden speziell für die Übertragung und Darstellung von 3D-Szenen über Internet entwickelt. Durch entsprechende Client-Programme oder BrowserPlug-In werden die übertragene 3D-Szene beim Anwender sichtbar. Aspekte wie Kompression oder Streaming werden bei diesen Formaten aufgrund der oft geringen Bandbreite des Netzes mitberücksichtigt. Typische Vertreter sind: o WRL: Datei dieses Formates enthält Szenenbeschreibung gemäß der VRML Spezifikation. VRML (Virtual Reality Modeling Language) ist eine standardisierte (ISO/IEC 14772) Sprache zur Beschreibung interaktiver 3DSzene. o X3D: Das Nachfolger-Format von WRL, ist XML basiert und weist gegenüber WRL einige Verbesserungen auf. 81 In den folgenden Abschnitten wird der Aufbau für zwei häufig verwendete Formate (3DS und OBJ) der 3D-Modellierung näher erläutert. 5.1.1.1 Das OBJ-Format Das OBJ-Format des Modellierungsprogrammes Advanced Visualizer der Firma Wavefront ist ein ASCII Format und daher besonders als Austausch-Format geeignet. Die Spezifikation ist öffentlich zugänglich. Das Format unterstützt Linien, Polygone, Freiform-Kurven (z.B. Bezier, B-Spline) und Oberflächen. Linien und Polygone werden durch ihre Punkte beschrieben. Bei Kurven und Oberflächen dagegen sind es die Kontrollpunkte oder geometrische Parameter des jeweiligen Objektes. Daten werden zeilenweise interpretiert. Eine Zeile beginnt mit einem Schlüsselwort gefolgt von schlüsselwortabhängigen Attributen. Um beispielsweise ein Polygon zu definieren, werden zunächst die Eckpunkte mit dem Schlüsselwort v spezifiziert und anschließend die entstandene Fläche mit dem Schlüsselwort f angegeben. Dabei legen die Attribute von f die Reihenfolge der zuvor definierten Eckpunkte fest. Das folgende Beispiel beschreibt ein einfaches Dreieck. v v v f 0.0 0.0 0.0 1 2 0.0 0.0 1.0 0.0 0.0 1.0 3 Farb- und Material-Informationen werden nicht in dem OBJ-Format definiert, sondern in einer externen Material-Library (Material Library File, *.MTL). Diese Material-Library kann mit dem Schlüsselwort mtllib geladen werden. Die Material-Library enthält Informationen wie Farbe, Material-Beschaffenheit oder Eigenschaften wie Transparenz und Lichtbrechung. In der OBJ-Datei kann ein Material mit dem Schlüsselwort usemtl referenziert werden. Alle folgenden Flächen bekommen dieses Material bis zum nächsten usemtl Befehl zugeordnet. Szene-Beschreibung ist in dem OBJ-Format nicht explizit vorgesehen. Es können allerdings Objekt-Gruppen und somit Hierarchien definiert werden. 5.1.1.2 Das 3DS-Format Ein sehr weit verbreitetes 3D-Format ist das 3DS von Autodesk 3D Studio. Die Spezifikation dieses Formats ist nicht offen gelegt. Es existieren jedoch einige inoffiziellen Beschreibungen zu dem Format. Im Gegensatz zum einfach aufgebauten OBJ-Format ist 3DS ein binäres Format und von der Struktur her etwas komplexer. 3DS-Dateien bestehen aus sog. Chunks (Blöcken). Jeder Chunk beginnt mit einer ID, die die Art der in diesem Chunk enthaltenen Daten festlegt. Darauf folgt die Länge des Chunks. Ein 3DS-Loader-Programm kann somit ein Chunk mit unbekannter ID ignorieren und einfach zum nächsten Chunk überspringen. Chunks sind in Hierarchie-Ebene eingeordnet. In dieser Hierarchie können die meisten Chunks nur an bestimmten Stellen vorkommen. Einige wenige Chunks dagegen, wie etwa Farbwerte, können an unterschiedlichen Positionen auftreten. Abbildung 47 zeigt den vereinfachten Aufbau schematisch. 82 Abbildung 47 Schematische Darstellung des 3DS-Formats Der MAIN3DS38 Chunk leitet die Hierachie in der Datei ein. Die Konfigurationen des 3D Studio Desktops zum Zeitpunkt der Speicherung werden in den Chunks wie EDIT_CONFIGS1 oder EDIT_VIEW_P1 festgehalten. Material-Informationen stehen global in dem EDIT_MATERIAL Chunk und können von Polygon-Flächen individuell referenziert werden. Mehr von Bedeutung für einen 3DS-Viewer ist der EDIT_OBJECT Chunk. Hier sind sowohl die geometrischen Objekte als auch Objekte wie Licht und Kamera definiert. Geometrische Objekte werden in 3DS als Dreieck-Gitter (Triangle-Mesh) unter dem Chunk OBJ_TRIMESH gespeichert. Der Sub-Chunk TRI_VERTEXL beinhaltet die Anzahl der Punkte sowie deren Koordinaten des Triangle-Mesh Objektes. Die Oberflächen des TriangleMesh Objektes werden in dem Sub-Chunk TRI_FACEL1 definiert, dabei wird jede PolygonFläche durch 3 Punkte (Index auf TRI_VERTEXL) beschrieben. Die im EDIT_OBJECT Chunk definierten Objekte werden der Reihe nach hintereinander gespeichert. Die Strukturen und Hierarchien der gesamten 3D-Szene sind im KEYF3DS Chunk abgelegt. Dieser Chunk ist eigentlich für die Definition von sog. Keyframes (Schlüsselpositionen von Objekten für Animationen) vorgesehen und bietet aber auch Möglichkeiten, Objekt-Hierarchien zu definieren. 5.1.2 Fisheye View In der Computergrafik versteht man unter einer klassischen Fisheye View eine wie in Abbildung 48 (a) gezeigten Projektion. Diese Bilder entstehen entweder durch Aufnahme mit einer entsprechenden Kamera und Linse oder durch Nachbearbeitung mit einem Bildverarbeitungsprogramm. 38 Die Benennung der Chunks sind aus [Bou96] übernommen. 83 Abbildung 48 (a) Eine klassische Fisheye-Projektion. (b) Eine normale 2D-Karte Darstellung. (c) Vergrößerung mit einer Fisheye-Linse Solche Visualisierungseffekte werden auch als eine Art Vergrößerungslinse (Fisheye-Linse) beispielsweise bei der Darstellung von Landkarten verwendet (siehe Abbildung 48 (b) (c)). Im 3D-Bereich wird diese Vergrößerungstechnik (3D-Fisheye Zoom nach [Raab96]), u.a. auch für die Visualisierung von medizinischen Daten eingesetzt. Abbildung 49 Hervorhebung durch 3D-Fisheye Zoom bei der Visualisierung von medizinischen Daten nach [Raab96] Überträgt man diese Darstellungseffekt auf eine 3D-Karte mit plastischen Häuser-Modellen, so entsteht eine interessante Perspektive. Zum Beispiel stehen Häuser auf einem kugelförmigen Grundriss wie auf der Erdkugel. In der Mittel der Kugel werden die Objekte vergrößert und mit mehr Details dargestellt als Objekte am Rande der Kugel. Eine mögliche Projektion dieser Art ist in Abbildung 50 als Schnitt dargestellt. 84 z x (a) (b) Abbildung 50 (a) Objekte/Häuser auf der Ebene. (b) Projektion der Objekten/Häuser auf eine Kugelfläche. Die in der Literatur beschriebenen Verfahren von Fisheye-Projektionen beschränken sich meist auf die Vergrößerung von 2D-Bilder. Auch das in [Raab96] vorgestellte Verfahren lässt sich hierfür nur bedingt übertragen. Im Bereich der Kartografie existieren zwar eine Menge von Verfahren zum Thema Kugel-Projektion39. Sie behandeln jedoch den Umgekehrten Fall, nämlich die Projektion der Daten von der Kugeloberfläche (Erde) auf eine Ebene (Landkarte). Ausserdem sind diese Daten zwei dimensional. Die Grundlage für eine derartige Projektion kann aber auch aus einfachen geometrischen Überlegungen hergeleitet werden. Ein Punkt p(x, y, z) auf der Kugelfläche mit dem Radius r kann durch die Winkel θ und φ wie folgt ausgedrückt werden (siehe Abbildung 51). x = r · sin θ · sin φ y = r · sin θ · cos φ z = r · cos θ 39 (1) (0 ≤ θ ≤ π und 0 ≤ φ < 2π) Vgl. [Sosna] 85 z 180° p(r, θ, φ) θ r x 270° 90° φ y 0° Abbildung 51 Winkel θ und φ in Beziehung zu Punkt p Um die Koordinaten von Punkt p’ auf der Kugelfläche nach (1) zu berechnen, müssen zunächst die Winkel θ und φ bestimmt werden. Der Punkt p aus der original Darstellung wird dabei auf die Ebene bei z=0 projiziert (siehe Abbildung 52). Ausgehend von dieser Position kann θ und φ ermittelt werden. z p’ x’ x z’ p z Ebene bei z=0 θ Projizierte Stelle auf der Ebene von p h r r+z x Abbildung 52 Herleitung der Projektion von p nach p’ bei φ = 90° oder 270° 86 Abbildung 53 zeigt die geometrische Beziehung zwischen dem Punkt p und den Winkel θ und φ. Daraus lässt sich die Formel (2) konstruieren. z p’ p x φ z y h x θ y Abbildung 53 Bestimmung von θ und φ θ = arctan( sqrt(x 2 + y2) / h) φ = arctan( x / y ) (2) Die Formel für die Koordinaten von Punkt p’(x’, y’, z’) auf der Kugeloberfläche kann aus (1) hergeleitet werden. x’ = (r+z) · sin θ · sin φ y’ = (r+z) · sin θ · cos φ z’ = ((r+z) · cos θ ) - h (3) Die Größe h und r können je nach gewünschtem Effekt (Krümmung und Verzerrung) angepasst werden. Dabei soll h logischerweise kleiner als r gewählt werden. 5.2 Implementierung Das für die Evaluierung zur Verfügung gestellte 3D-Stadt-Modell liegt in 3DS-Format vor. Es ist daher nahe liegend, ein 3DS-Viewer zu implementieren. Es existieren aufgrund der weiten Verbreitung dieses Formates einige Tutorials und Open-Source-Projekte im Internet. Das Beispielprogramm aus [GamTut] wurde als Basis der Implementierung herangezogen. Wie bereits im Kapitel 3.2 dargestellt, werden auch bei diesem Programm die SDL-Funktionen durch entsprechende glICM-Funktionen ersetzt. Wie bei der OpenGL-Programmierung üblich, teilt sich der Ablauf des Programms in zwei Phasen. In der Initialisierungsphase wird die 3DS-Datei eingelesen. In der Darstellungsphase 87 wird die 3D-Szene in einer Schleife abhängig von der Kamera-Einstellung gerendert und angezeigt. Das Kernstück des Programms ist die 3DS-Import-Funktion Import3DS(). Hier wird die 3DS-Datei gemäß der Formatbeschreibung in Kapitel 5.1.1.2 importiert. In der Funktion ProcessNextChunk() der Klasse CLoad3DS wird ein Chunk mit der Funktion ReadChunk() eingelesen. Je nach Chunk-Typ werden weitere Funktionen wie ProcessNextObjectChunk() oder ProcessNextMaterialChunk() rekuriv aufgerufen. Ist ein Objekt-Chunk gefunden, so werden die Vertex-Koordinaten dieses Objektes mit den Funktionen wie ReadVertices() oder ReadVertexIndices() eingelesen und in entsprechende Arrays abgespeichert. In der Darstellungsphase wird die Koordinaten-Transformation für die Fisheye-Projektion gemäß Formel (3) optional in der Funktion RenderScene() durchgeführt. Bei der Berechnung der Winkel φ sind die Vorzeichen der x- und y-Koordinaten zu beachten und Korrekturen ggf. vorzunehmen. Anschließend werden die Vertices mit der bekannten Funktion glVertexf() an OpenGL weitergeleitet und somit die 3D-Szene gerendet. 5.3 Diskussion Die mit dem Viewer eingelesenen und gerenderten POI-Objekte sind in Abbildung 54 dargestellt. Die Kenndaten der Objekten: (a) 437 Vertices, 336 Faces, 2 Objects. (b) 437 Vertices, 336 Faces, 2 Objects. Abbildung 54 Ein gerendertes POI Objekt. Ein Screenshot des Stadt-Modells ist in Abbildung 55 zu sehen. Die Kenndaten des Objektes: 48859 Vertices, 55887 Faces, 2435 Objects. Im direkten Vergleich zum POI-Objekt machen sich die fehlenden Texturen des Stadt-Modells optisch bemerkbar. Abbildung 55 Stadt-Modell als normale Perspektivendarstellung 88 Abbildung 56 zeigt das Stadt-Modell in Fisheye-View aus verschiedenen Blick-Richtungen. Hier sind an manchen Stellen Sprünge zu erkennen. Das liegt daran, weil die langen Linien in dem vorgelegten Modell nur durch einige wenige Stützpunkte modelliert wurden. Die später für die Produktion vorgesehenen Modelle werden mit mehr Punkten ausgestattet sein. Abbildung 56 Stadt-Modell in Fisheye-View aus verschiedenen Blick-Richtung Als Vergleich wurde ein kommerzielles Straßen-Modell40 mit dem Viewer auf einem X86 PC gerendert. Das Modell entspricht etwa den Szenen aus Computerspielen. Navigationssysteme der nächsten Generation könnten eine solche Ansicht darstellen. Das Rendern dieser Bilder nimmt aber erheblich mehr Zeit in Anspruch. Ohne Hardwarebeschleunigung ist eine solche Anwendung daher nicht realisierbar. 40 Quelle [TurboS] 89 Abbildung 57 Darstellung einer künstlichen 3D-Straßen-Szene 90 6 Zusammenfassung und Ausblick Ziel dieser Arbeit war anhand von ausgesuchten Themen die Möglichkeit der Integration von 3D-Grafik in einem Automotive-Embedded-System zu evaluieren. Dabei wurden in vier Kapiteln unterschiedliche Untersuchungen durchgeführt. In Kapitel 2 wurde eine mit 3D-Grafik animierten GUI zum Demonstrationszweck erörtert. Im Grundlagenteil dieses Kapitels wurden zunächst die Möglichkeiten der Integration von GUI mit 3D-Unterstützung in der HMI-Komponente sowie in das gesamte Automotive Framework diskutiert. Ausserdem wurde auf einige Basiskonzepte der OpenGLProgrammierung eingegangen, die auch im Rahmen dieser Arbeit eingesetzt wurden. Für die Realisierung der GUI wurde die Open-Source OpenGL-Implementierung Mesa auf dem Target portiert. Für die OpenGL-Anbindung zum QNX-Window-System Photon wurde eine eigene Entwicklung als Ersatz für das fehlende kommerzielle Produkt von QNX prototypisch implementiert. Die Ergebnisse bezüglich der Funktionalität waren sehr zufriedenstellend. Die Performance war dagegen, nicht zuletzt wegen der fehlenden Hardware-Beschleunigung, weniger überzeugend. Anzumerken bleibt, dass PerformanceOptimierung kein Gegenstand dieser Arbeit ist und somit als Thema für ergänzende Ausarbeitungen vergeben werden könnte. Für solche Ausarbeitungen würde sogar eine neue Target-Hardware mit einem hardwarebeschleunigten 3D-Grafik-Chipsatz und mit der OpenGL-Library ausgestattet zur Verfügung stehen. Dadurch lassen sich zusammen mit der Multi-Layer-Technik interessante 3D-GUI und 3D-Anwendungen realisieren. Im Kapitel 3 wurde ein Algorithmus mit LOD-Unterstützung für die Darstellung von 3DGeländen auf dem Target portiert. Das Ziel der Arbeit war, zu untersuchen, ob ein derartiges Verfahren, das normalerweise für High-End-Simulationen konzipiert ist, auch auf die verfügbare Target –Hardware mit beschränkten Ressourcen eingesetzt werden kann. Auch die Portierbarkeit eines komplexeren OpenGL-Programm sollte in diesem Rahmen gezeigt werden. Der eingesetzte SOAR-Algorithmus ist ein weit verbreitetes Verfahren. Varianten davon werden sowohl für Simulatoren in der Industrie als auch in Computerspielen verwendet. Im Grundlagenteil dieses Kapitels wurde kurz auf dieses Verfahren eingegangen. Für die Implementierung wurde ein auf dem Windows-Plattform lauffähiges OpenGL-Programm portiert. Die Portierung erfolgte weitest gehend problemlos, es waren hauptsächlich CodeAnpassungen in der Verbindungsschicht zu QNX-Photon erforderlich. Die eingesetzte MesaLibrary auf der SH4-Target-Plattform hat sich als sehr ausgereift erwiesen. Für die Evaluierung des Verfahrens wurden einige Tests durchgeführt. Die korrekte Arbeitsweise des LOD-Algorithmus konnte durch Einstellung der Parametern gezeigt werden. Weiterhin wurde gezeigt, dass Gittergröße von 512 x 512 Punkten derzeit die Obergrenze für das Target-System darstellt. Der implementierte Prototyp kann nur mit Geländen umgehen. Für ein richtiges 3DNavigationssystem müssen noch Strassen, Landschaftslayer und weitere Objekte, wie z.B. Häuser oder gar Stadt-Modelle, die wiederum in unterschiedlichen Formaten vorliegen, integriert werden. Für den produktiven Einsatz sind daher alternative Algorithmen zu evaluieren. 91 OpenGL Java-Bindings wurden im Kapitel 4 behandelt. Dabei wurden drei Open-Source OpenGL Java-Binding bezüglich Konzeption und Native-Anbindung eingehend untersucht. Diese Produkte haben jeweils ihre Stärken und Schwächen. JOGL ist die ReferenzImplementierung von Sun und ist konzeptionell ein „Eins zu Eins“-Binding. Bezüglich der Performance ist JOGL zurzeit das beste Open-Source Java-Binding auf dem Markt. Java3D verfolgt dagegen ein High-Level-Ansatz, hier werden 3D-Objekte in einer virtuellen Welt nach dem Szenen-Graph-Modell abstrahiert. Java3D ist nicht so performant wie JOGL, bietet aber viel mehr High-Level-Funktionalitäten. SWTOGL schliesslich ist ein experimentelles Java-Binding für das SWT-GUI-Framework. Aufgrund der fehlenden AWT-Unterstützung bei der installierten Java Virtual Machine J9 wurde SWTOGL für die Portierung ausgewählt. Die Portierung konnte mit den in Kapitel 2 beschriebenen Funktionen für die Photon-Anbindung erfolgreich durchgeführt werden. Die einwandfreie Funktionalität konnte durch anschließende Tests nachgewiesen werden. Die Performance ist wie erwartet nicht zufriedenstellend. Hier sind sowohl auf Seite der Virtual Machine (J9 ist noch nicht in der Version 1.4 verfügbar) als auch auf Seite der OpenGLAnbindung Verbesserungspotentiale gegeben. In Kapitel 5 wurde ein 3D-Viewer zum Visualisieren von 3D-Modellen auf dem Target portiert. Zusätzlich ist eine Fisheye-Projektion als Visualisierungseffekt implementiert. Ebenso wurde ein kurzer Überblick über die gängigen 3D-Formate gegeben. Ausserdem wurde die geometrische Herleitung der verwendeten Fisheye-Projektion dargestellt. Die Ergebnisse der Implementierung wurden anschließend gezeigt und diskutiert. Die vorliegende Arbeit zeigt, dass 3D-Grafik auf der getesteten Target-Hardware sowohl durchführbar als auch für einfache Anwendungen nutzbar ist. Benutzeroberflächen mit 3DAnimationen sind schon selbst mit einer 3D-Library auf Softwarebasis ohne Einschränkungen möglich. Die im Rahmen dieser Arbeit gewonnenen Erkenntnisse können als Basis bzw. als Einstieg für die Entwicklung zukünftiger 3D-Applikationen im Automotive-EmbeddedBereich herangezogen werden. 92 Literatur [Bou96] 3D-Studio File Format: http://astronomy.swin.edu.au/~pbourke/dataformat/3ds/. Stand 01.09.2005 [Feh04] J. Fehr: Who Gives a Blit; QNX Technical Articles; http://www.qnx.com/developers/articles/article_291_2.html. Stand 01.09.2005 [GamTut] Game Tutorials http://www.GameTutorials.com. Stand 01.09.2005 [GLUT] The OpenGL Utility Toolkit: http://www.opengl.org/resources/libraries/glut.html. Stand 01.09.2005 [Hit02] R. Hitchens: Java NIO; 2002; O’Reilly [ICM04] S.Freitag: Projektbericht InCarMultimedia; Wintersemester 03/04; FHDarmstadt [J3DTut] Java3D Tutorial: http://java.sun.com/developer/onlineTraining/java3d/index.html. Stand 01.09.2005 [Java3D] Java3D: https://java3d.dev.java.net/. Stand 01.09.2005 [JNITut] JNI Tutorial: http://java.sun.com/docs/books/tutorials/native1.1/. Stand 01.09.2005 [JOGL] JOGL: https://jogl.dev.java.net/. Stand 01.09.2005 [JCP] Java Community Process: http://www.jcp.org. Stand 01.09.2005 [Lin01] P. Lindstrom, V.Pascucci: Visualization of Large Terrains made Easy; 2001; Proceedings of IEEE Visualization 2001 [Mesa] Mesa 3D Library: http://www.mesa3d.org. Stand 01.09.2005 [MOST01] Multimedia and Control Networking Technology, Function Catalog MOST Cooperation, V1.0; 2001 [Nis04] A.Nischwitz, P. Haberäcker: Masterkurs Computergrafik und Bildverarbeitung; 2004; Vieweg Verlag [QNX3D] QNX: 3D Graphics Technology Development Kit: http://www.qnx.com/products/tech_dev_kits/3d_graphics.html. Stand 01.09.2005 [Photon04] QNX: Photon microGUI for QNX Neutrino 6.3, Programmer’s Guide; 2004; QNX Software System Ltd 93 [Raab96] Andreas Raab, Michael Rüger: D-ZOOM: Interactive Visualization of Structures and Relations in Complex Graphics; infix-Verlag; 1996 [SDL] Simple Directmedia Layer: http://www.libsdl.org. Stand 01.09.2005 [Shr04] D. Schreiner, M. Woo, J. Neider, T. Davis: OpenGL Programming Guide, 4rd edition; 2004; Addison-Wesley [SMLynx] Lynx3DM+ DataBook: Silicon Motion, Inc. ftp://ftp.siliconmotion.com.tw/databooks/SM722_Rev08.pdf. Stand 01.09.2005 [Sosna] D. Sosner: Vorlesungskript Geoinformationssysteme I, Universität Leipzig; http://www.informatik.uni-leipzig.de/~sosna/karten/netze.html. Stand 01.10.2005 [SWTOGL] SWT Experimental OpenGL Plug-in: http://www.eclipse.org/swt/opengl/opengl.html. Stand 08.10.2005 [TurboS] Turbo Squid Inc: http://www.turbosquid.com/FullPreview/Index.cfm/ID/168319/SID/250762/ blFP/1; Stand 01.09.2005 [Ulr00] T. Ulrich: Continuous LOD Terrain Meshing Using Adaptive Quadtrees; 2000; Gamasutra; http://www.gamasutra.com/features/20000228/ulrich_01.htm. Stand 01.09.2005 [Wat02] A.Watt: 3D-Computergrafik; 2002; Pearson Studium [Wie05] J. Wietzke, M.T. Tran: Automotive Embedded System; 2005; Springer Verlag 94 Anhang A: Technische Daten der Target-Hardware Abbildung 58 Fotografische Abbildung der Target-Hardware CPU Grafik-Chip RAM Flash ATAPI-Laufwerk Diaplay Schnittstelle Erweiterungsslot Netzwerk Angeschlossene Geräte Stromanschluss Stromaufnahme Betriebssystem Hitachi SH4, 200 MHz Silicon Motion Lynx3DM 32 MB 32 MB 20 GB 2,5“ Harddisk, DVD Laufwerk Farbdisplay, Auflösung 320 x 96 Pixel 2 x Seriell, MOST und CAN 1 x PCI, 1 x PCMCIA PCMCIA-Ethernet-Karte GPS-Antenne, Tuner und Verstärker über MOST 12V 4A QNX 6.3 Tabelle 7 Technische Daten der Target-Hardware. Quelle [ICM04]. 95 Gateway-Box Head-Unit Abbildung 59 Schematische Darstellung der Hardware. Quelle [ICM04]. 96 Selbständigkeitserklärung Ich versichere, dass ich die vorliegende Masterarbeit selbständig verfasst und keine anderen als die angegebenen Quellen und Hilfsmittel verwendet habe. ____________________________________ Tsz Kwong Yung 97