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

Documentos relacionados