Die Community zu .NET und Classic VB.
Menü

Ein Device erstellen

 von 

Übersicht 

Willkommen zum ersten Teil der Reihe "Managed DirectX 9 mit VB .NET", welche die Grundlagen der Programmierung von DirectX mit VB .NET erklären soll. Die Tutorials richten sich vor allem an Einsteiger in die Programmierung mit DirectX, über in grundlegendes Wissen über VB .NET sollten sie allerdings verfügen.

In diesem Tutorial geht es ersteinmal darum, was DirectX eigentlich genau ist und wie wir die an die Funktionen für dreidimensionale Grafiken rankommen.

DirectX aus der Vogelperspektive  

Zur Zeiten von DOS mussten Spieleprogrammierer meist direkt auf die Grafikkarte zugreifen um die Bilder auf den Schirm zu bekommen und auch mit Windows 3.1 änderte sich daran wenig. Der Vorteil dieser Methode war natürlich die hohe Geschwindigkeit, auch wenn es dann häufig Probleme gab, diese Programme auch auf Rechnern mit anderen Hardwarekonfigurationen zum Laufen zu bekommen. Mit Windows 95 bzw. NT änderte sich dies allerdings. Es war nicht mehr möglich direkt auf die Grafikkarte zuzugreifen. Auch die Verwendung des GDI, der Windows-API für Grafiken, war keine große Alternative, da die Aufrufe bevor sie zur Hardware gelangen mehrere Ebenen des Betriebssystems durchlaufen müssen. Das GDI ist zwar für einfache zweidimensionale Grafik gerade richtig, entspricht aber nicht den Ansprüchen die dreidimensionale Grafik stellt.

Aus diesem Grund veröffentlichte Microsoft kurz nach dem Erscheinen von Windows 95 DirectX. DirectX ist ein Hardware Abstraction Layer (HAL), also eine Schnittstelle zwischen den Programmierern und der Hardware. Der Vorteil liegt darin, dass die Klassen und Funktionen für die Kommunikation mit unserer Geforce 4 genau die selben sind, wie die für eine Radeon 9800 oder für sonstige Grafikkarten. DirectX kümmert sich um die Unterschiede zwischen den verschiedenen Karten.

DirectX ist eigentlich nur ein Überbegriff für eine Sammlung an Schnittstellen. In DirectX 9 gibt es Direct3D für dreidimensionale Grafik, DirectDraw, für Grafik in zwei Dimensionen, DirectSound für die Ausgabe von Musik und Tönen, DirectInput für die Überwachnung von Maus, Tastatur und Joysticks, DirectShow für den Umgang mit Multimedia-Dateien und DirectPlay, das für Netzwerkverbindungen aller Art zuständig ist. In dieser Tutorial-Reihe soll es um Direct3D gehen. Der grundsätzliche Ablauf eines Direct3D-Programms sieht eigentlich immer relativ gleich aus. Zuerst müssen wir die Hardware initialisieren. Danach können wir anfangen Einstellungen zu verändern, unsere 3D-Welt und Texturen laden.

Einstellungen am Anfang  

Eine häufige Frage ist immer, wie man denn überhaupt an die Klassen von Direct3D herankommen kann. Dies ist eine recht einfache Sache, es passieren aber meist schon bei der Installation Fehler. Wichtig ist, dass DirectX 9 unbedingt nach dem .NET-Framework installiert wird, da ansonsten die Dateien für Managed DirectX nicht kopiert werden.

Der Rest ist nun wirklich einfach: zuerst muss ein Verweis (im Visual Studio .NET im Menu "Projekt" -> "Verweis hinzufügen") auf die Assemblies Microsoft.DirectX.dll, Microsoft.DirectX.Direct3D.dll und Microsoft.DirectX.Direct3DX.dll angelegt werden. Anschließend müssen wir nur noch die beiden Namespaces Microsoft.DirectX und Microsoft.DirectX.Direct3D per Imports importieren:

Imports Microsoft.DirectX
Imports Microsoft.DirectX.Direct3D

Listing 1

Ein kleines Wort zum DirectX SDK: Das DirectX SDK ist vor allem wegen der Hilfe (die eine komplette Referenz zu DirectX enthält) und einigen Tools interessant. Allerdings wird sie für keines der hier angesprochenen Beispiele gebraucht. Gerade Benutzer von langsamen Internetverbindungen (Modem / ISDN u.ä.) sollten sich deshalb vorher fragen, ob sich der Download von 220 MB wirklich lohnt.

Enumeration  

Bevor wir jedoch damit anfangen Direct3D zu initialisieren, müssen wir uns zuerst einen recht wichtigen Teil Direct3Ds anschauen, dessen Existenz von manchen Programmierern hin und wieder vergessen wird: Enumeration. Enumeration bietet uns die Möglichkeit herauszubekommen, was unsere Hardware (also die Grafikkarte) unterstützt. Im Fall unseres kleinen Beispiels wollen wir vorher abfragen, ob das Programm in einem Fenster oder im Fullscreen-Modus laufen soll. Falls der Anwender den Fullscreen-Modus gewählt hat, wird auch noch die Auflösung abgefragt.


Abbildung 1: Das Enumerationsfenster des Programms

Der Code um die Liste mit den verschiedenen Auflösungen zu füllen siehts so aus:

Try
  Dim dispmode As DisplayMode
  Dim adapterinfo As AdapterInformation = Manager.Adapters(0)

  For Each dispmode In adapterinfo.SupportedDisplayModes
    If dispmode.Format = Format.R5G6B5 And dispmode.RefreshRate = 60 Then
      cmbResolution.Items.Add(dispmode.Width & " x " & _
                              dispmode.Height & " x " & _
                              16)
    ElseIf dispmode.Format =Format.X8R8G8B8 And dispmode.RefreshRate = 60 Then
      cmbResolution.Items.Add(dispmode.Width & " x " & _
                              dispmode.Height & " x " & _
                              32)
    End If
    If dispmode.Equals(adapterinfo.CurrentDisplayMode) Then _
      cmbResolution.SelectedIndex = cmbResolution.Items.Count - 1
  Next
Catch err As Direct3DXException
Catch
End Try

Listing 2

Die AdapterInformation-Klasse stellt einen Adapter da. Damit ist nichts anderes gemeint als unsere Grafikkarte. Wenn wir einfach 0 übergeben, erhalten wir den Standardadapter (wobei ich noch nicht viele Rechner mit mehreren Grafikkarten gesehen habe). In der SupportedDisplayModes-Auflistung befinden sich sämtliche Auflösungen die unsere Grafikkarte unterstützt. Vorerst interessiert uns dort nur die Auslösung und die Farbtiefen 16 und 32 Bit, obwohl Direct3D 9 dort einige recht interessante Dinge hinzufügt (z.B. 64 und 128 Bit Farbtiefen, die Fließkommazahlen verwenden).

Die Überprüfung ob die Wiederholrate 60 ist ist nur ein kleiner Trick, damit nicht sämtliche Auflösungen hundertmal in der Liste auftauchen. Der weitere Code dieser Form ist eher uninteressant, da er lediglich dazu dient die ausgewählten Werte nach außen hin zu "veröffentlichen"

Unsere Engine-Klasse  

Da mit .NET auch endlich Visual Basic von der Objektorientierung eingeholt worden ist, wollen wir auch unsere Grafik-Engine als Klasse anlegen. Das hat den Vorteil, dass wir diese Klasse benutzen können, ohne irgendwie mit DirectX in Berührung zu kommen. Die Klasse bekommt zwei Konstruktoren: einen, dem man nur die Form übergeben muss, auf die die Grafik gezeichnet werden soll und einen, der zusätzlich noch die Auflösung für den Fullscreen-Modus übergeben bekommt.

Hier zuerst einmal der Programmcode für den Konstruktor für den Fenster-Modus:

Public Sub New(ByVal target As Control)
  Dim pp As New PresentParameters()
  With pp
    .Windowed = True

    .SwapEffect = SwapEffect.Discard
    .BackBufferCount = 1
    .BackBufferFormat = Manager.Adapters(0).CurrentDisplayMode().Format
    .BackBufferWidth = target.Width
    .BackBufferHeight = target.Height

    p_target = target
  End With
  init = InitDevice(pp)
End Sub

Listing 3

Die PresentParameters-Klasse enthält alle Einstellungen, die wir brauchen um ein Device (engl. Gerät) zu erstellen. Doch was ist eigentlich ein "Device"? Das Device repräsentiert unsere Grafikkarte, d.h. wenn wir dem Device erzählen, dass etwas zeichnen soll, so kommunizieren wir letztendlich mit unserer Grafikkarte. Hier noch ein Wort zu englischen (Fach-)Wörtern: ich versuche so wenig wie möglich englische Ausdrücke zu verwenden. Dies macht aber nur bis zu einem bestimmten Punkt Sinn, da es zum einen für viele der Fachbegriffe keine passende deutsche Übersetzung gibt oder sie keiner verwendet...

Mit Windowed kann man festlegen, ob wir den Fullscreen-Modus benutzen wollen, oder ein Fenster vorziehen. Das Einzige worauf wir hier achten müssen ist der BackBuffer. Der Backbuffer verhindert, dass wir etwas von dem Zeichenprozess mitbekommen. Wenn sämtliche Aufrufe direkt auf den Bildschirm zeichnen würden, so könnten wir bei größeren Szenen sehen, wie ein Objekt nach dem anderen gerendert wird. Bei der Geschwindigkeit, bei der die Bilder neugezeichnet werden, wäre dies nicht allzu schlimm, würde sich aber trotzdem mit einem hässlichen Flimmern bemerkbar machen. Aus diesem Grund können wir einen Backbuffer einrichten, in dem zuerst im Hintergrund gerendert wird und welcher nach Abschluss auf den Monitor kopiert wird. Danach wird die Funktion aufgerufen die dann das Device erstellt. Da dieser Code aber sowohl für Fullscreen als auch Windowed-Modus gleich ist, brauchen wir diesen Teil nur einmal.

Der Fullscreenmodus wird vor allem in Spielen eingesetzt. Zum Einen bringt er einen deutlichen Geschwindigkeitsvorteil, da Direct3D einen großen Teil des Grafiksystems übernimmt und die komplette Grafikausgabe des Computers kontrolliert. Zum Anderen ist es auch besser für die Stimmung eines Spiels, wenn die Nahaufnahme eines Monsters nicht von einem bunten Rahmen begleitet wird. Hier der Code für den Fullscreen-Konstruktor:

Public Sub New(ByVal target As Control, _
    ByVal width As Integer, _
    ByVal height As Integer, _
    ByVal depth As Integer)
  Dim pp As New PresentParameters()
  With pp
    .BackBufferCount = 1
    .BackBufferWidth = width
    .BackBufferHeight = height
    Select Case depth
    Case 16
      .BackBufferFormat = Format.R5G6B5
    Case 32
      .BackBufferFormat = Format.X8R8G8B8
    End Select

    .SwapEffect = SwapEffect.Copy
    .PresentationInterval = PresentInterval.Immediate

    p_target = target
  End With
  Init = InitDevice(pp)
End Sub

Listing 4

Auch hier haben wir wieder einen Backbuffer, der diesmal allerdings so groß ist, wie die von uns gewünschte Auflösung. Auch die Farbtiefe hat hier eine Bedeutung (im Windowed-Mode wird die eingestellte übernommen). Direct3D bietet uns hier eine große Liste. Bis zu DirectX8 sahen alle Farbformate so aus:

<Farbe 1><Bitanzahl1><Farbe2><Bitanzahl2> ...

Wie zum Beispiel R5G6B5 oder X8R8G8B8, welche in den Beispielen verwendet werden. In diesen Formaten stehen die Buchstaben A, X, R, G, B für verschiedene Komponenten in einer Farbe: A (Alpha) für Transparent, X für unbenutzte Bits, R für Rot, G für Grün und B für Blau. DirectX fügt dazu noch vier neue Typen hinzu, die allesamt ein F am Ende besitzen. Dieses F zeigt an, dass die Teile der Farben als Fließkommazahlen gespeichert werden. Da sie diese Formate aber nur in den fortgeschrittensten Engines brauchen werden, kümmern wir uns hier nicht um das für und wieder dieser Formate.

Das Device erstellen  

Jetzt wo wir alle benötigten Einstellungen zusammenhaben, können wir anfangen unser Device erstellen. Dies ist schon der erste recht schwierige Punkt, an dem man verzweifeln kann, da selbst bei kleinen Fehlern eine Exception droht.

Dim D3DCaps As Caps = Manager.GetDeviceCaps(0, DeviceType.Hardware)
Dim CreateFlgs As CreateFlags

pp.AutoDepthStencilFormat = DepthFormat.D16
pp.EnableAutoDepthStencil = True

If D3DCaps.DeviceCaps.SupportsHardwareTransformAndLight() And _
    D3DCaps.DeviceCaps.SupportsPureDevice Then
  CreateFlgs = CreateFlags.HardwareVertexProcessing Or _
    CreateFlags.PureDevice
ElseIf D3DCaps.DeviceCaps.SupportsHardwareTransformAndLight() Then
  CreateFlgs = CreateFlags.HardwareVertexProcessing
Else
  CreateFlgs = CreateFlags.SoftwareVertexProcessing
End If
CreateFlgs = CreateFlgs Or CreateFlags.MultiThreaded

Listing 5

Bevor wir das Device erstellen können, müssen wir nocheinmal zwei Einstellungen setzen. Zuerst wird der Tiefenbuffer (Depth-Buffer) initialisiert. Die Erklärung, was genau dieser Tiefenbuffer machen, folgt im nächsten Tutorial.

Der Rest ist nur dazu da um auszulesen, wieviel Arbeit wir unserer Grafikkarte aufhalsen können. Manche, häufig ältere, Grafikkarten unterstützen kein Hardware Transform&Lighting, d.h. das Rendern unserer 3D-Welt wird komplett von unserer CPU übernommen. Dass dies natürlich nicht besonders verträglich für die Performance ist, sollte klar sein. Besser ist da schon die Einstellung HardwareProcessing, mit der Direct3D der Hardware das Maximum an Arbeit gibt, allerdings bei mangelnder Unterstützung nachhilft. Wenn zusätzlich noch PureDevice verwendet wird, so muss die Grafikkarte alles übernehmen, nicht unterstützte Techniken können in dem Fall nicht verwendet werden. Zusätzlich wird noch die Option MultiThreaded gesetzt, damit das Device-Objekt threadsicher ist.

Try
  p_D3DDevice = New Device(0, DeviceType.Hardware, p_target, CreateFlgs, pp)
Catch
  Return False
End Try

Listing 6

Hier haben wir (endlich) den Code um das Device zu erstellen. Die 0 im ersten Parameter gibt nur an, dass wir den Standardadapter verwenden wollen. p_target enthält das Control auf das wir rendern wollen.

Wie Sie jetzt eine einfache Grafik auf den Schirm bekommen lernen Sie im nächsten Tutorial.

Das Beispielprojekt zu diesem Tutorial herunterladen [9487 Bytes]

Ihre Meinung  

Falls Sie Fragen zu diesem Tutorial haben oder Ihre Erfahrung mit anderen Nutzern austauschen möchten, dann teilen Sie uns diese bitte in einem der unten vorhandenen Themen oder über einen neuen Beitrag mit. Hierzu können sie einfach einen Beitrag in einem zum Thema passenden Forum anlegen, welcher automatisch mit dieser Seite verknüpft wird.