|
Texturierte Objekte in Manged
DirectX - Teil 1
|
|
|
Autor/Einsender: |
|
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.
|
|
|
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
|
|
|
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
|
|
|
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…
|
|
|
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
|
|
|
|
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…
|
|
|
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
|
|
|
|
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.
|
|
|
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. |
|