Artikel

Oktober 2004 | Artikel

DirectX-Crashkurs

(Link zum Artikel: http://www.it-republik.de/dotnet/artikel/0636)

Multimediaprogrammierung mit der DirectX-Schnittstelle, Teil 1

Text: von Jens Konerow
  • Teilen
  • kommentieren
  • empfehlen
  • Bookmark and Share
Früher galten Computer als reine Arbeitshilfen. Diesen Status haben sie zwar heutzutage keinesfalls abgelegt, doch längst hat sich ihr Einsatzgebiet auf die Unterhaltungsbranche ausgebreitet. Fast jeder vergnügt sich ab und zu mal bei einem Spielchen am Computer. Wollten Sie nicht auch schon immer mal Ihre eigenen Ideen verwirklichen? Dann stellt DirectX das ideale Fundament für Ihr Vorhaben dar. Die Multimediaschnittstelle aus dem Hause Microsoft bietet neben dreidimensionaler Grafikausgabe die Wiedergabe von Audiodaten - Zugriff auf Eingabegeräte wie Joysticks oder Lenkräder - und auch die nötigen Funktionalitäten zum Austausch von Daten über ein Netzwerk sind gegeben.

Dieser Artikel eröffnet den großen DirectX-Crashkurs (allerdings ohne Absturz), welcher sich über insgesamt sechs Folgen erstreckt (je nach Resonanz der Leser sollen weitere Beiträge zu spezielleren Themen folgen). Angefangen von der Ausgabe dreidimensionaler Welten bis hin zur Netzwerkprogrammierung lernen Sie sämtliche Grundlagen. Dieser erste Teil widmet sich zunächst ein wenig dem Konzept, welches hinter DirectX steckt und lehrt einige Grundlagen der linearen Algebra. Im zweiten Teil werden Sie Ihre erste kleine Direct3D-Anwendung schreiben.

Managed DirectX
Neben der klassischen DirectX-Variante, auf welche vorwiegend die C++-Programmierer zurückgreifen, existiert nun eine zweite Version namens Managed DirectX (MDX). Wie es der Name vielleicht vermuten lässt, gilt jene Version für Anwendungen mit verwaltetem Code. MDX ist der konsequente Weg Microsofts in Richtung .NET. Die Vorteile von Managed DX liegen auf der Hand: Sie können sämtliche Features des .NET Framework nutzen. Speicherverwaltung und mit Assemblies einhergehende Sicherheitseinstellungen seien als Beispiele genannt. Sie sollten DirectX möglichst immer nach dem .NET Framework installieren, da andernfalls die Dateien für MDX nicht im GAC platziert werden können. Wenn .NET doch erst nach DirectX installiert wurde, ist eine erneute Ausführung des DX-Setup mit dem Command Line Parameter /InstallManagedDX nötig.
Die Komponenten von DirectX
Die DirectX-Schnittstelle besteht aus mehreren Komponenten für die jeweiligen Aufgabenbereiche. Im Folgenden werden diese aufgelistet und deren Einsatzgebiet kurz beschrieben.
  • Direct3D: Diese Komponente dient der Ausgabe dreidimensionaler Grafiken. Geometriedaten durchlaufen mehrere Stufen, in denen sie ausgerichtet und beleuchtet werden.
  • DirectSound: Neben der zweidimensionalen Wiedergabe von Audiodaten bietet DirectSound die Möglichkeit zum Erzeugen einer dreidimensionalen Klangakustik. Außerdem lassen sich Sound-Effekte anwenden oder eingehende Streams aufnehmen und als Wave-Datei abspeichern.
  • DirectInput: Dass Sie über DirectInput Maus und Tastatur ansprechen können, ist nichts Besonderes. DirectInput bietet aber natürlich auch eine Unterstützung für Joysticks und Lenkräder, wobei deren Effekte (Force Feedback) genutzt werden können.
  • DirectPlay: Machen Sie Ihre Anwendung, unabhängig von irgendwelchen Protokollen, netzwerkfähig. DirectPlay ist auf Netzwerkspiele ausgerichtet. So werden Sessions und Spieler mit dementsprechenden Klassen direkt unterstützt. Auch Voice-Übertragungen sind kein Hindernis.
  • AudioVideoPlayback: Diese Komponente eignet sich zur Wiedergabe von Audio- und Videodaten. Erwähnenswert an dieser Stelle ist sicher die Möglichkeit zum Abspielen von MP3-Dateien.
  • DirectDraw: 2D-Grafiken wurden bis einschließlich DirectX 7.0 über DirectDraw auf den Monitor gebracht. Ab Version 8.0 gibt es DirectDraw nicht mehr als selbstständige Komponente, sondern Direct3D übernimmt nun dessen Aufgabe. Dennoch können .NET-Programmierer auf die alte DirectX 7.0 Bibliothek für DirectDraw über einen Wrapper zugreifen.
Vertices und Primitive
Hinter einer virtuellen 3D-Welt im Computer verbirgt sich ein Drahtgittermodell, aufgebaut aus unzähligen Dreiecken. Solche Dreiecke werden als Primitive oder auch als Polygone bezeichnet. Sie pictureen eine glatte Oberfläche und werden mit Bitmaps (Texturen) überzogen. Neben den Dreiecken zählen auch Linien oder eine Ansammlung von Punkten zu den Primitiven. Welche Primitive Direct3D genau zur Verfügung stellt, erfahren Sie im nächsten Teil dieser Serie. Beim Dreieck beispielsweise existieren jeweils drei Eckpunkte - Vertices (Abpictureung 1). Ein Vertex kann neben den Koordinaten auch Farbangaben oder Textur-Koordinaten enthalten. Zusammenfassend lässt sich sagen, das eine 3D-Welt aus unzähligen Vertices besteht, die zu so genannten Primitiven verbunden werden.
Linkshändiges kartesisches Koordinatensystem
Kartesische Koordinatensysteme sind weit verbreitet. Bereits im Mathematikunterricht beim Thema Analysis haben Sie vielleicht den Graph in ein kartesisches Koordinatensystem der Ebene eingezeichnet. In Direct3D wird ein linkshändiges kartesisches Koordinatensystem standardmäßig eingesetzt. Dabei verläuft die X-Achse horizontal mit der positiven Ausrichtung nach rechts. Die positive Y-Achse zeigt nach oben und die positive Z-Achse verläuft quasi in den Monitor hinein (Abpictureung 2).
Vektoren
Im Gegensatz zu einem Punkt gibt ein Vektor keine absolute Position an. Stattdessen beschreiben jene mathematischen Komponenten eine Verschiebung. Vektoren besitzen eine Länge, eine Richtung und einen Richtungssinn. In der Physik dienen Vektoren meist der Darstellung von Kräften.
In der unteren linken Ecke der Abpictureung 3 sehen Sie zwei Vektoren mit derselben Länge und Richtung, aber einen unterschiedlichen Richtungssinn. Fällt das Wort Richtung in Bezug auf die Vektoren, ist im Verlauf dieser Artikelserie der Richtungssinn inbegriffen. Um Positionen im Raum zu definieren, kommen sog. Ortsvektoren zum Einsatz. Jene Verschiebungen gehen vom Ursprung (Origo oder engl. origin) aus. Mithilfe des Satz des Pythagoras lässt sich die Länge eines Vektors ermitteln. Dies ist insofern von Bedeutung, da Sie somit den Abstand zweier Positionen errechnen können. Abpictureung 4 zeigt die Gleichung zur Berechnung der Länge.

Zu guter Letzt seien die Einheitsvektoren genannt. Jene Vektoren besitzen eine Länge von einer Einheit. Manchmal treffen Sie auch auf den Begriff normalisierter Vektor. Mit der Gleichung aus Abpictureung 5 wird der Einheitsvektor ermittelt. DirectX stellt im Namensraum Microsoft.DirectX den Datentyp Vector3 zur Verfügung. Neben den x-, y- und z-Koordinaten verfügt die Struktur über diverse Hilfreiche Methoden, welche in Tabelle 1 zusammengefasst sind.
Methode Beschreibung
Add() Addiert zwei Vektoren. Gibt einen 3D-Vektor zurück.
Cross() Errechnet das Kreuzprodukt zweier Vektoren. Gibt einen 3D-Vektor zurück, der senkrecht zu den beiden Spannvektoren ist.
Dot() Gibt das Skalarprodukt (Punktprodukt) als Single-Wert zurück.
Length() Liefert die Länge des Vektors als Single-Wert.
LenghtSq() Liefert das Quadrat der Länge eines Vektors als Single-Wert.
Multiply() Führt eine Skalar-Multiplikation durch. Das Ergebnis ist vom Typ Vector3.
Normalize() Normalisiert den gegebenen Vektor, sprich: Es wird ein Einheitsvektor erzeugt. Gibt einen 3D-Vektor zurück.
Subtract() Subtrahiert zwei Vektoren voneinander. Liefert ein Ergebnis vom Typ Vector3.
 
 
Matrizen
Hinter dem Begriff Matrix (Plural: Matrizen) verbirgt sich ein rasterartiger Aufbau von Skalaren. Folglich besitzt eine Matrix Spalten und Zeilen. Ein Vektor entspricht einem Array von Skalaren. Analog dazu entspricht eine Matrix einem Array von Vektoren. Matrizen dienen der Manipulation von Vektoren oder anderer Matrizen. Hinter diesem eventuell für Sie neuartigen Konstrukt steckt eine vereinfachte Schreibweise einer mathematischen Operation.

Die Anzahl der Zeilen und Spalten pictureet den Namen der Matrix und teilt somit dessen Größe mit. Die Matrix aus Abpictureung 6 ist eine 3x3-Matrix, da sie drei Zeilen und drei Spalten enthält. Der Index mij gibt das Element in der Matrix M an, dessen Zeilennummer i und dessen Spaltennummer j entspricht. Matrizen mit derselben Anzahl von Zeilen und Spalten werden als quadratische Matrizen bezeichnet.
Wie bereits erwähnt, dienen Matrizen der Manipulation von Vektoren, sprich sie führen Verschiebungen, Rotationen und Skalierungen aus. Im Fachjargon findet man zusammenfassend den Begriff Transformation. Soll ein Objekt mit beispielsweise 1.000 Vertices um 30 Einheiten entlang der x-Achse verschoben und anschließend um 30° um seine Y-Achse rotiert werden, können Sie beide Transformationen mit jeweils einer Matrix durchführen. Letztendlich bleibt es am Computer hängen, die 2.000 Transformationen durchzuführen. Fassen Sie stattdessen beide Transformationen in einer Matrix zusammen und führen dann die Transformation durch, haben Sie die Hälfte der Rechenzeit für andere Aufgaben eingespart. Doch wie fassen wir zwei Aktionen in einer Matrix zusammen? Ganz einfach: indem Sie beide Matrizen miteinander multiplizieren! Doch bei der Multiplikation zweier Matrizen gilt es einen Merksatz zu beachten:

Eine Multiplikation zweier Matrizen ist dann undefiniert, wenn die Anzahl der Spalten, der Matrix A, nicht mit der Anzahl der Zeilen, der Matrix B, übereinstimmt.
Jene Regel gilt auch bei Vektor-Matrix-Multiplikationen, denn ein Vektor ist nichts anderes als eine 1xn- oder nx1-Matrix. Abpictureung 7 schildert den Rechenvorgang schematisch.
Koordinaten-Typen
Wenn Sie mit Direct3D arbeiten, werden Ihnen unterschiedliche Typen von Koordinaten begegnen. Die Positionsangaben der Vertices liegen in Objektkoordinaten vor. Jene sind relativ zum Ursprung des Objekts. Nachdem die World-Transformation durchgeführt wurde, liegen die x-, y- und z-Komponenten der Vertices in Weltkoordinaten vor. Abschließend werden die Vektoren in Kamerakoordinaten transformiert. Danach ist die virtuelle Kamera immer in Richtung der positiven Z-Achse ausgerichtet, wobei sich die z-Komponente im Wertebereich [0, 1] befindet. x- und y-Werte liegen im Bereich von -1 bis 1.

Nun stellt sich die Frage: Warum dieser Aufwand? Würden die Positionen der Vertices alle in Weltkoordinaten definiert werden, müssten selbst identische Körper jeweils neue Koordinaten bekommen. Somit läge ein Objekt mehrmals im Speicher. Dank der Transformationen existiert eine Version des Objekts im Speicher, welches durch Verschieben, Rotieren und Skalieren in der 3D-Welt platziert und ausgerichtet werden kann.
Transformationen
Nun wissen Sie zwar, dass Matrizen zur Transformation von Vektoren dienen und wie Matrix-Multiplikationen durchgeführt werden. Doch wie transformiert eine Matrix nun einen Vektor? Entgegen allen Erwartungen verwendet Direct3D 4x4-Matrizen. Als Grund sei die Translation (Verschiebung) genannt, welche sich mit einer 3x3-Matrix nicht beschreiben ließe. Erinnern Sie sich noch an den Merksatz bei einer Matrix-Multiplikation? Demzufolge ist es nicht möglich, einen 3D-Vektor mit einer 4x4-Matrix zu transformieren. Intern sind deshalb auch 4D-Vektoren (Homogenous Vektoren) im Einsatz. Die vierte Komponente w enthält den Wert 1, um korrekte Transformationen durchführen zu können. In Abpictureung 8 sehen Sie den Aufbau einer Skalierungsmatrix.

Folgende mathematische Operation wird ausgeführt, wenn jene Matrix einen Vektor transformiert. Ausschlaggebend sind die S-Faktoren der Matrix. Zu Demonstrationszwecken wird lediglich die neue x-Komponente berechnet.
  1. x' = x * Sx + y * 0 + z * 0

Die Matrix-Klasse, welche im Namensraum Microsoft.DirectX deklariert ist, greift Ihnen mit der Scaling()-Methode unter die Arme:
  1. Dim mScaling As Matrix
  2. mScaling = Matrix.Scaling(2, 2, 2)

Wenn der Mittelpunkt eines Objekts nicht im Ursprung liegt, tritt neben der Größenänderung eine Verschiebung auf. Es sollte deshalb immer danach gestrebt werden, den Mittelpunkt im Origo zu halten. Unter dem Synonym Translation (lateinisch: translare) versteht man eine Verschiebung. Eine Matrix für solch eine Aktion muss wie in Abpictureung 9 aufgebaut sein.

Die T-Elemente der Matrix bestimmen die Verschiebung entlang einer jeweiligen Achse. Daraus ergibt sich die folgende Rechnung:
  1. x' = x * 1 + y * 0 + z * 0 + 1 * Tx
  2. x' = x + Tx

Übergeben Sie der Matrix.Translation()-Methode entweder drei Single-Werte oder eine Variable vom Typ Vector3, um eine Translationsmatrix zu erhalten:
  1. Dim mTranslation As Matrix
  2. mTranslation = Matrix.Translation(New Vector3(5, 0, -10))

Fehlt jetzt lediglich noch eine Matrix zum Rotieren des Objekts. Besser gesagt drei Matrizen, je eine für die Rotation um eine entsprechende Achse. Aufgebaut sind die Rotationsmatrizen wie in Abpictureung 10 zu sehen.

Wenn Sie die Gedanken nicht auf Anhieb nachvollziehen können, keine Sorge: DirectX gibt die folgenden Hilfestellungen:
  1. Matrix.RotationX(angle As Single)
  2. Matrix.RotationY(angle As Single)
  3. Matrix.RotationZ(angle As Single)

Übergeben Sie den Winkel im Bogenmaß. Entweder bedienen Sie sich einer Formel oder nutzen die DegreeToRadian()-Methode der Geometry-Class:
  1. Dim sAngle As Single
  2. sAngle = Geometry.DegreeToRadian(45)
Die Rendering-Pipeline
Direct3D bedient sich dem Pipelining-Prinzip zur Verarbeitung der Geometriedaten. Die Rohdaten gelangen sozusagen in die erste Stufe, in der sie bearbeitet und anschließend an die nächsthöhere Stufe weitergegeben werden. Nachdem die Geometriedaten die Direct3D-Pipeline vollständig durchlaufen haben, erhalten Sie eine fertig gerenderte Szene. Abpictureung 11 zeigt eine schematische Darstellung der Pipeline.
Tesselation
Zwar nimmt die Leistungsfähigkeit der Computer stetig zu, dennoch sind im 3D-Bereich viele Grenzen gesetzt. Rundungen werden durch eine Vielzahl von Dreiecken realisiert. Desto mehr Dreiecke verwendet werden, desto ansehnlicher sind die Ergebnisse. Zum einen erhöht sich der Zeitaufwand zum Rendern der vielen Dreiecke und zum anderen - was viel entscheidender ist - müssen Sie mehr Daten über den Bus schicken. Seit DirectX 8.0 besteht die Möglichkeit, so genannte Curved Surfaces bzw. High Order Surfaces darzustellen, indem ein Dreieck während der Laufzeit unterteilt wird. Moderne Grafikkarten führen diesen Prozess, der im Fachjargon Tesselation genannt wird, im Grafikchip durch. Daraus resultiert eine Reduzierung der Bus-Belastung. Natürlich bleibt der erhöhte Aufwand zum Rendern der Primitive nicht aus. Nach dem vierten Tag wissen Sie die Tesselation zu nutzen, um den Detailgrad eines 3D-Objekts zu erhöhen.
Transformation & Lighting
Direct3D stellt im Rahmen der so genannten Fixed-Function-Pipeline die Funktionalitäten zur Transformation und zur Beleuchtung der Primitive zur Verfügung. Diverse Methoden und Eigenschaften eröffnen dem Programmierer, wie die Vertex-Daten zu verarbeiten sind. Die Fixed-Function-Pipeline bietet keine Möglichkeit zur Erweiterung des gegebenen Funktionsumfangs. Als Alternative, nicht etwa als zusätzliche Option, gelten die Vertex Shader. Shader sind kleine Programme, die vom Grafikprozessor ausgeführt werden (insofern die Hardware Vertex Shader unterstützt). Wählt der Programmierer diesen Weg, kann oder besser muss er die Transformationen und die Beleuchtung der Vertices vornehmen. Ein klarer Vorteil zeichnet sich in der hohen Flexibilität ab. Vertex Shader zählen zu der Programmable Pipeline.
Clipping, Culling und Rasterization
Zwischen einer dreidimensionalen virtuellen Welt und deren Darstellung auf dem Monitor besteht ein Konflikt, denn die Koordinaten lassen sich nicht direkt übertragen. Wie bereits besprochen, durchläuft die Geometrie deshalb mehrere Transformationen, bis die Koordinaten nach der Projection-Transformation in 2D-Koordinaten vorliegen.

Natürlich ist nicht sichergestellt, dass sich alle in die Pipeline gejagten Primitive voll im sichtbaren Bereich befinden. Schließlich ist die Fläche zum Anzeigen der Szene begrenzt und wird durch den Viewport beschrieben. Als Viewport gilt in diesem Fall ein Rechteck, welches die Größe des Sichtbereichs absteckt. Primitive, die sich außerhalb des angegebenen Bereichs befinden, werden geclippt. Direct3D unterstützt außerdem von Haus aus das Back Face Culling. Jene Option sorgt im aktiven Zustand dafür, dass wahlweise die Primitive mit im oder gegen den Uhrzeigersinn angeordneten Vertices entfernt werden. Dadurch soll Direct3D verhindern, dass vom Betrachter abgewandte Primitive auf dem Monitor erscheinen. Wenn die Sichtbarkeit der Dreiecksrückseiten erwünschst ist, muss lediglich das Back Face Culling deaktiviert werden.

Nun können die Daten im Rasterisations-Prozess (engl. rasterization) in entsprechende Farbwerte übertragen werden, sprich Direct3D berechnet die Pixel an den jeweiligen Flächen und speichert die Werte in einem so genannten Surface. Ein Surface ist lediglich ein sequenziell aufgebauter Speicherbereich, vergleichbar mit einem Bitmap.
Multitexturing-Einheit vs. Pixel Shader
Die Einheit zur Transformation und Beleuchtung der Vertices ist nur ein Bestandteil der Fixed-Function-Pipeline gewesen. Jene Einheit zum Hinzufügen von Texturen pictureet den zweiten Bestandteil. Analog zur gesamten Architektur von Direct3D ist die Multitexturing-Einheit aufgebaut, d.h. sie besitzt ebenfalls mehrere Bearbeitungsstufen. Mehrere Bearbeitungsstufen sind deshalb von Belang, damit der Programmierer die Möglichkeit zur Kombination unterschiedlicher Texturen erhält. Wie Sie später kennen lernen werden, sind solche Bitmaps selbst zur Beleuchtung anderer Texturen zu gebrauchen.

Das Pendant zur Multitexturing-Einheit stellt der Pixel Shader dar. Wie beim Vertex Shader obliegt es dem Programmierer, die Funktionalitäten der Fixed-Function-Pipeline zu implementieren (insofern dies erwünscht bzw. erfordert ist). Vorteile sind wieder in der Flexibilität erkennbar. So ermöglichen Pixel-Shader die dynamische Beleuchtung während des Texturierungs-Prozess.
Tiefen- und Alpha-Test
Bevor der aktuelle Pixel auf das Ziel-Surface übertragen wird, muss dieser sich dem Tiefen- und dem Alpha-Test unterziehen. Letzterer bewirkt den Verfall solcher Pixel, deren Farbwert in den als transparent definierten Farbbereich fällt. Der Tiefentest verhindert, dass jene Pixel in das Surface geschrieben werden, die eigentlich durch andere verdeckt werden. Wir nehmen uns des Tiefenproblems später an und demonstrieren den Unterschied zwischen aktiviertem und deaktiviertem Tiefentest.
Fog Blending
In Abhängigkeit von der Distanz zwischen einem Objekt und der Position der Kamera oder allein durch die Stelle des Objekts auf der Z-Achse werden dessen Pixel zu guter Letzt mit dem Farbwert des Nebels kombiniert. Körper außerhalb eines definierten Bereichs sind bei aktiviertem Fog Blending gänzlich unsichtbar und werden vom Nebel überdeckt. In diesem Fall wird der Farbwert des Nebels in das Surface geschrieben.
Ausblick
Nachdem Sie in diesem Artikel lediglich theoretische Kenntnisse gesammelt haben, sollen Sie die folgenden Beschreibungen und Screenshots etwas auf den Geschmack bringen. Die gezeigten Beispiele werden nicht gleich alle in der nächsten Ausgabe behandelt.

Nebeleffekte besitzen im Groben zwei Aufgaben: Zum einen dient Nebel zum Erzeugen einer bestimmten Atmosphäre und zum anderen, um den Sichtbereich des Benutzers einzugrenzen. Abpictureung 11 demonstriert das entsprechende Beispielprojekt zum Thema.

Das Terrainbeispiel aus Abpictureung 12 bezieht die Höheninformationen des Geländes aus einer Graustufengrafik. Jene besitzt jeweils 8 Bit für jeden Pixel, sprich der Wertebereich reicht von 0 bis 255. Die einzelnen Farbwerte bestimmen die Höhe eines Vertices. Mit einem Faktor lassen sich die Höhenkontraste verringern bzw. erhöhen.


Anzeige

Kommentare

Gravatar Becker 10.01.2012
um 05:47 Uhr
Hallo,

vielen Dank für dieses sehr hilfreiche Tutorial.
Wo finde ich jedoch den 2ten Teil ?


Vielen Dank und schönen Gruß

Suerte
#zitieren
zurück zum Seitenanfang