www.rolandk.de
- Aktuelle Themen zu .Net -
Achtung: Hier handelt es sich um meine alte Seite.
Die aktuelle ist unter folgendem Link erreichbar: www.rolandk.de/wp/
Home Tutorials DirectX10 Basics Chapter 1: Grundlagen




















































Chapter 1: Grundlagen
Mittwoch, den 01. Juli 2009 um 07:50 Uhr

 

 

Was braucht man?

Eigentlich ist dieses DirectX Beispiel recht simpel. Zuerst brauchen wir ein normales Windows.Forms Projekt, welches wir wie gehabt mit VisualStudio problemlos anlegen können. Als nächstes müssen wir noch ein paar Funktionen von DirectX aufrufen, damit wir auch was am Bildschirm sehen. Doch hier fangen die Probleme an: Wie greift man überhaupt von .Net Programmen auf DirectX zu?
Von C# aus ist es erst mal schwierig, denn DirectX ist keine .Net API. Generell ist es eher für die Programmierung unter C++ gedacht, wo es auch am häufigsten Verwendung findet. Doch wie greifen wir jetzt auf DirectX zu? Die Lösung liegt in einem DirectX-Wrapper. Ein Wrapper ist eine Bibliothek, welche alle/einige Funktionen einer anderen Bibliothek implementiert und dorthin weiterleitet. In meinen Tutorials wird der Wrapper SlimDX verwendet. Dabei handelt es sich um ein Community-Projekt und sieht mittlerweile auch ganz gut aus. Alles was wir also brauchen, um auf DirectX zuzugreifen, finden wir auf der Hompeage von SlimDX. Um jetzt DirectX zu nutzen, brauchen wir bloß auf die Datei SlimDX.dll verweisen.

Bevor ich jetzt genauer mit dem Coding anfange, erst noch eine andere Kleinigkeit. Bei SlimDX hat es sich bewährt nicht direkt usings auf die dort bereitgestellten Namespaces zu schreiben (Mit Ausnahme des SlimDX-Namespace). Der Grund liegt im mehrfachen Vorkommen mancher Namen. Beispielsweise gibt es die Device Klasse in den Namespaces SlimDX.DirectX10 und SlimDX.DXGI. Um dieses Problem zu umgehen, deklariere ich die usings immer so:

  1. using SlimDX;
  2.  
  3. using DX10 = SlimDX.Direct3D10;
  4. using DXGI = SlimDX.DXGI;

Auf diese Weise werden die Namespaces "umgeleitet". Wenn man jetzt schreibt DX10.Device, dann bedeutet das für den Compiler SlimDX.Direct3D10.Device. Der Vorteil ist die kurze Schreibweise und man weiß sofort zu welchem Namespace oder zu welcher API die Klasse gehört.

 

Direct3D initialisieren

Wie weiter oben schon geschrieben, brauchen wir zuerst ein Windows.Forms Projekt. All unser Coding für dieses Tutorial können wir dann in die Hauptfensterklasse packen. Im ersten Schritt legen wir am besten die benötigten Variablen an:

  1. private DX10.Device m_device;
  2. private DX10.RenderTargetView m_renderTarget;
  3. private DXGI.SwapChain m_swapChain;
  4. private DXGI.SwapChainDescription m_swapChainDesc;
  5. private DXGI.Factory m_factory;
  6. private bool m_initialized;

Was versteckt sich denn jetzt hinter all diesen Variablen? 
Device: Das wichtigste Objekt in einer Direct3D Anwendung. Es repräsentiert die Grafikhardware und ist damit verantwortlich für Das Rendern, für das Laden von 3D-Objekten und für vieles mehr.

  • RenderTargetView: Dieses Objekt Kapselt das Render-Ziel, in unserem Fall also das Hauptfenster.
  • SwapChain: Hängt mit dem RenderTargetView zusammen. Dieses Objekt hält die Speicherbereiche, in die gerendert wird.
  • SwapChainDescription: Nur ein Hilfsobjekt, wird benötigt, um eine SwapChain zu erstellen.
  • Factory: Ähnlich wie die SwapChainDescription wird auch dieses Objekt nur benötigt, um eine SwapChain zu erstellen.

Die letzte Variable m_initialized sagt unserem Hauptfenster noch, ob Direct3D richtig initialisiert wurde. Ist der Wert dieser Variable false, weiß die Anwendung, das es einen Fehler während der Initialisierung der 3D-Ansicht gab und es kann entsprechend reagiert werden. 

Im zweiten Schritt erstellen wir die Methode Initialize3D. Wie der Name schon sagt, wird dort die 3D-Ansicht initialisiert. Die grundsätzliche Struktur dieser Methode sieht so aus:

  1. private bool Initialize3D()
  2. {
  3. try
  4. {
  5. //TODO: Hier kommt der Initialisierungs-Code rein
  6.  
  7. m_initialized = true;
  8. }
  9. catch (Exception ex)
  10. {
  11. MessageBox.Show("Error while initializing Direct3D: n" + ex.Message);
  12. m_initialized = false;
  13. }
  14.  
  15. return m_initialized;
  16. }

Was hier passiert ist recht einfach. Alle Direct3D-Aufrufe werden in den try-Block eingefügt. Falls dort ein Fehler auftritt, wird dieser im catch-Block sofort ausgegeben und die m_initialized Variable auf false gesetzt. Zusätzlich wird ein Wert vom Typ bool zurückgegeben, um den Aufrufer über den Erfolg oder Misserfolg zu berichten.
Die Direct3D Aufrufe selbst teile ich hier etwas auf, um sie besser erklären zu können. Das Erste was wir mit Direct3D machen ist meistens das selbe: Wir erzeugen uns ein Device-Objekt.

  1. m_device = new DX10.Device(DX10.DeviceCreationFlags.SingleThreaded);
  2.  

Sieht doch schon mal einfach aus, oder? Alles was man hier wissen muss ist, ob man für den Zugriff auf Direct3D mehrere Threads verwendet, oder nicht. Die Anwendung selbst kann natürlich unabhängig davon in mehrere Threads aufgeteilt sein, hier geht es lediglich um die Aufrufe der Direct3D API.
Die zweite Aufgabe innerhalb der Initialisierungsfunktion ist das Erzeugen eines SwapChain Objekts. Wir erinnern uns, dieses Objekt hält alle Speicherbereiche, in die gerendert wird. Um ein solches Objekt zu erzeugen, ist dieser Code notwendig:

  1. m_factory = new DXGI.Factory();
  2.  
  3. m_swapChainDesc = new DXGI.SwapChainDescription();
  4. m_swapChainDesc.OutputHandle = this.Handle;
  5. m_swapChainDesc.IsWindowed = true;
  6. m_swapChainDesc.BufferCount = 1;
  7. m_swapChainDesc.Flags = DXGI.SwapChainFlags.AllowModeSwitch;
  8. m_swapChainDesc.ModeDescription = new DXGI.ModeDescription(
  9. this.Width,
  10. this.Height,
  11. new Rational(60, 1),
  12. DXGI.Format.R8G8B8A8_UNorm);
  13. m_swapChainDesc.SampleDescription = new DXGI.SampleDescription(1, 0);
  14. m_swapChainDesc.SwapEffect = DXGI.SwapEffect.Discard;
  15. m_swapChainDesc.Usage = DXGI.Usage.RenderTargetOutput;
  16.  
  17. m_swapChain = new DXGI.SwapChain(m_factory, m_device, m_swapChainDesc);

Das Factory-Objekt ist für uns eher unwichtig, es wird lediglich für den SwapChain Konstruktor benötigt. Viel interessanter ist da schon die SwapChainDescription. Wie der Name schon vermuten lässt, beschreibt dieses Objekt eine SwapChain. Die wichtigste Eigenschaft wird hier bereits als erstes gesetzt: Das OutputHandle. Damit wird die Verbindung zu unserem Hauptfenster über das Fensterhandle hergestellt. Auf die anderen Werte will ich hier nicht näher eingehen, da Sie für den Einsteiger auch erst mal nicht so wichtig sind. Wer mehr darüber erfahren will, kann in der Dokumentation des DirectX SDKs nachschlagen. Dort wird zwar grundsätzlich C++ gesprochen, doch die Namen sind die selben. Das aktuelle DirectX SDK findet ihr hier. Die letzte Anweisung in diesem Code-Ausschnitt erzeugt schließlich unsere SwapChain. 

Als nächstes müssen wir festlegen, wo in unserem Hauptfenster gerendert wird. Generell ist es nämlich gar nicht nötig, auf die komplette Fläche zu rendern wenn man eigentlich nur einen kleine Ausschnitt braucht. Damit wir Direct3D den Ziel-Bereich mitteilen können, verwenden wir die Viewport Struktur wie folgt:

  1. DX10.Viewport viewPort = new DX10.Viewport();
  2. viewPort.X = 0;
  3. viewPort.Y = 0;
  4. viewPort.Width = this.Width;
  5. viewPort.Height = this.Height;
  6. viewPort.MinZ = 0f;
  7. viewPort.MaxZ = 1f;

Die Eigenschaften dieser Struktur sind jetzt wieder etwas einfacher. Einzig die Felder MinZ und MaxZ werfen beim Einsteiger Fragen auf. Grundsätzlich bedeutet der Buchstabe Z in Direct3D "Tiefe". Neben der X-Achse und der Y-Achse gibt es in 3D auch noch die Z-Achse. Man kann sich das einfach so vorstellen, dass die Z-Achse in den Bildschirm hinein geht, während X und Y lediglich die Seitenkannten darstellen. Direct3D speichert sich nach dem Render-Vorgang für jeden Pixel auch dessen Tiefeninformation, sprich, wie weit dieser Pixel von der Kamera entfernt ist. Dieser Z-Wert reicht von 0 bis 1. Kurz gesagt verhalten sich diese beiden Eigenschaften genau so wie die anderen für X und Y auch, beziehen sich aber auf die Tiefe eine Pixels. 

Jetzt brauchen wir noch ein RenderTargetView Objekt. Bei diesem Objekt handelt sich um eine Verbindung zwischen Direct3D und der SwapChain.

  1. DX10.Texture2D backBuffer =
  2. DX10.Texture2D.FromSwapChain<DX10.Texture2D>(m_swapChain, 0);
  3. m_renderTarget = new DX10.RenderTargetView(m_device, backBuffer);

In der ersten Zeile hohlen wir den Ziel-Buffer aus der SwapChain raus. Damit Direct3D auch etwas damit anfangen kann brauchen wir diesen als Texture2D. Was sich hinter einer Texture verbirgt, werde ich in einem späteren Tutorial genauer erklären. In der zweiten Zeile erstellen wir lediglich das RenderTargetView. 

Jetzt haben wir es fasst geschafft, im letzten Schritt der Initialisierungsmethode müssen wir Direct3D noch sagen, dass es diese Objekte überhaupt gibt. Für diesen Zweck stellt uns das Device entsprechende Methoden bereit:

  1. m_device.Rasterizer.SetViewports(viewPort);
  2. m_device.OutputMerger.SetTargets(m_renderTarget);

Das war dann auch schon alles, unsere Initialisierungsfunktion ist fertig. Zwecks Vollständigkeit hier noch einmal die Methode Initialize3D in einem Stück:

  1. private bool Initialize3D()
  2. {
  3. try
  4. {
  5. m_device = new DX10.Device(DX10.DeviceCreationFlags.SingleThreaded);
  6.  
  7. m_factory = new DXGI.Factory();
  8.  
  9. m_swapChainDesc = new DXGI.SwapChainDescription();
  10. m_swapChainDesc.OutputHandle = this.Handle;
  11. m_swapChainDesc.IsWindowed = true;
  12. m_swapChainDesc.BufferCount = 1;
  13. m_swapChainDesc.Flags = DXGI.SwapChainFlags.AllowModeSwitch;
  14. m_swapChainDesc.ModeDescription = new DXGI.ModeDescription(
  15. this.Width,
  16. this.Height,
  17. new Rational(60, 1),
  18. DXGI.Format.R8G8B8A8_UNorm);
  19. m_swapChainDesc.SampleDescription = new DXGI.SampleDescription(1, 0);
  20. m_swapChainDesc.SwapEffect = DXGI.SwapEffect.Discard;
  21. m_swapChainDesc.Usage = DXGI.Usage.RenderTargetOutput;
  22.  
  23. m_swapChain = new DXGI.SwapChain(m_factory, m_device, m_swapChainDesc);
  24.  
  25. DX10.Viewport viewPort = new DX10.Viewport();
  26. viewPort.X = 0;
  27. viewPort.Y = 0;
  28. viewPort.Width = this.Width;
  29. viewPort.Height = this.Height;
  30. viewPort.MinZ = 0f;
  31. viewPort.MaxZ = 1f;
  32.  
  33. DX10.Texture2D backBuffer =
  34. DX10.Texture2D.FromSwapChain<DX10.Texture2D>(m_swapChain, 0);
  35. m_renderTarget = new DX10.RenderTargetView(m_device, backBuffer);
  36.  
  37. m_device.Rasterizer.SetViewports(viewPort);
  38. m_device.OutputMerger.SetTargets(m_renderTarget);
  39.  
  40. m_initialized = true;
  41. }
  42. catch (Exception ex)
  43. {
  44. MessageBox.Show("Error while initializing Direct3D: n" + ex.Message);
  45. m_initialized = false;
  46. }
  47.  
  48. return m_initialized;
  49. }

 

Was wir sonst noch brauchen

Das Gröbste wäre geschafft, jetzt brauchen wir nur noch 2 Dinge: Zum Einem müssen wir die Methode Initialize3D noch irgendwo aufrufen und zum Anderen müssen wir natürlich noch Rendern. Aber keine Sorge, dass wird jetzt ganz leicht.
Zuerst kümmern wir uns um den Aufruf unserer Methode. Der ideale Platz dafür ist das Load-Event unseres Hauptfensters:

  1. protected override void OnLoad(EventArgs e)
  2. {
  3. base.OnLoad(e);
  4. Initialize3D();
  5. }

Nachdem alle Variablen initialisiert wurden können wir rendern. Hierfür genügt es, ähnlich wie für 2D Sachen auch, das OnPaint Event zu nutzen:

  1. protected override void OnPaint(PaintEventArgs e)
  2. {
  3. base.OnPaint(e);
  4.  
  5. if (m_initialized)
  6. {
  7. m_device.ClearRenderTargetView(m_renderTarget, new Color4(Color.CornflowerBlue));
  8.  
  9. m_swapChain.Present(0, DXGI.PresentFlags.None);
  10. }
  11. }

Wie weiter oben schon beschrieben nutzen wir die m_initialized Variable um zu schauen, ob auch alles glatt lief. Falls nicht, wird einfach Garnichts gerendert. Das Rendern selbst beschränkt sich bis jetzt auf eine einzige Methode, und zwar Device.ClearRenderTargetView. Diese Methode setzt einfach jeden Pixel im Ziel-Buffer auf die angegebene Farbe. Der Aufruf von SwapChain.Present zeigt das Ergebnis schließlich auf dem Bildschirm an.

 

Kommentar hinzufügen

Ihr Name:
Kommentar:

Kommentare (8)

8. RolandK
Samstag, den 27. Oktober 2012 um 04:29 Uhr
Hallo zusammen,

habe das Coding jetzt so angepasst, dass es unter der aktuellsten Version von SlimDX kompiliert. Danke für den Hinweis!

Gruß
Roland

7. Tomarr
Freitag, den 26. Oktober 2012 um 15:28 Uhr
Shit, zeigt der Kommentar nicht richtig an.

Vor die klammern(m_swapChain, 0) muss in spitzen Klammern noch der Rückgabetyp DX10.Texture2D

6. Tomarr
Freitag, den 26. Oktober 2012 um 15:26 Uhr
Ah, habs rausgefunden. Sollte im Tutorial vielleicht geändert werden in:

DX10.Texture2D backBuffer = DX10.Resource.FromSwapChain(m_swapChain, 0);

5. Tomarr
Freitag, den 26. Oktober 2012 um 09:15 Uhr
Der Vorschlag von Niklas funktioniert leider auch nicht.

Ich habe zwar per Objektkatalog nach Alternativen gesucht, da ich aber aus gutem Grund ein Anfängertutorial durcharbeite stehe ich da etwas auf verlorenem Posten, zumal SlimDX da doch etwas umfangreicher ist. Und im nächsten Kapitel mit dem Dreieck habe ich da jetzt auch keine Lösung gefunden.

Hat sich da eventuel schon wieder etwas geändert?

4. Niklas
Samstag, den 18. September 2010 um 11:07 Uhr
Ich habs so gemacht:

DX10.Texture2D backBuffer = DX10.Resource.FromSwapChain(m_swapChain, 0);

3. Kathi
Montag, den 02. November 2009 um 18:20 Uhr
das funktioniert nicht mehr:
DX10.Texture2D backBuffer = m_swapChain.GetBuffer(0);

siehe: http://www.rkoenig.eu/index.php?option=com_content&view=article&id=20:chapter-2-das-erste-dreieck&catid=6:directx10-basics&Itemid=3

Changed behavior:
Resource::FromSwapChain method introduced
SwapChain::GetBuffer removed.

2. Roland
Samstag, den 23. Mai 2009 um 12:07 Uhr

Hallo Flo, das muss natürlich so aussehen:
DX10.Texture2D backBuffer = m_swapChain.GetBuffer(0);
Danke für den Hinweis


Gruß Roland


1. Flo
Samstag, den 23. Mai 2009 um 08:00 Uhr

DX10.Texture2D backBuffer = m_swapChain.GetBuffer(0);


"m_swapChain.GetBuffer(0);"
Fehler 1 Die Typargumente der SlimDX.DXGI.SwapChain.GetBuffer(int)-Methode können nicht per Rückschluss aus der Syntax abgeleitet werden. Geben Sie die Typargumente explizit an.