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 DirectX 11 Basics Chapter 1: Grundlagen




















































Chapter 1: Grundlagen
Sonntag, den 28. Oktober 2012 um 07:33 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.DirectX11 und SlimDX.DXGI. Um dieses Problem zu umgehen, deklariere ich die usings immer so:

  1. using SlimDX;
  2.  
  3. using DX11 = SlimDX.Direct3D11;
  4. using DXGI = SlimDX.DXGI;

Auf diese Weise werden die Namespaces "umgeleitet". Wenn man jetzt schreibt DX11.Device, dann bedeutet das für den Compiler SlimDX.Direct3D11.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 DX11.Device m_device;
  2. private DX11.DeviceContext m_deviceContext;
  3. private DX11.RenderTargetView m_renderTarget;
  4. private DXGI.SwapChain m_swapChain;
  5. private DXGI.SwapChainDescription m_swapChainDesc;
  6. private DXGI.Factory m_factory;
  7. 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.
  • DeviceContext: Dieses Objekt ist neu in Direct3D 11. Über dieses Objekt erfolgen alle Render-Aufrufe und Einstellungen für das Rendering.
  • 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 und holen uns das DeviceContext Objekt für Render-Aufgaben.

  1. m_device = new DX11.Device(
  2. DX11.DriverType.Hardware,
  3. DX11.DeviceCreationFlags.SingleThreaded);
  4. m_deviceContext = m_device.ImmediateContext;

Sieht doch schon mal einfach aus, oder? 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. //Query for the dxgi adapter
  2. DXGI.Device dxgiDevice = new DXGI.Device(m_device);
  3. DXGI.Adapter dxgiAdapter = dxgiDevice.Adapter;
  4. m_factory = dxgiAdapter.GetParent<DXGI.Factory>();
  5.  
  6. m_swapChainDesc = new DXGI.SwapChainDescription();
  7. m_swapChainDesc.OutputHandle = this.Handle;
  8. m_swapChainDesc.IsWindowed = true;
  9. m_swapChainDesc.BufferCount = 1;
  10. m_swapChainDesc.Flags = DXGI.SwapChainFlags.AllowModeSwitch;
  11. m_swapChainDesc.ModeDescription = new DXGI.ModeDescription(
  12. this.Width,
  13. this.Height,
  14. new Rational(60, 1),
  15. DXGI.Format.R8G8B8A8_UNorm);
  16. m_swapChainDesc.SampleDescription = new DXGI.SampleDescription(1, 0);
  17. m_swapChainDesc.SwapEffect = DXGI.SwapEffect.Discard;
  18. m_swapChainDesc.Usage = DXGI.Usage.RenderTargetOutput;
  19.  
  20. 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. Wichtig ist nur, wie es erzeugt wird. Man könnte es zwar einfach über den Standardkonstruktor erzeugen, aber bei manchen Systemen kommt es dabei zu einen Fehler. Richtig ist es, sich das Factory Objekt aus dem vorher erzeugten Device Objekt herzuleiten.

 

Weiter geht es mit der 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. DX11.Viewport viewPort = new DX11.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. DX11.Texture2D backBuffer =
  2. DX11.Texture2D.FromSwapChain<DX11.Texture2D>(m_swapChain, 0);
  3. m_renderTarget = new DX11.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. Wichtig an der Stelle ist, dass man anders als in vorherigen DirectX Versionen nicht das Device Objekt, sondern den DeviceContext entsprechend konfigurieren muss. Nachfolgend die beiden nötigen Zeilen.

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

 

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_deviceContext.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 nichts gerendert. Das Rendern selbst beschränkt sich bis jetzt auf eine einzige Methode, und zwar DeviceContext.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 (2)

2. RolandK
Donnerstag, den 03. Januar 2013 um 18:10 Uhr
Hi,

ich kann zwar jetzt schlecht sagen, was bei dir das Problem war, aber auf einem Windows 7 Laptop mit NVidia-Karte startete das Programm heute bei mir auch nicht. Problem ist scheinbar folgende Zeile:

m_factory = new DXGI.Factory();

Diese ist zu ersetzen, wie oben im Artikel beschrieben (soeben aktualisiert).
Scheinbar hat DXGI manchmal Probleme damit, wenn man ein eigenes Factory-Objekt erzeugt. Ich lade gleich noch die angepassten Quellcodes hoch.

Was bei dir sonst noch sein kann:
Hast du SlimDX in der aktuellen Version bei dir installiert? SlimDX ist zwingende Vorraussetzung für dieses Tutorial.

Gruß
Roland

1. -.-
Montag, den 31. Dezember 2012 um 18:33 Uhr
funktioniert nicht!