Florian Heidinger „Gegenüberstellung von Direct3D und Java3D am praktischen Beispiel“ Seminararbeit Java 3D “Gegenüberstellung von Direct3D und Java3D am praktischen Beispiel“ Florian Heidinger [email protected] 1 Florian Heidinger „Gegenüberstellung von Direct3D und Java3D am praktischen Beispiel“ Seminararbeit Java 3D “Gegenüberstellung von Direct3D und Java3D am praktischen Beispiel“ Inhalt Einführung 1 Ist ein Vergleich von Java3D und Direct3D möglich? 2 Architektur von Java3D und Direct3D 4 Warum gibt es Direct3D und was will es? 5 Eine Anwendung in Java3D 7 Ein neues geometrisches Objekt 7 Ein Klassenentwurf für Java3D 9 Klasse Pyramide (Java3D) 10 Klasse Pyramidenboden (Java3D) 11 Klasse Pyramidenflaeche (Java3D) 14 Eine einfache Beispielszene 16 Ausführung der internen Form 20 Die Anwendung in Direct3D 21 Initialisierung der Zeichenfläche 25 Initialisierung von Direct3D 26 Die Direct3D Rendering Pipeline 32 Die Szene in Direct3D 34 Geometriedefinition in Direct3D 37 Das 3D Koordinatensystem von Java3D und Direct3D 38 Beleuchtung in Direct3D 39 Texturen und Material in Direct3D 40 Zeichnen der Szene 42 Literaturhinweise 45 Anlage: Quellcodes 2 Florian Heidinger „Gegenüberstellung von Direct3D und Java3D am praktischen Beispiel“ Einführung Die Seminararbeit mit dem Thema „Gegenüberstellung von Direct3D und Java3D am praktischen Beispiel“ veranschaulicht grundlegende Gemeinsamkeiten und Unterschiede der beiden 3D-Graphik-APIs anhand eines konkreten Anwendungsbeispiels. Dabei soll auf die wesentlichen Aspekte, die bei der Konzeption und Umsetzung dieses Beispiels in beiden Technologien eine Rolle spielten aufgezeigt, erläutert und soweit dies möglich ist vergleichend gegenübergestellt werden. Ziel dieser Auseinandersetzung kann deshalb keine vollständige Betrachtung der Anwendungsschnittstellen, sondern lediglich eine Fokussierung auf wesentliche Merkmale und Unterschiede aus Sicht des Anwendungsentwicklers sein. Ist ein Vergleich von Java3D und Direct3D möglich? Ziel der Seminararbeit Im Gegensatz zu Vergleichen zwischen OpenGL und Direct3D, die im Grunde genommen immer der scheinbar entscheidenden Frage nachgehen: „Welche der beiden Technologien liefert schönere, schnellere Ergebnisse, ist vollständiger und einfacher zu handhaben ?“ , findet man kaum Literaturstellen oder Quellen im WWW, die sich mit einer Gegenüberstellung von Java3D und Direct3D befassen. Die Thematik dieser Seminararbeit erschließt also ein völlig neues, bisher fast unbearbeitetes Gebiet der Graphikprogrammierung. Man kann nun zwei Gründe für das Fehlen einer vergleichenden Beurteilung dieser, in der modernen Computergraphik zentralen Technologien annehmen. Entweder sind Java3D und Direct3D zwei völlig verschiedene, nicht vergleichbare „Welten“ oder sie stehen nicht in einem Konkurrenz-, sondern in einer Art Kooperationsverhältnis – einer Symbiose zweier graphischer Systeme, um einen Begriff aus der Ökologie zu bemühen. Wie die nachfolgenden Ausführungen deutlich machen werden ist die Antwort auf diese, In welchem Verhältnis stehen Java3D und Direct3D ? sich zwangsläufig bei der Auseinandersetzung mit diesen beiden GraphikprogrammierungsSchnittstellen aufkommenden Frage in der Realität nicht eindeutig zu liefern. Unbestritten ist die Tatsache, dass in Java3D und Direct3D tatsächlich zwei, zumindest auf den ersten Blick völlig verschiedene Konzepte und Philosophien aufeinandertreffen. Auf der einen Seite eine Allianz von Unternehmen wie Sun Microsystems, Intel, Apple oder IBM, um nur die wichtigsten zu nennen, welche 1997 die Java3D API ins Leben rief - auf der anderen Seite Microsoft mit seinem seit Anfang der 90er Jahre entwickelten DirectX-Paket, dessen wesentlicher Bestandteil Direct3D darstellt. Unternehmen mit völlig unterschiedlichen, ja teilweise gegensätzlichen Philosophien. Während Sun Microsystems seit jeher großen Wert auf Plattformunabhängigkeit, Kompatibilität und Portabilität („Write once, run anywhere“) legt und der Open-Source 3 Florian Heidinger „Gegenüberstellung von Direct3D und Java3D am praktischen Beispiel“ Gedanke vorherrscht, entwickelt Microsoft ausschließlich Lösungen für eigene Plattformen und Betriebssysteme, die zumeist lizensierungspflichtig sind, wobei das DirectX Self Development Kit (SDK) hier eine der wenigen Ausnahmen darstellt. Es ist samt Dokumentation frei verfügbar und kann unter http://www.mircrosoft.com/directx Direct3D SDK Download bezogen werden. Ferner treffen aufeinander der objektorientierte Ansatz, der in Java3D konsequent umgesetzt wurde und der eher prozedurale Ansatz, den man in Direct3D oder auch OpenGL verfolgt. Damit zusammenhängend finden auch andere Programmiersprachen Verwendung. Während in Java3D die einzig verwendbare Sprache natürlich Java darstellt, kann man in Direct3D scheinbar frei wählen, ob man seine Anwendung lieber in C/C++, Visual Basic oder Microsoft Java entwickeln möchte. Diese Liste der Gegensätzlichkeiten ließe sich noch beliebig fortsetzen. Allein eine Diskussion der Thematik welche Programmiermodelle und Sprachen für die Graphikprogrammierung besser geeignet sind, könnte leicht einige Dutzend Seiten füllen und würde hier nicht zum erwünschten Ergebnis führen. Trotzdem kann man festhalten, dass die Entwicklung verschiedener Graphik-APIs maßgeblich von den Entwicklung der Programmiermodelle, der Programmiersprachen und Einordnung Java3D und Direct3D in 3D Graphik APIs natürlich auch dem technischen Fortschritt in der Entwicklung leistungsfähiger GraphikHardware geprägt wurde. Diese Entwicklungen öffneten die Graphikprogrammierung für eine Vielzahl unterschiedlicher Anwender und Anwendungen mit verschiedenen Ansprüchen und Erwartungen an eine 3D-Graphik-API. APIBezeichnung 1. Siggraph Generation CORE 2. Generation PHIX/PEX 3. OpenGL® / Generation Direct3D® Hardware Programmier- Anwender / sprache Anwendungsbereiche Raster Terminals / FORTRAN Minicomputer Workstations C 3D C/C++ beschleunigte VisualBasic Workstations MS Java MCAD Unternehmen MCAE Workstation-Benutzer Animation Simulation professionelle Spiele Entwickler Netzwerk4. Generation Java3D® Rechner, Java Terminals, Java Spiele Entwickler Web-Designer Spiele-Konsolen 4 Florian Heidinger „Gegenüberstellung von Direct3D und Java3D am praktischen Beispiel“ Wie man aus der vorangehenden Tabelle ersehen kann, sind OpenGL und Direct3D Graphik-APIs der älteren dritten Generation, während Java3D eine API der neuen vierten Aus „Java Markets Whitepaper”, S. 2 Generation darstellt. Da die APIs einer bestimmten Generation in der Regel keine einfachen Weiterentwicklungen der APIs älterer Generationen darstellen, sondern jeweils völlig andere Technologien und Konzepte aufgreifen, sowie andere Benutzerkreise ansprechen, scheint ein Vergleich von Java3D und Direct3D in der Tat nicht besonders sinnvoll zu sein. Architektur von Java3D und Direct3D Warum also kann die Aussage der völlig verschiedenen „Welten“ nun trotzdem nicht eindeutig beantwortet werden ? Trotz aller Unterschiede muss folglich in irgend einer Form doch ein mehr oder weniger enger Zusammenhang zwischen Java3D und Direct3D bestehen. Betrachten wir die Architektur der beiden APIs etwas näher, um diesen Sachverhalt zu klären. Die Java3D API stellt eine „plattformunabhängige High-Level 3D API mit einem hohen Grad an Interaktivität“ (aus „The Java 3D API – Technical Whitepaper“), also eine abstrakte, völlig Was sind High- und Low-Level GraphikAPIs ? plattform- und hardwareunabhängige Programmier-Schnittstelle dar. Eine solche API benutzt in der Regel andere, hardwarenähere APIs, um ihre Aufgaben durchzuführen, indem sie entsprechende Aufrufe aus den Benutzerprogrammen in native Befehle umwandelt und der Low-Level API zur Verarbeitung übergibt. Als Low-Level APIs finden neben OpenGL und QuickDraw3D, wie vielleicht bereits vermutet unter WindowsBetriebssystemen vor allem die Direct3D API Verwendung. Man kennt hier verschiedene Versionen der Java3D zur Umwandlung in native Aufrufe gegen Direct3D oder OpenGL, die von der Sun Microsystems-Webseite (http://java.sun.com/) heruntergeladen und installiert werden können. Im Rahmen der Java3D API 1.2 für Direct3D Download Ausarbeitungen des Seminars „Java3D“ wurde jedoch hauptsächlich die Java3D API in der Version 1.2 gegen die Direct3D API verwendet. Java-Applet oder Anwendung Java3D Aus „Java 3D API“, S. 1 Java2 C/C++-Direct3D Anwendung Direct3D API Native Graphik-API Aufrufe OpenGL QuickDraw3D Graphik-Treiber und -Hardware 5 Florian Heidinger „Gegenüberstellung von Direct3D und Java3D am praktischen Beispiel“ Wie Java3D oder auch OpenGL, stellt Direct3D eine Sammlung von APIs, also eine Menge von Funktionen und Methoden dar, die von Anwendungsprogrammen oder High-Level APIs zur Graphikausgabe verwendet werden können. Zusammenspiel von Java3D und Direct3D Der vereinfachte Architekturüberblick lässt an dieser Stelle bereits vermuten, dass unter Windows-Betriebssystemen für graphische Szenen gleichen Inhaltes (in Java3D- und Direct3D-Anwendungen) das Ergebnis der von der Java Virtual Machine mit Hilfe der Java3D-Implementierung durchgeführten Umwandlung in native Aufrufe mehr oder weniger exakt der (kompilierten) Form der Direct3D-Anwendung entspricht. Warum gibt es Direct3D und was will es? Setzen wir uns zunächst mit der entscheidenden, existenzberechtigenden Frage nach der Notwendigkeit einer Low-Level Graphik- oder 3D-API wie Direct3D auseinander. Wie bereits anhand der tabellarischen Übersicht im vorangegangenen Abschnitt verdeutlicht wurde, sind die Entwicklungen im Graphikbereich vor allem auf Fortschritte in der Entwicklung von Hardware und Programmiersprachen zurückzuführen. Zu nennen sind hier vor allem das Aufkommen von Graphikkarten mit hardwareunterstützter 3D Beschleunigung, die Steigerung der Rechenleistung bei handelsüblichen Rechnersystemen und objektorientierter Programmiersprachen. Die daraus resultierende Vielfalt an Hardwarevarianten und Funktionen stellte die Entwickler von multimedialen oder graphisch aufwändigen Anwendungen vor nicht unerhebliche Probleme. Zweifelsohne mussten die Möglichkeiten der modernen Hardwarebeschleuniger genutzt werden, um Fortschritte in Geschwindigkeit und Darstellungs-Qualität der Anwendungen zu erreichen. Aufgrund der Vielzahl von Hardwarevarianten und Treibern, die im Umlauf waren und zumindest was die besonderen Funktionen anbelangte jeweils spezielle eigene Ansteuerungsroutinen verlangten, war dies jedoch nur eingeschränkt möglich. Dies hätte eine Vervielfachung des Entwicklungsaufwandes bedeutet und vom Anwendungsentwickler weitreichende Kenntnisse der Treiber- und Hardwarearchitekturen erfordert. Wie wurden graphische Anwendungen vor Direct3D oder Java3D programmiert ? Es fehlte ein einheitlicher Standard zur Hardwareansteuerung, der eine hardwaretransparente Anwendungsentwicklung ermöglichen konnte. Man beschränkte sich deshalb auf einen Kompromiss, indem man Kompatibilitäts-Gruppen bildete und jeweils nur die Hardware-Eigenschaften in den Anwendungen unterstützte, die bei allen Gruppenmitgliedern vorhanden und auf die gleiche Art und Weise ansteuerbar waren. Innovationen und spezielle Entwicklungen der Hardwarehersteller, die Verbesserungen in Graphikqualität und Performance hätten bringen können wurden also nicht genutzt. Diese Tatsache verhinderte jahrelang entscheidende Fortschritte im Graphikbereich, wie sie in anderen Hardwarebereichen an der Tagesordnung waren. Hatte die geschilderte Methode zwar graphisch wenig für den Endbenutzer zu bieten, besaß sie jedoch zumindest den Vorteil der Performanz, da auf DOS-Ebene direkt gegen die Hardwaretreiber programmiert wurde. Mit der schrittweisen Ablösung von MS-DOS entstand 6 Florian Heidinger „Gegenüberstellung von Direct3D und Java3D am praktischen Beispiel“ jedoch ein dringender Handlungsbedarf. Zwar kennt auch Windows selbst bereits eine Graphik-API, das sogenannte Graphics Device Interface (GDI), diese war jedoch keinesfalls in der Lage, den gestellten Anforderungen gerecht zu werden. Diese Tatsache führte schließlich zur Entwicklung von DirectX. DirectX sollte einerseits das Problem der unvollkommenen Hardwareausnutzung Was ist DirectX ? beseitigen, indem es sich als, die transparente Ansteuerung der zugrundeliegenden Hardware durch die Anwendung ermöglichende Software-Schnittstelle zwischen Anwendung und Hardware bzw. Hardwaretreiber legt, andererseits aber trotzdem eine möglichst performante und direkte Hardwareansteuerung (daher auch der Name Direct3D) unter Umgehung der Standard Windows-Graphikroutinen (GDI) ermöglicht. Hierfür stellt die DirectX API verschiedene, jeweils eine bestimmte Hardwarekategorie ansteuernde Bestandteile bereit, von denen für die Graphikausgabe Direct3D verantwortlich zeichnet. Wie bereits im Architekturüberblick erwähnt, handelt es sich bei der Direct3D API um eine sogenannte Low-Level API, die jedoch im Gegensatz zu vielen anderen maschinen- oder Was versteht man unter dem Hardware Abstraction Layer ? hardwarenahen Schnittstellen einen geräteunabhängigen Zugriff auf die Graphikhardware aus einem Anwendungsprogramm heraus erlaubt. Dies schließt die Verwendung von, durch die Hardware beschleunigten Funktionen ein. Möglich wird dies über das sogenannte Hardware Abstraction Layer (HAL), welches der Direct3D API signalisiert, welche Funktionen durch die Hardware unterstützt werden und welche nicht. Die API lässt dann so viele Berechnungen wie möglich durch die spezialisierten Graphikprozessoren erledigen, um eine Performance-Steigerung zu erzielen. Das HAL, welches vom Hardwarehersteller geliefert wird ist meist Bestandteil des Hardwaretreibers und sorgt dafür, dass die geräteunabhängigen Direct3D-Nachrichten in die entsprechenden treiberverständlichen Befehle umgesetzt werden. Von der Hardware nicht unterstützte Funktionen werden von der API softwareseitig emuliert. Dies stellt sicher, dass auch Rechnersysteme mit älteren Graphikkarten die Direct3D-Anwendungen ausführen können. Java3D-Anwendungen, welche die Java3D Implementierung für Direct3D verwenden, können also implizit die hardwarebeschleunigten Funktionen nutzen. Trotz der soeben angesprochenen Vereinfachungen für den Anwendungsentwickler fordern Low-Level-APIs im Allgemeinen und Graphik APIs der dritten Generation im Besonderen Wo liegen Nachteile von LowLevel APIs der 3. Generation ? sehr viel Programmiererfahrung, um anwendungsseitig ein optimales Laufzeitverhalten zu erzielen. Diese Tatsache führte zur Entwicklung der Java3D API, die vor allem für, in der Graphikprogrammierung weitgehend unerfahrene Programmierer oder GelegenheitsComputergraphiker eine interessante Alternative darstellt. Der Entwickler kann sich hier voll und ganz auf Konzeption und Umsetzung der eigentlichen Szene konzentrieren, ohne ständig die Auswirkungen (der Reihenfolge) bestimmter Anweisungen auf die Performance 7 Florian Heidinger „Gegenüberstellung von Direct3D und Java3D am praktischen Beispiel“ seiner Anwendung im Hinterkopf behalten zu müssen. Die Java3D Implementierung nimmt dem Anwendungsentwickler hier einen Großteil der Arbeit ab, für die ein Direct3DProgrammierer selbst Verantwortung trägt. Trotzdem sollte der Java3D-Entwickler einige grundlegende Regeln beachten, die der Java3D Implementierung die Aufgabe erleichtern und akzeptable Verbesserungen und Optimierungen erlauben. Eine Anwendung in Java3D Anhand eines konkreten Beispiels soll nun in einem ersten Schritt auf den Aufbau einer exemplarischen, in Java3D programmierten Szene eingegangen werden, die neben der Definition eines neuen geometrischen Objekts, seiner Materialeigenschaften und seiner Texturierung auch die Verwendung von Transformationen, Beleuchtung und Interaktionen in Java3D aufzeigt. In einem nächsten Schritt wird die, bei der Kompilierung und Ausführung dieser Szene entstehende interne Form diskutiert und versucht, die von der Java Virtual Maschine erzeugten nativen Aufrufe gegen die Direct3D-API mit Hilfe der Struktur einer geeigneten, in C++ gegen die Direct3D API programmierten Anwendung mit identischem Szeneninhalt zu erläutern. Ein neues geometrisches Objekt Im Rahmen der Beispielszene soll eine Pyramide mit quadratischer Grundfläche als neue, in den Java3D-Bibliotheken noch nicht vorhandene Geometrie-Klasse mit Hilfe der zur Verfügung stehenden graphischen Primitive modelliert werden. Auch nach intensiver Suche in verschiedenen Quellen im weltweiten Netz und Konsultation der Java3D API-Spezifikation, sowie des Tutorials konnte ich neben den beinahe obligatorischen Sphere, Cone- und Cube-Objekten keine weiteren, aus Primitiven gebildeten vordefinierten geometrischen Objekte oder Objektbibliotheken finden. Hier ist es also Aufgabe des Programmierers sich selbst einen Vorrat an immer wieder benötigten einfach zu handhabenden aus Bryce, 3DStudioMax, Lightwave oder Maya bekannten weiteren geometrischen Objekten wie Röhren (offene Zylinder), Torus („Donut“) oder eben Pyramiden zu programmieren, die in zukünftigen Projekten mit wenigen Anweisungen in den Szenengraphen eingebunden werden können. Die wachsende Java3D-Gemeinde wäre sicherlich über eine solche, frei zur Verfügung gestellte Erweiterung alles andere als unglücklich. 8 Florian Heidinger „Gegenüberstellung von Direct3D und Java3D am praktischen Beispiel“ Im folgenden sollen die Überlegungen zur Geometrieberechnung kurz aufzeigt werden. Eine einfache Pyramide mit quadratischer Bodenfläche besteht aus einem quadratischen Boden-Polygon und vier, jeweils auf dem Boden-Polygon aufsetzenden gleichschenkligen kongruenten Dreiecken, die an ihren Spitzen gegeneinandergeneigt sind. Die Berechnung der Geometrie gestaltet sich in diesem Fall sehr einfach und kann ohne weiteres ohne Zuhilfenahme von visuellen Modellierungswerkzeugen berechnet und erstellt werden. Sinnvollerweise konzipiert man ein neues geometrisches Objekt in Java3D aber auch in anderen graphischen Systemen so, dass zumindest seine wesentlichen geometrischen und Worauf sollte man bei der Definition eines neuen geometrisch en Objekts achten ? visuellen Eigenschaften durch Angabe von Parametern entsprechend den Wünschen der Benutzer eingestellt werden können. Man erreicht hierdurch eine optimale Wiederverwendbarkeit des Objektes, was es somit für die Aufnahme in eine Java3D Graphik- und Objektbibliothek geeignet macht. Um die Wiederverwendbarkeit der Pyramide für einen möglichst großen Benutzerkreis zu gewährleisten, sollte der Anwendungsentwickler in der Lage sein, die Seitenlänge der quadratischen Grund- oder Bodenfläche, sowie die Höhe der Pyramide über Parameter einzustellen. Aus diesen Angaben können dann die konkreten Modellkoordinaten leicht erzeugt werden. Seitenlänge der Pyramide (fBreite) Höhe der Pyramide (fHoehe) Den Mittelpunkt sollte bei neu zu schaffenden geometrischen Objekten immer der Ursprung des lokalen Koordinatensystems (Modellkoordinatensystem) bilden. Eine abweichende Modellierung der Modellkoordinaten kann bei Transformationen (Rotationen) zu unerwarteten oder unerwünschten Ergebnissen führen. 9 Florian Heidinger „Gegenüberstellung von Direct3D und Java3D am praktischen Beispiel“ Ein Klassenentwurf für Java3D Ein Klassenentwurf für die Pyramide, die zur direkten Verwendung im Szenengraphen von einer Gruppenklasse (Group oder GroupNode) abgeleitet werden sollte und vier Pyramidenflächen, sowie eine Bodenfläche, die jeweils von Shape3D abgeleitet werden als Referenz aggregiert, stellt sich wie folgt dar: Javax.media.j3d.* Group Shape3D setGeometry() setAppearance () addChild() Pyramide Pyramide fHoehe : float fBreite : float shp3dFlaechen : Shape3d[] app : Appearance Pyramide(float fHoehe, float fBreite) erstelleAppearance() : Appearance GetFlaechen() : Shape3D[] 1 1 1 Pyramidenboden fHoehe : float fBreite : float pt3fBoden : Point3f[] farbe : Color3f Pyramidenboden(float fHoehe, float fBreite, Appearance app) erstelleAppearance() : Appearance erstelleGeometrie() : Geometry getCoordinates() : Point3f[] 4 Pyramidenflaeche fHoehe : float pt3fBodenkoord : Point3f[] punkt1 : Point3f punkt2 : Point3f farbe : Color3f Pyramidenflaeche(float fHoehe, float fBreite, Appearance app) erstelleAppearance() : Appearance erstelleGeometrie() : Geometry 10 Florian Heidinger „Gegenüberstellung von Direct3D und Java3D am praktischen Beispiel“ Klasse Pyramide (Java3D) Im Konstruktor der von „Group“ abgeleiteten Klasse „Pyramide“ wird ein Objekt der Klasse „Pyramidenboden“ instanziiert, die von dieser Instanz errechnete Geometrieinformation (der Bodenfläche) über „getCoordinates()“ abgerufen und anschließend vier Objekte der Klasse „Pyramidenfläche“ erzeugt, die im Array „shp3dFlaechen“ zur späteren Verwendung abgelegt werden. Bei der Instanziierung der Pyramidenflächen werden den Konstruktoren die beiden jeweils richtigen über „getCoordinates()“ abgerufenen Eckpunkte der Bodenfläche übergeben, die dann in Kombination mit dem Raumpunkt der Pyramidenspitze ein Dreieck der Seitenfläche bilden. Die so aggregierten Referenzen werden dann in einer Schleife durchlaufen und über die von „Group“ geerbte „addChild()“-Methode dem Pyramiden-Knoten mittels einer ParentChild-Beziehung zugeordnet. Vor der Instanziierung der Teilflächen wird im Konstruktor die Methode „erstelleAppearance()“ aufgerufen. Diese hat die Aufgabe, ein geeignetes „Appearance“-Objekt zu instanziieren und mit Referenzen auf zu erstellende „ColoringAttributes“, „Material“ und „Texture2D“ Objekte zu füllen. Jedes dieser Objekte wird mit geeigneten Werten belegt, die entweder direkt dem Konstruktor übergeben oder über set-Methoden einstellt werden können. Dies beinhaltet das Setzen der Polygonfüllfarbe als RGB(A)-Wert und des Shading-Modus in der „Coloring-Attributes“ Instanz, der ambienten, diffusen, spekulären, und emittierten Objekt-Farbe, sowie der Größe der spekulären Glanzpunkte im „Material“-Objekt und das Laden und Zuweisen der gewünschten Oberflächentextur bzw. das Einstellen des Texturierungsmodus. Neben Gouraud- (SHADE_GOURAUD) und Flat-Shading (SHADE_FLAT) kennt Java3D Welche ShadingModi kennt man in Java3D (Direct3D) ? hier noch die zusätzlichen Optionen NICEST und FASTEST, die die am besten erscheindende bzw. die schnellste von der installierten Hardware bzw. der von Java3D genutzten Low-Level API unterstützte Shading-Variante auswählen. In Direct3D entspricht NICEST dem GOURAUD_SHADING, FASTEST entsprechend dem ebenfalls von Direct3D unterstützten FLAT_SHADING-Modus. Wie Direct3D, verwendet auch Java3D standardmäßig den GOURAUD-SHADING Modus, um dem Benutzer die bestmögliche Darstellungsqualität zu bieten. Anschließend muss ein Objekt der Klasse „Material“ erzeugt und dem „Appearance“-Objekt zugewiesen werden. Die Klasse „Material“ verwendet vier verschiedene RGB-Farbwerte für ambientes, diffuses, spekuläres und vom Objekt emittiertes Licht, sowie einen Wert, der die Reflexionseigenschafen des mit dem Material versehenen Objektes definiert. 11 Florian Heidinger „Gegenüberstellung von Direct3D und Java3D am praktischen Beispiel“ Zusätzlich zu den Materialeigenschaften für ein geometrisches Objekt ist häufig der Einsatz von Texturen sinnvoll, um feine komplexer Strukturen auf der Oberfläche durch eine Graphik nachzubilden. Die Verwendung einer Textur gestaltet sich in Java3D und Direct3D von der Vorgehensweise sehr ähnlich. Zunächst ist in einem geeigneten Bildbearbeitungsprogramm eine Texturgraphik zu erstellen. In beiden Technologien sind hier Bildabmessungen gefordert, die in Höhe bzw. Breite einer Potenz von 2 entsprechen. Unterschiede kann man jedoch bei den von den die Texturen ladenden Klassen bzw. Objekten feststellen. Während Java3D vor allem die gebräuchlichen JPG und GIF-Formate unterstützt, sind in Direct3D zusätzliche Formate wie zum Beispiel Bitmap-Dateien (BMP) verwendbar. Zum Laden einer Texturgraphik bieten beide APIs in ihren Klassen- / Objektbibliotheken geeignete Komponenten an, die eine Textur aus einer Datei in ein programmtechnisch verwendbares Objekt übertragen können. Eine dieser Komponenten stellt die Klasse „TextureLoader“ des Java3D Hilfspaketes „com.sun.j3d.utils“ dar, mit deren Hilfe sehr einfach eine Bilddatei in geeignetem Format in einer programmtechnischen Form als „ImageComponent2D“-Objekt abgelegt werden kann. Klasse Pyramidenboden (Java3D) Im Konstruktor der Klasse „Pyramidenboden“ werden die übergebenen Parameter in den Klassenvariablen abgelegt, die Klassen-Methode „erstelleGeometrie()“ aufgerufen, deren Rückgabereferenz, sowie die Rückgabereferenz der Klassen-Methode „erstelleAppearance()“ werden dann den von Shape3D geerbten Methoden „setGeometry()“ und „setAppearance()“ übergeben, um Geometrie und visuelle Eigenschaften des Pyramidenbodens entsprechend festzulegen. Die im Konstruktor der Klasse „Pyramidenboden“ aufgerufene Methode „erstelleGeometrie()“ errechnet die Eckpunkte der quadratische Bodenfläche bei einer, über die Konstruktorparameter eingestellten Pyramidenhöhe „fHoehe“ und einer Seitenlänge „fBreite“ und legt sie im Array „pt3fBoden“ ab. Java3D verwendet ein rechtshändiges V0 V3 Koordinatensystem. Bei der Angabe der In welcher Reihefolge sind die Raumpunkte in Java3D anzugeben ? Raumpunkte für das Bodenquadrat ist jedoch entgegen der sonstigen Gewohnheit darauf zu X achten, dass diese im Uhrzeigersinn und nicht entgegengesetzt spezifiziert werden, da die Bodenfläche aus einer unteren Ansicht der Pyramide sichtbar sein soll. V1 V2 Z Raumpunkte eines, aus der Aufsicht sichtbaren Polygons in einem rechtshändigen Koordinatensystem 12 Florian Heidinger „Gegenüberstellung von Direct3D und Java3D am praktischen Beispiel“ Zusätzlich zu den Geometriedaten ist es sinnvoll für Shading- oder Texturierungseffekte Normalenvektoren und Texturkoordinaten anzugeben. Java3D bietet zur Unterstützung des Entwicklers hier eine Helfer-Klasse zur automatischen Generierung der Normalenvektoren. Diese ist besonders bei komplexeren Objekten sehr hilfreich, erweist sich aber bei Primitiven, in denen Raumpunkte mehrfach verwendet werden als wenig handlich. Der Normalengenerator wandelt zum Beispiel ein TRIANGLEFANARRAY implizit zunächst ein indiziertes Primitiv um. Für jeden Eintrag der Indexliste wird dann ein Normalenvektor generiert, so dass für mehrfach verwendete Raumpunkte auch mehrere Normalenvektoren je nach Verwendungshäufigkeit berechnet werden. Die Anzahl der Vektoren, die der Generator liefert stimmt letztlich also nicht mit der Anzahl der Raumpunkte im ursprünglich erzeugten Primitiv überein und eine Zuweisung über „setNormals()“ verursacht einen Ausnahmefehler. Da die Geometrie der Pyramide von benutzerdefinierten Parametern abhängt, gestaltet sich die manuelle Berechnung der Normalenvektoren, wie sie in der Beispielanwendung Wie können Normalenvektoren von Hand errechnet werden ? durchgeführt wurde, nicht so einfach wie bei einem Würfel. Man benötigt schon ein klein wenig mathematisches Grundwissen, um eine einfache Möglichkeit der Normalenberechnung zu realisieren. Graphische Systeme verwenden als Grundelemente zur Nachbildung komplexerer Objekte Dreiecke. Diese besitzen den Vorteil, dass alle Eckpunkte auf einer Ebene liegen, also koplanar sind. Anders herum betrachtet kann man feststellen, dass diese Ebene durch die drei Raumpunkte eines Dreiecks eindeutig definiert ist. Sieht man die Raumpunkte als Vektoren an, so kann man die beiden, die Ebene aufspannenden Vektoren gewinnen, indem man jeweils den Vektor der Dreiecksspitze von den Vektoren der Basispunkte subtrahiert. Einen auf dieser Ebene senkrecht stehenden Vektor (den Normalenvektor) erhält man dann durch Bildung des Kreuzproduktes zwischen den beiden Vektoren. Bei der Durchführung dieser Vektorfunktion kann man die in der Klasse „Vector3f“ definierte Funktion „cross“ verwenden. Normalisiert man anschließend den Ergebnisvektor, so erhält man im Prinzip das gleiche Ergebnis, das auch der Normalengenerator liefert. Zur Verwendung von Texturen muss die Geometrieinformation um eine weitere zusätzliche Komponente, die Texturkoordinaten ergänzt werden, die in Java3D als Array von Objekte der Klasse „TexCoord2f“ oder „TexCoord3f“ erzeugt werden. Java3D bietet auch hier, wie bei den Normalen eine Klasse zur Generierung der Texturkoordinaten an, die vor allem beim Versehen komplexerer Objekte mit Texturkoordinaten eine große Hilfe darstellt. Das einfache Pyramiden-Beispiel erlaubt jedoch ohne weiteres eine Angabe der Texturkoordinaten von Hand, die sich für das, die Bodenfläche bildende Quadrat, sowie die vier Seitenflächen eigentlich von selbst ergibt. 13 Florian Heidinger „Gegenüberstellung von Direct3D und Java3D am praktischen Beispiel“ Gegen Ende der Methode wird eine, von der Klasse „Geometry“ abgeleitete Primitv-Klasse verwendet, um ein Objekt zu instanziieren, welches die Geometriedaten, Normalenvektoren und Texturkoordinaten aufnimmt. Die Referenz auf dieses Objekt wird dem Konstruktor der Klasse „Pyramidenboden“ dann über den „return“-Befehl zurückgegeben und kann mit Hilfe der von Shape3D geerbten Methode „setGeometry()“ der Bodenfläche zugeordnet werden (siehe Konstruktor). Zur Beschreibung der Bodenfläche bietet sich als geometrisches Primitv hier die Verwendung eines QUADARRAY an, bei dem jeweils vier Raumpunkte der zugeordneten Geometrie zu einem rechteckigen Polygon zusammengefasst werden. Bei der Instanziierung eines QUADARRAY-Objektes sind hier die Anzahl an Raumpunkten des Primitivs, sowie die Art der Daten (Geometrie, Normalenvektoren, Texturkoordinaten) anzugeben, die für das Primitiv verwendet werden sollen. Die errechneten Geometrie, -Normalen und Texturdaten können dann entweder einzeln oder als Array über set–Methoden der Primitiv-Klasse eingestellt werden. Zur effizienteren internen Verarbeitung werden jedoch auch die durch QUADARRAY definierten Polygone immer auf Dreiecke zurückgeführt, da dann in jedem Fall sichergestellt ist, dass alle das Polygon (Dreieck) spezifizierenden Raumpunkte in einer Ebene liegen (koplanar sind). Ähnlich wie im Konstruktor der Pyramidenklasse stellt sich das Erzeugen und Zuweisen der visuellen Eigenschaften mittels eines Objektes der Klasse „Appearance“ dar. Die vom Konstruktor der Klasse „Pyramidenboden“ aufgerufene Methode „erstelleAppearance()“ instanziiert ein entsprechendes Objekt und gibt dieses dem Konstruktor zurück, der daraufhin die Zuweisung über die von Shape3D geerbte Methode „setAppearance()“ durchführen kann. Die Methode „erstelleAppearance()“ führt dabei ähnliche Operationen aus, wie die entsprechende, in der Klasse Pyramide definierte Methode, setzt in den „Texture2D“, „Material“ und „ColoringAttributes“-Objekten jedoch andere Werte. Bei der Instanziierung eines Pyramidenboden-Objekts werden also automatisch die beiden Referenz-Beziehungen zur Festlegung der geometrischen und visuellen Eigenschaften Wie sieht der Teilgraph für einen Pyramidenboden aus ? hergestellt. Der Szenenteilgraph für einen Pyramidenboden stellt sich somit folgendermaßen dar: PyramidenbodenTeilgraph 14 Florian Heidinger „Gegenüberstellung von Direct3D und Java3D am praktischen Beispiel“ Klasse Pyramidenflaeche (Java3D) Der Pyramidenflächen-Konstruktor speichert wie die bereits beschriebenen Konstruktoren die übergebenen Parameter zunächst in privaten Klassenvariablen ab. Im Gegensatz zur „Pyramidenboden“-Klasse verwendet er mit „erstelleGeometrie()“ jedoch lediglich eine einzige eigene Methode Über die von Shape3D geerbte Methode „setAppearance()“ wird von jeder Pyramidenseitenfläche eine Referenz auf das, durch die Klasse „Pyramide“ instanziierte und dem Konstruktor einer Pyramidenfläche als Referenz übergebene „Appearance“-Objekt (app) verwendet. Die seitlichen, die Pyramidenform bildenden Dreiecke verwenden außer denen, für die Bodenfläche definierten Punkten lediglich einen weiteren Raumpunkt - den der Pyramidenspitze. Die beiden anderen, erneut zu verwendenden Punkte können, da sie für die Grundfläche in der Methode „erstelleGeometrie()“ bereits errechnet und als Objekte der Klasse „Point3f“ instanziiert wurden, dem Konstruktor über die Referenzen punkt1 bzw. punkt2 übergeben werden. Eine solche Vorgehensweise, d. h. die Übergabe von Geometrie- oder Texturinformation Geometrieund Texturdaten sind falls möglich per Referenz zu übergeben per Referenz wird auch im „Java 3D API Collateral — 1.2.1 Performance Guide“ als speichersparende Maßnahme bei der Erstellung von geometrischen Objekten in Java3D empfohlen, auch wenn sie für ein derart einfache geometrische Objekte wie im vorliegenden Fall kaum ins Gewicht fallen dürfte. So wird das erste Dreieck aus der immer einbezogenen Spitze der Pyramide mit den Raumkoordinaten (0.0, fHoehe/2, 0.0) und den Raumpunkten v1 und v0 in dieser Reihenfolge gebildet, um eine sichtbare Fläche (Front-Face) zu erhalten. V0 S V1 S V3 V2 V1 V0 Alle weiteren Flächen können auf ähnliche Weise durch Übergabe der Raumpunkt-Paare (v1, v2), (v2, v3), sowie v3 und v0 durch den Konstruktor der Klasse „Pyramide“ bei der Instanziierung der Pyramidenflächen erhalten werden. In der Methode „erstelleGeometrie()“ müssen auch hier wieder, wie im Abschnitt zur Klasse „Pyramidenboden“ beschrieben, die Normalenvektoren und Texturkoordinaten ermittelt und über die set-Methoden des gewählten Geometrie-Primitivs gesetzt werden. 15 Florian Heidinger „Gegenüberstellung von Direct3D und Java3D am praktischen Beispiel“ Bei der Instanziierung eines geeigneten Primitv-Objektes kann man hierbei zum Beispiel auf die Klasse TRIANGLESTRIPARRAY zurückgreifen. Die ersten drei spezifizierten Raumpunkte bilden hier ein Dreieck, also genau das was in diesem Fall gewünscht ist. Alle weiteren bilden mit den beiden unmittelbaren vorangehenden Raumpunkten jeweils ein weiteres Dreieck, so dass ein Streifen aus Dreiecken gebildet werden kann. Die vier Pyramidenseitenflächen bilden folgenden Teilgraphen: Wie sieht der Teilgraph einer Pyramidenflaeche aus ? Eine vollständige Pyramide stellt sich also als Teil-Szenengraph wie folgt dar: Wie sieht der Teilgraph einer Pyramide aus ? Wie am oben abgebildeten Teilszenengraph für die Pyramide zu erkennen ist, kommen für Sparsamer Umgang mit Shape3DObjekten die Definition der Pyramide fünf Objekte der Klasse Shape3D zum Einsatz. Die Verwendung von Shape3D Objekten oder von Objekten, deren Klassen von Shape3D abgeleitet wurden, ist immer mit einem gewissen Mehraufwand beim Zeichnen der Szene verbunden. Der „Java 3D API Collateral — 1.2.1 Performance Guide“ empfiehlt deshalb, lediglich „so viele Shape3D Objekte wie nötig, so wenige wie möglich zu verwenden“. 16 Florian Heidinger „Gegenüberstellung von Direct3D und Java3D am praktischen Beispiel“ Der Anwendungsentwickler kann hier zum Beispiel die Zusammenfassung der Dreiecke, die die Seitenflächen bilden zu einem einzigen Shape3D Objekt, welches etwa ein TRIANGLEFANARRAY als Primitiv nutzt realisieren. Mehrere Objekte sind dagegen immer dann erforderlich, wenn denen durch Shape3D-Objekte definierten geometrischen Objekten jeweils verschiedene visuelle Eigenschaften, d.h. Materialien oder Texturen zugewiesen werden sollen. Dies ist im vorliegenden Beispiel jedoch nicht der Fall, zumindest was die Pyramidenseitenflächen anbelangt. Eine Zusammenfassung der Geometrie wäre hier also ohne weiteres möglich. Der Optimierer der Java3D Umgebung kann den Anwendungsentwickler bei der Zusammenfassung von Shape3D Objekten unterstützen, indem er sie selbständig durchführt, sobald mehrere Shape3D Knoten, die dem gleichen Gruppenknoten über eine Parent-Child-Beziehung zugeordnet wurden und dasselbe Appearance-Objekt referenzieren zusammenfasst. Eine einfache Beispielszene Das soeben beschriebene, in Form von drei Klassen gebildete Pyramiden-Paket kann nun sehr einfach in einer Java3D-Anwendung verwendet werden. Im Rahmen des Beispiel soll das SimpleUniverse mit Standard-Einstellungen für die Betrachterposition und –ausrichtung genutzt werden. Die Definition des Szenengraphen unterteilt sich deshalb in die bekannten fünf Schritte: Welche Schritte sind bei der Erstellung einer Szene in Java3D durchzuführ en ? 1. Erzeugen eines Fensters und eines „Canvas3D“-Objektes 2. Erzeugen des Ansichtsgraphen (Verwendung des „SimpleUniverse“-Objektes) 3. Erzeugen des Inhaltsgraphen 4. Kompilieren des / der Graphen 5. Hinzufügen der Graphen in das „Locale“-Objekt des „SimpleUniverse“ Nachdem in einer Java3D-Anwendung ein geeignetes Fenster mit eingebettetem „Canvas3D“-Objekt zur graphischen Ausgabe erzeugt wurde, kann die eigentliche Szene beschrieben werden. Dies geschieht durch die Definition eines sogenannten Szenengraphen, der durch Verwendung von Java3D-Objekten entsteht bzw. aus diesen zusammengefügt wird. Man kennt hier Objekte zur Durchführung von Transformationen und Animationen, zur Erstellung von geometrischen Objekten, Lichtquellen oder ähnlichem. Tatsächlich kann eine Java3D Szene als Graph aus Knoten, den Objekten des „javax.j3d.media“-Pakets bzw. von diesen abgeleiteten Objekten und Kanten, den Beziehungen zwischen diesen Objekten dargestellt werden. Die Anordnung oder Definition der Kanten-Beziehungen unterliegt jedoch einigen Restriktionen. Während Gruppen-Objekte, also solche für Transformations- oder Geometriegruppen lediglich einen Vorgänger, jedoch beliebig viele Nachfolgeknoten im Szenengraphen besitzen dürfen, (parent-Child-Beziehung) sind alle von „Leaf“ abgeleiteten 17 Florian Heidinger „Gegenüberstellung von Direct3D und Java3D am praktischen Beispiel“ Klassen, zu denen vorwiegend die geometrischen Primitive, aber auch Beleuchtungsobjekte gehören, nur einem direkten Vorgängerknoten zuzuordnen. Sie befinden sich also in den Blättern des Szenengraphen, der somit das Aussehen einer hierarchischen Baumstruktur annimmt. Mit Hilfe einer zweiten Beziehungsart, den Relationen können von den Blatt-Objekten mehrere andere Objekte referenziert oder diesen zugeordnet werden. Es handelt sich dabei um Objekte von „NodeComponent“-Unterklassen wie Materialien, Texturen oder Geometriedaten, die das Aussehen eines geometrischen Primitivs bestimmen bzw. seine Geometrie definieren. Sie unterliegen nicht der Parent-Child-Restriktion, können also von beliebigen Blatt-Objekten („Vorgängern“) auch mehrfach referenziert werden. Wichtig ist bei der Definition der Beziehungen, dass der definierte Graph keine Zyklen bildet, um eine eindeutige Umwandlung der Beziehungspfade in eine lineare, von einer geeigneten Low-Level 3D-API verarbeitbaren Folge von nativen Aufrufen erlaubt. Da entlang des Pfades von der Wurzel des Baumes zu einem der Blatt-Objekte, dem sogenannten „Szenengraphen-Pfad“ vorwiegend Transformationen bzw. Transformationsgruppen angeordnet sind, die Skalierungen, Rotationen oder Translationen für die untergeordneten jeweils durch Parent-Child-Beziehungen verbundenen Blatt-Objekte definieren, ist durch den Szenengraphen-Pfad eines Blatt-Objektes seine Größe, Ausrichtung und Position in der Szene eindeutig definiert. Dies sollte den aufmerksamen Leser an eine aus anderen graphischen Systemen wie zum Beispiel OpenGL bekannte Transformationsart erinnern, die sich mit eben der Positionierung von Objekten in der virtuellen Welt beschäftigt: der Welttransformation, die ein Objekt mit seinen Modellkoordinaten in das für alle Objekte der Szene gemeinsame Weltkoordinatensystem transformiert. Die Welttransformationsmatrix zur Positionierung und Ausrichtung eines Objektes in der virtuellen Welt ist wie der Szenen-Graph Pfad in der Regel für keine zwei Objekte gleich (diese würden sonst an der gleichen Stelle gezeichnet). Sie ergibt sich durch Kombination (Multiplikation) aller Transformationen auf dem Pfad von der Wurzel zum jeweiligen BlattObjekt. Die Reihenfolge, in der die Objekte des Szenen-Graphen gezeichnet werden, ist also für das Ergebnis nicht entscheidend und kann durch den Java3D-Programmierer auch nicht beeinflusst werden. Es ergeben sich im Zusammenhang mit der Betrachtung des Aufbaus und der Arbeitsweise einer typischen Direct3D-Anwendung interessante Erkenntnisse über die Arbeitsweise der Java3D Implementierung bei Kompilierung und Konvertierung des Szenengraphen in eine für Low-Level APIs geeignete Form. Letztlich muss das Ergebnis der Verarbeitung dieser optimierten Form durch die Java Virtual Machine dem einer kompilierten C++-Anwendung, welche direkt gegen die Direct3D API programmiert wurde und die gleiche Szene darstellt, mehr oder weniger exakt entsprechen. 18 Florian Heidinger „Gegenüberstellung von Direct3D und Java3D am praktischen Beispiel“ Jeder Szenengraph besitzt eine Wurzel, das sogenannte „VirtualUniverse“, dem ein oder mehrere „Locale“-Objekte zugeordnet werden können. Man kann sich ein solches „Locale“Objekt als Weltkoordinatensystem der diesem Objekt zugeordneten Szene vorstellen. Eine Verwendung mehrerer Weltkoordinatensysteme dürfte für die meisten Anwendungen nicht sinnvoll sein und wird von Direct3D im übrigen auch nicht direkt unterstützt. Es handelt sich hierbei folglich um eine, durch die Java3D API nachgebildete Funktion. Eine API-interne Realisierung wäre zum Beispiel durch eine Erweiterung der Welttransformationen aller Blatt-Objekte eines bestimmten „Locale“-Objektes um den relativen Ursprungsvektor möglich. Ein Szenengraph besteht aus zwei Teilgraphen: dem Inhaltsgraphen (Content Branch Graph) und den Ansichtsgraphen (View Branch Graph), die wie die Bezeichnungen bereits Woraus besteht der Szenengraph ? erahnen lassen den Inhalt einer Szene, d. h. Ausrichtung, Position und Größe von geometrischen Objekten, ihr Aussehen und ihre Beleuchtung bzw. Position und Ausrichtung des Betrachters bzw. der Kamera beschreiben. Im Rahmen der einfachen Beispielszene sollen innerhalb des Szenengraphen zwei Pyramiden erzeugt und über geeignete Welttransformationen platziert und ausgerichtet werden. Zudem wird die Szene durch zwei Lcihtquellen, ein Umgebungslicht, und ein Spot-Light beleuchtet. Mit Hilfe des MouseRotate-Verhaltens (Behavior) kann ein Rotieren der gesamten Szene durch Verschieben der Maus bei gedrückter linker Maustaste realisiert werden. Es ergibt sich der rechts abgebildete Szenen-Graph: Zum Kompilierungszeitpunkt findet die Umwandlung des Szenengraphen in eine optimierte interne Was geschieht in Schritt 4 der Szenendefinition ? Darstellungsform statt, indem Transformationsgruppen ohne gesetztes „Capability-Bit“ zu einer Gesamttransformation zusammengefasst werden. Das standardmäßig nicht gesetzte „Capability-Bit“ garantiert in diesem Fall, dass sich die durch die Gruppen-Objekte repräsentierten Matrizen zur Laufzeit nicht ändern. Sie können also durch Multiplikation zu einer konstanten Matrix zusammengefasst und einer einzelnen Transformationsgruppe zugeordnet werden. Die Multiplikation muss auch hier wie aus anderen 19 Florian Heidinger „Gegenüberstellung von Direct3D und Java3D am praktischen Beispiel“ graphischen Systemen gewohnt nach dem LIFO-Prinzip erfolgen und kann auch durch den Entwickler mit Hilfe der „mul“-Methode der Klasse „Transform3D“ durchgeführt werden. Es empfiehlt sich deshalb „Capability-Bits“ nur zu setzen, falls dies beispielsweise zur Realisierung von Animationen oder Benutzerinteraktionen unbedingt erforderlich ist, da die Möglichkeiten des Java3D Compilers in diesem ersten Optimierungsschritt sonst sehr stark eingeschränkt werden. In einem sich unmittelbar anschließenden zweiten Optimierungsschritt können nun mehrere Shape3D-Objekte (oder solche von Shape3D-Unterklassen), die im Szenengraphen Warum sollten AppearanceInstanzen mehrfach referenziert werden ? Nachfolger einer gemeinsamen Transformationsgruppe sind zu einer einzigen Figur zusammengefasst werden, sofern sie das gleiche Appearance-Objekt referenzieren. Die Geometrieinformation verschiedener Shape3D-Knoten, die zu einer Transformationsgruppe gehören wird in der Regel nicht gleich sein, es ist jedoch empfehlenswert ein „Appearance“-Objekt einmalig zu instanziieren, um es dann von mehreren der Shape3D-Knoten aus über eine Referenz-Beziehung zu verwenden. Dadurch ist Java3D-Compiler in der Lage, die Zusammengehörigkeit der Shape3D Knoten zu erkennen und sie zusammenführen. Dies geschieht so, dass die Geometrieinformation der Blattknoten in einem geeigneten größer dimensionierten Primitiv kombiniert und mit der „Appearance-Referenz“ einem einzigen resultierenden Shape3D-Objekt zugewiesen wird. Da in der Beispielanwendung alle Pyramidenseitenflächen das gleiche, im Konstruktor der Klasse „Pyramide“ instanziierte „Appearance“-Objekt referenzieren, können alle vier Seitenflächen zu einem einzigen Primitiv zusammengelegt werden, was eine Reduzierung von fünf auf zwei Shape3D-Objekte pro Pyramide in der optimierten internen Form des Szenengraphen bedeutet. Diese Vorgehensweise besitzt mehrere Vorteile. Neben dem wiederum geringeren Speicherverbrauch, der aus der Wiederverwendung des „Appearance“-Objekts mittels Referenzen entsteht, kann auch die Anzahl und damit der Zeichenaufwand für die geometrischen Primitive reduziert werden. Das resultierende größere Primitiv mit höherer Polygonzahl wird zudem effizienter bearbeitet, da der Zustand der Rendering-Pipeline weniger häufig geändert werden muss. In diesem speziellen Fall muss also das Umladen der Geometrie- und Texturdatendaten weniger häufig pro Einzelbild (Frame) geschehen. Zum Laden und Entladen von Geometrie- und / oder visueller Information vieler kleiner geometrischer Primitive wird die Hardware mit einem, im Verhältnis zum tatsächlichen Zeichenaufwand sehr hohen Verwaltungsaufwand belastet. 20 Florian Heidinger „Gegenüberstellung von Direct3D und Java3D am praktischen Beispiel“ Java3D bietet zur manuellen Kombination der Geometrie mehrerer Objekte im Utility-Paket (com.sun.j3d.util.*) ein sogenanntes „Stripifier“-Werkzeug an, welches ein entsprechendes TRIANGLESTRIPARRAY liefert. Sollen verschiedene Flächen eines zusammengesetzten geometrischen Objektes jeweils zumindest teilweise andere visuelle Eigenschaften aufweisen, d. h. kann eine „Appearance“-Instanz aus konzeptionellen Gründen nicht von allen, dieses Objekt beschreibenden Shape3D-Objekten referenziert werden, so empfiehlt sich aus den oben genannten Gründen zumindest die mehrfache Verwendung / Referenzierung von Texturen und Materialien. Ausführung der internen Form Der erzeugte minimale Szenengraph kann nun in einem nächsten Schritt durch die Java Virtual Machine in native Graphik-API Aufrufe umgewandelt werden, indem er in geeigneter In welcher Reihenfolge werden die Objekte gezeichnet ? Reihenfolge durchlaufen wird, um die verschiedenen Blatt-Objekte zu zeichnen. Da die visuellen und geometrischen Eigenschaften wie Position, Ausrichtung, Größe, Farbe, Material und Textur eines Objekts vollständig und überdies eindeutig durch den Pfad von der Wurzel des gerichteten zyklenfreien Szenengraphen zu den Blättern bestimmt sind, können die einzelnen geometrischen Objekte bei Verwendung eines Z-Puffers in beliebiger Reihenfolge durch Übergabe der entsprechenden Daten und Aufrufe an die Direct3D API gezeichnet werden. Man spricht in diesem Zusammenhang auch vom ungeordneten Zeichnen („Unordered Rendering“). Aufgabe der Java3D-Implementierung ist es, eine möglichst optimale Reihenfolge zu ermitteln, in der die Blattobjekte gezeichnet werden sollen. Die Umordnung der Blattobjekte Nach welchen Kriterien findet die Ermittlung der Zeichenfolge statt ? hat dabei idealerweise so geschehen, dass die Rendering-Pipeline der zugrundeliegenden Low-Level API, also die von Direct3D zur Darstellung eines Einzelbildes (Frame) mit möglichst wenigen Statusänderungen auskommt. Die entstehende Zeichenfolge von geometrischen Primitiven und Objekten bezeichnet man deshalb auch als „State Ordered Rendering“. Dabei werden Blattknoten so umgeordnet, dass solche mit gleicher Textur, Geometrie, Material oder Welttransformation unmittelbar aufeinanderfolgend gezeichnet werden. 21 Florian Heidinger „Gegenüberstellung von Direct3D und Java3D am praktischen Beispiel“ Nachdem die „State Ordered Rendering“Liste erstellt wurde, kann der Java3DWie arbeitet der Java3DRenderer ? Renderer gestartet werden, um die Objekte durch die Low-Level API zeichnen zu lassen. Dieser führt bei der Verarbeitung einer Szene eine Endlosschleife aus, in der Benutzereingaben, sowie Animationen in (Teil)-Bereichen des Szenengraphen verarbeitet, und der Szenengraph in der geeigneten Reihenfolge durchlaufen wird. Die sichtbaren Objekte werden dann der While (true) { Eingaben verarbeiten If (Programmende) Break Verarbeite Behavior-Objekte Traversiere den optimierten Szenengraphen und übergebe die geometrischen und visuellen Daten an die D3D API zum zeichnen } Aufräumen und Beenden Direct3D API zum Zeichnen übergeben. Die Endlosschleife kann nach der Verarbeitung der Benutzereingaben verlassen werden, falls eine entsprechende Benutzeraktion zum Abbruch der Anwendung (Schließen des Ausgabefensters) vorausging. Die Anwendung in Direct3D Bei der Ausführung der Szene übersetzt die Java Virtual Maschine die Anwendung mit der internen Form des Szenengraphen mit Hilfe der Laufzeitbibliotheken in native Aufrufe gegen die Direct3D und die Windows32 API. Zu Beginn einer jeden C++-Anwendung zur graphischen Ausgabe unter Windows muss, analog zu den fünf Schritten zur Java3D-Programmierung ein Fenster erzeugt werden. Obwohl man in beiden Technologien zum gleichen Ergebnis, einem Standard-WindowsFenster ohne Menü oder Inhalt gelangt, stellt sich der Weg aus Sicht des Anwendungsentwicklers bis hin zu diesem Resultat in beiden Anwendungen völlig anders dar. Das eigentliche Zeichnen und die Verwaltung des Fensters wird in beiden Fällen nicht durch die Java3D- bzw. Direct3D-API, sondern durch die Windows32-API durchgeführt. Während die Direct3D-Anwendung hier direkt mit der Windows-API durch den Aufruf entsprechender Methoden zur Anmeldung, Initialisierung und Erzeugung des Fensters kommuniziert, bietet eine Java- oder Java3D-Anwendung hier nur den Umweg über die Java Virtual Machine bzw. deren systemabhängige Laufzeitbliotheken, die die Anweisungen des plattformunabhängigen Bytecodes in entsprechende Aufrufe gegen die Windows-API umsetzen. Diesen Sachverhalt beschreibt auch nachfolgende Graphik. 22 Florian Heidinger „Gegenüberstellung von Direct3D und Java3D am praktischen Beispiel“ Direct3D- / WindowsAnwendung Java / Java3DAnwendung Laufzeitbibliothek Java Virtual Machine Windows32 API Hardware- und Hardwaretreiber Direct3D- / WindowsAnwendung Java / Java3DAnwendung Java3D bedient sich dabei der Klassen des AWT (Abstract Windowing Toolkit), die stets importiert Laufzeit- Wie wird ein Fenster in Java3D erzeugt ? werden müssen, um ein Fenster zur Ausgabe bibliothek import java.awt.event.*; import java.awt.BorderLayout; Java Virtual Machine import java.awt.Frame; anlegen zu können. Windows32 API Das eigentliche Erzeugen eines Ausgabepublic static void main(String args[]) Fensters für eine einfache Java3D-Anwendung { Frame fenster = new geschieht gewöhnlich in der Main-Methode und Hardware- und Hardwaretreiber MainFrame(new Szene(), 800, 600); gestaltet sich sehr einfach, schnell und fenster.setTitle("Java3D Seminar"); unkompliziert, da lediglich eine Objektinstanz der } Klasse „Frame“ des AWT-Paketes erzeugt werden muss. Dem Konstruktor wird dabei in der Regel gleich eine Applet-Objektinstanz, sowie einige Parameter übergeben, die das Fensterformat, d. h. die Höhe und Breite des Ausgabe-Fensters beschreiben. Über die set-Methoden der Klasse „Frame“ kann das erstellte Fenster dann weiter an die individuellen Bedürfnisse angepasst werden. So sind z. B. der Fenstertitel, das Fenstericon, die Koordinaten der linken oberen Fensterecke, das Cursoraussehen uvm. veränderbar. Diese wenigen benutzerdefinierten Anweisungen, hinter denen jedoch komplexe, in den AWT-Bibliotheken implementierte Klassen stehen, werden bei der Programmausführung in native Aufrufe gegen die Windows32 API umgesetzt. Diese entsprechen weitgehend der kompilierten Form, der in C++-Anwendungen formulierten Anweisungen zum Anlegen eines Ausgabefensters, die im nächsten Abschnitt kurz vorgestellt werden sollen. In einer Direct3D oder Standard-Windows Anwendung muss sich der Programmierer mit Hilfe von Aufrufen gegen die Windows-API selbst ein geeignetes Fenster erzeugen. Dies Wie wird ein Fenster in C++-Anwendungen erzeugt ? stellt sich wesentlich aufwendiger und auch fehlerträchtiger als in Java dar. Zahlreiche Tutorials und Dokumentationen, aber auch moderne Entwicklungsumgebungen für C++, wie zum Beispiel Microsoft Visual C++ können den unerfahrenen Anwendungsentwickler hierbei unterstützen. Im folgenden soll das eigenhändige Erzeugen eines Fensters mit Hilfe der 23 Florian Heidinger „Gegenüberstellung von Direct3D und Java3D am praktischen Beispiel“ Windows API in seinen wesentlichen Schritten kurz vorgestellt werden. Wenn es aus Sicht der Java-Entwickler zunächst auch kompliziert und umständlich erscheint, so kann man doch die einmal erzeugten Klassen und Headerdateien bei überlegt durchgeführter Modularisierung in zukünftigen Projekten als „Windowing Toolkit“ (WT) erneut verwenden, wodurch sich das Erzeugen eines Fensters unter C++ dann ebenso einfach und komfortabel wie unter Java mit Hilfe der AWT-Klassen darstellt. Auch hier wird das Erzeugen eines Fensters, vergleichbar der Vorgehensweise in JavaAnwendungen in der WinMain-Funktion, dem Startpunkt der Ausführung einer WindowsAnwendung angestoßen. Neben der auch hier erforderlichen Einbindung von Hilfsstrukturen in Form der „windows.h“-Headerdatei sind zur Programmierung eines Fensters unter Windows drei Schritte notwendig. 1) Erzeugen eines Fensterobjektes (Fensterschablone) Zunächst muss durch den Anwendungsentwickler ein geeignetes Fensterobjekt definiert werden. Dies geschieht mit Hilfe der in der „windows.h“-Headerdatei enthaltenen Datenstruktur WNDCLASSEX, über deren Strukturelemente verschiedene Eigenschaften des zu schaffenden Fenster festlegt werden können. Bemerkenswerterweise bietet diese Datenstruktur Strukturelemente für Fenstereigenschaften wie Fenstericon, Cursoraussehen, Hintergrundfarbe oder Menü, die für die „Frame“-Klasse des AWT-Objekts über entsprechende setMethoden eingestellt werden können. Man kann also davon ausgehen, dass Java unter Windows zur Erstellung eines Fensters (Frame) ebenfalls eine WNDCLASSEX Datenstruktur mit vordefinierten Standard-Werten verwendet (Teetassen-Icon, Standard-Cursor, weißer Hintergrund, ...). Diese Standardeinstellungen kann der Benutzer dann über die entsprechenden setMethoden manipulieren, um ein, seinen individuellen Anforderungen eher entsprechendes Ergebnis zu erhalten. Hat man sich in C++ einmal ein geeignetes Fensterobjekt definiert, so spricht nichts dagegen, dieses immer wieder zu verwenden. Da man sich dann die jeweiligen Aufrufe zum expliziten Setzen von bestimmten Eigenschaften sparen kann, ist diese Vorgehensweise auf Dauer sogar etwas komfortabler in Java – immer vorausgesetzt, Sie geben sich mit den Java-Standardeinstellungen nicht zufrieden. 2) Registrieren des Fensterobjektes bei Windows In einem zweiten Schritt muss das soeben erzeugte Fensterobjekt bei Windows registriert werden. Erst nachdem Windows die erstellte Schablone bekannt gemacht wurde, kann ein entsprechendes Fenster erzeugt werden. Die Registrierung geschieht über die API-Funktion „RegisterClassEx“, der lediglich die Adresse der erzeugten WNDCLASSEX Strukturvariablen übergeben werden muss. 24 Florian Heidinger „Gegenüberstellung von Direct3D und Java3D am praktischen Beispiel“ 3) Erstellen des Fensters Im letzten Schritt, der dem eigentlichen Erzeugen und Anzeigen des Fensters dient, werden der „CreateWindowEx“-API-Methode neben dem Schablonennamen des Fensterobjekts, welches bereits einige der Eigenschaften des Fensteraussehens definiert (s. o.) weitere Merkmale wie Fenstertitel, Koordinaten der linken oberen Fensterecke und Höhe bzw. Breite des Fensters übergeben. Hier wird auch deutlich, warum in Java Fenstertitel, Höhe und Breite des Fensters, sowie die Koordinaten der linken oberen Fensterecke direkt dem Konstruktor der Klasse Frame übergeben werden und nicht wie Cursor, Hintergrundfarbe oder Icon nur durch SET-Methoden manipuliert werden können. Die Java Virtual Machine setzt die, dem Konstruktor der Klasse Frame übergebenen Werte einfach in die CreateWindowEx-Methode ein, während die sekundären Fenstereigenschaften üblicherweise durch die, der Java Virtual Machine ebenfalls zur Verfügung stehenden Standardschablone ausreichend festgelegt sind. Wichtig ist in diesem Zusammenhang der Wert der dem Strukturelement „lpszClassName“ zugewiesen wurde. Dieser wird beim eigentlichen Erzeugen des Fensters benötigt, damit das gewünschte, bereits bei Windows registrierte Fensterobjekt (Schablone) identifiziert werden kann. Die „CreateWindowEx“-Methode liefert als Rückgabenwert ein HWND-Objekt, welches ein Handle für ein Fenster darstellt. Ein Handle ist in diesem Zusammenhang nicht anderes als ein eindeutiger Identifizierer eines aktiven Fensters, der eine Unterscheidung von den anderen aktiven Fenstern und damit zur Laufzeit eine Zugriffsmöglichkeit auf das Fenster bietet. Darüber hinaus können im Rahmen der „CreateWindowEx-Methode“ verschiedene Fensterstile, z. B. randlose Fenster, Fenster ohne Buttons zum Minimieren / Maximieren, usw. ausgewählt werden, was von Java nicht unterstützt wird. Das AWT-Paket bietet hier wirklich nur die Fenster-Funktionen und Eigenschaften, die von allen Betriebssystemen, für die eine Java Virtual Machine existiert, unterstützt werden. Die eigentliche Darstellung, Ausgabe und Verwaltung eines Fensters obliegt in jedem Fall der Windows-Implementierung, die letztlich darüber entscheidet welche Formen, Farben und Funktionen das Fenster (Titelbalken und Umrandung) bzw. die Fensterelemente besitzen. So sieht ein, in Direct3D oder Java3D-Anwendungen erzeugtes Fenster unter WindowsXP in der Regel völlig anders aus als zum Beispiel unter Windows98. 25 Florian Heidinger „Gegenüberstellung von Direct3D und Java3D am praktischen Beispiel“ Initialisierung der Zeichenfläche Nachdem ein leeres Fenster zur Ausgabe zur Verfügung steht, findet in Java oder Java3DAnwendungen die Aufteilung der sichtbaren Fensterfläche in bestimmte Regionen mit Hilfe Welche Schritte sind in Java3D zur Initialisierung der Zeichenfläche erforderlich ? geeigneter Layout-Manager statt. Mindestens eine der Layout- public Szene() { setLayout(new BorderLayout()); Manager-Regionen bekommt dann ein Canvas3D-Objekt als Canvas3D zeichenflaeche = new Canvas3D(null); add("Center", zeichenflaeche); ... Zeichenfläche für die Java3DAnwendung zugewiesen. Andere Bereiche des definierten Layouts können zum Beispiel mit Standard- NORTH AWT oder Swing-Komponenten belegt werden, um eine Steuerung der 3D-Applikation zu gestatten. EAST CENTER WEST Diese Komponenten werden wie in gewöhnlichen Java-Oberflächen- Canvas3D-Objekt SOUTH Anwendungen durch das AWT bzw. Swing-Paket verwaltet. Im Rahmen der Beispielanwendung wird nur der CENTER-Bereich des BorderLayout-Managers mit einer Canvas3DKomponente belegt. Alle anderen Bereiche (NORTH, SOUTH, EAST und WEST) bleiben ungenutzt, weshalb sich die Zeichenfläche über den gesamten sichtbaren Fensterbereich erstreckt. Nun kann der Fensterbereich, in den das Canvas3D-Objekt integriert wurde zur Ausgabe von 3D-Graphiken verwendet werden. Beim Anlegen einer entsprechenden Canvas3D Objekt-Instanz hat der Anwendungsentwickler jedoch nur wenig Einfluss darauf, wie die vorhandene Hardware zur Ausgabe der zu erstellenden Szene genutzt werden soll. Lediglich zwei Konstruktoren, denen jeweils ein „GraphicsConfiguration“-Objekt übergeben werden kann, bieten hier Möglichkeiten an, auf virtuellen Bildschirmen zu zeichnen. Anders und wie vielleicht bereits vermutet wesentlich komplizierter als in Java3DAnwendungen gestaltet sich die Vorbereitung eines soeben erzeugten Fensters zur Welche Schritte sind in Direct3D zur Initialisierung der Zeichenfläche erforderlich ? Direct3D-Graphikausgabe. Bis zu diesem Zeitpunkt handelt es sich bei dem, durch Aufrufe gegen die Windows API geschaffenen Fenster lediglich um ein gewöhnliches Windows-Fenster, auf dessen Darstellungsfläche die aus der täglichen Arbeit mit Windows bekannten 2D-Dialogelemente angeordnet werden können. Dies kann hier jedoch nicht wie in Java mit Hilfe von Layoutmanagern realisiert werden, die 26 Florian Heidinger „Gegenüberstellung von Direct3D und Java3D am praktischen Beispiel“ Position und Größe der Dialogelemente bei einer Änderung der Fenstergröße entsprechend der neuen sichtbaren Darstellungsfläche so anpassen, dass sie die zur Verfügung stehende Fläche bestmöglich genutzt wird. Windows-Formulare und Dialoge werden dementsprechend immer mit festen Positionen für die linke obere Ecke der Dialogelemente entworfen. Diese Einschränkung fällt hier weniger ins Gewicht, da die relativ zur linken oberen Ecke der Darstellungsfläche angegebenen Positionen der Dialogelemente unter Windows-Systemen, denn nur dort läuft eine entsprechende Anwendung, bei gleicher Auflösung auch in identische Bildschirmkoordinaten übersetzt werden. Für eine Direct3D Anwendung, die den gesamten sichtbaren Darstellungsbereich eines Fensters durch entsprechende Definition des Viewports nutzen soll oder gar im VollbildModus ausgeführt wird, spielt diese Tatsache jedoch keine Rolle. Um nun auch graphische Ausgaben durch Direct3D in dem erzeugten Fenster durchführen lassen zu können, muss die API zunächst initialisiert werden. Im Rahmen dieser Initialisierung besteht auch die Möglichkeit eine Direct3D-Anwendung als VollbildApplikation auszuführen, indem Direct3D die Kontrolle über das erstelle Standard-Windows Fenster übergeben wird. Java3D-Anwendungen verfügen nicht über dieser erweiterten Möglichkeiten der Fenstermanipulation, da sie sich zur Bereitstellung von Fenstern allein des Abstract Windowing Toolkit und nicht der entsprechenden Funktionalitäten der DirectX-API bedienen können. Dies hätte aus Kompatibilitätsgründen auch wenig Sinn, da alle anderen 3D APIs wie OpenGL oder QuickDraw diese Möglichkeiten ebenfalls unterstützen müssten um eine einheitliche Schnittstelle in Java3D anbieten zu können. Die Ausführung einer Anwendung als Vollbild-Anwendung unter Windows besitzt neben der Vergrößerung der Darstellungsfläche noch einen weiteren, mindestens ebenso wichtigen Nebeneffekt. Die Rechenkapazität muss hier nicht mit anderen Windows-Anwendungen (Fenstern) geteilt werden, die ansonsten immer einen bestimmten Anteil der zur Verfügung stehenden Rechenkapazität als Menge von Zeitscheiben zur Durchführung ihrer (Hintergrund-)Aktionen erhalten. Initialisierung von Direct3D Die Direct3D API, die dem Anwendungsentwickler in Form der Objektbibliothek „d3d8.lib“ zur Verfügung gestellt wird muss, bevor mit den ersten Aufrufen gegen die Schnittstelle begonnen werden kann in das Projekt eingebunden (link) werden. Zusätzlich empfiehlt sich die Verwendung der Werkzeugbibliotheken (d3dx8.lib) und im Rahmen dieser Beispielanwendung auch die der DirectInput API (dinput.lib) zur Verarbeitung von Benutzerinteraktionen. Jeder dieser Objektbibliotheken ist eine entsprechende Headerdatei zugeordnet, welche die im Umgang mit den Bibliotheksfunktionen benötigten Datenstrukturen und 27 Florian Heidinger „Gegenüberstellung von Direct3D und Java3D am praktischen Beispiel“ Methodendefinitionen enthält. Diese sind durch „include“-Anweisungen in den Quelltext zur Initialisierung von Direct3D bzw. DirectInput einzubeziehen. Direct3D API Direct3DX Werkzeugbibliothek DirectInput API Objektbibliothek d3d8.lib d3dx8.lib dinput.lib Headerdatei d3d8.h d3dx8.h dinput.h Die eigentliche Initialisierung von Direct3D kann entweder auf einem leichten oder aber einem schweren Weg durchgeführt werden. Entscheidet man sich für die einfachere Variante, so mag dies in vielen Fällen ausreichen. Für anspruchsvollere Anwendungen auf leistungsstarken Hardwareplattformen empfiehlt es sich jedoch den steinigeren Weg einzuschlagen. Die Initialisierung kann im einfachen Fall durch einige wenige Funktionsaufrufe gegen die API, welche die Verwendung von Standard-Hardware und Standard Hardwareeinstellungen zur Folge haben durchgeführt werden. Für die graphischen Ausgaben wird dann einfach die erstbeste Graphikkarte, welche auch von Windows verwendet wird, eingesetzt. Nachteilig ist diese Verfahrensweise immer dann, wenn leistungsfähigere 3D-Beschleuniger bei dieser „Auswahl“ von Geräten einfach ignoriert werden, obwohl sie für die Anwendung die wesentlich geeignetere Alternative darstellen würden. Der schwerere Weg ist nun dadurch gekennzeichnet, dass mittels einer geeigneten Abfrage zunächst alle installierten Graphikkarten mit ihren jeweiligen Funktionen und Features aufgezählt werden. Man spricht in diesem Zusammenhang auch von HardwareEnumerierung bzw. Hardware-Aufzählung. Aus dem Aufzählungsergebnis wird dann die leistungsfähigste Komponente mit den besten / meisten hardwareseitig realisierten Funktionalitäten initialisiert und damit zur Graphikausgabe ausgewählt. Ein Beispiel für eine solche Funktion stellt zum Beispiel ein Hardware-Rasterisierer dar, der als verdrahteter Algorithmus im Graphikkarten-Prozessor wesentlich effizienter als eine entsprechende, den Haupt-Prozessor nutzende Software-Implementierung arbeitet. Die CPU ist durch diese spürbare Entlastung in der Lage andere Aufgaben durchzuführen. In Direct3D wird eine Graphikkarte auch als Adapter bezeichnet. Dieser unterstützt mehrere verschiedene Kombinationen von Auflösungen und Farbtiefen, die sogenannten GraphikWas ist ein Adapter bzw. 3DDevice ? Modi. Zudem sind jeder Graphikkarte in der Regel verschiedene 3D-Devices zugeordnet. Bei der Enumerierung in einer Direct3D-Anwendung wird nun nichts anderes gemacht, als für jeden im Rechnersystem installierten Adapter die unterstützen Gaphik-Modi, sowie die 28 Florian Heidinger „Gegenüberstellung von Direct3D und Java3D am praktischen Beispiel“ angeschlossenen 3D-Devices abzufragen und eine geeignete Adapter / 3D Device / ModusKombination auszuwählen. Zu diesem Zweck sind durch den Anwendungsentwickler geeignete Datenstrukturen zu definieren, die die abgefragten Eigenschaften aufnehmen können. Sinnvollerweise wählt man hier drei verschiedene Strukturen für Adapter, 3D-Device und Graphikmodi, die man hierarchisch strukturiert konzipiert, d.h. Die Devices-Struktur besitzt ein StrukturelementArray zum Speichern von 3D Device-Strukturen, dieses enthält wiederum ein Strukturelement-Array zum Speichern der Strukturen der vom jeweiligen Device unterstützten Bildmodi (vgl. „Strukturen.h“). Bei der Abfrage der verschiedenen 3D-Devices, die von einem Graphikadapter bzw. einer einzelnen Graphikkarte unterstützt werden unterschiedet man in Direct3D drei Klassen. Welche 3DDeviceTypen kennt man in Direct3D ? D3DDEVTYPE_HAL Unter die Hardware Abstraction Layer-Klasse (HAL) fallen alle Adapter, die die hardwareseitige Berechnung von Dreiecken, sowie deren Rasterisierung und Zeichnung (Rendering) unterstützen. Moderne Graphikkarten dieser Kategorie verfügen in der Regel zusätzlich über eine sogenannte T(ransform)&L(ighting)-Engine, die die Ausführung von Beleuchtungs- und Transformationsoperationen ebenfalls in der Hardware ausführen kann. D3DDEVTYPE_SW In die zweite Klasse der Devicetypen fallen alle Adapter ohne 3D-Fähigkeit, d. h. reine 2D-Karten. Die Berechnung von Dreiecken, sowie alle weiteren 3D-Funktionen muss hier durch geeignete Softwarealgorithmen realisiert werden. Es handelt sich also um ein reines Software-Device, das wesentlich geringere Bildraten als die HAL-Typen erreicht. In der Regel unterstützt das Software-Device auch nicht alle Funktionen der jeweils aktuellen Direct3D Version D3DDEVTYPE_REF Um diesem Umstand abzuhelfen wurde eine dritte Klasse, die sogenannte Referenzklasse (Reference-Rasterizer) entworfen, die zwar alle aktuellen Funktionen unterstützt, jedoch ebenfalls rein softwaremäßig implementiert ist. Aufgrund der dadurch entstehenden Performanceprobleme empfiehlt sich der Einsatz des Reference Rasterizer Device lediglich zu Testzwecken 29 Florian Heidinger „Gegenüberstellung von Direct3D und Java3D am praktischen Beispiel“ Zur Aufzählung der Graphikadapter, 3D-Devices und der vom jeweiligen Device unterstützten Graphik-Modi, wird in der Methode „geraeteAufzaehlen()“ der Klasse „D3Dinit.cpp“ ein erstes Direct3D Objekt, das bezeichnenderweise den Namen DIRECT3D8 trägt und mit Hilfe der Methode „Direct3DCreate8()“ bereitgestellt wird genutzt. Es handelt sich um ein typisches COM-Objekt, welches ausschließlich über sein Schnittstellenobjekt vom Typ LPDIRECT3D8 zugegriffen werden kann. Die Direct3DCreate8-Methode gibt deshalb eben dieses Schnittstellenobjekt zurück, das in jeder Direct3D-Anwendung den Ausgangspunkt zur Erzeugung weiterer COM-Objekte bildet. Das „Component Objekt Model“ (COM) stellt ein objektorientiertes Programmiermodell dar, welches eine strenge Kapselung der Objekte fordert. Was ist unter dem Component ObjectModel (COM) zu verstehen ? Diese dürfen also keine „public“-Methoden anbieten. Stattdessen sind jedem COM-Objekt eine oder mehrere Schnittstellen zugeordnet, die den Zugriff auf die Objekt-Methoden gestatten. COM-Objekte kann man sich deshalb als „Black-Boxes“ vorstellen, die den Anwendungen Dienste und Methoden über ihre Schnittstellen zur Verfügung stellen. Sowohl die DirectX API, als auch die Laufzeitumgebung selbst liegen vornehmlich als COMkonforme Objekte vor. Das DIRECT3D8-Objekt kann nun in der Methode „devicelisteErstellen()“ zunächst dazu verwendet werden, die Anzahl der installierten Adapter (GetAdapterCount()) zu ermitteln. Anschließend iteriert man in einer Schleife über alle Adapter, von denen jeder durch einen eindeutigen numerischen Wert, der dem Schleifenzähler entspricht, identifiziert wird, um die vom jeweiligen Adapter unterstützten Bild- und Farbtiefenformate (EnumAdapterModes()) zu erhalten. In einer weiteren geschachtelten Schleife, die über die drei bekannten Devicetypen läuft prüft man nun ab, welche der ermittelten Adapter-Formate von welchem 3D-Device unterstützt werden. Dies kann sehr einfach über die Methode „CheckDeviceType()“ geschehen. Dieser wird neben der numerischen Adapter-ID und dem zu testenden 3D Device-Bezeichner (HAL, SW, REF) das zu testende Bildformat übergeben. Zusätzlich kann untersucht werden, welche hard- oder softwareseitige Vertex-, bzw. Transformations- und Beleuchtungs-Operationen durch das 3D-Device durchgeführt werden können. Unmittelbar an diese Tests, muss in der „findeStencilFormat()“-Methode zusätzlich ermittelt werden, ob für die Device / Format-Kombination ein Stencil- und ein Back-Buffer erzeugt werden können (CheckDeviceFormat()). Entspricht mindestens einer der vom Device angezeigten Graphik-Modi der gewünschten, der Methode über die Parameter „dwBreite“ und „dwHoehe“ übergebenen Auflösung bei 16Bit Farbtiefe und wird dieser Graphik-Modus vom 3D-Device voll unterstützt, so das 3DDevice bzw. die 3D-Deviceklasse zur Ausgabe geeignet und kann zur Initialisierung von Direct3D verwendet werden. 30 Florian Heidinger „Gegenüberstellung von Direct3D und Java3D am praktischen Beispiel“ In einem letzten Schritt Ermittle Anzahl installierter Adapter iteriert man über die Über alle installierten Adapter Ermittle die vom Adapter unterstützten Graphik-Modi Über alle drei 3D Devicetypen (HAL, REF, SW) Über die gefundenen Graphik-Modi d. Adapters Prüfe, ob der Graphik-Modus vom 3D Device unterstützt wird gefundenen Adapter / 3D Device-Kombinationen, die den gewünschten GraphikModus unterstützen und wählt das erste in der Liste aus. Dabei wird den Adapter Führe weitere Tests für 3D DeviceFunktionen wie zum Beispiel Hardware Transformations und Beleuchtungsoperationen durch / HAL-DeviceKombinationen Priorität eingeräumt, da in der Prüfe, ob für das 3D Device ein Back- bzw. Stencil-Buffer erzeugt werden kann Schleife über die Trage die Adapter / Device-Kombination bei erfolgreichen Tests in eine Liste ein Devices untersucht wurden. Devicetypen zuerst HALDie entsprechenden Kombinationen stehen also an den führenden Listenpositionen. JA Liste geeigneter Adapter/DeviceKombinationen leer ? NEIN Über alle Listeneinträge Fehler: Direct3D kann nicht initialisiert werden Wähle erste gefundene Kombination Wurde nun endlich ein geeigneter Adapter mit 3D Device für den geforderten GraphikModus gefunden, kann Direct3D in der Methode „d3dObjekteInitialisieren()“ initialisiert werden. Wichtigster Schritt hierbei stellt das Erzeugen eines D3DDEVICE8-Objektes dar, welches der Anwendung den Zugriff auf die 3D-Graphik-Hardware gestattet. Wie beim DIRECT3D8 COM-Objekt wird auch hier über die „CreateDevice()“-Methode der LPDIRECT3D8-Schnittstelle ein COM Objekt instanziiert dessen Schnittstelle als LPDIRECT3D8DEVICE-Objekt zurückgegeben wird. Der „CreateDevice“-Methode können hier eine Vielzahl von Parametern, teilweise in Form einer vorher zu füllenden Strukturvariablen übergeben werden. Zunächst sind die Nummer (ID) des mühsam ermittelten Graphikadapters nebst dem gefundenen 3DDeviceIdentifizierer (HAL, SW, REF) zu übergeben. Zudem können in der Struktur zum Beispiel Anzahl und Format der Back-Buffer, Windowed-Parameter, uvm. eingestellt werden. Das Back-Buffer-Format, d. h. Höhe und Breite des Hintergrund-Bildspeichers entspricht dabei natürlich implizit der gewünschten, als Parameter übergebenen Auflösung der Darstellungsfläche (Graphik-Modus). Gleichzeitig erwartet die Funktion den Handle auf das bereits erzeugte Windows Ausgabe- 31 Florian Heidinger „Gegenüberstellung von Direct3D und Java3D am praktischen Beispiel“ Fenster. Hierdurch ist DirectX in der Lage die Kontrolle über das Fenster zu übernehmen, um etwa in den Vollbildmodus zu wechseln. Da dies im Rahmen der Beispielanwendung geschehen soll, wird das „Windowed“-Element der D3DPRESENT_PARAMETERS-Struktur auf dem Wert FALSE gesetzt. Das soeben erzeugte DIRECT3DDEVICE8-Objekt kann nun sofort verwendet werden, um einige Parameter der Direct3D Rendering-Pipeline zu setzen. Hier sind vor allen Dingen die beabsichtigte Verwendung von Beleuchtung, die Farbe des ambienten Lichtes, der CullModus oder die Verwendung von Z-Puffern anzuzeigen. Alle Einstellungen können durch Aufrufe der „SetRenderState()“-Methode des DIRECT3DDEVICE8-Objektes über die zugehörige LPDIRECT3DDEVICE8-Schnittstelle gesetzt werden. Dabei ist als erster Methodenparameter die zu ändernde Eigenschaft der Rendering-Pipeline wie zum Beispiel D3DRS_LIGHTING, D3DRS_AMBIENT, usw. und als zweiter, vom Wert dieses ersten Parameters abhängiger Parameter der zu setzende Wert zu übergeben. Meist können hier in der „d3d8.h“-Headerdatei vordefinierte „sprechende“ Bezeichner (D3DCULL_NONE, D3DZB_TRUE) oder einfach ein boolescher Wert verwendet werden. Jeder dieser einfach zu handhabenden Bezeichner ist headerintern mit einem ganzzahligen Wert codiert, auf den er zum Zeitpunkt der Programmübersetzung zurückgeführt wird. Es ist nun mit Hilfe zahlreicher Anweisungen und Parameter eine Fläche geschaffen worden, in die die Anwendung mit Hilfe der Direct3D-API Ausgaben machen kann. Was sich in Java3D quasi mit zwei Zeilen Code realisieren lässt kann den Entwickler für Direct3DAnwendungen einige Stunden bis Tage an Entwicklungsarbeit kosten. Dank des Wiederverwendbarkeitskonzepts objektorientierter Programmiersprachen, zu denen auch C++ gehört, können die erzeugten Methoden jedoch in nahezu jeder zukünftig zu entwickelnden Direct3D-Anwendung wiederverwendet werden. Der kritische Leser wird nun anmerken, dass Routinen, die derart häufig benötigt werden, doch eigentlich in Form von optimierten Bibliotheksfunktionen angeboten werden sollten. In Warum existieren keine Bibliotheksfu nktionen zur „schweren“ Direct3DInitialisierung ? der Tat wäre eine solche Lösung für den unerfahreneren Anwender zu begrüßen. Man sollte jedoch nicht vergessen, dass der „einfache“ Weg der Initialisierung von Direct3D durch Auswahl des Standard-Graphikadapters, des Standard-3D-Device, sowie der Ausführung im Fenster dieser Zielgruppe doch wesentlich praktikabler erscheinen dürfte. Zudem werden parametrisierbare Bibliotheksfunktionen in der Regel niemals alle Methoden anbieten können, um sämtliche Funktionen des Direct3D Paktes durch wenige globale Aufrufe nutzbar zu machen. Eine vollständige Bibliothek wäre so komplex, umfangreich und unhandlich, dass eine lange Einarbeitungszeit unumgänglich ist, von den Nachteilen im Laufzeitverhalten durch spezialisierte Möglichkeiten ganz zu Schweigen. DirectX im Allgemeinen und Direct3D im Besonderen folgen in diesem Zusammenhang dem Prinzip dem Anwendungsentwickler „nur so viel Funktionalität wie unbedingt nötig anzubieten“. 32 Florian Heidinger „Gegenüberstellung von Direct3D und Java3D am praktischen Beispiel“ Die Direct3D Rendering Pipeline Im vorangegangenen Abschnitt wurde bereits kurz die Direct3D Rendering-Pipeline erwähnt, auf deren Eigenschaften durch die „setRenderState()“-Methode des Welche Bedeutung / Funktion hat das DIRECT3DD EVICE8Objekt ? DIRECT3DDEVICE8-Objektes im Rahmen der Initialisierung von Direct3D zugegriffen wurde. Das DIRECT3DDEVICE8-Objekt bietet zahlreiche weitere Funktionen an, um den Zustand der zugrundeliegenden Rendering-Pipeline zur Laufzeit zu setzen bzw. zu manipulieren, was zur graphischen Ausgabe einer jeden Szene unerlässlich ist. Es bietet also einen abstrakten und hardwareunabhängigen Zugriff auf die RenderingPipeline und stellt damit das wesentliche Bindeglied zwischen Anwendung und Hardware dar. Für die nachfolgende Betrachtung der C++-Anwendung zur Ausgabe der Beispielszene ist es durchaus sinnvoll, kurz auf Aufbau und Struktur der Direct3D Rendering-Pipeline einzugehen, obwohl sich im Vergleich zu der aus OpenGL bekannten nur wenige Überraschungen ergeben dürften. VertexBuffer Welttransformation Vertex-Shader Ansichtstransformation Projektionstransformation Farbe „Vertex-Assembly“-Prozess Texturkoordinaten Primitv-Operationen Textur PixelShader Material Alpha, Stencil, Tiefentest FrameBuffer 33 Florian Heidinger „Gegenüberstellung von Direct3D und Java3D am praktischen Beispiel“ Die Rendering-Pipeline von Direct3D stellt eine typische Pipeline zur Graphikverarbeitung mit mehreren sequentiell zu durchlaufenden Verarbeitungsschritten dar. Aufgabe der Geometrie- / Transformations-, Beleuchtungs- und Textur-Verarbeitungseinheiten ist letztlich immer die Umwandlung der in der Anwendung definierten räumlichen Szenenbeschreibung in Bildpunkte auf einem zweidimensionalen Bildschirm. Die Direct3D Rendering-Pipeline besitzt dazu im wesentlichen zwei programmier- bzw. steuerbare Einheiten: Vertex-Shader und Pixel Shader. Die Vertex Shader bearbeiten dabei die, durch die Anwendung initialisierte und in VertexPuffern abgelegte reine Geometrieinformation, die in Form von Vektoren (Raumpunkten) vorliegt. Hier finden Geometrie-, Projektions- und Ansichtstransformationsoperationen auf den Daten statt, die durch den Direct3DX Vertex Shader Assembler gesteuert werden. Der erste Schritt in der Verarbeitungsfolge stellt also eine Art Geometrie- oder Transformationspipeline dar, die mit Hilfe der durch die Anwendung spezifizierten Matrizen die Welt-, Ansichts- und Projektionstransformation durchführt. Die resultierenden Daten werden dann im sogenannten Vertex-Assembly-Prozess, bei dem weitere Eigenschaften eines Raumpunktes wie Farbinformation, Texturkoordinaten oder ähnliches in die Verarbeitung mit einbezogen werden zu Einheiten aus geometrischer und visueller Information kombiniert. Diese werden durch Operationen zum Zeichnen von Primitiven (Draw-Primitive) in vorläufige Bildpunktinformation umgewandelt und einem Pixel-Shader übergeben, der unter anderem die Farb- und Transparenzwerte von Texturen bei der Berechnung der Farbinformation eines Bildpunktes mit berücksichtigt. Die Art und Weise wie der Pixel Shader die zur Verfügung gestellten Daten verarbeitet, wird auch hier wieder durch eine programmierbare Einheit, den Direct3DX Pixel Shader Assembler gesteuert. Das Ergebnis der Berechnungen, also der resultierende Bildpunkt durchläuft schließlich Alpha-, Stencilund Tiefentests, bevor er durch den Rasterisierer in den FrameBuffer ausgegeben werden kann. Im Herzstück eines jeden graphischen Systems, der Transformations- oder GeometriePipeline finden nacheinander eine Welttransformation, also Anordnung, Ausrichtung und Skalierung von Objekten in der 3D-Welt eine Ansichtstransformation, die Positionierung des Betrachters oder der Kamera in der Szene realisiert und eine Projektionstransformation, die die Szene auf eine imaginäre zweidimensionale-„Bildplatte“ projiziert statt. In Direct3D müssen deshalb stets entsprechende Matrizen für die Modell-, Ansichts- und Projektionstransformation spezifiziert und zur Laufzeit geladen bzw. umgeladen werden. Soll beispielsweise ein bestimmtes geometrisches Objekt gezeichnet werden, so ist die Pipeline stets vor dem Aufruf der DrawPrimitve-Methode über geeignete Methodenaufrufe auf dem DIRECT3DDEVICE8-Objekt mit den Geometriedaten, der Objekttextur, dem 34 Florian Heidinger „Gegenüberstellung von Direct3D und Java3D am praktischen Beispiel“ Material bzw. der Polygon-Füllfarbe, der Welttransformationsmatrix zur Anordnung des Objekts usw. zu laden. Befinden sich zumindest Teile dieser Daten aus dem unmittelbar vorausgehenden Zeichenvorgang noch in der Pipeline, so müssen sie nicht erneut geladen werden, sondern können für alle weiteren Zeichenvorgänge genutzt werden. Besonders bei sehr großen Geometriedatenmengen, vor allem aber bei hochauflösenden Texturen kann dies eine nicht unerhebliche Zeitersparnis mit sich bringen, die sich in einer höheren möglichen Bildrate äußert. Diese Tatsache muss eine auf Laufzeiteffizienz abgestimmte Direct3D Anwendung durch Realisierung einer geeigneten Zeichenfolge unbedingt berücksichtigen. Die Szene in Direct3D In einem nächsten, sich unmittelbar an die Erzeugung des Fensters und des Canvas3DObjektes anschließenden Schritt wird in Java3D-Anwendungen ein Ansichtsgraph erstellt, welcher die Position relativ zum Ursprung des „Locale“-Objektes, sowie die Blickrichtung und Ausrichtung des Benutzers bzw. der Kamera in der Szene definiert. Es besteht hier jedoch die Möglichkeit eine Klasse mit Standard-Einstellungen (SimpleUniverse) zu verwenden, die in den meisten Fällen absolut ausreicht und auch im Rahmen der Java3DBeispielanwendung zum Einsatz kommt. Der Entwickler von Java3D-Anwendungen bleibt also bei Verwendung des Simple-Universe von der Definition und Initialisierung spezieller Matrizen für Projektions- und Ansichtstransformation verschont. Es sind lediglich Objekte der Klasse „3Dtransform“ erforderlich, die einer geeigneten Transformationsgruppe (TransformGroup) zugeordnet werden, um Welttransformationen oder Animationen von geometrischen Objekten zu ermöglichen. Man kann deshalb davon ausgehen, dass für die Projektionstransformation in Java3D stets eine Standardmatrix Verwendung findet. Die Ansichtstransformation wird im „SimpleUniverse“ ebenfalls über eine vordefinierte Matrix durchgeführt. Hierzu wird der Betrachter in negative z-Richtung schauend im Punkt (0, 0, 2.4) positioniert. Zur Durchführung von Animationen, bei denen die Betrachterposition verändert wird, verwendet Java3D eine erweiterte Welttransformation, d. h. ein „Entfernen“ des Betrachters Was ist eine erweiterte Welttransfor mation ? um 2 Einheiten in positive Z-Richtung (0, 0, 4.4) führt statt zu einer Änderung der Ansichtsmatrix zu einer Verschiebung der Objekte in der Szene um 2 Einheiten vom fixen Betrachterpunkt weg in negative Z-Richtung. Grundgedanke hierbei ist, dass für Animationen des virtuellen Betrachterstandortes mit dieser Technik die Matrix für die Ansichtstransformation lediglich einmalig zu Beginn der Szeneninitialisierung und nicht für jeden Frame erneut berechnet und in die Transformationspipeline geladen werden muss, während die jeweiligen Matrizen für die 35 Florian Heidinger „Gegenüberstellung von Direct3D und Java3D am praktischen Beispiel“ Positionierung der Objekte in der Szene über Welttransformationen ohnehin für jeden zu zeichnenden Frame für jedes zu zeichnende Objekt in die Pipeline geladen werden müssen. Diese Maßnahme bietet neben dem, zumindest für einfache Szenen sicherlich entstehenden Gewinn in der Bildrate auch eine Vereinfachung für den Anwendungsentwickler mit sich, der sich lediglich um eine einzige Transformationsart kümmern muss. In Direct3D sind dagegen keine Standardmatrizen für Projektions- und Ansichtstransformation definiert, so dass der Programmierer dies selbst übernehmen muss. Wie müssen Ansichtsund Projektionstransformation in Direct3D gewählt werden ? Es spricht jedoch auch hier nichts dagegen eine ähnliche Technik wie in Java3D zu realisieren, die statt einer permanenten Anpassung der Ansichtstransformation eine erweiterte Welttransformation realisiert. Man bezeichnet die entstehende Matrix auch als „World-View“-Matrix. Diese Vorgehensweise wird unter anderem auch in der „Dokumentation zu DirectX 8 für C++“ im Kapitel „The View Transformation – Setting Up a View Matrix“ empfohlen. Analog zur Erstellung eines Ansichtsgraphen in Java3D, findet auch in Direct3DAnwendungen eine (einmalige) Spezifizierung der Betrachter- oder Kameraposition und – ausrichtung in der Szene durch die Definition von Projektions- und Ansichtsmatrix statt. Die entsprechenden Routinen können einmalig nach der Erzeugung des DIRECT3DDEVICE8-Objekts erfolgen (s. Methode „szeneInitialisieren()“der Klasse „D3Dinit.cpp“), wenn für Animationen der Betrachterposition eine World-View-Matrix verwendet wird, was im Rahmen des C++Beispielprogramms geschehen soll. Wie Java3D arbeitet auch Direct3D mit einer 4x4-Matrix, die als Struktur (D3DXMATRIX) in der „d3dx8.h“-Headerdatei für die Werkzeugbibliothek definiert ist. Generell ist festzustellen, dass in der Direct3D-Programmierung sehr häufig Strukturen zum Einsatz kommen, während die in Java3D implementierten Entsprechungen als Objekte vorliegen. Die Java3D Implementierung bildet die Werte der Klassenvariablen in geeigneter Weise auf die entsprechenden Strukturelemente ab, wobei die von Java3D nicht unterstützten StrukturEigenschaften durch die API mit Standardwerten gefüllt und an Direct3D übergeben werden. Die Reihenfolge, in der die jeweiligen Matrizen für Projektions- und Ansichtstransformation deklariert, mit Werten gefüllt und in die Pipeline geladen werden ist dabei nicht von Bedeutung. Beginnt man zum Beispiel mit der Angabe Projektionsmatrix, so kann man sich der Helfermethode „D3DXMatrixPerspectiveLH()“ der „DXUtility“-Bibliothek bedienen, um die Matrix mit, entsprechend den übergebenen Parametern von der Methode berechneten Werten füllen zu lassen. Die Endung LH deutet hier darauf hin, dass innerhalb der Szene ein linkshändiges Koordinatensystem zum Einsatz kommen soll, wie es in Direct3DAnwendungen standardmäßig der Fall ist. Man kennt hier jedoch auch eine entsprechende 36 Florian Heidinger „Gegenüberstellung von Direct3D und Java3D am praktischen Beispiel“ Methode für rechtshändige Koordinatensysteme (D3DXMatrixPerspectiveRH()).Die Parameter spezifizieren ähnlich wie in OpenGL ein Viewing Volume, welches durch den Öffnungswinkel, sowie die Z-Koordinaten der vorderen bzw. hinteren Begrenzungsebenen definiert werden kann. Zusätzlich ist der Aspect-Ratio Korrekturfaktor aus den Abmessungen des automatisch bei der Instanziierung des DIRECT3DDEVICE8-Objekts erstellten Viewport zu berechnen und ebenfalls als Parameter zu übergeben. Die den Viewport beschreibende Struktur (D3DVIEWPORT8) mit den Strukturelementen Höhe, Breite und den Koordinaten des linken oberen Eckpunktes kann über die „getViewport()“-Methode des DIRECT3DDEVICE8-Objektes zugegriffen werden. Die hier eingetragenen Werte für Höhe und Breite des Viewports können dann dazu verwendet werden, um das Pixelseitenverhältnis als Quotient zu berechnen. Mit Hilfe der „setTransform()“-Methode des DIRECT3DDEVICE8-Objektes kann die so erzeugte Projektionsmatrix in die Transformationspipeline geladen werden, wo sie in der Regel bis zum Ende des Programms in unveränderter Form abgelegt wird. Der Typ der übergebenen Matrix wird beim Aufruf von „setTransform()“-Methode durch die Konstante D3DTS_PROJECTION als Projektionsmatrix angezeigt. Neben den perspektivischen Projektionen für links- und rechtshändige Koordinatensysteme kennt man in Direct3D auch verschiedene orthogonale Projektionsarten. Als nächstes muss eine konstante Ansichtsmatrix so erstellt werden, dass die Szene unmittelbar nach dem Programmstart aus einer ähnlichen Position wie in Java3D betrachtet werden kann. Die DirectX Werkzeug-Bibliothek bietet hierfür ebenfalls geeignete Hilfsfunktionen für links- und rechtshändige Koordinatensysteme an, mit denen eine entsprechende Matrix nach Angabe des Betrachterstandpunktes (0.0, -2.41, 0.0), des Zielpunktes, auf den der Blick gerichtet wird und des Kameralagevektors (aufrecht stehend mit 0.0, 1.0, 0.0) automatisch mit den richtigen Werten gefüllt wird. Die Helferfunktion „D3DXMatrixLookAtLH()“, die im Rahmen des Beispielprogramms zum Einsatz kommt, erwartet als Parameter die Adressen einer D3DXMATRIX-Struktur, welche innerhalb der Methode mit den errechneten Werten gefüllt wird, sowie von drei D3DXVECTOR3Strukturen mit Informationen zu Betrachterstandpunkt (Augpunkt), Zielpunkt und Kameralagevektor. Durch Angabe von D3DTS_VIEW als erstem Parameter der bereits bekannten „setTransform()“-Methode des DIRECT3DDEVICE8-Objektes wird die Matrix in die Transformationspipeline eingestellt. Diese einmaligen Initialisierungen der Projektions- und Ansichtsmatrizen, sowie der wichtigsten Direct3D-Objekte bildet den Ansichts-Szenengraphen, wie er in einer Java3DAnwendung bei Verwendung des SimpleUniverse entsteht in Direct3D, exakt nach. Zusätzlich ist es sinnvoll, die Objektgeometrien und die, für die visuellen Eigenschaften von geometrischen Objekten benötigten Daten wie Texturen und Materialien, usw. einmalig vor dem eigentlichen Zeichnen der Szene im Rahmen einer Szeneninitialisierungsroutine zu berechnen bzw. zu laden. Rein theoretisch könnten diese Schritte auch für jeden zu 37 Florian Heidinger „Gegenüberstellung von Direct3D und Java3D am praktischen Beispiel“ zeichnenden Frame ausgeführt werden, die Dateizugriffe zum permanenten Laden von Texturen oder komplexe Geometrieberechnungen würden jedoch die Bildrate deutlich absenken. Geometriedefinition in Direct3D Die konkrete Berechnung der Geometrie, sowie die Klassenaufteilung wurden von mir für diese Seminararbeit ähnlich realisiert wie in der Java3D Beispielanwendung. Die Pyramide liegt also auch hier wiederum in Form der drei Klassen „Pyramide.cpp“, „Pyramidenflaeche.cpp“ und „Pyramidenboden.cpp“ vor, die aus jeweils zwei Dateien bestehen. Jedes dieser Dateipaare enthält erstens die Klassendefinition in einer entsprechend bezeichneten Headerdatei und zweitens die Methodendefinition in einer C++Datei. Die Klassenschnittstelle weist dabei Methoden mit nahezu identischen Signaturen wie die Java-Anwendung auf. Zusätzliche private Variablen oder Methodenparameter sind implementierungstechnisch zu begründen. In diesem Zusammenhang ist die für mich wichtigste Eigenschaft gewesen, dass die Pyramidenklassen in der Lage sein sollten, dem DIRECT3DDEVICE8-Objekt ihre Geometriedaten direkt zu übergeben, um anschließend über den Aufruf der „DrawPrimitive()“-Methode „ihren“ Zeichenvorgang einzuleiten. Unter Direct3D werden in der Struktur eines Raumpunkts neben den 3D-Koordinaten immer auch die zusätzlich benötigten Informationen wie Normalen- oder Texturkoordinaten mit Worin bestehen Unterschiede in der Geometriedefinition zwischen Java3D und Direct3D ? abgespeichert, während in Java3D vordefinierte Objekte für Raumpunkte, Textur- und Normalkoordinaten jeweils als Objektarry erzeugt und mit Daten gefüllt werden müssen. Da Art und Umfang der für einen Raumpunkt in Direct3D benötigten Information von Anwendung zu Anwendung, ja selbst von Objekt zu Objekt variieren kann, die Struktur für die Raumpunkte also unterschiedliche Strukturelemente besitzt (von den X, Y und ZKoordinaten einmal abgesehen) muss dem DIRECT3DDEVICE8 bzw. dem Vertex-Shader der Aufbau der Struktur für die Raumpunkte explizit mitgeteilt werden. Dieser Aufbau wird im sogenannten Direct3D Flexible Vertex Format, kurz D3DFVF spezifiziert. Zur Definition müssen hier nach dem Baukastenprinzip vorgegebene Bezeichner wie zum Beispiel D3DFVF_XYZ, D3DFVF_NORMAL oder D3DFVF_TEX1 für die möglichen Strukturelemente bzw. Strukturelementgruppen zusammengestellt werden. Der Vorteil einer solchen Vorgehensweise ist sehr einfach erklärbar. Für jeden Raumpunkt eines Objektes in einer Szene muss ein Speicherbereich entsprechend der Größe der, den Raumpunkt beschreibenden Datenstruktur reserviert werden. Je nach Anwendung wäre es denkbar, dass zumindest einige der Strukturelemente eines Standard-Vertexformats nicht benötigt werden, jedoch trotzdem Speicher belegen. Diesen verschwenderischen Umgang mit System-Ressourcen kann man sich bei komplexen animierten graphischen Szenen nicht erlauben. Diese Tatsache besitzt auch in Java3D-Anwendungen eine bemerkenswerte Parallele. So muss bei der Instanziierung eines Primitvs angegeben werden, welche Informationen bzw. 38 Florian Heidinger „Gegenüberstellung von Direct3D und Java3D am praktischen Beispiel“ Datenarrays dem Primitv zugeordnet verwendet werden können. Java3D besitzt also implizit für jedes Primitv-Objekt ein eigenes Vertex-Format, das der Direct3D API beim Zeichnen angezeigt wird. Bei der Berechnung der Pyramidengeometrien ist in Direct3D-Anwendungen darauf zu achten, dass hier ein linkshändiges Koordinatensystem zum Einsatz kommt. Das 3D Koordinatensystem von Java3D und Direct3D Man kennt in graphischen Systemen zwei Arten von kartesischen Koordinatensystemen: Wodurch sind linksbzw. rechtshändige Koordinaten systeme gekennzeichnet ? linkshändiges und rechtshändiges. Die Achsenrichtungen dieser kartesischen Systeme folgen dabei jeweils der Rechte- bzw. Linke-Hand-Regel, die positive x-Achse ist also nach rechts, die positive y-Achse nach oben gerichtet. Unterschiede bestehen in der Richtung der positiven Z-Achse. Während Java3D ein rechtshändiges Koordinatensystem definiert, bei dem die positive ZAchse dem Betrachter entgegengerichtet ist (er schaut in negative Z-Richtung), verwendet Direct3D ein linkshändiges Koordinatensystem bei dem die positive Z-Achse quasi in den Betrachtungsraum hineingerichtet ist. Da die Java3D-API auf der Direct3D API aufsetzt und diese zur Ausgabe der Szene nutzt, müssen vor der Übergabe der Daten einige Änderungen durchgeführt werden, um die in einem rechtshändigen Koordinatensystem definierte Szene auch tatsächlich wie erwartet ausgeben zu können. Diese Änderungen beziehen sich einerseits auf die Reihenfolge der Raumpunkte, die die Polygone definieren, andererseits auch auf die durchzuführende Ansichtstransformation. Rechtshändige Koordinatensysteme besitzen die Eigenschaft, dass die Raumpunkte der Polygone (Dreiecke), die ein beliebig komplexes Objekt bilden entgegen dem Uhrzeigersinn angegeben werden müssen, damit die Polygonfläche gefüllt dargestellt wird. Polygone, deren Raumpunkte im Uhrzeigersinn angegeben wurden werden nicht angezeigt (BackFace Culling). Hierdurch kann ein graphisches System Rechenzeit beim Zeichnen von Objekten sparen, indem es bestimmte Flächen, nämlich die Rückseiten von Objekten von der Verarbeitung ausschließt. Damit die in Java3D definierten Geometrie-Objekte durch Direct3D wie gewünscht darstellt werden können, muss die Reihenfolge der Raumpunkte, die jeweils ein Primitiv bilden also invertiert werden. Aus der Definition eines Dreiecks mit den benutzerspezifizierten Raumpunkten v0, v1 und v2 in Java3D würde dann die Reihenfolge v0, v2, v1 durch die Java3D API erzeugt und an die Direct3D API übergeben. Da der Betrachter einer in einem linkshändigem Koordinatensystem definierten Szene den in Java3D erstellen Objekten den „Rücken zukehrt“, indem er in positive Z-Richtung blickt muss eine weitere Anpassung durchgeführt werden. Möglich wäre hier etwas eine inverse Welttransformation in ZRichtung für alle Szenenobjekte. Wesentlich einfacher gestaltet sich hier jedoch eine 39 Florian Heidinger „Gegenüberstellung von Direct3D und Java3D am praktischen Beispiel“ Skalierungs-Ansichtstransformation um den Faktor –1 in Z-Richtung, was einer Spiegelung der Raumpunkte an der X-Y-Ebene entspricht. Bei der Pyramidenbodenfläche war zudem die Verwendung eines anderen Primitivs erforderlich, da in Direct3D das in Java3D und OpenGL vorhandene QUADARRAY nicht zur Verfügung stellt. Die Java3D API Implementierung für Direct3D wandelt ein QUADARRAY also implizit in andere, auf dreieckigen Polygonen basierende Primitive um. Hier kann zum Beispiel das von mir verwendete TRIANGLEFANARRAY oder ein beliebiges anderes, gegebenenfalls auch Indiziertes Primitiv zum Einsatz kommen. Ein weiterer Unterschied zur Java3D Anwendung stellt die Bereitstellung der Vertexdaten in VertexBuffern dar, die bei der Erzeugung auf das spezielle Vertexformat eingestellt werden. Was sind VertexBuffer ? Das Erstellen der Vertex-Buffer kann einmalig bei der Initialisierung der Geometrie stattfinden, falls es sich um Objekte mit statischen Geometriedaten handelt. Eine Ausnahme stellen hier die im Rahmen dieser Seminararbeit nicht behandelten Morph-Objekte dar. Ein VertexBuffer kann dabei nur beschrieben werden, wenn er durch die „Lock()“-Methode für andere Zugriffe gesperrt wurde. Dies ist deshalb erforderlich, da die Vertex-Daten häufig im stark frequentierten Graphikspeicher gehalten werden, für den es in der Regel keine besonderen Speicherschutzmechanismen gibt. Die Sperre muss anschließend durch das Gegenstück zu Lock, der „Unlock()“-Methode aufgehoben werden. Die Berechnung der Normalenvektoren über das Kreuzprodukt von zwei Vektoren, die zu jeweils einer das Dreieckspolygon begrenzenden Kanten parallel sind und die anschließende Normalisierung kann ähnlich wie in Java3D durch Methoden der Hilfsbibliothek geschehen. Anders als in Java3D kennt man in Direct3D jedoch auch im Rahmen der Werkzeugbibliotheken keine Routinen, um Texturkoordinaten oder Normalenvektoren automatisch zu generieren. Eine Spezifizierung muss hier also stets manuell auf die an anderer Stelle geschilderte Art und Weise für Normalenvektoren geschehen. Bei komplexeren Objekten gestaltet sich die Spezifizierung der Texturkoordinaten per Hand jedoch ungleich schwieriger. Die Definition von Textur- oder Texelkoordinaten geschieht in Direct3D genau wie in Java3D durch Angabe vor Werten zwischen 0 und 1. Zudem verlangt auch Direct3D die Beschränkung des Bildformates auf ein Potenz von 2. Beleuchtung in Direct3D Neben den Geometrien können auch die im Java3D-Szenengraphen definierten Lichtquellen in der Initialisierungsmethode der Direct3D-Anwendung als D3DLIGHT8Strukturen deklariert, mit geeigneten, die Lichteigenschaften beschreibenden Wertern gefüllt und mittels der „setLight()“-Methode des DIRECT3DDEVICE8-Objektes der Beleuchtungspipeline zugeordnet werden. 40 Florian Heidinger „Gegenüberstellung von Direct3D und Java3D am praktischen Beispiel“ Die auf diese Art und Weise in die Beleuchtungs-Pipeline eingestellten Lichter sind in Direct3D standardmäßig ausgeschaltet und müssen explizit über einen Aufruf der Methode „LightEnable()“ des DIRECT3DDEVICE8 einzeln eingeschaltet werden. Eine Sonderstellung kommt den Umgebungslichtern (ambiente Lichtquellen) zu. Eigentlich kennt Direct3D lediglich Direktional-, Punkt- und Spot-Lichtquellen, während das Umgebungs-Licht nicht als Lichtquelle zählt. Die Farbe des ambienten Lichtes kann nach der erfolgreichen Initialisierung von Direct3D über „setRenderStates()“ des DIRECT3DDEVICE8-Objekts eingestellt werden. Die in Java3D spezifizierten Parameter für die Lichtquellen wie Farbe, Position, Richtung und Öffnungswinkel besitzen in ihren Direct3D Gegenstücken geeignete Entsprechungen, so dass hier eine einfache Umsetzung stattfinden kann. Von den Java3D API-Lichtobjekten nicht unterstützte Eigenschaften wie der innere Öffnungswinkel bei SpotLights, Unterteilung der Lichtfarbe in ambiente, diffuse und spekuläre Farben, usw. werden durch die Java3DImplementierung mit Standardwerten gefüllt. Bei der Verwendung von Lichtquellen in Direct3D ist stets auf Vollständigkeit der Parameter-Werte zu achten. Sind eine oder mehrere Eigenschaften nicht mit (gültigen) Werten versehen, so setzt die Direct3D API keine Standardeinstellungen wie Java3D, sondern die Lichtquelle verhält sich auf unvorhersehbare Art und Weise. Texturen und Material in Direct3D Zusätzlich werden im Initialisierungsschritt einer Direct3D-Anwendung alle (in Java3D von den Shape3D-Objekten referenzierten) Texturen und Material-Objekte geladen bzw. in geeigneten Datenstrukturen zur späteren Verwendung während des Zeichenvorgangs abgelegt. Für die Polygonfüllfarbe aus Java3D, den sogenannten „ColoringAttributes“ existiert hier kein entsprechendes Pendant. Die Farbinformation, mit der eine Fläche gefüllt werden soll, kann in den jeweiligen Strukturen für die Raumpunkte mit abgelegt werden, falls dies im flexiblen Vertex-Format (FVF) entsprechend angemeldet wurde. Der Ladevorgang einer Textur kann auch hier wie in Java3D mit Hilfe einer in der Werkzeug-Bibliothek angebotenen Methode (D3DXCreateTextureFromFileA()) durchgeführt werden. Neben dem Pfad der Texturdatei, die verschiedene Formate besitzen kann ist hier vor allem das DIRECT3DDEVICE8 und ein Surface-Objekt zu spezifizieren. In Direct3D wird eine Textur zu programmtechnischen Verwendung immer in einem sogenannten Surface abgelegt. Ein Surface repräsentiert im Prinzip einen rechteckigen oder quadratischen Speicherbereich im System- oder Graphikspeicher, der mit Hilfe des Surface-COM-Objekts verwaltet werden kann. Ein Surface erlaubt neben dem Speichern und Holen der Bild- / Texturinformation auch die Abfrage der Surface- und damit der Bildgröße. Man erkennt auch hier wieder deutlich die starke Anlehnung der in Java3D konzipierten Klassen an die Möglichkeiten von Direct3D. 41 Florian Heidinger „Gegenüberstellung von Direct3D und Java3D am praktischen Beispiel“ Um den zu zeichnenden Objekten ein bestimmtes Erscheinungsbild / Reflexionseigenschaften zu geben werden anschließend die im Rahmen der Szene verwendeten Materialien als Strukturvariablen vom Typ D3DMATERIAL8 definiert, dessen Strukturelemente ebenfalls weitgehend mit den Klassenvariablen der in Java3D verwendeten „Material“-Klasse identisch sind. Der Anwendungsentwickler kann hier also die Farbinformation als RGBA-Wert für beleuchtete bzw. selbst leuchtende Bereiche (ambientes, diffus, spekulär, emittiert) definieren, sowie die Reflexionseigenschaften des Objektes über einen „Rauhigkeitsfaktor“ spezifizieren. Zeichnen der Szene Nach den erfolgreich durchgeführten Initialisierungen kann endlich mit dem Zeichenvorgang begonnen werden. Jede fensterbasierte Windows-Anwendung realisiert dazu in ihrer WinMain-Methode (Klasse „Hauptprogramm.cpp“) eine Endlosschleife (vgl. Arbeitsweise des Java3D Renderers !!), in der die vom Betriebssystem an diese Anwendung adressierten Nachrichten verarbeitet werden. Diese Nachrichten werden vom System nicht direkt an die Anwendung übergeben (Push), sondern in einer Nachrichtenwarteschlange abgelegt, die in der Endlosschleife bei jedem Durchlauf auf wartende Nachrichten untersucht wird (Pull durch „PeekMessage()“-Methode). Warten Nachrichten im Puffer, so werden diese über „TranslateMessage()“ aufbereitet und per „DispatchMessage()“ über das Betriebssystem an eine sogenannte Callback-Prozedur, deren Adresse (Funktionsname) beim Anlegen des Fensterobjektes in der Schablone spezifiziert werden muss geschickt. Die Callback-Prozedur ist dabei als mehr oder weniger umgangreiche Switch-CaseAnweisung (s. „EventListener.h“) ausgeführt, die die Art der Nachricht auswertet und in Abhängigkeit davon entsprechende Aktionen durchführt. In der Beispielanwendung wird eine Nachricht der Art WM_KEYDOWN, also ein Tastaturanschlag abgefangen und mit Hilfe des Ereignisparameters WPARAM, der in konkreten Fall einen Identifizierer der gedrückten Taste enthält näher untersucht. Handelt es sich bei der gedrückten Taste um die Escape-Taste VK_ESCAPE, so wird eine weitere Nachricht (PostMessage(WM_CLOSE, ...)) in die Nachrichtenwarteschlange geschrieben, die zum Beispiel auch beim Schließen des Fensters durch einen Mausklick auf das x-Symbol eingestellt würde. Die „WM_Close“-Nachricht wird nicht explizit in einem Zweig der Switch-Case Anweisung sondern durch die von Windows standardmäßig definierten Routinen verarbeitet. Dies wird durch die DefWindowProc()-Anweisung implizit veranlasst. Wartet keine Nachricht, was bei der Mehrzahl der Durchläufe der Fall sein sollte, so kann der eigentliche Zeichenprozess durchgeführt werden. In der Beispielanwendung ist neben der Verarbeitung der Tastaturereignisse zusätzlich eine Behandlung von Mausereignissen erforderlich, um beispielsweise eine Bewegung des 42 Florian Heidinger „Gegenüberstellung von Direct3D und Java3D am praktischen Beispiel“ Benutzers in der Szene analog zu den „MouseRotate“-Verhalten in Java3D zu ermöglichen. Die Java3D Implementierung verwendet hierfür ebenfalls die AWT-Routinen, die auf dem Windows-Nachrichtensystem aufsetzen. Es wäre also durchaus möglich, die Verarbeitung der Mausereignisse ebenfalls in der Callback-Prozedur durchzuführen. DirectX bietet hierfür jedoch mit DirectInput eine spezialisierte API-Sammlung an, mit der neben der hochperformanten Ansteuerung von Standard-Eingabegeräten wie Maus und Tastatur auch spezielle Eingabegeräte wie Joysticks, mit einer Vielzahl von Knöpfen und Funktionstasten und mehreren Achsen angesteuert werden können. Da die DirectInput-API nicht Gegenstand der Seminararbeit darstellt, soll an dieser Stelle nur kurz erwähnt werden, dass wie im Windows Nachrichtensystem eine Warteschlange definiert wird, in die bei Mausereignissen Nachrichten für Bewegungen in X- und Y-Richtung, sowie das Drücken oder Loslassen von Buttons eingestellt werden. Diese Nachrichten, die neben dem Interaktionstyp zusätzliche Ereignisparameter enthalten, über die zum Beispiel festgestellt werden kann, welche Taste gedrückt wurde oder wie weit die Maus in eine bestimmte Richtung bewegt wurde, kann wiederum durch eine Switch-Case-Anweisung („MausPufferVerarbeiten()“-Methode) verarbeitet werden. In den Switch-Case-Zweigen findet hier die Realisierung der erweiterten WeltAnsichtstransformation statt. Für jedes in der Szene dargestellte Objekt muss für eine adäquate Nachbildung des in Java3D angebotenen „MouseRotate“-Verhaltens eine Rotation entgegen der Bewegungsrichtung der Maus erfolgen. Soll die Szene also beispielsweise im Uhrzeigersinn um eine bestimmte Achse rotiert werden, so sind die Objekte um den entsprechenden Winkel entgegen dem Uhrzeigersinn um diese Achse zu rotieren. Wichtig ist hierbei die Behandlung dieser Transformation nach dem LIFO-Prinzip, d.h. die entsprechende Rotationsmatrix muss in der resultierenden World-View Matrix immer als erster Faktor des Produktes, die aktuelle Weltmatrix des Objektes als zweiter Faktor eingehen. Neben den Verhalten zur Behandlung von Benutzerinteraktionen kennt man in Java3D auch solche zu Realisierung von Animationen, die sich eines geeigneten Alpha-Objektes bedienen, um zum Beispiel Rotationswinkel pro Frame, Rotationsanzahl und Rotationsgeschwindigkeit zu definieren. Diese Funktionalität muss in Drect3D durch eine, in der Endlosschleife für jeden Frame vor dem Zeichnen des zu animierende Objekts stattfindende Anpassung der Welttransformationsmatrix erfolgen. Nach der Verarbeitung der Mausereignisse und der „Animationen“ kann das Zeichnen der Objekte mit Hilfe der bei der Initialisierung vorbereiteten Datenstrukturen für Geometrien (VertexBuffer), Texturen (Surfaces) und Materialien begonnen werden. Beginn und das Ende der Zeichenvorgänge müssen dem DIRECT3DDEVICE8-Objekt durch Aufrufe von „BeginScene()“ und „EndScene()“ angezeigt werden. Nur zwischen 43 Florian Heidinger „Gegenüberstellung von Direct3D und Java3D am praktischen Beispiel“ diesen beiden Aufrufen ist es möglich, Primitive durch Aufruf von „DrawPrimitive()“ zu zeichnen. Dazu muss die Rendering-Pipeline in möglichst optimaler Reihenfolge mit den Vertexdaten, Materialien, Texturen und Transformationsmatrizen versorgt (vgl. State Ordered Rendering Was geschieht vor dem Zeichnen eines Objekts ? in Java3D) werden. Alle diese Operationen können mit Hilfe der Schnittstellenmethoden des DIRECT3DDEVICE8-Objektes durchgeführt werden, das auch in diesem Zusammenhang wiederum eine zentrale Rolle einnimmt. Die Pipeline nimmt dabei zum Zeichnen eines konkreten Objektes einen eindeutig definierten, in Java3D Anwendungen durch den Pfad von der Wurzel des Inhaltsgraphen zum jeweiligen Blatt-Objekt vollständig definierten Zustand an, der zur Ausgabe des in der Zeichenfolge unmittelbar nachfolgenden Objektes möglichst nur geringfügig geändert werden sollte. Wichtig ist dabei Tatsache, dass einmal in der Initialisierungsphase der Anwendung vorbereitete, mit Daten gefüllte und über das Device-Objekt in der Rendering-Pipeline eingestellte Material-Struktur, Textur-Surface oder Matrizen für Projektions-, Ansichts- und Welttransformationen für alle, ab diesem Zeitpunkt gezeichneten Objekte bis zum Austausch einer oder mehrerer dieser Komponenten Verwendung finden. Die Änderung des Pipeline-Status gestaltet sich dabei je nach auszutauschender Komponente unterschiedlich aufwendig. So erfordert das Einstellen einer anderen Oberflächentextur den größten Aufwand. Dies gilt ganz besonders dann, wenn die Texturen im Systemspeicher und nicht im schnelleren Graphikkartenspeicher abgelegt werden können und bei einem Wechsel folglich über das Bussystem vom Hauptspeicher in den Graphikspeicher transportiert werden müssen. Etwas einfacher gestaltet sich ein Wechsel der Geometriedaten, d. h. die Umstellung auf einen anderen VertexBuffer. Trotzdem sollte man den Aufwand für diese Operation nicht unterschätzen. Was für Java3D-Anwendungen gilt, ist also auch für Direct3D Anwendungen von Bedeutung: Die Verwendung vieler kleiner Primitive und damit VertexBuffer kann sich nachhaltig negativ auf das Laufzeitverhalten auswirken. Es liegt hier also in der Verantwortung des Anwendungsentwicklers, Geometrien soweit als möglich zu größeren Einheiten zusammenzufassen, die in einem einzigen VertexBuffer abgelegt werden können. Ein Stripifier-Werkzeug wie in Java3D steht hierfür jedoch nicht zur Verfügung. Der Wechsel von Material-Struktur oder Welttransformationsmatrix sind in diesem Zusammenhang weniger kritische Operationen auf der Rendering Pipeline, die jedoch ebenfalls möglichst selten durchgeführt werden sollten. In der Regel ist zumindest ein Austausch der Welttransformationsmatrix für jedes zu zeichnende Objekt erforderlich, da sich sonst mehrere Objekte mit gleicher Position, Ausrichtung und Größe ganz oder zumindest teilweise in der Szene überdecken. Die angestellten Überlegungen zur Vorgehensweise der Java3D Implementierung bei der Ausgabe einer Szene führen auch den Direct3D-Entwickler auf Regeln zur optimalen Gestaltung des Programms. Auf die Beispielanwendung bezogen sollte also durch die Java3D Implementierung bzw. den Direct3D Entwickler folgende Zeichenreihenfolge eingehalten bzw. erstellt werden. 44 Florian Heidinger „Gegenüberstellung von Direct3D und Java3D am praktischen Beispiel“ 1. Zeichnen der Objekte mit gleicher Textur 2. Zeichnen der Objekte mit verschiedenen Texturen aber gleicher Geometrie 3. Zeichnen der Objekte mit verschiedenen Texturen und Geometrien aber gleichen Materialeigenschaften und / oder Welttransformationen Für die Beispielanwendung bedeutet dies, dass zum Beispiel zunächst die (durch die Java3D Implementierung zusammengefassten) Pyramidenseitenflächen gezeichnet werden sollten. Diese verwenden gleiche Texturen und Materialien, es müssen also lediglich die Geometriedaten und die Welttransformationsmatrix ausgetauscht werden Nach einem Austauschen der Textur, des Materials, der Geometrie und der Welttransformationsmatrix kann dann der erste Pyramidenboden gezeichnet werden. Zur Ausgabe der zweiten Bodenfläche müssen dann wiederum lediglich die Geometriedaten und die Welttransformationsmatrix ausgetauscht werden. Natürlich könnte in diesem einfachen Beispiel auch zunächst die Ausgabe der Pyramidenböden erfolgen und erst anschließend die Pyramidenseitenflächen gezeichnet werden. Die im Rahmen der Objekt-Definition der Pyramide spezifizierte Zusammengehörigkeit der geometrischen Objekte Pyramidenfläche und Pyramidenboden wird also beim Wieso sind Direct3DAnwendungen prozedural aufgebaut ? Zeichenvorgang in geeigneter Weise aufgespalten, um eine optimale, den Status der Rendering-Pipeline möglichst weitgehend erhaltende Reihenfolge zu erhalten. Diese Vorgehensweise widerspricht dem Prinzip der Objektorientierung, erlaubt jedoch die effiziente Nutzung der zugrundeliegenden Hardware. Hierin ist sicherlich einer der Gründe zu suchen, weshalb Direct3D-Anwendungen trotz der in C++ möglichen Klassenbildung vorwiegend rein prozedural konzipiert und implementiert sind. Vor vier Tutorials zur Thematik Direct3D verfolgt lediglich eines einen objektorientierten Ansatz, alle anderen, auch die offiziell von Microsoft in der SDK Dokumentation veröffentlichten Beispielanwendungen bilden da keine Ausnahme. Trotzdem reichen diese Erkenntnisse in überschaubaren Anwendungen wie der Beispielanwendung aus, um auch als unerfahrener Entwickler eine geeignete Zeichenfolge zu erstellen. Was hier jedoch nur im kleinen Rahmen bzw. mit umfangreichen Erfahrungen möglich ist wird dem Anwendungsentwickler in Java3D vollständig durch die Implementierung abgenommen. Für die Beispielanwendung habe ich trotzdem eine Bereitstellung der Pyramide in Klassenform gewählt da bei einem derart einfachen geometrischen Objekt durchaus der Aspekt der Wiederverwendbarkeit in anderen Projekten den Ausschlag gegenüber der nur unwesentlich besseren Zeichenreihenfolge geben kann. 45 Florian Heidinger „Gegenüberstellung von Direct3D und Java3D am praktischen Beispiel“ Literaturhinweise Java3D Bouvier, Dennis “The Java3D Tutorial” – Kapitel 1-7 (1999) Subra, Mohan “Java Markets Whitepaper” (1998) Sun Microsystems, “The Java3D API - Technical Whitepaper” (1997) Sun Microsystems, “Java 3D API Collateral – 1.2.1 Performance Guide” (2002) Sun Microsystems, “Java 3D Programming: A Technical Overview” (1998) www.j3d.org “Java 3D API” Direct3D Dunlop, Robert “Teach Yourself DirectX 7 in 24 Hours” (1998) Microsoft, “DirectX 8.0 Documentation (Visual C++)“ (2000) Zerbst, Stefan “3D Graphik-Programmierung und Spiele-Programmierung mit DirectX“ (2002) 46