Programmieren mit DirectX
Transcrição
Programmieren mit DirectX
Programmieren mit DirectX Malte Ried 14. Januar 2006 Vorwort Dieses Skript entstand im Wintersemester 2005/2006 im Zuge der Veranstaltung Programmieren mit DirectX“. ” Über dieses Skript Es handelt sich bei diesem Skript um die erste Version. Sollte jemand Fehler (Rechtschreib-, Grammatik- aber auch inhaltliche Fehler) finden, so wäre ich sehr dankbar, wenn mir diese mitgeteilt würden. Danksagung Ich möchte mich bei Herrn Professor Igler für seine Unterstützung bei der Planung bedanken. Des weiteren gilt mein Dank Claudia Fladerer, die dieses Skript in stundenlanger Arbeit korrekturgelesen hat. Gold hat einen bestimmten Preis. ” Bücher nicht, Bücher sind mehr wert als Gold.“ Chinesisches Sprichwort I II Inhaltsverzeichnis I Einführung 1 Organisatorisches 1.1 Allgemeines . . . . . . . . 1.2 Vortrag und Praktikum . 1.3 Umfang und Eigenleistung 1.4 Voraussetzungen . . . . . 1.5 Werkzeuge . . . . . . . . . 1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 Windowsprogrammierung 2.1 Einleitung . . . . . . . . . . . . . . . 2.2 Das Nachrichtenkonzept . . . . . . . 2.3 Der Einstiegspunkt . . . . . . . . . . 2.4 Vier Schritte für ein Fenster . . . . . 2.4.1 Fensterklasse . . . . . . . . . 2.4.2 Anzeigen des Fensters . . . . 2.4.3 Nachrichten abholen . . . . . 2.4.4 Nachrichten verarbeiten – Die . . . . . . . . . . . . . . . . . . . . 3 3 3 4 4 4 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Callback-Funktion . . . . . . . . . . . . . . . . . . . . . . . . 5 5 5 5 6 6 8 8 9 . . . . . . . . . . 13 13 14 14 14 15 15 16 16 17 17 . . . . . . . . . . . . . . . 3 Einführung in DirectX 3.1 Vorbereitungen . . . . . . . . . . . . . . . 3.2 DirectX im Detail . . . . . . . . . . . . . 3.2.1 DirectX - technisch . . . . . . . . . 3.2.2 DirectX-Komponenten . . . . . . . 3.3 COM – Component Object Model . . . . 3.3.1 COM-Objekte . . . . . . . . . . . 3.3.2 Schnittstellen . . . . . . . . . . . . 3.3.3 GUIDs - Global Unique Identifiers 3.3.4 Der Rückgabewert HRESULT . . . . 3.3.5 Lebenszeit . . . . . . . . . . . . . . III . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . IV II INHALTSVERZEICHNIS Mathematik 4 Vektoren 4.1 Grundlagen . . . . . . . . . . . . 4.1.1 Darstellung . . . . . . . . 4.1.2 Betrag eines Vektors . . . 4.1.3 Einheitsvektor . . . . . . 4.2 Rechnen mit Vektoren . . . . . . 4.2.1 Addition und Subtraktion 4.2.2 Multiplikation . . . . . . 4.3 Vektoren in DirectX . . . . . . . 19 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23 23 23 23 24 24 24 25 28 5 Matrizen 5.1 Grundlagen der Matrizen . . . . . 5.1.1 Einheitsmatrizen . . . . . . 5.1.2 Multiplikation von Matrizen 5.2 Bewegung . . . . . . . . . . . . . . 5.2.1 Verschieben . . . . . . . . . 5.2.2 Rotation . . . . . . . . . . . 5.2.3 Bewegungen kombinieren . 5.3 Bewegung mit Direct3D . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29 29 29 30 30 30 31 33 34 6 Ebenen 6.1 Darstellung . . . . . . . . . . . . . 6.2 Berechnungen . . . . . . . . . . . . 6.2.1 Berechnen einer Ebene . . . 6.2.2 Klassifikation eines Punktes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . bezüglich einer Ebene . . . . . . . . 35 35 36 36 37 III DirectX Graphics 7 DirectX Graphics initialisieren 7.1 DirectX Graphics - Einführung . . . . . . 7.2 Vorbemerkungen . . . . . . . . . . . . . . 7.3 Fachbegriffe . . . . . . . . . . . . . . . . . 7.3.1 Adapter . . . . . . . . . . . . . . . 7.3.2 Device . . . . . . . . . . . . . . . . 7.3.3 Modus . . . . . . . . . . . . . . . . 7.3.4 Format . . . . . . . . . . . . . . . 7.3.5 Buffer . . . . . . . . . . . . . . . . 7.3.6 Front- und Backbuffer . . . . . . . 7.3.7 Depthbuffer . . . . . . . . . . . . . 7.4 Initialisierung . . . . . . . . . . . . . . . . 7.4.1 Initialisierung mit Standardwerten 39 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41 41 41 42 42 42 42 43 43 43 43 43 44 INHALTSVERZEICHNIS 7.4.2 V Optimale Initialisierung . . . . . . . . . . . . . . . . . 8 Zeichnen einfacher Formen 8.1 Einführung . . . . . . . . . . . . . 8.2 Rendern in 2D . . . . . . . . . . . 8.2.1 Der Viewport . . . . . . . . 8.2.2 Punkte angeben . . . . . . 8.2.3 Farben . . . . . . . . . . . . 8.2.4 Anlegen der Geometrie . . . 8.2.5 Abändern der Hauptschleife 8.2.6 Rendern . . . . . . . . . . . 8.3 Die dritte Dimension . . . . . . . . 8.3.1 Das View Frustum . . . . . 8.3.2 Initialisierung der Szene . . 8.3.3 Geometrie erzeugen . . . . 8.3.4 Rendern . . . . . . . . . . . 8.4 Texturen . . . . . . . . . . . . . . . 8.4.1 Geometrie erzeugen . . . . 8.4.2 Rendern . . . . . . . . . . . 44 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51 51 51 51 52 53 54 55 55 56 56 58 61 61 62 62 63 9 Animation 9.1 Animation durch Transformation . . . . 9.1.1 Erzeugen der Weltmatrix . . . . 9.1.2 Setzen der Weltmatrix . . . . . . 9.1.3 Backface Culling . . . . . . . . . 9.2 Animation durch Bewegung der Kamera 9.3 Interaktion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67 67 67 69 70 70 73 10 Modelle 10.1 Pools . . . . . . . . . . . . . . 10.1.1 Speicherbereiche . . . 10.1.2 Pools . . . . . . . . . 10.2 Das .x-Format . . . . . . . . 10.2.1 Laden von .x-Dateien 10.2.2 Rendern des geladenen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75 75 75 76 76 77 77 IV . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Modells . . . . . . Techniken 11 Bounding Boxen & Culling 11.1 Allgemeines . . . . . . . . . . . . . . . 11.1.1 Warum macht man das? . . . . 11.2 Achsen-Ausgerichtete-Bounding-Boxen 11.2.1 Berechnung von AABBs . . . . 79 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83 83 83 84 84 VI INHALTSVERZEICHNIS 11.3 Culling . . . . . . . . 11.3.1 Allgemeines . 11.3.2 Punkte finden 11.3.3 Klassifikation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84 84 86 87 12 BSP-Bäume 89 12.1 Grundlagen . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89 12.2 Theorie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89 12.2.1 Ziel . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90 12.2.2 BSP-Baum erstellen . . . . . . . . . . . . . . . . . . . 90 12.2.3 Die beste Schnittebene finden . . . . . . . . . . . . . . 95 12.2.4 Bounding-Boxen . . . . . . . . . . . . . . . . . . . . . 95 12.3 Pseudo-Code . . . . . . . . . . . . . . . . . . . . . . . . . . . 95 12.3.1 Datenstruktur . . . . . . . . . . . . . . . . . . . . . . 95 12.3.2 Baum erstellen . . . . . . . . . . . . . . . . . . . . . . 96 12.3.3 Daten laden . . . . . . . . . . . . . . . . . . . . . . . . 97 12.3.4 Texturen . . . . . . . . . . . . . . . . . . . . . . . . . 97 12.3.5 Knoten erstellen . . . . . . . . . . . . . . . . . . . . . 98 12.3.6 Beste Teilungsebene finden . . . . . . . . . . . . . . . 100 12.3.7 Dreiecke bezüglich einer Ebene klassifizieren . . . . . . 102 12.3.8 Ein Dreieck aufteilen . . . . . . . . . . . . . . . . . . . 104 12.3.9 Schnittpunkt einer Ebene und einer Gerade bestimmen 106 12.4 Anwendung . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108 12.5 Rendern . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108 12.5.1 Aufruf . . . . . . . . . . . . . . . . . . . . . . . . . . . 108 12.5.2 Rekursion . . . . . . . . . . . . . . . . . . . . . . . . . 108 V DirectX Input 13 DirectInput 13.1 Vorgehensweise – theoretisch 13.1.1 Versionitits . . . . . . 13.1.2 Ereignisse und Puffer 13.2 Ein praktisches Beispiel . . . 13.2.1 Maus . . . . . . . . . 13.2.2 Tastatur . . . . . . . . Glossar 111 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113 . 113 . 114 . 114 . 114 . 115 . 116 117 Teil I Einführung 1 Kapitel 1 Organisatorisches 1.1 Allgemeines Zunächst einmal die schlechte Nachricht: Für diese Veranstaltung gibt es keinen Schein. Allerdings wird es einen Zettel geben auf dem so etwas wie Herr/Frau - hier Namen eintragen - hat an der Veranstaltung Program” ” mieren mit DirectX“ erfolgreich teilgenommen“ steht. Ebenso wird hierauf vermerkt sein, wie oft der Teilnehmer an den Veranstaltungen teilgenommen hat. Zusätzlich besteht die Möglichkeit den ,Zettel‘ in einen richtigen Schein zu verwandeln, indem entweder gegen Ende des Semesters ein Programm geschrieben wird, welches ein wenig über das in der Vorlesung behandelte Wissen hinausgeht. Dieses Programm soll dann in einem Vortrag und einer Ausarbeitung vorgestellt werden. Dies kann dann als kleines Seminar angerechnet werden. Die zweite Möglichkeit besteht darin, dass ich nächstes Semester erneut eine ,halbe‘ Vorlesung zu dem Thema anbiete, die dann mit dieser zusammengerechnet wird, und mit einer Klausur abschließst. Dann bekommt der Teilnehmer einen normalen Wahlpflicht-Schein. 1.2 Vortrag und Praktikum Diese Veranstaltung geht über das ganze Semester, allerdings nur im zweiwöchentlichen Rythmus. Das heißt, alle zwei Wochen gibt es einen Vortrag und alternierend dazu ein Praktikum. Im Praktikum soll das im Vortrag Gehörte praktisch genutzt werden. Dazu werden Übungsaufgaben auf der Homepage des Autors [1] bereitgestellt. 3 4 KAPITEL 1. ORGANISATORISCHES 1.3 Umfang und Eigenleistung Am Ende wird die Basis für das effiziente Anzeigen eines 3D-Levels, also einer virtuellen Szene mit Räumen, in der sich der (virtuelle) Betrachter bewegen kann, vorhanden sein. Werden alle Übungsaufgaben bearbeitet, so erhält man ein lauffähiges Programm, das ein solches Level anzeigt. Dazu wird einiges an Eigeninitiative verlangt. Es müssen am Ende binäre Bäume rekursiv durchlaufen und einiges mit 3D-Mathemtik berechnet werden. 1.4 Voraussetzungen Um an dieser Veranstaltung teilnehmen zu können, müssen ein paar Voraussetzungen erfüllt sein: 1. Anmeldung im eStudy-Portal 2. erfolgreiche Teilnahme an Programmieren II“ ” 3. ebenso erfolgreiche Teilnahme an Grafische Datenverarbeitung“ ” 1.5 Werkzeuge Als Entwicklungsumgebung wird Visual Studio .NET 2003 von Microsoft verwendet. Um Applikationen mit DirectX entwickeln zu können, wird zusätzlich noch das DirectX SDK (Software Development Kit) benötigt. Zur Zeit (August 2005) sind die aktuellen Versionen 9.0c für DirectX und June ” 2005“ für das SDK. Beides kann bei Microsoft kostenlos heruntergeladen werden (Gesamtgröße etwa 381MB) [2]. Kapitel 2 Einführung in die Windowsprogrammierung 2.1 Einleitung Um mit DirectX etwas zu zeichnen, wird zunächst ein ganz normales Windows-Fenster benötigt. Dieses kann durch die normalen Windows-API-Befehle erzeugt werden. In diesem Abschnitt soll die Erzeugung eines Fensters ein wenig genauer betrachtet werden. Da vieles hier nur sehr knapp behandelt wird, ist dies jedoch keine vollwertige Einführung in die WindowsProgrammierung. 2.2 Das Nachrichtenkonzept Drückt der Benutzer in einem Fenster zum Beispiel einen Mausknopf, wird unter Windows eine sogenannte Nachricht“ an das Fenster gesendet. Die” se Nachricht enthält immer alle Informationen, die zu ihr gehören. Dies sind zum Beispiel Informationen worum es überhaupt geht (Mausknopf, Tastatureingabe, Timer, ...), an welcher Koordinate wurde der Mausknopf gedrückt und so weiter. Diese Nachrichten landen zuerst in einer Warteschlange der jeweiligen Applikation. Die Applikation ist wiederum selbst dafür zuständig, dass die Nachrichten abgearbeitet werden. Sie muss also die Nachrichten aus der Warteschlange holen und bearbeiten. Das Schema ist in Abbildung 2.1 auf Seite 6 dargestellt. Die Einzelheiten werden später klarer, wenn die Funktionalität implementiert wird. 2.3 Der Einstiegspunkt Wie jedes Programm benötigt auch ein Windowsprogramm einen definierten Einstiegspunkt. Dies ist im Gegensatz zu DOS- oder Unixprogrammen nicht 5 6 KAPITEL 2. WINDOWSPROGRAMMIERUNG Nachrichtenwarteschlange GetMessage(...) Windows Applikation DispatchMessage(...) Nachrichtenschleife Callback-Funktion Input Abbildung 2.1: Das Nachrichtenkonzept von Windows die main-Funktion sondern eine Funktion namens WinMain. Ihr Kopf ist in Listing 2.1 dargestellt. Die einzelnen Elemente sollen hier nicht weiter betrachtet werden. 1 2 int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hprevinst, LPSTR lpcmdline, int ncmdshow) Listing 2.1: Der Kopf der Funktion WinMain. 2.4 Vier Schritte für ein Fenster Soll ein Fenster unter Windows erzeugt werden, so werden vier Schritte benötigt: 1. Erstellen und regisitrieren einer Fensterklasse, 2. Erstellen des eigentlichen Fensters, 3. eine Schleife, die die Nachrichten abholt und 4. eine Callback-Funktion, die die Nachrichten verarbeitet. 2.4.1 Fensterklasse Zuerst muss eine Fensterklasse erstellt und registriert werden. Eine Fensterklasse ist eine Sammlung von Informationen und Eigenschaften zu einem 2.4. VIER SCHRITTE FÜR EIN FENSTER 7 Fenster. Die Eigenschaften werden extra gespeichert, da eine Anwendung durchaus mehrere Fenster vom selben Typ erstellen kann. Alle diese Fenster besitzen die gleichen Eigenschaften. Daten zu dieser Klasse werden in der WNDCLASSEX-Struktur gespeichert. Listing 2.2 zeigt eine minimale Belegung dieser Struktur. 1 WNDCLASSEX xWndClass; //Die Fensterklasse 2 3 4 //Speicher leeren ZeroMemory(&xWndClass, sizeof(WNDCLASSEX)); 5 6 7 8 9 10 11 12 13 14 15 16 17 //Angabe der Groesse der Struktur xWndClass.cbSize = sizeof(WNDCLASSEX); //Name der Callback-Funktion (s.u.) xWndClass.lpfnWndProc = WindowProc; //Handle zur Instanz des Programms xWndClass.hInstance = hinst; //Standardcursor xWndClass.hCursor = LoadCursor(NULL, IDC_ARROW); //weisser Hintergrund xWndClass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH); //Name unserer Klasse xWndClass.lpszClassName = WND_CLASS_NAME; Listing 2.2: Minimale Belegung der Fensterklasse. Die Bedeutung der einzelnen Felder soll nicht weiter interessieren. Auch können hier noch wesentlich mehr Attribute gesetzt werden, die aber hier ebenfalls nicht von Bedeutung sind. Nur ein Attribut soll etwas näher betrachtet werden: lpfnWndProc. Dieses Attribut muss auf einen Funktionszeiger gesetzt werden, der auf die Callback-Funktion (siehe unten) verweist. Diese wird aufgerufen, wenn eine Nachricht für das Fenster eintrifft. Als nächstes muss die soeben definierte Klasse noch bei Windows registriert werden. Dies geschieht mit dem Code aus Listing 2.3. 1 2 3 4 5 //Klasse registrieren if(!RegisterClassEx(&xWndClass)) { return -1; } Listing 2.3: Registrierung einer Fensterklasse 8 2.4.2 KAPITEL 2. WINDOWSPROGRAMMIERUNG Anzeigen des Fensters Nun sind alle Vorbereitungen getroffen und ein ,echtes‘ Fenster kann erzeugt und anzeigt werden. Auch hier sollen die einzelnen Parameter nicht weiter interessieren. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 //Fenster anzeigen hMainWindow = CreateWindowEx(//Style NULL, //Klassenname WND_CLASS_NAME, //Titel "Mein erstes Fenster!", //Style WS_POPUPWINDOW | WS_VISIBLE, //Position/Groesse 100,100, 800,600, //Parent/Menu/Instanz/Param NULL, NULL, hinst, NULL); if(!hMainWindow) { return -2; } Listing 2.4: Erstellen eines Fensters 2.4.3 Nachrichten abholen Nach dem Anzeigen des Fensters kann das Programm anfangen Nachrichten zu empfangen. Um dieses zu erreichen, wird eine Schleife benötigt, die so lange in die Warteschlange schaut und neue Nachrichten daraus hervorholt, bis diese leer ist. Der Einfachheit halber ist die Schleife in Listing 2.5 in einer gesonderten Funktion untergebracht. Die Nachricht wird mit GetMessage in Zeile 6 aus der Warteschlange abgeholt und dabei gleich aus dieser entfernt. Ist keine weitere Nachricht mehr vorhanden, so wird an dieser Stelle gewartet, bis wieder eine angekommen ist. Als nächster Schritt wird die Nachricht eventuell ,übersetzt‘. Dabei werden Nachrichten, die sogenannte virtuelle Tastaturcodes enthalten, abgefangen und in ,echte‘ Buchstaben umgewandelt. Im letzten Schritt wird die Nachricht ,ausgeliefert‘. Dazu wird Windows veranlasst, die oben angegebene Callback-Funktion (siehe unten) aufzurufen und zwar diejenige, die zu dem entsprechenden Fenster gehört. 2.4. VIER SCHRITTE FÜR EIN FENSTER 1 2 3 9 void mainLoop() { MSG xMessage; //Nachricht die verarbeitet werden soll. 4 //Nachricht aus der Warteschlange holen while(GetMessage(&xMessage, NULL, 0, 0)) { //Nachricht übersetzen TranslateMessage(&xMessage); //Nachrichten ausliefern DispatchMessage(&xMessage); } 5 6 7 8 9 10 11 12 13 } Listing 2.5: Die Schleife zum Abholen und bearbeiten der Nachrichten. 2.4.4 Nachrichten verarbeiten – Die Callback-Funktion Zu diesem Zeitpunkt könnte schon ein Fenster angezeigt werden, wenn der Compiler die Funktion WindowProc finden könnte. Diese fehlt auch noch für den vierten und letzten Schritt. Die Callback-Funktion wird jedesmal von Windows aufgerufen, wenn eine Nachricht für das Fenster bereitsteht. Da die Funktion WindowProc von Windows aufgerufen wird, muss sie nach den vorgeschriebenen Konventionen aufgebaut sein. Die Funktion kann zum Beispiel wie in Listing 2.6 aussehen. Die Parameter der Funktion werden in Tabelle 2.2 aufgelistet und beschrieben. In Zeile 6 wird nachgeschaut, welche Nachricht überhaupt eingetroffen ist und entsprechend in den richtigen case-Zweig verzweigt. In diesem Beispiel werden genau zwei verschiedene Nachrichten behandelt: WM_DESTROY und WM_KEYDOWN. Erstere tritt auf, wenn das Fenster geschlossen wurde, die zweite, wenn eine Taste auf der Tastatur gedrückt wurde. In Zeile 14 wird der Parameter wParam ausgewertet, um festzustellen welche Taste gedrückt wurde. Wenn es die Escape-Taste (dargestellt durch VK_ESCAPE) war, so wird die Funktion PostQuitMessage aufgerufen. Diese wird auch aufgerufen, wenn das Fenster geschlossen wurde. Sie dient dazu, das Programm, also nicht das Fenster, zu beenden. In Zeile 22 wird die Windows-Funktion DefWindowProc aufgerufen. Innerhalb dieser Funktion sind Standardaktionen zu den Nachrichten ausprogrammiert. Man ruft sie daher für die Nachrichten auf, deren Behandlung man nicht selbst implementieren möchte. Diese Funktion sollte immer am Ende der Callback-Funktion aufgerufen werden! 10 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 KAPITEL 2. WINDOWSPROGRAMMIERUNG //Callback - Funktion für Windows LRESULT CALLBACK WindowProc(HWND hWindow, UINT uiMessage, WPARAM wParam, LPARAM lParam) { //Können wir die Message selbst verarbeiten? switch(uiMessage) { //Das Fenster wurde geschlossen case WM_DESTROY: PostQuitMessage(0); return 0; break; case WM_KEYDOWN: if(wParam == VK_ESCAPE) { PostQuitMessage(0); return 0; } break; } //Sonst Windows-Standardverarbeitung aufrufen! return DefWindowProc(hWindow, uiMessage, wParam, lParam); } Listing 2.6: Die Callback-Funktion 2.4. VIER SCHRITTE FÜR EIN FENSTER Parameter HWND hWindow UINT uiMessage WPARAM wParam LPARAM lParam 11 Beschreibung Beinaltet das Handle des Fensters, auf das sich diese Nachricht bezieht. Durch Übermittlung dieses Wertes ist es möglich eine Callback-Funktion für mehrere Fenster zu nutzen. Dieser Wert wird im Beispiel nicht beachtet, da nur ein Fenster vorhanden ist. Dieser Wert beschreibt, um welche Nachricht es sich handelt. Dies kann zum Beispiel die Mitteilung sein, dass der Benutzer das Fenster geschlossen hat oder einen Mausklick signalisieren. Diese Nachrichten haben fest vorgegebene Werte, die in winuser.h definiert sind. Es ist aber auch möglich weitere Nachrichten zu definieren. Mit diesem Wert wird unterschieden, was weiter mit der Nachricht geschehen soll. Dies erledigt der switch-Block ab Zeile 6. In diesem Paramter können weitere Informationen zu der Nachricht gespeichert sein. So wird an dieser Stelle bei einer Größenänderung des Fensters angegeben, ob das Fenster minimiert oder maximiert wurde. Auch in diesem Paramter können weitere Informationen zu der Nachricht gespeichert sein. So wird bei der Nachricht zur Größenänderung die neue Größe des Fensters gespeichert. Tabelle 2.2: Beschreibung der Parameter der Callback-Funktion 12 KAPITEL 2. WINDOWSPROGRAMMIERUNG Kapitel 3 Einführung in DirectX 3.1 Vorbereitungen Um mit DirectX Programme zu entwickeln, muss das SDK für DirectX installiert sein. Zur Zeit1 ist die Version 9.0c mit dem Update vom Juni 2005 aktuell. Beide Dateien müssen nacheinander installiert werden. DirectX gibt es in zwei Varianten: zum einen die ,normale‘ Variante und zum anderen eine ,Debug‘-Variante. Zum Entwickeln von Programmen wird immer die Debug-Variante verwendet. Bereits bei der Installation muss die Debug-Variante ausgewählt werden, da sonst nur die normale Variante installiert wird. Diese Einstellung kann nachträglich verändert werden. Dazu gib es in der Systemsteuerung einen Eintrag ,DirectX‘. Hier kann für die einzelnen Komponenten von DirectX eingestellt werden, ob die Debug- oder die normale Version verwendet werden soll (siehe Abbildung 3.1). 1 August 2005 Abbildung 3.1: Einstellung der zu verwendenden DirectX-Version 13 14 3.2 KAPITEL 3. EINFÜHRUNG IN DIRECTX DirectX im Detail DirectX ist heute auf so gut wie jedem Windows-Rechner vorhanden, aber die wenigsten wissen etwas darüber. Die häufigste Antwort auf die Frage, was denn DirectX sei oder wofür man es benötigt, ist wohl Für Computer” spiele halt.“. Das ist zwar in der Tat das größte Einsatzgebiet, aber es gibt noch weitere wie zum Beispiel Multimedia-Anwendungen und Programme zur Simulationen von Städte(bau)planungen. Auch Design- oder Konstruktionsprogramme verwenden DirectX. 3.2.1 DirectX - technisch DirectX liegt, auf Dateiebene betrachtet, als dll-Dateien im Verzeichnis WINDOWS\system32. Es handelt sich aber nicht um normale dll-Dateien, sondern um COM-Komponenten. DirectX basiert fast vollständig auf COM, weshalb auch ein kurzer Blick auf diese Technologie geworfen werden muss (siehe Kapitel 3.3 auf Seite 15). 3.2.2 DirectX-Komponenten DirectX 9 besteht aus den folgenden drei Komponenten: Komponente DirectX Graphics DirectX Input DirectX Sound Beschreibung Beinhaltet alles, was benötigt wird, um Grafiken zu erzeugen. Ist zuständig für die Ansteuerung von Eingabegeräten, wie zum Beispiel Maus und Tastatur, aber auch Joysticks, Gamepads oder Lenkräder. Des weiteren kann hiermit auch die Force-Feedback-Technik von den Geräten genutzt werden. Dient der Ausgabe von Sound und Musik in allen Variationen. Tabelle 3.2: Die Komponenten von DirectX 9 Es gibt noch vier weitere Komponenten, die aber als veraltet gelten, und daher nicht mehr verwendet werden sollen: Komponente DirectDraw DirectMusic Beschreibung Wurde genutzt, um zweidimensionale Grafiken anzuzeigen. Dies ist inzwischen vollständig in Direct3D (DirectX Graphics) und den dazugehörigen Hilfs-Funktionen enthalten. Wird verwendet um Musik abzuspielen. Dies kann noch verwendet werden, solange es keinen Ersatz gibt. Den wird es geben, sobald neue Technologien vorhanden sind. 3.3. COM – COMPONENT OBJECT MODEL Komponente DirectPlay DirectShow 15 Beschreibung Wurde verwendet, um über Netze Verbindungen aufzubauen. Aus Sicherheitsgründen soll diese Komponente nicht mehr genutzt werden. Statt dessen stehen die Windows Sockets bereit. Hiermit können Filme angezeigt werden. Kann noch verwendet werden, wird aber nicht empfohlen und muss separat installiert werden. Tabelle 3.4: Komponenten die in DirectX 9 nicht mehr aktuell sind. DirectX ist eine Hardware-unabhängige Schnittstelle zwischen Programmen und Hardware. Hardware-unabhängig, weil Funktionen, die nicht in Hardware vorhanden sind, durch Software simuliert werden können. Ist die entsprechende Hardware vorhanden, so kann auf diese direkt, also durch die (DirectX-)HAL (Hardware Abstraction Layer) zugegriffen werden. Nur so ist es überhaupt möglich, schnelle Programme zu entwickeln. 3.3 COM – Component Object Model Da der Großteil von DirectX als COM-Objekt vorliegt, soll hier noch ein kurzer Blick auf COM geworfen werden. Oft hört man, COM-Programmierung sei ein kompliziertes Thema. Diese Aussage trifft aber nur teilweise zu. Es muss unterschieden werden, ob es um das Benutzen oder das Erstellen von COM-Objekten geht. Ersteres, also das Benutzen von COM-Objekten, ist nicht schwieriger als gewöhnliche C++-Programmierung. Das Erstellen von COM-Objekten ist jedoch eine weitaus schwierigere Aufgabe, die nur mit viel Wissen über COM richtig erledigt werden kann. Glücklicherweise müssen bei der Programmierung mit DirectX keine COM-Objekte erstellt werden, sondern nur auf die vorhandenen Objekte von DirectX zugegriffen werden. Aber auch dafür wird ein grundsätzliches Verständnis des COM benötigt, welches im Folgenden vermittelt werden soll. 3.3.1 COM-Objekte Ein COM-Objekt ist eine ,Black-Box‘, welche bestimmte Funktionalitäten beinhaltet. Von außen ist nicht ersichtlich, was in ihrem Inneren passiert. COM-Objekte sind normalerweise2 in dlls gespeichert - dabei können auch mehrere in einer dll enthalten sein. Da es sich im Grunde um eine normale dll handelt, könnte man versucht sein, auf die Funktionen der dll zugreifen zu wollen. Dies ist aber nicht möglich. Alle Funktionalität befindet sich in sogenannten Schnittstellen, von denen ein COM-Objekt mindestens eine haben muss. Diese eine Schnittstelle heißt IUnknown (,I‘ von Interface). 2 Es ist auch möglich COM-Objekte in ausführbaren Dateien unterzubringen. 16 KAPITEL 3. EINFÜHRUNG IN DIRECTX IUnknown IZwei COM-Objekt IEins Abbildung 3.2: Ein typisches COM-Objekt Ein COM-Objekt mit zwei eigenen Schnittstellen (IEins und IZwei) ist in Abbildung 3.2 auf Seite 16 dargestellt. 3.3.2 Schnittstellen Über Schnittstellen ist der Zugriff auf die Funktionalität des COM-Objektes möglich. Um eine solche Schnittstelle zu erhalten, muss zuerst das entsprechende COM-Objekt erzeugt und anschließend ein Zeiger auf die Schnittstelle abgefragt werden. Dabei ist es zunächst nur möglich, auf die Schnittstelle IUnknown zuzugreifen. Über diese können dann alle anderen Schnittstellen abgefragt werden. Ist eine Schnittstelle einmal offengelegt, so darf sie nicht mehr verändert werden; es dürfen auch keine neuen Funktionen hizugefügt werden. Soll eine neue (erweiterte) Version der Schnittstelle erscheinen, so ist es üblich der Schnittstelle die Versionsnummer anzuhängen. So steht bei den meisten DirectX-Schnittstellen am Ende eine ,9‘3 . 3.3.3 GUIDs - Global Unique Identifiers Um ein bestimmtes COM-Objekt zu erzeugen oder eine Schnittstelle abzufragen, muss angegeben werden, welches Objekt beziehungsweise welche Schnittstelle erzeugt werden soll. Da der Name eines Objekts durchaus mehrfach vorhanden sein kann, gibt es GUIDs. Diese sind weltweit eindeutige Identifizierer mit 128 Bit. Gewöhnlicherweise werden sie in der Form {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} angegben4 . Ein GUID eines COM-Objektes wird als CLSID (Class Identifier) bezeichnet, wohingegen er bei einer Schnittstelle IID (Interface Identifier) genannt wird. Der Einfachheit halber wird allerdings in Dokumentationen immer der Name verwendet, zum Beispiel IDirect3D9 anstatt {1DD9E8DA-1C77-4D40B0CF-98FEFDFF9512}. Um Tippfehler und andere Irrtümer zu verhindern, 3 Zumindest bei DirectX 9 Erzeugt werden diese GUIDs mit dem Programm guidgen.exe, welches man auch von Visual Studio aus aufrufen kann (Menu Extras→GUID erzeugen) 4 3.3. COM – COMPONENT OBJECT MODEL 17 wird normalerweise auch ein Symbol für eine GUID in einer Header-Datei mitgeliefert. Diese werden im Allgemeinen mit dem Präfix CLSID_ beziehungsweise IID_ versehen. So heißt das Symbol der Schnittstelle IDirect3D9 IID_IDirect3D9. 3.3.4 Der Rückgabewert HRESULT Jede Methode eines COM-Objektes liefert als Rückgabewert einen Wert vom Typ HRESULT. An diesem Wert können zwei wichtige Informationen abgelesen werden: • Hat der Aufruf der Methode funktioniert? • Wenn nicht: Warum hat er nicht funkioniert? Es gibt auch hier, wie bei den Nachrichten, bereits vordefinierte Werte, diesmal definiert in winerror.h. Alle Rückgabewerte, die Erfolg signalisieren, tragen das Präfix S_ (Succeeded), alle anderen E_ (Error). Die beiden am häufigsten verwendeten Werte sind S_OK und E_FAIL. Wenn nur geprüft werden soll, ob ein Aufruf erfolgreich war, so sollte entweder das Makro SUCCEEDED oder aber das Makro FAILED verwendet werden. Sollen andere Werte ,zurückgegeben‘ werden, so werden ,Rückgabeparameter‘ verwendet. Das sind Parameter vom Typ Zeiger auf Wert“, denen ” man beim Aufruf einen Speicherplatz angibt, in dem die Funktion ihr Ergebnis speichern soll. Nach der Rückkehr der Funktion befindet sich das Ergebnis an der angegebenen Stelle. 3.3.5 Lebenszeit Jedes COM-Objekt zählt mit, wie oft es verwendet wird. Jedesmal, wenn eine Schnittstelle angefordert wird, wird der Zähler erhöht und jedesmal, wenn eine Schnittstelle wieder freigegeben wird, wird dieser Zähler erniedrigt. Erreicht dabei der Zähler null, so kann das Objekt aus dem Speicher entfernt werden. Damit dieser Mechanismus funktioniert, müssen nicht mehr benötigte Schnittstellen auch wirklich wieder freigegeben werden. Dies geschieht durch den Aufruf der Methode Release, welche für jede Schnittstelle aufgerufen werden muss, die nicht mehr benötigt wird. 18 KAPITEL 3. EINFÜHRUNG IN DIRECTX Teil II Mathematik 19 21 In diesem Kapitel werden die mathematischen Grundlagen erläutert. Die für DirectX wichtigen Stellen sind mit dem DirectX-Logo gekennzeichnet. 22 Kapitel 4 Vektoren Vektoren sind ein Spezialfall von Matrizen. Sie sind weniger komplex und sollen desshalb hier zuerst behandelt werden. 4.1 Grundlagen 4.1.1 Darstellung Einen Vektor kann man sich als Ortsangabe in Koordinaten vorstellen. So wäre ein 2D-Vektor zum Beispiel (1 | 2), was so viel bedeutet wie: Eine ” Einheit in X-Richtung und zwei Einheiten in Y -Richtung.“ Natürlich funktioniert das auch im dreidimensionalen Raum, dann mit drei Komponenten (zum Beispiel (1 | 2 | 3)). Im Allgemeinen hat ein Vektor beliebig viele Komponenten, was hier mit (v1 | v2 | . . . | vn ) dargestellt werden soll. Diese Darstellung wird auch als Zeilenvektor bezeichnet. Es ist auch noch eine andere Schreibweise verbreitet, bei der die einzelnen Komponenten übereinander geschreiben werden und Spaltenvektor heißt: a ~v = b c Hier soll aber die erste und einfachere Schreibweise verwendet werden. Liegt ein Vektor als Variable vor, so kennzeichnet man dies üblicherweise mit einem kleinen Pfeil wie in ~v . Grafisch wird solch ein Vektor normalerweise als Pfeil (siehe Abbildung 4.1) dargestellt. Handelt es sich um einen Ortsvektor, so beginnt dieser immer im Ursprung. Er kann aber auch irgendwo beginnen, dann spricht man von einem beliebigen Vektor. 4.1.2 Betrag eines Vektors Der Betrag eines Vektors ist seine Länge. Sie wird mit Hilfe des Satzes des Pytagoras berechnet. Die allgemeine Formel zur Berechnung der Länge sieht 23 24 KAPITEL 4. VEKTOREN Y v~1 = (1 | 3) v~2 = (4 | 1) X Abbildung 4.1: Darstellung von Vektoren wie folgt aus: v u n uX | ~v |= t vi2 i=0 Beispiel: Für den Vektor ~v = (3 | 4) ist der Betrag √ √ √ zweidimensionalen 2 2 | ~v |= 3 + 4 = 9 + 16 = 25 = 5. 4.1.3 Einheitsvektor Unter einem Einheitsvektor versteht man einen Vektor mit der Länge eins. So ist der Vektor (1 | 0 | 0) ebenso wie (0, 8 | 0, 6) ein Einheitsvektor, da sie beide die Länge eins haben. Oft ist es nötig, einen beliebigen Vektor in einen Einheitsvektor umzurechnen. Das heißt, er muss so verkleinert oder vergrößert werden, dass er die Länge eins bekommt. Das ist eine einfache Rechnung, bei der der Vektor durch seine jetzige Länge geteilt wird. Siehe hierzu das Kapitel Multiplikation. 4.2 Rechnen mit Vektoren 4.2.1 Addition und Subtraktion Die Addition zweier Vektoren ist einfach: Dabei werden lediglich die Komponenten des Vektors addiert. Sind also die Vektoren v~1 = (a | b) und v~2 = (x | y) gegeben, dann ist v~1 + v~2 = (a + x | b + y). Grafisch kann man sich das wie in Abbildung 4.2 dargestellt vorstellen. 4.2. RECHNEN MIT VEKTOREN 25 Y v~1 = (1 | 3) v~2 = (4 | 1) v~S = v~1 + v~2 = (1 + 4 | 3 + 1) = (5 | 4) v~2 v~1 v~S X Abbildung 4.2: Addition von Vektoren Diese Operation ist offensichtlich kommutativ, das heißt, es ist egal ob v~1 + v~2 oder v~2 + v~1 gerechnet wird. Die Subtraktion verläuft nach den selben Regeln – nur wird subtrahiert. 4.2.2 Multiplikation Hier wird es etwas schwieriger. Zunächst einmal muss zwischen der Multiplikation eines Vektors mit einem Skalar (also einer Zahl), der Multiplikation zweier Vektoren und dem Kreuzprodukt unterschieden werden. Ersteres wird normalerweise einfach als Multiplikation bezeichnet, wohingegen zweiteres als Punkt- oder Skalarprodukt bekannt ist. Multiplikation Hierbei wird der Vektor mit einer Zahl α multipliziert. Dabei wird jede Komponente des Vektors mit α multipliziert: ~v = (a | b), ~v · α = (a · α | b · α). Die grafische Darstellung ergibt dann, dass der Vektor dadurch ,skaliert‘ wird (siehe Abbildung 4.3). Skalarprodukt Beim Skalarprodukt werden zwei Vektoren miteinander multipliziert. Als Ergebnis erhält man einen Skalar, also eine Zahl. Die Rechenvorschrift ist wie folgt definiert: ~v · ~u = (v1 | v2 | . . . | vn ) · (u1 | u2 | . . . | un ) = (v1 · u1 + v2 · u2 + · · · vn · un ) Es werden also die jeweiligen Komponenten miteinander multipliziert und die Ergebnisse aufaddiert. 26 KAPITEL 4. VEKTOREN v~1 · 2 = (1 · 2 | 3 · 2) Y v~1 = (1 | 3) v~2 = (4 | 1) X v~2 · −0.5 = (−0.5 · 4 | −0.5 · 1) Abbildung 4.3: Multiplikation von Vektoren mit Skalaren Es gibt noch eine andere Regel, die sehr wichtig ist: ~u · ~v =| ~v | · | ~u | · cos ϕ Dabei ist ϕ der Winkel zwischen den beiden Vektoren ~u und ~v . Man kann also mit dem Skalarprodukt den Winkel zwischen zwei Vektoren bestimmen. Dazu stellt man die obige Gleichung nach ϕ um: ϕ = arccos ~u · ~v | ~v | · | ~u | Handelt es sich bei ~u und ~v um Einheitsvektoren, so kann der Teil unter dem Bruch entfallen (da er eins ist). Bildlich erhält man den Anteil (also die Länge) von ~u, der in die Richtung von ~v zeigt. Dies ist auf Abbildung 4.4 dargestellt. Kreuzprodukt Das Kreuzprodukt ist nur im dreidimensionalen Raum definiert. Es liefert zu zwei Vektoren einen senkrecht auf diesen stehenden Vektor (siehe Abbildung 4.5). Geschrieben wird das Kreuzprodukt mit einem Kreuz (×), damit es von dem Skalarprodukt unterschieden werden kann. Berechnet wird das Kreuzprodukt zu den Vektoren v~1 = (x1 | y1 | z1 ) und v~2 = (x2 | y2 | z2 ) wie 4.2. RECHNEN MIT VEKTOREN 27 Y v~1 = (1 | 3) v~2 = (4 | 1) X v~1 · v~2 Abbildung 4.4: Das Skalarprodukt Y v~1 = (1 | 2 | 2) Z v~1 × v~2 v~2 = (2 | 1 | 1) X Abbildung 4.5: Das Kreuzprodukt 28 KAPITEL 4. VEKTOREN folgt: v~1 × v~2 = (y1 · z2 − z1 · y2 | z1 · x2 − x1 · z2 | x1 · y2 − y1 · x2 ) Hinweis:Das Kreuzprodukt ist nicht kommutativ. Es gilt: v~1 × v~2 = −v~2 × v~1 . Dies kommt daher, dass das Kreuzprodukt sein Ergebnis immer nach der sogenannten Rechten-Hand-Regel liefert. Vertauscht man nun zwei Achsen (beziehungsweise Finger), so kehrt sich die andere Achse um. Achtung: Das Kreuzprodukt liefert nur dann ein sinnvolles Ergebnis, wenn die Vektoren linear unabhängig, das heißt nicht parallel sind. Sind sie es doch, so erhält man als Ergebnis (0 | 0 | 0). Die Unabhängigkeit kann zum Beispiel durch das Punktprodukt festgestellt werden. Ist also der Winkel 0◦ beziehungsweise 180◦ , dann sind die Vektoren parallel. 4.3 Vektoren in DirectX DirectX kennt natürlich Vektoren und bietet dementsprechend auch einige Datentypen für Vektoren an. Zum Einen gibt es mit D3DVECTOR eine einfache Struktur mit drei float-Werten x, y und z. Solch eine Struktur wird oft als Parameter von Methoden verwendet. Diese ist aber nur eingeschränkt nützlich, da sie nur in dieser einen Version ohne zusätzliche Funktionalität bereitsteht. Komfortabler sind da die Versionen der Zusatzbibliothek D3DX. Hier gibt es zum Einen drei Versionen von Vektoren mit zwei, drei und vier Komponenten (sie heißen D3DXVECTOR2, D3DXVECTOR3 und D3DXVECTOR4) und zum Anderen gibt es unter C++ einige Erweiterungen, die den Umgang mit Vektoren vereinfachen. Hier sind Operatoren, Konstruktoren und Methoden vorhanden, die die gängigen Rechnungen durchführen. Des Weiteren liefert die D3DX-Bibliothek auch eine Menge Funktionen zum Umgang mit Vektoren. Deren Name beginnen alle mit D3DXVec gefolgt von der Zahl der Komponenten des Vektors (also 2, 3 oder 4) und danach die Operation. Hier gibt es zum Beispiel D3DXVec3Cross zum Berechnen des Kreuzproduktes, oder D3DXVec3Length, um die Länge eines Vektors zu bestimmen. Allen Funktionen ist gemeinsam, dass sie mit der erweiterten Version des Vektors arbeiten. Kapitel 5 Matrizen 5.1 Grundlagen der Matrizen Soll ein Objekt, zum Beispiel ein Dreieck, im Raum verschoben werden, so müssen alle seine Punkte verschoben werden. Dies erreicht man am geschicktesten durch die Multiplikation mit einer Matrix. Eine Matrix ist zunächst ein mal so etwas wie eine Tabelle mit Zahlen. Allerdings kann man mit diesen Matrizen auch rechnen, näheres dazu erläutert ein entsprechendes Mathematikbuch. Es folgt eine 4 × 4-Einheitsmatrix um die Schreibweise zu verdeutlichen: 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 Dies stellt eine sogenannte Einheitsmatrix dar (siehe Kapitel 5.1.1). Um solche Matrizen im Rechner zu speichern, liefert DirectX eine Struktur D3DMATRIX die ihrerseits aus lauter float Komponenten mit dem Namen _ij besteht, wobei i und j jeweils immer zwischen 1 und 4 liegen und i dabei die Zeile und j die Spalte angibt: 5.1.1 11 21 31 41 12 22 32 42 13 23 33 43 14 24 34 44 Einheitsmatrizen Matrizen können miteinander multipliziert werden. Wird eine Matrix A mit der Einheitsmatrix (die die selbe Größe, das heißt die selbe Anzahl Zeilen und Spalten, haben muss wie Matrix A) mutipliziert, so ist das Ergebnis der Multiplikation wieder die Matrix A. 29 30 KAPITEL 5. MATRIZEN 1 2 3 4 5.1.2 ! · 1 0 0 1 ! = 1 2 3 4 ! Multiplikation von Matrizen Wenn zwei Matrizen multipliziert werden sollen, so wird jede Spalte mit jeder Zeile multipliziert. Dies geschieht wiederum durch das Multiplizieren des ersten Eintrags und der Addition der Multiplikation des zweiten Eintrags und so weiter. Damit das etwas klarer wird hier ein entsprechendes Beispiel mit einer 2 × 2 Matrix: a b c d ! · w x y z ! = a·w+b·y a·x+b·z c·w+d·y c·x+d·z ! Nach dem selben Prinzip werden auch größere Matrizen miteinander multipliziert. Damit eine Multiplikation durchgeführt werden kann, muss die Matrix, die links steht, genau so viele Spalten haben, wie die, die rechts steht, Zeilen. Bei DirectX kommt aber immer eine 4×4 Matrix zum Einsatz, so dass diese Bedingung immer erfüllt wird. 5.2 Bewegung Um nun ein Objekt zu verschieben, wird eine Matrix benötigt, die diese Transformation beschreibt. Die einfachste Form ist dabei eine Matrix, die einen Punkt verschiebt. 5.2.1 Verschieben Um einen Punkt zu verschieben, nimmt man eine Einheitsmatrix und setzt in der letzen Zeile die ersten drei Elemente auf die gewünschte Verschiebung in X, Y und Z: 1 0 0 0 0 1 0 0 0 0 1 0 ∆X ∆Y ∆Z 1 Beispiel Der Punkt P = (1 | 2 | 0) soll um zwei Einheiten nach rechts (∆X), eine nach oben(∆Y ) und drei nach hinten(∆Z) verschoben werden. Dazu muss aber der Vektor P zunächst um eine Komponente erweitert werden, damit er genau so viele Spalten hat, wie die Matrix Zeilen, da sonst die Multiplikation nicht möglich ist. Erweitert wird er durch eine Komponente die 5.2. BEWEGUNG 31 Y (3 | 3 | 3) Z (1 | 2 | 0) X Abbildung 5.1: Verschieben eines Punktes das Ergebnis nicht verändert: eine 1. Damit die Gleichung nach dem dritten Gleichheitszeichen nicht unendlich lang wird, habe ich die einzelnen Komponenten übereinander geschrieben. Abbildung 5.1 stellt den Sachverhalt noch einmal grafisch dar. P0 = P · M 1 0 0 2 0 1 0 1 0 0 1 3 + + + + 0·0 0·0 0·1 0·0 + + + + 1·2 1·1 1·3 1·1 = (1 | 2 | 0 | 1) · 1·1 + 2·0 1·0 + 2·1 = 1·0 + 2·0 1·0 + 2·0 = (3 | 3 | 3 | 1) 5.2.2 0 0 0 1 Rotation Es gibt eine allgemeine Matrix für Rotationen in alle Richtungen. Allerdings ist diese sehr unübersichtlich, deshalb trennt man sie gerne in drei Matrizen auf. Es muss also für die Rotation um eine Achse je eine einzelne Matrix 32 KAPITEL 5. MATRIZEN verwendet werden. Rotiert wird dabei immer um den Ursprung1 , also die Koordinate (0 | 0 | 0). Es gibt also drei Matrizen, eine für die Drehung um die X-Achse, eine für die Y-Achse und eine für die Z-Achse: X-Achse Rotx = 1 0 0 0 0 cos(a) sin(a) 0 0 − sin(a) cos(a) 0 0 0 0 1 Y-Achse Roty = 0 0 0 1 cos(a) sin(a) 0 0 − sin(a) cos(a) 0 0 0 0 1 0 0 0 0 1 cos(a) 0 sin(a) 0 0 − sin(a) 1 0 0 cos(a) 0 0 Z-Achse Rotz = Beispiel Hier soll ein Vektor der drei Einheiten nach oben weist um 45◦ nach rechts gedreht werden. Wird mit 45◦ gerechnet, so würde der Vektor nach links gedreht werden, da Winkel immer gegen den Uhrzeigersinn gemessen werden. Hier wird desshalb mit -45◦ gerechnet. Außerdem werden alle Zahlen nach der ersten Nachkommastelle abgeschnitten. Abbildung 5.2 stellt den Sachverhalt grafisch dar. 1 Es gibt auch Matrizen, die um einen anderen Punkt drehen, diese sollen hier aber nicht näher betrachtet werden. 5.2. BEWEGUNG 33 Y (2, 1 | 2, 1 | 0) (0 | 3 | 0) X Abbildung 5.2: Rotation eines Vektors ~ = (0 | 3 | 0) V ~ · Rotz V~ 0 = V = (0 | 3 | 0 | 1) · = (0 | 3 | 0 | 1) · cos(−45) sin(−45) 0 0 − sin(−45) cos(−45) 0 0 0 0 1 0 0 0 0 1 0, 7 −0, 7 0, 7 0, 7 0 0 0 0 0· 0, 7 + 3·0, 7 0·−0, 7 + 3·0, 7 = 0· 0 + 3· 0 0· 0 + 3· 0 = (2, 1 | 2, 1 | 0 | 1) 5.2.3 + + + + 0·0 0·0 0·1 0·0 + + + + 0 0 1 0 1·0 1·0 1·0 1·1 0 0 0 1 Bewegungen kombinieren Hat man alle benötigten Matrizen erstellt, so können diese miteinander Multipliziert werden, und ergeben eine kombinierte Matrix, die alle der angegebenen Transformationen auf einmal durchführt. 34 KAPITEL 5. MATRIZEN Urspungszustand Erster Schritt Zweiter Schritt Erst Rotiert, dann Verschoben Erst Verschoben, dann Rotiert Abbildung 5.3: Verschiedene Ergebnisse bei Matrix-Multiplikationen Dabei ist aber zu beachten, dass es wichtig ist, in welcher Reihenfolge die Matrizen miteinander multipliziert werden. So bietet es sich an, zuerst alle Rotationsmatrizen miteinander zu multiplizieren und dann erst das nun schon gedrehte Objekt zu verschieben (siehe hierzu Abbildung 5.3). 5.3 Bewegung mit Direct3D Um diese Matrizen zu verwenden, werden diese an Direct3D mit SetTransform übergeben. Direct3D kümmert sich dann um die eigentliche Transformation der Punkte, dies eventuell sogar in Hardware, was einen gewaltigen Geschwindigkeitsvorteil mit sich bringt. Danach werden alle Punkte normal an Direct3D übergeben. Diese Punkte werden als so genannte lokale Koordinaten übergeben, DirectX rechnet die Punkte dann in Weltkoordinaten um. Kapitel 6 Ebenen Ebenen werden neben Vektoren und Matrizen ebenso sehr häufig benötigt. Eine Ebene ist eine unendlich weit ausgedehnte und unendlich dünne, ebene Fläche im Raum. Ein Dreieck liegt zum Beispiel in genau einer Ebene. 6.1 Darstellung Es gibt verschiedene Möglichkeiten eine Ebene darzustellen. Die wohl bekannteste Form ist die Komponentenschreibweise ax + by + cz + dw = 0 Diese Form bietet DirectX in der Zusatzbibliothek sogar schon unter dem Namen D3DXPLANE an. Hier soll sie aber nicht weiter behandelt werden, da sie sich nicht besonders gut zur Programmierung eignet. Hier soll daher die Vektorschreibweise ~ · P~ = d N verwendet werden, da sie einige Vorteile bietet. Die einzelnen Komponenten sind: ~ Hierbei handelt es sich um den normierten Normalenvektor der Ebene, N also ein Vektor der Länge eins, der senkrecht auf der Ebene steht. P~ bezeichnet einen beliebigen Punkt auf der Ebene. d ist der Abstand zum Ursprung. Dabei wird der kürzest mögliche Abstand gewählt. Diesen erhält man, wenn man den Normalenvektor im Ursprung starten lässt und ihn bis zur Ebene verlängert. Abbildung 6.1 stellt die einzelnen Werte grafisch dar. 35 36 KAPITEL 6. EBENEN Y Ebene ~ N P~ d X ~ ist hier ebenso wie der Abbildung 6.1: Eine Ebene (Der Normalenvektor N Punkt P~ an einer beliebigen Stelle eingezeichnet.) 6.2 6.2.1 Berechnungen Berechnen einer Ebene Da immer alle drei Komponenten vorhanden sein sollen, müssen diese unter Umständen berechnet werden. Sind drei Punkte auf der gesuchten Ebene gegeben Das einfachste ist hierbei sicherlich der Punkt auf der Ebene: Er ist einfach einer der drei gegebenen Punkte. Etwas komplizierter ist da schon die Berechnung des Normalenvektors. Er steht senkrecht auf der Ebene, und alle drei Punkte sollen auf der Ebene liegen. Das bedeutet, dass man sich zwei Vektoren berechnen kann die einen beliebigen Winkel zueinander haben, aber beide auf der Ebene liegen. Das Kreuzprodukt bestimmt nun den senkrecht darauf (und damit auf der Ebene) stehenden Vektor. Dieser muss dann noch normalisiert werden. Als letztes muss der Abstand zum Ursprung festgestellt werden. Dazu wird erneut der Punkt auf der Ebene benötigt, der in diesem Fall als Vektor betrachtet werden soll. Jetzt wird der Anteil dieses Vektors in Richtung des Normalenvektors berechnet, was das Skalarprodukt sehr gut erledigt. 6.2. BERECHNUNGEN P~ ~ N 37 = P1 = (P2 − P1 ) × (P3 − P1 ) ~ d = P~ · N Ist der Normalenvektor und die Distanz gegeben Dies ist sicherlich eine der einfachsten Varianten. Da der Normalenvektor und die Distanz bereits gegeben sind, können diese einfach übernommen werden. Es fehlt nur noch ein Punkt auf der Ebene. Dieser lässt sich jedoch sehr einfach aus den beiden vorhandenen Werten berechnen. Schließlich ist ein Punkt auf der Ebenen dort, wo eine Gerade in Richtung des Normalenvektors die Ebene schneidet. Geht diese Gerade durch den Ursprung, so muss lediglich der Normalenvektor mit der Distanz multipliziert werden um einen Punkt zu erhalten. P~ ~ N ~ ·d = N ~ = N d = d 6.2.2 Klassifikation eines Punktes bezüglich einer Ebene Häufig muss herausgefunden werden, wo sich ein Punkt in Bezug zur Ebene befindet. Dabei können drei verschiedene Ergebnisse eintreten: • Der Punkt liegt vor der Ebene, • der Punkt liegt hinter der Ebene oder • der Punkt liegt auf der Ebene. Soll nun bestimmt werden, ob ein Punkt P1 hinter, auf oder vor einer Ebene liegt, so wird als erstes ein Vektor von P1 zu einem Punkt auf der Ebene berechnet. Anschließend wird das Punktprodukt mit diesem und dem Normalenvektor gebildet, also dieser Vektor auf den Normalenvektor abgebildet. Ist das Ergebnis k nun positiv, so liegt P1 hinter der Ebene, bei 0 auf dieser und bei einem negativen Wert davor. ~ k = (P~ − P~1 ) · N 38 KAPITEL 6. EBENEN Teil III DirectX Graphics 39 Kapitel 7 DirectX Graphics initialisieren 7.1 DirectX Graphics - Einführung Mit DirectX Graphics können Grafiken, egal ob zwei- oder dreidimensional, erzeugt werden. Doch was heißt das eigentlich? Zunächst einmal sollte überlegt werden, wie die einfachste Form aussieht, die ein Fläche besitzt. Ein einzelner Punkt und eine Linie, also die Verbindung von zwei Punkten, haben sicherlich keine Fläche. Drei Punkte hingegen bilden ein Dreieck, welches eine Fläche besitzt und noch dazu eine andere, schöne Eigenschaft hat: Diese Fläche liegt auf einer Ebene. Das heißt, ein Dreieck aus drei Punkten kann nicht ,gebogen‘ sein. (Wenn doch, so muss es mit mehrerern Dreiecken dargestellt werden!) Nun kann jede beliebige Form mit Dreiecken angenähert werden. Dies führt dazu, dass, wenn man Dreiecke auf den Bildschirm zeichnen kann, man jedes beliebige Objekt darstellen kann. Genau das erledigen Grafikkarten in unglaublich hoher Geschwindigkeit in Hardware. Mit DirectX Graphics existiert eine Schnittstelle zu solchen Grafikkarten1 . 7.2 Vorbemerkungen In Kapitel 2 wurde ein einfaches Fenster erzeugt, welches man lediglich wieder schließen konnte, von zwei- oder gar dreidimensionalen Zeichnungen ganz zu schweigen. In diesem Kapitel werden nun die Grundlagen dafür gelegt, dass später Zeichenoperationen durchgeführt werden können. Dazu muss als erstes Direct3D initialisiert werden. Direct3D ist der Hauptteil von DirectX Graphics. Weitere Teile sind Shader, Hilfsroutinen, 1 Sollte in dem Rechner keine Grafikkarte enthalten sein, die diese Fähigkeiten besitzt, emuliert DirectX Graphics diese Funktionalität in Software auf der CPU – freilich wesentlich langsamer. 41 42 KAPITEL 7. DIRECTX GRAPHICS INITIALISIEREN Effekte und Dateiformate. Bei der Initialisierung muss Direct3D mitgeteilt werden, welche Einstellungen verwendet werden sollen. Mit Einstellungen sind Auflösung, Farbtiefe aber auch Grafikkarten (falls mehr als eine vorhanden sein sollten) gemeint. Dafür werden Funktionalitäten aus der d3d9.lib benötigt. Damit sich das Programm linken lässt, muss also diese Bibliothek dem Linker als Eingabedatei mitgegeben werden. Dies erreicht man unter den Eigenschaften des Projekts (Menu Projekt→Eigenschaften von ...), dort unter ,Linker‘ und ,Eingabe‘. Hier wird dem Wert Zusätzliche Abhängigkeiten“ d3d9.lib hin” zugefügt. 7.3 Fachbegriffe Zunächst werden ein paar Fachbegriffe erklärt, die bei der Verwendung von DirectX Graphics immer wieder auftauchen: 7.3.1 Adapter Ein Adapter ist eine Grafikkarte. 7.3.2 Device Jeder Adapter hat ein oder mehrere angeschlossenen 3D-Devices. Ein Device ist das Stück Soft- oder Hardware, das die eigentliche Arbeit verrichtet. Hiervon gibt es normalerweise bei jeder Grafikkarte (also jedem Adapter) zwei: Einmal das ganz normale HAL-Device. Dies ist die echte Hardware, das heißt, alle Operationen werden hier in der Hardware des Grafikchips ausgeführt. Als zweites sollte auch immer ein Ref-Device zur Verfügung stehen. Dieses Device ist eine reine Software-Referenz-Implementierung. Da es sich dabei um Software handelt, die auf der CPU ausgeführt wird, ist dieses Device natürlich langsamer. Es sollte daher nur zu Testzwecken genutzt werden. Des weiteren gibt es noch zwei andere Devices: Das NullRef-Device, wird verwendet, wenn keine Hardwarebeschleunigung und kein Ref-Device vorhanden sind (dies sollte eigentlich nie der Fall sein). Das SW-Device ist eine reine Softwarelösung, die aber nicht das gesamte Funktionsspektrum abdeckt. 7.3.3 Modus Die Auflösung mit Bildwiederholfrequenz und Farbtiefe bezeichnet man als einen Modus. 7.4. INITIALISIERUNG 7.3.4 43 Format Jeder Grafikprozessor benutzt eine bestimmten Menge RAM (Random Access Memory). In diesem Speicher werden Texturen, 3D-Daten aber auch der aktuelle Bildschirminhalt abgelegt. Die Art und Weise, wie diese Daten im Speicher liegen, nennt man Format. Diese Formate werden unter Direct3D in der Aufzählung D3DFORMAT gelistet. Zum Beispiel steht D3DFMT_A8R8G8B8 für 8 Bit alpha, 8 Bit rot, 8 Bit grün und 8 Bit blau“. Das heißt, für jede ” Komponente stehen zum Speichern 8 Bit zur Verfügung. Dadurch erstreckt sich der Wertebereich pro Komponente von 0 bis 256. Das ähnliche Format D3DFMT_X8R8G8B8 unterscheidet sich nur bei der Alpha-Komponente. Diese gibt es hier nicht, die ersten 8 Bit werden schlicht nicht beachtet. 7.3.5 Buffer Bei Direct3D gibt es diverse Buffer. In ihnen werden aktuelle Rechenergebnisse, wie das aktuelle Bild, das nächste Bild, Tiefeninformationen und so weiter abgelegt. Es gibt drei wichtige Buffer: Frontbuffer, Backbuffer und Depthbuffer (bei Direct3D immer in Verbindung mit dem Stencilbuffer). Jeder dieser Buffer muss mit dem richtigen Format angelegt werden. 7.3.6 Front- und Backbuffer Im Grafikspeicher, im sogenannten Frontbuffer, liegt ein Abbild des momentan auf dem Monitor dargestellen Bildes. Dieser Speicher wird kontinuierlich ausgelesen und zum Monitor gesendet. Würde man nun in diesen Speicher schreiben, so würde es zwangsläufig dazu kommen, dass der Monitor zum Beispiel die obere Hälfte des alten Bildes bereits angezeigt hat, dann würde (in den Puffer) gezeichnet werden und dann die untere Hälfte des Bildes angezeigt. Um das zu verhindern, zeichnet man nicht in den Frontbuffer, sondern in einen zweiten Puffer gleicher Größe, den sogenannten Backbuffer. Erst wenn das Bild vollständig fertig ist, wird es in den Frontbuffer kopiert, oder besser, Front- und Backbuffer werden getauscht. 7.3.7 Depthbuffer Der Tiefenpuffer (auch Z-Buffer) hat normalerweise ein anderes Format als der Front- oder Backbuffer. In ihm werden Tiefeninformationen, also die entfernung von Pixeln zur Kamera, gespeichert. Auch hier muss für jeden Pixel ein Wert gespeichert werden, normalerweise ist dies ein Gleitkommawert. 7.4 Initialisierung Es gibt zwei Möglichkeiten Direct3D zu initialisieren: Einmal die einfache, aber schlechte und einmal die komplizierte aber gute Methode. Im Folgen- 44 KAPITEL 7. DIRECTX GRAPHICS INITIALISIEREN dem werden beide etwas genauer betrachtet: Beiden Methoden ist gemeinsam, dass als erstes ein Zeiger auf das Interface IDirect3D abgefragt werden muss: 1 2 //Zeiger auf das Interface IDirect3D9 LPDIRECT3D9 pxD3D; 3 4 5 6 7 8 9 //Erstellen des Direct3D-Hauptobjektes pxD3D = Direct3DCreate9(D3D_SDK_VERSION); if(pxD3D == NULL) { //...Fehlerbehandlung } Listing 7.1: Erstellung des Direct3D-Hauptobjektes Hier wird durch den Aufruf der freien Funktion Direct3DCreate9 versucht ein Direct3D-Objekt zu erstellen und einen Zeiger auf die Schnittstelle IDirect3D zu erhalten. Diese Schnittstelle bietet Zugiff auf das Direct3DHauptobjekt. Sie bietet Methoden zur Überprüfung der vorhanden Hardware und eine Methode um ein Direct3D-Device zu erstellen. Die Initialisierung, um die es in diesem Kapitel gehen soll, betrifft daher nicht diese IDirect3D-Schnittstelle, sondern eine vom Typ IDirect3DDevice, über die später alle Aufgaben ausgeführt werden. 7.4.1 Initialisierung mit Standardwerten Die einfache Version besteht aus nur sehr wenig Code, ermöglicht aber auch nur eine sehr eingeschränkte Funktionalität. Angenommen, in einem Rechner wäre ein standardmäßig laufender Onboard-Grafikchipsatz mit beschränkter 3D-Funktionalität und eine Hochleistungsgrafikkarte der neusten Generation installiert. Nun wird Direct3D mit einfachen Standardwerten initialisiert. Bei dieser Variante wird leider zwangsläufig der Onboard-Chipsatz verwendet. Es könnte sein, dass der Anwender damit nicht so ganz glücklich wird. Dann sollte die kompliziertere Variante genutzt werden. In dieser einfachen Variante werden sichere Standardwerte eingesetzt, ohne dass Rücksicht auf die tatsächlich vorhandene Hardware genommen wird. Zuerst (ab Zeile 9 im Listing 7.2) wird die Struktur xD3DPresentParameters mit den richtigen Werten belegt. Diese Struktur gibt an, wie das Direct3D-Device initialisiert werden soll. Zum einen wird hier gesagt, dass das Programm im Fenster (statt Vollbild) laufen soll, zum anderen wird festgelegt, wie die einzelnen Bilder angezeigt werden sollen. Näheres dazu findet sich weiter unten. 7.4. INITIALISIERUNG 1 2 3 4 //Zeiger auf IDirect3DDevice LPDIRECT3DDEVICE9 pxD3DDevice; //Parameter der Anzeige D3DPRESENT_PARAMETERS xD3DPresentParameters; 5 6 7 ZeroMemory(&xD3DPresentParameters, sizeof(D3DPRESENT_PARAMETERS)); 8 9 10 11 12 13 14 15 16 17 18 //Fenster-Modus xD3DPresentParameters.Windowed = true; //Anzahl der Backbuffer xD3DPresentParameters.BackBufferCount = 1; //Backbuffer durch Vertauschen zum Frontbuffer machen xD3DPresentParameters.SwapEffect = D3DSWAPEFFECT_FLIP; //Backbuffer kann verriegelt werden, d.h. man kann darauf //zeichnen xD3DPresentParameters.Flags = D3DPRESENTFLAG_LOCKABLE_BACKBUFFER; 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 //Erstellen des Direct3D Devices if(FAILED(pxD3D->CreateDevice( //Adapter D3DADAPTER_DEFAULT, //Device D3DDEVTYPE_HAL, //Handle des Fensters hMainWindow, //Optionen D3DCREATE_SOFTWARE_VERTEXPROCESSING, //Parameter &xD3DPresentParameters, //Ergebnis &pxD3DDevice))) { //...Fehlerbehandlung } Listing 7.2: Initialisierung von Direct3D mit Standardwerten 45 44 KAPITEL 7. DIRECTX GRAPHICS INITIALISIEREN Adapter 1 * Device 1 * Mode Abbildung 7.1: Datenstruktur zur Enumeration Der nächste Schritt ist nun das Anlegen des Device. Dabei werden diverse Werte mitgegeben, die im Folgenden erläutert werden: Zeile 23 25 27 29 31 33 Beschreibung Ist der zu verwendende Adapter. D3DADAPTER DEFAULT gibt an, dass der Adapter verwendet werden soll, der auch den WindowsDesktop verwendet. Ist der Typ des zu erstellenden Device. Hier soll ein HAL-Device (siehe oben) erstellt werden. Gibt das Handle des Fensters an, in das gezeichnet werden soll (und das die Nachrichten empfangen soll). Gibt weitere Optionen an, die das Device betreffen. Hier: Vertices sollen in Software bearbeitet werden. Weist einen Zeiger auf die eben befüllte Struktur zu. Hier wird der Zeiger auf das Ergebnis angegeben. Tabelle 7.2: Parameter, die bei der Erstellung des D3D-Devices verwendet werden. Mit Hilfe des Makros FAILED kann überprüft werden, ob die Operation erfolgreich war. 7.4.2 Optimale Initialisierung Hier wird vor dem Initialisieren die vorhandene Hardware ,überprüft‘. Dazu listet Direct3D die vorhandene Hardware auf – dieser Vorgang wird Enumeration genannt. Zuerst einen Überblick über alle zu enumerierenden Dinge: 1. Alle vorhandenen Grafikadapter 2. Bei jedem Adapter: Welche Modi kann dieser darstellen? 3. Bei jedem Adapter: Welche Devices unterstützt dieser? (Dieser Schritt kann unter Umständen entfallen, wenn man sich nur auf ein Device beschränkt.) Um am Ende die beste Kombination auszuwählen zu können, ist es zweckmäßig, sich alle abgefragen Werte abzuspeichern. Dazu benötigt man eine Datenstruktur wie sie in Abbildung 7.1 dargestellt ist. 7.4. INITIALISIERUNG 45 Adapter Als erstes soll festgestellt werden, wieviele und welche Adapter es im System gibt. Zu jedem Adapter sollte man sich Folgendes merken: • Die Eigenschaften des Adapters: Hierfür bietet DirectX bereits eine Struktur – D3DADAPTER_IDENTIFIER9. Diese kann mit der Funktion GetAdapterIdentifier abgerufen werden. • Den aktuellen Modus: Wenn das Programm im Fenster-Modus laufen soll, so muss es den selben Modus benutzen, den der Adapter bereits verwendet. Dieser kann mit der Methode GetAdapterDisplayMode abgefragt werden. • Die verschiedenen Devices Um festzustellen, wie viele Adapter sich im System befinden, verwendet man die Methode GetAdapterCount. In einer Schleife durchläuft man danach alle Adapter. Modi Da die Modi für alle Devices gültig sind, fragt man als nächstes alle vorhandenen Modi ab. Um herauszubekommen, wie viele Modi der Adapter überhaupt unterstützt, gibt es die Methode GetAdapterModeCount. Nun kann man in einer Schleife EnumAdapterModes mit der jeweiligen ModusNummer als Argument aufrufen. Als Ergebnis (per Rückgabeparameter) erhält man eine Struktur vom Typ D3DDISPLAYMODE, die alle Informationen zu dem Modus enthält: Breite, Höhe, Bildwiederholrate und Format. Bei beiden Funktionen muss von vornherein angegeben werden, für welches Format die Modi betrachtet werden sollen. Möchte man herausfinden, welche Formate unterstützt werden, müssen leider alle einzeln durchprobiert werden. Nun sollte man prüfen, ob für dieses Format Hardwarebeschleunigung möglich ist. Dazu verwendet man die Methode CheckDeviceType. Dieser übergibt man die Nummer des Adapters, als Device-Typ D3DDEVTYPE_HAL und das Front- und Backbuffer-Format als Parameter mit. Das BackbufferFormat ist dabei idealerweise das Frontbuffer-Format mit Alpha-Anteil, das heißt aus D3DFMT_X8R8G8B8 wird D3DFMT_A8R8G8B8. Dies ist auch der richtige Zeitpunkt, um festzustellen, welches DepthStencil-Format das Device mit dem verwendeten Format handhaben kann. Dazu durchläuft man alle erwünschten Depth-Stencil-Formate und prüft, ob diese mit diesem Adapter, Device und Format nutzbar sind. Diese Überprüfung geschieht mit den Methoden CheckDeviceFormat und CheckDepthStencilMatch. 46 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 KAPITEL 7. DIRECTX GRAPHICS INITIALISIEREN if(SUCCEEDED(pxD3D->CheckDeviceFormat( //AdapterNr uiAdapter, //HAL, REF, ... xDeviceType, //Gewünschtes (Front)Buffer-Format xAdapterFormat, //Auf Depth + Stencil prüfen D3DUSAGE_DEPTHSTENCIL, //Resourcetype D3DRTYPE_SURFACE, //Gewünschtes Depth-Stencil-Format xFormat))) { if(SUCCEEDED(pxD3D->CheckDepthStencilMatch( //AdapterNr uiAdapter, //HAL, REF, ... xDeviceType, //Display Mode xAdapterFormat, //Render Target (=Backbuffer) xAdapterFormat, //Gewünschtes Depth-Stencil-Format xFormat))) { //xFormat ist ein mögliches Depth-Stencil-Format } } Listing 7.3: Finden eines passenden Depth-Stenzil-Puffers 7.4. INITIALISIERUNG 47 Device Dies ist der Teil mit der größten Vielfalt an Möglichkeiten. Die einfachste ist, sich lediglich auf das HAL-Device zu beschränken (was auch einigermaßen praxistauglich sein sollte, da alle anderen Devices schlicht zu langsam sind). Dazu prüft man mit der Methode CheckDeviceType ob der Adapter für die entsprechenden Formate ein HAL-Device anbietet. Bietet der Adapter die nachgefragte Kombination nicht an, so wird diese gleich aus der Liste entfernt. Des weiteren ist es jetzt möglich, bestimmte Eigenschaften des Device abzufragen und dabei zu überprüfen, ob bestimmte Funktionen angeboten werden. Diese Eigenschaften werden unter DirectX als ,Capabilities‘, kurz ,Caps‘ bezeichnet, und können mit der Methode GetDeviceCaps abgefragt werden. Es hat sich als praktisch herausgestellt, diese Werte bei Programmstart einmal abzufragen, und dann für den Rest der Programmlaufzeit zu behalten. So kann immer, wenn sie benötigt werden, schnell darauf zugegriffen werden. Schon bei der Initialisierung kann überprüft werden, ob bestimmte Eigenschaften vorhanden sind, und, wenn diese nicht vorhanden sein sollten, der Adapter aus der Liste entfernt werden. Wird kein Adapter oder Modus gefunden, so kann das Programm auf dem Zielrechner nicht ausgeführt werden. Dies sollte mit einer aussagekräftigen Fehlermeldung kundgetan werden. Um sich einen Überblick über die auf dem (Entwicklungs)System vorhandenen Adapter, Modi und Devices zu verschaffen, verwendet man das Programm DXCapsViewer.exe. Es listet, nicht nur für Direct3D, vorhandene Hardware mit ihren Eigenschaften auf (siehe Abbildung 7.2 auf Seite 48). Initialisierung Nun sind alle Informationen gesammelt und es ist bekannt, ob ein verwendbarer Adapter im System vorhanden ist. Auch ist bekannt, welcher der beste Adapter, Modus beziehungsweise das beste Device ist. Anhand dieser gesammelten Informationen kann nun ein Device initialisiert werden, welches wesentlich besser auf die aktuelle Hardware eingestellt ist, als bei der einfachen Lösung. Dies bedingt aber, dass die gesammelten Informationen weiter ausgewertet werden. Es sollte unbedingt ausgewertet werden, ob das Device Transformationen in Hardware unterstützt, das heißt, ob es Punkte in Hardware berechnen kann. Dazu kann in den Eigenschaften des Device der Wert DevCaps überprüft werden, ob er D3DDEVCAPS_HWTRANSFORMANDLIGHT enthält. Tut er dies, so sollte das Device entsprechend erstellt werden (Eigenschaft = D3DCREATE_HARDWARE_VERTEXPROCESSING). Ebenso verhält es sich mit der Eigenschaft D3DDEVCAPS_PUREDEVICE (Eigenschaft zum Erstellen: D3DCRE- 48 KAPITEL 7. DIRECTX GRAPHICS INITIALISIEREN Abbildung 7.2: DirectX Caps Viewer 7.4. INITIALISIERUNG 49 ATE PUREDEVICE). Die Initialisierung erfolgt dann wie in der einfachen Variante, mit dem Unterschied, dass weitere Informationen mit angegeben werden. So wird nun als Adapter nicht mehr D3DADAPTER_DEFAULT gewählt, sondern der beste vorhandene. Außerdem wird D3DCREATE SOFTWARE VERTEXPROCESSING durch die oben erkannten Eigenschaften ersetzt. Als letzte Änderung wird schließlich noch angegeben, welches Depth-Stencil- und BackbufferFormat verwendet werden soll. Dies geschieht durch drei Attribute der D3DPRESENT PARAMETERS-Struktur: 1 2 3 4 5 6 7 //Das zu verwendende Format des Backbuffers xD3DPresentParameters.BackBufferFormat = xD3DBackBufferFormat; //Depth-Stencil wird von Direct3D verwaltet xD3DPresentParameters.EnableAutoDepthStencil = TRUE; //Gefundenes Format angeben xD3DPresentParameters.AutoDepthStencilFormat = xD3DDepthStencilFormat; Listing 7.4: Belegung der Struktur D3DPresentParameters mit gefunden Werten Bei der Angabe des Backbuffer-Formats sollte natürlich wieder ein Format mit Alpha-Anteil gewählt werden. 50 KAPITEL 7. DIRECTX GRAPHICS INITIALISIEREN Kapitel 8 Zeichnen einfacher Formen 8.1 Einführung Bis jetzt zeigt das Programm nur ein weißes Fenster an. Zwar ist DirectX bereits initialisiert, aber ohne Zeichen-Anweisungen verändert sich im Fenster logischerweise nichts. Dies soll sich nun ändern. Dazu wird zunächst ein Dreieck in 2D gezeichnet werden, bevor alles in die dritte Dimension ausgeweitet wird. 8.2 Rendern in 2D Alle zu zeichnenden Objekte müssen Direct3D als Liste von Punkten angegeben werden. Diese Punkte werden dann nach der angegebenen Art verbunden und bilden dadurch ein sogenanntes Primitive. So sind ein Dreieck, aber auch Linien und Punkte Primitive. 8.2.1 Der Viewport Der Viewport ist der Teil des Bildschirms, auf den mit Direct3D gezeichnet werden soll. Es handelt sich also um den Bereich, in dem die Szene dargestellt werden soll. Dies muss nicht unbedingt der ganze Bildschirm oder das ganze Fenster sein. Es könnte zum Beispiel oben noch ein Menü, unten StatusInformationen und links einige Knöpfe angezeigt werden. Dann bleibt als 3D-Bereich nur noch ein Teil des gesamten Bildschirms oder Fensters übrig. Bei Direct3D werden alle Attribute zu einem Viewport in der Struktur D3DVIEWPORT9 gespeichert. Dieser kann mit den Methoden GetViewport abgefragt und mit SetViewport gesetzt werden. Die Struktur enthält die Koordinate der linken oberen Ecke (X und Y) und die Größe des Viewports (Width und Height) in Bildschirm-Koordinaten. Bei diesen Bildschirm-Koordinaten ist der Ursprung, also die Koordinate (0, 0), in der linken oberen Ecke (siehe Abbildung 8.1). 51 52 KAPITEL 8. ZEICHNEN EINFACHER FORMEN Fenster / Bildschirm Viewport X,Y Height Width Abbildung 8.1: Der Viewport 8.2.2 Punkte angeben Da Direct3D die angegebenen Punkte verarbeiten soll, muss man sich an die Konventionen von Direct3D halten. Ein Punkt wird unter Direct3D normalerweise als Vertex (Mehrzahl: Vertices) bezeichnet. Zu einem Punkt können sehr viele verschiedene Daten gehören (Positionen in verschiedenen Formen, verschiedene Farben, ebenso verschiedene Textur-Informationen und so weiter), die aber nicht immer alle benötigt werden. Die Menge der Daten, die zu den Punkten gehören, sollte möglichst klein sein, da sie alle an die Grafikkarte übermittelt und dort verarbeitet werden müssen. Je mehr Daten das sind, um so länger dauert es. Um diesem Problem entgegen zu wirken, definiert man sich ein genau passendes Format für die Vertices, welches nur diejenigen Daten enthält, die wirklich benötigt werden. Dazu bietet DirectX das Flexible Vertex Format oder kurz FVF. Dies bedingt allerdings auch, dass man Direct3D mitteilen muss, wie die eigenen Vertex-Strukturen aufgebaut sind. Der erste Schritt zu einem eigenen FVF ist das Erstellen einer entsprechenden Struktur, in der ein Vertex gespeichert wird. In Listing 8.1 wird zu einem Vertex die Koordinate in Viewport-Koordinaten in den Attributen x, y und z gespeichert, wobei der Z-Anteil nicht berücksichtigt wird, da der Monitor ja schließlich keine dritte Dimension bietet (Zeile 9). Beim Viewport ist der Ursprung in der linken oberen Ecke, X wächst nach rechts und Y nach unten. Das Attribut rhw ist eine Art Gewichtung des Punktes. Je größer der Wert, desto mehr Gewicht hat er. Das bedeutet hier, dass sich die Farbe dieses Vertex mehr ,ausbreitet‘. rhw steht dabei für reciprocal of homogeous w und soll vorerst auf den Wert 1.0 gesetzt werden. Bei diesen Viewport-Koordinaten spricht man im Allgemeinen von trans- 8.2. RENDERN IN 2D 1 2 3 4 5 6 7 8 9 10 11 12 13 14 53 /** * Ein einfacher Punkt in Monitor Koordinaten mit Farbe. */ struct D3DVERTEX_2D { /** * Koordinate des Punktes */ float x, y, z, rhw; /** * Farbe des Punktes */ D3DCOLOR xColor; }; Listing 8.1: Struktur der Vertexdaten für 2D formierten Koordinaten. Normalerweise werden Punkte in 3D angegeben. Diese werden dann durch mehrere Transformationen in Bildschirm- oder besser Viewport-Koordinaten umgewandelt. Als letztes Attribut ist schließlich noch eine Farbe vorgesehen. Es handelt sich hierbei um die Farbe des so genannten diffusen Lichts. Dieses Licht kommt von einer ganz bestimmten Position aus, hat also eine Richtung. Das heißt, dass Flächen, die senkrecht zu den Lichtstrahlen stehen, besonders stark und solche die ,flach‘ stehen, besonders wenig Licht abbekommen. Damit sind Plätze für alle Daten vorhanden, aber Direct3D muss noch mitgeteilt werden, welche Werte überhaupt pro Vertex angegeben werden. Dies geschieht mit der Beschreibung der Struktur am Besten wie in Listing 8.2 angegeben. 1 #define D3DFVF_2D (D3DFVF_XYZRHW | D3DFVF_DIFFUSE) Listing 8.2: Beschreibung der Vertex-Daten für 2D Dabei wird eine neue Konstante definiert, die später an Direct3D übergeben werden kann. Dabei wird angegeben, dass der Vertex die Koordinate im XYZRHW-Format und eine Farbe für die diffuse Beleuchtung enthält. 8.2.3 Farben Farben werden häufig im RGB-Farbraum angegeben. Dabei wird die Farbe in ihre Komponenten rot, grün und blau zerlegt. Auch DirectX verwendet dieses Modell. Dabei sind die einzelnen Komponenten mit 8 Bit gespeichert, eine Farbe besteht damit aus 24 Bit. Dadurch bleiben noch weitere 8 Bit 54 KAPITEL 8. ZEICHNEN EINFACHER FORMEN übrig, die für den Alpha-Wert, also die Transparenz, genutzt werden. Zusammen ergibt dies 32 Bit und wird in einem DWORD gespeichert, welches auch mit dem Typ D3DCOLOR bezeichnet wird. Da das Schreiben von Farben als DWORD etwas umständlich ist, gibt es diverse Makros, die sich leichter bedienen lassen. Die am meisten verwendeten sind D3DCOLOR_ARGB und D3DCOLOR_XRGB. Ersteres nimmt vier Parameter im Bereich zwischen 0 und 255 für die Komponenten Alpha, Rot, Grün und Blau entgegen, das zweite nur drei - der Alpha-Wert fehlt. Es gibt auch ein Makro, das den Alpha-Wert als letzten Parameter erwartet und ein Makro D3DCOLOR_COLORVALUE, das die Werte im Bereich von 0.0 bis 1.0 erwartet. Weitere Makros sind für das YUV-Modell vorhanden. Beispiele für verschiedene Farben: R 255 0 0 255 255 0 255 8.2.4 G 0 255 0 255 0 255 255 B 0 0 255 0 255 255 255 Anlegen der Geometrie Nachdem nun alle Vorbereitungen abgeschlossen sind, kann die Geometrie, also die Punkte des Dreiecks, erzeugt werden. Dafür werden drei Punkte benötigt, die man in dem eben angelegten Format erstellen muss. Die Estellung eines Arrays mit drei Vertices ist in Listing 8.3 dargestellt. 1 2 3 4 5 6 7 8 9 D3DVERTEX_2D ax2DVertices[] = { //links unten { 0.0, 600.0, 0.0, 1.0, D3DCOLOR_XRGB(255,0,0)}, //Mitte oben {400.0, 0.0, 0.0, 1.0, D3DCOLOR_XRGB(0,255,0)}, //rechts unten {800.0, 600.0, 0.0, 1.0, D3DCOLOR_XRGB(0,0,255)} }; Listing 8.3: Rendern des zweidimensionlen Dreiecks Allen Punkten ist der Z-Wert von 0.0 und der rhw-Wert von 1.0 gemeinsam. Des weiteren bekommt jeder Punkt eine andere Koordinate und Farbe zugewiesen. Damit sind alle Informationen zu einem Dreieck vorhanden. 8.2. RENDERN IN 2D 8.2.5 55 Abändern der Hauptschleife Das Dreieck liegt nun fertig vorbereitet vor, jetzt fehlt nur noch das eigentliche Zeichnen in der Hauptschleife. Diese soll dazu noch ein wenig angepasst werden, um auch weiterhin effektiv genutzt werden zu können. Die erste Änderung betrifft den GetMessage-Aufruf. GetMessage ist eine blockierende Funktion, das heißt, sie kehrt erst zurück wenn eine Nachricht vorhanden ist. Ist das nicht der Fall, so wartet die Funktion bis eine neue Nachricht eintrifft. Das ist hier etwas hinderlich, da gewünscht wird, dass immer zu gezeichnet werden soll, also das Programm immer mit rendern beschäftigt ist. Andernfalls könnte nur gezeichnet werden, wenn gerade eine Nachricht eingetroffen ist. Um diese Änderung zu erreichen, wird GetMessage durch PeekMessage ersetzt, wobei ein Parameter PM_REMOVE hinzugefügt werden muss. Dieser Parameter besagt, dass die Nachricht aus der Nachrichten-Warteschlange entfernt werden soll. Die Funktion PeekMesage wartet nicht mehr auf das Eintreffen einer neuen Nachricht, wenn keine vorhanden ist, sondern kehrt sofort zurück. Da jetzt aber die Hauptschleife verlassen wird, sobald keine Nachricht mehr vorliegt (denn dann liefert PeekMessage false) muss die alte Schleife von einer neuen umgeben werden, die so lange läuft, bis das Programm beendet wird. Das war die zweite Änderung. 8.2.6 Rendern Das eigentliche Rendern geschieht in überschaubaren sechs Schritten. Als erstes muss Direct3D mitgeteilt werden, dass jetzt ein neues Bild gerendert werden soll. Dies geschieht mit der Methode BeginScene. Als nächstes bietet es sich an, den gesamten Viewport (besser: den Backbuffer) zu löschen, das heißt mit einer bestimmten Farbe zu überschreiben. Bei dieser Gelegenheit kann man auch den Tiefen- und Stencilpuffer löschen. Das geschieht durch die Methode Clear. Der nächste Schritt ist die Angabe des Vertex-Formates, das oben definiert wurde. Hier reicht ein Aufruf der Methode SetFVF mit der oben angegebenen Beschreibung D3DFVF_2D. Jetzt kommt das eigentliche Zeichnen. Dabei hat man diverse Möglichkeiten. Zunächst kann man die (Vertex-)Daten im normalen Systemspeicher, aber auch in einem Strom (der sich seinerseits auf der Grafikkarte befindet) angeben. Hier soll der Systemspeicher verwendet werden. Des weiteren ist es möglich, die Vertices indiziert oder einfach der Reihe nach zu zeichnen. Indiziert bedeutet, dass neben der Liste von Vertices auch noch eine Index-Liste mit übergeben wird, in der steht, welche Vertices in welcher Reihenfolge zu zeichnen sind. Dadurch ist es möglich Vertices nur einmal zu speichern (und damit zu übertragen), sie aber mehrfach zu verwenden. Hier soll wiederum die einfachere Möglichkeit (also die nicht indizierte) genutzt werden, zumal 56 KAPITEL 8. ZEICHNEN EINFACHER FORMEN P1 P3 P2 P5 P1 P6 P4 Triangle List P3 P2 P5 P2 P3 P6 P4 Triangle Strip P1 Triangle Fan P4 P5 P6 Abbildung 8.2: Die verschiedenen Möglichkeiten Dreiecke anzugeben die indizierte Variante bei einem Dreieck keinen Vorteil bringt. Die entsprechende Methode ist dann DrawPrimitiveUP. Ihr wird neben der Anzahl der zu rendernden Dreiecke, einem Zeiger auf die Vertices und der Größe (in Byte) eines Vertices auch noch ein Typ mit übergeben. Dieser Typ gibt an, wie die Vertices interpretiert werden sollen. Möglich sind neben Punktelisten und verschiedenen Arten von Linien die für Dreiecke interessanten Modi Triangle List, Strip und Fan. Bei Triangle List werden immer drei aufeinander folgende Punkte als ein Dreieck angesehen. Beim Triangle Strip bilden die beiden vorhergegangenen Punkte mit dem nächsten ein neues Dreieck. Der letzte schließlich interpretiert den ersten Punkt als Punkt von allen Dreiecken, die dann jeweils nur noch durch einen weiteren Punkt angegeben werden (siehe Abbildung 8.2). Da hier nur ein einziges Dreieck gezeichnet werden soll, ist es egal, welcher dieser Modi verwendet wird. Der fünfte Schritt besteht darin, Direct3D mit der Methode EndScene mitzuteilen, dass das Bild nun fertig ist. Jetzt befindet sich das Bild bereits fertig im Backbuffer, es muss nun noch in den Frontbuffer gebracht werden. Dies erledigt die Methode Present. Das Ergebnis könnte so aussehen, wie in Abbildung 8.3 dargestellt ist. 8.3 Die dritte Dimension Mit dem grundsätzlichen Verständnis über das Rendern in zwei Dimensionen soll nun dasselbe in drei Dimensionen vorgenommen werden. Damit dies funktioniert, muss allerdings Direct3D initialisiert werden, damit es die Abbildung aus dem virtuellen 3D-Raum auf den Monitor richtig durchführen kann. 8.3.1 Das View Frustum Der Bereich, der die ,sichtbaren‘ 3D-Objekte enthält, wird als View Frustum bezeichnet. Dies ist im Allgemeinen ein Pyramidenstumpf. Die Kamera befindet sich dabei an der (nicht vorhandenen) Spitze der Pyramide. Seitlich 8.3. DIE DRITTE DIMENSION Abbildung 8.3: So könnte das Ergebnis in 2D aussehen. 57 58 KAPITEL 8. ZEICHNEN EINFACHER FORMEN Far Clipping Plane Near Clipping Plane View Frustum Abbildung 8.4: Das View Frustum wird der Bereich durch den Rand des Monitors begrenzt, vorne und hinten aus technischen Gründen. Dabei wird die vordere Ebene, an der das View Frustum endet, also die ,Oberseite‘ des Pyramidenstumpfes, als Near Clipping Plane und die gegenüberliegende Seite als Far Clipping Plane bezeichnet (Siehe Abbildung 8.4). Der Winkel der Pyramide wird als Sichtfeld oder Field of Vision bezeichnet (Siehe Abbildung 8.5). Es wird später nötig sein, die Ebenen des View Frustums zu kennen. Daher sollten diese in einer einfachen Struktur abgespeichert werden. Da sich die Kamera normalerweise bewegt, verändern sich die Ebenen des View Frustums, weswegen diese Ebenen bei jedem Frame neu berechnet werden müssen. Die Mathematik, die zur Berechnung der Ebenen verwendet wird, ist recht kompliziert und soll daher hier vorerst nicht besprochen werden. Die verwendete Berechnung stammt von Gil Gribb und Klaus Hartmann. Dabei werden die Ebenen des View Frustums aus der Projektions- und ViewMatrix berechnet. Der Code ist in Listing 8.4 dargestellt. Dabei wird die in Kapitel 6 verwendete Form der Ebenendarstellung verwendet. 8.3.2 Initialisierung der Szene Punkte werden, wenn sie in 3D vorliegen, durch einige Transformationen so geändert, dass sie in Viewport-Koordinaten vorliegen. Die erste Transformation wandelt die lokalen Koordinaten in absolute oder Weltkoordinaten um. Diese Transformation wird durch die so genannte Weltmatrix angegeben. 8.3. DIE DRITTE DIMENSION 1 2 3 D3DXMATRIX xViewMatrix; D3DXMATRIX xProjektionMatrix; D3DXMATRIX xCombiMatrix; 4 5 6 7 8 9 10 //Matrizen abfragen pxD3DDevice->GetTransform(D3DTS_PROJECTION, &xProjektionMatrix); pxD3DDevice->GetTransform(D3DTS_VIEW, &xViewMatrix); //Matrizen multiplizieren xCombiMatrix = xViewMatrix * xProjektionMatrix; 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 //Linke Clipping-Plane xLeftPlane = Plane(xCombiMatrix._14 + xCombiMatrix._11, xCombiMatrix._24 + xCombiMatrix._21, xCombiMatrix._34 + xCombiMatrix._31, xCombiMatrix._44 + xCombiMatrix._41); //Rechte Clipping-Plane xRightPlane = Plane(xCombiMatrix._14 - xCombiMatrix._11, xCombiMatrix._24 - xCombiMatrix._21, xCombiMatrix._34 - xCombiMatrix._31, xCombiMatrix._44 - xCombiMatrix._41); //Obere Clipping-Plane xTopPlane = Plane(xCombiMatrix._14 - xCombiMatrix._12, xCombiMatrix._24 - xCombiMatrix._22, xCombiMatrix._34 - xCombiMatrix._32, xCombiMatrix._44 - xCombiMatrix._42); //Untere Clipping-Plane xBottomPlane = Plane(xCombiMatrix._14 + xCombiMatrix._12, xCombiMatrix._24 + xCombiMatrix._22, xCombiMatrix._34 + xCombiMatrix._32, xCombiMatrix._44 + xCombiMatrix._42); //Nahe Clipping-Plane xNearPlane = Plane(xCombiMatrix._13, xCombiMatrix._23, xCombiMatrix._33, xCombiMatrix._43); //Entfernet Clipping-Plane xFarPlane = Plane(xCombiMatrix._14 - xCombiMatrix._13, xCombiMatrix._24 - xCombiMatrix._23, xCombiMatrix._34 - xCombiMatrix._33, xCombiMatrix._44 - xCombiMatrix._43); Listing 8.4: Berechnung der Ebenen des View Frustums 59 60 KAPITEL 8. ZEICHNEN EINFACHER FORMEN X od. Y Far CP Near CP Kamera FOV Z Abbildung 8.5: Das Sichtfeld Nach dieser Transformation handelt es sich um absolute Koordinaten. Sie wurden also verschoben, gedreht oder skaliert, damit sie an der gewünschten Position in der Szene liegen. Die zweite Transformation beschreibt nun die Kamera. Darunter fallen die Position und Richtung. Die letzte Transformation wird mit der Projektionsmatrix berechnet, sie wandelt die KameraKoordinaten in Viewport-Koordinaten um. Dies ist quasi die Einstellung der Linse der Kamera. Diese Matrix muss also erzeugt und gesetzt werden. Dazu wird vorher der aktuelle Viewport abgefragt, die Methode GetViewport erledigt das. Hier muss man allerdings aufpassen, denn mit dieser Funktion gab es bereits große Probleme dahingehend, dass sie den Viewport nicht richtig liefern konnte. Es ist also auf alle Fälle angebracht, hier einen alternativen Weg vorzusehen, also mit den Werten zu arbeiten, die auch beim Erzeugen des Fensters verwendet wurden. Um eine Projektionsmatrix zu erstellen, stehen mehrere Wege zur Verfügung. Zum einen kann man die Berechnung von Hand“ in einer eige” nen Funktion durchführen. Zum anderen aber eine mitgelieferte Funktion verwenden, die sich in der Hilfs-Bibliothek befindet. Hier bietet sich die Funktion D3DXMatrixPerspectiveFovLH an. Sie erstellt anhand des Sichtfeldes eine Projektionsmatrix für ein linkshändisches Koordinatensystem. Die Funktion nimmt als Parameter einen Zeiger auf eine Matrix entgegen, in die sie das Ergebnis schreibt. Der zweite Parameter bestimmt das Sichtfeld in Radians. Der nächste Parameter ist das Seitenverhältnis Breite / Höhe, diese Werte kommen aus dem Viewport. Die letzten beiden Parameter geben schließlich den Abstand der Kamera von der Near und Far Clipping Plane an. Wenn diese Funktion verwendet werden soll, muss zusätzlich noch die 8.3. DIE DRITTE DIMENSION 61 d3dx9.lib zu dem Projekt gebunden werden. Die soeben erstellte Projektionsmatrix wird jetzt mit der Methode SetTransform an Direct3D übermittelt. Würde man mit diesen Einstellungen anfangen zu rendern, so sähe man nur ein schwarzes Dreieck. Das kommt daher, dass Direct3D nun versucht die Farbe selbst zu berechnen, aber keine Angaben zur Beleuchtung gemacht wurden. Da aber pro Punkt eine absolute Farbe angegeben wurde, ist dieser Schritt sowieso überflüssig. Also muss diese Berechnung nun durch Setzen eines Renderstates wie in Listing 8.5 abgestellt werden: 1 m_pxD3DDevice->SetRenderState(D3DRS_LIGHTING, FALSE); Listing 8.5: Licht ausschalten 8.3.3 Geometrie erzeugen Als nächster Schritt muss nun wieder eine passende Geometrie erzeugt werden. Da diesmal dreidimensionale Koordinaten angegeben werden sollen, muss auch die FVF-Struktur und Beschreibung abgeändert werden. Dazu reicht es aber, das Attribut rhw zu entfernen und bei der Beschreibung D3DFVF_XYZRHW durch D3DFVF_XYZ zu ersetzen. Die Vertices können dann wie in Listing 8.6 gezeigt angelegt werden. 1 2 3 4 5 6 7 8 9 D3DVERTEX_3D ax3DVertices[] = { //links unten {-0.75, -1.0, 3.0, D3DCOLOR_XRGB(255,0,0)}, //Mitte oben { 0.0 , 1.0, 3.0, D3DCOLOR_XRGB(0,255,0)}, //rechts unten { 0.75, -1.0, 3.0, D3DCOLOR_XRGB(0,0,255)} }; Listing 8.6: Rendern eines dreidimensionalen Dreiecks Alle Punkten haben einen Z-Wert von 3,0. Das heißt, dass sich das Dreieck 3 Einheiten von der Kamera entfernt befindet (da sich die Kamera im Ursprung befindet) und damit gut im sichtbaren Bereich liegt. 8.3.4 Rendern Beim Rendern ergeben sich keine Unterschiede zur zweidimensionalen Version, wenn man davon absieht, dass natürlich die richtigen Vertices und das 62 KAPITEL 8. ZEICHNEN EINFACHER FORMEN Abbildung 8.6: So könnte das Ergebnis in 3D aussehen. richtige FVF angegeben werden müssen. Das Ergebnis sieht dann auch der ersten Version ziemlich ähnlich, wie Abbildung 8.6 auf Seite 62 zeigt. 8.4 Texturen Bis jetzt sind nur einfache Farben verwendet worden, um die Oberfläche des Dreiecks einzufärben. Dies reicht im Allgemeinen nicht aus, um einen realistischen Eindruck zu erzeugen. Um dem abzuhelfen, gibt es Texturen. Eine Textur wird wie eine Art ,Tapete‘ auf das Objekt ,geklebt‘. Damit ist es möglich auch sehr feine Strukturen darzustellen, oder zum Beispiel (Straßen-) Schilder zu beschriften. 8.4.1 Geometrie erzeugen Hier muss nun erneut das Vertex-Format geändert werden, damit zusätzlich die Textur-Koordinaten gespeichert werden können. Die einzelnen Pixel der Textur werden im Allgemeinen Texel genannt. Bei einer Textur ist in der linken oberen Ecke die Koordinate (0.0 | 0.0) und in der unteren rechten (1.0 | 1.0). Man stellt sich nun vor, dass das Objekt sich hinter der Textur 8.4. TEXTUREN 63 befindet und sucht für alle Vertices die entsprechende Textur-Koordinate heraus. Die erweiterte Geometrie sieht dann wie in Listing 8.7 dargestellt aus: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 /** * Ein einfacher Punkt in 3D-Koordinaten mit Textur. */ struct D3DVERTEX_3D_TEXTURED { /** * Koordinate des Punktes. */ float x, y, z; /** * Texturkoordinaten */ float u, v; }; Listing 8.7: Vertexstrukur von dreidimensionalen Punkten mit TexturKoordinaten Die Farbe ist durch zwei float-Werte ersetzt worden (Zeile 13), die die Textur-Koordinaten enthalten werden. Die Beschreibung muss wie in Listing 8.8 gezeigt angepasst werden. 1 #define D3DFVF_3D_TEXTURED (D3DFVF_XYZ | D3DFVF_TEX1) Listing 8.8: Beschreibung der Vertexstruktur mit Textur-Koordinaten Zu den normalen Koordinaten kommt also noch D3DFVF_TEX1 hinzu, welches besagt, dass eine Textur-Koordinate gespeichert ist. Mit D3DFVF_TEX1 bis D3DFVF_TEX8 sind bis zu acht verschiedene Koordinaten möglich. Dies kann man verwenden, um mehrere Texturen auf ein Objekt zu ,legen‘. Mit diesen Vorbereitungen kann nun die eigentliche Geometrie erzeugt werden (Listing 8.9). Dabei sind nun vier Vertices enthalten, die zusammen ein Rechteck bilden. Die Texturkoordinaten sind so gewählt, dass die ganze Textur einmal auf dem Rechteck zu sehen sein wird. 8.4.2 Rendern Diesmal sind beim Rendern ebenfalls ein paar Änderungen nötig. Zunächst einmal muss die Textur aus einer Datei geladen werden. Dies erledigt man am einfachsten mit der Funktion D3DXCreateTextureFromFileA aus der 64 1 2 3 4 5 6 7 KAPITEL 8. ZEICHNEN EINFACHER FORMEN D3DVERTEX_3D_TEXTURED ax3DTextureVertices[] = { {-1.5, 1.0, 3.0, 0.0, 0.0}, //links oben { 1.5, 1.0, 3.0, 1.0, 0.0}, //rechts oben {-1.5, -1.0, 3.0, 0.0, 1.0}, //links unten { 1.5, -1.0, 3.0, 1.0, 1.0} //rechts unten }; Listing 8.9: Anlegen der Vertices D3DX-Bibliothek. Die Funktion lädt unter Angabe des D3DDevices und des Dateinamens die Textur und liefert einen Zeiger auf die Schnittstelle IDirect3DTexture9 per Rückgabeparameter zurück. Die Textur ist nun zwar geladen, aber sie wird deshalb noch nicht verwendet. Um das zu erreichen, muss dies Direct3D noch mit der Methode SetTexture mitgeteilt werden. Der erste Parameter der Methode soll dabei hier nicht weiter betrachtet werden, und vorerst auf 0 gesetzt werden. Wenn dann auch noch das Vertex-Format eingestellt wird und die richtigen Vertices angegeben werden, könnte das Ergebnis wie in Abbildung 8.7 auf Seite 65 dargestellt aussehen. 8.4. TEXTUREN Abbildung 8.7: So könnte das Ergebnis in 3D mit Textur aussehen. 65 66 KAPITEL 8. ZEICHNEN EINFACHER FORMEN Kapitel 9 Animation Als Animation wird hier die Illusion von Bewegung verstanden. Darunter fallen naturgemäß sehr viele und sehr unterschiedliche Dinge. So könnte sich zum Beispiel eine Maschine oder Figur bewegen. Aber selbst bei diesem kleinen Beispiel gibt es schon mehrere Möglichkeiten. So kann sich die Figur in sich bewegen (sie könnte zum Beispiel winken) oder sie kann ihre Position ändern. Eine weitere Form der Bewegung ist die der Kamera. In diesem Kapitel soll es vorerst nicht um die Bewegung von Objekten in sich gehen, sondern nur um Kamera- und Objektbewegung. Dabei werden häufig Matrizenrechnungen benötigt, die in Kapitel 5 ab Seite 29 näher beschrieben wurden. 9.1 Animation durch Transformation Um die Illusion eines sich bewegenden Objektes zu erzeugen, wird in jedem Frame das Objekt ein wenig anders gezeichnet. Dazu kann es zum Beispiel gedreht oder verschoben werden. Hier soll als Beispiel eine Drehung um die Y-Achse simuliert werden. Dazu kann die bereits in Kapitel 5 angegebene Matrix verwendet werden: 9.1.1 cos(a) 0 sin(a) 0 0 − sin(a) 1 0 0 cos(a) 0 0 0 0 0 1 Erzeugen der Weltmatrix Es gibt, wie so oft, mehrere Möglichkeiten das Ziel zu erreichen. Im Folgenden sollen drei verschiedene Varianten etwas genauer beschrieben werden. Variante 1 Bei dieser Variante wird die benötigte Matrix von Hand erstellt. 67 68 1 2 3 4 KAPITEL 9. ANIMATION D3DXMATRIX xWorldMatrix(cos(fAngle), 0 , sin(fAngle), 0 , 0, 1, 0, 0, -sin(fAngle), 0 , cos(fAngle) , 3.0f , 0, 0, 0, 1); Listing 9.1: Das Anlegen einer Matrix, die zum Drehen um die Y-Achse verwendet werden kann In Listing 9.1 wird ab Zeile 1 eine Matrix angelegt, die zum Drehen um die Y-Achse verwendet werden kann. Hierbei wird der Typ D3DXMATRIX verwendet, welcher eine erweiterte Version der Standard-Matrix D3DMATRIX ist. D3DXMATRIX ist eine um Konstruktoren, Methoden und Operatoren erweiterte Version. Dieser Matrix werden mit dem Konstruktor bereits alle nötigen Werte angegeben. Zusätzlich wird auch bei Zeile 4, Spalte 3 eine 3.0 angegeben. Hiermit wird das Objekt um drei Einheiten in Richtung Z verschoben. Es sind also bereits zwei Transformationen in einer Matrix enthalten. Variante 2 Hierbei wird die Matrix mit Hilfe der Funktionen aus der D3DX-Bibliothek erstellt. 1 D3DXMATRIX xWorldMatrix; 2 3 4 D3DXMatrixRotationY(&xWorldMatrix, fAngle); xWorldMatrix._43 = 3.0f; Listing 9.2: Erstellen einer Matrix mit Hilfe der D3DX-Bibliothek In Listing 9.2 wird in Zeile 1 wie auch bei Variante 1 eine Matrix vom Typ D3DXMATRIX erstellt. Diesmal wird diese aber nicht mit dem Konstruktor gefüllt. Um die Matrix mit den richtigen Werten zu füllen wird hier die Funktion D3DXMatrixRotationY verwendet. Sie enthält alle Berechnungen, die in Variante 1 von Hand durchgeführt wurden. In Zeile 3 wird schließlich noch die Verschiebung in Z-Richtung hinzugefügt. Variante 3 Auch hier werden Funktionen aus der D3DX-Bibliothek genutzt. Die endgültige Transformations-Matrix wird jedoch aus mehreren Matrizen erstellt. Diese Variante muss immer bei komplizierteren Transformationen verwendet werden. 9.1. ANIMATION DURCH TRANSFORMATION 1 2 3 69 D3DXMATRIX xMatrixRotY; D3DXMATRIX xMatrixTranslate; D3DXMATRIX xWorldMatrix; 4 5 6 D3DXMatrixRotationY(&xMatrixRotY, fAngle); D3DXMatrixTranslation(&xMatrixTranslate, 0.0f, 0.0f, 3.0f); 7 8 9 10 D3DXMatrixMultiply(&xWorldMatrix, &xMatrixRotY, &xMatrixTranslate); Listing 9.3: Erstellen einer Transformations-Matrix aus mehreren Matrizen Bei dieser in Listing 9.3 dargestellten Variante werden in den ersten drei Zeilen drei Matrizen erstellt. Die erste wird in Zeile 5 mit der bereits bekannten Funktion D3DXMatrixRotationY mit Werten gefüllt, die zum Drehen um die Y-Achse benötigt werden. In Zeile 6 wird die zweite Matrix mit Hilfe der Funktion D3DXMatrixTranslation mit den Werten gefüllt, die zum Verschieben in Z-Richtung benötigt werden. Ab Zeile 8 werden die beiden gefüllten Matrizen mit der Funktion D3DXMatrixMultiply miteinander multipliziert und das Ergebnis in der Matrix xWorldMatrix abgelegt. Hierbei muss auf die Reihenfolge der Matrizen geachtet werden. In Listing 9.3 wird das Objekt zuerst gedreht und dann verschoben. 9.1.2 Setzen der Weltmatrix Alle drei Varianten ergeben eine Weltmatrix, die nun an Direct3D übermittelt werden muss. 1 2 glb_xD3DDevice.m_pxD3DDevice->SetTransform(D3DTS_WORLD, &xWorldMatrix); Listing 9.4: Setzen der Weltmatrix In Listing 9.4 wird die erstellte Matrix an Direct3D übermittelt. Dazu wird die Methode SetTransform des Direct3D-Device aufgerufen. Mit dieser Methode können verschiedene Matrizen gesetzt werden. Im ersten Parameter wird angegeben, welche Matrix gesetzt werden soll. Hier wird D3DTS_WORLD verwendet, das heißt es wird die Weltmatrix angegeben. Sie gilt, solange keine neue Weltmatrix gesetzt wird. Soll eine kontinuierliche Bewegung dargestellt werden, so muss der Wert von fAngle bei jedem Frame leicht geändert werden. Die Weltmatrix muss natürlich auch jedesmal neu erstellt werden. 70 KAPITEL 9. ANIMATION P2 P3 P2 P1 vorne P1 P3 hinten Abbildung 9.1: Punkte im und gegen den Uhrzeigersinn Wird hiermit zum Beispiel ein Dreieck gedreht, so verschwindet es plötzlich, sobald die ,Rückseite‘ zu sehen sein sollte. Dieser Effekt ist beabsichtigt und wird als Backface Culling bezeichnet. 9.1.3 Backface Culling Unter Culling versteht man das Entfernen von unsichtbaren Dreiecken. Ein Grund für das Entfernen von unsichtbaren Dreiecken ist zum Beispiel, dass die Dreiecke außerhalb des Sichtfeldes liegen. Eine sehr wichtige Art des Cullings ist jedoch das Backface Culling, also das Enfernen von Rückseiten. Dabei werden Dreiecke, die von hinten gesehen werden, nicht gezeichnet. Um festzustellen, welches die Vorder- und welches die Rückseite eines Dreiecks ist, werden die Punkte des Dreiecks betrachtet. Sind sie gegen den Uhrzeigersinn zu sehen, so handelt es sich um die Vorderseite, andernfalls um die Rückseite. Dies ist die Voreinstellung, die aber geändert werden kann. Dazu muss der Renderstate D3DRS_CULLMODE angepasst werden. Standardmäßig ist hier D3DCULL_CCW (,CCW‘ steht für Counter Clock Wise“, ” was auf deutsch gegen den Uhrzeigersinn“ bedeutet) eingestellt, aber es ” steht natürlich das Gegenteil mit D3DCULL_CW ( Clock Wise“, im Uhrzei” ” gersinn“) zu Verfügung. Ebenso ist es möglich mit D3DCULL_NONE das Culling ganz abzuschalten. 9.2 Animation durch Bewegung der Kamera Prinzipiell handelt es sich hierbei um die selbe Aktion wie bei der Transformation mit der Weltmatrix. Der einzige Unterschied ist, dass hier die View-Matrix verwendet wird. Trotzdem lohnt es sich, einen genaueren Blick auf die Vorgehensweise zu werfen, da hierbei auch andere Aspekte ein Rolle spielen. Bei der Kamera spricht man normalerweise nicht von der Drehung um ” die XYZ-Achse“, sondern von Pitch, Yaw und Roll. Tabelle 9.2 zeigt die Zuordnung dieser Namen zu den Achsen um die gedreht werden kann. 9.2. ANIMATION DURCH BEWEGUNG DER KAMERA Name Pitch Yaw Roll 71 Bedeutung Drehung um die X-Achse. Drehung um die Y-Achse. Drehung um die Z-Achse. Tabelle 9.2: Zuordnung der Namen zu den Achsen Soll die Kamera rotieren, so müssen lediglich die Werte für Yaw, Pitch und Roll angepasst werden. Etwas komplizierter sieht es mit der Verschiebung der Kamera aus. Sie muss in Abhängigkeit ihrer Drehung verschoben werden. Das heißt es reicht nicht, die Kamera nur in Richtung der Achsen zu verschieben. Um das Verschieben in Abhängigkeit der Drehung zu erreichen, speichert man sich zusätzlich noch drei Vektoren, die die Richtung der Kamera nach vorne, oben und rechts angeben. Diese werden im Allgemeinen als Right, Up und Direction bezeichnet. Das bedingt natürlich auch, dass diese Vektoren mit jeder Änderung der Ausrichtung erneuert werden müssen. Die letzte zu speichernde Eigenschaft ist schließlich die Position der Kamera. Es müssen also einige Werte gespeichert werden. Hierzu bietet sich eine Klasse an, in der auch alle Funktionen implementiert werden. Eine mögliche Klasse ist in Listing 9.5 zu sehen. 1 2 3 4 5 6 class Camera { public: void UpdateMatrix(); void Rotate(float p_fRotX, float p_fRotY, float p_fRotZ); void Move(float p_fX, float p_fY, float p_fZ); 7 Camera(); ~Camera(); 8 9 10 private: float fYaw; //Drehung um Y float fPitch; //Drehung um X float fRoll; //Drehung um Z D3DXVECTOR3 vUp; D3DXVECTOR3 vDirection; D3DXVECTOR3 vRight; D3DXVECTOR3 vPosition; 11 12 13 14 15 16 17 18 19 } Listing 9.5: Deklaration einer Klasse für eine Kamera. Die Datenelemente sind bereits beschrieben worden. Was fehlt ist die 72 KAPITEL 9. ANIMATION Beschreibung der Methoden. Am einfachsten ist sicherlich die Drehung der Kamera. Hierbei werden lediglich die Wert Yaw, Pitch und Roll verändert, sonst passiert nichts. Ebenfalls sehr einfach ist das Verschieben der Kamera, da nur die Position verändert werden muss. Dabei dürfen aber nicht die übergebenen Werte einfach auf die Position addiert werden, sondern es muss die Ausrichtung der Kamera beachtet werden. Dies geschieht durch das Verrechnen mit den Vektoren Right, Up und Direction. Soll die Kamera also um eine Einheit nach oben bewegt werden, so muss einmal Up zu der Position hinzuaddiert werden. Das selbe gilt auch für die anderen Achsen. Jetzt bleibt nur noch die Funktion übrig, die die View-Matrix und Right, Up und Direction auf den neusten Stand bringt. Die endgültige ViewMatrix muss im folgendem Format vorliegen: Right.x U p.x Direction.x Right.y U p.y Direction.y Right.z U p.z Direction.z −P osition · Right −P osition · U p −P osition · Direction 0 0 0 1 Bevor die Matrix erstellt wird, müssen also Right, Up und Direction aktualisiert werden. Um das zu erreichen, muss der ursprüngliche Richtungsvektor gedreht werden und zwar um alle drei Achsen. Dies könnte man nun durch drei Matrixoperationen erreichen. Diese sind aber im Allgemeinen recht langsam, daher bietet es sich an, zunächst eine allgemeine Formel für jede Richtung auf dem Papier zu entwickeln, und diese dann auszuprogrammieren. Bei der allgemeinen Formel handelt es sich um eine Kombination der drei Matrixoperationen. Dies soll hier an dem Beispiel für Up dargestellt werden. Dabei werden die Winkel nur mit ihren Anfangsbuchstaben angegeben. Gestartet wird mit einem Vektor U~p, der nach oben zeigt: U~p = (0 | 1 | 0). Nun soll die erste Rotation um die X-Achse durchgeführt werden. Dazu wird die aus Kapitel 5 bekannte Rotationsmatrix Rotx verwendet, welche aber auf drei Zeilen und drei Spalten verkleinert wurde: z~1 = U~p · Rotx 0·1 + 1·0 + 0·0 = 0 · 0 + 1 · cos(P ) + 0 · − sin(P ) 0 · 0 + 1 · sin(P ) + 0 · cos(P ) 0 = cos(P ) sin(P ) In z~1 befindet sich jetzt ein Vektor, der um Pitch um die X-Achse geneigt ist. Nun wiederholt man den Schritt mir der Y-Achse: 9.3. INTERAKTION 73 z~2 = z~1 · Roty 0 · cos(Y ) + cos(P ) · 0 + sin(P ) · sin(Y ) + cos(P ) · 1 + sin(P ) · 0 = 0·0 0 · − sin(Y ) + cos(P ) · 0 + sin(P ) · cos(Y ) sin(P ) · sin(Y ) = cos(P ) sin(P ) · cos(Y ) z~2 enthält nun die Rotation um die X- und Y-Achse. Es fehlt noch die Rotation um die Z-Achse, die analog berechnet wird: U~p = z~2 · Rotz sin(P )·sin(Y )·cos(R) + cos(P )·− sin(R) + sin(P )·cos(Y )·0 = sin(P )·sin(Y )·sin(R) + cos(P )·cos(R) + sin(P )·cos(Y )·0 sin(P )·sin(Y )·0 + cos(P )·0 + sin(P )·cos(Y )·1 sin(P ) · sin(Y ) · cos(R) + cos(P ) · − sin(R) = sin(P ) · sin(Y ) · sin(R) + cos(P ) · cos(R) sin(P ) · cos(Y ) Nun enthält U~p die richtigen Formeln, um den Vektor neu zu berechnen. Dasselbe ist analog für die beiden anderen Vektoren Right und Direction durchzuführen. Die erhaltenen Formeln können nun ausprogrammiert werden, wobei man darauf achten sollte, dass die Funktionen Sinus und Cosinus nicht mehrfach pro Wert (Pitch, Yaw und Roll) ausgeführt werden. Man berechnet also zu Beginn der Methode die benötigten Sinus- und Cosinus-Werte und nutzt diese anschließend. Dadurch wird das Programm ein bißchen schneller. Nachdem die Matrix fertig zusammengestellt wurde, wird sie mit der Methode SetTransform an Direct3D als View-Matrix übergeben. Die hier vorgestellte Berechnung gilt übrigens nicht nur für die Kamera, sie kann auch zum Verschieben von Objekten verwendet werden. 9.3 Interaktion Soll die Kamera oder ein Objekt interaktiv bewegt werden, so müssen Benutzereingaben entgegengenommen werden. Dies kann über die normalen Windows-Nachrichten erfolgen oder aber über DirectInput. DirectInput wird in Kapitel 13 genauer behandelt. Hier wird die Variante mit WindowsNachrichten erklärt. Windows sendet für jeden Tastendruck die Nachricht WM_KEYDOWN. Im Parameter wParam der Callback-Funktion steht der Keycode der jeweiligen 74 KAPITEL 9. ANIMATION Taste. Bei jedem Eintreffen einer solchen Nachricht überprüft man, welche Taste gedrückt wurde und ändert gegebenenfalls zum Beispiel die Position der Kamera. Alternativ kann auch auf die Nachricht WM_CHAR reagiert werden. Bei dieser Nachricht werden im Gegensatz zu WM_KEYDOWN nicht virtuelle Keycodes, sondern echte Buchstaben“ in wParam übermittelt. Allerdings ist es ” mit WM_CHAR nicht möglich, die Cursortasten abzufragen. Kapitel 10 Modelle Bis jetzt wurden alle Vertices direkt im Programmcode angegeben. Dies ist bei kleinen Objekten wie Dreiecken oder Vierecken, aber auch bei regelmäßigen Objekten, deren Form berechnet werden kann, gut machbar. Bei größeren und vor allem komplexeren Objekten, wie zum Beispiel einem Tisch oder einer Person, geht das sehr schlecht. Daher verwendet man einen 3D-Editor wie zum Beispiel Autodesks 3ds Max, um ein Modell zu erzeugen und dieses dann als Datei auf der Festplatte abzuspeichern. Dieses Modell kann dann geladen und angezeigt werden. Dabei entwickelt man am Besten ein eigenes Format für die Daten, welches genau auf die Bedürfnisse der Engine abgestimmt ist und weitere Informationen wie zum Beispiel Kollisionsinformationen oder den Schwerpunkt zur physikalischen Simulation enthält. Hier soll vorerst nur auf das Standard.x-Format (siehe Kapitel 10.2) eingegangen werden. 10.1 Pools Bevor das Laden und Nutzen von Modellen beschrieben werden soll, muss noch ein Blick auf das sogenannte Pooling geworfen werden. 10.1.1 Speicherbereiche Bei der ,normalen‘ Programmierung arbeitet man schlimmstenfalls mit zwei verschiedenen Speicherbereichen: dem Hauptspeicher und Plattenspeicher. Bei der Grafikprogrammierung kommen aber noch zwei weitere Bereiche hinzu: Zum einen der Speicher auf der Grafikkarte (oft als Videospeicher bezeichnet) und zum anderen ein Speicherbereich im Hauptspeicher, auf den die Grafikkarte Zugriff hat. Diesen Speicher nennt man AGP-Speicher. Die Speicherverwaltung auf der Grafikkarte funktioniert aber ein wenig anders als die des Hauptspeichers. Wenn eine Applikation die Grafikkarte nutzt, hat sie Zugriff auf deren Speicher. Wird das Programm verdrängt, 75 76 KAPITEL 10. MODELLE das heißt hat es keinen Zugriff mehr auf die Grafikkarte, so spricht man davon, dass das Programm das (D3D-)Device ,verloren‘ hat. Dabei gehen alle Speicherbereiche auf der Karte verloren. Sie müssen, sobald das Device wieder zur Verfügung steht, wiederhergestellt werden. 10.1.2 Pools Ein Pool ist ein Bereich, in den Daten gespeichert werden können. Unter Direct3D gibt es vier verschiedene Pool s. Jeder Pool wird durch eine Konstante aus D3DPOOL angegeben. Die vier Pools sind im Folgenden zusammen mit einer Beschreibung aufgelistet: D3DPOOL SYSTEMMEM Die Daten liegen im Hauptspeicher und können von der Grafikkarte nicht erreicht werden. Dafür gehen sie aber nicht verloren, wenn das Device verloren geht. Auf diese Bereiche kann immer vom Programm aus zugegriffen werden. Diese Daten können als Quelle für eine Kopie im Grafikspeicher dienen. D3DPOOL DEFAULT Hier liegen die Daten im Video- oder AGP-Speicher. Das heißt aber auch, dass sie im Falle eines verlorenen Devices wiederhergestellt werden müssen. Außerdem kann je nach Art der Daten nicht auf sie zugegriffen werden. D3DPOOL MANAGED DirectX verwaltet hier die Daten und kopiert sie in den Speicher, den die Grafikkarte erreichen kann, sobald das nötig ist. Es existieren normalerweise zwei Kopien der Daten: eine im Grafikspeicher und eine im Hauptspeicher. Die Kopie im Systemspeicher kann jederzeit verändert werden und wird dann automatisch wieder in den Videospeicher kopiert. Geht das Device verloren, so werden die Daten im Videospeicher automatisch wiederhergestellt. Für die meisten Fälle ist dieser Pool am besten geeignet. D3DPOOL SCRATCH Hierbei handelt es sich um reinen Systemspeicher, der gar nicht mit einem Direct3D-Device bearbeitet werden kann. Er kann aber als Quelle für Kopien und andere Arbeiten dienen. 10.2 Das .x-Format Microsoft hat mit DirectX 2.0 das .x-Format für 3D-Modelle eingeführt. Ab DirectX 3.0 existiert es in einer stark erweiterten Version. Ab Version 6.0 bietet dieses Schnittstellen und Funktionen, um mit diesem Format arbeiten zu können. Das .x-Format ist ein template-basiertes Format zum Speichern von 3DDaten. Das bedeutet, dass die verschiedensten Daten gespeichert werden 10.2. DAS .X-FORMAT 77 können, auch vom Benutzer definierte Strukturen sind möglich. Typischerweise werden neben dem Mesh die Texturen beziehungsweise Materialien in einer einfachen Datei abgespeichert. Ein Mesh (zu deutsch Netz) beinhaltet die eigentlichen 3D-Daten, also die Positionen der Punkte, und zusätzlich die Information, aus welchen Punkten ein Dreieck zusammengesetzt werden soll. Es können aber auch Animationen in einer solchen Datei abgelegt werden. 10.2.1 Laden von .x-Dateien Um ein Modell aus einer Datei zu laden, gibt es auch hier eine Funktion in der D3DX-Bibliothek. Die Funktion D3DXLoadMeshFromX lädt das Mesh, alle Materialien und alle Effekte. Texturen werden nicht geladen, sehr wohl aber der Name einer Textur. Alle Teile, die nicht benötigt werden, können mit NULL angegeben werden, wie es hier vorerst bei den Effekten der Fall sein soll. Beim Laden rechnet die Funktion auch noch aus, welche Dreiecke die Nachbarn eines anderen Dreiecks sind. Auch das soll hier nicht weiter betrachtet werden und mit NULL angegeben werden. Die übrigbleibenden Parameter sind der Dateiname und das Direct3D-Device. Des Weiteren muss angegeben werden, in welchen Pool das Modell geladen werden soll, dies kann mit dem zweiten Parameter eingestellt werden. Für den Anfang soll hier die Angabe von D3DXMESH_MANAGED genügen. Die Materialien werden nicht direkt geladen, sondern landen in einem D3DXBUFFER, wobei es sich um einen generischen Puffer handelt. Die letzten beiden Parameter geben schließlich an, wie viel Materialien geladen wurden und das Mesh selbst. Das Mesh ist dabei eine Schnittstelle vom Typ D3DXMESH. Hat das Laden funktioniert, kümmert man sich als nächstes um die Materialien. Dazu müssen diese erst aus dem Puffer mit der Methode GetBufferPointer geholt werden. Die Methode gibt dann einen Zeiger auf eine D3DXMATERIAL-Struktur zurück. Diese entspricht der D3DMATERIAL9Struktur mit der Ausnahme, dass sie zusätzlich noch einen String für einen Dateinamen enthält. Anschließend können diese Texturen in ein zuvor erstelltes Textur-Array geladen werden. Damit ist das Laden des Modells abgeschlossen. 10.2.2 Rendern des geladenen Modells Man könnte auf den ersten Blick denken, dass nun das Modell Dreieck für Dreieck gerendert wird. Dies ist aber nicht besonders schnell, da schlimmstenfalls für jedes Dreieck eine andere Textur eingestellt werden muss. Solche Einstellungen benötigen immer sehr viel Zeit, daher sollte man immer versuchen, mit möglichst wenig Einstellungen auszukommen. Dabei ist nicht nur die Textur relevant, auch Lichtquellen, Renderstates oder Materialien zählen hierzu. Es bietet sich also an, zuerst alle Dreiecke mit einer Textur zu 78 KAPITEL 10. MODELLE Abbildung 10.1: Eine Truhe gerendert mit DirectX zeichnen, dann die Dreiecke mit der zweiten Textur und so weiter. Dies hört sich schlimmer an, als es ist: Man benötigt lediglich eine Schleife über alle Materialien und darin zwei Methodenaufrufe. Einen zum Setzen der Textur (SetTexture) und einen zum Zeichnen (DrawSubset). Wird als Modell eine Truhe verwendet, so kann das Ergebnis wie in Abbildung 10.1 aussehen. Teil IV Techniken 79 81 Bei den hier vorgestellten Techniken steht nicht mehr so sehr DirectX im Vordergrund, es geht vielmehr um generelle Techniken, die in der Computergrafik verwendet werden. Die meisten, wenn nicht sogar alle, zielen darauf ab, die Menge der zu zeichnenden Dreiecke zu reduzieren. Es ist durchaus sinnvoller, etwas (Lauf-)Zeit dazu zu verwenden, festzustellen, welche Dreiecke überhaupt sichtbar sind und dann nur diese zu berechnen, anstatt alle Dreiecke blind“ zu zeichnen. Am meisten Zeit verbraucht das Zeichnen ” selbst, auch wenn eine Hardwarebeschleunigung vorhanden sein sollte. 82 Kapitel 11 Bounding Boxen & Culling Bounding-Boxen sind ein wichtiger Bestandteil der Computergrafik. Es gibt sie in den verschiedensten Ausprägungen, von denen hier vorerst nur die einfachste Art, nämlich die Achsen-Ausgerichtete-Bounding-Box (kurz: AABB) besprochen werden sollen. Zusätzlich wird die Methode vorgestellt, die benötigt wird, um zu prüfen, ob eine Box innerhalb des sichtbaren Bereichs liegt. 11.1 Allgemeines Eine Bounding-Box ist eine Kiste, in die ein Objekt genau hineinpasst. Dabei wird die Größe der Kiste so gewählt, dass sie so klein wie möglich, aber so groß wie nötig ist. Natürlich sind auch andere Formen als Boxen denkbar, so ist zum Beispiel für ein Fass ein Zylinder als Bounding-Box besser. Auch eine Kugel kann verwendet werden. Prizipiell sind auch beliebig komplexe Formen denkbar, allerdings geht dann der Geschwindigkeitsvorteil, der durch eine einfach Form entsteht, verloren. 11.1.1 Warum macht man das? Angenommen, man hätte in der Szene diverse Objekt mit zum Beispiel je 10000 Dreiecken. Nun soll geprüft werden, welche Objekte beziehungsweise Dreiecke sichtbar sind, um festzustellen, welche an die Grafikkarte gesendet werden müssen. Es muss pro Objekt mindestens 10000 mal überprüft werden, ob das Dreieck im sichtbaren Bereich liegt oder nicht. Das dauert mit Sicherheit länger als das blinde Rendern der Dreiecke, ob sichtbar oder nicht. Hierbei helfen Bounding-Boxen ungemein. Packt man jedes Obejekt in eine Bounding-Box, so muss nur noch geprüft werden, ob die Box im sichtbaren Bereich liegt. Und das geht wirklich bedeutend schneller als die 10000 Überprüfungen. 83 84 KAPITEL 11. BOUNDING BOXEN & CULLING Pmax Y Z Pmin X Abbildung 11.1: Eine an den Koordinatenachsen ausgerichtete Box 11.2 Achsen-Ausgerichtete-Bounding-Boxen Achsen-Ausgerichtete-Bounding-Boxen sind, wie der Name bereits sagt, an den Achsen des Koordinatensystems ausgerichtet. Das heißt, sie können sehr einfach dargestellt und berechnet werden. Zur Darstellung benötigt man lediglich zwei Punkte: einen, der vorne links unten liegt, und einen der hinten rechts oben liegt. Das reicht, da alle Kanten der Box parallel zu den Koordinatenachsen sind (siehe Abbildung 11.1). 11.2.1 Berechnung von AABBs Die Berechnung ist denkbar einfach. Hat man eine Menge an Punkten gegeben, so sucht man nach dem kleinsten X-, Y- und Z-Wert. Damit hat man den kleinsten“ Punkt gefunden. Dasselbe macht man nun für den größten ” X-, Y- und Z-Wert, und erhält somit den größten“ Punkt (siehe Abbildung ” 11.2). Man beachte hierbei, dass der gefundene Punkt nicht zwangsläufig einer der vorhanden Punkte sein muss, ja im Allgemeinen sogar nicht sein wird. 11.3 Culling 11.3.1 Allgemeines Unter culling (zu deutsch sammeln / wählen) versteht man das Entfernen nicht sichtbarer Dreiecke. Eine einfache Form, nämlich das Backface-Culling, wurde schon in Kapitel 9.1.3 besprochen. Hier soll aber geprüft werden, ob die Bounding-Box zumindest teilweise innerhalb des View-Frustums liegt. 11.3. CULLING Abbildung 11.2: Eine Bounding Box um ein Objekt 85 86 KAPITEL 11. BOUNDING BOXEN & CULLING Y View Frustum 1 3 2 X Abbildung 11.3: Ergebnisse des Culling-Tests Dazu werden die Ebenen des View Frustums wie in Kapitel 8.3.1 beschrieben benötigt. Beim Test, ob eine Bounding Box sichtbar ist, können drei verschiedene Ergebnisse eintreten: 1. Die Box liegt komplett innerhalb des View Frustums. 2. Die Box liegt komplett außerhalb des View Frustums. 3. Die Box liegt teilweise im View Frustum. Die verschiedenen Möglichkeiten zeigt Abbildung 11.3. Das View Frustum besteht aus insgesamt sechs Ebenen. Jede Ebene teilt den Raum in zwei Halbräume. Es ist einfach festzustellen, in welchem Halbraum ein Punkt liegt. Hier soll der Halbraum, in das der Normalenvektor der Ebene zeigt, als positiver und der andere als negativer Halbraum bezeichnet werden. 11.3.2 Punkte finden Um festzustellen, ob eine Box innerhalb des View Frustums liegt, muss sie mit jeder Ebene des View Frustums verglichen werden. Dazu soll die zu testende Ebene (gedanklich1 ) so parallel verschoben werden, dass sie durch den Mittelpunkt der Bounding Box geht. Jetzt wird die Ecke der Box gesucht, die am weitesten im positiven Halbraum liegt. Des weiteren wird die Ecke gesucht, die am weitesten im negativen Halbraum liegt. Diese sind in Abbildung 11.4 die linke untere (negativ) und die rechte obere (positiv) Ecke. 1 Das Verschieben dient hier nur zum Beschreiben der Punkte. In der Applikation wird die Ebene kein bißchen verschoben! 11.3. CULLING 87 Y E0 E X Abbildung 11.4: (Verschobene) Ebene mit Normalenvektor Um die Ecken zu finden, initialisiert man am besten zwei neue Punkte mit den beiden Punkten der Box und überprüft anschließend, ob diese Initialisierung richtig war. Dazu muss man sich lediglich die Richtung der Normalenvektoren komponentenweise ansehen. Das heißt, man prüft für X, Y und Z, ob dieser Anteil positiv ist und vertauscht dann die entsprechenden Komponenten der Punkte. Beispiel Ein Beispiel ist in Abbildung 11.5 dargestellt. Bei diesem Beispiel soll dieselbe Ebene E wie in Abbildung 11.4 verwendet werden. Der erste Schritt ist ganz links dargestellt und mit ,Start‘ bezeichnet. Hier wurden die beiden Punkte P os und N eg mit den angegebenen Werten belegt. Anschließend wird diese Annahme überprüft. Dazu wird geprüft, ob die X-Komponente des Normalenvektors der Ebene positiv (das heißt größer Null) ist. In diesem Beispiel ist er das (der Normalenvektor zeigt nach oben rechts), desshalb werden die beiden Punkte in X-Richtung vertauscht. Den selben Schritt führt man jetzt mit dem Y-Anteil durch. Auch dieser ist positiv, wesshalb die Punkte erneut vertauscht werden, diesmal aber in Y-Richtung. Jetzt liegt der Punkt P os im positiven Halbraum und der Punkt N eg entsprechend im negativen. 11.3.3 Klassifikation Sind die beiden Punkte gefunden, so muss man feststellen, wie diese zur Ebene liegen. Um das festzustellen, muss man als erstes wissen, in welche Richtung die Normalenvektoren der Ebenen zeigen, also ob sie nach innen 88 KAPITEL 11. BOUNDING BOXEN & CULLING Neg = Max Neg Pos y > 0? x > 0? Pos = Min Start Pos x>0 Neg y>0 Abbildung 11.5: Ausrechnen der richtigen Eckpunkte oder außen zeigen. Bei der verwendeten Berechnung zeigen sie nach innen. Das heißt also, wenn der Punkt N eg im positiven Halbraum liegt, ist mit Sicherheit die ganze Box außerhalb. Liegt der Punkt P os im positiven Halbraum, so könnte von der Box etwas zu sehen sein. (Könnte? Sie kann auch noch auf der anderen Seite des View Frustums außerhalb liegen. Dann ist aber P os im positiven Halbraum, das heißt außerhalb. In diesem Fall ist die Box nur teilweise im View Frustum.) Um festzustellen auf welcher Seite ein Punkt liegt, bietet es sich an, das Punktprodukt zu verwenden. Es muss der Normalenvektor der Ebene mit dem zu testenden Punkt multipliziert werden. Dadurch erhält man die Länge des Punktvektors in Richtung des Normalenvektors (gerechnet vom Ursprung). Nun addiert man die Distanz der Ebene vom Ursprung hinzu. Durch diesen Schritt erhält man die Läge des Vektors von der Ebene gerechnet. Ist das Ergebnis Null, so liegt der Punkt genau auf der Ebene. Ist es jedoch größer, so liegt der Punkt im positiven Halbraum, ist er kleiner entsprechend im negativen Halbraum. Diesen Test führt man mit allen sechs Ebenen durch, bis man mit Sicherheit ein Ergebnis hat. Kapitel 12 BSP-Bäume Bei den BSP-Bäumen handelt es sich um die wichtigste Technik in der 3DGrafik-Programmierung. Fast alle Indoor-Engines (also die Berechnung von Szenen innerhalb von Gebäuden) verwenden diese Technik. 12.1 Grundlagen BSP-Bäume (oder BSP-Trees) werden nur für Indoor-Engines eingesetzt. Typisch für eine Indoor-Engine ist, dass man nicht weit blicken kann, da immer wieder Wände oder ähnliches die Sicht versperren. Dies ist der Hauptunterschied zu Outdoor-Engines, bei denen man typischerweise sehr weit blicken kann. Dabei sind aber Details am Horizont nicht mehr wichtig, wohingegen man bei Indoor-Engines alle Details sehen möchte. BSP steht für Binary Space Partition und ist eine Technik, um die Anzahl der zu rendernden Dreiecke zu reduzieren. 12.2 Theorie Wird eine Szene einfach ,blind‘ an die Grafikkarte gesendet, so muss diese alle Dreiecke berechnen, obwohl ein Großteil gar nicht sichtbar ist. Die Idee ist nun, die Szene in einzelne kleine Bereiche zu unterteilen, und nur die sichtbaren Teile an die Grafikkarte zu senden. Daher kommt das ,SP‘ in BSP, also das Zerteilen (Partition) der Szene (Space). Es bleibt noch zu klären, was das ,B‘ bedeutet. Diese Teile werden in einen binären Baum einsortiert, der es ermöglicht, mit sehr hoher Geschwindigkeit zu entscheiden was sichtbar ist, und was nicht. Dabei geht es nur um die statischen Dinge in einer Szene und genauer gesagt sogar nur um die Architektur. Detailierte Objekte wie Einrichtungsgegenstände werden erst später hinzugefügt. 89 90 KAPITEL 12. BSP-BÄUME konvex konkav Abbildung 12.1: Konvex und Konkav 12.2.1 Ziel Das Ziel ist eine in einen Baum verpackte Darstellung der Geometrie. Dabei sollen die einzelnen Teilstücke jeweils konvex sein. Die Definition von ,konvex‘ ist: Wenn man zwei beliebige Punkte verbindet, so schneidet diese Strecke keine andere Verbindung. Abbildung 12.1 stellt noch einmal den Unterschied zwischen konvex und konkav grafisch dar. 12.2.2 BSP-Baum erstellen Als Beispiel soll hier eine Szene verwendet werden, die sehr einfach ist1 . Sie ist in Abbildung 12.2 dargestellt. Dort sieht man das Level von oben; Boden und Decke sind nicht eingezeichnet und werden hier auch nicht weiter beachtet. Die dargestellten Zahlen sind die Nummern der Wände. Der Kasten unter dem Level beinhaltet den Startzustand als Baum, das heißt, es befinden sich alle Wände in dem einzigen Knoten des Baums. Weiterhin ist interessant, welche Seite die ,Vorderseite‘ der Wand ist. Dies wird durch so genannte Normalenvektoren gekennzeichnet. Dabei handelt es sich um normalisierte Vektoren (das heißt Vektoren der Länge eins), die senkrecht auf der Fläche stehen und in die Richtung zeigen, die vorne ist. Diese sind in den folgenden Beispielen nicht eingezeichnet, da sie immer nach innen zeigen. Schritt 1 Dieses Level soll nun in einen Baum umgewandelt werden. Dazu muss es aufgeteilt werden. Man könnte nun einfach einen Schnitt in der räumlichen Mitte machen. Das ist aber, wie später noch zu sehen sein wird, ungeschickt. Stattdessen sucht man eine Wand, die das Level in zwei ähnlich große Teile aufteilt. Hier soll dazu die Wand 3 beziehungsweise 11 verwendet werden. Es werden zwei neue Knoten K1 und K2 erstellt. Der Knoten K0 wird zu A umbenannt, da er keine Wände mehr enthält. Die beiden neuen Knoten 1 In Wirklichkeit ist einerseits die Szene komplexer (im Allgemeinen wird sie nicht nur aus rechten Winkeln bestehen und wesendlich größer sein) und andererseits ist dort alles dreidimensional. 12.2. THEORIE 91 2 6 3 4 5 1 11 12 7 10 9 8 K0 1 2 3 4 5 6 7 8 9 10 11 12 Abbildung 12.2: Das verwendete Beispiellevel mit zugehörigem Baum 92 KAPITEL 12. BSP-BÄUME A 2 6 3 4 5 1 11 7 10 9 12 8 A K1 1 2 3 11 12 K2 4 5 6 7 8 9 10 12 Abbildung 12.3: Beispiellevel mit BSP-Baum nach der ersten Unterteilung werden A untergeordnet. K1 ist dabei der sogenannte Front- und K2 der Back-Zweig. Jetzt wird jede Wand in einen der neuen Knoten des Baumes einsortiert, und zwar je nachdem, auf welcher Seite der Schnittebene sie sich befindet. Die Wände, die sich ,vor‘ der Schnittebene befinden, werden in den Front-Zweig, die anderen in den Back-Zweig eingeordnet. Die Wände, die direkt auf der Schnittebene liegen, werden nach ihren Normalenvektoren einsortiert. Das Ergebnis dieses ersten Schnitts ist in Abbildung 12.3 mit zugehörigem Baum dargestellt. Schritt 2 Das Erstellen eines Baumes ist normalerweise ein rekursiver Prozess. Bei BSP-Bäumen ist das auch der Fall. Als nächstes schaut man sich also K1 an und überprüft, ob sich hier eine weitere sinnvolle Unterteilung vornehmen 12.2. THEORIE 93 A 2 6 3 4 5 7a 1 B 10 11 9 12 7b 8 A B K1 1 2 3 11 12 K3 4 5 6 7a 10 K4 7b 8 9 Abbildung 12.4: Beispiellevel mit BSP-Baum nach dem zweiten Schritt lässt. Dabei betrachtet man nur noch die Wände, die sich innerhalb dieses Knotens befinden. Da es sich bereits um ein konvexes Teil des Levels handelt, ist keine weitere Unterteilung von nöten. Daher geht man wieder einen Knoten nach oben (zu Knoten A) und von dort aus in den nächsten Knoten nach unten, also zu K2 . Hierbei handelt es sich noch nicht um ein konvexes Teilstück, es muss also noch weiter unterteilt werden. Dazu soll die Wand 10 verwendet werden. Auch diesmal werden zwei neue Knoten (K3 und K4 ) erstellt und die Wände aufgeteilt. K2 wird bei dieser Gelegenheit nach B umbenannt. Bei diesem Schnitt gibt es aber im Gegensatz zu Schritt 1 eine Besonderheit: der Schnitt läuft direkt durch Wand 7. Das heißt sie befindet sich auf beiden Seiten der Schnittebene, und dadurch muss sie auch in beide neuen Knoten einsortiert werden. Damit eine Wand aber nicht in zwei Knoten vorkommt, muss sie vorher zerlegt werden. Dadurch ergeben sich zwei neue Wände 7a und 7b. Die alte Wand 7 wird entfernt. Das Ergebnis ist in Abbildung 12.4 dargestellt. 94 KAPITEL 12. BSP-BÄUME A C 2 6 3 4 5 7a 1 11 10b B 10a 9 12 7b 8 A B K1 1 2 3 11 12 C K5 5 6 7a 10a K6 4 10b K4 7b 8 9 Abbildung 12.5: Beispiellevel mit BSP-Baum nach dem dritten Schritt Schritt 3 Die Rekursion wird mit K3 fortgesetzt. Dieser Knoten enthält immer noch ein konkaves Teilstück, muss also weiter unterteilt werden. Hier bietet sich zum Beispiel Wand 5 an. Alle weiteren Aktionen sind schon in Schritt 2 beschrieben worden. Das Ergebnis des dritten Schritts ist in Abbildung 12.5 dargestellt. Schritt 4 Rekursiv wird nun weiter nach unten gestiegen. Dabei wird als erstes Knoten K5 betrachtet. Er ist bereits konvex, braucht also nicht weiter unterteilt zu werden. Der nächste Knoten K6 ist ebenfalls konvex. Über Knoten C und B gelangt man schließlich noch zu Knoten K4 . Auch dieser Knoten muss nicht weiter unterteilt werden, die Rekursion kommt zum Ende. Damit ist der BSP-Baum für dieses kleine Beispiel-Level fertig erstellt und könnte nun gerendert werden. 12.3. PSEUDO-CODE 12.2.3 95 Die beste Schnittebene finden Zwei Aspekte spielen bei der Auswahl der besten Schnittebene eine Rolle: Einerseits soll der Baum möglichst ausgewogen sein, andererseits sollten so wenig Wände wie möglich geteilt werden. Ersteres besagt, dass sich im linken Ast möglichst genau so viele Wände befinden wie im rechten. Zweiteres sollte man beachten, da sich bei jedem Teilen die Anzahl der Dreiecke erhöht. 12.2.4 Bounding-Boxen In Kapitel 11 wurden Bounding-Boxen behandelt. Diese werden nun eingesetzt, um einen gewaltigen Geschwindigkeitsvorteil zu erzeugen. Für jeden Knoten wird eine Bounding-Box berechnet, und zwar über alle enthaltenen Wände (auch die in Knoten weiter unten liegen). Dadurch erreicht man erst den großen Geschwindigkeitsvorteil der BSP-Bäume. Beim Rendern wird dann der Baum von oben her durchlaufen und für jeden Knoten geprüft, ob er sichtbar ist. Ist er es nicht, können auch seine untergeordneten Knoten nicht sichtbar sein und müssen nicht weiter getestet werden. So kann im besten Fall bereits nach einem Test festgestellt werden, dass die Hälfte der Wände nicht mehr gerendert werden muss. 12.3 Pseudo-Code Hier soll die Vorgehensweise in Pseudo-Code dargestellt werden.2 12.3.1 Datenstruktur Knoten Soll ein Baum erstellt werden, so muss das Ergebnis irgendwo abgespeichert werden. Es muss also eine Struktur geschaffen werden, die alle benötigten Informationen enthält. Eine mögliche Struktur ist in Listing 12.1 dargestellt. Wenn ein solcher Knoten abgespeichert wird, muss unterschieden werden, ob es sich um einen inneren Knoten oder um ein Blatt handelt. Die Unterscheidung, um welche Art es sich handelt, befindet sich in dem Feld boLeaf, welches im Falle eines Blattes ,wahr‘ enthält. Je nach Wert dieses Feldes sind einige Felder der Struktur ungültig. In beiden Fällen gibt es ein Feld, dass die Bounding-Box dieses Knotens enthält. Die nächsten beiden Felder haben nur bei einem Blatt Gültigkeit. Sie beziehen sich auf die Dreiecke, die dieses Blatt enthält. lCountTriangles enthält die Anzahl der Dreiecke, axTriangles die Dreiecke selbst. 2 Kann mit dem Pseudo-Code-Compiler von Hellwig Geisse nicht übersetzt werden. 96 1 2 3 4 5 6 KAPITEL 12. BSP-BÄUME struct Node { //Um was für einen Knoten handelt es sich? bool boLeaf; //Die BB um den Knoten (und seine Kinder) AABox xBoundingBox; 7 8 9 10 11 //Die Anzahl der Dreiecke long lCountTriangles; //Die Dreiecke in diesem Leaf BSPTriangle *axTriangles; 12 13 14 15 16 17 18 //Die Teilungsebene Plane xSplittingPlane; //Die beiden Kind-Knoten Node *pxBackNode; Node *pxFrontNode; }; Listing 12.1: Struktur zum Abspeichern eines BSP-Knotens Die letzten drei Felder sind nur bei einem inneren Knoten von Bedeutung. Das erste Feld (xSplittingPlane) gibt die Ebene an, an der das Level in diesem Knoten geteilt wurde. Die nächsten beiden Felder beinhalten Zeiger auf die beiden Kinder-Knoten. Dreieck In der Struktur Node wurde ein Datentyp BSPTriangle verwendet. Dieser ist in Listing 12.2 dargestellt. Da jedes Dreieck nur ein einziges Mal zum Teilen verwendet werden darf, muss man sich merken, dass ein Dreieck schon als Teiler genutzt wurde. Dies wird in dem Feld boWasSplitter vermerkt. In xPlane wird die Ebene des Dreiecks gespeichert und in iTextureIndex der Index der Textur für dieses Dreieck. Das letzte Element enthält schließlich die drei Punkte des Dreiecks. 12.3.2 Baum erstellen Jetzt sind alle Vorbereitungen abgeschlossen und es kann der Baum erstellt werden. In Listing 12.3 ist kurz beschrieben, wie die generelle Funktionsweise aussieht. Hier wird zuerst das Level in einen Knoten geladen und als nächstes die rekursive Methode zum Erstellen des Baumes angestoßen. Die erste Methode erzeugt also einen Baum mit nur einem Knoten, dem Wurzelknoten. Die 12.3. PSEUDO-CODE 1 2 3 4 5 6 7 8 9 10 11 97 struct BSPTriangle { //Wurde dieses Dreieck schon mal zum Teilen verwendet? bool boWasSplitter; //Die Ebene in der dieses Dreieck liegt Plane xPlane; //Der Index der Textur für dieses Dreieck int iTextureIndex; //Die Punkte des Dreiecks D3DVERTEX_3D_TEXTURED axPoints[3]; }; Listing 12.2: Der Datentyp BSPTriangle 1 Lade Leveldaten nach Knoten[0]; 2 3 ErstelleBaum(0); Listing 12.3: Die generelle Vorgehensweise zum Erstellen eines BSP-Baumes zweite Methode unterteilt nun diesen Knoten und fügt eventuell zwei neue hinzu. Danach ruft sie sich unter Umständen erneut auf. 12.3.3 Daten laden Das Laden der Daten soll nicht genauer behandelt werden, da es eine Unmenge an Möglichkeiten gibt. Hat man sein eigenes Datenformat entwickelt, so muss die Lade-Routine darauf angepasst sein. Es ist auch möglich, das Standard-.x-Format zu verwenden. Dann muss man nach dem Laden des Meshs Zugriff auf die Vertex- und Texturdaten erhalten und diese dann in den Wurzelknoten übertragen. 12.3.4 Texturen Wurde bisher ein Mesh verwendet, so wurden auch die Texturen vom Mesh verwaltet. Wird ein Mesh aber ,auseinandergenommen‘ so muss man sich selbst um die Verwaltung der Texturen kümmern. Man braucht also eine Liste der benötigten Texturen samt einer Art ,Textur-Managers‘, der sich um die Verwaltung der Texturen kümmert. So sollten Texturen bei mehrfachem Gebrauch nur einmal im Speicher liegen. Der Index der Textur kann dann in dem Datenfeld iTexturIndex des Dreiecks gespeichert werden. 98 KAPITEL 12. BSP-BÄUME 12.3.5 Knoten erstellen Diese Methode erstellt den eigentlichen Baum, in dem sie einen Knoten entgegen nimmt und diesen eventuell unterteilt. Werden zwei neue Knoten hinzugefügt, dann ruft sie sich selbst wieder auf. Listing 12.4 zeigt die Vorgehensweise in Pseudo-Code. Dieser Methode wird als Parameter einen Zeiger auf den Knoten übergeben, den sie bearbeiten soll. Dadurch ist die Rekursion möglich. Als erste Aktion wird in Zeile 3 eine Bounding-Box um diesen Knoten erstellt. Jeder Knoten muss eine Bounding-Box um alle seine Dreiecke (auch die seiner Kinder-Knoten) enthalten, um die Darstellung zu beschleunigen. In Zeile 4 wird dann versucht eine Teilungsebene zu finden, dies ist in eine eigene Methode ausgelagert. Wird keine Teilungsebene gefunden (Überprüfung in Zeile 6), so handelt es sich bei dem aktuellen Knoten um ein Blatt. In diesem Fall ist keine weitere Bearbeitung mehr nötig und die Methode wird verlassen (Zeile 9). Dies ist der Basisfall der Rekursion, das heißt, sie endet hier. Wurde eine Teilungsebene bestimmt, so müssen als nächstes zwei neue Knoten erstellt werden. Diese werden als Front- und Back-Node bezeichnet. Anschließend müssen alle Dreiecke des aktuellen Knotens auf die beiden neuen Knoten verteilt werden. Die Aufteilung geschieht je nach Lage der Dreiecke in Relation zur Teilungsebene. Für jedes Dreieck (Zeile 13) des aktuellen Knotens wird getestet, auf welcher Seite der Ebene sich das Dreieck befindet (Zeile 15). Dabei können vier Fälle eintreten: 1. das Dreieck ist im positiven Halbraum, also vor der Ebene, 2. das Dreieck liegt im negativen Halbraum, also hinter der Ebene, 3. das Dreieck schneidet die Ebene oder 4. das Dreieck liegt genau auf der Ebene Die ersten beiden Fälle sind einfach zu behandeln. Liegt das Dreieck vor der Ebene, so wird es in den Front-Node verschoben (Zeile 16-19). Liegt es hingegen hinter der Ebene, wird es in den Back-Node verschoben (Zeile 20-23). Der nächst schwierigere Fall ist der vierte: Hier muss entschieden werden, in welche der beiden Knoten das Dreieck sortiert wird. Dazu müssen die Normalenvektoren der Ebene und des Dreiecks verglichen werden (Zeile 26). Zeigen sie in die gleiche Richtung, so wird das Dreieck in den FrontNode, andernfalls in den Back-Node sortiert. Im dritten Fall (das Dreieck schneidet die Ebene) muss das Dreieck in mehrere neue Dreiecke zerteilt werden. Dies ist hier in eine gesonderte Methode ausgelagert. Das Ergebnis sind dann mehrere Dreiecke, die je nach ihrer Lage in die beiden Knoten einsortiert werden müssen. 12.3. PSEUDO-CODE 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 Erstelle Knoten(Knoten *) { Erstelle Bounding-Box für diesen Knoten; Suche beste Trennungsebene; Falls(keine Trennungsebene gefunden) { //! Dies ist ein Blatt return; } Erstelle zwei neue Knoten; Für alle Dreiecke diese Knotens { Prüfe auf welcher Seite ein Dreieck liegt; Falls(Dreieck davor) { Speichere Dreieck in Front-Node; } Falls(Dreieck dahinter) { Speichere Dreieck in Back-Node; } Falls(Dreieck auf der Ebene) { Falls(Normale des Dreiecks = Normale der Ebene) { Speichere Dreieck in Front-Node; } Sonst { Speichere Dreieck in Back-Node; } } Sonst { Teile Dreieck auf Front- und Back-Node auf; } } Knoten in Baum einhängen; Erstelle Knoten(Front-Node); Erstelle Knoten(Back-Node); } Listing 12.4: Erstellung eines Knotens in Pseudo-Code 99 100 KAPITEL 12. BSP-BÄUME Jetzt sind die beiden neuen Knoten erstellt, und können in den Baum eingehängt werden. Dies geschieht in Zeile 42. Das Einhängen hätte auch schon vor der Schleife geschehen können. Die letzten beiden Anweisungen bilden schließlich die Rekursion. In den Zeilen 44 und 45 wird diese Methode erneut aufgerufen, diesmal aber mit den neuen Knoten als Parameter. 12.3.6 Beste Teilungsebene finden Um einen Knoten zu teilen, muss die beste Teilungsebene gefunden werden. Dabei sind zwei Dinge zu beachten: 1. Die Ebene sollte die Szene in zwei gleich große Hälften teilen und 2. sie sollte möglichst wenige Dreiecke schneiden. Der erste Punkt besagt, dass ungefähr gleich viele Dreiecke vor und hinter der Ebene liegen sollen. Die tatsächliche Größe der Dreiecke spielt dabei keine Rolle. Dadurch erhält man am Ende einen möglichst ausgeglichenen Baum. Der zweite Punkt besagt, dass möglichst wenig Dreiecke geteilt werden sollen. Schneidet die Ebene ein Dreieck, so muss es in mehrere kleinere Dreiecke zerteilt werden, was die Gesamtanzahl der Dreiecke erhöht. Dies ist aber kontraproduktiv, da hier versucht wird, die Anzahl der zu zeichnenden Dreicke zu minimieren. Daher ist es auch schlecht, ein Level einfach in seiner räumlichen Mitte zu teilen, da dann wesentlich mehr Schnitte durchgeführt werden müssten. Listing 12.5 beschreibt das Finden der besten Teilungsebene in PseudoCode. Der Methode wird eine Liste von Dreiecken übergeben, aus der sie die beste Ebene bestimmen soll. Dazu probiert die Methode alle möglichen Ebenen der Reihe nach durch (Zeile 3). In Zeile 5 wird die Ebene des aktuellen Dreiecks als Teilungsebene definiert. Sollte dieses Dreieck schon einmal eine Teilungsebene geliefert haben (dies kann in dem Datenfeld boWasSplitter des Dreiecks gespeichert werden), so wird dieses Dreieck nicht beachtet, da es mit Sicherheit nicht das beste ist. Als nächstes werden alle Zähler mit ,0‘ initialisiert (Zeile 11-13). Nun werden alle Dreiecke bezüglich ihrer Lage zur Teilungsebene betrachtet. Dazu wird, wie auch bei der Methode zum Erstellen von Knoten, geprüft wie das Dreieck liegt und der entsprechende Zähler erhöht. Hierbei wird also noch nichts sortiert oder zerteilt. Wenn diese Zählung abgeschlossen ist, also alle Dreiecke bezüglich der Teilungsebene beurteilt wurden, wird überprüft, ob die aktuelle Teilungsebene eventuell besser ist als die bisher beste. Es muss sich also die beste 12.3. PSEUDO-CODE 1 2 3 4 5 6 7 8 9 Finde beste Teilungsebene(Dreiecke) { Für alle Dreiecke i { Ebene von Dreieck i ist jetzt testhalber Teilungsebene; Falls(i war schon Teilungsebene) { Weiter bei nächstem Dreieck; } 10 Zähler Davor = Dahinter = Geschnitten = 0; 11 12 Für alle Dreiecke j { Falls(i = j) { Weiter bei nächstem Dreieck; } 13 14 15 16 17 18 19 Prüfe auf welcher Seite das Dreieck j liegt; Falls(Dreieck davor) { Zähler Davor ++; } Falls(Dreieck dahinter) { Zähler Dahinter ++; } Falls(Dreieck geschnitten) { Zähler Geschnitten ++; } 20 21 22 23 24 25 26 27 28 29 30 31 32 } 33 34 Falls(Ebene besser als bisher beste) { Diese Ebene ist nun die beste; } 35 36 37 38 } return Beste Ebene; 39 40 41 } Listing 12.5: Das Finden der besten Teilungsebene in Pseudo-Code 101 102 KAPITEL 12. BSP-BÄUME Ebene gemerkt werden. Um festzustellen, wie gut eine Teilungsebene ist, bietet sich ein Punktesystem an. Dabei wird ausgerechnet, wie viele Punkte die Ebene erhält. Dies kann zum Beispiel nach folgender Formel erfolgen: P unkte =| Davor − Dahinter | +Geschnitten ∗ 5 Eine ideale Teilungsebene3 würde hierbei null Punkte erreichen: Es sind genauso viele Dreiecke vor wie hinter der Ebene, daher ergibt die Subtraktion Null. Außerdem schneidet sie kein anderes Dreieck, so dass auch der zweite Teil Null wird. Je kleiner die Anzahl der Punkte ist, desto besser ist die Teilungsebene. Ist die Anzahl der Punkte kleiner als die der bisher besten Teilungsebene, so könnte es sich hierbei um eine neue beste Teilungsebene handeln. Jetzt muss noch überprüft werden, ob auf beiden Seiten der Ebene überhaupt Dreiecke liegen. Sind nur auf einer Seite Dreiecke vorhanden, so ist diese Ebene nicht zu gebrauchen. Dazu wird überprüft, ob in den Zählern Davor und Dahinter oder im Zähler Geschnitten ein Wert größer Null enthalten ist. Trifft auch das zu, so wurde eine neue beste Teilungsebene gefunden. Wurde insgesamt eine Teilungsebene gefunden, so wird diese zurückgegeben. Wurde keine Ebene mehr gefunden, so handelt es sich um eine konvexe Anordnung von Dreiecken und es handelt sich letztendlich um ein Blatt im Baum. 12.3.7 Dreiecke bezüglich einer Ebene klassifizieren Listing 12.6 zeigt den Pseudo-Code zum Klassifizieren eines Dreiecks bezüglich einer Ebene. Voraussetzung für diese Methode ist, dass man einen Punkt bezüglich einer Ebene klassifizieren kann. Dies ist bereits in Kapitel 11.3.3 beschrieben worden. Mit Hilfe dieser Funktionalität überprüft man jeden Punkt des Dreiecks (Zeile 9) und erhöht die entsprechenden Zähler (Zeile 12, 16 und 20 bis 22). Wichtig ist hierbei, bei einem Punkt auf der Ebene alle Zähler zu erhöhen. Sind alle Punkte untersucht worden, so wird festgestellt wie das Endergebnis aussieht. Sind alle drei Punkte auf der Ebene (Zeile 25), so ist auch das Dreieck auf der Ebene. Sind alle Punkte davor (Zeile 29) oder dahinter (Zeile 33), so ist auch das Dreieck davor beziehungsweise dahinter. Wenn die Punkte jedoch teilweise auf der einen und teilweise auf der anderen Seite liegen (Zeile 37), so schneidet die Ebene das Dreieck. Wichtig ist hierbei, dass die Reihenfolge der Abfragen nicht verändert wird, da sonst falsche Ergebnisse erzeugt werden. 3 Dieser Fall wird im Allgemeinen nicht eintreten. 12.3. PSEUDO-CODE 1 2 3 4 5 103 Klassifiziere Dreieck(Dreieck, Ebene) { Zähler Davor = 0; Zähler Dahinter = 0; Zähler AufEbene = 0; 6 Für jeden Punkt i des Dreiecks { Klassifiziere Punkt(i, Ebene); Falls(Punkt davor) { Zähler Davor ++; } Falls(Punkt dahinter) { Zähler Dahinter ++; } Sonst { Zähler Davor ++; Zähler Dahinter ++; Zähler AufEbene ++; } } Falls(Zähler AufEbene = 3) { return Dreieck auf Ebene; } Falls(Zähler Davor = 3) { return Dreieck davor; } Falls(Zähler Dahinter = 3) { return Dreieck dahinter; } return Dreieck schneidet; 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 } Listing 12.6: Pseudo-Code zum Klassifizieren eines Dreiecks bezüglich einer Ebene 104 KAPITEL 12. BSP-BÄUME E P3 P3 E P3 S1 S1 =⇒ P1 P2 P1 P2 Abbildung 12.6: Teilung eines Dreiecks (einfach) 12.3.8 Ein Dreieck aufteilen Unter Umständen ist es nötig ein Dreieck zu zerteilen. Dann tritt die Methode, welche in Listing 12.7 angegeben ist, in Aktion. Die Methode benötigt das zu zerlegende Dreieck und die Ebene an der das Dreieck zerschnitten werden soll. Diese werden als Parameter übergeben (Zeile 1). Alle Punkte des Dreiecks müssen aufgeteilt und gespeichert werden. Dazu werden zwei Listen für die Punkte vor und hinter der Ebene benötigt (Zeile 3). Jetzt wird jeder Punkt des Dreiecks bezüglich der Ebene klassifiziert. Liegt der aktuelle Punkt auf der Ebene, so wir er einfach in beide Listen einsortiert. Liegt er jedoch nicht auf der Ebene, so wird überprüft, ob die Strecke zwischen dem aktuellen Punkt und seinem Vorgänger (Achtung beim ersten Punkt! Diesen eventuell extra behandeln.) die Ebene schneidet. Ist die nicht der Fall, so wird der aktuelle Punkt einfach in die entsprechende Liste einsortiert. Schneidet die Strecke jedoch die Ebene, so muss der Schnittpunkt genau berechnet und in beide Listen eingefügt werden. Bei der Berechnung des Schnittpunktes müssen natürlich auch die Texturkoordinaten neu berechnet werden. Alle Punkte, auch die neuen Schnittpunkte, sind nun in den beiden Listen vorhanden. Jetzt müssen daraus neue Dreiecke erstellt werden. Um die nächsten Schritte zu verstehen, ist es wichtig zu wissen, welche Fälle beim Zerteilen eines Dreiecks entstehen können. Die einfache Variante zeigt Abbildung 12.6. Dabei wird ein Dreieck in zwei kleinere Dreiecke zerteilt, da die Ebene genau durch eine der Ecken des Dreiecks läuft. Die etwas schwierigere Variante ist in Abbildung 12.7 dargestellt. Hier 12.3. PSEUDO-CODE 1 2 3 105 Teile Dreieck(Dreieck, Ebene) { Erstelle Front- und Back-Punkte-Liste; 4 Für jeden Punkt i des Dreiecks { Klassifiziere Punkt(i, Ebene); Falls(Punkt auf Ebene) { Punkt in Front-Liste; Punkt in Back-Liste; } Sonst { Falls(Strecke Punkt i zu Punkt i-1 die Ebene schneidet) { Berechne Schnittpunkt; Füge Schnittpunkt in Front-Liste; Füge Schnittpunkt in Back-Liste; } Falls(Punkt davor) { Punkt in Front-Liste; } Falls(Punkt dahinter) { Punkt in Back-Liste; } } } 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 Erstelle 2-3 neue Dreiecke; Setze Punkte der neuen Dreiecke; Übernimm Eigenschaften des alten Dreiecks; 32 33 34 35 } Listing 12.7: Das Zerteilen eines Dreieck an einer Ebene im Pseudo-Code 106 KAPITEL 12. BSP-BÄUME E E P3 P3 =⇒ P1 P2 P1 S2 S2 S1 S1 P2 Abbildung 12.7: Teilung eines Dreiecks (schwer) liegt keiner der Punkte auf der Ebene. Diese Variante ist desshalb etwas schwieriger, da hier zwei Schnittpunkte berechnet werden müssen. Außerdem entstehen auf der einen Seite ein Polygon, welches in Dreiecke zerlegt werden muss. Dies ist in der Abbildung durch die gestrichelte Linie angedeutet. Es müssen also, je nach Anzahl Punkte in den Listen, die entsprechende Anzahl Dreiecke erzeugt werden (Zeile 32). In diese Dreiecke werden die Punkte aus den Listen nun übertragen. Des weiteren muss man auch die Eigenschaften des ursprünglichen Dreiecks, wie zum Beispiel den Texturindex, in die neuen Dreiecke übernehmen. 12.3.9 Schnittpunkt einer Ebene und einer Gerade bestimmen Die Berechnung des Schnittpunktes einer Geraden und einer Ebene ist eine relativ einfache Angelegenheit. Benötigt wird dazu lediglich zwei mal das Punktprodukt und eine einfache Relation. In Abbildung 12.8 sind alle Berechnungen noch einmal grafisch dargestellt. Zuerst berechnet man den Abstand des ersten Punktes P1 zur Ebene mit dem Punktprodukt: v1 = P − P1 ~ Abstand1 = v~1 · N Danach tut man so, als ob man die Ebene so verschoben hätte, dass der zweite Punkt P2 auf der Ebene liegt. Nun berechnet man den Abstand von P1 zur Ebene erneut: v2 = P2 − P1 12.3. PSEUDO-CODE v~1 107 E E P P P1 P1 v~2 P2 P2 ~ N ~ N ~ v~1 · N ~ v~2 · N Abbildung 12.8: Grafische Darstellung der Berechnungen für den Schnittpunkt E P a~2b a~2a P1 v~2a S v~2b ~ N P2 Abbildung 12.9: Zur Berechnung des Schnittpunktes ~ Abstand2 = v~2 · N Für die folgende Aussage ist die Abbildung 12.9 wichtig. Hier wurde der Vektor v2 in seine beiden Teilstücke aufgeteilt. Diese gehen von P1 zum Schnittpunkt S und von dort zu Punkt P2 . Der waagerechte Vektor, von dem bisher nur die Längen berechnet wurde, wurde hier mit a2a und a2b bezeichnet. Mit dieser Definition gilt folgender Satz: a2a a2b = v2a v2b Nun berechnet man Abstand1 = P rozent Abstand2 P rozent ist dann der Anteil von v~2 nach dem (von P1 aus gesehen) der Schnittpunkt liegt. Das heißt, dass dieser nun durch S = P1 + (v~2 · P rozent) 108 KAPITEL 12. BSP-BÄUME berechnet werden kann. Es bleibt nun noch die Frage nach den Texturkoordinaten. Dies funktioniert prinzipiell genau so wie die Berechnung des Schnittpunktes. Man berechnet den Unterschied zweier Texturkoordinaten: ∆T ex = T ex2 − T ex1 Die neuen Texturkoordinaten ergeben sich dann aus der ersten Texturkoordinate und diesem Delta-Wert multipliziert mit dem oben berechneten P rozent: T ex0 = T ex1 + (∆T ex · P rozent) 12.4 Anwendung Die beste Methode ist das Erstellen eines BSP-Compilers. Dieser nimmt die Rohdaten (zum Beispiel im .x-Format) entgegen und erzeugt einen BSPBaum mit allen benötigten Daten. Dadurch erspart man sich das Erstellen zur Laufzeit des Programms und damit ,Ladezeit‘4 . Im eigentlichen Programm wird dann nur noch der fertige Baum von der Festplatte geladen und angezeigt. 12.5 Rendern Ist der BSP-Baum geladen, kann dieser angezeigt werden. Das Rendern des Baumes ist eine erschreckend einfache Sache. 12.5.1 Aufruf Da es sich um einen Baum handelt, muss natürlich auch das Zeichnen einen rekursive Funktion sein. Um das Zeichnen aber möglichst in einen Funktionsaufruf zu kapseln, erstellt man zunächst eine Funktion, die alle Vorbereitungen, wie das Setzen der Renderstates, vorher durchführt. Diese Funktion kann auch dazu verwendet werden, um eventuelle Einstellungen nach erledigter Arbeit wieder rückgängig zu machen. 12.5.2 Rekursion Listing 12.8 zeigt die rekursive Funktion in Pseudo-Code. Dieser Code birgt an sich keine großen Überraschungen. Als erstes wird in Zeile 3 getestet, ob dieser Knoten überhaupt sichtbar ist. Wenn er nicht sichtbar ist, kann man mit Sicherheit sagen, dass auch alle seine Kinder unsichtbar sind, daher endet die Rekursion hier. 4 Hier wird ,Ladezeit‘ angegeben, da die Erstellung des BSP-Baumes dann beim Laden des Levels durchgeführt werden muss. 12.5. RENDERN 109 In Zeile 8 wird geprüft, ob es sich um ein Blatt handelt. Wenn das der Fall ist, werden alle Dreiecke dieses Knotens gezeichnet. Die einzige Sache, die etwas nähere Betrachtung erfordert, sind die Zeilen 14 bis 24. Hier wird geprüft, auf welcher Seite der Teilungsebene des Knotens sich die Kamera befindet. Befindet sie sich im negativen Halbraum, so wird zunächst der Front-Node und anschließend der Back-Node bearbeitet. Befindet sie sich jedoch im positiven Halbraum, dann werden die Kinder-Knoten in der anderen Reihenfolge abgearbeitet. 1 2 3 4 5 6 Render BSP Rekursiv(Knoten) { Falls(Knoten.AABB unsichtbar) { return; } 7 Falls(Knoten.boLeaf) { Rendern der Dreiecke; } Sonst { Klassifiziere Kamera-Position bzgl. Knoten.Teilungsebene; Falls(Position hinter der Ebene) { Render BSP Rekursiv(Knoten.Front-Node); Render BSP Rekursiv(Knoten.Back-Node); } Sonst { Render BSP Rekursiv(Knoten.Back-Node); Render BSP Rekursiv(Knoten.Front-Node); } } 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 } Listing 12.8: Der rekursive Anteil der Render-Funktion Es bleibt also die Frage, warum man diese Unterscheidung vornimmt. Ein Vorteil von BSP-Bäumen ist, dass man in der richtigen Reihenfolge zeichnen kann. Das heißt man zeichnet erst die Dreiecke, die weiter weg sind, und dann erst die dichteren. Das hat zwei Vorteile: Zum einen kann man den Z-Test abschalten, was Geschwindigkeit bringt, zum anderen werden nur dann Transparenzen richtig gezeichnet. Transparenzen werden berechnet, in 110 KAPITEL 12. BSP-BÄUME dem das neue Pixel mit dem bereits vorhandenen Wert verrechnet wird. Das heißt, dass das was hinter der ,Scheibe‘ liegt bereits berechnet sein muss. Teil V DirectX Input 111 Kapitel 13 DirectInput DirectInput ist die Komponente, die zur Steuerung von Eingabegeräten dient. Dabei kann sie sowohl Daten einlesen (zum Beispiel Eingaben mit der Tastatur oder der Maus) als auch diese Geräte steuern (zum Beispiel bei Force-Feedback Geräten). Dabei greift DirectInput direkt auf die Hardware zu, das heißt, es bezieht seine Informationen ohne die sonst übliche Nachrichtenwarteschlange, auch der dadurch entstehende Overhead fällt weg. Dadurch ist es möglich, Daten auch dann zu erhalten, wenn das Programm nicht im Vordergrund aktiv ist. Zunächst soll hier ein theoretischer Überblick über die Vorgehensweise der Nutzung von DirectInput gegeben werden. Danach soll beispielhaft das Verwenden von Maus und Tastatur dargestellt werden. 13.1 Vorgehensweise – theoretisch Das Nutzen von DirectInput gliedert sich in folgende Schritte: 1. Erstellen des DirectInput-Hauptobjektes. 2. Analog zur Enumeration der Grafikadapter werden nun die Eingabegeräte enumeriert. Dieser Schritt kann entfallen, wenn nur die Standardmaus oder -tastatur verwendet werden soll. 3. Erstellen des DirectInputDevice-Objektes für jedes zu verwendende Gerät. 4. Einrichten des Device 5. Angeben, dass das Gerät nun genutzt werden soll, also Zugriff auf das Gerät erhalten. 6. Daten empfangen 7. Beim Programmende müssen die Geräte und die Ojekte unbedingt wieder freigegeben werden. 113 114 13.1.1 KAPITEL 13. DIRECTINPUT Versionitits Bei der Nutzung von DirectInput in der Version 9.0c von DirectX fällt auf, dass alle Schnittstellen, Typen und so weiter die Zahl ,8‘ statt ,9‘ im Namen tragen. Das liegt daran, dass DirectX 9.0c immer noch die Version von DirectInput nutzt, die auch DirectX 8.0 verwendet hat. Hieran hat sich seither nichts geändert. 13.1.2 Ereignisse und Puffer Es gibt mehrere Möglichkeiten um bei DirectInput Daten abzufragen. Zunächst wird zwischen ereignisgesteuerten und gepollten Eingaben unterschieden. Bei ersterem wird jedesmal, wenn ein neues Datum vorliegt, ein Ereignis (englisch: Event) gesendet, welches dann eine ,Warte-Funktion‘ (zum Beispiel WaitForSingleObjekt) unterbricht. Dies ist die wahrscheinlich seltener genutzte Technik, da man bei Spielen zumeist keine Zeit mit warten verbringen möchte. Die zweite Technik, das Pollen, ist die weitaus häufiger verwendete, da man bei jedem Render-Durchgang auch noch gleich den Eingabe-Status abfragen kann. Dabei gibt es wiederum zwei verschiedene Möglichkeiten: gepuffert oder ungepuffert. Bei der gepufferten Variante werden alle eingegebenen Daten bei ihrem Auftreten in einen Puffer geschrieben. Dadurch können keine Eingaben, die eventuell zwischen zwei Abfragen eingegeben werden, verpasst werden. 13.2 Ein praktisches Beispiel Hier soll nun exemplarisch das Vorgehen dargestellt werden, welches zur Nutzung der Maus und der Tastatur erforderlich ist. Es wird empfohlen, diese Schnittstelle in einer eigenen Klasse zu implementieren. Als erstes muss in allen Fällen das DirectInput-Hauptobjekt erstellt werden. Dies erledigt die Funktion DirectInput8Create wie in Listing 13.1 dargestellt. 1 2 3 4 5 6 LPDIRECTINPUT8 pxDInput; DirectInput8Create(GetModuleHandle(NULL), DIRECTINPUT_VERSION, IID_IDirectInput8, (void**)&pxDInput, NULL); //HInstance //Version //Interface ID //Ergebnis //IUnknown Listing 13.1: Erstellen des DirectInput-Hauptobjektes Hier muss eine Versionsnummer mit angegeben werden (zweiter Parameter). Das liegt daran, dass DirectInput 8 auch die älteren Versionen von 13.2. EIN PRAKTISCHES BEISPIEL 115 DirectInput emulieren kann. Es ist aber immer zweckmäßig die vordefinierte Konstante DIRECTINPUT_VERSION anzugeben. 13.2.1 Maus Initialisierung Als Erstes soll die Maus verwendet werden. Dazu muss zunächst ein DirectInputDevice-Objekt erstellt werden. Dies erledigt die Methode CreateDevice des DirectInput-Objektes. Soll das Device zu einem enumerierten Gerät erstellt werden, so muss hier als erster Parameter die dadurch erhaltene ID angegeben werden. Da in diesem Beispiel aber die ohnehin immer vorhandene Systemmaus verwendet werden soll, kann hier die GUID GUID_SysMouse angegeben werden. Damit ist das DirectInputDevice-Objekt erstellt und auch an das Gerät gebunden. Es ist aber noch nicht festgelegt, wie die Daten erwartet werden. Der nächste Schritt ist daher das Einrichten des Device. Dabei muss insbesondere angegeben werden, wie die Daten erwartet werden. Dies kann durch die Methode SetDataFormat eingestellt werden. Dabei wird die globale Konstante c_dfDIMouse übergeben. Die zweite wichtige Einstellung betrifft den Kooperations-Level. Dieser gibt an, wie und wann das Gerät genutzt werden soll. Liegt das Fenster im Hintergrund, so empfängt es normalerweise keine Daten. Durch Angabe von DISCL_BACKGROUND kann der Empfang von Daten aber dennoch erreicht werden. Eine andere Möglichkeit ist das Anfordern der exklusiven Nutzungsrechte durch DISCL_EXCLUSIVE. Dies bedeutet jedoch nicht, dass andere Programme keine Möglichkeit haben Daten zu erhalten, ihnen ist es lediglich nicht möglich, ebenfalls exklusiv auf das Gerät zuzugreifen. All diese Einstellungen werden mit der Methode SetCooperativeLevel eingestellt. Des Weiteren ist es hier auch nötig das Fenster-Handle anzugeben, an welchem festgestellt werden soll, ob die Applikation im Vorder- oder Hintergrund arbeitet. Der letzte Schritt vor dem Aktivieren des Devices ist die Angabe der Puffergröße. Dieser Schritt kann entfallen, wenn eine ungepufferte Eingabe erwünscht wird, denn die Standardgröße des Puffers ist 0 Byte. Um die Größe zu setzen, muss die Struktur DIPROPDWORD mit Werten gefüllt werden. Sie enthält dabei ein Attribut vom Typ DIPROPHEADER, welcher nach dem Muster aus Listing 13.2 initialisiert werden muss. Mit dem zweiten Attribut (dwData) wird die Puffergröße angegeben. Diese Struktur wird mittels der Methode SetProperty an DirectInput übergeben. Als Property wird dabei DIPROP_BUFFERSIZE angegeben. Nun wird noch die Methode Acquire des Maus-Device aufgerufen, um Daten von dem Gerät empfangen zu können. Damit ist die Initialisierung der Maus abgeschlossen. 116 1 KAPITEL 13. DIRECTINPUT DIPROPDWORD xProperty; 2 3 4 5 6 xProperty.diph.dwSize xProperty.diph.dwHeaderSize xProperty.diph.dwObj xProperty.diph.dwHow = = = = sizeof(DIPROPDWORD); sizeof(DIPROPHEADER); 0; DIPH_DEVICE; Listing 13.2: Initialisieren des Headers des DIPROPDWORD’s Datenabfrage Um nun Daten zu bekommen, muss der Puffer mit der Methode GetDeviceData abgefragt werden. Dabei muss ein Array von Strukturen vom Typ DIDEVICEOBJECTDATA und deren Anzahl mit angegeben werden. Anschließend muss jedes Element im Array auf seine Daten überprüft werden. Im Attribut dwOfs steht dabei die Art des Wertes, also ob es sich dabei um eine Bewegung in X-Richtung oder einen Mausklick handelte. Der Wert selbst befindet sich dann in dem Attribut dwData. 13.2.2 Tastatur Initialisierung Die Initialisierung der Tastatur verläuft fast genau so wie die Initialisierung für die Maus. Die einzigen beiden Unterschiede sind die ID und das Datenformat. Als ID muss hier GUID_SysKeyboard und als Datenformat c_dfDIKeyboard angegben werden, der Rest bleibt gleich. Datenabfrage Auch die Abfrage der Daten gleicht der Abfrage der Maus, lediglich die Auswertung ist eine andere. DirectInput liefert nur Änderungen der Tasten, also nur Taste x wurde ” niedergedrückt“ oder Taste y wurde losgelassen“, aber nicht Taste x ist ” ” immer noch gedrückt“ oder gar Taste y ist nicht gedrückt“. Um welche ” Taste es sich handelt kann anhand des dwOfs Wertes in Erfahrung gebracht werden. Ob die Taste gedrückt oder losgelassen wurde, kann anhand des Wertes in dwData erkannt werden: Ist das höchstwertige Bit gesetzt, so wurde die Taste gedrückt. Glossar A AABBs Axis-Alligned-Bounding-Boxen (kurz AABBs) sind an den Koordinatenachsen ausgerichtete Boxen, die ein Objekt vollständig enthalten. Sie sind so klein wie möglich, aber so groß wie nötig. Sie können durch ihre Ausrichtung durch nur zwei Punkte angegeben werden. AGP-Speicher Hierbei handelt es sich um einen Speicherbereich im Systemspeicher, auf den die Grafikkarte zugreifen kann. B Backbuffer Dieser Puffer liegt im Speicher der Grafikkarte und enthält das nächste Bild. Das heißt, dass in diesen Puffer gezeichnet wird. Ist das Bild fertig gerendert, wird der Backbuffer mit dem Frontbuffer vertauscht. Es ist unter Direct3D möglich mit mehr als einem Backbuffer zu arbeiten. Backface Culling Bezeichnet das Entfernen von Dreiecken, die nur von hinten zu sehen sind. BSP Hierbei handelt es sich um die wichtigste Technik der 3D-GrafikProgrammierung. Sie wird verwendet um geschlossene Szenen effizient darzustellen. BSP steht für B inary S pace P artition. C COM COM steht für C omponent Object M odel. Komponenten bieten eine Funktionalität an, die von verschiedenen anderen Programmen genutzt werden kann. Die Komponenten stecken häufig in dlls, seltener in ausführbaren Programmen (Siehe auch Kapitel 3.3 ab Seite 15). Culling Bezeichnet das Entfernen von unsichtbaren Dreiecken. Die wichtigste Form ist das Backface Culling. 117 118 GLOSSAR D D3DX Bei D3DX handelt es sich um eine Bibliothek mit einigen nützlichen Zusatz-Funktionen für Direct3D. Es handelt sich also um eine Art Hilfsbibliothek. Die hierin enthaltenen Funktionen werden nicht zwingend zur Arbeit mit Direct3D benötigt, erleichtern einem die Arbeit jedoch sehr. Depthbuffer Dieser Puffer liegt im Speicher der Grafikkarte und tritt immer zusammen mit dem Stencilbuffer auf. Der Depthbuffer wird auch Tiefenpuffer oder Z-Buffer genannt. Er enthält den (virtuellen) Abstand eines jeden Pixels von der Kamera. Diffuses Licht ist das Licht, das von einer einzelnen Lichtquelle ausgeht. Es hat also neben der Farbe auch eine Richtung und eventuell (je nach Art der Lichtquelle) einen Ort. F Face Bei einem Face handelt es sich um eine Fläche im virtuellen 3D-Raum. In DirectX ist dies immer ein Dreieck. Far Clipping Plane Die Far Clipping Plane ist die Ebene, die senkrecht vor der Kamera steht, aber wesentlich weiter weg ist als die Near Clipping Plane. Objekte, die hinter der Far Clipping Plane liegen, werden nicht mehr beachtet. Field Of Vision kurz: FOV. Gibt den Sichtbereich im View Frustum an. Wird als Winkel angegeben, der von der Kamera aus gemessen von links nach rechts beziehungsweise von oben nach unten reicht. Flexible Vertex Format kurz: FVF. Darunter versteht man die Technik, mit der Vertexdaten an Direct3D übermittelt werden. Es handelt sich dabei um ein flexibles Format, bei dem auch angegeben werden kann, welche Werte zu einem Vertex gespeichert werden müssen. Force-Feedback Hierbei handelt es sich um eine Technik, mit der es möglich ist haptische Reize über die Eingabegeräte zu übermitteln. Dazu gehören vibrierende Gamepads ebenso wie Joysticks, bei denen das Drücken in eine Richtung eventuell schwerer geht als in eine andere. Frontbuffer Dieser Puffer liegt im Speicher der Grafikkarte und beinhaltet das aktuell auf dem Monitor angezeigte Bild. GLOSSAR 119 H HAL HAL steht für H ardware Abstraction Layer. Es handelt sich dabei um eine Schicht zwischen Software und Hardware. Durch sie muss die Software nicht wissen, welche Hardware vorhanden ist. Sie verwendet einfach die Funktionalität des HAL. I Indoor-Engine Eine Indoor-Engine ist ein Teil eines Programms, welches für die Berechnung der Grafik von geschlossenen Szenen verantwortlich ist. Es werden also Bilder berechnet, wie sie in geschlossenen Räumen, also Gebäuden, vorkommen. L Lokale Koordinaten sind Koordinaten eines Objektes (Punktes) vor der Transformation durch die Welt-Matrix. M Makro Makros sind vom Präprozessor verarbeitete Anweisungen. Sie werden mit #define definiert und üblicherweise ausschließlich mit Großbuchstaben gekennzeichnet. Mesh ist ein Netz aus (3D-)Koordinaten und der Angabe welche Punkte zu welchem Dreieck gehören. N Near Clipping Plane ist eine Ebene, die senkrecht vor der Kamera steht. Alle Objekte, die vor dieser Ebene sind, werden nicht mehr beachtet. Siehe auch Far Clipping Plane. O Outdoor-Engine Eine Outdoor-Engine ist ein Teil eines Programms, welches für die Berechnung der Grafik von offenen Szenen verantwortlich ist. Es werden also Bilder berechnet, wie sie draußen vorkommen. P Pool Unter einem Pool versteht man unter DirectX einen Speicherbereich, der entweder von DirectX selbst, oder der Anwendung verwaltet wird. In einem Pool werden Resourcen, wie zum Beispiel Texturen, oder 3D-Daten, gespeichert. 120 GLOSSAR Pool Schwimmbecken Primitive Primitive sind die Dinge, die Direct3D zeichnen kann. Es gibt dabei lediglich drei verschiedene Primitive: Punkte, Linien und Dreiecke. Projektionsmatrix Sind Daten eines 3D-Modells im Rechner vorhanden, so stellt sich die Frage, wie man diese Daten auf dem Monitor visualisiert. Da der Monitor nur eine 2D-Oberfläche besitzt, muss die dritte Dimension simuliert werden. Die Umrechnung zwischen den ,echten‘ 3D-Daten und dem 2D-Monitor übernimmt hierbei eine Matrix, die so genannte Projektionsmatrix. Sie muss zu Beginn an Direct3D übermittelt werden, damit dieses zeichnen kann. Es gibt verschiedene Arten wie man diese Matrix berechnen kann, auch DirectX bringt bereits einige Methoden dazu mit. R Render Target Hierbei handet es sich um den Bereich, auf den gezeichnet werden soll. Normalerweise ist dies der Backbuffer. Es ist aber auch möglich in andere Puffer oder auf Texturen zu zeichnen. Renderstate Direct3D kennt diverse Schalter, die die Ausgabe beeinflussen. So ist es zum Beispiel möglich, bestimme Funktionalitäten abzuschalten oder zu aktivieren. Manchmal kann man auch aus verschiedenen Modi wählen. Diese Schalter werden RenderState genannt. S Stencilbuffer ist ein Puffer der für Spezialeffekte genutzt werden kann. Hierzu zählen Schatten, Reflektionen und so weiter. Tritt immer zusammen mit dem Tiefenpuffer auf. T Texel Ein Texel ist ein Pixel auf einer Textur. Textur Eine Textur ist eine Art ,Tapete‘, die auf ein Objekte ,geklebt‘ wird. Dazu verwendet man Bitmaps, die dem Objekt ein wesentlich natürlicheres Aussehen verleihen. Transformationen Mit Matrizen ist es möglich Punkte zu transformieren, indem man Punkte selbst als Matrizen ansieht. Es gibt verschiedene Matrizen, die unterschiedliche Wirkungen haben. So gibt GLOSSAR 121 es Matrizen, die Punkte um einen anderen Punkt drehen, aber auch solche zum Verschieben oder Skalieren. transformierte Vertices haben alle Transformationen durchlaufen und liegen nun in Viewport-Koordinaten vor. V Vertex Ein Vertex ist ein Punkt im 3D-Raum, Vertices sind mehrere davon. View Frustum Der virtuelle dreidimensionale Bereich, der von der Kamera ,gesehen‘ werden kann. Hat im Allgemeinen die Form eines Pyramidenstumpfes. Der Bereich wird vorne von der Near Clipping Plane und hinten von der Far Clipping Plane begrenzt. View-Matrix ist die Matrix, die das Sichtfeld bestimmt. Sie beinhaltet auch die Kameraposition. Viewport enspricht dem Teil des Render Targets, auf den gezeichnet werden kann. Dies kann bei einer Vollbild-Applikation der ganze Bildschirm sein, aber auch nur ein Teil davon. Auch im FensterModus kann das ganze Fenster oder aber nur ein Teil dessen der Viewport sein. Der Viewport ist flach, das heißt, er ist zweidimensional. Beschrieben wird er durch die linke obere Ecke (in Bildschirm- oder Fenster-Koordinaten) und seiner Breite und Höhe (in Pixeln). W Welt-Matrix wird auch World-Matrix gennant. Sie entspricht der Matrix, die lokale Koordinaten in Weltkoordinaten umrechnet. Sie kann dadurch zum Plazieren von Objekten in einer 3D-Szene verwendet werden. Weltkoordinaten Hierbei handelt es sich um durch die Welt-Matrix transformierte lokale Koordinaten. Dadurch entstehen quasi absolute Koordinaten. . .x-Format Bei dem .x-Format handelt es sich um ein von Microsoft enwickeltes Format zum Speichern von 3D-Daten. Das .x-Format ist template-basierend, das heißt, es kann nach eigenen Wünschen erweitert werden. 122 GLOSSAR Listing-Vereichnis 2.1 2.2 2.3 2.4 2.5 2.6 7.1 7.2 7.3 7.4 8.1 8.2 8.3 8.4 8.5 8.6 8.7 8.8 8.9 9.1 9.2 9.3 9.4 9.5 12.1 12.2 12.3 12.4 12.5 12.6 Der Kopf der Funktion WinMain. . . . . . . . . . . . . . . . . 6 Minimale Belegung der Fensterklasse. . . . . . . . . . . . . . 7 Registrierung einer Fensterklasse . . . . . . . . . . . . . . . . 7 Erstellen eines Fensters . . . . . . . . . . . . . . . . . . . . . 8 Die Schleife zum Abholen und bearbeiten der Nachrichten. . . 9 Die Callback-Funktion . . . . . . . . . . . . . . . . . . . . . . 10 Erstellung des Direct3D-Hauptobjektes . . . . . . . . . . . . 44 Initialisierung von Direct3D mit Standardwerten . . . . . . . 45 Finden eines passenden Depth-Stenzil-Puffers . . . . . . . . . 46 Belegung der Struktur D3DPresentParameters mit gefunden Werten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49 Struktur der Vertexdaten für 2D . . . . . . . . . . . . . . . . 53 Beschreibung der Vertex-Daten für 2D . . . . . . . . . . . . . 53 Rendern des zweidimensionlen Dreiecks . . . . . . . . . . . . 54 Berechnung der Ebenen des View Frustums . . . . . . . . . . 59 Licht ausschalten . . . . . . . . . . . . . . . . . . . . . . . . . 61 Rendern eines dreidimensionalen Dreiecks . . . . . . . . . . . 61 Vertexstrukur von dreidimensionalen Punkten mit TexturKoordinaten . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63 Beschreibung der Vertexstruktur mit Textur-Koordinaten . . 63 Anlegen der Vertices . . . . . . . . . . . . . . . . . . . . . . . 64 Das Anlegen einer Matrix, die zum Drehen um die Y-Achse verwendet werden kann . . . . . . . . . . . . . . . . . . . . . 68 Erstellen einer Matrix mit Hilfe der D3DX-Bibliothek . . . . 68 Erstellen einer Transformations-Matrix aus mehreren Matrizen 69 Setzen der Weltmatrix . . . . . . . . . . . . . . . . . . . . . . 69 Deklaration einer Klasse für eine Kamera. . . . . . . . . . . . 71 Struktur zum Abspeichern eines BSP-Knotens . . . . . . . . . 96 Der Datentyp BSPTriangle . . . . . . . . . . . . . . . . . . . 97 Die generelle Vorgehensweise zum Erstellen eines BSP-Baumes 97 Erstellung eines Knotens in Pseudo-Code . . . . . . . . . . . 99 Das Finden der besten Teilungsebene in Pseudo-Code . . . . 101 Pseudo-Code zum Klassifizieren eines Dreiecks bezüglich einer Ebene . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103 123 124 LISTING-VEREICHNIS 12.7 12.8 13.1 13.2 Das Zerteilen eines Dreieck an einer Ebene im Der rekursive Anteil der Render-Funktion . . Erstellen des DirectInput-Hauptobjektes . . . Initialisieren des Headers des DIPROPDWORD’s . Pseudo-Code . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105 109 114 116 Abbildungsverzeichnis 2.1 Das Nachrichtenkonzept von Windows . . . . . . . . . . . . . 6 3.1 3.2 Einstellung der zu verwendenden DirectX-Version . . . . . . . Ein typisches COM-Objekt . . . . . . . . . . . . . . . . . . . 13 16 4.1 4.2 4.3 4.4 4.5 Darstellung von Vektoren . Addition von Vektoren . . . Multiplikation von Vektoren Das Skalarprodukt . . . . . Das Kreuzprodukt . . . . . . . . . . 24 25 26 27 27 5.1 5.2 5.3 Verschieben eines Punktes . . . . . . . . . . . . . . . . . . . . Rotation eines Vektors . . . . . . . . . . . . . . . . . . . . . . Verschiedene Ergebnisse bei Matrix-Multiplikationen . . . . . 31 33 34 6.1 ~ ist hier ebenso wie der Eine Ebene (Der Normalenvektor N Punkt P~ an einer beliebigen Stelle eingezeichnet.) . . . . . . . 36 7.1 7.2 Datenstruktur zur Enumeration . . . . . . . . . . . . . . . . . DirectX Caps Viewer . . . . . . . . . . . . . . . . . . . . . . . 44 48 8.1 8.2 8.3 8.4 8.5 8.6 8.7 Der Viewport . . . . . . . . . . . . . . . . . . . . . . Die verschiedenen Möglichkeiten Dreiecke anzugeben So könnte das Ergebnis in 2D aussehen. . . . . . . . Das View Frustum . . . . . . . . . . . . . . . . . . . Das Sichtfeld . . . . . . . . . . . . . . . . . . . . . . So könnte das Ergebnis in 3D aussehen. . . . . . . . So könnte das Ergebnis in 3D mit Textur aussehen. . . . . . . . . 52 56 57 58 60 62 65 9.1 Punkte im und gegen den Uhrzeigersinn . . . . . . . . . . . . 70 10.1 Eine Truhe gerendert mit DirectX . . . . . . . . . . . . . . . 78 11.1 Eine an den Koordinatenachsen ausgerichtete Box . . . . . . 11.2 Eine Bounding Box um ein Objekt . . . . . . . . . . . . . . . 84 85 . . . . . . . . . . . . . . . . mit Skalaren . . . . . . . . . . . . . . . . 125 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126 ABBILDUNGSVERZEICHNIS 11.3 Ergebnisse des Culling-Tests . . . . . . . . . . . . . . . . . . . 11.4 (Verschobene) Ebene mit Normalenvektor . . . . . . . . . . . 11.5 Ausrechnen der richtigen Eckpunkte . . . . . . . . . . . . . . 12.1 12.2 12.3 12.4 12.5 12.6 12.7 12.8 12.9 Konvex und Konkav . . . . . . . . . . . . . . . . . . . . . . . Das verwendete Beispiellevel mit zugehörigem Baum . . . . . Beispiellevel mit BSP-Baum nach der ersten Unterteilung . . Beispiellevel mit BSP-Baum nach dem zweiten Schritt . . . . Beispiellevel mit BSP-Baum nach dem dritten Schritt . . . . Teilung eines Dreiecks (einfach) . . . . . . . . . . . . . . . . . Teilung eines Dreiecks (schwer) . . . . . . . . . . . . . . . . . Grafische Darstellung der Berechnungen für den Schnittpunkt Zur Berechnung des Schnittpunktes . . . . . . . . . . . . . . . 86 87 88 90 91 92 93 94 104 106 107 107 Tabellenverzeichnis 2.2 Beschreibung der Parameter der Callback-Funktion . . . . . . 11 3.2 3.4 Die Komponenten von DirectX 9 . . . . . . . . . . . . . . . . Komponenten die in DirectX 9 nicht mehr aktuell sind. . . . 14 15 7.2 Parameter, die bei der Erstellung des D3D-Devices verwendet werden. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44 Zuordnung der Namen zu den Achsen . . . . . . . . . . . . . 71 9.2 127 128 TABELLENVERZEICHNIS Literaturverzeichnis [1] Meine Homepage: http://homepages.fh-giessen.de/~hg13419 [2] Microsoft DirectX downloads: http://www.microsoft.com/ downloads/search.aspx?displaylang=en&categoryid=2 [3] Microsoft Developer Network: http://msdn.microsoft.com [4] ZFX-Online: http://www.zfx.info 129