Texturierte Objekte in Manged DirectX - Teil 1
Autor/Einsender:   Sascha Bajus
Anregungen/Tipps an: Sascha Bajus
Willkommen zum ersten Teil des Tutorials über texturierte Objekte in MDX. Alles was benötigt wird, ist VB Express 2008 oder Visual Studio 2008, das DirectX Redistributable 9.0c August 2008 von Microsoft und viel Durchhaltevermögen.
Das Tutorial richtet sich an leicht Fortgeschrittene in der DirectX- Programmierung und ist in mehrere Teile gefasst, die jeweils den aktuellen Source-Code enthalten. Es soll den Einstieg vermitteln, wie Billboards erstellt, komplexe Objekte importiert und wie Textureffekte mit Renderstates und der Fixed-Function-Pipe erstellt werden.
Grundüberlegung
Das Grundgerüst
Device Erstellung
Die PresentParameters
Der Renderloop per PaintEvent
Der Rendervorgang
Optimierungen
Beispielprojekt
Auf die Erstellung des grundlegenden Gerüstes, welches man zum Rendern von 3D- Objekten benötigt, wird zwar nicht verzichtet, aber es wird auch nicht im Detail erklärt.
Grundlegendes Wissen in VB.Net ist Pflicht, erste gemachte Schritte in DirectX wären sehr hilfreich. Um Teile des erstellten Code später weiter nutzen zu können, werde ich die komplexeren und erweiterbaren Teile in Klassen unterbringen. Dadurch wird der Code etwas komplexer, aber auch besser nutzbar. Um alles lesbarer zu halten wird kein Errorhandling und keine Enumeration implementiert.
Wer noch keine Erfahrungen in DirectX besitzt sollte sich zum besseren Verständnis mit folgenden Themen befassen:
•  Rendern von Primitiven
•  Grundlagen zu Texturen
•  Grundlagen des DirectX Lightning
1. Grundüberlegung
Was wollen wir alles in die Mini-Engine einbauen und nutzen können?
•  Tastatureingaben verarbeiten
  Eine FPS- Anzeige, um zu sehen wie schnell unsere Hardware rendert
 Verschiedene Grafikauflösungen nutzen
  Natürlich eine Szene mit 3D Objekten rendern
  Und vielleicht ein paar Effekte wie Beleuchtung und Texturen
Wie soll das Programm ablaufen?
•  Startbildschirm anzeigen
•  Starten der Szene
•  Beenden der Szene über Tastatur
Was soll die Szene zeigen?
•  einen Mond
•  einen Planeten
•  Sterne
•  Sonne
2. Das Grundgerüst
Wenn wir bei DirectX von Device sprechen, meinen wir immer einen Grafik-Device. Das ist im Grunde die eingebaute Grafikkarte die das Rendern der Szene erledigt und auf dem Monitor zur Anzeige bringt. Nun muss man diesen Device erst mal in DirectX erstellen. Damit wird im Grunde eine Verbindung zwischen DirectX und der Grafikkarte über den Treiber vorgenommen.
Bei der Erstellung werden verschiedene Parameter übergeben, die festlegen welche Eigenschaften der Device haben soll, z.B. die Auflösung des Bildes. Manche Eigenschaften können aber nicht realisiert werden, da die Hardware dies nicht unterstützt. Deswegen besteht die Möglichkeit den Device auszulesen. Diesen Vorgang nennt man Enumeration. Dieser Vorgang stellt z.B. fest, welche Auflösungen oder welche Hardware-Effekte unterstützt werden.
Mit den Informationen muss die Engine angepasst werden, damit beim Rendern keine Grafikfehler auftreten oder der Monitor einfach schwarz bleibt. Die Enumeration werden wir uns aber aus Übersichtsgründen sparen. In dem Beispiel werden wir Einstellungen verwenden, die in der Regel von jeder Hardware unterstützt werden. Weiterhin können unbekannte Fehler auftreten. Das Errorhandling werden wir aber ebenfalls streichen um den Code lesbar zuhalten. Die Bildschirmauflösungen werden wir fest vorgeben.
An dem Device werden sämtliche Objekte gebunden, also alles was gerendert werden soll muss der Device kennen. Zudem muss er wissen wo und wie er rendern soll.
Zunächst einmal das Wo.
Das Device benötigt ein Ziel auf dem gerendert wird. Das Ziel wird über ein Handle festgelegt. Das Handle wird von Windows beim Erstellen eines Controls vergeben. Somit kann man im Prinzip auf allem Rendern was ein Handle besitzt. Da würde sich zum Beispiel ein Panel oder einer PictureBox anbieten. Da wir aber auch den Vollbildmodus nutzen wollen entscheiden wir uns für eine Form. Unser RenderTarget wird also eine Form.
Jetzt wird es Zeit das Projekt zu erstellen. Wir brauchen also:
•  eine Form für den Startbildschirm
•  eine Form zum Rendern
•  weitere Klassen für unsere Engine
Im Projekt müssen noch Verweise auf die Assemblies Microsoft.DirectX.dll, Microsoft.DirectX.Direct3D.dll und Microsoft.DirectX.Direct3DX.dll angelegt werden, um diese nutzen zu können. In allen Klassen wo DirectX verwendet wird, sollte der Namespace direkt Import werden:
 
Imports Microsoft.DirectX
Imports Microsoft.DirectX.Direct3D
 
Folgende Verweise sollten vorhanden sein:
Das Projekt sollte so aussehen:
Die Projektmappe enthält also folgende Elemente:
•  die Startform frmStartup
•  die Klasse für die Engine (Form) clsEngine
Dann wollen wir das mal mit Leben füllen und springen in clsEngine…
3. Device Erstellung
Jetzt geht es weiter mit der Erstellung des Devices, das in unserem Fall von der clsEngine-Klasse übernommen wird.
Was soll die Szene zeigen?
•  einen Mond
•  einen Planeten
•  Sterne
•  Sonne
 
Imports Microsoft.DirectX
Imports Microsoft.DirectX.Direct3D

Public Class clsEngine
   ' Parameter für den Device
  Private _dxsettings As New PresentParameters
   ' unser  GraficDevice
  Private _dxdevice As Device

  Public Enum Displaymodes As Short
     ' die Auflösungen die wir unterstützen möchten
    mode_Windowed = 0
    mode_640x800 = 1
    mode_800x600 = 2
    mode_1024x768 = 3
    mode_1280x1024 = 4
  End Enum

End Class
 
Wir legen die Variablen die wir benötigen fest.
Für die Auswahl der Auflösung, die wir auf unserer Startform setzen, erstellen wir ein Enum. Der Displaymode wird ausgewertet und die Funktion zur Device-Erstellung initDevice aufgerufen:
 
Public Sub New(ByVal displaymode As Displaymodes)
   '#Device erstellen
  Select Case CShort(displaymode)
    Case 0
      _dxdevice = initDevice(Me.Handle, 0, 0, 32, True)
    Case 1
      _dxdevice = _
          initDevice(Me.Handle, 640, 480, 32, False)
    Case 2
      _dxdevice =
           initDevice(Me.Handle, 800, 600, 32, False)
    Case 3
      _dxdevice =
          initDevice(Me.Handle, 1024, 768, 32, False)
    Case 4
      _dxdevice =
          initDevice(Me.Handle, 1280, 1024, 32, False)
  End Select
End Sub
 
Da die Engine-Klasse auch das Renderziel ist, übergeben wir mit Me.Handle, das eigene Handle
Mit _dxsettings werden die Parameter des Devices festgelegt. Danach wird das Device erstellt und zurückgegeben.
 
Private Function initDevice(ByVal wHandle As IntPtr, ByVal width _
      As Integer, ByVal height As Integer, ByVal bpp As Integer, _
      ByVal windowed As Boolean) As Device

   ' Default-Einstellungen
  With _dxsettings
    .SwapEffect = SwapEffect.Discard       ' Backbufferverhalten
    .BackBufferCount = 1                    ' Backbuffer verwenden
    .BackBufferWidth = width                ' Backbuffersize
    .BackBufferHeight = height
    .PresentationInterval = PresentInterval.Immediate
    .AutoDepthStencilFormat = DepthFormat.D16
    .EnableAutoDepthStencil = True          ' Z-Buffer benötigt

    ' Fenster oder Vollbild
    If windowed Then
      .Windowed = True
    Else
      .Windowed = False
    End If

    ' Die Farbtiefe des BackBuffers 16 oder 32 Bit
    If bpp = 16 Then
      _dxsettings.BackBufferFormat = Format.R5G6B5    ' 16Bit
    Else
      _dxsettings.BackBufferFormat = Format.X8R8G8B8  ' 32Bit
    End If
  End With

  ' Device erstellen und zurückgeben
  Return New Device(0, DeviceType.Hardware, wHandle, _
      CreateFlags.SoftwareVertexProcessing, _dxsettings)
End Function
 
4. Die PresentParameters
Der Back-Buffer verhindert das Flackern beim Rendern. Alles wird zuerst in den Back-Buffer geschrieben und erst zum Schluss präsentiert. Das AutoDepthStencilFormat gibt das Format für den Z-Buffer an, er lässt sich ein- und ausschalten und regelt das Tiefenverhalten von Objekten auf der Z-Achse. Der Z-Buffer entscheidet, ob ein Objekt hinter einem anderen liegt, und ob es somit sichtbar ist oder nicht. Auch wenn kein Z-Buffer benötigt wird, sollte das AutoDepthStencilFormat gesetzt werden, da somit Fehler und langes Suchen vermieden werden können.
Bei der Erstellung des Devices können verschiedene Flags gesetzt werden. Eine Enumeration der Hardware ermöglicht ein dynamisches Setzen der Flags, um den Device an die unterstützen Funktionen der Grafikkarte anzupassen.
Aufruf der Engine aus der Startform
Irgendwo müssen wir ja noch sagen, dass die Engine erstellt werden soll. Das übernimmt die Startform. Hier ist der Aufwand etwas kleiner, denn es wird nur die neue Engine aufgerufen die uns den Device erstellt.
 
Public Class frmStartup
  Private _dxEngine As clsEngine
  Private _displaymode As clsEngine.Displaymodes

  Private Sub tsRun_Click(ByVal sender As System.Object, _
      ByVal e As System.EventArgs) Handles tsRun.Click

    _dxEngine = New clsEngine(_displaymode)
    Me.Dispose()
  End Sub
 
Beim Laden der Startform, wird diese Center gesetzt und beim Schließen ein End aufgerufen.
Standardmäßig ist der Displaymode immer der Fenstermodus.
 
Public Class frmStartup
  Private _dxEngine As clsEngine                 ' die Engine
  Private _displaymode As clsEngine.Displaymodes ' der Displaymode


  Private Sub frmMain_Load(ByVal sender As Object, ByVal e _
      As System.EventArgs) Handles Me.Load
    Me.StartPosition = FormStartPosition.CenterScreen
     ' In der PictureBox lässt sich z.B. hier ein Logo laden
    Me.tsWindow.Checked = True
  End Sub

  Private Sub tsExit_Click(ByVal sender As System.Object, ByVal e _
      As System.EventArgs) Handles
    tsExit.Click
    End
  End Sub
 
Jetzt fehlt nur noch das Ändern des Displaymodes.
 
Private Sub changeDisplaymode(ByVal sender As _
    System.Object, ByVal e As System.EventArgs) Handles _
    tsWindow.Click, _
    ts640.Click, _
    ts800.Click, _
    ts1024.Click, _
    ts1280.Click

  Dim Item As ToolStripMenuItem
  Item = CType(sender, ToolStripMenuItem)

  tsWindow.Checked = False
  ts640.Checked = False
  ts800.Checked = False
  ts1024.Checked = False
  ts1280.Checked = False

  Select Case Item.name
    Case Is = "tsWindow"
      tsWindow.Checked = True
      _displaymode = clsEngine.Displaymodes.mode_Windowed
    Case Is = "ts640"
      ts640.Checked = True
      _displaymode = clsEngine.Displaymodes.mode_640x800
    Case Is = "ts800"
      ts800.Checked = True
      _displaymode = clsEngine.Displaymodes.mode_800x600
    Case Is = "ts1024"
      ts1024.Checked = True
      _displaymode = clsEngine.Displaymodes.mode_10240x768
    Case Is = "ts1280"
      ts1280.Checked = True
      _displaymode = clsEngine.Displaymodes.mode_1280x1024
  End Select
End Sub
 
Je, nach Klick wird der Displaymode verändert. Ein Klick auf Run erstellt die Engine. Die Startform brauchen wir nicht mehr und beenden diese mit einem Dispose.
Jetzt können wir schon mal einen Device mit einer bestimmten Auflösung erstellen. Leider werden wir davon noch nicht viel sehen, da noch die Renderschleife fehlt.
5. Der Renderloop per PaintEvent
Eine 3D Szene wird ständig neu gerendert und am Besten so schnell wie es geht. Dazu braucht man eine  Schleife. Ist der Rendervorgang abgeschlossen startet die Schleife den Rendervorgang wieder neu. Die Schleife muss so erstellt werden, dass genug Zeit bleibt um Events zu verarbeiten. Somit liegt es nah  einen Event zur Schleifenbildung zu nutzen.
.Net bringt einen schönen Event mit sich, der Paint Event. Dies funktioniert so:
Ist der Rendervorgang abgeschlossen wird ein Invalidate der Renderform (clsEngine) gestartet. Dadurch wird die Form neu gezeichnet und der Paint-Event ausgelöst, der das Sub renderLoop in der clsEngine  aufruft um den Rendervorgang neu zu starten.
Dazu muss das automatische Neuzeichnen der Form deaktiviert werden, sonst würde ein schönes  Flackern entstehen, da die Form eigenständig mit der Control-Hintergrundfarbe überschrieben wird.
Also springen wir in die Engine-Klasse und fügen folgendes ein:
 
Public Class clsEngine…

   ' Hier wird das Flackern unterbunden
  Me.SetStyle(ControlStyles.AllPaintingInWmPaint, True)
  Me.SetStyle(ControlStyles.Opaque, True)
 
Somit wird beim Laden der Form der ControlStyle geändert, um das Flackern zu verhindern.
Die Engine benötigt jetzt noch den Paint-Event:
 
 ' Handles hinzufügen
AddHandler Me.Paint, AddressOf renderloop

 'Die Renderform Sichtbar machen
Me.Show()
 
Me.show zeigt die Renderform erst an, wenn die Engine die Initialisierung aller Objekte abgeschlossen hat.
Jetzt müssen wir noch sagen, was in renderLoop passieren soll…
6. Der Rendervorgang
Ein Rendervorgang läuft immer gleich ab. Dem Device wird gesagt, dass eine Szene beginnt. Da wir uns in einer Renderschleife befinden, hat das Device ja vielleicht schon ein Bild gerendert. Das müssen wir natürlich löschen, da sonst bei bewegten Objekten eine Überlagerung stattfinden würde.
Das Bild wird in den Back-Buffer geschrieben, also müssen wir diesen löschen. Da es sich im Prinzip um Farben und Pixel handelt, werden alle Pixel des Buffers mit einer Farbe überschrieben die wir festlegen können. Dann erfolgt der eigentliche Rendervorgang aller 3D Objekte.
Jetzt befindet sich das fertige Bild im Back-Buffer. Da alles fertig ist kann das Bild am Monitor präsentiert werden. Wenn die Szene beendet ist, muss der Rendervorgang neu gestartet werden. Da wir einen Z-Buffer verwenden können, wird das Device angewiesen diesen zu löschen, um später Fehler in der Darstellung zu vermeiden.
Wir erstellen vier neue Subs in clsEngine:
•  renderLoop  (startet beim Paint-Event)
•  renderStart   (alles was vor dem Start passieren soll)
•  renderScene (rendern der Objekte)
•  renderEnd     (alles was zum Ende passieren soll)
 
Private Sub renderloop(ByVal sender As Object, ByVal e _
     As EventArgs)
  renderStart()
  renderScene()
  renderEnd()
End Sub
 
 
Public Sub renderStart()
  _dxdevice.Clear(ClearFlags.Target Or _
      ClearFlags.ZBuffer, Color.Black, 1, 0)

   ' Beginn der Szene
  _dxdevice.BeginScene()
End Sub
 
 
Public Sub renderEnd()
   ' Ende der Szene
  _dxdevice.EndScene()
   ' Präsentation der Szene
  _dxdevice.Present()
   ' Aufruf des Paint-Event's in clsSurface
  Me.Invalidate()
End Sub
 
Das Sub renderScene rendert noch nichts, aber der Back-Buffer wird bereits mit einer blauen Farbe überschrieben.
Und wie wird jetzt der Rendervorgang beendet und die Form geschlossen? Dafür benötigen wir Tastatureingaben…
Tastatureingaben
Um einfache Tastatureingaben zu realisieren kann man den KeyDown Event von clsEngine nutzen.
 
AddHandler Me.KeyDown, AddressOf Keypressed   ' Tastendruck
 
Wird ESC gedrückt, soll die Engine beendet werden und der Startbildschirm wieder erscheinen.
 
#Region "Keyboard"

  Private Sub Keypressed(ByVal sender As Object, _
       ByVal e As KeyEventArgs)
    Select Case e.KeyValue
      Case Keys.Escape
        CloseEngine()         ' Sub zum Beenden der Engine
    End Select
  End Sub
#End Region

#Region "Close Engine"
  Private Sub closeEngine()
    _dxdevice.Dispose()       ' den Device freigeben
    Me.Dispose()              ' die Renderform beenden
    frmStartup.Show()         ' Startbildschirm aufrufen
  End Sub
#End Region
 
FPS Anzeige
Um die Geschwindigkeit des Rendervorgangs zu messen benötigt man eine Frame-Anzeige. Sie zeigt uns wie viele Bilder pro Sekunde gerendert werden. Um eine solche Anzeige zu ermöglichen, müssen wir lediglich einen einfachen Text renderen.
Zuerst erstellen wir eine Klasse clsFPS. Die Klasse soll die Bilder pro Sekunde zählen, also wie oft der Renderloop pro Sekunde durchläuft. Dazu benutzen wir den Environment.TickCount der uns die verstrichene Zeit seit Start der Anwendung in ms zurück gibt. Die Genauigkeit des Timers reicht für den Vorgang aus.
 
Public Class clsFPS

  Private _fps As String
  Private _fpsCount As Integer
  Private _fpsTick As Integer

  Public Sub New()
    _fps = "0"
    _fpsCount = 0
    _fpsTick = Environment.TickCount
  End Sub

  Public Function getFPS() As String
    _fpsCount += 1
    If Environment.TickCount - _fpsTick >= 1000 Then
      _fps = _fpsCount.ToString
      _fpsTick = Environment.TickCount
      _fpsCount = 0
    End If
    Return "FPS: " & _fps
  End Function
End Class
 
Die Funktion getFPS wird am Ende jedes Rendervorgangs aufgerufen. In der Funktion wird _fpsCount immer um eins erhöht. Wenn die verstrichene Zeit minus _fpsTick >= einer Sekunde ist, bekommt _fps den _fpsCount–Wert, der die Durchläufe gespeichert hat. _fpsTick wird auf die neue Zeit aktualisiert und der Counter wieder auf 0 gesetzt.
Über das Sub renderText wird die Frame-Anzeige gerendert. Der Aufruf erfolgt in clsEngine als letzter Rendervorgang in renderEnd.
 
Private _dxtext As Font
Private _dxtextRec As Rectangle
Private _key_displayOSD As Boolean
------------------------------------------------------------------
 ' Setup TextObject
_dxtext = New Font(_dxdevice, (New Drawing.Font("Verdana", 10, _
          FontStyle.Bold)))
_dxtextRec = New Rectangle(0, 0, 200, 150)
------------------------------------------------------------------
Public Sub renderEnd()
   ' Frameanzeige
  If _key_displayOSD Then Me.RenderText()
------------------------------------------------------------------
#Region "Render Text"
  Private Sub RenderText()
    _dxtext.DrawText(Nothing, _dxFPS.getFPS, _dxtextRec, _
              DrawTextFormat.Left, Color.Yellow)
  End Sub
#End Region
 
Über F1 kann die Anzeige ein- und ausgeschaltet werden
 
Case Keys.F1
 _key_displayOSD = Not _key_displayOSD  ' OSD an/aus
 
7. Optimierungen
Durch undefinierte Zustände von Renderstates des Devices, kann es zu Fehlern in der Darstellung kommen. So kann es passieren, dass ein Objekt einfach schwarz bleibt, obwohl man alles richtig gemacht hat. Hintergrund wäre hier, dass die Beleuchtung nicht explizit deaktiviert wurde. Da keine Lichtquelle vorhanden ist, bleibt das Objekt schwarz.
Folgende States sollten daher immer definiert werden:
 
_dxdevice.RenderState.Lighting = False
_dxdevice.RenderState.ZBufferEnable = False
_dxdevice.RenderState.AlphaBlendEnable = False
 
Texturierte Objekte positionieren sich in unterschiedlichen Entfernungen und ändern dadurch optisch ihre Größe. Die Texturen müssen sich der Größe anpassen und skalieren mit. Um die Qualitätsverluste zu reduzieren werden Texturfilter angewendet:
 
 ' Texturfilter für Stage 0
_dxdevice.SetSamplerState(0, SamplerStageStates.MagFilter, _
    TextureFilter.Linear)
_dxdevice.SetSamplerState(0, SamplerStageStates.MinFilter, _
    TextureFilter.Linear)
 
Der MinFilter ist zum Verkleinern und der MagFilter zum Vergrößern der Textur, und sind dafür gedacht, die Blockbildung und Grisseleffekte zu reduzieren. Der lineare Filter wird von jeder Hardware unterstützt. Ob andere Filter verwendet werden können, muss über eine Enumeration des Devices herausgefunden werden.
Eine 3D-Anwendung braucht alles an Geschwindigkeit was der Rechner zu bieten hat. Um anderen Programmen das Leben zu erschweren, kann man die Threadpriorität erhöhen:
 
 'Threading erhöhen
Threading.Thread.CurrentThread.Priority = _
   Threading.ThreadPriority.Highest
 
Bei deaktivierten VSync wird nach Fertigstellung eines Frames durch den Swap-Befehl der Back-Buffer zum Front-Buffer. Der RAMDAC erzeugt das Bild auf dem Bildschirm. Dabei kann es zu einem Perleffekt  (Tearing) kommen, bei dem das ausgegebene Bild aus mehreren aufeinander folgenden Frames besteht. Die Aktivierung des VSync bewirkt, dass der Swap-Befehl erst ausgeführt wird, wenn der RAMDAC den Frame auf dem Monitor dargestellt hat.
 
 ' Vsync aktivieren
_dxdevice.PresentationInterval = PresentInterval.One
 
Rendert die GPU schneller als der RAMDAC das Bild aktualisiert, pausiert die GPU in der Zeit. Um die Leistung der Hardware voll nutzen zu können, wird ein zweiter Back-Buffer erstellt, der in der Wartezeit gerendert werden kann. Es wird immer abwechselnd in Back-Buffer 1 und 2 gerendert und an den Front-Buffer übergeben. Diesen Aufbau wird Triple-Buffer genannt.
 
 ' Zweiten Backbuffer verwenden
_dxdevice.BackBufferCount = 2
 
Damit steht das Grundgerüst zum Rendern von Objekten. Jetzt kann es losgehen, die ersten Billboards zu erstellen. Dies und mehr wird Gegenstand des 2. Teils dieses Tutorials.
Hinweis
Das Beispiel-Projekt ist ausführlich kommentiert. Aus Gründen der Übersichtlichkeit wurde auf der HTML-Seite auf die Kommentare verzichtet oder entsprechend verkürzt.
Bei Fragen zu diesem Tutorial nutzen Sie bitte unser DirectX-Forum.


Download  (397 kB) Downloads bisher: [ 2009 ]

Zum Seitenanfang

Startseite | VB-/VBA-Tipps | Projekte | Tutorials | API-Referenz | Komponenten | Bücherecke | Gewinnspiele | VB-/VBA-Forum | DirectX-Forum | VB.Net | .Net-Forum | Foren-Archiv | Chat | Spielplatz | Links | Suchen | Stichwortverzeichnis | Feedback | Impressum

Seite empfehlen Bug-Report
Letzte Aktualisierung: Donnerstag, 27. November 2008