Computer Graphics Charakter-Animation
Transcription
Computer Graphics Charakter-Animation
Hochschule Fulda – University of Applied Sciences Computer Graphics Charakter-Animation Eine Gegenüberstellung unter DirectX und OpenGL Autoren: Michael Genau Andreas Gärtner Charakter-Animation Eine Gegenüberstellung unter DirectX und OpenGL WS 08/09 Eigenständigkeitserklärung Wir versichern hiermit, dass wir unsere Teilbereiche der Ausarbeitung selbstständig verfasst und keine anderen als die angegebenen Hilfsmittel benutzt haben. Die Stellen, die anderen Werken dem Wortlaut oder dem Sinn nach entnommen wurden, haben wir in jedem einzelnen Fall durch die Angabe der Quelle kenntlich gemacht. ____________ Ort, Datum _________________________________ Unterschrift (Michael Genau) ____________ Ort, Datum ___________________________________ Unterschrift (Andreas Gärtner) Anmerkungen 1 Wie zuvor abgesprochen überschreite ich, Michael Genau, den normalen Umfang einer Ausarbeitung von bis zu 25 Seiten. Des Weiteren wird, wie abgesprochen, in der Implementierung des Teils DirectX C++ eingesetzt. Dies ist darin begründet, dass für die im DirectX-SDK angebotenen Funktionen der Bibliothek D3DX, beispielsweise zum Laden von 3D Objekten aus X-Dateien, die Hierarchien oder benutzerdefinierte Templates enthalten, Strukturen erweitert und zugleich deren Methoden überschrieben werden müssen. Anmerkungen 2 Wie zuvor abgesprochen konnte ich, Andreas Gärtner, die Implementierung des Skinning mit OpenGL aufgrund von Zeitmangel nicht mehr durchführen. Da das 3DS Dateiformat keine Bones speichert, hätte ich ein neues Dateiformat und einen dafür geeigneten Loader suchen müssen. Die Durchführung der anderen Aufgaben (siehe Aufwandsabschätzung) dauerte bereits bis Anfang Januar. Seite |I Charakter-Animation Eine Gegenüberstellung unter DirectX und OpenGL WS 08/09 Inhaltsverzeichnis Eigenständigkeitserklärung Anmerkungen 1 Anmerkungen 2 I I I 1. Einleitung 1 1 1 1.1. Motivation 1.2. Zielsetzung 2. Theoretische Grundlagen (Verfasst von Michael Genau) 2.1. Einführung in die 3D Animation 2.2. Verfahren zur Interpolation 2.3. Datei-Formate für 3D-Objekte 2.3.1. Das X-Dateiformat 2.3.2. Das 3DS-Dateiformat (Verfasst von Andreas Gärtner) 2.4. Vergleich zwischen Animationen mit der CPU und der GPU 2.5. 3D-Bibliotheken und ihre Shader-Sprachen 2.5.1. Shader im Allgemeinen 2.5.2. DirectX 2.5.2.1. Entwicklungsgeschichte von DirectX 2.5.2.2. DirectX-Graphics 2.5.2.3. HLSL 2.5.3. OpenGL (Verfasst von Andreas Gärtner) 2.5.3.1. GLSL 2.6. Zusammenfassung 2 2 3 4 4 5 7 7 7 8 8 9 9 11 11 13 3. Konzeption (Verfasst von Michael Genau) 14 3.1. Die Bedeutung der Zeit 3.2. Konzeption der DirectX-Anwendungen 3.2.1. Einrichtung der Arbeitsumgebung 14 3.2.2. DirectX Graphics und D3DX 15 3.2.2.1. Konzept zur Implementierung der Animationstechnik Skinning 15 3.2.2.2. Konzept zur Implementierung der Animationstechnik Tweening 17 3.2.3. Animation Blending 18 3.2.4. DirectX-Shader (HLSL) 3.2.4.1. Einführung in HLSL 3.2.4.2. Integration Eines Vertexshaders in eine Anwendung 3.2.4.3. Konzept zur Implementierung einer Charakter-Animation auf Basis eines Shaders 22 3.2.4.4. Effekt Dateien – eine Erweiterung zur Implementierung von Shadern 22 3.3. Konzeption der OpenGL-Anwendungen (Verfasst von Andreas Gärtner) 24 3.3.1. Einrichtung der Arbeitsumgebung 24 3.3.2. Kompilieren und Starten der Anwendungen 25 3.3.3. Animationen mit GLSL 3.3.3.1. Datentypen und Funktionen 25 3.3.3.2. Integration von Shadern in ein Programm 27 3.4. Zusammenfassung 14 14 18 18 19 25 29 S e i t e | II Charakter-Animation Eine Gegenüberstellung unter DirectX und OpenGL WS 08/09 4. Realisierung der Konzeption 30 4.1. Modellierung und Animation des Charakters 30 (Verfasst von Michael Genau) 4.1.1. Modellierung 4.1.2. Animation 4.2. Implementierung der DirectX-Anwendungen (Verfasst von Michael Genau) 4.2.1. Aufbau einer Windows-Applikation 4.2.2. Initialisierung der benötigten DirectX-Komponenten 4.2.2.1. DirectX-Graphics 4.2.2.2. DirectInput 4.2.3. Laden von 3D-Objekten aus einer X-Datei 4.2.4. Zeitmessung 4.2.5. CPU-basierte Charakter-Animation durch Tweening 4.2.6. GPU-basierte Charakter-Animation durch Tweening mit einem HLSLVertexshader (Effekt-Datei) 4.2.7. Repräsentation der Animationen in einer X-Datei 4.2.8. Erweiterung des X-Dateiformats für Morphing und Key-Frames 4.2.9. Charakter-Animation durch Skinning 4.2.9.1. Laden des Meshs und Aufbau der benötigten Datenstrukturen 4.2.9.2. Animation des Charakters 4.2.9.3. Rendern des Charakters mit einem HLSL Vertexshader 4.3. Implementierung der OpenGL-Anwendungen (Verfasst von Andreas Gärtner) 4.3.1. Laden der 3d Objekte 4.3.2. Normalenberechnung des 3ds Loaders 4.3.3. Initialisierung von OpenGl und Glut 4.3.4. Morphing mit OpenGL 4.3.5. Morphing mit GLSL 4.3.5.1. Erweiterung der bestehenden Implementierung 4.3.5.2. Implementierung des Vertex Shaders 4.3.5.3. Implementierung des Fragment Shaders 4.4. Zusammenfassung 30 31 31 32 33 33 34 35 37 38 39 42 45 46 46 47 47 50 50 51 52 53 55 55 57 58 59 5. Fazit 61 6. Anwendungsdokumentation 62 62 62 62 6.1. Erläuterung der Ordnerstruktur 6.2. Verwendete Werkzeuge 6.3. Aufwandsabschätzungen VI. Literaturverzeichnis LXIV VII. Abbildungsverzeichnis VIII. Weitere Anlagen LXV LXVII S e i t e | III Charakter-Animation Eine Gegenüberstellung unter DirectX und OpenGL WS 08/09 1. Einleitung In der heutigen Zeit werden animierte Filme und Videospiele immer realistischer. Einen großen Anteil daran tragen die Animationen der Charaktere bei. Daher wird in dieser Ausarbeitung das Thema der Animation von 3D-Objekten und insbesondere von Charakteren behandelt. 1.1. Motivation Sie haben sicher schon einmal eine Animation gesehen und waren davon gefesselt. Ein Grund hierfür ist die immer geringer werdende Grenze zwischen Computergrafik und Realität. Besonders deutlich wird dieser Prozess dann, wenn man sich die verschiedenen Generationen der Grafikkarten und die dazugehörigen Computerspiele ansieht. Als Motivation für diese Arbeit dienen daher die verschiedenen aktuellen Techniken und Grafikbibliotheken, die für die Realisierung solcher Animationen zur Verfügung stehen. Begriffe wie DirectX oder OpenGL sind hier sicher jedem bekannt. Doch immer, wenn man vor einer neuen Aufgabe steht, muss man sich entscheiden, auf welche Art und Weise man das Vorhaben umsetzt. Um diese Entscheidung zu erleichtern, ist es das Ziel dieser Arbeit die vorhandenen Techniken miteinander zu vergleichen, was im nächsten Abschnitt detaillierter aufbereitet ist. 1.2. Zielsetzung Das Ziel dieser Ausarbeitung ist es die vorhandenen Techniken zur Charakter-Animation gegenüber zu stellen. Dabei sind zwei Grafikbibliotheken, nämlich DirectX und OpenGL, zu unterscheiden. Doch neben dieser Differenzierung in zwei Grafikbibliotheken gilt es auch die jeweiligen CPU- und GPUbasierten Methoden zu erläutern. Im Vordergrund steht dabei jeweils die Realisierung einer Animation. Wobei vor allem Performanceunterschiede zwischen den CPU- und den GPU-basierten Methoden zu erwarten sind. Ein Ziel ist es daher, diese Unterschiede zu messen und somit am Ende die Performance der Methoden zu vergleichen. Doch die Geschwindigkeitsunterschiede sind nicht das einzige Maß, an denen die Techniken bewertet werden. Das zweite Kriterium ist die Komplexität, denn nicht für jede Aufgabe benötigt man eine extrem hohe Performance, wenn man auf der anderen Seite Vorteile wie eine geringere Komplexität und einen geringeren Programmierungsaufwand besitzt. Neben diesen praktischen Zielen wird unter anderem auf die benötigte Theorie zur Animation eingegangen. Hier sind vor allem Skinning und Tweening mittels Key-Frames von Bedeutung. Solche Animationen werden typischerweise mit Hilfe von 3D-Modellierungsprogrammen wie Maya, Lightwave3D oder 3DStudio Max erstellt. Um diese Modelle und Animationen in einer eigenen Anwendung zu laden, gilt es wichtige Bestandteile der Dateiformate zu erläutern. Ein grundlegendes Wissen über die verwendeten Formate ist für die Verarbeitung von großer Bedeutung. Ein weiterer wichtiger Punkt in der Animation ist die Interpolation, daher werden hier zwei bedeutende Verfahren vorgestellt. Doch kann in dieser Ausarbeitung das Thema der Charakter-Animation nicht allumfassend behandelt werden. Aus diesem Grund wird beispielsweise auf die Simulation von Kleidung, die die Charaktere eventuell tragen, nicht eingegangen. Bei Interesse an solch fortgeschrittenen Techniken sind jedoch die angegebenen Quellen zu empfehlen. S e i t e | IV Charakter-Animation Eine Gegenüberstellung unter DirectX und OpenGL WS 08/09 2. Theoretische Grundlagen Das Kapitel 2 – Theoretische Grundlagen – beschreibt einführend Techniken, die bei 3D-Animationen von Bedeutung sind und erläutert den dafür benötigten theoretischen Hintergrund. Auf Basis dieser Grundlagen wird auf zwei Dateiformate für 3D-Objekte näher eingegangen. Des Weiteren werden, wie bereits angesprochen, die Animationen auf DirectX und OpenGL basieren. Aus diesem Grund gilt es diese zwei Bibliotheken mit samt den dazugehörigen Shader-Programmiersprachen und weitere bedeutende Belange vorzustellen. 2.1. Einführung in die 3D Animation In der Computer-Animation kann man mehrere Techniken zur Animation unterscheiden. In dieser Ausarbeitung werden Tweening, auch als Morphing bekannt, und Skinning, auch als SkelettAnimation bekannt, behandelt. Bei beiden Techniken sind Key-Frames von großer Bedeutung. Um Skinning zu verstehen ist grundlegendes Wissen über Bones notwendig. Bones dienen als Gerüst für Skelette, um die Knochen der Charaktere zu simulieren. Als Gelenke, die diese Knochen miteinander verbinden, dienen so genannten Joints. Die Verwendung von Skinning erleichtert die Animation von Körperbewegungen ungemein. Dadurch, dass jeder Vertex einem oder mehreren Bones zugewiesen werden kann, ist es nicht notwendig, die Vertices einzeln zu animieren. Ein Bone oder Joint ist also ein Kontrollpunkt für eine Gruppe von Vertices. Wenn sich dieser bewegt, dann bewegen sich alle zugewiesenen Vertices automatisch mit. Wie und in welcher Form hängt dabei von den Parametern und eventuell hierarchisch untergeordneten Kontrollpunkten ab. Durch diese Hierarchie entsteht demnach eine Baumstruktur, wobei die Wurzel als Root-Joint bezeichnet wird. Eine Animation lässt sich hiermit beispielsweise erstellen, indem man in mehreren Key-Frames die Bones derart manipuliert, so dass ein Walk-Cycle entsteht. Als praktisches Beispiel kann man sich hier die Bewegung des Oberarmes vorstellen. Indem jemand seinen Oberarm bewegt, bewegt sich dessen Unterarm automatisch mit. Wird jedoch lediglich der Unterarm bewegt, so bleibt der Oberarm ruhig. Ein großer Vorteil der Animation mit Bones ist der Realismus bei der Animation von Charakteren. Deutlich wird dieser Realismus vor allem dann, wenn man sich klar macht, dass ein Programm die Bones beliebig manipulieren kann. Beispielsweise bei einer Kollision mit einem anderen Gegenstand in der Szene oder bei einer Rag-Doll-Animation. Ein Nachteil, den dieses Verfahren jedoch besitzt ist seine Komplexität. Diese erschwert es das Verfahren zu verstehen und zu implementieren. Im vorherigen Abschnitt ist der Begriff Key-Frame verwendet wurden. Doch was ist ein Key-Frame eigentlich? Ein Key-Frame ist ein Stützpunkt in einer Szene, an dessen Position beispielsweise ein Bone manipuliert wird. Für jeden Key-Frame werden daher die Parameter der Objekte, also unter anderem die Koordinaten und Rotationseigenschaften der Bones, gespeichert. Hierbei ist zwischen einer kombinierten Matrix oder separaten Werten für die entsprechenden Parameter in der späteren Betrachtung zu differenzieren. Dadurch ist man in der Lage zwischen jeweils zwei Key-Frames zu interpolieren. Eine wichtige Frage, die an dieser Stelle zu klären ist, ist die Form der Interpolation. Die einfachste Variante wäre es immer linear zu interpolieren. Dieses Vorgehen zerstört jedoch sehr schnell den Eindruck der virtuellen Realität. Aus diesem Grund bieten 3D-Modellierungswerkzeuge die Möglichkeit für Animationen mit Key-Frames Kurven für jede Achse anzugeben. Doch diese Verfahren spielen unter anderem bei der Spieleentwicklung eine untergeordnete Rolle. Daher gibt es verschiedene Algorithmen, um eine hohe Performance und Realismus zu vereinen. Sehr oft werden für die Interpolation die Verfahren LEPR beziehungsweise SLERP verwendet. Diese beiden Verfahren nutzen zur Berechnung Quaternionen und werden später vorgestellt. Seite |V Charakter-Animation Eine Gegenüberstellung unter DirectX und OpenGL WS 08/09 Die zweite Form der Charakter-Animation ist das Tweening. Hierbei handelt es sich ebenfalls um eine Technik, die zwischen zwei Key-Frames interpoliert. Im Gegensatz zu Skinning werden allerdings keine Bones oder ähnliches als Stützgerüst benötigt. Stattdessen sind in jedem Frame die gesamten Objektdaten vorhanden, eben nur verschieden animiert. Man muss also die Vertices beziehungsweise Polygone von Hand manipulieren, wodurch sich viele Möglichkeiten ergeben, die sich bei Skinning nicht bieten. Ein Beispiel hierfür sind zwei Key-Frames, wobei im ersten eine Figur positioniert wurde die keine besondere Mimik aufweist. Wenn man im zweiten Key-Frame dieser Person ein Lächeln animiert, so kann man durch Tweening einen flüssigen Übergang zwischen einer Person die nicht lächelt und einer Person die lächelt realisieren und umgekehrt. Bisher wurden diese zwei Techniken nur getrennt voneinander betrachtet. Sie lassen sich aber durchaus kombinieren, denn beide haben ihre Vorteile. Beispielsweise indem man Körperbewegungen wie das Laufen mit Skinning und die Mimik mit Tweening realisiert. Ein weiteres Beispiel für Tweening ist, wenn ein Charakter mutiert, in solch einer Situation stößt Skinning an seine Grenzen. Man sieht demnach, dass beide Techniken Verwendung finden und man sich daher gründlich überlegen muss, wie man was realisiert. 2.2. Verfahren zur Interpolation Im vorherigen Kapitel wurde oftmals der Begriff Interpolation verwendet. Dieses Kapitel beschreibt, was man darunter versteht und wie man solch ein Verfahren realisiert. Eine Interpolation ist eine diskrete Approximation. Unter Approximation versteht man die Bestimmung einer Ersatzfunktion aus einer gegebenen Funktionsklasse , die von einer Funktion möglichst wenig abweicht. Wobei der Approximationsfehler der Interpolation an endlich vielen, fest vorgegebenen Stützstellen, zu null wird. (1) Um die später betrachteten Verfahren zu verstehen ist an dieser Stelle ein wenig Mathematik notwendig. Beispielsweise sind Vektoren und Matrizen in der 3D-Computergrafik von grundlegender Bedeutung. Doch in diesem Abschnitt geht es um Quaternionen. W.R Hamilton erdachte sie im Jahr 1843 in Dublin (2). Dabei wurden sie als eine vierdimensionale Erweiterung der komplexen Zahlen verwendet. In diesem Kontext werden sie jedoch zur Beschreibung von Rotationen in einem dreidimensionalem Raum verwendet. Ein Quaternion besteht aus vier Zahlen, die Rotationen repräsentieren. Dabei besteht ein Quaternion aus zwei Komponenten – einem Vektor (x, y, z) und einem Skalar (w). Der Wert des Skalars beschreibt den Winkel der Rotation in der Form 2-1 Gimbal Lock. Quelle: (2) . An dieser Stelle kann man sich fragen wofür man das alles benötigt – nicht wahr? Dies tiefgründig hier zu belegen sprengt den Rahmen der Ausarbeitung. Der wichtigste Grund ist, dass Quaternionen unempfindlich gegenüber dem Phänomen Gimbal Lock sind. Gimbal Lock bezeichnet ein Problem bei Transformationen, dass in bestimmten Situationen auftritt, wenn man Eulerwinkel zur Berechnung zugrunde legt. Dabei entsteht eine Blockade einer Drehrichtung bei einer kardanischen Aufhängung. S e i t e | VI Charakter-Animation Eine Gegenüberstellung unter DirectX und OpenGL WS 08/09 Weitere Vorteile sind, dass durch die Beschreibung mittels Quaternionen weniger Speicherplatz als bei der Verwendung von Matrizen benötigt wird und weichere Animationen realisiert werden können. Nachteilig wirkt sich jedoch aus, dass man die Quaternionen in Matrizen konvertieren muss, bevor man sie an die Grafikkarte sendet. In (2) wird mathematisch beschrieben, wie diese Transformationen durchgeführt werden, so dass man sich hierfür eine eigene Klasse implementieren kann. Jedoch existiert beispielsweise in DirectX solch eine Klasse schon. Nach der Erklärung dieser Grundlagen verstanden, ist man auch in der Lage den Hintergrund der Verfahren LERP und SLERP zur Interpolation zu verstehen. LERP bezeichnet eine lineare Interpolation und SLERP bezeichnet eine lineare kugelförmige Interpolation. Der Vorteil von SLERP ist eine weichere Animation. Wobei SLERP bei kurzen Distanzen unrealistisch wirkt, aus diesem Grund wird in solchen Situationen LERP verwendet. Bei tiefgründigem Interesse an diesen Verfahren, wobei auch das rechnen mit Quaternionen von Bedeutung ist, ist die Quelle (2) zu empfehlen. 2-2 Visualisierung von LERP und SLERP. Quelle: (2) 2.3. Datei-Formate für 3D-Objekte Im Laufe der Zeit sind viele Dateiformate entstanden. Beispielsweise das MD2 Format von id Software, was unter anderem bei Quake 2 zum Einsatz kam oder MD3 für Quake 3. Ein weiteres bekanntes Format ist das MDL-Format von Valve im Spiel Half-Life. In dieser Ausarbeitung werden allerdings zwei andere Formate verwendet. Auf der einen Seite das X-Format in Verbindung mit DirectX und auf der anderen Seite das 3DS-Format bei OpenGL. Daher werden diese beiden Formate anschließend näher betrachtet. Zuerst gilt es jedoch zu klären, was in solch einer Datei alles gespeichert wird. Grundsätzlich gilt es immer die Vertizes der 3D-Modelle zu speichern. Neben diesen Daten sind auch Materialeigenschaften des Objektes oder von einzelnen Teilen des Objektes darin enthalten. Des Weiteren können Informationen über Texturen, Beleuchtung oder auch Animationen enthalten sein. Wie genau diese Daten in den verwendeten Formaten abgelegt sind wird in den beiden folgenden Kapiteln erklärt. 2.3.1. Das X-Dateiformat In diesem Kapitel wird das X-Format von Microsoft insofern erläutert, dass man es grundlegend versteht und in der Lage sind sich ein Bild vom Aufbau zu machen. In der Anlage 8.1 ist eine solche Datei abgebildet. (3) Am Anfang der Datei steht der Header. Das erste Token identifiziert dabei die Datei als eine X-Datei. Das zweite Token zeigt die Version der X-Datei an und bestimmt zugleich, dass die Datei als Textformat vorliegt. Neben diesem Format existieren auch ein binäres sowie ein binäres komprimiertes Format. Das letzte Token des Headers gibt an, ob die Datei in einer 32 oder 64 Bit Version vorliegt. Nach dem Header folgen mehrere Chunks, die sich in Templates und Data Objects unterteilen lassen. Ein Chunk, engl. für Brocken oder auch Stück, ist eine Menge an Informationen. Chunks werden in Formaten für 3D-Modelle sehr oft verwendet. Sie erleichtern es beispielsweise eigene Ladefunktionen zu schreiben. Dabei besteht eine Datei aus einer Anzahl aneinandergereihter Chunks. Wobei jeder Chunk aus einem Header und Daten besteht. S e i t e | VII Charakter-Animation Eine Gegenüberstellung unter DirectX und OpenGL WS 08/09 Ein Data Object ist dabei immer eine Instanz eines Templates. In Folge dessen beschreiben die Templates Informationen wie das Layout der 3D-Objekte, die in der X-Datei abgebildet sind. Das erste Template in diesem Beispiel heißt Header, was einem Klassennamen gleichkommt. Es enthält eine GUID und drei weitere DWORD Variablen. Die GUID (Global Unique Identification Number) dient DirectX dazu das Template eindeutig zu identifizieren, wenn es geladen wurde. In Verbindung mit diesem Template steht das Data Object Header. Dieses initialisiert die drei DWORD Variablen, wobei alle Variablen in der korrekten Reihenfolge initialisiert werden müssen. Das zweite Template in dieser Datei beschreibt einen Frame. Ein Frame ist ein spezielles Template, es beschreibt keine Objektdaten sondern Hierarchien. Daher werden innerhalb dieses Templates andere Template-Klassen referenziert. In diesem Beispiel sind es die Klassen FrameTransformMatrix und Mesh. Dadurch, dass nur diese zwei Klassen referenziert sind, wird sogleich eine Restriktion erstellt. Betrachtet man diese Templates genauer, so wird ersichtlich, dass auf den Mesh zwei Transformationsmatrizen angewendet werden. Durch solche Hierarchien wird es prinzipiell möglich, das im Kapitel 2.1 – Einführung in die 3D-Animation – angesprochene Beispiel mit der Bewegung der Arme abzubilden. Wenngleich die interne Repräsentation in Verbindung mit Bones komplexer ist. Des Weiteren sind dem Mesh Materialeigenschaften zugewiesen, die an dieser Stelle aber nicht weiter besprochen werden. Im Kapitel 4.2.7. wird auf die Repräsentation von Animationen noch genauer eingegangen. Dieses Kapitel dient eher dazu einen Überblick zu schaffen. DirectX bietet eine große Anzahl an standardmäßig vorhandenen Templates an. Einen Ausschnitt dazu ist im Anhang 8.2 aufgeführt. Neben diesen Templates können aber auch eigene Definiert werden. Hierzu enthält das DirectX-SDK ein Programm zur Generierung der GUIDs. Wobei solch eine Erweiterung später noch vorgestellt wird. So viel zu diesem Format zur Abbildung von 3D-Objekten und Animationen, im nächsten Kapitel wird ein weiteres Format beschrieben, da für OpenGL kein Loader für das X-Format existiert. 2.3.2. Das 3DS-Dateiformat (Andreas Gärtner) Als Dateiformat der Objekte im OpenGL Teil dieser Arbeit findet das 3DS Format Verwendung. Es ist ein weit verbreitetes Format und man findet dementsprechend auch ausreichend viele Tutorials und Loader dafür. Das 3DS Format ist ein binäres Dateiformat zur Speicherung der Daten einer 3D Szene. Die komplette 3DS Datei wird aus Chunks aufgebaut. Wie schon zuvor erwähnt, kann man sich einen Chunk als einen Datenblock vorstellen, in dem die Informationen zu Teilaspekten einer 3D Szene gespeichert werden (siehe Glossar). Im 3DS Format gibt es beispielsweise einen Block mit dem Namen „Keyframer Chunk“. Innerhalb dieses Blocks werden, wie der Name schon vermuten lässt, sämtliche Informationen zu den deklarierten Keyframes innerhalb der 3D Szene beschrieben. Daneben gibt es noch zahlreiche andere Chunks, zum Beispiel für die Koordinatenwerte der Vertices, den Namen jedes Objekts, die Liste aller Polygone etc. Die wichtigsten Chunks, die im Rahmen dieser Arbeit zum Darstellen des 3D Objekts in der OpenGL Anwendung relevant sind, werden im Praxisteil dieser Ausarbeitung erläutert. Zuvor jedoch eine Anmerkung zur Struktur der Chunks. Diese ist nicht linear, sondern hierarchisch aufgebaut. Es gibt Vater Kind Beziehungen, so das einige Chunks von anderen einer höheren Ebene abhängig sind. Will man mit einem Parser die Daten eines Kind Chunks auslesen, so müssen zunächst erst die Daten von sämtlichen übergeordneten Chunks ermittelt werden. Die folgende Abbildung illustriert die Chunk Hierarchie einiger wichtiger Elemente: Fehler: Referenz nicht gefunden 2-3 Hierarchie einiger 3ds chunk Elemente, Quelle (4) Will man also beispielsweise alle Vertices eines Objekts auslesen, so erfolgt dies über das „Vertices List“ Chunk. Um jedoch dorthin zu gelangen, müssen zuvor erst die Blöcke „Main Chunk“, „3D Editor Chunk“, „Object Block“, sowie „Triangular Mesh“ in dieser Reihenfolge gelesen werden. Enthält ein Chunk keine relevanten Informationen, und ist nur notwendig, um an ein Kindelement zu S e i t e | VIII Charakter-Animation Eine Gegenüberstellung unter DirectX und OpenGL WS 08/09 gelangen, so kann anhand der ermittelten Länge des Chunks der Dateizeiger entsprechend weiterbewegt werden. (siehe Praxisteil) Sämtliche Parameter werden im Hexadezimalformat abgespeichert. Jeder Datenblock hat zunächst einen Identifier. In diesem 2 Byte langen Datenfeld wird jeder Block durch den entsprechenden Hexadezimalwert eindeutig identifiziert. Benötigt man also nicht alle Chunks für seine Anwendung, so kann man anhand des Identifiers unter Zuhilfenahme der entsprechenden 3DS Dokumentation (es existieren jedoch nur inoffizielle Dokumentationen, eine solche befindet sich auf dem Datenträger) recht schnell sehen, ob der aktuelle Datenblock gelesen werden soll oder nicht. Als nächstes folgt das 4 Byte große Feld mit der Angabe der Chunk Länge. Dabei ergibt sich die Länge eines Datenblocks nicht nur aus der Anzahl der Bytes, die zur Speicherung seiner Daten benötigt werden, sondern auch aus der Länge aller abhängigen Kindblöcke. Diese beiden Größenangaben werden aufsummiert, um die Gesamtlänge zu bilden. Im nächsten Feld befinden sich dann die eigentlichen Daten des jeweiligen Chunks. Da diese von der Art des Chunks abhängig sind, gibt es hier keine feste Anzahl von Bytes zur Speicherung, sondern die Länge des Datenfeldes ist variabel. Manche Chunks, wie der Main Chunk beispielsweise, besitzen auch überhaupt keine Daten. Der Grund hierfür ist, dass die Daten in den entsprechenden Kindblöcken hinterlegt sind. Im letzten Feld werden schließlich sämtliche Kinder des jeweiligen Chunks aufgelistet, sofern vorhanden. Die nachfolgende Tabelle illustriert dies noch einmal: Offset 0 2 6 6+n Länge 2 4 n m Beschreibung Chunk identifier Chunk Länge Chunk Daten Kind Chunks 2-4 Felder eines Chunks, Quelle: (4) (4) (5) 2.4. Vergleich zwischen Animationen mit der CPU und der GPU In diesem Kapitel geht es darum die Unterschiede herauszuarbeiten zwischen der CPU-basierten Animation und der GPU-basierten (Shader) Animation von 3D-Objekten. Am Anfang der 3D-Grafik blieb den Programmieren nichts anderes übrig, als die Animationen von der CPU berechnen zu lassen. Als jedoch die ersten programmierbaren Grafikkarten erschienen, hat sich dieser Zustand geändert. In den letzten Jahren haben sich die sogenannten Shader stark verbreitet und es sind Hochsprachen wie HLSL und GLSL entstanden. Ein Vorteil den die Berechnung mit Hilfe der Grafikkarte bringt ist der enorme Performancezuwachs. Der Nachteil ist jedoch, dass die Grafikkarte der Endanwender die verwendeten Shader unterstützen muss. Dies wird vor allem daran deutlich, dann schon frühzeitig nach dem Erscheinen des Shader Models 3.0 viele Spiele es auch gefordert haben. Hatte man also eine ältere, dennoch aber leistungsstarke Grafikkarte mit Shader Model 2.0 Unterstützung, so kann man diese Spiele nicht spielen. Der deutliche Vorteil bei der Ausführung der Berechnungen auf dem Grafikprozessor liegt in dessen Parallelität. Hierdurch werden sehr viele Rechenoperationen gleichzeitig ausgeführt, die bei einer normalen CPU sequentiell abgearbeitet werden müssten. Zusammenfassend lässt sich sagen, dass die Berechnungen der Animationen mittels der CPU zwar nicht so Performant sind dafür jedoch prinzipiell auf jedem Computer ausgeführt werden können. Doch dieser Vorteil ist in Zeitkritischen Bereichen nicht von all zu großer Bedeutung, so dass bei Aufwändigeren Animationen die GPU bevorzugt werden sollte. Hier ist dann aber zu beachten, dass möglichst viele Grafikkarten unterstützt werden. Als Stichwort sei an dieser Stelle auf Effektdateien und verschiedene Techniken zur Realisierung der Effekte verwiesen oder auf eine separate CPUMethode, falls die Grafikkarte das gewünschte Feature nicht unterstützt. S e i t e | IX Charakter-Animation Eine Gegenüberstellung unter DirectX und OpenGL WS 08/09 2.5. 3D-Bibliotheken und ihre Shader-Sprachen Wie bereits erwähnt, wird in dieser Arbeit die Charakter-Animation mit zwei verschiedenen Grafikbibliotheken behandelt. In diesem Kapitel werden eben diese Bibliotheken und die dazugehörigen Shader-Sprachen vorgestellt. Dies ist notwendig, da ein grundlegendes Verständnis vorhanden sein muss, um die weiteren Kapitel zur Konzeption und Implementierung der Anwendung zu verstehen. 2.5.1. Shader im Allgemeinen Das Wort Shader ist leicht mit Shading und somit mit Schattieren zu verwechseln. Dies trifft aber nicht die eigentliche Bedeutung. In OpenGL wird anstelle von Shader oft das Wort Programm gebraucht, was die Sache besser beschreibt. Demnach ist ein Shader also ein Programm. In den meisten Fällen ein relativ kleines, das einmal pro Vertex oder einmal pro Pixel aufgerufen wird. Man unterscheidet demnach die Shader in Vertex- und Pixel-Shader(DirectX), auch FragmentShader(OpenGL) genannt. Vertex-Shader können auf jede Komponente eines Vertex zugreifen, dass bedeutet man hat die volle Kontrolle über dessen Parameter. Analog hierzu kann man sich PixelShader vorstellen. Der Unterschied ist nur, dass diese Programme einmal pro Pixel aufgerufen werden und eben jeweils nur auf diesen Pixel Zugriff haben. Das besondere an Shader-Programmen ist, dass sie auf dem Grafikprozessor ausgeführt werden, was einen großen Geschwindigkeitsvorteil mit sich bringt. Am Anfang der Shader-Entwicklung wurden diese Programme in speziellen ShaderAssembler-Sprachen geschrieben, doch mittlerweile wurden hierfür Hochsprachen und Entwicklungsumgebungen veröffentlicht, die von der Architektur der Grafikkarte so abstrahieren, wie beispielsweise Java von der CPU. Ein weiterer großer Vorteil neben der Geschwindigkeit ist die große Flexibilität. (6) Nach dieser Begriffsklärung wird in den nächsten Kapiteln noch genauer auf Shader und die dazugehörigen Hochsprachen eingegangen. 2.5.2. DirectX DirectX ist eine Sammlung von verschiedenen COM-basierten Bibliotheken, aus dem Hause Microsoft, die hauptsächlich in der Spieleentwicklung aber auch ganz allgemein in der Entwicklung von Multimediasoftware verwendet werden. Im Gegensatz zu OpenGL ist DirectX nicht nur für die Grafik zuständig. Im Folgenden sind die enthaltenen Komponenten aufgelistet und mit einer kurzen Beschreibung versehen. DirectX Graphics • DirectX Graphics ermöglicht den direkten Zugriff auf die Grafikkarte und stellt eine API für 2D und 3D Grafiken zur Verfügung. DirectX Audio • DirectX Audio ist für den Sound verantwortlich. Hierzu zählen sowohl Musik als auch Audioeffekte und 3D-Sound sowie eine Mikrofon-Unterstützung. DirectInput • DirectInput ist für Eingabegeräte zuständig. Hierzu zählen alle Arten wie Mäuse, Tastaturen, Lenkräder oder auch Game-Pads und Joy-Sticks mit Force-Feedback-Technik. DirectPlay Seite |X Charakter-Animation Eine Gegenüberstellung unter DirectX und OpenGL WS 08/09 • DirectPlay vereinfacht die Nutzung von Netzwerken und Kabelverbindungen. Hierdurch lassen sich recht komfortabel Multiplayeranwendungen entwickeln. DirectShow • DirectShow ermöglicht die Wiedergabe aller Typen von Multimedia-Daten. DirectSetup • DirectSetup dient dazu ein DirectX-Setup in eine Anwendung zu integrieren. Dieses kann der Benutzer installieren, sollte seine Version nicht aktuell sein. In dieser Ausarbeitung geht es hauptsächlich um DirectX Graphics, auch als Direct3D bekannt. In der DirectX Version 8 wurden jedoch die Komponenten DirectDraw und Direct3D zu DirectX Graphics vereint. 2.5.2.1. Entwicklungsgeschichte von DirectX In den 1990er Jahren wurden Spiele entweder für Konsolen oder für das Betriebssystem DOS entwickelt. Auch als grafische Oberflächen mit Windows 3.0 oder Windows 95 sich etablierten, blieb dieser Zustand unverändert. Das lag daran, dass es noch kein DirectX für Windows gab und die Entwickler keinen direkten Zugriff auf die Hardware hatten. Diesen direkten Zugriff bot DOS jedoch. Der Nachteil liegt aber darin, dass man entweder nur Funktionen verwendet, die jede Hardware ausführen kann oder separat für einzelne Hardware entwickelt, was den Aufwand extrem steigert. Aus diesem Grund veröffentlichte Microsoft zuerst WinG und Wavemix. WinG war eine Sammlung von Funktionen, die in Windows performanter war, als das normale GDI. Dieser Ansatz fand allerdings kaum Beachtung, wodurch später das Game SDK veröffentlich wurde, welches später in DirectX SDK umbenannt wurde. DirectX wurde im Jahr 1995 in der Version 1.0 veröffentlicht und hat seitdem eine rasante Entwicklung durchgemacht. (7) Momentan ist die Version 10.1 aktuell, Microsoft hat jedoch für das Jahr 2010 DirectX 11 angekündigt. Die größte Entwicklung hat nach der Veröffentlichung von DirectX 8 die Komponente DirectX Graphics durchgemacht. Beispielsweise heißen bei DirectX 9.0c die Schnittstellen der Komponente DirectInput noch genauso wie in der Version 8. Dies soll verdeutlichen, dass sich an dieser Stelle nichts oder nur sehr geringfügig etwas geändert wurde. Auch wenn DirectX 10.1 im Moment aktuell ist, wird in diesem Projekt DirectX 9.0c verwendet. Das ist darin begründet, da die verwendete Hardware keine neuere Version unterstützt und nicht auf die Referenzimplementierung (in Software) zurückgegriffen werden soll. 2.5.2.2. DirectX-Graphics Im Teilbereich DirectX dieser Ausarbeitung liegt der Schwerpunkt auf der DirectX-Komponente DirectX Graphics. Sie ermöglicht es sowohl 2D als auch 3D Grafiken zu zeichnen und stellt die Schnittstelle zwischen Programm und Grafikkarte dar. Des Weiteren enthält sie eine sehr hilfreiche Bibliothek namens D3DX. Sie bildet eine High-Level-API und beinhaltet nützliche Funktionen, die die Arbeit mit der Low-Level-API Direct3D vereinfachen. Dazu zählen vor allem Funktionen, die mathematische Aufgaben übernehmen oder auch Datenstrukturen. Wie bereits einführend erwähnt, baut DirectX auf dem COM (Component Object Model) auf. Dies erlaubt eine Vereinfachung der Organisation von großen Funktionsmengen. DirectX besitzt demnach eine große Anzahl von Funktionen, Klassen, Methoden, Schnittstellen und Strukturen. Das COM erlaubt es eine Schnittstelle parallel von mehreren Programmteilen oder Programmen zu nutzen. Aus diesem Grund enthält jede COM-Schnittstelle einen Referenzzähler. Dieser ist notwendig, da ein Programm die Schnittstelle nicht löschen darf, wenn andere Programme sie noch verwenden. Um diese und andere Verwaltungsmethoden nicht für jede Schnittstelle implementieren zu müssen, existiert die Schnittstelle IUnknown, von der alle anderen Schnittstellen abgeleitet sind. S e i t e | XI Charakter-Animation Eine Gegenüberstellung unter DirectX und OpenGL Methode WS 08/09 Zweck AddRef Release Erhöhen des Referenzzählers um 1. Freigeben der Schnittstelle, bzw. Verringern des Referenzzählers um 1 und Abbau der Schnittstelle beim Wert 0. QueryInterface Anfordern eines anderen Schnittstellentype. 2-5 Beschreibung der Schnittstelle IUnknown. Quelle: (6) Wie sie später sehen werden, wird eine DirectX-Anwendung in eine normale Windows-Anwendung integriert. Solche Windows-Anwendungen nutzen aber niemals direkt den Grafik-Adapter, sondern das GDI (Graphic Device Interface). DirectX-Anwendungen nutzen hingegen DirectX Graphics und somit dessen Low-Level-API Direct3D und den HAL (Hardware Abstraction Layer). Hierdurch werden so viele Befehle wie möglich direkt von der Grafikkarte ausgeführt. Ist der HAL nicht verfügbar, so kann das Reference Device verwendet werden. Dies ist allerdings in Software implementiert und daher vergleichsweise sehr langsam. Dies war eine kurze Einführung zu DirectX-Graphics – mehr aber auch nicht. Wie man damit praktisch arbeitet, lässt sich in den passenden Abschnitten des Kapitels 4 nachlesen. 2.5.2.3. HLSL HLSL steht für High Level Shader Language und ist die Programmiersprache für Shader im Bereich DirectX. Sie wurde mit DirectX 9 eingeführt, was die Shader-Programmierung stark vereinfachte. Des Weiteren wurde mit DirectX 9 das Shader Model 3 eingeführt. 2.5.2.3.1. Shader in der DirectX-Renderpipeline Um in die Shader-Programmierung einzusteigen, benötigt man zuerst ein Verständnis darüber, wie und an welcher Stelle diese zum Tragen kommen. Aus diesem Grund betrachten wir zuerst die Renderpipeline. Fixed Function Pipeline Grafikprimitive Clipping Culling Vorverarbeitung Vertex- und Pixelshader Texturierung Transformation und Beleuchtung Endverarbeitung Bildschirmpixel Pixelshader Vertexshader 2-6 Skizze der DirectX Renderpipeline. Quelle: (11) In der Abbildung 2.4 ist zu erkennen, dass man wahlweise Vertexshader und Pixelshader einsetzen kann. Es gilt jedoch zu beachten, dass wenn man Vertexshader einsetzt, man sich um die Transformation und Beleuchtung innerhalb des Vertexshaders kümmern muss. Ein Mischen ist nicht möglich. Analog dazu verhält es sich bei den Pixelshadern. Die genannten Einschränkungen gelten jedoch immer nur für ein Modell, beispielsweise einen Mesh oder eine Dreiecksliste. Außerdem kann nach jedem Frame wieder zur jeweils anderen Variante gewechselt werden. 2.5.2.3.2. Vertexshader In diesem Kapitel wird der Aufbau eines Vertexshaders betrachtet. Vereinfacht lässt sich ein Vertexshader durch die Abbildung 2.5 darstellen. Konstanten Input Vertexshader Register Quelle: (11) 2-7 Funktionsweise eines Vertexshader. Output S e i t e | XII Charakter-Animation Eine Gegenüberstellung unter DirectX und OpenGL WS 08/09 Der Vertexshader bekommt Input Streams von Vertices, die nach einer bestimmten festgelegten Deklaration aufgebaut sind. Darin können Informationen über Position, Normalen-Vektoren, diffuse oder spekulare Farbe und weitere Informationen enthalten sein. Als Konstanten bezeichnet man die Variablen, auf die der Shader nur lesend zugreifen kann. Sie werden vom Anwendungsprogramm aus gesetzt. Im Register legt der Shader Zwischenergebnisse ab. Dieser Bereich wird jedoch von HLSL verwaltet, demnach muss sich der Programmierer hierum nicht kümmern. Letztendlich ergeben die Berechnungen des Vertexshaders einen Output. Hierbei kann es sich beispielsweise um geänderte Geometriedaten oder Texturkoordinaten handeln. 2.5.2.3.3. Pixelshader Ein Pixelshader ist grundsätzlich genauso wie ein Vertexshader aufgebaut. Jedoch ist der Kontext ein anderer, da es sich in diesem Fall nicht mehr um Geometriedaten sondern um Pixel handelt. In der eigentlichen Thematik dieser Ausarbeitung spielen die Pixel-Shader eine untergeordnete Rolle, daher werden sie nicht detaillierter besprochen. 2.5.3. OpenGL (Andreas Gärtner) OpenGL steht für Open Graphics Library und ist im Gegensatz zu DirectX eine plattformunabhängige Programmierschnittstelle zur Entwicklung von 3D-Grafiken. OpenGL beschränkt sich jedoch im Gegensatz zu DirectX auf Funktionen zum Darstellen von Objekten. Es existieren also keine Komponenten, die für Audio oder Eingabegeräte zuständig sind. Die folgende Tabelle verdeutlicht die Entwicklung der API. Version 1.0 1.1 1.2 Zeitraum Juli 1992 1997 März 1998 1.2.1 1.3 1.4 Oktober 1998 August 2001 Juli 2002 1.5 2.0 Juli 2003 September 2004 2.1 August 2006 3.0 August 2008 2-8 OpenGL Historie Quelle (8) Technische Neuerungen Erste Veröffentlichung Vertex Arrays, Texture Objects, Polygon Offset 3D-Texturen, BGRA-, Packed-Pixelformat, Level-Of-DetailTexturen Einführung der ARB-Erweiterungen, ARB-Multitexture Komprimierte Texturen, Cube-Maps, Multitexturing Tiefentexturen, Automatische MipMap-Erzeugung, Nebelkoordinaten Pufferobjekte, Occlusion Queries Einführung der GL Shading Language (GLSL / GLslang) Multiple Render Targets, variable Texturgrößen Pixel Buffer Objects, OpenGL Shading Language 1.2, sRGBTexturen OpenGL Shading Language 1.3, Codebasis aufgeräumt, die Architektur nähert sich DirectX an, erstmals ein weitestgehender Verzicht auf Abwärtskompatibilität Der OpenGL-Standard wird seit 1992 von ARB verwaltet. ARB steht für Architecture Review Board. Dies ist ein Konsortium zu dem Firmen wie NVidia, ATI, IBM und Intel zählen. Im Jahr 2003 hat Microsoft, einer der Gründer, das Team verlassen. Die Weiterentwicklung von OpenGL wird von herstellerspezifischen Erweiterungen geprägt. Das ARB entwirft aus diesen Erweiterungen herstellerunabhängige ARB-Erweiterungen und fügt diese zum Standard von OpenGL hinzu. Dieses Prinzip ermöglicht das Ziel der Plattformunabhängigkeit weiter zu verfolgen. Des Weiteren ist OpenGL von vielen ergänzenden Bibliotheken umgeben. So zum Beispiel die GLUBibliothek zum darstellen komplexer Objekte oder der GLUT-Bibliothek zur betriebssystemabhängigen Fensterverwaltung. S e i t e | XIII Charakter-Animation Eine Gegenüberstellung unter DirectX und OpenGL WS 08/09 2.5.3.1. GLSL GLSL oder auch GLslang ist die Shader-Programmiersprache im Bereich OpenGL. Im Kapitel HLSL wurden Vertex- und Pixelshader behandelt. Bei OpenGL spricht man im Gegensatz zu DirectX nicht von Pixel- sondern von Fragmentshadern. 2.5.3.1.1. Shader in der OpenGL-Renderpipeline Die Abbildung 2.7 zeigt vereinfacht die Renderpipeline von OpenGL. Die Vertex- beziehungsweise Fragment-Shader können dabei den statischen Teil der Renderpipeline ersetzen. Zunächst wollen wir die klassische Renderingpipeline betrachten und einige wichtige Elemente darin erläutern: 2-9 OpenGL Renderingpipeline, Quelle: (9) Mittels Darstellungslisten, auch Displaylisten genannt, können bestimmte Aufgaben, wie das Zeichnen mehrerer Vertex Punkte, zwischengespeichert und erst zu einem späteren Zeitpunkt alle auf einmal ausgeführt werden. Dies nennt man auch retained mode. Dadurch wird die Performance der Anwendung deutlich erhöht, da nur einmal der entsprechende glCallList() Aufruf erfolgen muss und das Netz nicht durch das unmittelbare Ausführen jeder einzelnen Anweisung belastet wird (auch immediate mode genannt). Die Evaluatoren kommen vor allem im Zusammenhang mit Freiformkurven oder –flächen vor. Kurven werden zum Beispiel meistens zuerst auf normale Vertexdaten approximiert und erst danach gezeichnet. Die Evaluatoren dienen hierbei als Berechnungsmethode, um die Umwandlung der Kurvengeometriedaten auf die Vertexdaten vorzunehmen. Die Transformations Primitive kümmert sich um Berechnungen wie die Verschiebung, Rotation oder Skalierung eines Objekts. Sie kümmert sich zudem um die Ansichtstransformation. Darunter vertseht man etwa die Positionierung der Kamera innerhalb einer Szene mit der gluLookAt() Funktion. Die Beleuchtungs Primitive kümmert sich um die Berechnung von Licht innerhalb einer Szene. Die Clipping Primitive sorgt dafür, dass Vertexdaten, die sich außerhalb eines definierten Bereichs befinden, nicht dargestellt werden. Im Rasterisierungsvorgang werden aus den zuvor angegebenen, geometrischen Informationen der Grafikprimitive die konkreten Pixel im Ausgabefenster gezeichnet. Dadurch entstehen die so genannten Fragmente. Ein Fragment ist eine quadratische Fläche, die von einem Pixel belegt wird. In OpenGL existiert eine Vielzahl von unterschiedlichen Operationen, die auf ein Fragment angewendet werden können. Ein Beispiel einer Fragmentoperation ist der Alpha Test. Hierbei wird S e i t e | XIV Charakter-Animation Eine Gegenüberstellung unter DirectX und OpenGL WS 08/09 der Alpha Wert eines Fragments mit einem Referenzwert auf Baiss einer Vergleichsoperation (z.B. GL_GREATER) verglichen. Voraussetzung hierfür ist, dass der RGBA Modus verwendet wird. Erfüllt ein Fragment den Alphatest, so wird es zum Framebuffer durchgelassen, sonst verworfen. Wo setzen nun die Shader an? Der Vertexshader kann an Stelle der normalen Transformations Primitive eingesetzt werden. Dadurch müssen die entsprechenden Operationen, wie etwa die Beleuchtung, selber implementiert werden. Einige Anweisungen innerhalb der statischen Pipeline, beispielsweise „glEnableLighting“ haben keine Auswirkungen mehr, da der statische Teil der Pipeline ersetzt wurde. Andere Operationen hingegen sind nach wie vor nutzbar. Die Verwendung von „glVertex()“ schickt die Vertexdaten aber nun direkt an den Vertexshader und die Variable „gl_Vertex“. Für die Shaderprogrammierung existieren zahlreiche solcher fest definierten Variablen (so genannte built in Variablen). Der Fragment Shader wiederum kann die normalen Fragmentoperationen ersetzen. Er entspricht dem Pixel Shader unter DirectX. Der Programmierer kann dadurch etwa einen eigenen Alpha Test oder Blending Algorithmus entwickeln. Eine genauere Erläuterung der Shaderprogrammierung erfolgt im Praxisteil. (9) (10) 2.6. Zusammenfassung Nachdem in diesem Kapitel doch recht viele unterschiedliche Komponenten Grafikprogrammierung angesprochen wurden ist eine Zusammenfassung angebracht. der Am Anfang des Kapitels wurde eine kurze Einführung in die 3D-Animation gegeben. Der Schwerpunkt lag hier auf der Charakter-Animation mit Hilfe von Skinning und Tweening und den damit verbundenen Key-Frames, die zur Interpolation benötigt werden. Des Weiteren wurde auf den mathematischen Hintergrund für Interpolationen eingegangen und zwei bedeutende Verfahren wurden erwähnt. Je nachdem mit welcher Anwendung man diese Objekte und Animationen erstellt lassen sich die Dateien in verschiedene Formate exportieren. Doch nachdem man diese Dateien erstellt hat, stellt sich die Frage danach, wie man sie in einem DirectX oder OpenGL Programm lädt, anzeigt und auf die Animationen zugreift. Je nach Format liegen die Daten in anderer Form vor. Man benötigt also jeweils einen spezifischen Loader. Hier stehen viele Möglichkeiten offen, entweder man schreibt einen eigenen oder man verwendet einen vorhanden Loader. Um aber einen eigenen Loader zu implementieren sind zwingend Kenntnisse über die Struktur der Formate notwendig. Aus diesem Grund werden die zwei verwendeten Formate einführend angesprochen. Nachdem dieser Abschnitt abgehandelt wurde folgt eine Diskussion über die Vor- und Nachteile der Realisierung der Animationen mit der CPU bzw. der GPU. Hierzu lässt sich zusammenfassend sagen, dass die Animation durch die GPU einen deutlichen Performancezuwachs erzielt. Jedoch muss die jeweilige GPU des Endanwenders die verwendete Technik auch unterstützen. Das letzte und größte Kapitel befasst sich mit den 3D-Grafikbibliotheken DirectX und OpenGL inklusive der dazugehörigen Shader-Programmiersprachen. Bisher wurden allerdings nur theoretische Aspekte, wie die Einbettung der Shader in die Renderpipeline oder die Shader-Arten, angesprochen. Wie Anwendungen davon Gebrauch machen folgt in den nächsten zwei Kapiteln. Ob man für die Realisierung eines Projektes nun besser DirectX oder OpenGL nimmt liegt im Auge des Betrachters. Grundsätzlich kann man mit beiden großartige Animationen und Computerspiele erstellen. S e i t e | XV Charakter-Animation Eine Gegenüberstellung unter DirectX und OpenGL WS 08/09 3. Konzeption Das dritte Kapitel beschreibt nach der Einführung und den grundlegenden Sachverhalten die Struktur der Anwendung sowie die grundsätzliche Herangehensweise. 3.1. Die Bedeutung der Zeit Im zweiten Kapitel wurde das Thema Interpolation und Key-Frames besprochen. Wie später zu sehen ist, wird in jedem Durchlauf des Programms die Animation fortgeführt und die Szene gerendert. Hierbei kann es zu Problemen kommen, wenn man die Zeit nicht berücksichtigt. Wenn man versucht auf einem neuen Rechner ein altes Spiel zu spielen, dann steht man oft vor einem Problem, denn das Spiel wird viel zu schnell laufen. Aus diesem Grund ist es wichtig die vergangene Zeit, seit dem die Anwendung gestartet wurde, zu messen. Des Weiteren ist es von großer Bedeutung zu messen, wie viel Zeit benötigt wird um die Szene einmal zu bewegen und zu rendern. Denn das Ergebnis dieser Berechnungen fließt in die Animationen ein. Je mehr Zeit dabei vergeht, desto „schneller“ muss die Animation also fortschreiten – und umgekehrt. Daraus ergibt sich eine Differenz zwischen zwei Frames von: Legt man nun eine Zeit fest, die normalerweise insgesamt für eine Animation vergehen sollte, so kann man daraus den Faktor für den aktuellen Stand der Animationen berechnen. Daraus ergibt sich: Woraus folgt, dass am Anfang der Animation (zwischen zwei Key-Frames) der Faktor gleich 0 und am Ende gleich 1 ist. Hierdurch ist gewährleistet, dass unabhängig von der Rechengeschwindigkeit des einzelnen Systems die Animation die gleiche Zeitspanne benötigt. 3.2. Konzeption der DirectX-Anwendungen Dieses Kapitel beschreibt die benötigten Belange im Bereich der DirectX-Entwicklung. Dazu zählen unter anderem die Einrichtung der Entwicklungsumgebung und die Struktur der Anwendungen, sowie die Konzeption der Integration der Shader in den Verarbeitungsablauf aber auch allgemeine Erklärungen zu den verwendeten Techniken. 3.2.1. Einrichtung der Arbeitsumgebung In diesem Projekt wird als Arbeitsumgebung für die DirectX-Anwendungen MS Visual Studio 2008 verwendet. Diese IDE ist über MSDNAA kostenfrei verfügbar. Des Weiteren wird das DirectX-SDK benötigt, welches auf der Homepage von Microsoft als Download zur Verfügung steht. In diesem Projekt wird die DirectX-SDK Version vom März 2008 verwendet. Diese Software sollte man installieren, um die auf der CD im Anhang befindlichen Projekte zu bearbeiten. S e i t e | XVI Charakter-Animation Eine Gegenüberstellung unter DirectX und OpenGL WS 08/09 Um eine Anwendung zu kompilieren und zu Linken müssen bestimmte Pfade gesetzt werden, damit MS Visual Studio 2008 auf die benötigten Elemente zugreifen kann. Andernfalls scheitert das Kompilieren beziehungsweise das Linken. Hierfür startet man MS Visual Studio 2008 und klickt auf Extras, Optionen. Dort navigiert man zu den Einstellungen für C++ Verzeichnisse (Projekte und Projektmappen – C++ Verzeichnisse). Wenn man dieses Menü öffnet, kann man die entsprechenden Ordner für die Bibliotheksdateien und Include-Dateien angeben. Hier fügt man die Ordner des DirectX SDKs ein. Im nächsten Schritt erstellt man ein neues Projekt. Dabei Achten Sie darauf, dass Sie ein Win32Projekt erstellen. Im Anschluss daran öffnet man das Projekt-Menü und klickt dort auf Eigenschaften. In dem daraufhin erscheinenden Fenster navigiert man sich anschließend zu den passenden LinkerEinstellungen durch. (Konfigurationseigenschaften->Linker->Eingabe). Öffnet man dieses Menü, kann man zusätzliche Abhängigkeiten festlegen. In diesem Projekt wurden immer die folgenden Abhängigkeiten dort eingetragen, an den man sich orientieren kann. dinput8.lib, dxguid.lib, dxerr9.lib, dsound.lib, d3dx9.lib, winmm.lib, d3d9.lib, kernel32.lib, user32.lib, comdlg32.lib, gdi32.lib, shell32.lib 3-10 Ein Beispiel für zusätzliche Linkeroptionen für DirectX-Anwendungen. Wenn diese Schritte erfolgreich durchgeführt wurden, lassen sich eigene Anwendungen erstellen, die DirectX-Komponenten verwenden. 3.2.2. DirectX Graphics und D3DX Wie im zweiten Kapitel beschrieben handelt es sich bei DirectX Graphics um die DirectXKomponente, die für 2D und 3D Grafiken zuständig ist. In diesem Projekt wird überwiegend die Bibliothek D3DX verwendet. Hierbei handelt es sich um eine High-Level-API für Direct3D. Es existieren daher viele nützliche Funktionen und Datenstrukturen für mathematische Konstrukte oder auch 3D-Modelle. Eine dieser Datenstrukturen ist das Interface ID3DXMESH. Das ist eine Datenstruktur, die einen Mesh – ein 3D Objekt – enthält. Ein solcher Mesh besteht prinzipiell aus vier Teilen, einem Vertexbuffer, einem Indexbuffer, einem Attributbuffer und einer Attributtabelle. Um den Aufbau dieser Struktur zu verstehen ist ein grundlegendes Wissen über Index- und Vertexbuffer notwendig. Doch wozu werden diese zwei Buffer verwendet? Angenommen es wird ein normaler Würfel in eine 3D-Szene geladen, dann besteht dieser aus acht Eckpunkten und vier Seitenflächen. Prinzipiell muss für jede Seite des Würfels jeder Eckpunkt gespeichert werden. Würde man an dieser Stelle für jede Seite die Eckpunkte (Vertices) separat speichern, dann führt das zu einer hohen Redundanz und Speicherbelastung. Aus diesem Grund sind die einzelnen Vertices in einem Vertexbuffer abgelegt auf die über einen Indexbuffer zugegriffen wird. Für jeden Eckpunkt existiert daher ein Eintrag im Indexbuffer, mit dessen Hilfe sehr performant die Vertexdaten ausgelesen werden können. Der Attributbuffer und die Attributtabelle teilen den Mesh in verschiedene Untermengen. Eine solche Menge (Subset) zeichnet sich durch eine einheitliche Textur beziehungsweise ein einheitliches Material aus. Im Attributbuffer befinden sich daher für jede Oberfläche Informationen über das Material, mit dem diese Oberfläche zu belegen ist. (11) Weitere Datenstrukturen die bei der Animation von Bedeutung sind, sind ID3DXSKININFO, ID3DXMESHCONTAINER, ID3DXAnimationController, D3DXMATRIX oder auch D3DXVECTOR3. Auf Basis dieser Elemente wird im nächsten Kapitel die Herangehensweise beschrieben, wie man eine Animation in einer Anwendung umsetzt. 3.2.2.1. Konzept zur Implementierung der Animationstechnik Skinning S e i t e | XVII Charakter-Animation Eine Gegenüberstellung unter DirectX und OpenGL WS 08/09 Die Grundlage von Skinning bilden die Vertices und Bones der Objekte. Zur Animation werden dabei die Eigenschaften der verschiedenen Bones zu den jeweiligen Key-Frames ausgelesen. Des Weiteren werden Informationen darüber benötigt welche Vertices, durch welche Bones, wie stark, beeinflusst werden. Um das Objekt zu animieren muss im folgenden Schritt in Abhängigkeit der Zeit zwischen den jeweiligen Key-Frames interpoliert werden. Diese Beschreibung ist jedoch eher theoretisch und sehr wage. Aus diesem Grund beschreibt dieses Kapitel die Vorgehensweise etwas genauer. Fehler: Referenz nicht gefunden 3-11 Beispiel für eine Bone-Hierarchie. Die Abbildung 3.2 zeigt eine Hierarchie von Bones. Dabei ist dem Oberarm der Unterarm untergeordnet und diesem wiederum die Hand und so weiter. Auch wenn der Oberarm in dieser Betrachtung in der Hierarchie am höchsten steht, so muss bei der Transformation auch dessen Lage in der Szene berücksichtigt werden, dafür dient die World-Matrix. Unter der Annahme, dass für jeden Bone die lokalen Eigenschaften im Verhältnis zu seinem Vater gespeichert sind, muss zu dessen Transformation die gesamte Hierarchie herabgestiegen werden. Es gilt demnach die folgende Aussage: Wenn F1 die Transformationsmatrix für T1, T0 ist und F2 für T2, T1 und so weiter, wobei F0 die Transformationsmatrix für T0, World darstellt, dann lässt sich wie folgt die Szene in das WorldKoordinatensystem transformieren. Eine zweite und sogleich effizientere Methode ist es aber einen Top-Down-Ansatz zu verfolgen. Hierdurch wird die Redundanz stark verringert und somit Zeit eingespart. Erst am Ende, wenn alle Einzelteile transformiert wurden, wird die World-Matrix angewendet. Nach dieser Betrachtung stellt sich die Frage danach, wie man die Animation umsetzen kann. Zuvor wurde die Datenstruktur eines Meshs beschrieben, in der bekanntlich die Vertices eines 3D-Modells abgelegt sind. Des Weiteren wurde im zweiten Kapitel das Thema Interpolation angesprochen und auch Quaternionen wurden erwähnt. Das alles ist an dieser Stelle nun von Bedeutung. Je nachdem, welche Einstellungen man beim Export des Modells aus der Modellierungssoftware vornimmt, sind verschiedene Daten in den Key-Frames gespeichert. Matrizen sind an dieser Stelle zur Interpolation leider nicht geeignet, da man zwischen ihnen nicht direkt interpolieren kann (12). Normalerweise verwendet man die Werte für Rotation, Skalierung und Translation unabhängig voneinander und kombiniert diese zum Schluss wieder zu einer Matrix. Sollte jedoch eine kombinierte Matrix vorliegen, so kann diese mit der Funktion D3DXMatrixDecompose in die entsprechenden Bestandtele extrahieren. Je nachdem, ob eine besonders weiche Animation gewünscht ist oder nicht, verwendet man zur Interpolation das Verfahren SLERP oder LERP (siehe Kapitel 2.2 – Verfahren zur Interpolation). Wobei für eben diese beiden Verfahren im dreidimensionalen Raum in der D3DXBibliothek entsprechende Funktionen existieren (D3DXVec3Lerp, D3DXQuaternionSlep). Eine Feinheit, die noch zu beachten ist, ist das die Vertices die ein Bone beeinflusst nicht relativ zu dessen Koordinatensystem liegen. Daher müssen die Vertices zuvor mittels eines Offsets transformiert werden. S e i t e | XVIII Charakter-Animation Eine Gegenüberstellung unter DirectX und OpenGL WS 08/09 Um flüssige Übergänge zwischen den Bestandteilen des zu animierenden Objekts herzustellen verwendet man Vertex Blending. Dabei wird die Position der Vertices durch die Position und Gewichtung der Bones beeinflusst, wobei in der Regel nicht mehr als vier Bone-Gewichte je Vertex benötigt werden (13). Daraus ergibt sich die folgende Formel. Analog dazu lassen sich auch die Normalen-Vektoren berechnen. Durch diese tiefgehende Betrachtung sollte man in der Lage sein die spätere Implementierung zu verstehen und gegebenenfalls selbst umzusetzen. 3.2.2.2. Konzept zur Implementierung der Animationstechnik Tweening Im Gegensatz zu Skinning sind bei Tweening keine Bones notwendig. Trotzdem sind sie sehr hilfreich um das Objekt innerhalb einer 3D-Modellierungssoftware zu animieren. Der Unterschied liegt jedoch darin, dass die Berechnung der Animation innerhalb der Anwendung nicht auf Bones basiert, sondern die Positionen der Vertices an sich zwischen den Key-Frames interpoliert werden. Daher gilt es in diesem Kapitel zu erklären, wie solch ein Algorithmus strukturell aufgebaut ist. Grundsätzlich müssen bei Tweening bestimme Bedingungen erfüllt sein. Es müssen in beiden Instanzen, zwischen denen Interpoliert werden soll: die gleiche Anzahl an Vertices vorhanden sein und die Vertices müssen in der gleichen Reihenfolge nummeriert sein. Da hierbei auf Bones und andere komplexe Gebilde verzichtet wird, reicht es aus die Vertices im Laufe der Zeit vom Start- zum Endpunkt zu interpolieren und da die Vertices in Form von dreidimensionalen Vektoren vorliegen, kann man dies durch einfache Multiplikation mit einem Skalar und Additionen durchführen. Am Anfang der Animation ist der Wert von s gleich null und am Ende eins, er wird also in Abhängigkeit der Zeit erhöht. Viel mehr muss man hierbei auch nicht berechnen. Es sei denn, man möchte die Normalen verwenden, dann müssen diese auch berechnet werden. Die Animation jedoch ist durch diese Formel beschrieben. 3.2.3. Animation Blending Bisher wurden Animationen jeweils separat betrachtet, das ist aber oft nicht sinnvoll, da so sehr abgehakte Animationen entstehen können. Beispielsweise wenn ein Charakter anfangs geht und danach sofort rennt. Ein anderes Beispiel für Animation Blending ist das Heben der Hand, dies kann man sich in Computerspielen in vielen Situationen, wie das Zielen mit einer Waffe, vorstellen. Diese Aktion kann der Charakter nun sowohl im Stehen als auch während des Laufens durchführen. Wenn man in solchen Situationen jeweils einzelne Animationen erstellt, wächst die Anzahl derer exponentiell und wird praktisch unbrauchbar. Aus diesem Grund gibt es Animation Blending, womit man mehrere Animationen überlagert und somit die beschriebenen Probleme löst. Auf eine detaillierte Beschreibung dieser Technik wird verzichtet. Dies hat den Grund, dass es einerseits den Umfang dieser Ausarbeitung sprengen würde und andererseits in DirectX Funktionen integriert sind, die diese Arbeit bei Skinning stark vereinfachen, wodurch keine eigenen Berechnungen implementiert werden müssen. S e i t e | XIX Charakter-Animation Eine Gegenüberstellung unter DirectX und OpenGL WS 08/09 3.2.4. DirectX-Shader (HLSL) Im zweiten Kapitel wurde bereits einführen erklärt, was HLSL ist und wie es funktioniert. An dieser Stelle hingegen wird der Aufbau eines HLSL-Shaders erklärt und beschrieben wie ein Shader für Charakter-Animationen eingesetzt werden kann. Aus diesem Grund folgt an dieser Stelle eine Einführung in die Programmiersprache HLSL (11). 3.2.4.1. Einführung in HLSL Die Programmiersprache HLSL ist stark an C angelehnt und gliedert sich in die folgenden Bereiche: Skalare Datentypen • In HLSL gibt es bool, half, int, float und double als skalare Datentypen. Vektoren • Notation eines Vektors: vector <type, size> instancName; • Jedoch wird in der Regel eine verkürzte Schreibweise verwendet. • Notation in kurzer Schreibweise: float4 myVector; Matrizen • Allgemeine Notation: matrix <type, rows, columns>; • Notation in kurzer Schreibweise: matrix3x2 myMatrix; Texturen und Sampler • Sampler sind Datentypen, die dem Zugriff auf Texturen dienen. • Beispiele hierfür sind texture2D myTextur; und sampler2D mySampler;. Annotationen • Annotationen sind Zusatzinformationen, die beispielsweise einer Variablen hinzugefügt werden können. Sie tragen in HLSL jedoch keine Semantik, können aber vom verarbeitenden Programm ausgelesen und verarbeitet werden. Arrays • Arrays können wie in C angelegt und verwendet werden. • float array[19]; array[0] = 1.23; Strukturen • Auch Strukturen können wie in C angelegt werden. • struct int float4 myStruct{ index; vector; } S e i t e | XX Charakter-Animation Eine Gegenüberstellung unter DirectX und OpenGL WS 08/09 Speicherklassen • Ähnlich wie in C können Variablen lokal und global definiert werden. Des Weiteren gibt es die Möglichkeit Schlüsselwörter wie const, extern, static, shared und uniform zu verwenden. Operatoren • In HLSL existieren wie in allen Hoch-Sprachen viele verschiedene Operatoren. In der Hilfe von DirectX ist eine Tabelle mit deren Bedeutung und Abarbeitungsreihenfolge enthalten. Kontrollflusssteuerung • Im Gegensatz zur CPU ist die GPU nicht für prozedurale Programmierung gedacht. Deshalb sollte man wenn möglich darauf verzichten. Nichts desto trotz ist es manchmal notwendig, daher gibt es Fallunterscheidungen mit if und else und auch Schleifen wie die for-Schleife. Die Syntax ist dabei wieder wie in C. Vordefinierte Funktionen • HLSL bietet viele vordefinierte Funktionen. Hierzu zählen unteranderem mathematische Grundfunktionen, wie Sinus und Cosinus. Eigendefinierte Funktionen • Da die GPU nicht für Unterprogrammaufrufe konzipiert wurde, kann sie auch keine echten Unterprogramme ausführen. Es existiert auch kein Stack, auf dem die dafür benötigten Daten abgelegt werden könnten. Jedoch kann man als Programmierer Funktionen wie in C schreiben, diese werden jedoch immer als „Inline“ deklariert und somit im Quelltext an die entsprechenden Stellen direkt eingefügt. Ein- und Ausgabesemantik • Der Shader besitzt in der Regel Datenstrukturen für die Eingabe und die Ausgabe. Um beispielsweise die Eingabedaten mit Informationen aus der Rendering-Pipeline zu füllen, müssen die Elemente der Eingabe-Datenstruktur spezifiziert werden. Hierfür dienen Semantics – Schlüsselwörter. Shader Hauptprogramm • Ein Shader besitzt immer einen Einstiegspunkt, ähnlich einer main-Funktion in C. Nach dieser kurzen und zugleich umfassenden Einführung in HLSL – der Hochsprache zur ShaderProgrammierung für DirectX - folgt im nächsten Schritt die Integration eines Shaders in eine Anwendung. Im speziellen wird die Integration eines Vertexshaders erläutert, da Pixelshader in dieser Ausarbeitung nur eine zweitrangige Rolle spielen. 3.2.4.2. Integration Eines Vertexshaders in eine Anwendung Der erste Schritt um einen Vertexshader in eine Anwendung zu integrieren ist die Definition der benötigten Komponenten innerhalb des Shaders. Dazu zählen globale Variablen, auch als Konstanten bezeichnet, sowie die Eingangs- und Ausgangsdatenstruktur, die somit die Schnittstellen festlegen. Dieses Beispiel definiert zwei Strukturen, die hier zufälligerweise gleich sind. Die Variable pos beziehungswiese posNew deutet an, dass die Position des Vertex in irgendeiner Form geändert wird. Die Variable text wird nicht verändert, sie ist jedoch notwendig, da sonst die Textur nicht in der S e i t e | XXI Charakter-Animation Eine Gegenüberstellung unter DirectX und OpenGL WS 08/09 Ausgabe integriert wäre. Die Textur-Koordinaten werden also nur durch geleitet. Die Angaben nach dem „:“ sind Semantics, dessen Bedeutung anhand des Namens meist selbsterklärend sind. // globale Variablen float4x4 globalVariable; // Eingangsdatenstruktur struct VS_INPUT{ float4 pos : POSITION; float2 text : TEXCORD0; } // Definition einer 4 mal 4 Matrix. // Position des Vertex im Raum. // Texturkoordinate der Texturebene 0. // Ausgangsdatenstruktur struct VS_OUTPUT{ float4 posNew : POSITION; // Transformierte Position des Vertex. float2 text : TEXCORD0; // Texturkoordinate der Texturebene 0. } 3-12 Definition der Input- und Output-Strauktur eines Vertex-Shaders. Um den Shader zu vervollständigen muss ein Einstiegspunkt hinzugefügt werden. Man benötigt also noch eine vs_main-Funktion. Der Name ist allerdings frei definierbar und muss daher später beim Laden des Shaders angegeben werden. VS_OUTPUT vs_main (VS_INPUT in){ VS_OUTPUT out; // Instanz der Vertex-Output-Datenstruktur. … // Bearbeitung des Inputs. return out; // Rückgabe des bearbeiteten Vertex. } 3-13 Definition einer Main-Methode in einem Vertex-Shader. An der Stelle “…” würde die eigentliche Bearbeitung der Vertices stattfinden. An dieser Stelle ist das allerdings nicht von Bedeutung, da ja nur das Prinzip erläutert wird. Daher werden im Anschluss an die Implementierung des Shaders auf der Anwendungsseite die benötigten Komponenten erstellt. LPDIRECT3DVERTEXSHADER9 vertexShader; // Zeiger auf einen Vertexshader. LPD3DXCONSTANTTABLE globalVariable; // Zeiger auf eine Konstantentabelle. 3-14 Definition bedeutender DirectX-Variablen zur Verwendung von Vertex-Shadern. Im Wesentlichen sind es diese zwei Variablen, sie dienen dazu auf den Shader beziehungsweise dessen Konstanten zuzugreifen. Da der Shader-Quelltext in einer separaten Datei vorliegt, muss diese Datei zuerst geladen werden, wie in Code-Ausschnitt 3.6 dargestellt. Bevor man einen Shader innerhalb einer Anwendung lädt, empfiehlt es sich externe Compiler zum Testen zu verwenden, um zum Beispiel Syntaxfehler vorher auszuschließen. Das Debuggen ist aber auch innerhalb von MS Visual Studio möglich. Hierfür muss die Compiler-Option D3DXSHADER_DEBUG gesetzt sein und D3DDEVTYPE_REF beim Erzeugen des Devices angegeben werden. Um zu testen ob der jeweilige Rechner auf dem die Anwendung ausgeführt wird auch das mindestens benötigte Shader-Model unterstützt, müssen die Caps des Grafikadapters ausgelesen werden. Mehr hierzu im Abschnitt zur Initialisierung von DirectX im vierten Kapitel. HRESULT D3DXCompileShaderFromFile( LPCSTR pSrcFile, // Zeiger auf einen String (Dateipfad). CONST D3DXMACRO* pDefines, // Optional, Array von D3DXMACRO Strukturen. LPD3DXINCLUDE pInclude, // Optional, Zeiger auf ein ID3DXInclude // Interface. LPCSTR pFunctionName, // Zeiger auf einen String (Main-Funktion). LPCSTR pProfile, // Zeiger auf ein Shader-Profil. DWORD Flags, // Kompilierungsoptionen S e i t e | XXII Charakter-Animation Eine Gegenüberstellung unter DirectX und OpenGL WS 08/09 LPD3DXBUFFER* ppShader, // Gibt einen Buffer mit den kompilierten // Shader zurück. LPD3DXBUFFER* ppErrorMsgs, // Gibt einen Buffer mit den Fehlermeldungen // zurück. LPD3DXCONSTANTTABLE* ppConstantTable // Gibt ein ID3DXConstantTable // Interface zurück. ); 3-15 DirectX-Funktion zum Laden eines Shaders aus einer Datei. Der nächste Schritt in der Anwendung besteht darin den Vertexshader zu erzeugen. Hierfür stellt die Schnittstelle IDirect3DDevice9 eine passende Funktion zur Verfügung. HRESULT CreateVertexShader( CONST DWORD * pFunction, // Zeiger auf ein Array, // mit Kompilerinformationen. IDirect3DVertexShader9** ppShader // Zeiger einen resultierenden Shader. ); 3-16 DirectX-Funktion zum Erstellen eines vertex-Shaders. Wenn hierbei kein Fehler aufgetreten ist, was anhand des Rückgabewertes überprüft werden kann, lassen sich die globalen Variablen (Konstanten) setzen. Hierzu benötigt man allerdings zuvor die Handles der Konstanten-Tabelle. D3DXHANDLE GetConstantByName( D3DXHANDLE hConstant, // Name in Bezug zur Vater-Struktur oder 0. LPCSTR pName // Name der Konstante. ); 3-17 DirectX-Funktion für den Zugriff auf die Konstanten eines Shaders. Mit Hilfe dieser Handles werden die globalen Variablen der Konstanten-Tabelle gefüllt. Hierbei gibt es für die verschiedenen Datentypen spezifische Methoden. In diesem Beispiel (float4x4) wird die passende Methode für eine Matrix angewendet. HRESULT SetMatrix( LPDIRECT3DDEVICE9 pDevice, // Grafikkartenadapter D3DXHANDLE hConstant, // Zuvor erstelltes Handle. CONST D3DXMATRIX* pMatrix // Die Matrix, die dem Shader zugeordnet wird. ); 3-18 DirectX-Funktion zum Setzen einer Konstanten (einer Matrix). Im anschließenden und letzten Schritt muss der Vertexshader in den Render-Prozess integriert werden. Die Schnittstelle IDirect3DDevice9 bietet dazu die folgende Funktion an. HRESULT SetVertexShader( IDirect3DVertexShader9* pShader // Der zuvor kompilierte Vertex-Shader. ); 3-19 DirectX-Funktion zum Setzen eines Vertex-Shaders im Rendervorgang. Nach diesem Schritt folgen Aktionen wie das Setzen der Materialen und der Texturen für die einzelnen Bestandteile des Meshs, wie sie sonst auch ausgeführt werden, wenn man ohne Shader arbeitet. Zum Zeichnen des Meshs muss nur noch die anschließende Methode aufgerufen werden. HRESULT DrawSubset( DWORD AttribId // Spezifiziert das Subset des Meshs. ); 3-20 DirectX-Funktion zum Zeichnen eines Meshs. Soviel zur Erklärung der Integration eines Vertexshaders, die genaue Implementierung des HLSL Shaders zur Animation eines Charakters befindet sich im Anhang und auf der CD. Das Prinzip dieses Shaders wird jedoch im nächsten Kapitel erläutert. S e i t e | XXIII Charakter-Animation Eine Gegenüberstellung unter DirectX und OpenGL WS 08/09 3.2.4.3. Konzept zur Implementierung einer Charakter-Animation auf Basis eines Shaders Dieses Kapitel beschreibt die Idee und somit die Funktionsweise, wie man mit einem Shader eine Charakter-Animation berechnen kann. Dabei wird sowohl auf Tweening als auch auf Skinning eingegangen. (14) (15) Beim Tweening benötigt der Shader zu jedem Zeitpunkt die Daten der zwei Key-Frames zwischen denen interpoliert werden soll. Unter Daten werden hier die Positionen und Normalen der Vertices verstanden. Des Weiteren wird ein Faktor benötigt, der angibt wie weit die Animation fortgeschritten ist. Wenn dieser Faktor am Anfang 0 und am Ende 1 ist, dann ergibt sich für die Animation die folgende Berechnung. Diese Berechnung ist unabhängig vom Shader, sie muss also auch bei den CPU-basierten Verfahren ausgeführt werden. Daher ist es die gleiche Formel wie im Kapitel 3.2.2.2. Im Anschluss daran muss noch die World-, View- und Projektionsmatrix darauf angewendet werden. Diese kann man dem Shader als eine kombinierte Transformationsmatrix (Konstante) übergeben. Des Weiteren kann man um die Performance zu verbessern die Berechnung von auch in der normalen Anwendung durchführen und das Ergebnis als eine weitere Konstante dem Shader übergeben. So muss diese Berechnung nicht für jeden Vertex einzeln durchgeführt werden. Beim Skinning werden anstatt der Vertexdaten zu den jeweiligen Key-Frames die Matrizen der Bones und eine Liste der je Bone beeinflussten Vertices benötigt. Des Weiteren wird auch hier der oben beschriebene Faktor zur Interpolation verwendet. Die Berechnung stützt sich aber auf eine Reihe von Matrixmultiplikationen. Dabei müssen die Gewichtungen des jeweiligen Bones mit dessen Transformationsmatrix multipliziert und anschließend kumuliert werden. Nach diesem Schritt ist auch wieder die Transformation von World-, View- und Projektionsmatrix und gegebenenfalls weitere Berechnungen durchzuführen. Im Grunde genommen müssen die gleichen Schritte wie bei einer äquivalenten CPU-basierten Methode durchgeführt werden. Der Unterschied liegt darin, dass dem Shader die benötigten Werte mitgeteilt werden und der Shader einmal je Vertex aufgerufen wird. Ihn umgibt demnach eine „unsichtbare Schleife“, die sonst von Hand programmiert werden muss. Um eine effiziente und allgemeingültige Implementierung der Shader zu gewährleisten, muss dem Shader bei der Tweening-Variante ein zweiter Stream zugewiesen werden. Es ist in diesem Fall nicht sinnvoll, das komplette Objekte und somit unter anderem seine Vertexdaten als Konstanten zu übergeben. Wie man einen solchen zweiten Stream zuweist, ist im Kapitel zur Implementierung eines Vertex-Shaders für Tweening erklärt. 3.2.4.4. Effekt Dateien – eine Erweiterung zur Implementierung von Shadern Neben der Möglichkeit einen einfachen Vertexshader zu schreiben gibt es die Möglichkeit EffektDateien zu erstellen. Diese Dateien kombinieren Vertex- und Pixelshader und bieten zusätzlich die Möglichkeit, mehrere Techniken für einen Effekt in einer Datei zu kombinieren. Das bedeutet, dass je nach Hardware des Anwendungssystems eine entsprechende Technik angewendet werden kann. Hierfür bietet DirectX Methoden an, um zur Laufzeit zu testen ob ein System eine bestimmte Technik unterstützt oder nicht. Für das Erstellen dieser Effekte gibt es zwei bekannte IDEs, FX Composer von NVidia und RenderMonkey von AMD. Eine Beschreibung wird hier nicht weiter ausgeführt, denn das würde den Rahmen sprengen. Jedoch wird in diesem Kapitel aufgezeigt, wie man mit solch einer Effektdatei in einer Anwendung arbeitet, wie man sie lädt, wie man die Konstanten setzt und wie man den Render-Prozess entsprechend steuert. S e i t e | XXIV Charakter-Animation Eine Gegenüberstellung unter DirectX und OpenGL WS 08/09 Effekt-Dateien sind allerdings mehr als eine Kombination von Vertex- und Pixelshadern. Beispielsweise kann eine Technik mehrere Passes (Durchläufe) enthalten. Des Weiteren kann man in ihr Effekt-States festlegen, ähnlich wie die Render-States in der Fixed-Function-Pipeline. Daher ergibt sich die Möglichkeit unabhängig von der späteren Anwendung komplette Shader zu entwickeln, wobei die Anwendung diese auf sehr einfache Weise verwenden kann. Im Folgenden wird die Integration einer solchen Datei beschrieben. Wie auch bei einem VertexShader, muss die Effekt-Datei zuerst geladen werden. Hierfür gibt es die Methode D3DXCreateEffectFromFile(…). HRESULT D3DXCreateEffectFromFile( LPDIRECT3DDEVICE9 pDevice, LPCTSTR pSrcFile, CONST D3DXMACRO * pDefines, LPD3DXINCLUDE pInclude, DWORD Flags, LPD3DXEFFECTPOOL pPool, LPD3DXEFFECT * ppEffect, // // // // // // // // // // // // Grafikkartenadapter Zeiger auf einen String (Pfad). Optional, Array von D3DXMACRO Strukturen. Zeiger auf ein ID3DXInclude Interface. D3DXSHADER und D3DXFX Flags. Zeiger auf ein ID3DXEffectPool Objekt. Gibt den resultierenden Effekt zurück. Gibt Fehlermeldungen zurück. LPD3DXBUFFER * ppCompilationErrors ); 3-21 DirectX-Funktion zum Laden einer Effektdatei. Anschließend muss man die Handles erstellen, um auf die Konstanten des Effektes zuzugreifen. Das Vorgehen ist dabei Äquivalent zum vorherigen Beispiel (GetConstantByName). Danach erstellt man einen Handle um auf eine Technik des Effektes zuzugreifen. D3DXHANDLE GetTechniqueByName( LPCSTR pName // Zeiger auf einen String (Name). ); 3-22 DirectX-Funktion für den Zugriff auf die Konstanten einer Effektdatei. Jetzt kann der Render-Prozess beginnen, dazu gilt wie oberhalb auch, dass man mit Methoden wie SetMatrix die Konstanten des Effektes mit Werten belegt. Anschließend wird die gewünschte Technik aktiviert, deren Passes in einer Schleife durchlaufen und der Effekt zum Schluss beendet. effect->SetTechnique(name); effect->Begin(&anzPasses, 0); // // // // // // // Aktiviert eine best. Technik. Ermittelt die Anzahl der Durchläufe. Durchlaufe alle Durchläufe. Beginne Durchlauf pass. Führe weitere Aufgaben aus. Beende Durchlauf pass. for(pass=0; pass<anzPasses; pass++){ effect->BeginPass(pass); // … effect->EndPass(); } effect->End(); // Beende den Effekt. 3-23 Code-Ausschnitt zum Rendern eines Effekts. Wie ersichtlich, ist der Render-Programmcode sehr allgemein gehalten und kann in gleicher Weise auf „alle“ Objekte angewendet werden. Dies ist zumindest dann der Fall, wenn der Effekt die benötigten Effekt-States setzt. Dieses Kapitel dient als Grundlage für die Umsetzung des Teils DirectX in Kapitel vier. Im Kapitel 3.3. wird zuvor allerdings eine Grundlage zum Teil der OpenGL-Programmierung geschaffen. 3.3. Konzeption der OpenGL-Anwendungen S e i t e | XXV Charakter-Animation Eine Gegenüberstellung unter DirectX und OpenGL WS 08/09 3.3.1. Einrichtung der Arbeitsumgebung Im Kapitel 3.2.1. wurde die Einrichtung der Arbeitsumgebung für DirectX beschrieben. Wenn Sie diese Schritte durchgeführt haben, dann haben Sie Visual Studio 2008 bereits eingerichtet. Grundsätzlich lassen sich damit OpenGL Anwendungen ausführen. Um jedoch ein geeignetes Fenstersystem für die Ausgabe zur Verfügung zu haben, benötigen Sie für die OpenGL-Entwicklung die GLUT-Bibliothek. In diesem Fall wurde die aktuelle Version 3.7.6 verwendet, die von Nate Robin nach Windows portiert wurde und unter der folgenden URL zu beziehen ist: http://www.xmission.com/~nate/glut.html Im Anschluss daran, entpacken Sie das heruntergeladene Archiv. Sie finden im entpackten Archiv eine Readme Datei, in der beschrieben wird, welche Dateien in welche Verzeichnisse kopiert werden müssen. Für die Einbindung von GLUT in Visual Studio 2008 folgt hier aber auch nochmal eine kurze Anleitung. Für 32 Bit Betriebssysteme kopieren sie bitte die folgenden Dateien in die angegebenen Verzeichnisse: glut32.dll nach %WinDir%\System32, glut32.lib nach {VS Root}\VC\lib, und glut.h nach {VS Root}\VC\include\GL. VS Root steht hierbei für den Stammordner, in dem Visual Studio 2008 installiert ist. Nachdem Sie diese Schritte durchgeführt haben sollten Sie OpenGL Anwendungen unter Verwendung der GLUT Bibliothek in Visual Studio 2008 kompilieren und ausführen können. Es sind keine zusätzlichen Einstellungen im Programm nötig. Für die Shadersprache GLSL gibt es ebenfalls eine Bibliothek, welche dem Programmierer mit diversen Funktionen die Arbeit an Shaderprogrammen erleichtert. Dies ist die GLEW (GL extension wrangler) Bibliothek, die sich unter http://glew.sourceforge.net/ befindet. Die darin enthaltenen Dateien müssen wie folgt kopiert werden: glew32.dll nach %WinDir%\System32 glew32.lib nach {VS Root}\VC\lib glew.h und wglew.h nach {VS Root}\VC\include\GL Im jeweiligen Projekt, in dem glew benutzt werden soll, muss anschließend noch die glew32.lib als zusätzliche Abhängigkeit eingetragen werden, sonst kommt es zu Fehlern im Link Prozess. Dazu rechtsklicken Sie auf das Projekt, wählen den Reiter Eigenschaften und navigieren zu Konfigurationseigenschaften->Linker->Eingabe->Zusätzliche Abhängigkeiten. Dort muss der Eintrag „glew32.lib“ (ohne Anführungszeichen) eingetragen werden. Falls Sie Windows Vista in der 64 Bit Version verwenden, so müssen die glut32.dll und die glew32.dll Dateien statt nach System32 ins SysWOW64 Unterverzeichnis kopiert werden. 3.3.2. Kompilieren und Starten der Anwendungen Im Rahmen dieser Ausarbeitung wurden zwei OpenGL Programme implementiert, die das Morphing einmal mittels CPU- und einmal mittels Shaderberechnung realisieren. Die Programme befinden sich auf dem Datenträger unter „Source-Code->OpenGL->„Morphing mittels CPU“ bzw. Morphing mittels S e i t e | XXVI Charakter-Animation Eine Gegenüberstellung unter DirectX und OpenGL WS 08/09 Shadern“. Im jeweiligen Programmordner doppelklicken Sie auf die Solution Datei (Morphing mittels CPU.sln/Morphing mittels Shadern.sln). Daraufhin öffnet sich das entsprechende Visual Studio Projekt. Die oben beschriebene, zusätzliche Abhängigkeit der glew32.lib ist bereits gesetzt, so das ein Druck auf die F5 Taste ausreicht, um das Programm auszuführen. Im Pfad „Source-Code->OpenGL>„Morphing mittels CPU-> Morphing mittels CPU“ bzw. „Source-Code->OpenGL->„Morphing mittels Shadern -> Morphing mittels Shadern “ befinden sich einige Objektdateien im 3DS Format. In den Zeilen 107-109 der Datei main.c sehen Sie, welche 3DS Dateien standardmäßig für die Animation benutzt werden (ein menschlicher Kopf in zwei Positionen). Wollen Sie einige der übrigen Objekte verwenden, so muss der entsprechende Dateiname bei der „ObjLoad()“ Funktion geändert werden. Sollten eine oder beide Objektdateien nicht existieren, wird eine Fehlermeldung im Konsolenfenster ausgegeben. Im Verzeichnis Quellen->links.txt“ ist ersichtlich, woher die verwendeten 3DS Dateien stammen. 3.3.3. Animationen mit GLSL Die Shadersprache GLSL ist, wie auch das Pendant HLSL für DirectX, an C angelehnt. GLSL orientiert sich dabei besonders stark an ANSI C während HLSL auch viele aus C++ bekannte Konstrukte enthält. Natürlich gibt es auch neue Elemente, die so nicht in C vorzufinden sind und speziell auf den Bereich der 3D Grafik abgestimmt sind. Deshalb folgt hier zunächst eine Einführung in die Syntax einiger wichtiger Elemente der Shadersprache: 3.3.3.1. Datentypen und Funktionen Einige Datentypen wurden unverändert von C übernommen. Dazu gehören unter anderem: bool int float Da im Grafikbereich viel mit Vektoren gerechnet wird, gibt es entsprechende Konstrukte in GLSL dafür. Zum einen den Datentyp vecx wobei x die Werte 2, 3 und 4 annehmen kann. Damit wird ein 2 bis 4 dimensionaler Vektor definiert, dessen Werte Fließkommazahlen (float) sind. Neben dem Vektor für Fließkommazahlen gibt es noch zwei weitere Vektortypen, für Integer und boolsche Werte, die ebenfalls 2 bis 4 dimensional sein können. Die Datentypen dafür sind: bvecx (bool) ivecx(int) Neben Vektoren wird im Bereich der Computergrafik auch viel mit Matrizen gerechnet. In GLSL gibt es dafür folgende Datentypen: mat2 mat3 mat4 Diese spezifizieren eine 2x2, 3x3 und 4x4 Matrix mit float Werten. Speziell für den Zugriff auf Texturen gibt es in GLSL eine Reihe von so genannten Sampler Datentypen, die dann benötigt S e i t e | XXVII Charakter-Animation Eine Gegenüberstellung unter DirectX und OpenGL WS 08/09 werden, wenn man auf konkrete Werte einer Textur zugreifen möchte. Für 1 bis 3 dimensionale Texturen kann der Datentyp samplerxD mit x = 1 ,2 oder 3 eingesetzt werden. Zudem gibt es noch: samplerCube sampler1DShadow sampler2DShadow für den Zugriff auf eine Cubemap und eine 1D bzw. 2D Tiefentextur mit Vergleichsoperationen. GLSL unterstützt arrays auf die gleiche Weise wie in C mit einem Unterschied: Eine Vorbelegung mit Werten bei der Initialisierung eines Arrays ist nicht möglich. Structs funktionieren analog zu C. Auch Schleifen werden unterstützt nämlich die for, while und do while Schleife. Nachdem nun einige Datentypen vorgestellt wurden, wird als nächstes auf einige Besonderheiten bei der Deklaration von Variablen eingegangen, da es hier einige Unterschiede zu C gibt. Generell setzt GLSL sehr stark auf Konstruktoren (nicht zu verwechseln mit Konstruktoren von Klassen in Java/C+ +) für die Initialisierung von Variablen und Typumwandlungen (typecast). Das folgende Beispiel veranschaulicht dies: Fehler: Referenz nicht gefunden 3-24 Besonderheiten bei der Initialisierung von GLSL Variablen, Quelle (16) Für die Deklaration der Vektor und Matrizentypen mit Wertevorgabe werden ebenfalls die Konstruktoren verwendet. Der jeweilige Konstruktor besitzt dabei den selben Namen wie die Typendeklaration. (zum Beispiel vec4(float,float,float,float)) als Konstruktor für einen 4 dimensionalen Vektor. Zur Vereinfachung müssen im obigen Beispiel nicht zwingend 4 float Werte eingetragen werden. Man kann beispielsweise auch einen 3 dimensionalen Vektor vec3 zur Belegung der x, y und z- Komponente verwenden und nur die w Komponente numerisch spezifizieren: Fehler: Referenz nicht gefunden 3-25 Nutzung des vec Datentyps, Quelle (17) Einer Variablen kann noch ein Qualifier vorangestellt werden. Bedingung dafür ist, dass die Variable global definiert wird. Neben dem schon aus C bekannten const, welches eine nur lesbare Variable definiert, deren Wert unveränderlich ist, gibt es in GLSL unter anderem folgende Qualifier: uniform Uniform Variablen bieten dem Programmierer die Möglichkeit, Daten von der Anwendung an einen Shader zu übergeben. Diese Variablen können sowohl im Vertex- als auch im Fragmentshader definiert werden. Für die Shader sind es nur lesbare Variablen. Uniform Variablen werden für Werte benutzt, die sich innerhalb eines Grafik Primitivs nicht verändern, etwa der Skalierungsfaktor einer Animation. attribute Genau wie die uniform Variablen, dienen attribute Variabeln dazu, Daten von der Anwendung an den Shader zu übermitteln. Im Gegensatz zu den uniform Variablen können attribute Variablen nur im Vertexshader verwendet werden. Zudem gelten Sie nicht nur für ein ganzes Primitiv. So kann theoretisch jeder einzelne Vertexpunkt an eine attribute Variable übergeben werden. S e i t e | XXVIII Charakter-Animation Eine Gegenüberstellung unter DirectX und OpenGL WS 08/09 varying Diese Variablen sind für die Kommunikation zwischen Vertex- und Fragmentshader nötig. Sie müssen in beiden Shadern deklariert werden, sind jedoch nur im Vertexshader änderbar. Der Sinn von varying Variablen besteht darin, Daten vom Vertex- an den Fragmentshader zu geben. Zu beachten ist noch, dass nur bestimmte Datentypen den Qualifier varying verwenden können, konkret sind dies: float, vec2, vec3, vec4, mat2, mat3 und mat 4 (10) (16) (17) (18) 3.3.3.2. Integration von Shadern in ein Programm Nachdem nun einige grundlegende Dinge zur Syntax von GLSL erläutert wurden, beschreibt dieses Kapitel die Schritte, die nötig sind, um einen Shader in eine Anwendung zu integrieren. Der erste Schritt zur Erstellung eines Shaders in OpenGL ist der Aufruf folgender Funktion: Fehler: Referenz nicht gefunden 3-26 Erzeugung eines Shader Containers, Quelle: (18) Mit dieser Funktion wird ein Shader Container erzeugt und ein Handler für den Container als GLuint Variabel zurückgeliefert. Mittels der handler Variablen kann man in seinem OpenGl Programm mit den Shadern arbeiten. Es können beliebig viele Shader (sowohl Vertex als auch Fragment) zu einem OpenGL Programm hinzugefügt werden. Hierbei muss jedoch folgendes beachtet werden: Es darf für jeden Shader Type nur eine main Funktion existieren, ganz gleich, wie viele Shader einer Art man verwenden will. Nachdem nun das Grundgerüst für die Shaderverwendung existiert, wird im nächsten Schritt der Quellcode der Shaderprogramme in die Anwendung integriert. Shaderprogramme werden meistens in externe Dateien abgelegt. Folglich muss zunächst eine Funktion zum Auslesen der Quelltexte implementiert werden. Anschließend wird mit der folgenden Anweisung der zu verwendende Shader Code in OpenGL in den zuvor definierten Shader Container geladen: Fehler: Referenz nicht gefunden 3-27 Laden des Shader Codes in den Shader Container , Quelle: (18) Nun wird der Shader Code kompiliert: Fehler: Referenz nicht gefunden 3-28 Kompilieren des Shader Codes, Quelle: (18) Innerhalb der nächsten Schrite werden alle kompilierten Shader in ein Programm gelinkt. Dazu wird analog zum Shader Container zunächst ein Programm Container Objekt benötigt. Auch hier wird wieder ein handler zurückgegeben: Fehler: Referenz nicht gefunden 3-29 Erzeugung eines Programm Containers, Quelle: (18) Anschließend werden die oben erzeugten Shader dem Programmcontainer hinzugefügt. Geht man von dem Fall aus, dass ein Vertex- und ein Fragmentshader vorliegen, so wird die folgende Funktion zweimal aufgerufen: Fehler: Referenz nicht gefunden 3-30 Shader-Code zum Shader-Programm hinzufuegen, Quelle: (18) Nun folgt der oben erwähnte Linkprozess: S e i t e | XXIX Charakter-Animation Eine Gegenüberstellung unter DirectX und OpenGL WS 08/09 Fehler: Referenz nicht gefunden 3-31 Shader Programm binden , Quelle: (18) Jetzt sind alle erforderlichen Schritte getan, um das Shader Programm in der OpenGL Anwendung zu nutzen. Dazu muss noch ein finaler Funktionsaufruf getätigt werden: Fehler: Referenz nicht gefunden 3-32 Shader Code auf Grafikkarte installieren , Quelle: (18) Diese Funktion muss nur einmal implementiert werden. Sollte ein Shader Programm, dass bereits durch die obige Funktion verwendet wird, neu gelinkt werden, so wird die obige Funktion automatisch neu aufgerufen. Das grundlegende Konzept zur Animation mit einem Shader entspricht dem für HLSL (siehe Kapitel 3.2.4.3). (10) (16) (17) (18) 3.4. Zusammenfassung (Gemeinsam) Dieses Kapitel stellte zunächst die Bedeutung der Zeitmessung bei Animationen dar. Anschließend wurden wichtige Voraussetzungen zur Implementierung der Programme erörtert. Sowohl für DirectX, als auch für OpenGL wurden die nötigen Schritte zur Einrichtung der Arbeitsumgebung aufgezeigt. Im DirectX Teil folgte nun die Erläuterung der beiden Animationstechniken Morphing/Tweening und Skinning und wie diese in einer Anwendung umzusetzen sind. Anschließend wurde die Shadersprache HLSL erläutert, sowie notwendige Voraussetzungen zur Integration von Shadern in eine DirectX Anwendung besprochen. Am Ende wurden noch Effekt Dateien vorgestellt, die unter anderem dazu dienen, Vertex und Pixel Shader in einer gemeinsamen Datei zu entwickeln. Im OpenGL Teil wurde die Shadersprache GLSL vorgestellt und ebenfalls gezeigt, wie Shader in eine OpenGL Anwendung integriert werden. Dabei wurden auch wichtige Unterschiede der Shader Sprache im Vergleich zur herkömmlichen C Syntax erläutert. Da das grundlegende Konzept zur Realisierung der Animation identisch zu DirectX ist, wurde darauf im zweiten Teil nicht noch einmal eingegangen. S e i t e | XXX Charakter-Animation Eine Gegenüberstellung unter DirectX und OpenGL WS 08/09 4. Realisierung der Konzeption Nachdem die Konzeption der Anwendungen abgeschlossen ist, steht der nächste Schritt an – die Implementierung der Animationen. Dieses Kapitel zeigt daher Möglichkeiten auf, wie man solche Charakter-Animationen praktisch umsetzt. 4.1. Modellierung und Animation des Charakters Der erste praktische Schritt ist die Erstellung des zu animierenden 3D-Objektes. Hierfür wird unter anderem ein Charakter erstellt und in die entsprechenden Dateiformate, die in diesem Projekt für DirectX beziehungsweise OpenGL benötigt werden, exportiert. Diesen Charakter gilt es einerseits mit Hilfe von Bones zu animieren und auf der anderen Seite in mehreren Key-Frames so zu verformen, dass Tweening als zweite Animationstechnik anwendbar ist. 4.1.1. Modellierung Das Ziel, welches bei der Modellierung des Charakters verfolgt wird, ist es, mit möglichst wenigen Polygonen einen hohen Detailgrad zu erreichen. Aus diesem Grund besteht der Körper nur aus einer sehr geringen Anzahl von Polygonen, die durch Funktionen wie Bevel und Extrude aus einem Zylinder geformt werden. Auf diese Art und Weise lassen sich alle Körperteile bis auf den Kopf realisieren. Im Anschluss daran wird auf den Körper eine Funktion angewendet, die die wenigen existierenden Polygone aufteilt und abrundet, wodurch eine „organische“ Figur entsteht. In LightWave3D nennt sich diese Funktion SubPatch. Im zweiten Abschnitt der Modellierung wird der Kopf realisiert. Hierzu legt man am besten zwei Bilder in den Hintergrund der Modellierungssoftware. Eines in der Front-Perspektive und ein weiteres Bild, auf dem der Kopf von der Seite zu sehen ist. An dieser Stelle ist es wichtig darauf zu achten, dass beide Bilder den Kopf in der gleichen Größe darstellen. Des Weiteren ist ein möglichst hoher Detailgrad beziehungsweise eine möglichst hohe Auflösung der Bilder zu empfehlen. Nach diesem Schritt fängt man an und 4-33 Bild vom Auge zeichnet ein Polygon. Das Auge bietet sich hierbei als Startpunkt an, wobei des Charakters. man das erste Polygon immer wieder durch Extrude erweitert und im Anschluss daran so verschiebt, dass ein Auge entsteht. Wenn das Auge fertiggestellt ist, ist es empfehlenswert im nächsten Schritt die Nase zu modellieren. Hierzu geht man wie bereits beschrieben vor. Die Linien, die in den Abbildungen eingezeichnet sind skizzieren die eigentlichen Polygone. Das heißt, diese Polygone sind von Hand modelliert. Wohingegen der weiche, abgerundete Eindruck durch die zu anfang beschriebene Funktion (SubPatch) erreicht wird. Nun, da sowohl die Augen als auch die Nase fertiggestellt sind, folgt die Verknüpfung der beiden Objekte. Hierzu werden passende Polygone durch Extrude von der Nase erweitert. Durch die Funktion Weld lassen sich die Vertices der Nase und der Augen an den sich überlagernden Stellen vereinen. Auf 4-34 Bild vom Gesicht des diese Weise gelangt man letztendlich zu einem Objekt – das Charakters. keine unerwünschten Lücken besitzt. S e i t e | XXXI Charakter-Animation Eine Gegenüberstellung unter DirectX und OpenGL WS 08/09 In Folge dessen lassen sich schließlich die Wangen und der Rest des Kopfes modellieren. Hierzu erweitert man wie bisher auch die Polygone und achtet insbesondere darauf, dass das Ergebnis mit den Ansichten, in denen die Bilder hinterlegt sind, übereinstimmen. Außerdem empfiehlt es sich die perspektivische Ansicht als Kontrolle zu verwenden. Die oberhalb beschriebene Technik lässt sich für fast alle Körperteile anwenden. So zum Beispiel auch um die Füße oder Finger detaillierter zu modellieren. An dieser Stelle wird auf eine Beschreibung allerdings verzichtet. Eine einfachere Variante für Hände und Finger bilden jedoch Manipulationen der Polygone. Beispielsweise wenn man im Subpatch-Modus arbeitet und die Polygone durch Extrude und Bevel so erweitert, dass man zuerst das Nagelbett mittels negativem Bevel erstellt und anschließend den Fingernagel durch ein weiteres positives Bevel erzeugt. Der letzte Schritt in der Modellierung betrifft die Haare. In einer 4-35 Bild vom Gesicht des Animation, wie sie für animierte Kinofilme erstellt wird, lassen sich Charakters. Haare mit einem extrem hohen Detailgrad und einer ausgiebigen physikalischen Berechnung realisieren. In einer Animation, wie sie in einem Computerspiel vorkommt, ist dies aus zeittechnischen Gründen nicht möglich. Eine solche Berechnung wäre schlicht zu aufwendig. Daher werden die Haare normalerweise als simple Polygone modelliert. Um eine Bewegung der Haare innerhalb der Animation zu realisieren ist beispielsweise ein Bone für einen Zopf denkbar. Hierdurch ist ein angemessener Rechenaufwand in Verbindung mit einem gelungenen optischen Eindruck vereinbar. Außerdem können weitere Details effektiver durch Texturen und im speziellen Bumpmaps hinzugefügt werden. 4.1.2. Animation Nach der Modellierung folgt die Animation. Hierbei ist zu beachten, dass der Charakter einmal für das Tweening und ein weiteres mal für das Skinning animiert werden muss. An dieser Stelle sei jedoch gesagt, dass es bei beiden Verfahren möglich ist mit Bones innerhalb der Modellierungssoftware zu arbeiten. Beim Exportieren in das gewünschte Format muss man aber darauf achten, dass die Bones und Animationsdaten mit erfasst werden oder eben nicht. Von daher ist die Auswertung der Datei und die Verwendung der darin enthalten Informationen entsprechend der Animationstechnik anzupassen. Außerdem sollte man gut überlegen bei welchen Animationen Skinning beziehungsweise Tweening vorteilhafter ist. Aus diesem Grund basiert die Animation auf Key-Frames in Verbindung mit Bones. Hierzu werden die Bones so manipuliert, dass eine flüssige Animation entsteht. Auf eine Beschreibung dieser Animierung des Charakters wird verzichtet, letztendlich müssen hierbei „nur“ die Bones entsprechend rotiert und die Parameter der Bones angepasst werden, so dass jeweils nur die gewünschten Vertices beeinflusst werden. 4.2. Implementierung der DirectX-Anwendungen Dieser Teilbereich der Ausarbeitung beschränkt sich auf die Implementierung der Animationen mit DirectX. Der erste Schritt ist dabei die Erstellung einer Windows-Anwendung und im zweiten Schritt die Initialisierung von DirectX, beziehungsweise aller benötigten DirectX-Komponenten. Aus Gründen der Komplexität und da es nicht weiter notwendig ist, beschränken sich die Anwendungen dabei auf die Komponenten DirectX Graphics und DirectInput. Nachdem diese Schritte durchgeführt wurden, folgt das Laden der 3D-Objekte und ihrer Animationsdaten. Der letzte – und bedeutendste – S e i t e | XXXII Charakter-Animation Eine Gegenüberstellung unter DirectX und OpenGL WS 08/09 Teil dieses Kapitels liegt jedoch in der Implementierung der eigentlichen Animationen mit Hilfe von DirectX Graphics und HLSL. 4.2.1. Aufbau einer Windows-Applikation Dieses Kapitel beschreibt, wie man den Rahmen einer Windows-Anwendung erstellt und welche Bedeutung die einzelnen Bestandteile haben. Dies ist von grundlegender Bedeutung, da jede Anwendung, also auch ein Spiel auf Basis von DirectX, sich in Windows integrieren muss. Hierbei stehen der Anwendung nicht alle System-Ressourcen zur alleinigen Verfügung, wie es bei MS DOS war, daher wird erläutert, wie diese Integration funktioniert. Eine wichtige Überlegung vorweg. Da sich die Anwendung das System mit vielen anderen Anwendungen teilt und Windows dies managen muss, benötigt man hierfür einen speziellen Mechanismus. Auch muss jede Anwendung Nachrichten von Windows empfangen und verarbeiten können. Beispielsweise wenn der Benutzer auf den Schließen-Button drückt oder der Benutzer ein Fenster verschiebt, dass zuvor einen Teil des Anwendungsfensters dieser Anwendung verdeckt hat. Doch bevor es so weit ist, gilt es den Einstiegspunkt in die Anwendung zu implementieren. int APIENTRY _tWinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow ); 4-36 API der WinMain-Funktion. Der Code-Ausschnitt 4.4 zeigt die Schnittstelle der WinMain-Methode. Diese Methode ist vergleichbar mit einer main-Funktion in einer normalen C-Anwendung. Von Bedeutung sind dabei in erster Linie die Parameter hInstance und nCmdShow. hInstance ist die aktuelle Instanz der Anwendung und nCmdShow beschreibt den Darstellungsmodus. Im Darstellungsmodus wird zum Beispiel unter minimiert und maximiert unterschieden. Durch den Zugriff auf den Parameter hInstance lassen sich zum Beispiel auch neue Dialoge anzeigen. Die WinMain-Methode hat die Aufgabe einen Call-Back-Handler einzurichten. Dieser ist dafür verantwortlich, dass das System der Anwendung Nachrichten senden kann. Die Schnittstelle des CallBack-Handlers ist im Ausschnitt 4.5 aufgeführt. LRESULT CALLBACK WndProc( HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam ); 4-37 API für einen Callback-Handler. Der Name der Methode ist dabei frei wählbar. Der Parameter hWnd ist das Fenster, auf das sich die Botschaft bezieht. Der zweite Parameter message definiert die Nachricht anhand einer ID und die beiden letzten Parameter enthalten spezifische Informationen der Nachrichten. Nach diesen Grundlagen folgt die Erstellung eines Fensters. Hierzu muss eine Datenstruktur vom Typ WNDCLASSEX erstellt und mit Daten gefüllt werden. Daten bedeuten in diesem Fall Informationen über den Titel, den Style, das Icon, den Cursor, das Menü und weitere Eigenschaften. Diese Datenstruktur muss Registriert werden. Scheitert dieser Vorgang, so wird in der Regel die Anwendung beendet. Des Weiteren sollten die Akzeleratoren geladen werden. Akzeleratoren sind definierte Tastenkürzel, denen symbolische Konstanten zugewiesen sind. Im nächsten Schritt lässt sich das Fenster der Anwendung erzeugen. S e i t e | XXXIII Charakter-Animation Eine Gegenüberstellung unter DirectX und OpenGL WS 08/09 HWND CreateWindow( LPCTSTR lpClassName, LPCTSTR lpWindowName, DWORD dwStyle, int x, int y, int nWidth, int nHeight, HWND hWndParent, HMENU hMenu, HINSTANCE hInstance, LPVOID lpParam ); 4-38 API der Funktion zum Erstellen eines Windows-Fensters. Wobei die Parameter nach den vorangegangenen Erläuterungen selbsterklärend sind. Wenn der Aufruf von CreateWindow keinen Fehler verursacht, so ist man fast am Ziel. Der letzte Schritt ist die Implementierung der Hauptnachrichtenschleife. while(msg.message != WM_QUIT){ while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)){ if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg)){ TranslateMessage(&msg); DispatchMessage(&msg); } } RenderAnimation(); MoveAnimation(); } 4-39 Beispiel für eine Hauptprogrammschleife. In dieser Schleife wird in jedem Durchlauf überprüft, ob Windows Nachrichten an die Anwendung gesendet hat. Ist dies der Fall, so werden diese verarbeitet. Andernfalls, oder nach der Abarbeitung der Nachrichten, werden weitere Methoden ausgeführt. In diesem Beispiel sind es zwei Methoden zum Rendern und Bewegen der 3D-Animation. Beendet wird diese Endlosschleife dann, wenn der Benutzer die Anwendung schließt. Daher muss nach der Schleife gegebenenfalls Speicher wieder freigegeben werden oder andere Aufgaben ausgeführt werden. 4.2.2. Initialisierung der benötigten DirectX-Komponenten Nach der Einführung in die Windows-Programmierung steht als nächster Schritt die Initialisierung der benötigten DirectX-Komponenten an. Hierzu wird aufgezeigt wie und an welcher Stelle dies implementiert ist. 4.2.2.1. DirectX-Graphics Bei der Implementierung einer DirectX-Anwendung ist die Initialisierung der benötigten Komponenten der erste Schritt. Hierbei gilt es zu aller erst eine IDirect3D9-Schnittstelle zu erstellen. Durch diese Schnittstelle lassen sich alle auf dem Computer zur Verfügung stehenden Adapter auslesen. Das Makro D3D_SDK_VERSION ist in der Direct3D-Headerdatei deklariert und stellt die auf dem Computer installierte Version dar. LPDIRECT3D9 d3d = Direct3DCreate9(D3D_SDK_VERSION); if(!d3d) return 0; 4-40 Quell-Code zum initialisieren einer IDirect3D9-Schnittstelle. Sollte die Initialisierung fehlgeschlagen sein, kann dies durch eine Message Box angezeigt werden. Der Benutzer müsste dann seine DirectX-Version erneuern. In diesem Fall wird dann jedoch nur 0 S e i t e | XXXIV Charakter-Animation Eine Gegenüberstellung unter DirectX und OpenGL WS 08/09 zurückgegeben, wodurch die Anwendung beendet wird. Ist die Initialisierung erfolgreich verlaufen, so erlangt man Zugriff auf die im System vorhanden Adapter. Ein Adapter ist in diesem Fall eine Grafikkarte oder ein On-Board-Grafikchip. Benötigt man die Schnittstelle nicht mehr, so gibt man sie durch die Methode Release frei. Diese Methode wurde von der Schnittstelle IUnknown geerbt, die im zweiten Kapitel beschrieben ist. Durch diese Schnittstelle lassen sich im nächsten Schritt alle auf dem System zur Verfügung stehenden Adapter auflisten. Doch um einen detaillierteren Konfigurationsdialog zu erstellen müssen auch die Caps (Capabilities – Fähigkeiten) der Grafikkarte ausgelesen werden. Auf eine Beschreibung und Implementierung eines solchen Dialoges wird hier jedoch verzichtet. Stattdessen werden wenn möglich die Standardeinstellungen beibehalten. Prinzipiell geht man aber wie folgt vor. if( d3d->GetDeviceCaps( D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, &caps) < 0){ MessageBox( hWnd, "Kein HAL-Device verfügbar!", "Achtung!", MB_OK | MB_ICONWARNING | MB_SETFOREGROUND); devtype = D3DDEVTYPE_REF; // REF – Software Referenzimplementierung }else devtype = D3DDEVTYPE_HAL; // HAL – Hardware Abstraction Layer 4-41 Abfrage des HAL-Devices eines Systems. Bei tiefergehendem Interesse an dieser Thematik ist die Quelle (6) sehr zu empfehlen. Wichtig ist es aber in jedem Fall zu überprüfen, ob ein HAL-Device zur Verfügung steht. Das sollte heute zwar der Fall sein, aber es empfiehlt sich immer Fähigkeiten zuerst zu überprüfen und Rückgabewerte zu kontrollieren. Der Vorteil für den Programmierer von DirectX 10 liegt an dieser Stelle klar auf der Hand. Während bei DirectX 9 viele Fähigkeiten optional sind, müssen DirectX 10 fähige Grafikkarten eine große Menge an Funktionen zwingend Unterstützen. if(d3d->CreateDevice(D3DADAPTER_DEFAULT, devtype, hWnd, D3DCREATE_HARDWARE_VERTEXPROCESSING, &d3dpp, &device) < 0) return 0; 4-42 Quell-Code zum Erstellen eines Direct3D9-Devices. Angenommen es sind alle Einstellungen wie zum Beispiel Auflösung und Antialiasing gewählt, so lässt sich im nächsten Schritt ein Device erstellen. Hierfür benötigt man eine IDirect3DDevice9Schnittstelle. Durch diese Schnittstelle lassen sich bei erfolgreicher Initialisierung die Objekte laden und auf den Bildschirm zeichnen. Zuvor beschreibt das nächste Kapitel jedoch die Initialisierung von DirectInput. 4.2.2.2. DirectInput Wie bereits erwähnt ist DirectInput für alle Arten von Eingabegeräten zuständig. In diesem Fall reicht es allerdings aus auf die Standard-Maus und Standard-Tastatur des Systems zuzugreifen. Wie bei DirectX-Graphics auch, ist der erste Schritt die Initialisierung der benötigten Schnittstelle. In diesem Fall vom Typ LPDIRECTINPUT8. Zur Initialisierung wird die folgende Methode aufgerufen. HRESULT DirectInput8Create( HINSTANCE hinst, DWORD dwVersion, REFIID riidltf, LPVOID *ppvOut, LPUNKNOWN punkOuter ); 4-43 Quell-Code zum Erstellen einer DirectInput-Schnittstelle. S e i t e | XXXV Charakter-Animation Eine Gegenüberstellung unter DirectX und OpenGL WS 08/09 Durch den Zugriff auf diese Schnittstelle lassen sich die Maus- und Tastatur-Devices erstellen und initialisieren. Diese Geräte sind vom Typ LPDIRECTINPUTDEVICE8. An dieser Stelle sei daraufhin gewiesen, dass die 8 am Ende der Bezeichnungen auch bei DirectX 9.0c aktuell ist. Es wird also kein DirectX 8 verwendet. HRESULT CreateDevice( REFGUID rguid, LPDIRECTINPUTDEVICE * lplpDirectInputDevice, LPUNKNOWN pUnkOuter ); 4-44 Quell-Code zum Erstellen eines DirectInput-Devices. Um die Verbindung zur Systemmaus herzustellen, muss der Parameter rguid auf die Konstante GUID_SysMouse gesetzt werden. Bei der Systemtastatur ist es GUID_SysKeyboard. Ist hierbei kein Fehler aufgetreten, so kann man im nächsten Schritt das Datenformat festlegen. HRESULT SetDataFormat( LPCDIDATAFORMAT lpdf ); 4-45 Quell-Code zum Setzen des Datenformats eines DirectInput-Devices. Bei der Maus wird zwischen c_dfDIMouse und c_dfDIMouse2 unterschieden. Der Unterschied liegt hierbei darin, dass c_dfDIMouse nur eine normale Maus mit 4 Tasten abbildet und c_dfDIMouse2 bis zu 8 Tasten. Bei der Tastatur wird als Parameter für das Datenformat c_dfDIKeyboard verwendet. Es ist aber auch möglich, seine eigenen Datenformate zu erstellen. Der letzte Schritt liegt in der Festlegung des Kooperationslevels. Hiermit gibt man an, ob die Anwendung die Maus oder Tastatur exklusiv oder nicht exklusiv verwendet oder ob die Daten auch dann an das Programm gesendet werden sollen, wenn sich das Anwendungsfenster nicht im Vordergrund befindet. Hier wird jedoch eine Kombination aus nicht exklusiv und Vordergrund verwendet, da die Anwendung in einem Fenster und nicht im Vollbildmodus läuft. HRESULT SetCooperativeLevel( HWND hwnd, DWORD dwLevel ); keyboardDevice->SetCooperativeLevel(hWindow, DISCL_NONEXCLUSIVE | DISCL_FOREGROUND); 4-46 Quell-Code zum Setzen des Kooperationslevels. Durch den Aufruf der Methode Acquire des Devices lässt sich die Verbindung nun noch aktivieren. Diese Methode muss auch immer dann aufgerufen werden, wenn der Zugriff darauf verloren gegangen ist. Dies passiert beispielsweise dann, wenn der Benutzer die Windows-Taste drückt oder wenn Er mit der Maus eine andere Anwendung aktiviert. 4.2.3. Laden von 3D-Objekten aus einer X-Datei S e i t e | XXXVI Charakter-Animation Eine Gegenüberstellung unter DirectX und OpenGL WS 08/09 Nachdem alle benötigten Komponenten initialisiert wurden, folgt im das Laden der 3D-Objekte. In diesem Fall beschränkt sich die Beschreibung auf einen normalen Mesh. Eventuell weitere Objekte in der Szene werden nicht beschrieben. Bei Interesse sehen Sie sich bitte den passenden Quell-Code an. LPD3DXMESH meshSource; D3DMATERIAL9 *materialien; DWORD anz_mat; LPDIRECT3DTEXTURE9 *texturen; LPD3DXBUFFER materialbuffer = 0; 4-47 Variablen zur verwaltung eines Meshs sowie dessen Materialien und Texturen. Oberhalb sind eine Reihe Variablen aufgeführt, die zum Laden und anzeigen eines Meshs benötigt werden. Wie bereits beschrieben ist in der Datenstruktur LPD3DXMESH ein Mesh enthalten. Des Weiteren existieren Variablen in denen nachfolgend dessen Materialien und Texturen gespeichert werden. Wie Sie im Ausschnitt 4.16 sehen, werden einige dieser Variablen übergeben, in denen nach erfolgreicher Ausführung der Ladefunktion wichtige Informationen gespeichert sind. HRESULT D3DXLoadMeshFromX( LPCTSTR PFILENAME, DWORD OPTIONS, LPDIRECT3DDEVICE9 PD3DDEVICE, LPD3DXBUFFER * PPADJACENCY, LPD3DXBUFFER * PPMATERIALS, LPD3DXBUFFER * PPEFFECTINSTANCES, DWORD * PNUMMATERIALS, LPD3DXMESH * PPMESH ); 4-48 Funktion zum Laden einer X-Datei. Sollte das Laden fehlgeschlagen sein, so wird 0 zurückgegeben. Dadurch springt die Anwendung zurück in die WINMAIN-Funktion. An dieser Stelle gilt es daraufhin die Anwendung zu beenden und gegebenenfalls Aufräumarbeiten durchzuführen. Neben dieser Ladefunktion existieren auch erweiterte Varianten, um etwa auf Animationen zuzugreifen. Wenn die X-Datei erfolgreich geladen wurde, stehen das Verarbeiten der Materialien und das Laden der Texturen an. Die Texturen müssen dabei separat geladen werden, da in den X-Dateien nur die entsprechenden Pfade gespeichert sind. mat = (D3DXMATERIAL*)materialbuffer->GetBufferPointer(); materialien = new D3DMATERIAL9[anz_mat]; texturen = new LPDIRECT3DTEXTURE9[anz_mat]; for( i = 0; i < anz_mat; i++){ materialien[i] = mat[i].MatD3D; materialien[i].Ambient = D3DXCOLOR( 1, 1, 1, 0); if( D3DXCreateTextureFromFile( device, mat[i].pTextureFilename, &texturen[i]) < 0) texturen[i] = NULL; } 4-49 Quell-Code zum Auslesen der Materialien und Texturen eines Meshs. Um sicher zu stellen, dass der Mesh Normalen-Vektoren enthält, wird dessen FVF angepasst. Dies kann man beispielsweise dadurch erreichen, dass man den alten Mesh klont und dabei das gewünschte FVF angibt. In dem temporären Mesh ist an dieser Stelle Speicherplatz für die Normalen der Vertices enthalten. Um die Normalen zu berechnen muss aber die Funktion D3DXComputeNormals(…) aufgerufen werden. meshSource->CloneMeshFVF( meshSource->GetOptions(), meshSource->GetFVF()| D3DFVF_NORMAL, device, &tmp_mesh ); meshSource->Release(); meshSource = tmp_mesh; S e i t e | XXXVII Charakter-Animation Eine Gegenüberstellung unter DirectX und OpenGL WS 08/09 D3DXComputeNormals( meshSource, 0); 4-50 Quell-Code zum Klonen eines Meshs um dessen FVF anzupassen. Nachdem diese Schritte durchgeführt wurden, sind alle benötigten Daten vorhanden. Daher wird in den nachfolgenden Kapiteln beschrieben, wie man diese Daten verarbeitet und in Abhängigkeit der Zeit eine Animation berechnet. 4.2.4. Zeitmessung Im Kapitel 3.1. wurde beschrieben warum es von Bedeutung ist die vergangene Zeit für die Animation zu Grunde zu legen. Dieses Kapitel erläutert wie man praktisch in einer Anwendung diese Berechnungen durchführen kann. In C gibt es die bekannte Funktion timeGetTime, um sich die aktuelle Systemzeit zurückgeben zu lassen. Die hier verwendete Methode ist jedoch noch genauer. __int64 cntsPerSec = 0; QueryPerformanceFrequency((LARGE_INTEGER*)&cntsPerSec); float secsPerCnt = 1.0f / (float)cntsPerSec; 4-51 Quell-Code zur Zeitmessung (1 von 3). Auf Basis von (19). Um die in jedem Frame vergangene Zeit beziehungsweise die Zeit seit Anwendungsstart zu berechnen wird ein Anfangswert benötigt. Des Weiteren beruht diese Zeitmessung auf dem PerformaceTimer, wozu windows.h eingebunden werden muss. Die Berechnung bezieht sich dabei auf Einheiten namens Counts. Die in diesem Kontext in Sekunden umgerechnet werden müssen. __int64 prevTimeStamp = 0; QueryPerformanceCounter((LARGE_INTEGER*)&prevTimeStamp); __int64 currTimeStamp = 0; QueryPerformanceCounter((LARGE_INTEGER*)&currTimeStamp); float dt = (currTimeStamp - prevTimeStamp)*secsPerCnt; 4-52 Quell-Code zur Zeitmessung (2 von 3). Auf Basis von (19). Der im Ausschnitt 4.20 dargestellt Code berechnet die vergangene Zeit zwischen zwei Messpunkten. Diese Berechnung wird in jedem Frame einmal ausgeführt und die Zeit für einen Durchlauf in der Variable dt gespeichert. Im nachfolgenden Ausschnitt 4.21 wird auf Basis einer Animation zwischen zwei Meshs und einer Animationsdauer von 3 Sekunden die aktuelle Zeit der Animation berechnet. Da die Animation, wenn sie am Ende angekommen ist wieder in ihre Ursprungsposition zurückkehren soll, wird die Zeit erst erhöht, dann erniedrigt und so weiter. if(animDirection == 0) curMorphTime += dt; else curMorphTime -= dt; if(curMorphTime >= 3.0f) animDirection = -1; else if(curMorphTime <= 0) animDirection = 0; 4-53 Quell-Code zur Zeitmessung (3 von 3). Es entsteht also ein Kreislauf zwischen zwei Meshs. Der letzte Ausschnitt in diesem Kapitel zeigt den Aufruf der Methode zum Tweening zweiter Meshs mit der CPU basierten Methode. Dabei wird die aktuelle Zeit der Animation durch 3 dividiert, was den Formeln im Kapitel 3.1 entspricht. Somit ist die Berechnung der Zeit abgeschlossen und kann nachfolgend verwendet werden. morph(curMorphTime / 3.0f, meshSource, meshTarget, meshToDraw); morph(curMorphTime / 3.0f, meshWater, meshWaterTarget, meshWaterToDraw); 4-54 Quell-Code zum AUfruf der Morphing-Funktion in Abhängigkeit der Zeit. 4.2.5. CPU-basierte Charakter-Animation durch Tweening S e i t e | XXXVIII Charakter-Animation Eine Gegenüberstellung unter DirectX und OpenGL WS 08/09 Dieses Kapitel beschreibt, wie der Algorithmus zum Tweening mit der CPU aufgebaut ist und wie er funktioniert. Im vorherigen Kapitel wird die Berechnung der Zeit und der Aufruf der Tweening/Morphing Funktion beschrieben. Die in diesem Kapitel beschriebene Funktion ist genau die, dessen Aufruf im Ausschnitt 4.22 dargestellt wird. In dieser Funktion wird zwischen zwei Meshs linear interpoliert. Key-Frames sind hier noch nicht von Bedeutung. Allerdings ändert die Verwendung von Key-Frames im Vergleich zu mehreren Objekten nichts am Algorithmus. Da man in diesem Fall sowohl den Anfangszustand, den Endzustand und auch den jeweils zu zeichnende Mesh benötigt, werden insgesamt drei Variablem vom Typ Mesh verwendet. Zur Berechnung der aktuellen Position benötigt man den Zugriff auf die Vertices der Meshs. Dazu wird die Funktion LockVertexBuffer aufgerufen. Im nächsten Schritt werden die Größen der Vertexdaten berechnet. Die Funktion D3DXGetFVFVertexSize erwartet dazu ein FVF, anhand der darin enthaltenen Daten wird dessen Größe berechnet und zurückgegeben. char *pSourcePtr, *ptargetPtr, *pResultPtr; meshSource->LockVertexBuffer(D3DLOCK_READONLY, (void **)&pSourcePtr); meshTarget->LockVertexBuffer(D3DLOCK_READONLY, (void **)&ptargetPtr); meshToDraw->LockVertexBuffer(0, (void **)&pResultPtr); DWORD pSourceSize DWORD pTargetSize DWORD pResultSize 4-55 Quell-Code zum = D3DXGetFVFVertexSize(meshSource->GetFVF()); = D3DXGetFVFVertexSize(meshTarget->GetFVF()); = D3DXGetFVFVertexSize(meshToDraw->GetFVF()); Tweening mit der CPU (1 von 3). Danach werden in einer Schleife alle Vertices durchlaufen. Am Anfang wird ein Pointer für jeden Vertex der drei Meshs erstellt und in eine Struktur gecastet, die die Vertexposition, dessen Normale und dessen Farbe enthält. Nach diesem Schritt folgt die Interpolation, wobei aus Effizienzgründen die Berechnung von 1-currentAnimationTime besser aus der Schleife heruasgezogen werden sollte. Am Ende der Schleife wird der Pointer so verschoben, dass er auf das nächste Element zeigt. DWORD numVertices = meshToDraw->GetNumVertices(); for(DWORD i=0; i<numVertices; i++){ meinvertex *pSourceVertex = (meinvertex*) pSourcePtr; meinvertex *pTargetVertex = (meinvertex*) ptargetPtr; meinvertex *pResultVertex = (meinvertex*) pResultPtr; D3DXVECTOR3 vecSource = pSourceVertex->pos; vecSource *= (1.0f - currentAnimationTime); D3DXVECTOR3 vecTarget = pTargetVertex->pos; vecTarget *= currentAnimationTime; pResultVertex->pos = vecSource + vecTarget; pSourcePtr += pSourceSize; ptargetPtr += pTargetSize; pResultPtr += pResultSize; } meshSource->UnlockVertexBuffer(); meshTarget->UnlockVertexBuffer(); meshToDraw->UnlockVertexBuffer(); 4-56 Quell-Code zum Tweening mit der CPU (2 von 3). Nachdem die Vertices interpoliert wurden, werden die VertexBuffer wieder freigegeben und der Mesh kann gerendert werden. Aus diesem Grund ist in 4.25 der Ausschnitt der Renderfunktion dargestellt, der die Materielien und Texturen des Meshs setzt und den Mesh zeichnet. DWORD i; for( i = 0; i < anz_mat; i++ ){ S e i t e | XXXIX Charakter-Animation Eine Gegenüberstellung unter DirectX und OpenGL WS 08/09 device->SetMaterial( &materialien[i]); device->SetTexture( 0, texturen[i]); meshToDraw->DrawSubset( i); } 4-57 Quell-Code zum Tweening mit der CPU (3 von 3). Dieser Vorgang ist in einer Schleife implementiert, da alle Untermengen des Meshs so durchlaufen werden, auch wenn in diesem Beispiel der Mesh nur aus einer einzigen Menge besteht. Eine andere Variante der Interpolation liegt in der von DirectX bereitgestellten Funktion zum Verfahren LERP. D3DXVECTOR3 * D3DXVec3Lerp( D3DXVECTOR3 * pOut, // CONST D3DXVECTOR3 * pV1, // CONST D3DXVECTOR3 * pV2, // FLOAT s ); 4-58 Definition der Funktion D3DXVec3Lerp. Diese Funktion erwartet als pOut einen Vektor, in dem das Ergebnis gespeichert wird. Dies entspricht demnach dem Positionsvektor des Ergebnis-Meshs (pResultVertex->pos). Die Parameter pV1 und pV2 stellen die beiden Eingangsvektoren des Source- und Target-Meshs dar und der Parameter s die aktuelle Zeit der Animation. 4.2.6. GPU-basierte Charakter-Animation durch Tweening mit einem HLSL-Vertexshader (Effekt-Datei) Dieses Kapitel beschreibt die Animation mit einem Vertexshader basierend auf dem Verfahren Tweening. Im vorherigen Kapitel wurde die CPU basierte Methode vorgestellt, daher gilt es nun die Unterschiede herauszuarbeiten. Ein grundlegender Unterschied liegt darin, dass kein separater Mesh benötigt wird, der den aktuell zu zeichnenden Zustand repräsentiert. Es werden im Gegensatz dazu beide Meshs, eher gesagt dessen Vertexdaten, dem Shader übergeben. Der Vertexshader wird dementsprechend einmal je Vertex aufgerufen und berechnet dessen aktuelle Position. Durch dieses Verfahren wird sowohl Zeit als auch Arbeitsspeicher eingespart. Da der Vertexshader in diesem Fall sowohl die Vertexdaten des Anfangsmeshs als auch die des Zielmeshs benötigt, müssen ihm zwei Streams zugeordnet werden. Bevor es aber soweit ist, müssen die Vertexdaten entsprechend ausgelesen und in das benötigte Format gewandelt werden. Die Struktur meinvertex repräsentiert dabei einen Vertex und die aufgelisteten Variablen enthalten die Meshs, die Effektdatei, die Vertexbuffer und weitere Elemente. struct meinvertex{ D3DXVECTOR3 Position; D3DXVECTOR3 Normal; D3DXVECTOR2 Texture; }; // Position des Vertex // Normalenvektor des Vertex // Texturkoordinate des Vertex LPD3DXMESH meshSource; // Anfangs-Mesh LPD3DXMESH meshTarget; // Ziel-Mesh LPD3DXEFFECT effect; // Effekt (fx-Datei) IDirect3DVertexBuffer9* vertexBuffer[2]; // Vertexbuffer IDirect3DIndexBuffer9* indexBuffer; // Indexbuffer IDirect3DVertexDeclaration9* declaration; // Vertex-Deklaration DWORD numIndices; // Anzahl Indizes DWORD numVertices; // Anzahl Vertices 4-59 Quell-Code zur Definition einer Vertexstruktur und Variablen. Im nachfolgenden Ausschnitt ist die Vertexdeklaration aufgeführt, die zur Spezifikation der Streams dient und die der Vertexshader als Input erwatet. Dabei ist zu erkennen, dass jeweils zwei Vertices S e i t e | XL Charakter-Animation Eine Gegenüberstellung unter DirectX und OpenGL WS 08/09 übergeben werden. Dabei ist dessen Position, Normale und Texturkoordinate der ersten Texturebene enthalten. Es könnten an dieser Stelle auch mehrere Ebenen vorhanden sein, wenn man Multitexturing verwendet. D3DVERTEXELEMENT9 decl[] = { { 0, 0, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_POSITION, 0 }, // { 0, 12, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_NORMAL, 0 }, // { 0, 24, D3DDECLTYPE_FLOAT2, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_TEXCOORD, 0 }, // { 1, 0, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_POSITION, 1 }, // { 1, 12, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_NORMAL, 1 }, // { 1, 24, D3DDECLTYPE_FLOAT2, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_TEXCOORD, 1 }, D3DDECL_END() // Definiert das Ende der Deklaration. }; if(device->CreateVertexDeclaration(decl, &declaration) != D3D_OK) return 0; 4-60 Quell-Code zur Definition einer Vertexdeklaration. Nach dieser Definition wird der Mesh geladen, wobei dies grundlegend dem entspricht, was im Kapitel 4.2.3 ausgeführt wird. Um sicher zu gehen, dass das FVF des Meshs dem benötigten Format entspricht, wird auch hier ein temporärer Mesh erstellt, der exakt diesem Format entspricht und dabei die Daten des originalen Meshs enthält. Nach diesem Schritt wird der originale Mesh verworfen und der Zeiger umgebogen, wodurch er das originale Objekt enthält und dem gewünschten Format entspricht. Da im Rest der Anwendung der Mesh an sich nicht benötigt wird, sondern nur dessen Vertex und Indexbuffer, werden diese ausgelsen und in entsprachende Variablen kopiert. meshTarget->GetIndexBuffer(&indexBuffer); device->CreateVertexBuffer(ddsdDescSrcVB.Size, D3DUSAGE_WRITEONLY, 0, D3DPOOL_MANAGED, &vertexBuffer[1], 0); vertexBuffer[1]->Lock(0, ddsdDescDolphinVB.Size, (void **)&pDest, D3DLOCK_NOSYSLOCK); pSrcVB->Lock(0, ddsdDescSrcVB.Size, (void **)&pSrc, D3DLOCK_NOSYSLOCK); memcpy(pDest, pSrc, ddsdDescSrcVB.Size); pSrcVB->Unlock(); vertexBuffer[1]->Unlock(); 4-61 Quell-Code zum Extrahieren der Vertexdaten der Meshs. Nach diesen Schritten ist alles zum Rendern vorbereitet. In der Renderfunktion muss an der entsprechenden Stelle der Effekt noch aktiviert und zuvor dessen Konstanten gesetzt sowie die Streams zugewiesen werden. Die Funktionen hierzu dürften an dieser Stelle nach der Einführung in HLSL selbsterklärend sein. device->SetStreamSource(0, vertexBuffer[0], 0, sizeof(meinvertex)); device->SetStreamSource(1, vertexBuffer[1], 0, sizeof(meinvertex)); device->SetVertexDeclaration(declaration); device->SetIndices(indexBuffer); effect->SetTechnique("morph"); effect->SetTexture("ps_textur", texturen[0]); effect->SetFloat("vs_weightTarget", curMorphTime/3.0f); effect->SetFloat("vs_weightSource", (1.0f - curMorphTime/3.0f)); effect->SetMatrix("vs_worldtrans", &world); effect->SetVector("vs_LightDir", &lightEye2); effect->SetVector("vs_LightAmbient", &ambient); 4-62 Quell-Code zum Setzen der Vertexdaten-Streams und Indices. S e i t e | XLI Charakter-Animation Eine Gegenüberstellung unter DirectX und OpenGL WS 08/09 Zum Rendern des Objekts müssen anschließend die Passes des gewählten Effekts durchlaufen werden. Dieses vorgehen im Ausschnitt dargestellt. Dabei wurden zuvor die Vertex- und Indexdaten des Objekts dem Device zugewiesen. Anhand dieser Daten lässt sich durch die Methode DrawIndexedPrimitive das Objekt als Dreiecksliste zeichnen. effect->SetTechnique("morph"); effect->SetTexture("ps_textur", texturen[0]); effect->SetFloat("vs_weightTarget", curMorphTime/3.0f); effect->SetFloat("vs_weightSource", (1.0f - curMorphTime/3.0f)); effect->SetMatrix("vs_worldtrans", &world); effect->SetVector("vs_LightDir", &lightEye2); effect->SetVector("vs_LightAmbient", &ambient); 4-63 Quell-Code zum Setzen der Eigenschaften eines Effekts. UINT uPasses; if (D3D_OK == effect->Begin(&uPasses, 0)) { for (UINT uPass = 0; uPass < uPasses; uPass++) { effect->BeginPass(uPass); device->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, 0, 0, numVertices, 0, numIndices/3); effect->EndPass(); } effect->End(); } 4-64 Quell-Code zum Rendern mit einer Effekt-Datei. Nachdem der Teil der Anwendung nun ausführlich erläutert wurde, folgt die Erklärung des Shaders an sich. Die Input-Struktur entspricht dabei der Vertexdeclaration aus 4.26. Den Shader-Code sollten Sie nach der Einführung ohne Erläuterung verstehen, die Berechnungen werden jedoch erläutert. In der Main-Methode des Vertex-Shaders wird zuerst eine Instanz der Output-Datenstruktur erstellt, welche als Input-Datenstruktur für den anschließenden Pixelshader dient, der hier nicht aufgeführt ist. Der erste Schritt der Berechnungen ist die lineare Interpolation zwischen der Vertexposition des ersten und zweiten Meshs. Im zweiten Schritt wird die selbe Berechnung auf die Normalen angewendet, dies ist notwenig, da die anschließende Licht und Schattenberechnung darauf basiert. Bei Interesse an Licht und Schattenberechnungen ist (20) zu empfehlen. Der letzte Schritt, der hier dargestellt wird, ist die Transformation der Vertexposition. Der Vertex wird demnach an die korrekte Position im Koordinatensystem verschoben. Wie Sie sehen, ist es die selbe Berechnung, die sonst auf CPU-Basis durchgeführt wurde. Hier hat man jedoch den Vorteil, dass man kein separates Objekt benötigt, in dem die jeweils aktuelle Position gespeichert werden muss. Das Verfahren funktioniert demnach „on the fly“. struct VS_INPUT{ float4 pos : POSITION; float3 norm : NORMAL; float2 tex : TEXCOORD0; float4 pos2 : POSITION1; float3 norm2 : NORMAL1; float2 tex2 : TEXCOORD1; }; struct VS_OUTPUT{ float4 transpos : POSITION; float2 tex : TEXCOORD0; float2 tex1 : TEXCOORD1; float4 color : COLOR0; } 4-65 Vertexshader Datenstrukturen // // // // // // Position des ersten Meshs Normalenvektor des ersten Meshs Texturkoordinate des ersten Meshs Position des zweiten Meshs Normalenvektor des zweiten Meshs Texturkoordinate des zweiten Meshs // // // // Endposition des Vertex Texturkoordinate Ebene 0 Texturkoordinate Ebene 1 Farbwert des Vertex für Tweening. VS_OUTPUT vs_main( VS_INPUT inp ){ VS_OUTPUT outp; S e i t e | XLII Charakter-Animation Eine Gegenüberstellung unter DirectX und OpenGL WS 08/09 // Interpolation float4 interpolatedPos = inp.pos * vs_weightSource + inp.pos2 * vs_weightTarget; inp.norm = inp.norm * vs_weightSource + inp.norm2 * vs_weightTarget; // Transformation outp.transpos = mul(interpolatedPos, vs_worldtrans); // Licht und Schattenberechnungen… return outp; } technique morph{ pass P0{ VertexShader = compile vs_2_0 vs_main(); // PixelShader = compile ps_2_0 ps_main(); // } } 4-66 Ausschnitt des Quell-Codes der Effektdatei zum Tweening. Nach diesen Vorüberlegungen und Implementierungen sind Sie in der Lage eine Animation durch Tweening zwischen zwei Meshs auf Basis der CPU und Basis eines HLSL Shaders durchzuführen. Da eine Animation aber selten zwischen nur zwei Formen ausgeführt werden soll, gilt es im nächsten Schritt die Key-Frames der Animationen einer X-Datei auszulesen. 4.2.7. Repräsentation der Animationen in einer X-Datei In diesem Kapitel wird beschrieben, wie und warum Key-Frames verwendet werden, worin der Vorteil im Vergleich zur Verwendung separater Meshs liegt und wie diese in einer X-Datei repräsentiert werden. Die nachfolgende Abbildung zeigt ein Objekt, dass im Laufe der Zeit verformt wird. Dabei entspricht jede Form einem Key-Frame. Bisher wurden immer zwei Meshs in die Anwendung geladen. Einer wurde Source und der andere Target genannt. Es war also ohne weiteres nicht möglich zwischen mehr als zwei Formen zu interpolieren. Aus diesem Grund wird in diesem Kapitel sowohl die interne Darstellung als auch der Zugriff auf Animationen und Key-Frames beschrieben. Durch den Zugriff auf diese Informationen lässt sich jeweils zwischen zwei Key-Frames interpolieren und somit eine beliebig komplexe Animation rendern. Um den Hintergrund aber verstehen zu können, wird an dieser Stelle noch etwas tiefgehender auf das X-Format eingegangen. Von besonderer Bedeutung sind hierbei die Templates Animation, AnimationSet und AnimationKey. template Animation { 4-67 Skizze zur Veranschaulichung von Key-Frames. <3d82ab4f-62da-11cf-ab39-0020af71e433> [...] } 4-68 Definition des X-File Templates Animation. Quelle: (21) Target Source Target Source S e i t e | XLIII T Charakter-Animation Eine Gegenüberstellung unter DirectX und OpenGL WS 08/09 Das Animation-Template enthält Animationen in Bezug auf einen vorherigen Frame. Dabei kann es neben der Referenz zum vorherigen Frame auch ein Set von AnimationKey-Objekten oder auch AnimationOptions-Objekte enthalten. template AnimationKey { <10dd46a8-775b-11cf-8f52-0040333594a3> DWORD keyType; DWORD nKeys; array TimedFloatKeys keys[nKeys]; } template TimedFloatKeys { < F406B180-7B3B-11cf-8F52-0040333594A3 > DWORD time; FloatKeys tfkeys; } 4-69 Definition der X-File Templates AnimationKey und TimedFloatKeys. Quelle: (21) Das AnimationKey-Template definiert ein Set von TimedFloatKeys, wobei dieses Template ein Set von float-Variablen definiert, die die Transformation beschreiben und einen Zeitstempel, um die Animation zu steuern. Des Weiteren definiert der keyType des Templates AnimationKey ob es sich bei den Transformationsdaten um Matrizen oder um separate Werte für Rotation, Skalierung und Position handelt. An dieser Stelle sei auf frühere Betrachtungen über Quaternionen und Interpolation verwiesen. template AnimationSet { < 3D82AB50-62DA-11cf-AB39-0020AF71E433 > [ Animation < 3D82AB4F-62DA-11cf-AB39-0020AF71E433 > ] } 4-70 Definition der X-File Templates AnimationSet. Quelle: (21) Das AnimationSet-Template fügt letzen Endes eine oder mehr Animation-Objekte zusammen, wobei alle Animationen die gleiche Zeitdauer besitzen. Im Anschluss an diese Betrachtung der Repräsentation der Animationen innerhalb der X-Datei folgt eine Beschreibung der Repräsentation innerhalb einer DirectX-Anwendung. Dabei sind zwei Datenstrukturen von großer Bedeutung und zwar die Strukturen D3DXFRAME und D3DXMESHCONTAINER. typedef struct D3DXFRAME { LPSTR Name; D3DXMATRIX TransformationMatrix; LPD3DXMESHCONTAINER pMeshContainer; D3DXFRAME * pFrameSibling; D3DXFRAME * pFrameFirstChild; } D3DXFRAME, *LPD3DXFRAME; typedef struct D3DXMESHCONTAINER { LPSTR Name; D3DXMESHDATA MeshData; LPD3DXMATERIAL pMaterials; LPD3DXEFFECTINSTANCE pEffects; DWORD NumMaterials; DWORD * pAdjacency; LPD3DXSKININFO pSkinInfo; D3DXMESHCONTAINER * pNextMeshContainer; } D3DXMESHCONTAINER, *LPD3DXMESHCONTAINE; 4-71 Definition der Structs D3DXFRAME und D3DXMESHCONTAINER. S e i t e | XLIV Charakter-Animation Eine Gegenüberstellung unter DirectX und OpenGL WS 08/09 Des Weiteren gibt es in DirectX Graphics den AnimationController, mit dem man beispielsweise eine einfache Animation ausführen oder auch Überblendungen zwischen zwei Animationen auf einfache Weise realisieren kann. Dadurch erleichtert sich die Berechnung ungemein. Außerdem kann man durch eine Instanz des Animation Controllers, die beim Laden eines Meshs mit der Funktion D3DXLoadMeshHierarchyFromX(…) erstellt wird, auf die in der X-Datei enthaltenen Animationen zugreifen. HRESULT D3DXLoadMeshHierarchyFromX( LPCSTR Filename, DWORD MeshOptions, LPDIRECT3DDEVICE9 pDevice, LPD3DXALLOCATEHIERARCHY pAlloc, LPD3DXLOADUSERDATA pUserDataLoader, LPD3DXFRAME* ppFrameHierarchy, LPD3DXANIMATIONCONTROLLER* ppAnimController ); 4-72 Definition der Funktion D3DXLoadMeshHierarchyFromX(…). Quelle: (21) Durch die oberhalb beschriebene Technik lässt sich jedoch nur Skinning realisieren. Das X-Format unterstützt standardmäßig kein Tweening. Allerdings wurde im Kapitel 2.3.1. - Das X-Format beschrieben, dass es erweiterbar ist. Dazu werden zwei neue Templates erstellt. template MorphAnimationKey { <4E50C940-3978-4428-A19E-7FE8CDAA451A> DWORD Time; // Zeit des Key-Frames. CSTRING MeshName; // Name des dazugehörigen Meshs. } template MorphAnimationSet { <8E3C96D4-91DD-4d64-AEBC-9E83950CDC67> DWORD NumKeys; // Anzahl der Key-Frames. array MorphAnimationKey Keys[NumKeys]; // Array der einzelnen Key-Frames. } 4-73 Erweiterung des X-Dateiformats durch Morphing. Auf Basis von Quelle (3). Wobei das Template MorphAnimationKey eine Art Key-Frame darstellt und somit eine Variable für die Zeit und eine Variable für den zugehörigen Mesh enthält. Die X-Datei enthält demnach mehrere Meshs, die durch Klonen entstanden sein können. Das Template MorphAnimationSet fasst die Keys zusammen, wodurch eine Liste entsteht, in der die aufeinanderfolgenden Tweening-Schritte (Zeit und Mesh) repräsentiert werden. Man kann natürlich auf solch eine Erweiterung verzichten. Möchte man aber ohne solch eine Erweiterung arbeiten, so wird man die einzelnen Meshs in der Anwendung auslesen müssen und kann diese Informationen nicht vernünftig kapseln. Soviel zur benötigten Theorie und dem praktischen Hintergrund, der für die weiteren Schritte benötigt wird. Im nächsten Kapitel wird die angesprochene Erweiterung implementiert, wobei die hier beschriebenen Grundlagen verwendet werden. 4.2.8. Erweiterung des X-Dateiformats für Morphing und Key-Frames Im vorherigen Kapitel wurden zwei neue Templates definiert. Um die dazugehörigen GUIDs zu erstellen, verwendet man das Tool GUID-Generator. Der Algorithmus, der an dieser Stelle für das Morphing verwendet wird, basiert auf der CPU-basierten Methode. Es wurden auf Grund teilweiser unterschiedlicher Datenstrukturen jedoch kleine Änderungen vorgenommen. Des Weiteren ist an dieser Stelle eine Berechnung notwendig, um die aktuellen Key-Frames und die dazugehörigen Meshs zu ermitteln. Wobei auch hier die Animation in einer Endlosschleife abläuft. S e i t e | XLV Charakter-Animation Eine Gegenüberstellung unter DirectX und OpenGL WS 08/09 Wie schon angemerkt, wird zum Laden einer X-Datei mit benutzerdefinierten Templates die Funktion D3DXLoadMeshHierarchyFromX verwendet. Hierbei gilt es ähnlich wie bei der Klasse AllocateHierarchy eine Klasse zu implementieren, die die vorhandenen Methoden überschreibt. In diesem Fall handelt es sich um die Klasse LoadUserData. Wobei nur die Methode LoadTopLevelData von Bedeutung ist. Bevor man allerdings auf die gewünschten Daten zugreifen kann, sind ein paar Schritte abzuarbeiten. Man muss mit dem Makro DEFINE_GUID die GUIDs in der Anwendung registrieren, womit man ihnen auch einen eindeutigen Namen zuteilt. // {4E50C940-3978-4428-A19E-7FE8CDAA451A} DEFINE_GUID(GUID_MORPHANIMATIONKEY, 0x4e50c940, 0x3978, 0x4428, 0xa1, 0x9e, 0x7f, 0xe8, 0xcd, 0xaa, 0x45, 0x1a); // {8E3C96D4-91DD-4d64-AEBC-9E83950CDC67} DEFINE_GUID(GUID_MORPHANIMATIONSET, 0x8e3c96d4, 0x91dd, 0x4d64, 0xae, 0xbc, 0x9e, 0x83, 0x95, 0xc, 0xdc, 0x67); 4-74 Definition der GUIDs mit dem DEFINE_GUID Makro. Außerdem müssen passende structs zu den Templatedefinitionen erstellt werden. Wobei gleichermaßen auf die Datentypen und auf die korrekte Reihenfolge der Variablen zu achten ist. struct MorphAnimationKey{ DWORD Time; // Zeitpunkt, ab dem dieser Key-Frame aktuell ist. char* MeshName; // Name des Meshs, der zu diesem Key-Frame gehört. }; struct MorphAnimationSet{ DWORD NumKeys; // Anzahl der Key-Frames. MorphAnimationKey Keys[255]; // Array der Key-Frames (Maximal 255). char* name; // Name der Animation. }; 4-75 Definition der structs zu dem Templates der X-Datei. Übergibt man der Funktion D3DXLoadMeshHierarchyFromX nun zusätzlich zu einem Objekt der Klasse AllocateHierarchy ein Objekt der Klasse LoadUserData, so wird die Funktion LoadTopLevelData aufgerufen, so bald der Parser auf das Data-Object des Templates MorphedAnimationSet trifft. HRESULT LoadUserData::LoadTopLevelData(LPD3DXFILEDATA pXofChildData){} 4-76 Funktionsdefinition LoadTopLevelData der Klasse LoadUserData. Um die structs mit den Daten der X-Datei zu füllen, muss sicherheitshalber überprüft werden um welches Template (GUID) es sich handlet. Dazu bietet das Interface ID3DXFILEDATA die Funktion GetType an. Vergleicht man die dadurch erhaltene GUID mit der zuvor definierten GUID MORPHANIMATIONSET, so lässt sich feststellen ob es sich an dieser Stelle um das gesuchte Data-Object handelt. Im nächsten Schritt werden die Daten der X-Datei geparst und in die zuvor definierten structs gespeichert. Um dies zu realisieren hat das Interface ID3DXFILEDATA die Funktion Lock. Übergibt man dieser als Parameter das zuvor erstellte struct, so werden bei erfolgreicher Ausführung die Daten darin gespeichert. Danach gilt es die structs auszulesen und neue Datenstrukturen zu erzeugen (TweeningAnimationSet, TweeningAnimatioonKey), die in der Anwendung dauerhaft gespeichert und zur Animation verwendet werden. Nach dem S e i t e | XLVI Charakter-Animation Eine Gegenüberstellung unter DirectX und OpenGL WS 08/09 die Daten an die entsprechenden Positionen kopiert wurden, gilt es durch die Funktion Unlock das Interface wieder freizugeben. Nachdem in der Klasse LoadUserData die benutzerdefinierten Templates geladen und in der Klasse AllocataHierarchy alle Meshs ausgelesen wurden, gilt es diese Daten zusammenzuführen. Die Klasse TweeningAnimator ist dabei an das in der D3DX Bibliothek integrierte Interface ID3DXAnimationController angenährt – befindet sich jedoch noch in einem frühstadium und bietet daher nur grundlegende Methoden zum hinzufügen einer TweeningAnimationSets sowie den Zugriff auf eine public Variable mit den Meshs sowie eine Methode zum Tweening mit der CPU an. 4.2.9. Charakter-Animation durch Skinning In diesem Kapitel wird sowohl beschrieben wie man eine X-Datei inklusiver ihrer Hierarchien, Bones und Animationen lädt als auch der Algorithmus zum Bewegen und Rendern des Charakters. Dabei wird, um den Umfang dieser Arbeit nicht zu sehr zu sprengen, weniger Code dargestellt. Bitte sehen Sie sich den passenden Code der Funktionen in der C++ Datei an. 4.2.9.1. Laden des Meshs und Aufbau der benötigten Datenstrukturen Im vorherigen Kapitel wurde die Funktion D3DXLoadMeshHierarchyFromX in der Abbildung 4.36 aufgeführt. Diese unterscheidet sich von der zuvor verwendeten Funktion zum Laden einfacher 3DObjekte und nimmt unter anderem Parameter für einen AnimationController und ein Zeiger auf ein Objekt der Klasse AllocateHierarchy an. Bei diesem Parameter handelt es sich eigentlich um eine Instanz einer eigenen Klasse, die von AllocateHierarchy abgeleitet ist. Diese Klasse, mit dem Namen CAllocateHierarchy, überschreibt vier Funktionen. Die zum Laden und Zerstören der Objekte dienen, wobei sie automatisch von DirectX aufgerufen werden. Ein weiterer Parameter ist ppFrameHierarchy. Das ist ein Zeiger auf das Root-Element der Mesh-Hierarchie, wodurch die Möglichkeit besteht die gesamte Baumstruktur zu traversieren. Das Ziel ist dabei Datenstrukturen aufzubauen, die ein Navigieren zwischen den Hierarchien erlauben, was durch die schon beschriebenen Strukturen D3DXFRAME und D3DXMESHCONTAINER gegeben ist. Auf der anderen Seite werden aus performancegründen diese Strukturen erweitert und Arrays erstellt, wodurch ein schneller zeigerbasierter Zugriff auf einzelne Elemente wie beispielsweise die Transformationsmatrix eines Bones ermöglicht wird. Dieses Vorgehen wird in den restlichen Abschnitten dieses Kapitels näher erklärt. Die Methode CreateFrame der Struktur CAllocateHierarchy wird während des Parsens von DirectX aufgerufen und dient dazu die Member-Variablen des Frames (D3DXFRAME_DERIVED) zu initialisieren. Im Anschluss daran wird die Methode CreateMeshContainer (Abbildung 4.39) aufgerufen. Wie anhand der Parameter zu sehen ist, sind hier alle Informationen über den Mesh enthalten. Es gilt demnach an dieser Stelle sowohl den normalen Mesh inklusive dessen Vertexdaten auszulesen als auch die Texturen und Skinning-Informationen. HRESULT CreateMeshContainer( LPCSTR Name, CONST D3DXMESHDATA * pMeshData, CONST D3DXMATERIAL * pMaterials, CONST D3DXEFFECTINSTANCE * pEffectInstances, DWORD NumMaterials, S e i t e | XLVII Charakter-Animation Eine Gegenüberstellung unter DirectX und OpenGL WS 08/09 CONST DWORD * pAdjacency, LPD3DXSKININFO pSkinInfo, LPD3DXMESHCONTAINER * ppNewMeshContainer ); 4-77 Beschreibung der Funktion CreateMeshContainer des Interfaces ID3DXAllocateHierarchy Quelle: (21) Wie auch zuvor, wird hier zuerst das FVF überprüft und wenn nötig angepasst. Beispielsweise indem die Normalen berechnet und hinzugefügt werden. Im Anschluss daran werden die Variablen der Struktur D3DXFRAME_DERIVED initialisiert und mit passenden Werten belegt. Beispielsweise werden an dieser Stelle die Materialien ausgelesen, geladen und in der Struktur gespeichert. Wenn der Parameter pSkinInfo nicht Null ist, dann sind Skinning-Informationen enthalten, welche es auszuwerten gilt. Dazu werden die Anzahl der Bones ermittelt und in einer Schleife die OffsetTransformationsmatrizen der Struktur D3DXFRAME_DERIVED berechnet. Im Anschluss daran wird die Funktion GenerateSkinnedMesh aufgerufen, die im nächsten Abschnitt beschrieben wird. Diese Funktion dient hauptsächlich dazu, den Mesh beziehungsweise dessen FVF entsprechend anzupassen. Die Funktion ConvertToIndexedBlendedMesh vom Interface ID3DXSkinInfo nimmt einen Mesh und weitere Daten, die in der Struktur D3DXFRAME_DERIVED gespeichert sind entgegen und gibt einen Mesh zurück, der blend weights, Indizes und eine Bone KombinationsTabelle enthält. Diese Tabelle beschreibt welche Bones welche Subsets des Meshs beschreiben. Da der Vertexshader aber eine Vertex-Deklaration benötigt und kein FVF, muss diese anschließend erstellt werden. Dabei ist darauf zu achten, dass sie der Input-Datenstruktur des Vertexshaders entspricht. Im letzten Schritt dieser Funktion werden die Bone-Matrizen noch in einer globalen Variable gespeichert, die später dem Shader übergeben wird. 4.2.9.2. Animation des Charakters Zur Animation wird der AnimationController verwendet, der beim erfolgreichen Laden des Meshs erstellt wird (siehe 4.37). Da in diesem Projekt der Charakter nur eine Animation besitzt, kann auf eine detailliertere Beschreibung verzichtet werden. Zur Überblendung mehrerer Animation sehen Sie bitte in der Dokumentation (21) nach. Wie auch zuvor gilt es in dieser Animation die Zeit seit Anwendungsstart zu messen. Übergibt man diesen Wert der Funktion AdvancedTime der AnimationController-Instanz, so berechnet diese die aktuelle Animation. Gibt man dabei keinen Callback-Handler an, so wird die Animation in einer Endlosschleife wiederholt. Im nächsten Schritt müssen die kombinierten Transformationsmatrizen neu berechnet werden, die wie im vorherigen Kapitel beschrieben aus Performancegründen angelegt werden. Da sowohl ein Zeiger auf das Root-Element vorhanden ist, als auch der AnimationController den aktuellen Stand berechnet hat, muss die Baumstruktur durchlaufen werden. Dabei wird in der rekursiven Funktion UpdateFrameMatrices die Struktur zuerst in der Tiefe durchlaufen und die jeweilige kombinierte Transformationsmatrix entsprechend den Formeln aus dem Kapitel 3.2.2.1. berechnet. 4.2.9.3. Rendern des Charakters mit einem HLSL Vertexshader Beim Rendern wird an dieser Stelle gleich die Shader-basierte Methode vorgestellt, auf eine CPUbasierte Methode wird aus Performancegründen verzichtet, da sie in der Praxis kaum eine Rolle spielt. Wobei der später dargestellte Ausschnitt aus einer Effektdatei sich auf den relevanten Teil, der das Skinning realisiert, beschränkt. Bevor der Render-Vorgang beginnt, müssen die benötigten Konstanten gesetzt werden. Wichtig sind dabei vor allem die Transformationsmatrizen. Diese werden zuvor in einer Schleife jedoch noch so manipuliert, dass dessen Offset mit einbezogen wird. Im Anschluss daran, wenn auch alle anderen Konstanten, wie Texturen oder Lichter, gesetzt wurden, wird der Mesh gerendert. Dazu wird wie im Ausschnitt 4.29 vorgegangen. S e i t e | XLVIII Charakter-Animation Eine Gegenüberstellung unter DirectX und OpenGL WS 08/09 Die Input-Datenstruktur für den Vertexshader ist hier grundverschieden im Vergleich zu derer des Shaders zum Tweening. Wie zuvor beschrieben, enthält der Mesh neben seinem Positionsvektor, Normalenvektor und seinen Texturkoordinaten diesmal auch Informationen über die Gewichtungen der Bones in Bezug zum jeweiligen Vertex. Diese Informationen sind anhand der Schlüsselwörter den Variablen zugewiesen, wobei die Schlüsselwörter selbsterklärend sind. Um bei Interesse nachzulesen, können Sie sich auf (21) über die Semantics informieren. Außerdem wird nur ein Stream zugewiesen, da keine zwei Objekte gleichzeitig benötigt werden. Die zugehörigen Matrizen zur Berechnung der Transformationen werden als Konstante übergeben, in diesem Fall ist es die Konstante mWorldMatrixArray, in der die passenden Matrizen entsprechend der Bones enthalten sind. static const int MAX_MATRICES = 26; float4x3 mWorldMatrixArray[MAX_MATRICES] : WORLDMATRIXARRAY; float4x4 mViewProj : VIEWPROJECTION; struct VS_INPUT{ float4 Pos : POSITION; float4 BlendWeights : BLENDWEIGHT; float4 BlendIndices : BLENDINDICES; float3 Normal : NORMAL; float3 Tex0 : TEXCOORD0; }; // // // // // Position des Vertex Gewichte Indices Normalenvektor Texturkoordinaten struct VS_OUTPUT{ float4 Pos : POSITION; // Transformierte Position float4 Diffuse : COLOR; // Farbwert float2 Tex0 : TEXCOORD0; // Texturkoordinaten }; 4-78 Konstanten und Datenstrukturen des Skinning-Vertexshaders. Quelle: (21) Nachdem die eingehenden Datenstrukturen erläutert wurden, folgt die Berechnung. Dabei liegen die im theoretischen Kapitel ausgearbeiteten Formeln zugrunde, um die Vertices entsprechend zu manipulieren. Am Anfang der Berechnung wird eine Ausgangsdatenstruktur erstellt, in der alle Transformierten Daten gespeichert werden. Danach werden mehrere Variablen erstellt, die als zwischenspeicher für die Berechnungen dienen. Des Weiteren werden die BlendIndices sicherheitshalber in UBYTE4 gewandelt, da sonst ein Fehler in Verbindung mit der Verwendung einer Geforce 3 Grafikkarte auftreten kann. Im Anschluss daran werden noch die Arrays, die die Gewichte und dazugehörigen Indices enthalten so umgewandelt, dass später leicht darauf zugegriffen werden kann. Dieser Cast wird demnach nur an einer Stelle durchgeführt, was Performancevorteile besitzt. In der darauffolgenden Schleife wird die Position und Normale des Vertex transformiert. Das ganze wurde deshalb in einer Schleife implementiert, um leichter erweiterbar zu sein, wenn beispielsweise jeder Vertex von vier Bones beeinflusst wird. Außerdem wird in der Schleife das Gewicht, den letzten Bones berechnet. Das Ergebnis hieraus berechnet sich folgendermaßen. Wobei n gleich der Anzahl der beeinflussenden Bones ist, w für weight (Gewicht) steht und somit die Formel die Akkumulation innerhalb der Schleife repräsentiert. Nach der Berechnung dieses Gewichtes wird der entsprechende Bone hierdurch in Verbindung mit der Vertex-Position und –Normale transformiert. Der letzte Schritt ist dann noch die Transformation der Position vom WorldKoordinatensystem in das Projektions-Koordinatensystem. VS_OUTPUT VShade(VS_INPUT i, uniform int NumBones) { VS_OUTPUT o; // Output-Datenstruktur S e i t e | XLIX Charakter-Animation Eine Gegenüberstellung unter DirectX und OpenGL WS 08/09 float3 Pos = 0.0f; // Output-Position float3 Normal = 0.0f; // Output-Normalenvektor float LastWeight = 0.0f; // letztes Gewicht // Kompensiert den Fehler der Geforce3 bei UBYTE4 Variablen. int4 IndexVector = D3DCOLORtoUBYTE4(i.BlendIndices); // Arrays in Vektoren casten, um sie nachfolgend zur Berechnung zu // verwenden. float BlendWeightsArray[4] = (float[4])i.BlendWeights; int IndexArray[4] = (int[4])IndexVector; // Berechnung der Position/Normale mittels der Gewichte und // Akkumulation der Gewichte. for (int iBone = 0; iBone < NumBones-1; iBone++){ LastWeight = LastWeight + BlendWeightsArray[iBone]; Pos += mul(i.Pos, mWorldMatrixArray[IndexArray[iBone]]) * BlendWeightsArray[iBone]; Normal += mul(i.Normal, mWorldMatrixArray[IndexArray[iBone]]) * BlendWeightsArray[iBone]; } // Berechnung des resultierenden Gewichtes für den letzen Bone. LastWeight = 1.0f - LastWeight; // Berechne die letzte Transformation für Position und Normale. Pos += (mul(i.Pos, mWorldMatrixArray[IndexArray[NumBones-1]]) * LastWeight); Normal += (mul(i.Normal, mWorldMatrixArray[IndexArray[NumBones-1]]) * LastWeight); // Transformiere den vertex vom World-Koordinatensystem in das // Projektions-Koordinatensystem. o.Pos = mul(float4(Pos.xyz, 1.0f), mViewProj); // Weitere Berechnungen hier nicht aufgeführt. return o; // Output-Datenstruktur zurückgeben } 4-79 Quell-Code der Skinning-Vertexsahder. Quelle: (21) Um das Skinning abzuschließen ist jedoch bei zwei Bones je Vertex ein zweiter Durchlauf dieser Vertexshader-Methode erforderlich. Dabei besitzt die Variable NumBones im ersten Durchlauf den Wert eins und im zweiten Durchlauf den Wert zwei. Bei näherer Betrachtung dieses Vorgehens im Vergleich zur zugrunde liegenden Theorie macht dies Sinn. So wird zuerst der erste Bone auf den Vertex angewendet und dessen Koordinate entsprechend der korrekten neuen Position transformiert und im Anschluss daran folgt die gleiche Berechnung anhand des zweiten Bones. Hiermit ist das Skinning und somit der DirectX-Teil zur Implementierung abgeschlossen. In den nachfolgenden Kapiteln wird auf Basis von OpenGL ein Charakter animiert. Seite |L Charakter-Animation Eine Gegenüberstellung unter DirectX und OpenGL WS 08/09 4.3. Implementierung der OpenGL-Anwendungen 4.3.1. Laden der 3D Objekte Unabhängig davon, ob die Animation mit der normalen OpenGL Renderingpipeline oder auf Basis von GLSL Shadern durchgeführt wird, müssen zuerst die Daten der Objekte in die Anwendung geladen werden. Dazu wird das Programm von http://www.spacesimulator.net/tut5_vectors_and_lighting.html verwendet. Es ist gut dokumentiert und einfach zu handhaben, da der Quellcode kompakt ist und sich darauf konzentriert, die Grundelemente zur Darstellung eines 3DS Objekts (Vertices, Polygone) zu laden und zu speichern. Zudem werden die Normalen eines Objekts berechnet, was später für die Implementierung von Licht- und Materialeigenschaften notwendig ist. Natürlich eignet sich der Loader nicht für komplexere Objekte mit Bones, Keyframes etc. Zur Erläuterung von Morphing reichen einfache Objekte jedoch aus. In diesem Kapitel soll deshalb die Funktionsweise des verwendeten 3DS Loaders erläutert werden. Zunächst ein Überblick über die Datenstruktur für die Objektdaten. Hierzu wird im Loader ein struct definiert, dass alle Vertices, Polygone, Normalen und die jeweilige Anzahl an Vertices und Polygonen speichert. Der folgende Codeausschnitt illustriert dies: Fehler: Referenz nicht gefunden 4-80 Datenstruktur eines Objekts in OpenGL Die Funktion Load3DS zum Laden der Objektdaten wird in der Datei load_3ds.c realisiert. Dabei wird ein Zeiger auf die obige Objektstruktur übergeben, sowie der Name der zu öffnenden Datei. Die Datei wird anschließend von Anfang bis Ende durchlaufen und in einer switch- case Anweisung jeweils die ID des aktuellen Chunks untersucht. Dazu muss sich der Dateizeiger immer an der richtigen Position befinden, also am Anfang eines Chunks. Zur Darstellung eines Objekts ist es ausreichend, alle Vertices und Polygone auszulesen. Anhand der 3DS Chunk Hierarchie in Abschnitt 2.3.2 lässt sich erkennen, welche Chunks relevant sind, nämlich „VERTICES LIST“ (Chunk ID 0x4110) für alle Vertexdaten und „FACES DESCRIPTION“ (Chunk ID 0x4120) für die Polygone. Alle nicht benötigten Chunks werden übersprungen, indem der Dateizeiger anhand der Chunk Länge weiterbewegt wird. Aufgrund der hierarchischen Struktur der 3DS Datei müssen die übergeordneten Elemente (Main Chunk“, „3D Editor Chunk“, „Object Block“, sowie „Triangular Mesh”)mitgelesen werden, selbst wenn diese keine relevanten Daten enthalten. Wie bereits erwähnt, ist die Länge aller Kind Elemente in einem Vater Element mit enthalten Würde man etwa den Main Chunk einfach überspringen, indem der Dateizeiger anhand der Länge des Main Chunks weiterbewegt wird, wäre bereits das Dateiende erreicht. Folglich liest der Loader zuerst den Main Chunk ein, der Dateizeiger wird weiterbewegt und befindet sich nun bei der ID des ersten Kind Elements, „3D Editor Chunk“. Das Verfahren wird fortgeführt, bis der Dateizeiger schließlich bei der ID 0x4110 ist. Als erstes wird nun die Gesamtzahl der Vertices des Objekts ermittelt, um damit die Größe des Feldes mit den Vertices zu erhalten. Anschließend werden in einer for Schleife alle Vertexpunkte in das struct Element vertex geschrieben: Fehler: Referenz nicht gefunden 4-81 Vertices der Datenstruktur hinzufügen Die Struktur zum Einlesen der Polygondaten ist identisch. Auch hier werden die Anzahl der Polygone des Objekts, sowie die Polygonkoordinaten eingelesen und gespeichert. Im default Zweig werden, wie S e i t e | LI Charakter-Animation Eine Gegenüberstellung unter DirectX und OpenGL WS 08/09 bereits erwähnt, alle nicht benötigten Chunks übersprungen. Damit hat man die benötigten Grundelemente der Objekte. Auf erweiterte Funktionen, wie etwa Texturen, wird hier verzichtet. Da der verwendete Loader nur die Grundelemente eines Objekts lädt gab es jedoch Probleme bei der Darstellung des Modells aus Kapitel 4.1, da dieses unter anderem biped Elemente verwendet. Deshalb wird für den OpenGL Teil auf ein anderes Modell zurückgegriffen. Ein menschlicher Kopf im 3DS Format, welcher unter http://www.3dvalley.com/models/models_bodyparts.shtml kostenlos heruntergeladen werden kann. Um nun eine zweite Objektposition zu erhalten, wurde das Objekt in Lightwave importiert, entsprechend neu positioniert (Rotation, Translation) und anschließend neu abgespeichert (als 3DS Export). Der Loader bietet standardmäßig nur die Funktion zum Laden eines Objekts an, deshalb ist noch eine weitere Funktion zum Laden des zweiten Objekts integriert worden. Zudem benötigt man nun natürlich 2 Elemente des structs obj_type. 4.3.2. Normalenberechnung des 3DS Loaders Damit das zu animierende Objekt nachher nicht flach, sondern dreidimensional erscheint, benötigt man Licht in der Szene. Zur korrekten Lichtberechnung werden alle Normalenvektoren eines Objekts benötigt. Das folgende Kapitel erläutert, wie der Loader die Normalen der Vertices und Polygone berechnet. Als Grundlage dient die Berechnung des Vektorprodukts von zwei Vektoren, die koplanar (Vektoren liegen in einer Ebene) zum jeweiligen Polygon des Objekts sind. Hierfür werden zwei Seiten von jedem Polygon herangezogen. Das Ergebnis wird anschließend noch normalisiert, das heißt auf die Einheitslänge 1 abgebildet. Zum Durchführen dieser Operation sind mehrere Schritte nötig: Zunächst wird in einer Schleife durch alle ermittelten Polygone gegangen. Jedes Polygon ist in diesem Fall ein Dreieck, so dass die drei Eckpunkte zunächst zwischengespeichert werden. Anschließend werden anhand von jeweils zwei Eckpunkten zwei koplanare Vektoren erstellt. Die Vektoren werden gebildet, indem die Koordinaten des zweiten Eckpunktes minus den Koordinaten des ersten Eckpunktes gerechnet werden. Anschließend wird das Vektorprodukt der zwei erzeugten Vektoren berechnet. Fehler: Referenz nicht gefunden 4-82 Berechnung der Normalen des Polygons Der daraus resultierende Vektor stellt die jeweilige Polygon Normale dar und wird nun normalisiert. Dazu berechnet man zuerst die Länge des Vektors und dividiert anschließend alle drei Komponenten (x,y,z) des Vektors durch dessen Länge. Fehler: Referenz nicht gefunden 4-83 Normalisieren eines Vektors Um nun die Normalen der Vertices zu berechnen, wird zuerst die Anzahl der Vertices bestimmt, die zu jedem Polygon gehören. Anschließend werden die Normalenwerte durch diese Anzahl dividiert: Fehler: Referenz nicht gefunden 4-84 Normalen Berechnung der Vertices (22) 4.3.3. Initialisierung von OpenGL und Glut Nachdem bisher auf die Funktionen des Loaders zugegriffen wurde, erläutern die folgenden Kapitel den selbst implementierten Teil der Arbeit. Zur Fensterverwaltung wird die GLUT Bibliothek eingesetzt. Deshalb müssen zunächst einige GLUT Funktionen als Callback angemeldet werden. Zum einen die display Funktion, die immer dann aufgerufen wird, wenn das Ausgabefenster neu gezeichnet S e i t e | LII Charakter-Animation Eine Gegenüberstellung unter DirectX und OpenGL WS 08/09 werden muss. Ändert der Benutzer die Größe des Ausgabefensters, so müssen das Viewing Volumen und der Viewport entsprechend angepasst werden. Hierfür wird die reshape Funktion registriert. Die dritte und letzte benötigte Callback Funktion ist die glutIdleFunc Funktion. Dieser wird als Parameter eine andere Funktion übergeben, die immer dann neu ausgeführt wird, wenn kein anderes Ereignis des Window Systems empfangen wird. Dadurch eignet sich die idle Funktion ideal zum Durchführen der Animation. Anstatt die display Funktion explizit in einer Endlosschleife auszuführen. reicht es aus, der idle Funktion die display Funktion zu übergeben. Hiermit wird die display Funktion zur Zeichnung der aktuellen Objektposition automatisch immer wieder neu aufgerufen. Außerdem wird unter anderem noch festgelegt, dass der Double Buffer und Z Buffer genutzt werden soll. Der Double Buffer sorgt dafür, dass die Animation nicht flackert. Während der eine Teil des Buffers (Front Buffer) das aktuelle Bild im Fenster darstellt, wird im zweiten Buffer(back Buffer) bereits das nächste Bild berechnet. Durch den Z Buffer kann ermittelt werden, welche Teile eines Objekts sichtbar sind und welche verdeckt werden. Die verdeckten Elemente werden dadurch nicht gezeichnet. Fehler: Referenz nicht gefunden 4-85 Main Funktion des Programms Für die OpenGL Initialisierung sind nur wenige Dinge nötig. Als erstes wird die Hintergrundfarbe des Ausgabefensters festgelegt. Anschließend werden die Licht-und Materialparameter spezifiziert. Diese wurden so festgelegt, dass ein gesichtsfarbener Lichteindruck entsteht, da innerhalb des Loaders keine Farbinformationen ausgelesen werden, was jedoch auch nicht notwendig ist. Als Position für die Kamera werden die Default Einstellungen verwendet, da sich die Objekte in der Nähe des Ursprungs befinden. In der display Funktion findet lediglich eine Translation der Objekte in negative Z Richtung statt. Zudem wird noch der Tiefentest für den Z Buffer aktiviert. Für detaillierte Informationen sei hier auf den entsprechenden Quellcode verwiesen. 4.3.4. Morphing mit OpenGL Die notwendigen Voraussetzungen zur Durchführung des Morphings sind nun erfüllt. Es werden 2 Objekte geladen und deren Vertices, Polygone, sowie die Normalen errechnet und gespeichert. Zudem wird Licht in der Szene aktiviert. Der erste Schritt für das Morphing besteht nun darin, die Zeit zu berechnen, die für einen Frame benötigt wurde(siehe Kapitel 3.1). Es gibt unter C mehrere Möglichleiten, Zeit zu messen. Nicht jede Methode ist jedoch effektiv. Zunächst wurde der Versuch unternommen, die Zeitmessung mittels der clock() Funktion aus time.h zu realisieren. Diese ermittelt die verbrauchte CPU Zeit eines Programms. Allerdings hat sich herausgestellt, dass die Funktion keine brauchbaren Werte liefert. Selbst nach Angabe von 8 Stellen hinter dem Komma, kommt bei der Zeitdifferenzmessung oft 0 heraus. Woran dies genau liegt, wurde nicht ersichtlich. Allerdings kann man vermuten, dass bei heutigen Rechnern mit 2 oder mehr Kernen für eine einfache Animation kaum CPU Zeit verbraucht wird. Deshalb wird zur Zeitmessung, analog zum DirectX Teil, ebenfalls auf den PerformanceTimer von windows.h zurückgegriffen. Die Implementierung unterscheidet sich kaum von der aus Kapitel 4.2.4 und soll deshalb nicht noch einmal erläutert werden. Wichtig zu wissen ist nur, dass die Zeitberechnung und Aktualisierung in der Display Funktion realisiert werden muss, da diese Funktion wiederholt aufgerufen wird und somit jeweils den aktuellen Animationsfaktor benötigt. Durch die Ausgabe der Zeitwerte konnte festgestellt werden, dass die Zeitmessung mittels PerformanceTimer zuverlässiger und genauer arbeitet als clock(). Die Animation wird nicht anhand von Keyframes, sondern durch Interpolation der Normalen und Vertices eines Ausgangsobjekts und eines Zielobjekts durchgeführt. Der zweite Schritt besteht deshalb darin, alle Vertices und Normalen der beiden Objekte auszulesen und pro Vertex/Normale den interpolierten Wert anhand der Formel aus Kapitel 3.2.2.2 zu errechnen. Da die 3DS Objekte als Menge von Polygonen mit jeweils 3 Eckpunkten in der Datenstruktur abgelegt sind, wird nun in einer for Schleife durch alle Polygone des ersten Objekts durchgegangen. Für das Morphing zweier Objekte, sollten diese die gleiche Anzahl an Vertices/Polygonen haben, deshalb bekommt man beim Durchlauf aller Polygone für ein Objekt auch alle Werte des zweiten Objekts. Dabei werden sowohl für die Normalen, als auch für die Vertices, die S e i t e | LIII Charakter-Animation Eine Gegenüberstellung unter DirectX und OpenGL WS 08/09 interpolierten Werte berechnet und jeweils in einem Feld zwischengespeichert. Der folgende Codeausschnitt stellt dies anhand des ersten Vertex eines Polygons für die interpolierten Normalen beispielhaft dar: Fehler: Referenz nicht gefunden 4-86 Berechnung der interpolierten Normalen Die Werte der Vertices und Normalen werden deshalb in je einem Feld ( normals[], vertices[]) abgespeichert, da zum Zeichnen der aktuellen Objektposition und Ausrichten der Normalen Vertex Arrays verwendet werden. 3D Objekte bestehen aus etlichen Vertices und Polygonen, würde deshalb zum Zeichnen jedes einzelnen Vertexpunktes die Funktion glVertex() und für die Normalen glNormal()innerhalb einer glBegin/glEnd Anweisung aufgerufen werden, resultiert dies in einer verringerten Performance der Anwendung. Pro Vertex/Normale ist dann ein separater Funktionsaufruf nötig. Für die einfache Animation innerhalb dieses Projektes mag der Unterschied noch vernachlässigbar sein, bei komplexeren Anwendungen sollte man in der Regel aber Vertex Arrays oder noch besser Displaylisten verwenden. Bei Vertex Arrays werden alle Primitivdaten durch einen einzigen Funktionsaufruf an OpenGL übergeben. Sind also alle interpolierten Vertices und Normalen berechnet, wird als nächstes die Zeichnungsfunktion aufgerufen. Darin werden zunächst mittels glVertexPointer() und glNormalPointer() die entsprechenden Objektdaten spezifiziert. Es werden jeweils 3 Float Komponenten verwendet. Als Daten werden die beiden arrays (normals[], vertices[]) übergeben. Das jeweilige Vertex Array muss anschließend aktiviert werden. Durch den Aufruf von glDrawArrays() erfolgt dann das Zeichnen des Objekts. Die Realisierung eines Primitivs erfolgt als Folge von Dreiecken, da jedes Polygon durch 3 Eckpunkte bestimmt ist. Die Normalen werden hierbei automatisch mitberücksichtigt. Fehler: Referenz nicht gefunden 4-87 Zeichnen des Objekts mittels Vertex arrays Damit eine flüssige Bewegung entsteht muss der oben beschrieben Vorgang natürlich wiederholt ausgeführt werden. Wie im vorherigen Kapitel beschrieben, geschieht dies durch das Nutzen der glutIdleFunc() Funktion. Dadurch wird die Display Funktion kontinuierlich neu aufgerufen. Der Skalierungsfaktor skalar wird jeweils anhand der Zeit neu berechnet und läuft dadurch von 0.0 beginnend langsam nach 1.0. Zu Beginn wird somit nur Objekt 1 gezeichnet, am Ende nur Objekt 2 und dazwischen die Übergänge von Objekt 1 nach Objekt 2. Anschließend wird die Animationsrichtung umgekehrt. Da der erneute Aufruf der display Funktion und das Zeichnen der aktuellen Objektposition sehr schnell geschieht, entsteht eine flüssige Bewegung (Animation). 4.3.5. Morphing mit GLSL In diesem Kapitel wird die Realisierung des Morphing Verfahrens mittels Vertex- und Fragmentshadern unter Nutzung der GLSL Sprache erläutert und Unterschiede, sowie Gemeinsamkeiten zur Implementierung im vorherigen Kapitel dargestellt. Einen großen Geschwindigkeitszuwachs bietet diese Lösung nicht, da die Animation relativ simpel ist und die Zeitdauer zur Berechnung eines Frames im Milisekundenbereich liegt. Die höhere Performance der GPU kommt erst bei komplexeren Szenen zum Tragen. Grundsätzlich gibt es zwei Schritte, die durchgeführt werden müssen. Zum einen muss die bestehende Implementierung modifiziert werden, damit die Shaderprogramme in die OpenGL Anwendung integriert werden und wichtige Parameter an das Shader Programm übergeben werden können. Zum anderen muss die eigentliche Programmlogik des Vertex- und Fragmentshaders entwickelt werden. S e i t e | LIV Charakter-Animation Eine Gegenüberstellung unter DirectX und OpenGL WS 08/09 4.3.5.1. Erweiterung der bestehenden Implementierung In Kapitel 3.3.3.2 wurde die generelle Vorgehensweise zur Integration von Shadern in ein OpenGL Programm bereits dargestellt. Durch die dort gezeigten Schritte können zuvor erzeugte Vertex- und Fragment Shader genutzt werden, für das Durchführen des Morphing Algorithmus sind aber noch einige Erweiterungen nötig. Dazu gilt es, sich zu überlegen, welche Teile der OpenGL Renderingpipeline durch die Shader ersetzt werden und welche Programmabschnitte dadurch nicht mehr mit herkömmlicher OpenGL Syntax durchgeführt, sondern durch GLSL ersetzt werden. Durch das Nutzen des Vertex Shaders muss zunächst die Interpolation der Vertices anhand des Skalierungsfaktors und das Zeichnen der resultierenden Vertexpunkte im Shader erfolgen. Der Vertex Shader benötigt also für jeden zu zeichnenden Frame den Wert des Skalierungsfaktors. Da dieser für ein ganzes Primitiv gilt, bietet sich eine uniform Variabel zur Speicherung an. Damit man den aktuellen Skalierungsfaktor an den Vertex Shader übergeben kann, benötigt man zwei Funktionen. Zunächst wird mit dem Aufruf Fehler: Referenz nicht gefunden die Position der im Vertex Shader spezifizierten uniform Variable ermittelt. Dabei bezeichnet ShaderProgramHandle den zuvor erzeugten Shader Programm Container und „skalar“ ist der Name der uniform Variable im Shader. Um Fehler zu vermeiden, sollte sichergestellt werden, dass die entsprechende Variabel auch exakt so heißt, wie man es im Funktionsaufruf angibt. Am Anfang der display Funktion kann nun der aktuelle Wert des Skalierungsfaktors durch den Aufruf von Fehler: Referenz nicht gefunden an die uniform Variabel im Vertex Shader übergeben werden. skalar enthält hier den float Wert des Skalierungsfaktors. Im Vertex Shader werden zudem die Werte der Normalen von den beiden Objekten, sowie deren Vertexdaten benötigt. Da Vertex- und Normalendaten innerhalb eines Primitivs unterschiedlich sind, werden hierfür attribute Variablen verwendet. Wie bereits in Kapitel 2.5.3.11 erläutert, können normale OpenGL Zeichnungsaufrufe nach wie vor verwendet werden, die Daten gelangen dadurch aber direkt an den Vertex Shader und die built-in-attribute Variablen gl_Vertex und gl_Normal. Nun benötigt man aber noch Variablen zur Speicherung der Daten des zweiten Objekts und einen geeigneten Übergabemechanismus. Ein solcher Mechanismus wird in GLSL durch generic vertex attributes realisiert. Diese werden unter OpenGL durch Angabe des Index Wertes des generic vertex attributes (von 0 beginnend) angesprochen. Im Shader kann eine attribute Variable zur Speicherung des jeweils aktuellen Vertexwertes verwendet werden. Innerhalb der OpenGL Anwendung wird nun zuerst mittels Fehler: Referenz nicht gefunden die Zuordnung des Indexes des generic vertex attributes (hier 1) zur angegebenen Variable (vertObj2) vorgenommen. Dieser Aufruf muss vor dem Binden des Shader Programms erfolgen. Anschließend kann mit der glVertexAttribPointer() Funktion, ein Feld von Vertexdaten an die oben beschriebene attribute Variable als kontinuierlicher Datenstrom übergeben werden. Für die konkrete Realisierung sorgt etwa der Aufruf Fehler: Referenz nicht gefunden dafür, dass alle Vertexdaten von Objekt 2, bestehend aus jeweils 3 float Komponenten, über den Index 1 an die oben zugeordnete attribute Variable (vertObj2) übergeben werden. Der vierte Parameter (GL_FALSE ) gibt an, ob die Vertexdaten zu normalisieren sind oder nicht. Anschließend muss noch der normale OpenGL Aufruf zum Zeichnen des Vertex Arrays erfolgen. S e i t e | LV Charakter-Animation Eine Gegenüberstellung unter DirectX und OpenGL WS 08/09 Der erste Implementierungsansatz verfolgte deshalb das Ziel, nur die Normalen und Vertices des zweiten Objekts über eine attribute Variable zu übergeben, die des ersten Objekts sollten über ein Vertex- und Normalenarray in die built-in-Variablen gl_Vertex und gl_Normal geschrieben werden. Für die Vertexdaten funktionierte dieser Ansatz auch, allerdings hatte das Aktivieren des Normalenarrays keinen Effekt. Es wurden nur die Normalen des Objekts zur Lichtberechnung berücksichtigt, die über ein generic vertex attribute übergeben wurden. Aufgrund dessen werden in der finalen Implementierung drei attribute Variablen für alle Normalen, sowie die Vertexdaten des zweiten Objekts verwendet. Die Vertexdaten des ersten Objekts erhält der Shader über den glDrawArrays Aufruf. Konkret sieht dies folgendermaßen aus: Fehler: Referenz nicht gefunden 4-88 Aufruf des Vertex Shaders Die Beleuchtungsberechnung muss nun ebenfalls im Shader realisiert werden. Näheres dazu finden Sie im nächsten Kapitel. Für die bestehende OpenGL Anwendung sind keine Änderungen nötig. Das Setzen der Licht und Materialeigenschaften bleibt so, wie bisher. Auf die dort gesetzten Werte kann im Shader mittels spezieller built-in Variablen zugegriffen werden. Anzumerken ist hier nur, dass das Aktivieren einer Lichtquelle und die Aktivierung der OpenGL Beleuchtungsberechnung über glEnable(GL_LIGHT1)und glEnable (GL_LIGHTING)keinen Effekt mehr haben. Das muss im Shader implementiert werden. 4.3.5.2. Implementierung des Vertex Shaders Im Vertex Shader wird das Morphing, sowie die Berechnung der Licht- und Materialparameter durchgeführt. Als Ausgabedaten erhält man die interpolierten Vertexdaten. Um die Shader Programme später leichter debuggen zu können, wird das kostenlose Werkzeug „Render Monkey“ von http://ati.amd.com/developer/rendermonkey/ eingesetzt. Auf eine Erläuterung des Programms wird hier verzichtet, da lediglich die Funktion zum direkten Kompilieren der Shader und Anzeigen eventueller Fehler Verwendung findet und der Code an sich von Hand programmiert wird. Der Vertex Shader wird pro Vertex des zu zeichnenden Objekts jeweils einmal aufgerufen. Zur Durchführung des Morphings deklariert man zunächst die globalen uniform und attribute Variablen für den Skalierungsfaktor und die Vertex- sowie Normalendaten. Der aktuelle Vertexpunkt des ersten Objekts befindet sich, wie bereits erwähnt, in der built-in-attribute Variable gl_Vertex. Anschließend berechnet man die interpolierten Normalen und Vertices entweder analog zur normalen OpenGL Implementierung anhand der Formel oder nutzt dafür gleich die in GLSL integrierte Funktion mix(x,y,a). Diese Funktion berechnet x*(1-a) + (y*a) und ist damit identisch zur vorher benutzen Formel. Somit ergibt sich folgender Code zur Berechnung der interpolierten Normalen und Vertices: Fehler: Referenz nicht gefunden 4-89 Berechnung der interpolierten Normalen und Vertices im Shader In jedem Vertexshader muss am Programmende die built-in Variable gl_Position geschrieben werden. Diese enthält die homogenen Koordinaten des zu setzenden Vertexpunktes. Dazu wird der interpolierte Vertexpunkt mit der GLSL uniform Variable gl_ModelViewProjectionMatrix multipliziert und man erhält die Clipping Koordinaten. Da es eine 4x4 Matrix ist, der Vertexpunkt aber nur drei Koordinaten enthält, wird dem Vertexpunkt zuvor noch der Wert 1.0 als vierte Koordinate hinzugefügt. Fehler: Referenz nicht gefunden 4-90 Schreiben der aktuellen Vertexposition Damit fehlt nun noch die Beleuchtungsberechnung. Hierfür wird das Lighthouse Tutorial „Directional Light per Pixel” verwendet. Die zu Beginn durchgeführte Normalisierung des Normalenvektors ist S e i t e | LVI Charakter-Animation Eine Gegenüberstellung unter DirectX und OpenGL WS 08/09 hier nicht mehr nötig, da der Shader bereits normalisierte Daten erhält. Zunächst wird die Position bestimmt, aus der das Licht kommt. Hierfür bietet GLSL die Datenstruktur gl_LightSource an. Mit gl_LightSource[0].position erhält man den im OpenGL Programm definierten Positionsvektor für die erste Lichtquelle. Da nachher im Fragment Shader mit dem Kosinus des Winkels zwischen Normalen- und Lichtvektor gerechnet wird, muss der Positionsvektor des Lichts zunächst normalisiert werden. Als nächstes wird der „halfVector“ benötigt. Dieser Vektor befindet sich genau in der Mitte zwischen Licht- und Kameravektor und ist später zur Berechnung des spekularen Lichtanteils nötig. Der halfVector wird ebenfalls normalisiert. Fehler: Referenz nicht gefunden 4-91 Normalisierungen im Vertex Shader Anschließend wird der diffuse und ambiente Anteil des Lichts anhand der im OpenGL Programm gesetzten Licht- und Materialparameter berechnet. Die Implementierung des Vertex Shaders ist damit abgeschlossen. Fehler: Referenz nicht gefunden 4-92 Lichtberechnung im Vertex Shader 4.3.5.3. Implementierung des Fragment Shaders Im Fragment Shader wird anhand der berechneten Lichtwerte im Vertex Shader das Licht pro Pixel gesetzt. Dabei bildet man die Standard OpenGL Lichtberechnung bei Nutzung der statischen Renderingpipeline (also ohne Shader) ab. Das Lighthouse Tutorial verwendet für den diffusen Lichtanteil die „Lambertian Reflection“ Formel: Intensität = Licht(diffus) * Material(diffus) *cos(Winkel zwischen Licht und Normalenvektor) Der ambiente Anteil wird unverändert vom Vertex Shader übernommen. Zur Berechnung des spekularen Lichtanteils wird das „Blinn Phong Modell“ genutzt, eine Vereinfachung des bekannten Phong Shading Algorithmus. Dabei bestimmt man die Intensität des spekularen Lichtanteils anhand des Winkels zwischen dem halfVector und dem Normalenvektor. Je größer der Winkel ist, umso intensiver wird das spekulare Licht. Die Formel hierzu ist: Spekular = (NormalV x halfV) shininess * Licht(spekular) * Material(spekular) Zur Durchführung der Berechnung werden zunächst einige Werte aus dem Vertexshader benötigt, unter anderem der Normalenvektor. Zum Zugriff auf diese Werte dienen varying Variablen, wobei die Variablennamen identisch zu denen aus dem Vertex Shader sein müssen. Für den diffusen und spekularen Lichtanteil wird nun das Skalarprodukt zwischen Licht- und Normalenvektor berechnet, man erhält den Kosinus des eingeschlossenen Winkels. Sofern dieser größer als 0 ist, werden die Lichtanteile anhand der oben beschriebenen Formeln berechnet: Fehler: Referenz nicht gefunden 4-93 Lichtberechnung im Fragment Shader Wie der Vertex Shader mit gl_Position, so besitzt auch der Fragment Shader eine spezielle Output Variabel, die immer geschrieben werden muss. In diesem Fall ist das gl_FragColor für die zu setzende Farbe des Fragments/Pixels. Die letzte Operation im Fragment Shader ist deshalb: Fehler: Referenz nicht gefunden 4-94 Schreiben der Farbe des aktuellen Pixels 4.4. Zusammenfassung S e i t e | LVII Charakter-Animation Eine Gegenüberstellung unter DirectX und OpenGL WS 08/09 Während die grundlegenden Berechnungen im Kapitel 2 und 3 erörtert wurden, galt es sie an dieser Stelle umzusetzen. Dazu wurde zu erst ein Charakter modelliert und Animiert, sowie im Anschluss daran in das passende Format für 3D-Objekte exportiert. Des Weiteren wurden die Verfahren Tweening und Skinning im Teilbereich DirectX implementiert, wobei zum Anfang Tweening zwischen zwei separat geladenen 3D-Objekten durchgeführt wurde. Da die Anwendungszwecke hierfür aber nur sehr gering sind und praktisch nicht von großer Bedeutung, wurde das X-Format erweitert um Tweening-Animationen erweitert. Außerdem wurde ein Animation-Controller für Tweening implementiert, der mehrere Animationen zwischen beliebig vielen Key-Frames kapselt. Im Anschluss daran wurde das Skinning-Verfahren auf Basis der DirectX-Samples implementiert. Hierbei ist die Analogie zwischen dem in DirectX vorhandenem Animation-Controller für Skinning und dem zuvor implementierten Animation-Controller für Tweening zu beachten. Des Weiteren wurde in diesem Kapitel die Integration und Implementierung der Shader erörtert und an praktischen Beispielen erklärt. Im OpenGL Teil wurde die Animation mittels Morphing durch Interpolation der Vertices und Normalen zweier Objekte betrachtet. Dies beinhaltet zunächst die Erläuterung des verwendeten 3DS Loaders. Anschließend folgten die nötigen Schritte zur Initialisierung von OpenGL und GLUT. Im Kernteil wurde nun die Realisierung des Morphing Verfahrens mittels CPU und durch Nutzung von Vertex- und Fragment Shader mit der GLSL Sprache beschrieben und Gemeinsamkeiten, sowie Unterschiede und nötige Erweiterungen aufgezeigt. S e i t e | LVIII Charakter-Animation Eine Gegenüberstellung unter DirectX und OpenGL WS 08/09 5. Fazit Zusammenfassend lässt sich sagen, dass die Charakteranimation ein sehr interessantes und vor allem umfangreiches Thema ist, was in dieser Ausarbeitung bei weitem nicht erschöpfend erläutert wurde. Zwar wurden die grundlegenden Techniken betrachtet und entsprechende Lösungen dafür implementiert, aber auf andere interessante Gebiete wie Gesichtsanimationen – sprich Mimik – wurde nicht besprochen. Die Implementierung der DirectX-Anwendungen lässt sich durchweg als erfolgreich zusammenfassen. Das einzige Problem, was dabei entstand, war dass das X-Dateiformat keine Tweening-Animationen mit Key-Frames unterstützt. Zwar wird an einigen Stellen beschrieben wie man das Format erweitert und wie die Templates dafür aussehen könnten. Jedoch gab es keine brauchbaren Informationen, außer den sehr knapp gehaltenen Dokumentationen von Microsoft, zur API. Aus diesem Grund wurde in den Teilbereich, der in dieser Ausarbeitung einen eher geringen Umfang besitzt verhältnismäßig viel Zeit investiert. Nichts desto trotz sind dadurch am Ende sowohl Animationen zwischen beliebig vielen Key-Frames auf Basis von Skinning und Tweening möglich. Leider fehlte die Zeit aufwendige Animationen und Charaktere zu erstellen, was allerdings in einem darauf aufbauendem Projekt sehr gut machbar ist. Daher möchte ich an dieser Stelle noch einen Ausblick in die Zukunft geben. Die Fortführung dieses Projektes könnte zu erst in der Modellierung und Animierung aufwendigerer, realistischer Charaktere liegen und im Anschluss daran die Implementierung der Überblendungen zwischen diesen Animationen beinhalten. Sind diese Schritte abgearbeitet, so kann man die Charaktere durch eine 3D-Szene bewegen und beispielsweise einen Übergang von Gehen über Laufen bis Rennen und einem anschließendem Sprung durchführen. Doch um dies zu realisieren, wäre wiederum eine Kollisionserkennung notwendig – was ein weiteres interessantes Gebiet darstellt. Bei der Implementierung des OpenGL Teils bestand zunächst das Problem, ein geeignetes Dateiformat und einen entsprechenden Loader zu finden. Da dies viel Zeit benötigte, wird als Kompromiss ein einfacher 3DS Loader eingesetzt. Mit diesem lässt sich das Morphing Verfahren mit CPU und GPU Berechnung gut und verständlich durchführen. Bones werden allerdings nicht unterstützt. Da die Implementierung der übrigen Verfahren jedoch bereits sehr lange dauerte, konnte das Skinning mit OpenGL ohnehin zeitlich nicht mehr durchgeführt werden. Der übrige Teil der OpenGL Implementierung lässt sich aber als erfolgreich bezeichnen. Insbesondere die Shader Programmierung war sehr interessant, da man zwar schon einige Begriffe aus diesem Bereich gehört oder gelesen hat, bisher aber nie eine konkrete Anwendung dazu realisiert hat. S e i t e | LIX Charakter-Animation Eine Gegenüberstellung unter DirectX und OpenGL WS 08/09 6. Anwendungsdokumentation 6.1. Erläuterung der Ordnerstruktur In diesem Kapitel wird die Struktur des beiliegenden Datenträgers beschrieben. In Anbetracht dessen erleichtert Ihnen das folgende Diagramm den Überblick. 6-95 Skizze der Ordnerstruktur des Datenträgers • Im Ordner „Ausarbeitung“ befindet sich dieses Textdokument. • Im Ordner „Plug-Ins“ sind beispielsweise Lightwave-Plug-Ins für den Export nach .X. • Im Ordner „Source-Code“ befindet sich der Quell-Code der entwickelten Anwendungen. • Im Ordner „Ausführbare Dateien“ befinden sich kompilierte Versionen der entwickelten Anwendungen. • Der Ordnet Quellen beinhaltet Kopien von Internetquellen. 6.2. Verwendete Werkzeuge Werkzeug Beschreibung MS Windows XP Adobe Acrobat Professional Lightwave 3D Export DirectX MS Visual Studio 2008 Rendermonkey FX Composer MS DirectX SDK (March 2008) Betriebssystem Dokumentationswerkzeug 3D-Modellierungs- und Animationssoftware. Lightwave Plug-In zur Konvertierung nach .X. Entwicklungsumgebung Shader IDE Shader IDE DirectX Software Development Kit 6-96 Verwendete Werkzeuge 6.3. Aufwandsabschätzungen S e i t e | LX Charakter-Animation Eine Gegenüberstellung unter DirectX und OpenGL WS 08/09 Aufwand (in Stunden) Aufgabe 35 2 20 6 15 30 108 Erstellung der Dokumentation Erstellung der Präsentation Implementierung – Tweening mit der GPU Implementierung – Tweening mit einem Vertex-Shader Implementierung – Tweening mit Key-Frames Implementierung – Skinning Gesamt 6-97 Aufwandsabschätzung – Michael Genau Aufwand (in Stunden) Aufgabe 30 2 20 5 20 77 Erstellung der Dokumentation Erstellung der Präsentation Suchen und Einrichten des Loaders Implementierung – Morphing mit OpenGL Implementierung – Morphing mit Shadern Gesamt 6-98 Aufwandsabschätzung Andreas Gärtner S e i t e | LXI Charakter-Animation Eine Gegenüberstellung unter DirectX und OpenGL VI. WS 08/09 Literaturverzeichnis 1. Bartsch, Hans-Jochen. Taschenbuch Mathematischer Formeln. s.l. : Fachbuchverlag Leipzig im Carl Hanser Verlag, 2004. ISBN: 3-446-22891-8. 2. Pipho, Evan. Focus On 3D Models. s.l. : Premier Press, 2003. ISBN: 1-59200-033-9. 3. Adams, Jim. Advanced Animation with DirectX. s.l. : Premier Press, 2003. ISBN-10: 1592000371. 4. [Online] http://www.spacesimulator.net/tut4_3dsloader.html. 5. [Online] http://www.jalix.org/ressources/graphics/3DS/_unofficials/3ds-info.txt. 6. Scherfgen, David. 3D-Spiele-Programmierung mit DirectX 9 und C++. s.l. : Hanser Fachbuchverlag, 2006. ISBN-13: 978-3-446-40596-7. 7. Rousselle, Christian. Jetzt lerne ich DirectX 9 und Visual C++. s.l. : Markt und Technik, 2003. ISBN-10: 3827264545. 8. Heinzel, Werner. Skript zur Vorlesung "Computer Graphics". 2008. 9. —. Skript zur Lehrveranstaltung "Graphische Datenverarbeitung". 2005. 10. Rost, Randi J. OpenGL Shading Language Second Edition. ISBN 0-321-33489-2. 11. Kaiser, Ulrich and Lensing, Philipp. Spieleprogrammierung mit DirectX und C++. Bonn : Galileo Computing, 2007. ISBN: 978-3-89842-827-9. 12. Parent, Rick. Computer Animation: Algorithms and Techniques. s.l. : Morgan Kaufmann, 2001. 1-55860-579-7 . 13. Möller, Thomas and Eric, Haines. Real-Time Rendering. s.l. : B&T, 2002. 1568811829. 14. Calver, Dean, et al. Shader X³ Advanced Rendering with DirectX and OpenGL. s.l. : Charles River Media, 2004. ISBN-10: 1584503572. 15. Engel, Wolfgang F. Direct3D Shaderx: Vertex and Pixel Shader Tips and Tricks. s.l. : Wordware Publishing Inc.,U.S., 2002. ISBN: 1-55622-041-3. 16. Rössler, Ronny. Einführung in die Shader Programmierung unter OpenGL 2.0. 17. [Online] http://wiki.delphigl.com/index.php/Tutorial_glsl. 18. [Online] http://www.lighthouse3d.com/opengl/glsl/index.php. 19. www.gamedev.net. [Online] [Cited: November 03, 2008.] 20. Heinzel, Werner. Skript zur Vorlesung Grafik-Programmierung. 2007. 21. MSDN Microsoft. [Online] [Cited: November 26, 2008.] http://msdn.microsoft.com. 22. [Online] http://www.spacesimulator.net/tut5_vectors_and_lighting.html. 23. Lever, Nik. Real-time 3D Character Animation with Visual C++. s.l. : Focal Press, 2002. ISBN: 0-240-51664-8. S e i t e | LXII Charakter-Animation Eine Gegenüberstellung unter DirectX und OpenGL WS 08/09 VII. Abbildungsverzeichnis 2-1 Gimbal Lock. Quelle: (2).........................................................................................................................................3 2-2 Visualisierung von LERP und SLERP. Quelle: (2).........................................................................................4 2-3 Hierarchie einiger 3ds chunk Elemente, Quelle (4)....................................................................................5 2-4 Felder eines Chunks, Quelle: (4).........................................................................................................................6 2-5 Beschreibung der Schnittstelle IUnknown. Quelle: (6).............................................................................9 2-6 Skizze der DirectX Renderpipeline. Quelle: (11)..........................................................................................9 2-7 Funktionsweise eines Vertexshader. Quelle: (11).......................................................................................9 2-8 OpenGL Historie Quelle (8)................................................................................................................................10 2-9 OpenGL Renderingpipeline, Quelle: (9)........................................................................................................11 dinput8.lib, dxguid.lib, dxerr9.lib, dsound.lib, d3dx9.lib, winmm.lib, d3d9.lib, kernel32.lib, user32.lib, comdlg32.lib, gdi32.lib, shell32.lib 3-10 Ein Beispiel für zusätzliche Linkeroptionen für DirectX-Anwendungen......................................14 3-11 Beispiel für eine Bone-Hierarchie.................................................................................................................15 3-12 Definition der Input- und Output-Strauktur eines Vertex-Shaders................................................19 3-13 Definition einer Main-Methode in einem Vertex-Shader....................................................................19 3-14 Definition bedeutender DirectX-Variablen zur Verwendung von Vertex-Shadern..................19 3-15 DirectX-Funktion zum Laden eines Shaders aus einer Datei.............................................................20 3-16 DirectX-Funktion zum Erstellen eines vertex-Shaders........................................................................20 3-17 DirectX-Funktion für den Zugriff auf die Konstanten eines Shaders..............................................20 3-18 DirectX-Funktion zum Setzen einer Konstanten (einer Matrix).......................................................20 3-19 DirectX-Funktion zum Setzen eines Vertex-Shaders im Rendervorgang......................................20 3-20 DirectX-Funktion zum Zeichnen eines Meshs..........................................................................................20 3-21 DirectX-Funktion zum Laden einer Effektdatei.......................................................................................22 3-22 DirectX-Funktion für den Zugriff auf die Konstanten einer Effektdatei........................................22 3-23 Code-Ausschnitt zum Rendern eines Effekts............................................................................................22 3-24 Besonderheiten bei der Initialisierung von GLSL Variablen, Quelle (16).....................................25 3-25 Nutzung des vec Datentyps, Quelle (17)....................................................................................................25 3-26 Erzeugung eines Shader Containers, Quelle: (18)..................................................................................26 3-27 Laden des Shader Codes in den Shader Container , Quelle: (18)......................................................26 3-28 Kompilieren des Shader Codes, Quelle: (18)............................................................................................26 3-29 Erzeugung eines Programm Containers, Quelle: (18)..........................................................................26 3-30 Shader-Code zum Shader-Programm hinzufuegen, Quelle: (18).....................................................26 3-31 Shader Programm binden , Quelle: (18).....................................................................................................27 3-32 Shader Code auf Grafikkarte installieren , Quelle: (18).......................................................................27 4-33 Bild vom Auge des Charakters.......................................................................................................................28 4-34 Bild vom Gesicht des Charakters...................................................................................................................28 4-35 Bild vom Gesicht des Charakters...................................................................................................................29 S e i t e | LXIII Charakter-Animation Eine Gegenüberstellung unter DirectX und OpenGL WS 08/09 4-36 API der WinMain-Funktion..............................................................................................................................30 4-37 API für einen Callback-Handler.....................................................................................................................30 4-38 API der Funktion zum Erstellen eines Windows-Fensters.................................................................31 4-39 Beispiel für eine Hauptprogrammschleife.................................................................................................31 4-40 Quell-Code zum initialisieren einer IDirect3D9-Schnittstelle...........................................................31 4-41 Abfrage des HAL-Devices eines Systems....................................................................................................32 4-42 Quell-Code zum Erstellen eines Direct3D9-Devices..............................................................................32 4-43 Quell-Code zum Erstellen einer DirectInput-Schnittstelle..................................................................32 4-44 Quell-Code zum Erstellen eines DirectInput-Devices...........................................................................33 4-45 Quell-Code zum Setzen des Datenformats eines DirectInput-Devices...........................................33 4-46 Quell-Code zum Setzen des Kooperationslevels.....................................................................................33 4-47 Variablen zur verwaltung eines Meshs sowie dessen Materialien und Texturen.....................34 4-48 Funktion zum Laden einer X-Datei...............................................................................................................34 4-49 Quell-Code zum Auslesen der Materialien und Texturen eines Meshs..........................................34 4-50 Quell-Code zum Klonen eines Meshs um dessen FVF anzupassen..................................................35 4-51 Quell-Code zur Zeitmessung (1 von 3). Auf Basis von (19)................................................................35 4-52 Quell-Code zur Zeitmessung (2 von 3). Auf Basis von (19)................................................................35 4-53 Quell-Code zur Zeitmessung (3 von 3). .....................................................................................................35 4-54 Quell-Code zum AUfruf der Morphing-Funktion in Abhängigkeit der Zeit..................................35 4-55 Quell-Code zum Tweening mit der CPU (1 von 3)..................................................................................36 4-56 Quell-Code zum Tweening mit der CPU (2 von 3)..................................................................................36 4-57 Quell-Code zum Tweening mit der CPU (3 von 3)..................................................................................37 4-58 Definition der Funktion D3DXVec3Lerp....................................................................................................37 4-59 Quell-Code zur Definition einer Vertexstruktur und Variablen........................................................37 4-60 Quell-Code zur Definition einer Vertexdeklaration...............................................................................38 4-61 Quell-Code zum Extrahieren der Vertexdaten der Meshs...................................................................38 4-62 Quell-Code zum Setzen der Vertexdaten-Streams und Indices.........................................................38 4-63 Quell-Code zum Setzen der Eigenschaften eines Effekts.....................................................................39 4-64 Quell-Code zum Rendern mit einer Effekt-Datei....................................................................................39 4-65 Vertexshader Datenstrukturen für Tweening..........................................................................................39 4-66 Ausschnitt des Quell-Codes der Effektdatei zum Tweening...............................................................40 4-67 Skizze zur Veranschaulichung von Key-Frames.....................................................................................40 4-68 Definition des X-File Templates Animation. Quelle: (21)....................................................................40 4-69 Definition der X-File Templates AnimationKey und TimedFloatKeys. Quelle: (21).................41 4-70 Definition der X-File Templates AnimationSet. Quelle: (21)..............................................................41 } D3DXMESHCONTAINER, *LPD3DXMESHCONTAINE; 4-71 Definition der Structs D3DXFRAME und D3DXMESHCONTAINER.................................................41 4-72 Definition der Funktion D3DXLoadMeshHierarchyFromX(…). Quelle: (21)...............................42 S e i t e | LXIV Charakter-Animation Eine Gegenüberstellung unter DirectX und OpenGL WS 08/09 4-73 Erweiterung des X-Dateiformats durch Morphing. Auf Basis von Quelle (3)..............................42 4-74 Definition der GUIDs mit dem DEFINE_GUID Makro............................................................................43 4-75 Definition der structs zu dem Templates der X-Datei..........................................................................43 4-76 Funktionsdefinition LoadTopLevelData der Klasse LoadUserData................................................43 4-77 Beschreibung der Funktion CreateMeshContainer des Interfaces ID3DXAllocateHierarchy Quelle: (21).......................................................................................................................................................................45 4-78 Konstanten und Datenstrukturen des Skinning-Vertexshaders. Quelle: (21)............................46 4-79 Quell-Code der Skinning-Vertexsahder. Quelle: (21)............................................................................47 4-80 Datenstruktur eines Objekts in OpenGL.....................................................................................................48 4-81 Vertices der Datenstruktur hinzufügen......................................................................................................48 4-82 Berechnung der Normalen des Polygons...................................................................................................49 4-83 Normalisieren eines Vektors..........................................................................................................................49 4-84 Normalen Berechnung der Vertices.............................................................................................................49 4-85 Main Funktion des Programms......................................................................................................................50 4-86 Berechnung der interpolierten Normalen ................................................................................................51 4-87 Zeichnen des Objekts mittels Vertex arrays.............................................................................................51 4-88 Aufruf des Vertex Shaders...............................................................................................................................53 4-89 Berechnung der interpolierten Normalen und Vertices im Shader................................................53 4-90 Schreiben der aktuellen Vertexposition.....................................................................................................53 4-91 Normalisierungen im Vertex Shader...........................................................................................................54 4-92 Lichtberechnung im Vertex Shader..............................................................................................................54 4-93 Lichtberechnung im Fragment Shader.......................................................................................................54 4-94 Schreiben der Farbe des aktuellen Pixels .................................................................................................54 6-95 Skizze der Ordnerstruktur des Datenträgers...........................................................................................57 6-96 Verwendete Werkzeuge....................................................................................................................................57 6-97 Aufwandsabschätzung – Michael Genau....................................................................................................58 6-98 Aufwandsabschätzung Andreas Gärtner....................................................................................................58 VIII-99 Text-Version einer einfachen X-Datei. Quelle: (3)........................................................................LXIII VIII-100 Liste einiger Templates von X-Dateien. Quelle: (21).................................................................LXIV S e i t e | LXV Charakter-Animation Eine Gegenüberstellung unter DirectX und OpenGL WS 08/09 VIII. Weitere Anlagen xof 0302txt 0032 template Header { <3D82AB43−62DA−11cf−AB39−0020AF71E433> DWORD major; DWORD minor; DWORD flags; } template Frame { <3D82AB46−62DA−11cf−AB39−0020AF71E433> [FrameTransformMatrix] [Mesh] } Header { 1; 0; 1; } Frame Scene_Root { FrameTransformMatrix { 1.000000, 0.000000, 0.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000, 0.000000, 0.000000, 1.000000;; } Frame Pyramid_Frame { FrameTransformMatrix { 1.000000, 0.000000, 0.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000, 0.000000, 0.000000, 1.000000;; } Mesh PyramidMesh { 5; 0.00000;10.00000;0.00000;, 58 −10.00000;0.00000;10.00000;, 10.00000;0.00000;10.00000;, −10.00000;0.00000;−10.00000;, 10.00000;0.00000;−10.00000;; 6; 3;0,1,2;, 3;0,2,3;, 3;0,3,4;, 3;0,4,1;, 3;2,1,4;, 3;2,4,3;; MeshMaterialList { 1; 6; 0,0,0,0,0,0;; Material Material0 { 1.000000; 1.000000; 1.000000; 1.000000;; 0.000000; 0.050000; 0.050000; 0.050000;; 0.000000; 0.000000; 0.000000;; } } } } } VIII-99 Text-Version einer einfachen X-Datei. Quelle: (3) S e i t e | LXVI Charakter-Animation Eine Gegenüberstellung unter DirectX und OpenGL WS 08/09 Name des Templates Beschreibung Animation AnimationKey Definiert Animationsdaten für einen Frame. Definiert einen Key Frame für das übergeordnete AnimationTemplate. Enthält Informationen zur Wiedergabe der Animation. Enthält eine Sammlung von Animation-Templates. Enthält einen boolschen Wert. Enthält zwei boolsche Werte. Enthält rot, grün und blau Werte. Enthält rot, grün, blau und einen alpha Wert. Definiert zwei Koordinatenwerte. Enthält ein Array von float-Werten. Enthält eine Transformationsmatrix für den übergeordneten Frame. Ein Template, das eine Hierarchie beschreibt. Der X-Datei Header, inkl. Versionsnummern. Enthält einen indexierten Farbwert. Enthält Materialfarbwerte. Enthält einen homogenen 4x4 Matrixcontainer. Enthält die Daten eines Mesh-Objektes. AnimationOptions AnimationSet Boolean Boolean2d ColorRGB ColorRGBA Coords2d FloatKeys FrameTransformMatrix Frame Header IndexedColor Material Matrix4x4 Mesh VIII-100 Liste einiger Templates von X-Dateien. Quelle: (21) S e i t e | LXVII