Meine Werkzeuge
Namensräume
Varianten

SharpDX Einstieg

Aus indiedev
Wechseln zu: Navigation, Suche
Tutorial
SharpDX Einstieg
Autor Roland "Glatzemann" Rosenkranz
Programmier­sprache C#
Kategorie SharpDX
Diskussion Thread im Forum
Lizenz indiedev article license


Da einigen Leuten C++ etwas zu kompliziert ist oder einfach C# bequemer finden blieb für diese bisher hauptsächlich XNA und SlimDX als Alternative übrig. Dies hat sich jedoch vor einiger Zeit Dank der Arbeit von Alexandre Mutel geändert. Er hat eine neuartige Art und Weise "erfunden" einen Managed Wrapper für DirectX zu erzeugen. Dabei werden die Header-Files von DirectX automatisch geparsed, daraus Managed-Code erzeugt und das daraus resultierende Kompilat (die sogenannte Intermediate Language IL) wird noch durch ein paar Tools aufgebessert. So entstand der bisher schnellste DirectX-Wrapper für Managed Sprachen wie C#. Nähres dazu findet ihr in Alexandre Mutels Blog-Eintrag Benchmarking C#/.Net Direct3D 11 APIs vs native C++. Sehr beeindruckende Zahlen wie ich finde.

Inhaltsverzeichnis

Voraussetzungen

Um diesem kurzen Tutorial folgen zu können benötigen wir Visual Studio von Microsoft. Die Express-Edition für C# bietet sich als kostenlose Variante hervorragend an. Dies kann auf der Microsoft-Website heruntergeladen werden. Zusätzlich dazu benötigen wir natürlich noch SharpDX. Dies bekommen wir auf der SharpDX-Website. In diesem Tutorial beziehe ich mich auf die Version SharpDX-Bin-2.0.4.

Damit SharpDX korrekt funktioniert ist es sehr ratsam dass DirectX SDK von Microsoft zu installieren. Dort sind einige Tools enthalten die sehr hilfreich sein können (PIX, DirectX Error-Code Lookup) oder aber unabdinglich sind (HLSL-Compiler). Aus Lizenzrechtlichen Gründen dürfen diese nicht mit SharpDX verteilt werden und müssen daher separat geladen werden. Aktuelle Version ist das DirectX June 2010 SDK, wenn man mal von den Previews für Windows 8 und Metro Style Games absieht.

Ist das alles heruntergeladen und installiert, kann es auch schon weitergehen.

Ich werde euch in diesem Tutorial übrigens nicht beibringen, wie man mit C# entwickelt. Dazu gibt es hier bei indiedev eine Reihe von Tutorials und im Glossar-Eintrag zu C# nützliche Links. Trotzdem richtet sich dieses Tutorial eher an Einsteiger und wird hauptsächlich Grundlagen erklären. Dem fortgeschritteneren Entwickler wird vermutlich vieles bereits sehr bekannt vorkommen.

Ich möchte an dieser Stelle übrigens direkt auf das Forum verweisen, in dem es zwei Kategorien gibt, in denen ausführlich über C# und SharpDX diskutiert werden kann: Spieleentwicklung - SharpDX und Sprachen - C#. Natürlich gibt es auch im Rest des Forums durchaus interessante Themen wie z.B. im Algorithmen-Brett, aber auch im DirectX-Bereich, denn praktisch alles was für DirectX gilt, gilt auch für SharpDX.

Projekt in der IDE einrichten

Nachdem wir unsere IDE Visual Studio Express gestartet haben, richten wir zunächst ein neues Projekt ein. Dieses Projekt wird ersteinmal nicht viel können. Im Grunde genommen bilden wir damit das nach, was XNA mit dem Standard-Game-Template macht, zumindest aus grafischer Sicht. Wir öffnen ein Fenster, in dem gerendert werden soll. Dann initialisieren wir DirectX über SharpDX und zuletzt lassen wir den Hintergrund im Fenster durch DirectX löschen. Im weiteren Tutorials werde ich dann später erklären, wie wir die ersten Primitive rendern können und so ein wenig Farbe auf den Bildschirm bekommen, dazu aber später mehr.

Los gehts also. Nachdem Visual Studio gestartet ist, erzeugen wir ein neues Projekt mit dem Menüpunkt Projekt / Neues Projekt. Daraufhin erscheint ein Wizard in dem wir Leeres Projekt wählen.

Zunächst benötigen wir ein paar Referenzen von SharpDX, damit alles so funktioniert, wie wir es uns vorstellen. Diese finden wir im SharpDX-Projektordner, den wir ja heruntergeladen und irgendwo entpackt haben. Wir benötigen die folgenden Assemblies aus diesem Ordner und fügen diese mit einem Rechtsklick auf Verweise (References) und dann Verweis hinzufügen zu unserem Projekt hinzu.

  • SharpDX
  • SharpDX.Direct3D11
  • SharpDX.DXGI

Was im einzelnen in diesen Assemblies enthalten ist, das werden wir im Verlauf der weiteren Tutorials noch erfahren.

Wir sollten im gleichen Zug auch noch die .NET-Assemblies System und System.Windows.Forms hinzufügen, da diese in einem leeren Projekt nicht vorhanden ist. Wir sollten hier - falls mehrere Versionen installiert sind - die Version 4.0 verwenden.

Startpunkt

Der Startpunkt ist die statische Main-Methode. Wir fügen unserem Projekt über Rechtsklick Projekt, gefolgt von Hinzufügen / Klasse eine neue Klasse mit Namen Program.cs hinzu. Diese befüllen wir erstmal mit dem folgenden Code.


using System;
using SharpDX.Windows;

namespace Einstieg
{
    internal static class Program
    {
        [STAThread]
        private static void Main()
        {

        }
    }
}

Starten wir das Programm, dann wird die Main-Methode als erstes ausgeführt. Im ersten Schritt werden wir unseren gesamten Code dort reinpacken. In späteren Teilen werden wir das etwas geschickter angehen und vor allem Objektorientierter, aber für einen Einstieg und das Grundverständnis reicht dies erstmal vollkommen aus.

Das RenderForm

Hier spiel SharpDX, in Verbindung mit C# und WinForms seine Stärken aus, denn ein Fenster kann deutlich einfacher und mit viel weniger Code als mit C++ initialisiert werden. Wie das geht, habe ich hier beschrieben. Dort gibt es ein paar interessante Hintergrundinformationen. Der Vergleich ist sicherlich ein wenig unfair, dann das RenderForm, dass wir nun öffnen werden kann man natürlich auch einmalig in C++ entwickeln und dann dort immer wieder verwenden.


var form = new RenderForm("indiedev Tutorial - SharpDX Einstieg");

Diese Zeile reicht zur Erzeugung des Fensters schon vollkommen aus.

DirectX initialisieren

Für die nächsten paar Schritte spare ich mir erstmal ausführliche Hintergrundinformationen, da diese bereits in einem anderen Artikel sehr ausführlich behandelt wurden und zwar hier. Ich mache dies aus einem bestimmten Grund. Es ist extrem hilfreich, wenn man zumindest vertraut ist mit dem C++/DirectX-Syntax. Damit steht einem eine komplett neue Welt offen, denn man kann C++-Beispiele einfach in ein SharpDX-Projekt "übersetzen".

Zunächst erzeugen wir die SwapChain. Dafür brauchen wir erstmal eine SwapChainDescription.


var desc = new SwapChainDescription()
{
  BufferCount = 1,
  ModeDescription = new ModeDescription(form.ClientSize.Width, form.ClientSize.Height, new Rational(60, 1), Format.R8G8B8A8_UNorm),
  IsWindowed = true,
  OutputHandle = form.Handle,
  SampleDescription = new SampleDescription(1, 0),
  SwapEffect = SwapEffect.Discard,
  Usage = Usage.RenderTargetOutput
};  

Device device;
SwapChain swapChain;

Device.CreateWithSwapChain(DriverType.Hardware, DeviceCreationFlags.None, desc, out device, out swapChain);
var context = device.ImmediateContext;  

var factory = swapChain.GetParent<Factory>();
factory.MakeWindowAssociation(form.Handle, WindowAssociationFlags.IgnoreAll); 

Mit den Informationen aus dem verlinkten DirectX 11 Jumpstart Artikel dürfte die SwapChainDescription leicht zu verstehen sein. In Zeile 15 jedenfalls wird die SwapChain erzeugt. Dieser Methodenaufruf liefert uns Referenzen auf das DirectX Device und auf die eigentliche SwapChain. Das Device ist übrigens mit dem GraphicsDevice aus XNA vergleichbar.

In Zeile 16 holen wir den sogenannten DeviceContext, und zwar die Immediate-Variante. Der Device-Kontext ist eine Neuerung die in DirectX 11 eingeführt wurde und dient der Thread-Safety. Mit dem Immediate-Context verhält sich alles im Grunde so wie immer. Befehle die ausgeführt werden, werden sofort an die Grafikkarte zur Verarbeitung gegeben. Wollen wir Grafiken im Multithreading rendern, benötigen wir einen Deferred-Context. Befehle die wir auf diesen ausführen, landen in einer Command-List (welche in einem fremden Thread aufgebaut werden kann). Ist diese Command-List vollständig, so können wir diese im Hauptthread durch das Device, dass wir in der Referenz in Zeile 12 gespeichert haben "abspielen". Das ist jedoch ein recht komplexes Thema und daher werden wir dies erstmal vollständig ignorieren.

In den Zeilen 18 und 19 binden wir die SwapChain an unser RenderForm.

Wenn wir diesen Code in unser bisheriges Projekt eingefügt haben, wir uns schnell auffallen, dass eine Menge Fehlermeldungen auftreten (rot gewellte Linien). Das liegt daran, dass wir Referenzen per using-Direktive bekanntgeben müssen. Dies kann Visual Studio automatisch für uns erledigen. Dazu klicken wir mit der Rechten Maustaste auf einen dieser Fehler und wählen im erscheinenden Kontextmenü Auflösen gefolgt von der passenden Using-Direktive. Dies wiederholen wir solange mit den weiteren Fehlern, bis keine mehr auftreten.

Wir werden aber leider trotzdem nicht alle Fehler auflösen können, da es noch einen Namenskonflikt mit Device gibt. Diesen können wir durch eine weitere Using-Direkte auflösen und zwar mit der folgenden.


using Device = SharpDX.Direct3D11.Device; 

Diese gibt schlicht und einfach an, dass wenn wir Device schreibe, wir das aus dem Namespace SharpDX.Direct3D11 verwenden möchten.

Die Fehler sollten nun alle korrigiert sein.

Der BackBuffer, Viewport und OutputMerger

Weiter geht es mit dem BackBuffer, dessen Referenz wir aus der SwapChain holen müssen. Dieser dient uns zum erzeugen des RenderView. Dieser wird dann als RenderTarget gesetzt und dient als Textur, in die wir rendern. Hört sich kompliziert an, ist aber tatsächlich mit drei Zeilen Code erledigt.


var backBuffer = Texture2D.FromSwapChain<Texture2D>(swapChain, 0);
var renderView = new RenderTargetView(device, backBuffer);

Nun müssen wir DirectX noch zwei Dinge mitteilen. Zum einen ist dies der Viewport. Das ist der rechteckige Bereich auf dem Bildschirm (bzw. dem Fenster) in den wir unsere Grafik ausgeben wollen. Wir verwenden hier erstmal das gesamte Fenster. Über mehrere Viewports können später z.B. Split-Screens realisiert werden.


context.Rasterizer.SetViewports(new Viewport(0, 0, form.ClientSize.Width, form.ClientSize.Height, 0.0f, 1.0f));

Der Rasterizer dem wir sagen, welcher Viewport verwendet werden soll kümmert sich übrigens darum, dass die einzelnen Bildpunkte im BackBuffer gesetzt werden. Eine Übersicht gibt es übrigens im Glossar-Eintrag Grafikpipeline.

Der letzte Schritt vor dem eigentlichen Rendern ist der OutputMerger. Dieser kümmert sich darum, dass die vom Rasterizer erzeugten Daten irgendwohin geschrieben werden. Dazu möchten wir den RenderView verwenden, der auf den BackBuffer zeigt.


context.OutputMerger.SetTargets(renderView);

Die Render-Loop

Unsere erste Render-Loop wird sehr einfach sein und daher arbeiten wir dort mit einem anonymen Delegate. Der Source dafür:


            RenderLoop.Run(form, () => {
                                            context.ClearRenderTargetView(renderView, Colors.CornflowerBlue);
                                            swapChain.Present(0, PresentFlags.None);
                                       }
                          );

Die Render-Loop-Klasse ruft dabei einfach die beiden Befehle in den Zeilen 2 und 3 regelmäßig auf. In Zeile 2 wird der RenderView mit der Farbe CornflowerBlue gelöscht und in Zeile 3 wird die swapChain weitergeschaltet. Die beiden Parameter geben an, dass dies mit den Einstellungen der Swap-Chain geschehen soll. Diese haben wir eingangs auf 60 Bilder pro Sekunde eingestellt und das ist der Intervall, der von Present nun eingehalten wird.

Am Ende müssen wir nur noch aufräumen. Das machen wir nach der RenderLoop. Die Befehle die dort angegeben sind, werden erst ausgeführt, wenn das RenderWindow geschlossen wird. Dies erfolgt mit dem Dispose-Befehl.


renderView.Dispose();
backBuffer.Dispose();
context.ClearState();
context.Flush();
device.Dispose();
context.Dispose();
swapChain.Dispose();
factory.Dispose();

Die Befehle ClearState und Flush dienen dabei der Zurückversetzung der Grafikkarte in einen definierten Zustand.

Abschluss

Nun können wir unser Programm mit F5 starten. Es erscheint ein blaues Fenster, so wie wir es z.B. von XNA kennen. Damit haben wir nun DirectX erfolgreich initialisiert und verwenden es um unseren Fensterhintergrund zu löschen. Dies bedeutet: Wir haben das Ziel des Tutorials erfolgreich erreicht.

Sicherlich ist euch aufgefallen, dass noch ein Konsolenfenster geöffnet wird, welches nicht sonderlich schön aussieht (aber praktisch für Debug-Ausgaben ist). Dieses kann in den Projekteigenschaften verhindert werden. Dazu klicken wir das Projekt mit der rechten Maustaste an und wählen Eigenschaften. Im Fenster das sich öffnet stellen wir den Ausgabetyp auf Windows-Anwendung' um und das Startobjekt auf Program (bzw. die einzige Auswahlmöglichkeit). Nach einem Start mit F5 erscheint jetzt nur noch das RenderWindow.

Damit möchte ich zum Ende dieses Artikels kommen. Nun noch ein paar gesammelte Links, gefolgt von einer Übersicht über den gesamten Sourcecode dieses Tutorials.

Ich verabschiede mich bis zum nächsten Tutorial und hoffe bis dahin auf ausgiebige Diskussion im Forum.

Weiterführende Links

Sourcecode


using System;
using SharpDX.Windows;
using SharpDX.DXGI;
using SharpDX.Direct3D11;
using Device = SharpDX.Direct3D11.Device;
using SharpDX.Direct3D;
using SharpDX; 

namespace Einstieg
{
    internal static class Program
    {
        [STAThread]
        private static void Main()
        {
            var form = new RenderForm("indiedev Tutorial - SharpDX Einstieg");

            var desc = new SwapChainDescription() 
            { 
                BufferCount = 1, 
                ModeDescription = new ModeDescription(form.ClientSize.Width, form.ClientSize.Height, new Rational(60, 1), Format.R8G8B8A8_UNorm), 
                IsWindowed = true, 
                OutputHandle = form.Handle, 
                SampleDescription = new SampleDescription(1, 0), 
                SwapEffect = SwapEffect.Discard, 
                Usage = Usage.RenderTargetOutput 
            }; 
            
            Device device; 
            SwapChain swapChain; 
            
            Device.CreateWithSwapChain(DriverType.Hardware, DeviceCreationFlags.None, desc, out device, out swapChain); 
            var context = device.ImmediateContext; 

            var factory = swapChain.GetParent<Factory>(); 
            factory.MakeWindowAssociation(form.Handle, WindowAssociationFlags.IgnoreAll);

            var backBuffer = Texture2D.FromSwapChain<Texture2D>(swapChain, 0);
            var renderView = new RenderTargetView(device, backBuffer);

            context.Rasterizer.SetViewports(new Viewport(0, 0, form.ClientSize.Width, form.ClientSize.Height, 0.0f, 1.0f));

            context.OutputMerger.SetTargets(renderView);

            RenderLoop.Run(form, () => {
                                            context.ClearRenderTargetView(renderView, Colors.CornflowerBlue);
                                            swapChain.Present(0, PresentFlags.None);
                                       }
                          );

            renderView.Dispose();
            backBuffer.Dispose();
            context.ClearState();
            context.Flush();
            device.Dispose();
            context.Dispose();
            swapChain.Dispose();
            factory.Dispose();
        }
    }
}

Navigation
Tutorials und Artikel
Community Project
Werkzeuge