www.codeworx.org/directx_tuts/DER VERTEXWAHN Eine Geschichte nach einer wahren Begebenheit

DER VERTEXWAHN Eine Geschichte nach einer wahren Begebenheit
Ein Tutorial von Julian Amann

„Ihr habt Kenny getötet. Ihr Schweine!“
Southpark


DirectGraphics: Eine Geschichte voller Missverständnisse...

Vorwort

Mit dem erscheinen von DirectGraphics sind in verschiedenen Foren und Newsgroups immer wieder die gleichen Fragen aufgetaucht. "Was ist mit DirectDraw passiert?", "Wie kann man Sprites mit DirectGraphics darstellen?", und so weiter... Da es dazu verschiedne Ansätze gibt und dieses Thema zu umfangreich ist um es in ein paar kurzen Sätzen beantworten zu können, habe ich mich entschlossen ein Tutorial dazu zu verfassen. Letztendlich wurde der Vorschlag für dieses Tutorial im Forum der C++ Ecke (www.c-plusplus.de/forum) gemacht, worauf ich mich an die Arbeit gemacht habe. Bevor es aber ans Eingemachte geht möchte ich erst noch auf die Vorraussetzungen hinweisen, die man benötigt um dieses Tutorial einigermaßen zu verstehen.

Vorausgesetzt sind Kenntnisse in C/C++ - keine Angst man braucht kein Guru zu sein um meinen Code interpretieren zu können - und Grundlagen in der WinAPI - man sollte in der Lage sein ein einfaches Fenster auf den Bildschirm zu bringen, da ich darauf nicht weiter eingehen werde.

Wenn jemand einen Fehler entdeckt oder Verbesserungsvorschläge hat, braucht dieser jemand mir nur eine E-Mail zu schicken. Auch bei Fragen einfach eine E-Mail an mich (JamesBlond008@gmx.de) oder noch besser einfach in einem Forum stellen, da es bei mir ein wenig dauern kann, bevor ihr eine Antwort von mir erhaltet.

Ich programmiere jetzt inzwischen seit einem guten Jahr mit DirectX, aber ich bin noch lange kein Profi. Deshalb entschuldige ich mich schon mal für alle Fehler in diesem Tutorial. Im nächsten Abschnitt werde ich einen kleinen Einblick in die DirectX Welt geben.

DirectX

DirectX ermöglicht einen schnellen Zugriff auf die Hardware auf Low-Level Ebene (= Hardwarenah). Der Geschwindigkeitsvorteil entsteht vor allem dadurch das bei DirectX nicht erst wie bei der Windows API unzählige Zwischenstufen zu andern APIs durchlaufen werden müssen, sondern so weit es geht die Hardware auf direkten Wege angesprochen wird. Im Grunde genommen ist DirectX ein Set von Low-Level-APIs, die für Spiele oder für Anwendungen, die eine möglichst hohe Performance benötigen, gedacht ist. Mit DirectX lässt sich viel anstellen. Man kann Grafikkarten, Soundkarten oder auch Eingabegeräte damit ansteuern und verwalten.

DirectX 8.0

Mit DirectX 8 hat sich vieles in DirectX geändert. Erschreckend für viele Programmierer war sicherlich das Fehlen von DirectDraw und DirectSound. Was ist mit diesen Komponenten passiert? Sind Außerirdische auf der Erde gelandet und haben diese Komponenten entführt? Hat sie Copperfield durch einen Zauberspruch verschwinden lassen? Oder war das nur eine Ente von Microsoft? Leider nicht. Kurz gesagt wurde DirectDraw und Direct3D zu DirectGraphics zusammengefasst, aus DirectSound und DirectMusic wurde DirectAudio und DirectShow (früher Bestandteil des Media SDK) gehört nun als neue Komponente zum DirectX Umfang.

Viele werden sich jetzt sicherlich fragen wie es sich jetzt mit der Aufwärtskompalität von DirectX verhält und was diese Änderungen nötig machte. Mit der Aufwärtskompalität bzw. Abwärtskompalität sieht es Momentan so aus: DirectDraw ist zwar weiterhin in der Laufzeitbibliothek von DirectX enthalten, wird aber nicht mehr weiterentwickelt und ist deshalb auch nicht mehr in der DXSDK Dokumentation zu finden. D. h. Programme, die DirectDraw benutzen, werden weiterhin auf jeden PC laufen, bis sich eines Tages MS dazu entschließt DirectDraw aus der DirectX Runtime zu nehmen. Aber warum? Warum kommt Microsoft nach 5 Jahren auf die Idee einfach die Aufwärtskompalität zu gefährden und diese Komponente durch eine neue zu ersetzen. Das weiß wohl nur Gott und Microsoft. Auf der offiziellen DirectX Entwickler Seite konnte man lesen, dass sich die Architektur so in dieser Form nicht mehr aufrecht erhalten ließ und man die API vereinfachen wollte und man sie daher kurzerhand geändert hat. Im Grunde genommen hat sich ja an DirectDraw seit DirectX 5.0 nichts großartiges mehr geändert.

So fragen sich jetzt viele Entwickler soll man nun umsteigen auf DirectX 8.0 oder vorerst weiter auf DirectX 7.0 aufbauen. Nun diese Frage ist gar nicht so leicht zu beantworten. Außer für OpenGL Anhänger. Aber dieses Tutorial heißt nun mal nicht "Umstieg von DirectX auf OpenGL". Außerdem hat DirectX weit mehr als nur eine Schnittstelle für die Graphische Ausgabe zu bieten. Nun aber zurück zur eigentlichen Frage. Obwohl es noch keine gescheiten Treiber gibt und bekanntere Software Firmen behaupten das in DirectX 8.0 schwerwiegende Fehler enthalten sind (was ich nicht bezweifle *g*), würde ich empfehlen auf DirectX 8.0 umzusteigen. Oder hat es noch Sinn sich in eine veraltete API einzulernen?

DirectX 8.0 hat sich entschieden in Hinsicht auf die 2D Grafik geändert. In DX 8 wird man keine Funktion mehr finden die Blit heißt. Auch die DirectDraw Clipper wird man vermissen und vor allem das Colorkeying. Doch wird das so bleiben? Wird DX OpenGL immer Verwandter und wie wird die Entwicklung weitergehen?

Ein Ausblick in die Zukunft von DX

Ich bin natürlich kein Hellseher oder Wahrsager, aber wie es aussieht wird die altbekannte Funktionalität wieder in DX 9 mit der vollständigen Verknüpfung von D3D mit DirectDraw zurückkehren. Wie genau diese Funktionen dann aussehen werden und welche Ähnlichkeiten es zu DD geben wird (Clipping, Sufaces, Colorkey) kann ich hier noch nicht sagen. Dabei wird auch die Zusammenführung von Windows 9x und Windows NT durchgeführt werden und DX 9 wahrscheinlich nur noch auf Windows ME und Windows 2000 laufen. Mehr kann ich dazu leider nicht sagen.

Natürlich gab es in DX 8 noch viele weitere Veränderungen gegenüber den ältern Versionen wie programmierbare Pixel- und VertexShader (Eingriff in die D3D Rendering Pipe). Diese Spielen aber in diesem Tutorial keine Rolle und so habe ich sie einfach Verschwiegen. Wer genaueres über die Veränderungen wissen will kann sich auf msdn.microsoft.com/directx genauer informieren.

Viele Wege führen nach Rom

Wie ein Sprichwort schon sagt "Viele Wege führen nach Rom". So verhält es sich auch bei DirectX und Sprites. Wie schon anfangs erwähnt gibt es mehrere Möglichkeiten mit DX 8 Sprites auf dem Bildschirm darzustellen.
a) Man bleibt bei DirectDraw, da DX aufwärtskompatibel ist
b) Unter Verwendung der Methoden SetBackBuffer und CopyRects
c) Durch die Implementierung einer eigenen Spriteengine, die auf D3D basiert
d) Durch die Benutzung des ID3DXSprite Interfaces

In diesem Tutorial werde ich auf die in b und c genanten Vorschläge eingehen und weiter ausführen.

(MS schlägt vor für 2D weiterhin DirectDraw7 zu verwenden.)

Voreinstellungen

Als erstes benötigt man das DiectX SDK. Dieses kann man unter folgender Adresse downloaden: http://www.microsoft.com/downloads/release.asp?ReleaseID=16927. Falls jemanden der download zu groß ist, kann man sich das SDK dort auch für etwa DM 25,- bestellen und zuschicken lassen.

Damit die hier beschriebenen Programme ohne Probleme laufen müssen erst ein paar Voreinstellungen getroffen werden. Bei jedem Projekt muss die Datei d3d8.lib in den Linker miteingebunden werden. Hier entstehen meistens schon die ersten Fehler, bei denen dann verwundert gefragt wird warum sich der Sourcecode nicht kompilieren lässt - also nicht vergessen diese Datei einzubinden. Wie man das macht liegt an der Entwicklungsumgebung. Ich werde es hier für Visual C++ beschreiben mit dem ich selber arbeite. Erstellen Sie ein neues Projekt. Wählen Sie im Menü Projekt->Einstellungen...->Linker. Fügen Sie die Datei d3d8.lib unter Objekt-/Bibliothek-Module ein.

Rahmenanwendung

Nun sollte man eine Rahmenanwendung erstellen in der man den hier beschriebenen Sourcecode einbauen und testen kann.

Ich will ihr aber nichts über die Windows API und das erstellen von Fenstern schreiben - ich bin einfach zu faul dafür. Bevor man sich also weiter mit DirectGraphics oder im generellen mit DirectX befasst sollte man erst ein paar Tutorials über WinAPI durcharbeiten.

Sprites unter Verwendung der Methoden SetBackBuffer und CopyRects

Ein paar DirectDraw Funktionen sind in DG ihn ähnlicher Form wieder zu finden. Es gibt immer noch Surfaces und Funktionen zum Blitten eines Surfaces in den Backbuffer (oder Frontbuffer - wenn man das will). In diesem Abschnitt werden wir ein Programm erstellen, das genau nach diesem Prinzip ablaufen wird. Wir verschaffen uns Zugriff auf den Backbuffer, werden ein Sprite in ein Surface laden und dieses Surface dann in den Backbuffer blitten und feststellen das die ganze Sache eine großen Hacken hat.

Flippen, Blitten und DoubleBuffering

Bevor es weiter gehen kann, müssen erst noch ein paar Grundlegende Begriffe geklärt werden.

Ein Surface kann man sich als Rechteck mir einer bestimmten Breite und Höhe vorstellen. Jede Einheit auf diesem Rechteck präsentiert ein Pixel mit einer bestimmten Farbwert, der vom verwendeten Farbmodell abhängt.

Blit ist die Abkürzung für "bit block transfer" und es bedeutet nichts anderes als Daten (gemeint sind Datenblöcke) von einen Speicherbereich in einen andren zu schieben (kopieren). Dies könnten z. B. Datenblöcke eines Surfaces sein, das in den Backbuffer geblittet wird oder der Inhalt das Backbuffer, der in das Primary Surface kopiert wird.

Die Grafikkarte besteht aus zwei oder mehreren Video Pages. Während auf einer Video Page das fertige Bild angezeigt wird, wird auf der andren Video Page das nächste Frame gezeichnet. Wenn das Frame fertig gezeichnet ist werden die beiden Pages miteinander vetauscht (Flippen) und das nächste Frame wird auf der anderen Video Page gezeichnet. In der Fachsprache spricht man vom PageFlipping. Häufig redet man auch vom sogenannten DoubleBuffering oder Backbuffering. Früher unterschied man zwischen PageFlipping und DoubleBuffering. Von DoubleBuffering sprach man wenn sich der Backbuffer im Arbeitsspeicher befand und vom PageFlipping wenn sich der Backbuffer im VRAM (Video RAM) befand. Inzwischen macht man bei diesen Begriffen keinen Unterschied mehr, da ein Backbuffer im VRAM nichts ungewöhnliches mehr ist. Angemerkt sei hier das man logischerweise nicht vom PageFlipping spricht wenn sich der Backbuffer im Arbeitsspeicher befindet.

Ich weiß das diese Erklärungen nicht besonders sind, aber ich hoffe sie helfen sich etwas unter den Begriffen vorzustellen zu können.

Das Eingemachte

Nach dem ganzen gelaber wollen wir nun zum praktischen Teil kommen.

Ich habe das Errorchecking hier aus Gründen der Übersichtlichkeit weggelassen. Im folgenden werde ich den Sourcecode Zeile für Zeile erklären.

Als erstes sehen wir uns die Headerdateien an, die benötigt werden.

// Direct3D Headerdateien
#include <d3d8.h> // Headerdatei fuer Direct3D
#include <d3dx8.h> // Eine in D3D integrierte Hilfsbibliothek

Beim kompilieren nicht vergessen d3d8.lib und d3dx8.lib in den Linker mit einzubinden. In der Headerdatei d3dx8 stecken Hilfsfunktionen die uns beim Laden eines Bitmaps in ein Surface helfen.

Nun legen wir einige mehr oder weniger nützliche Defines fest um später einfach Änderungen am Programmcode vornehmen zu können.

#define SCREEN_WIDTH 800
#define SCREEN_HEIGHT 600
#define SCREEN_BPP 16

Als nächstes definieren wir ein Makro, das uns Tastaturabfragen ermöglicht.

#define KEY_DOWN(vk_code) ((GetAsyncKeyState(vk_code) & 0x8000)    ? 1 : 0)

Nun legen wir einige globale Variablen fest.

LPDIRECT3D8 lpD3D = NULL;
LPDIRECT3DDEVICE8 lpD3DDevice = NULL;
D3DDISPLAYMODE d3ddm;
D3DPRESENT_PARAMETERS d3dpp;

lpD3D: Direct3D Hauptobjekt
lpD3DDevice: Device Objekt - ermöglicht den Zugriff auf die Rendering Pipe von D3D
d3ddm: Display Mode
d3dpp: Anzeige Eigenschafen (genauso könnte man sagen Pathfinding = Wegfindung... naja...)

Für genauere Informationen einfach mal in der DXSDK Docu nachschlagen.

LPDIRECT3DSURFACE8 lpBackbuffer = NULL;
LPDIRECT3DSURFACE8 lpSprite = NULL;

lpBackbuffer wird logischerweise als Backbuffer genutzt und lpSprite als Surface für ein Bild, das wir später aus einer Datei laden und dann auf den Backbuffer blitten.

POINT SpritePosition;
RECT rcSprite;

In einer POINT Struktur wird die Position des Sprites gespeichert und in einer RECT Struktur wird der Bereich festgelegt der auf den Backbuffer geblittet werden soll. Das sieht jetzt vielleicht etwas umständlich aus wird uns aber beim betracht der Parameter der CopyRect Funktion verständlicher. Vorher muss aber erst noch D3D initialisiert werden.

lpD3D = Direct3DCreate8(D3D_SDK_VERSION);

Das Direct3D Objekt wird erstellt, genauer gesagt wird uns durch diese Funktion ein Handle auf das IDirect3D8 COM Objekt zugewiesen, über den wir dann weitere Funktionen aufrufen können.

lpD3D->GetAdapterDisplayMode(D3DADAPTER_DEFAULT, &d3ddm);

Füllt die d3ddm Struktur, die uns u. a. Informationen über den Speicheraufbau der Grafikkarte liefert.

memset(&d3dpp, 0, sizeof(d3dpp));

Die d3dpp Struktur wird bereinigt - eine reine Vorsichtsmaßnahme - man weiß ja nie ;)

d3dpp.Windowed = FALSE;
d3dpp.SwapEffect = D3DSWAPEFFECT_FLIP;
d3dpp.BackBufferFormat = d3ddm.Format;
d3dpp.BackBufferWidth = 800;
d3dpp.BackBufferHeight = 600;
d3dpp.BackBufferCount = 1;
d3dpp.FullScreen_RefreshRateInHz = D3DPRESENT_RATE_DEFAULT;
d3dpp.FullScreen_PresentationInterval = D3DPRESENT_INTERVAL_ONE;
d3dpp.hDeviceWindow = main_window_handle;

Windowed: Wird auf FALSE gesetzt. Damit legen wir fest das unsere Anwendung im Vollbildmodus laufen soll
SwapEffect: Es soll geflipt werden
BackBufferFormat: Hier wird der Speicheraufbau der Grafikkarte angegeben (z. B. 5-5-5, 5-6-5). Da wir ja nicht wissen können welchen Speicheraufbau die Grafikkarte unseres Endbenutzers hat, nehmen wir den vorhin mit GetAdapterDisplayMode bestimmten Wert
BackBufferWidth: Breite des Backbuffers - normalerweise gleich SCREEN_WIDTH
BackBufferHeight: Höhe des Backbuffers - normalerweise gleich SCREEN_HEIGHT
BackBufferCount: Anzahl der verwendeten Backbuffer - wir wollen Doublebuffering deshalb setzen wird diesen Wert auf 1
FullScreen_RefreshRateInHz: Bei dieser Variablen könnte man denken das man hier die RefreshRate angeben könnte - dem ist aber nicht so. Hier muss ein fest definierter Wert folgen.
FullScreen_PresentationInterval: Gibt an wie, wann der Bildschirm refresht wird. Wir wollen das der Bildschirm sooft Aktuallisiert wird wie es geht (somit hängt die maximale Frameanzahl pro Sekunde von der Refreshrate des Monitors ab)
hDevicWindow: Window Handle des Fensters in dem sich alles abspielen soll


if(lpD3D->CheckDeviceType(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL,
d3ddm.Format, d3ddm.Format, FALSE)==D3D_OK)
{
   lpD3D->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, main_window_handle,
   D3DCREATE_SOFTWARE_VERTEXPROCESSING, &d3dpp, &lpD3DDevice;
}

else
{
   lpD3D->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_REF , main_window_handle,
   D3DCREATE_SOFTWARE_VERTEXPROCESSING, &d3dpp, &lpD3DDevice);
}

Als erstes wird geprüft ob die Grafikkarte Hardwarebeschleunigung unterstützt. Falls dies der Fall ist wird das Device mit Hardwarebeschleunigung erzeugt, ansonsten wird auf den Reference Rasterizer (Software) zurückgegriffen. Das Device verschafft uns dann Zugriff auf die Rendering Pipeline von D3D.

Auf den Reference Rasterizer wird immer dann Zugegriffen, wenn die Hardware ein bestimmtes Feature nicht unterstützt wie z. B. Alphablending. Der REF emuliert dann durch Softwarealgorithmen die benötigte Hardwarefunktion.

lpD3DDevice->SetRenderState(D3DRS_CULLMODE, D3DCULL_NONE);
lpD3DDevice->SetRenderState(D3DRS_LIGHTING, FALSE);
lpD3DDevice->SetRenderState(D3DRS_ZENABLE, FALSE);

Hier wird Culling, Lighting und Z-Buffering deaktiviert, da wir in unserem Fall darauf verzichten können. Diese sind erst bei dreidimensionalen Szenen von Bedeutung.

lpD3DDevice->GetBackBuffer(0, D3DBACKBUFFER_TYPE_MONO, &lpBackbuffer);

Mit GetBackBuffer verschaffen wir uns Zugriff auf den Backbuffer.

lpD3DDevice->CreateImageSurface(100, 100, d3ddm.Format, &lpSprite);
D3DXLoadSurfaceFromFile(lpSprite, NULL, NULL, "test.bmp", NULL,
D3DX_FILTER_POINT, 0, NULL);

Hier wird das Surface für unser Sprite erzeugt mit der entsprechenden Höhe und Breite und anschließend wird ein Bitmap in das Surface geladen.

if(KEY_DOWN(VK_ESCAPE))
PostMessage(main_window_handle, WM_DESTROY,0,0);

Falls die Escape Taste gedrückt wird, wird das Programm beendet.

lpD3DDevice->CopyRects(lpSprite, &rcSprite, 1, lpBackbuffer, &SpritePosition);

Blittet das Sprite in den Backbuffer.

lpD3DDevice->Present(NULL, NULL, NULL, NULL);

Zeigt das Bild an.

Und schon sind wir fertig!

Wie anfangs erwähnt gibt's dabei einen Hacken. Colorkeying wird nicht unterstützt. Somit wird diese Funktion für jedes kleinere 2D Spielchen praktisch nutzlos. Das einzige was man damit machen könnte wäre das Anzeigen von einem Hintergrundbild oder ähnlichem. Wie gesagt die 2D Funktionalität wird erst wieder mit DX 9 wieder vollständig integriert. Bis dahin könnte man weiterhin DirectDraw benutzen. Aber trotzdem stellt sich ein Frage: "If you've got 3D functionality, then why on earth would you want to use Blit anyway?"

(Allerdings könnte man mit obiger Funktion eine Kopie des Backbuffers in ein Surface erstellen und dieses Surface dann als Bild abspeichern - hilfreich wenn man Screenshots machen will.)

Implementierung einer eigenen Spriteengine

Nun Direct3D dürfte vielen ein Begriff sein. Wenn man dieses Tutorial (oder wie auch immer man es nennen will) ließt, sollte man wenigstes schon wissen das es sich dabei um einen Bestandteil von DirectX, einer einfachen 3D API handelt, die uns eine Menge Arbeit abnehmen kann. Erstens müssen wir uns um keine Treiber- und Grafikkartenspezifischen Probleme kümmern und zweitens bietet uns Direct3D schon einige Grundfunktionen für das Zeichnen und Darstellen von dreidimensionalen Szenen.

"If you can draw one pixel you can draw everything". Wie gesagt DirectX kann uns dabei eine Menge Arbeit abnehmen. D3D ist in der Lage verschiedene Primitve zu rendern. Nämlich Punkte, Linien und Dreiecke. Durch Dreiecke kann man nahezu jedes Objekt darstellen. Durch geschicktes Shading (Guardshading das Vertexweise ist) können auch Runde Formen und Objekte wie beispielsweiße Zylinder oder Kugeln dargestellt werden. Bevor wir aber zu solchen Themen wie Lighting, Shading oder Texturing kommen beschäftigen wir uns lieber noch kurz mit den Polygonen. Bei der Polygonalen Darstellung werden Objekte als Netz dargestellt. Gekrümmte Oberflächen müssen dabei angenähert werden. Da die Polygonale Repräsentation die Oberflächen von Objekten beschreibt, wird sie auch manchmal als boundary representation bezeichnet. Nachteile dieser Technik sind allerdings dass die Genauigkeit der Repräsentation von der verwendeten Anzahl der Polygonalen abhängt. Oh... kommen wir lieber wieder zurück in die einfache, flache Welt der Sprites...

Bevor es aber los gehen kann erst ein wenig Theorie. Wie wollen wir in einer 3D Umgebung ein 2D Sprite darstellen?

Wir nehmen zwei Dreiecke, mappen eine Textur (unser Sprite) auf die beiden Dreiecke und rendern es via Direct3D auf den Bildschirm.

Halt! So einfach geht es natürlich auch wieder nicht. Ein normales Sprite ist eigentlich immer rechteckig. In Direct3D gibt es aber nur Dreiecke. Das ist aber gar kein Problem den aus Dreiecken lässt sich ganz einfach ein Rechteck zusammenbauen.

Einfache Datenstrukturen in Direct3D

Hier sieht man eine Triangle List. Das kann man sich in etwa so vorstellen: Man füllt ein Array mit den Koordinaten der Eckpunkte eines Objektes. D3D fasst immer 3 nacheinanderfolgende Eckpunkte sinngemäß zu einem Dreieck zusammen und rendert es auf den Bildschirm. Hierbei würden wir für zwei Dreiecke 6 Vertices (zu deutsch etwa Eckpunkte) benötigen. Wenn wir unsere Figur etwas genauer betrachten stellen wir fest das die Dreiecke immer 2 Eckpunkte gemeinsam haben und wir mit 4 Vertices auskommen würden. Dazu gibt es eine andere Datenstruktur in D3D, nämlich den Triangle Strip.

Triangle Strip genauer betrachtet:

Der Vorteil beim Triangle Strip liegt auf der Hand. Man kann mehrere Dreiecke durch die Angabe von wenigeren Vertices beschreiben. Beim oberen Beispiel haben wir mit 8 Vertices 6 Dreiecke beschreiben. Hätten wir für die gleiche Aufgabe einen Triangle Strip benützt hätten wir 18 Vertices benötigt. Gesagt sei aber noch das die Dreiecke nicht immer so schön zusammenhängen und man deshalb auf andere Datenstrukturen zurückgreifen muss. In diesem Tutorial würde das aber jetzt zu weit reichen um es hier großartig auszuführen.

Textur Mapping

Um nun unser eigentliches Sprite auf dem Bildschim anzeigen zu können Betrachten wir es einfach als Textur. Unter Textur Mapping versteht man vereinfacht gesagt das "kleben" von Texturen auf Oberflächen. Das ganze wird gemacht damit die Textur richtig auf das angegebene Objekt angezeigt/gemapt wird.

Stellen wir uns vor wir wollen auf obiges Quadrat eine Textur mappen. Problem: der Computer weiß nicht wie er die Textur auf das Objekt packen soll. Wir wollen folgendes machen: Die linke obere Ecke der Textur soll auf A, die rechte obere Ecke der Textur soll auf B, die linke untere Ecke der Textur soll auf C und die rechte untere Ecke der Textur soll auf D. Eigentlich ja ganz logisch, aber man könnte genauso die Textur einfach Seitenverkehrt auf das Objekt mappen, wenn man das will. Nun kann man aber nicht zum Computer sagen schau her die Textur will ich so und so haben sondern man muss das dem PC irgendwie anders mitteilen. Dazu verwendet man sogenannte Textur-Koordinaten. Dazu wird ein zweidimensionales Koordinatensystem verwendet. Die Achsen in diesem System werden mit den Buchstaben u und v bezeichnet.

Der Ursprung/Nullpunkt (0,0) dieses Koordinatensystems liegt in der linken oberen Ecke. Die rechte obere Ecke der Textur hat die Koordinaten (1,0), die linke untere Ecke der Textur hat die Koordinaten (0,1) und die rechte untere Ecke hat die Koordinaten (1,1).

Somit müsste man die Textur so auf das Quadrat mappen:
A: (0,0)
B: (1,0)
C: (0,1)
D: (1,1)

Transparents

Bei DirectDraw verwendete man für Transparents einen ColorKey. Das Prinzip dahinter war eigentlich ganz einfach: Es wurden alle Pixel mit einer bestimmten Farbe nicht auf den Bildschirm gerendert. In D3D gibt es keinen Colorkey, dafür aber das Alphablending. Mit Alphablending kann man Objekte auch halbdurchscheinend darstellen. Um aber auf das Ergebnis eines Colorkeys zu kommen, werden wir ähnlich wie beim Colorkey einen Farbwert als Transparente Farbe definieren und mit Hilfe von Alphablending Transparents erzeugen.

Matrizen

Im Zusammenhang mit D3D werden wir häufiger dem Begriff "Matizen" oder "Matrix" begegnen. Man versteht unter einer Matrix eine mathematische Schreibweiße. Im allgemeinen bezeichnet man ein Koeffizientenschema der Form

als Matrix.

Durch solche Matrizen lassen sich auf einfache Weise Verschiebungen, Streckungen oder Spiegelungen beschreiben oder eben auch Translationen, Skalierungen, Rotationen und Transformationen, die letztendlich aus Zahlen komplizierte 3D Gebilde formen.

Um es kurz zu umreisen werden in einer normalen 3D Engine erst alle Objekte eingelesen, die Kameraposition definiert, die Transformation vom lokalen Koordinatensystem ins Welt-Koordinatensystem definiert, die Szene transformiert und anschließend die Szene gerendert.

Dazu erstellt man einfach verschiedene Matrizen. Z. B. eine Translationsmatrix, eine Rotationsmatrix (eine für jede Achse), eine Skalierungsmatrix usw. Multipliziert man alle diese Matrizen miteinander so erhält man eine Gesamtmatrix mit allen Transformationen - das aktuelle Abbild unserer 3D Welt.

Da dieses Tutorial auch nicht "Mach dir deine eigene Wireframe Engine" oder "Bau dir deinen eignen Renderer" heißt werde ich hierauf nicht weiter eingehen. Das würde wirklich den Rahmen dieses Tutorials sprengen (Das ist diesmal keine Ausrede dafür das ich einfach zu faul bin es zu erklären ;).

Da wir uns mit einfachen Sprites abgeben, müssen wir uns nicht intensiver mit diesem Thema auseinandersetzen. In D3D gibt es eine integrierte Bibliothek namens D3DX, die Funktionen für bestimmte Matrizenberechnungen zur Verfügung stellt.

In D3D begegnet man im groben folgenden Matrizen:

World Matrix: Unser vituelles Universium
Projection Matrix: Führt die 3D Objekte in eine 2D Projektion auf unserem Bildschirm über
View Matrix: Stellt unsere Kamera dar mit der wir uns in unserer virtuellen Welt bewegen können

Wir werden jedoch nur die Projektionsmatrix kennen lernen.

Das Flexible Vertex Format (FVF) und VertexBuffer

Die Idee hinter den VertexBuffern wurde oben schon einmal vereinfacht beschreiben: Im Grunde genommen ist ein VertexBuffer ein Array mit den Koordinaten der Eckpunkte eines Objektes. Man kann durch bestimmte Befehle D3D dazu veranlassen drei hintereinander folgende Koordinaten als ein Dreieck aufzufassen, das dann auf den Bildschirm gerendert wird. Das einzige Problem dabei ist, dass wir D3D erst noch sagen müssen wie die Koordinaten in unserm VertexBuffer abgespeichert sind.

Zu diesem Zweck wurde das sogenannte Flexible Vertex Format (kurz FVF) entwickelt. Damit kann man für jeden VertexBuffer ein spezielles Format erstellen nach dem die einzelnen Koordinaten abgespeichert werden.

So kann man z. B. neben den eigentlichen Koordinaten auch noch die u, v Koordinaten für das Texturmapping abspeichern. Ein anderer Vorteil liegt darin das Elemente der Vertex Struktur, die nicht benutzt bzw. benötigt werden somit gleich ausgeschlossen sind und nicht unnötig Speicher verbrauchen oder unsere Anwendung ausbremsen.

Transformed und Lit Vertices

Direct3D kann unsere Objekte transformieren und beleuchten. In manchen Fällen kann diese Funktionalität aber unerwünscht sein wenn wir z. B. die Beleuchtung selbst übernehmen wollen oder schon transformierte Koordinaten vorliegen haben da wir unsere Objekte durch eine eigene Transformpipeline gejagt haben. Dafür gibt es u. a. auch das FVF.

Der Praktische Teil

Die Implementierung einer Spriteengine kann sehr unterschiedlich aussehen und folgende ist nur eine von vielen Möglichkeiten. "Viele Wege führen nach..."

Im folgenden wird nur der neue Programmcode erklärt.

#define D3DFVF_VERTEX2D (D3DFVF_XYZ|D3DFVF_TEX1)

Oben wurde ein FVF definiert mit den entsprechenden FVF-Codes. D3DFVF_XYZ (Position der Vertices) und D3DFVF_TEX1 (weißt darauf hin das nach den Koordinaten des Objektes die Texturkoordinaten folgen).

struct VERTEX2D
{
   float x, y, z;
   float u, v;
};

Die Vertexstruktur - nicht mehr und nicht weniger... Eigentlich dürfte klar sein das x, y, z die Position des Sprites darstellen und u und v die Texturkoordinaten sind.

D3DCAPS8 d3dcaps;

d3dcaps: Caps - welche Möglichkeiten bietet uns die Hardware?

LPDIRECT3DTEXTURE8 lpSprite = NULL;
LPDIRECT3DVERTEXBUFFER8 lpVBSprite = NULL;

lpSprite wird als Buffer für das zu ladene Bild verwendet und lpVBSprite als VertexBuffer für die Vertices des Sprites.

VERTEX2D sprite_vertices[] =
{
   {-SPRITE_WIDTH/2,SPRITE_HEIGHT/2,0.0f,0.0f,0.0f,},
   {-SPRITE_WIDTH/2,-SPRITE_HEIGHT/2,0.0f,0.0f,SPRITE_HEIGHT/256,},
   {SPRITE_WIDTH/2,SPRITE_HEIGHT/2,0.0f,SPRITE_WIDTH/256,0.0f,},
   {SPRITE_WIDTH/2,-SPRITE_HEIGHT/2,0.0f,SPRITE_WIDTH/256,SPRITE_HEIGHT/256,},
};

Hier werden die Vertices der Sprites definiert.

#define SPRITE_WIDTH 69.0f
#define SPRITE_HEIGHT 110.0f

Sprite Breite und Höhe.

D3DXMATRIX matProj;

Hier wird die Projektionsmatrix definiert.

lpD3DDevice->SetRenderState(D3DRS_ALPHABLENDENABLE, TRUE);
lpD3DDevice->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);
lpD3DDevice->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);
if(d3dcaps.AlphaCmpCaps & D3DPCMPCAPS_GREATER)
{
   lpD3DDevice->SetRenderState(D3DRS_ALPHAFUNC, D3DCMP_GREATER);
   lpD3DDevice->SetRenderState(D3DRS_ALPHAREF, 0x00000000);
   lpD3DDevice->SetRenderState(D3DRS_ALPHATESTENABLE, TRUE);
}

Mit dem ersten Funktionsaufruf wird das Alphablending aktviert. Danach wird der RenderState gesetzt über den D3D mitgeteilt wird wie das Alphablending angewendet werden soll. In unserem Beispiel Missbrauchen wir es als eine Art Colorkey.

D3DXMatrixOrthoLH(&matProj, 800, 600, 0, 1);
lpD3DDevice->SetTransform(D3DTS_PROJECTION, &matProj);

Die Projektionsmatrix wird gesetzt.

Mit dem ersten Funktionsaufruf wird das Alphablending aktviert. Danach wird der RenderState gesetzt über den D3D mitgeteilt wird wie das Alphablending angewendet werden soll. In unserem Beispiel Missbrauchen wir es als eine Art Colorkey.

D3DXCreateTextureFromFileExA(lpD3DDevice, "joe256.bmp", D3DX_DEFAULT,
D3DX_DEFAULT, D3DX_DEFAULT, 0, D3DFMT_UNKNOWN, D3DPOOL_MANAGED, D3DX_DEFAULT,
D3DX_DEFAULT, D3DCOLOR_XRGB(255,0,255), NULL, NULL, &lpSprite);

Wahnsinn... wie viele Parameter hat die Funktion noch? ;)... Eigentlich sind für uns hier nur drei Parameter wichtig. Zu einem der zweite in dem der Dateiname der zu ladenen Textur angegeben wird, der letzte indem der Texturbuffer angeben wird, wo wir die Datei zwischenlagern wollen und der "Colorkey" (11ter Parameter), der mithilfe des Macros D3DCOLOR_XRGB definiert wurde. (Für genauere Informationen wie immer im DXSDK nachschlagen)

VOID *lpVertices;
lpD3DDevice->CreateVertexBuffer(sizeof(sprite_vertices), 0, D3DFVF_VERTEX2D,
D3DPOOL_DEFAULT, &lpVBSprite);
lpVBSprite->Lock(0, sizeof(sprite_vertices), (BYTE**)&lpVertices, 0);
memcpy(lpVertices, sprite_vertices, sizeof(sprite_vertices));
lpVBSprite->Unlock();

Füllt den VertexBuffern mit den Vertices unseres Sprites. VertexBuffer sind im allgemeinen schneller ansprechbar, da sie direkt im VRAM gespeichert werden.

Damit wurde die Initialisierung abgeschlossen. Kommen wir nun zum Rendern.

lpD3DDevice->Clear( 0, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB(0,0,0), 1.0f,    0);
lpD3DDevice->BeginScene();

Mit dem Aufruf von Clear wird der Backbuffer gelöscht und mit der angegebenen Farbe gefüllt (hier schwarz). BeginScene startet danach das Rendering unserer Szene.

lpD3DDevice->SetTexture(0, lpSprite);
lpD3DDevice->SetTextureStageState(0, D3DTSS_COLOROP, D3DTA_TEXTURE);
lpD3DDevice->SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTA_TEXTURE);

Und schon sind wir beim Rendern angelangt. Mit SetTexture bringen wir D3D auf die folgenden Objekte die ausgewählte Textur zu mappen. Mit beiden Aufrufen von SetTextureStageState wird das Alphablending für die ausgewählte Textur aktiviert.

lpD3DDevice->SetVertexShader(D3DFVF_VERTEX2D);
lpD3DDevice->SetStreamSource(0, lpVBSprite, sizeof(VERTEX2D));
lpD3DDevice->DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2);

Mit SetVertexShader wird D3D schnell noch mitgeteilt wie wir unser Vertieces gespeichert haben und schließlich wird das Objekt durch DrawPrimitive auf den Bildschirm gerendert. SetStreamSource vermittelt D3D die zu renderen Vertices.

lpD3DDevice->EndScene();
lpD3DDevice->Present( NULL, NULL, NULL, NULL );

Mit EndScene wir das Rendern unserer Szene beendet. Present zeigt dann unsere fertig gerenderte Szene an.

Zugegeben dieses Spritebeispiel ist sehr primitiv, aber ich hoffe ich konnte mit diesem Tutorial dazu beitragen mehr Licht ins Dunkle zu bringen.

The Never Ending Story...

So und schon sind wir am Ende dieses Tutorials angelangt. Obiges Beispielprogramm könnte man erweitern zu einer Spriteengine. Man könnte verschiedene Sprites Rotieren lassen und sich noch ein wenig am Alphablending austoben - Also noch viel Spaß beim Testen.

Download Demoprojekte