2010. augusztus 3., kedd

SlimDX9 01 - Kezdőlépések

Mit tehet az ember ha .NET programozó és megszeretne ismerkedni a DirectX API-val? Hát előveszi a MDX-t (Managed DirectX) amit azonban már 2006-ban lezártak. Így nem szerencsés túl sok energiát beleölni mivel csak a DX9-es verzióját támogatja. Pár bejegyzés erejével én is foglalkoztam vele, de már elszállt felette az idő. Helyette itt van a XNA stúdió, ami már egy komplett Framework a játékfejlesztéshez. De mi van, ha nem játékot akarunk írni, csak egyszerűen a DirectX API érdekel minket? Szerencsére nem kell lemondani a C# eleganciáról, mert itt van a SlimDX ami egy nagyon vékony wrapper a DirectX API fölé és már a DirectX 11-et is támogatja. A SlimDX az a DirectX-nek mint a Tao és az OpenTK az OpenGL-nek egy réteg, amin keresztül a natív API elérhető menedzselt környezetből. Persze az interop miatt van sebességkülönbség, de jelenleg ez egy picit sem izgat minket, mert nem DOOM 6-ot fejlesztünk! Pár FPS feláldozható a .NET környezet által biztosított kényelemért. Szerintem, megéri. Azért, hogy kedvet csináljak itt egy kis videó, hogy hozzáértő kezek mit is tudnak kivarázsolni a SlimDX-ből:



Persze a teljesítmény relatív. A C++ programozók soha nem fogják megérteni mi értelme van 3D API-t menedzselt környezetből elérni. Pedig a sebesség nem minden. A SlimDX támogatja a Direct3D9, Direct3D9Ex, Direct3D10, Direct3D 10.1, Direct3D 11, Direct2D, D3DCompiler, DirectWrite, DirectInput, DirectSound könyvtárakat és a felsorolás még nem is teljes. Egyenlőre mi is megmaradunk a DirectX 9-nél mert itt még shaderezés nélkül is kitudnunk rajzolni egy háromszöget és majd később térünk át DX10-re vagy 11-re...talán :)

A SlimDX használatbavétele


A SlimDX wrapper használatához meg kell látogatnunk a http://slimdx.org weboldalt ahonnan töltsük le a legutolsó Developer SDK-t. Fontos tudnunk, hogy a SlimDX segítségével fejlesztett programjaink csak akkor indulnak el, ha ez a library telepítve van, ezért célszer még letölteni az End User Runtime csomagot is, amit programunkhoz mellékelhetünk, ha azt a szomszédnak vagy éppenséggel a nagyvilágnak kívánjuk átadni.
A SlimDX SDK telepítése után (amivel nem lehet probléma) hozzunk létre egy új Windows Forms projektet VS-ben. A referenciákhoz adjuk hozzá a SlimDX.dll-t. A Form1-egyet nevezzük át mondjuk MainForm-ra és nézzük meg a háttérkódot. A névterekhez adjuk hozzá a következő sorokat:

using SlimDX;
using SlimDX.Direct3D9;
using SlimDX.Windows;

Amint látható a DirectX 9-es verziójával kezdünk el ismerkedni, talán egyszer a DX10-ig is eljutunk majd. Itt változtassuk meg a MainForm ősét Form-ról RenderForm-ra. A RenderForm a SlimDX.Windows névtérben lakozik, őse persze ennek is Form csak éppen a konstruktorában van egy kis trükk elrejtve, akit érdekel reflektorozza meg! Ezzel készen is vagyunk. A SlimDX.dll elérhető .NET 2.0 és 4.0 alá is, mi most a 2.0 Frameworkot igénylő szerelvényt fogjuk használni. Akkor kezdjünk el kódolni…

A Direct3D környezet lekérdezése és beállítása


Az első lépések egyike, hogy inicializáljuk a Direct3D környezetet. Ehhez létrehozunk egy példányt a Direct3D osztályból.

private Direct3D direct3D = new Direct3D ();

A direct3D példány segítségével elérhetőek azok a függvények amelyekkel a rendelkezésre álló megjelenítő hardvereszköz(ök) képességeit lekérdezhetjük. Ezt az objektumot kell legelőször létrehozni és persze ez is szűnik meg utoljára.
Természetesen ahhoz, hogy megjelenítsünk bármit is szükség van megjelenítő eszközre vagy adapterre. Az elérhető megjelenítő eszközöket a direct3D.Adapters tulajdonság segítségével kaphatjuk meg, amit egy AdapterCollection listában tárolunk:

AdapterCollection adapters = direct3D.Adapters;

A lista elemei tárolják az elérhető eszközök tulajdonságait amik az AdapterInformation osztályból származnak. Egy megjelenítő részletes leírását pedig a Details tulajdonságon keresztül érhetjük el, ami pedig az AdapterDetails osztály egy példánya. A Details osztálynak számtalan tulajdonsága van ami keresztül információkat kaphatunk, ezek közül a legfontosabbak:
  • Descripton – Eszköz leírása szövegesen
  • DriverName – Meghajtó
  • DeviceName – Logikai Név
  • DriverVersion – Meghajtó verzió
  • VendorID – Gyártóazonosító
  • DeviceIdentifier – Eszközazonosító
  • WHQLLevel (Windows Hardware Quality Labs (WHQL)) – Digitális aláírás
A WHQL a Microsoft tesztlaboratóriuma, amelynek feladata a Windows operációs rendszerhez szánt eszközök és eszközmeghajtó programok kompatibilitásának ellenőrzése, és erről a megfelelő tanúsítványok kiállítása. Összefoglalva így kérdezhetjük le az elérhető megjelenítőket:
 AdapterCollection adapters = direct3D.Adapters;

foreach (AdapterInformation adapter in adapters)
{
DisplayModeCollection modes = adapter.GetDisplayModes(Format.X8R8G8B8);
DisplayMode current = adapter.CurrentDisplayMode;
AdapterDetails detail = adapter.Details;
}
A CurrentDisplayMode tulajdonság egy DisplayMode struktúrát ad vissza, aminek adattagjai tartalmazzák a megjelenítő szélességét (Width) és magasságát (Height), valamint ezek egymáshoz viszonyított arányát (AspectRatio). A Format a pixelformátumot, azaz a képpontok memóriabeli leképezésének módját, a RefreshRate pedig a képfrissítés gyakoriságát tartalmazza. A DisplayModeCollection egy lista amiben a videokártya által támogatott üzemmódokat tárolhatjuk el. A listát az adapter GetDisplayModes metódusának meghívásával kérdezhetjük le. Paraméternek az ellenőrizni kívánt RGBA, mélység vagy stencil puffer formátumot kell átadnunk. Az alapértelmezett megjelenítő sorszámát a következőképpen kaphatjuk meg:

private int adapter = direct3D.Adapters.DefaultAdapter.Adapter;

A legtöbb esetben adapter = 0. Az adapter sorszámú megjelenítőhöz tartozó képernyő beállításokat egy DisplayMode struktúrában tároljuk el:

private DisplayMode mode =
= direct3D.GetAdapterDisplayMode(adapter);

A következő teendőnk felmérni az eszköz csúcspontfeldolgozó képességét. Ehhez a az IDirect3D9 interfész GetDeviceCaps() függvényét hívjuk meg, amely a végrehajtás során feltölt egy Capabilities típust az eszköz jellemzőivel:

private Capabilities caps =
= direct3D.GetDeviceCaps(adapter, DeviceType.Hardware);

A függvény első paramétere a megjelenítő eszköz sorszáma (adapter) – mi az elsődleges (alapértelmezett) megjelenítő eszköz képességeit szeretnénk felmérni. A második paraméter pedig az eszköz típusa, ami lehet (Hardware, Software, Reference, NullReference). Az eszköz képességeit a caps fogja tartalmazni. A hardweres csúcspontfeldolgozás lekérdezése a következő képpen valósítható meg:

CreateFlags createFlags = CreateFlags.None;

if ((caps.DeviceCaps & DeviceCaps.HWTransformAndLight) != 0)
createFlags = CreateFlags.HardwareVertexProcessing;
else
createFlags = CreateFlags.SoftwareVertexProcessing;

Amennyiben eszközünk támogatja a hardveres T&L műveleteket, leendő eszközünk csúcspontfeldolgozással kapcsolatos létrehozási paramétereit hardveres megoldásra állítjuk (CreateFlags.HardwareVertexProcessing), ellenkező esetben a szoftveres vertex feldolgozást választjuk. Érdemes megemlíteni, hogy vegyes csúcspont feldolgozás is választható (CreateFlags.MixedVertexProcessing). Amennyiben van hardveres vertex-támogatás, úgy ez az opció elérhető. Az alkalmazásban a szoftveres és a hardveres csúcspontfeldolgozás között az alábbi függvény segítségével váltogathatunk:

bool Device.SoftwareVertexProcessing

Vagyis a létrehozott device példány SoftwareVertexProcessing tulajdonságának szoftveres vertex feldolgozáshoz TRUE, hardveres feldolgozáshoz pedig FALSE értéket kell beállítani.
Természetesen fontos, hogy a kiválasztott képpontformátum támogatottságát is leellenörizzük, az első és a hátsó színpuffer esetében egyaránt, alblakos és szükség szerint teljes képernyős üzemmodókban is. Az ellenörzés a CheckDeviceType függvény segítségével történik:

bool check = direct3D.CheckDeviceType(adapter,
DeviceType.Hardware, mode.Format, mode.Format, false);

Az első paraméter az Adapter ami a megjelenítő eszköz sorszáma. A második paraméter a DeviceType ami az eszköz típusát adja meg. A DeviceType felsorolás egy eleme lehet, a leggyakoribb a DeviceType.Hardware, hiszen ez biztosítja a hardveres gyorsítást. Az AdapterFormat az elsődleges képernyőpuffer tesztelendő pixelformátuma, a BackBufferFormat a hátsó lapra szánt pixelformátum, mindegyik a Format felsorolás valamelyik tagja. Az utolsó paraméter pedig a Windowed ami TRUE, ha ablakos, és FALSE a teljes képernyős alkalmazás esetén. A megkülönböztetés azért fontos mert egyes pixelformátumok kizárólag teljes képernyős üzemmódban működnek, míg mások csakis ablakos alkalmazásokban. Példaprogramjainkban mind az első, mind a hátsó színpuffer esetén az asztal mindenkori pixelformátumát fogjuk alkalmazni, aminek a beállításait a mode változó tárolja.

A létrehozandó eszköz paramétereinek beállítása

A létrehozandó eszköz paramétereit egy „struktúra” tartalmazza, amelynek a helyes feltöltése létfontosságú. Hozzunk létre egy példányt a PresentParameters osztályból:

private PresentParameters presentParameters =
= new PresentParameters();

Ez a példány fogja tartalmazni az eszköz paramétereit, ezért most tekintsük át a PresentParameters adattagjait:
  • BackBufferWidth: a hátsó képernyőpuffer szélessége pixelben. Ablakos alkalmazások esetében a zérus érték azt jelenti, hogy a kliensablak szélességét érvényesítjük.
  • BackBufferHeight: a hátsó képernyőpuffer magassága pixelben. Ablakos alkalmazások esetében a zérus érték azt jelenti, hogy a kliensablak szélességét érvényesítjük.
  • BackBufferFormat: a hátsó lap pixelformátuma. Ablakos alkalmazások esetében használható a Format.Unknown érték, amivel azt jelezzük, hogy az asztal aktuális pixelformátumát szeretnénk átvenni.
  • BackBufferCount: a hátsó pufferek száma, általában kettő (dupla pufferelés)
  • Multisample: az elsimítás típusa, ami a MultisampleType felsorolás egy tagja. Az alapértelmezett érték a MultisampleType.None, viszont ha szeretnénk élsímitást a lapváltást SwapEffect.Discard értékre kell állítani.
  • MultisampleQuality: az élsimítás minősége, értéke nulla és CheckDeviceMultisampleType függvény által visszaadott (QualityLevels - 1) érték közötti tartományba tartozik.
  • SwapEffect: a lapváltás módja ami a SwapEffect felsorolás értékkészletéből kerül ki. A SwapEffect.Discard a legáltalánosabb és egyben a leghatékonyabb beállítás, nem használunk swappet, egyből megjelenítünk mindent.
  • DeviceWindowHandle: az alkalmazás ablakkezelője, beazonosítja az ablakot, ahol megjelenítjük a színteret.
  • Windowed: a TRUE értékkel azt jelezzük, hogy alkalmazásunk ablakos módban fut, ellenlező esetben pedig teljes képernyős alkalmazásról van szó.
  • EnableAutoDepthStencil: amennyiban TRUE értékre állítjuk akkor maga a Direct3D hozza létre és kezeli a mélység- és a stencilpuffert. Ellenkező esetben magunknak kell gondoskodnunk mindenről.
  • AutoDepthStencilFormat: a mélységpuffer pixelformátuma a Format felsorolás tagja. Kizárólag EnableAutoDepthStencil = true beállítás mellett érvényesül.
  • PresentFlags: további beállítások amik a PresentFlags felsorolás tagjai.
    FullScreenRefreshRateInHertz: a képernyő vízszintes frissítési frekvenciája. Ablakos üzemmódban értéke kötelezően nulla, teljes képernyős alkalmazás esetében pedig használjuk az mode.RefreshRate értékét.
  • PresentationInterval: a lapváltás maximális gyakorisáága. A leggyakoribb érték a PresentInterval.Default amikor is a függőleges visszatérés (vertical retrace) alatt történik meg a lapváltás. A PresentInterval.Immediate beállításnál azonnal végrehajtódik a lapváltás. A PresentInterval.One hasonlóan a Default értékhez a függőleges visszatérésre vár, de attól eltérően nem a rendszeridőt hanem az annál pontosabb és számításigényesebb timeBeginPeriod függvényt hívja meg.
Az eszköz elkészítése

Lassan de biztosan elérkeztünk a lényeghez, létrehozzuk a az eszközünket amelyen keresztül elérhetjük a videokártya szolgáltatásait. Az eszköz készítése a Device osztály példányosításából áll:

private Device device = new Device(direct3D, adapter,
DeviceType.Hardware, this.Handle, createFlags, presentParameters);

A Device konstruktorának át kell adni egy példányt a Direct3D osztályból, az adapter a megjelenítő sorszáma, DeviceType a már szokásos hardware, a controlHandle az ablak kezelője amelyhez eszközünket társítjuk. Ha a PresentParameters típus DeviceWindowHandle tagját érvényes értékre állítottuk akkor NULL értéket is állíthatunk ablakos alkalmazások esetében. A CreateFlags a leendő eszköz létrehozási opciói, érvényes értékeit CreateFlags felsorolás tartalmazza. A vertex-feldolgozás módja mellett itt jelezhető az is, ha az alkalmazást többszálú futtatásra szeretnénk felkészíteni (CreateFlags.Multithreaded). Végül a PresentParameters típus egy feltöltött példányát kell átadni ami az eszköz jellemzőit tárolja. A típus egyes adattagjai megváltozhatnak a hívás során. Ilyen általában a BackBufferWidth, a BackBufferHeight, a BackBufferCount valamint a BackBufferFormat.
Miután elkészültünk a DirectX eszközzel, nincs más hátra mint kirajzolni valamit a képernyőre ami nem is lehet más mint egy háromszög 2D-ben. Tudom, elég unalmas…

Rajzolás! Hurrá!

A rajzolás előkészítéseként definiálni kell egy struktúrát amiben eltároljuk a csúcspontok tulajdonságait azaz az attribútumokat. Az egyik legfontosabb attribútum a pozíció (x, y, z) ez triviális, de persze ennél sokkal több információ is csatolható egy vertexhez. Gondoljunk csak a színre, vagy a textúra koordinátára, stb. Nyilván való az is, miden feladatnak más-más vertex formátum felel meg ezért azt a programozónak szabadon kell tudnia változtatni. Nos, mi most egy elég alap szerkezetet hozunk létre, a pocizó mellet csak a vertex színét vesszük fel az attribútumok közé:

[StructLayout(LayoutKind.Sequential)]
public struct Vertex
{
public
Vector4 Position;
public int
Color;
}

A vertex struktúra felépítéséről viszont értesíteni kell a DirectX eszközt is, hiszen egyébként nem tudja helyesen értelmezni a kapott adatokat:

device.VertexFormat =
= VertexFormat.PositionRhw | VertexFormat.Diffuse;

A fenti beállítás azt mondja meg a Direct3D-nek, hogy a vertexeken nem kell már végrehajtani a transzformációs műveleteket (pl. forgatás, eltolás, stb) mivel eleve képernyő-koordinátákban szerepel a pozíció (PositionRhw). A VertexFormat Diffuse tagja pedig azt fejezi ki, hogy a pozíció mellett a vertexek színértéke foglal helyet. Röviden a definiált vertex formátumot a TransformedColored névvel illethetnénk. Persze ennél sokkal rugalmasabban is definiálhatunk vertex formátumot de erről majd később lesz majd csak szó. A Vertex struktúra felhasználásával hozzunk létre egy tömböt és töltsük is fel azt:
  private Vertex[] vertices = new Vertex[3];

private void OnResourceLoad()
{
vertices[0].Position = new Vector4((float)(0.50f * Width), (float)(0.25f * Height), 0f, 1f);
vertices[0].Color = Color.Red.ToArgb();

vertices[1].Position = new Vector4((float)(0.75f * Width), (float)(0.75f * Height), 0f, 1f);
vertices[1].Color = Color.Green.ToArgb();

vertices[2].Position = new Vector4((float)(0.25f * Width), (float)(0.75f * Height), 0f, 1f);
vertices[2].Color = Color.Blue.ToArgb();
}
Ezek után már valóban csak a rajzolás lépései vannak hátra ami azért elég hasonló az OpenGL-hez:

device.Clear(ClearFlags.Target | ClearFlags.ZBuffer,
Color.Black, 1.0f, 0);

A fenti sor törli a render Target-et és a Z-puffert, a háttérszínt pedig feketére állítja, vagyis a szín- és Z-puffer inicializálása történik meg. Meg kell adni még a Z-puffer kezdőértékét végül pedig a stencil puffer kitöltési értékét is, jelen esetben nem használjuk.

device.SetRenderState(RenderState.FillMode, FillMode.Solid);

A SetRenderState metódus segítségével beállítjuk a megjelenítés módját, vagyis, hogy pont, drótváz vagy kitöltött megjelenítést szeretnénk. Ehhez előszór a RenderState felsorolás FillMode tagját választjuk ki, majd a FillMode enum Solid elemét adjuk át. Ezután indítsuk el a jelenetet, vagyis a renderelést:
            //A jelenet indítása - függöny fel
device.BeginScene();
{
device.VertexFormat = VertexFormat.PositionRhw | VertexFormat.Diffuse;
device.DrawUserPrimitives(PrimitiveType.TriangleList, 1, vertices);
}
//A jelenet vége - függöny le
device.EndScene();
A device DrawUserPrimitives metóduson keresztül adjuk meg, hogy milyen primitív típust alkalmazzon a megjelenítés során (PrimitiveType.TriangleList). Példánkban a vertexeket háromszögek csúcspontjaként fogja kezelni a DX. Ezt követi a primitívek száma, majd a tömb ami a vertexeket tartalmazza. Ezután lezárjuk a jelenetet:

device.EndScene();

Majd következik a lapváltás - a hátsó puffer tartalma az elsődleges pufferre kerül, azaz megjelenik a képernyőn a háromszögünk:

device.Present();

Egyenlőre ennyiből tevődik össze a Render() metódusunk. Már csak meg kell hívnunk, tehát írjuk felül a Form OnPaint metódusát. Ez egy gyakori megoldás…de nem most!

Az alkalmazás főciklusa

Miután létrehozunk a Direct3D eszközt és megírtuk a Render() metódust következhet a megjelenítés. DirectX alkalmazások esetében a megjelenítést felelős Render() függvény a Run() függvényből hívódik meg folyamatosan, az alkalmazás futása alatt. A kérdés az, hogy miként valósítsuk meg a Run() metódust .NET-ben.
A legtöbb ablakos rendszert elsősorban a felhasználói reakciók fogadására és lekezelésére készítették fel és ez persze kihat az újrarajzolás és frissítés megvalósítására is. Egyszóval az OnPaint metódust nem arra tervezték, hogy folyamatosan hívogassuk és így valósítsuk meg a Run() metódust. Pedig a DirectX azt szereti, ha az adatok egyfolytában rendelkezésre állnak mivel a jelenetet folyamatosan megjeleníti függetlenül attól, hogy változott-e valami a színtérben vagy sem. A lényeg a folyamatosság és a gyorsaság! Ezt az ellentmondást nehéz feloldani. Az egyik lehetséges megoldás az, ha közvetlenül érjük el a Win32 szolgáltatásokat a menedzselt oldal teljes megkerülésével és így valósítjuk meg a Run() metódust. Szerencsére erre a SlimDX tartalmaz egy vékony wrappert amit a MessagePump osztályban találhatunk. Használata rendkívül egyszerű. Nyissuk meg a Program.cs fájlt és a Main() metódusban helyezzük el a következő sorokat:

var form = new MainForm();
MessagePump.Run(form, form.Render);

A MessagePump Run metódusának át kell adni az ablakunk egy példányát majd azt a metódust, ami a megjelenítést megvalósítja. Ezzel készen is vagyunk. Ha az ablakunkat bezárjuk illik felszabadítani (explicite) a létrehozott Direct3D és Device objektumokat, amit a következő megoldással automatikusan meg is tehetünk:

foreach
(var item in ObjectTable.Objects)
{ item.Dispose(); }

Ezzel meg is volnánk. A fenti megoldás egyszerű, gyors és takarékos, ráadásul kiválóan működik. Felmerülhet a kérdés, hogy mi is van a MessagePump osztályban? A "trükköt" az ötlet kitalálójától érdemes elolvasni amit meg is tehetünk itt: Tom Miller's Blog

Amikor az ablak mérete megváltozik…

Ha ablakos alkalmazást készítünk, akkor számolnunk kell azzal, hogy a felhasználó átméretezi az ablakot. Ez természetes, mi ezzel a gond? Csupán az, hogy a hátsó puffer méreteit az ablak méreteihez igazítottuk mivel arra rajzolunk. Amikor viszont az ablak mérete megváltozik, az nem vonja magával a hátsó puffer automatikus megváltoztatását így elég furcsa kép szörnyed ránk. A megoldás az, hogy ha az ablak mérete változik, újra beállítjuk a hátsó puffer szélesség-magasság tulajdonságait majd a DirectX eszközt reseteljük az új beállításokkal:
        protected override void OnResize(EventArgs e)
{
if (this.WindowState == FormWindowState.Minimized || isFullScreen == true) return;

OnResourceLoad();

if (device != null)
{
presentParameters.BackBufferWidth = this.ClientSize.Width;
presentParameters.BackBufferHeight = this.ClientSize.Height;

device.Reset(presentParameters);
}
}
Az ablakos és a teljes képernyős üzemmód között az ALT+ENTER billentyűkombináció segítségével válthatunk. A megvalósítás nem bonyolult mindenki implementálhatja maga is a fenti leírás alapján. A példa teljes forrása itt tölthető le:


És itt a végeredmény, amikor is megpendítjük a DirectX szörny bajszát a .NET Framework biztonsági ketrece mögül:

Amikor a SlimDX munkába lendül

Lassan végére érünk ennek a bejegyzésnek is. Sok mindennel nem foglalkoztunk, legfőbbként a DirectX eszköz elvesztésének esetével és annak lekezelésével, de majd mindenre sor kerül idővel. Remélem tudtam segíteni kicsit az elinduláshoz és itt meg kell jegyeznem, hogy Nyisztor Károly könyvei is sokat segítettek a bejegyzés írása közben.

0 megjegyzés :

Megjegyzés küldése