A következő címkéjű bejegyzések mutatása: Tutorial. Összes bejegyzés megjelenítése
A következő címkéjű bejegyzések mutatása: Tutorial. Összes bejegyzés megjelenítése

2013. november 30., szombat

AutoCAD 2013 SHOT


Kedves látogatók, örömteli hírrel szolgálhatok! Hosszú kemény munkával elkészült az első önálló teljes videó oktatósorozatom az AutoCAD 2013 rendszerről. Az általam készített 60 részes sorozat bevezeti a kedves hallgatót az AutoCAD csodálatos világába, egészen az első indítástól a bonyolult rajzok készítéséig. A sorozatot azoknak tudom jó szívvel ajánlani, akik teljesen kezdők és tanulmányaik sikeres teljesítéséhez szeretnének elmélyedni a világ egyik vezető CAD rendszerében. Természetesen nincsen mindenről szó, a fő cél az AutoCAD logikájának, működésének átadása. A videókat a YouTube-ra töltöttem fel, hogy mindenki szabadon elérje. Hogy lásd, egészen pontosan mit tartalmaz ez az oktató videó sorozat, leírom ide a teljes tartalomjegyzéket:
  1. AutoCAD 2013 SHOT - Bevezetés
  2. AutoCAD 2013 SHOT - Koordináta-rendszerek
  3. AutoCAD 2013 SHOT - Felhasználói felület
  4. AutoCAD 2013 SHOT - Fájlformátumok
  5. AutoCAD 2013 SHOT - Pont és pontstílus
  6. AutoCAD 2013 SHOT - Koordináta bevitel
  7. AutoCAD 2013 SHOT - Vonal rajzelem
  8. AutoCAD 2013 SHOT - Kijelölés és törlés (1)
  9. AutoCAD 2013 SHOT - Kijelölés és törlés (2)
  10. AutoCAD 2013 SHOT - Fogók és a használatuk
  11. AutoCAD 2013 SHOT - Raszter és Háló mód
  12. AutoCAD 2013 SHOT - Merőleges (Orto) mód
  13. AutoCAD 2013 SHOT - Poláris követés
  14. AutoCAD 2013 SHOT - Tárgyraszter
  15. AutoCAD 2013 SHOT - Tárgyraszter követés
  16. AutoCAD 2013 SHOT - Dinamikus adatbevitel
  17. AutoCAD 2013 SHOT - Gyorstulajdonságok
  18. AutoCAD 2013 SHOT - Vonallánc rajzolás alapok
  19. AutoCAD 2013 SHOT - Vonallánc rajzolás alparancsok
  20. AutoCAD 2013 SHOT - Téglalap rajzolás alapok
  21. AutoCAD 2013 SHOT - Téglalap rajzolás alparancsok
  22. AutoCAD 2013 SHOT - Kör rajzolás alapok
  23. AutoCAD 2013 SHOT - Kör rajzolás alparancsok
  24. AutoCAD 2013 SHOT - Körív rajzolás alapok
  25. AutoCAD 2013 SHOT - Körív rajzolás alparancsok
  26. AutoCAD 2013 SHOT - Ellipszis és Elliptikus ív rajzolás
  27. AutoCAD 2013 SHOT - Sokszög rajzolás
  28. AutoCAD 2013 SHOT - Spline görbe rajzolás
  29. AutoCAD 2013 SHOT - Szerkesztővonal és Sugár rajzelem
  30. AutoCAD 2013 SHOT - Gyűrű rajzolás
  31. AutoCAD 2013 SHOT - Színek és vonaltípusok
  32. AutoCAD 2013 SHOT - Fólia fogalma
  33. AutoCAD 2013 SHOT - Zoom eszközök
  34. AutoCAD 2013 SHOT - Mozgatás és Másolás
  35. AutoCAD 2013 SHOT - Párhuzamos és Merőleges szerkesztés
  36. AutoCAD 2013 SHOT - Tükrözés, Forgatás és Léptékezés parancsok
  37. AutoCAD 2013 SHOT - Letörés és Lekerekítés
  38. AutoCAD 2013 SHOT - Görbék simítása
  39. AutoCAD 2013 SHOT - Megtörés, Egyesítés és Szétvetés parancsok
  40. AutoCAD 2013 SHOT - Metsz és Elér parancs
  41. AutoCAD 2013 SHOT - Hosszabbít parancs
  42. AutoCAD 2013 SHOT - Felosztás és Beosztás
  43. AutoCAD 2013 SHOT - Négyszögletes kiosztás
  44. AutoCAD 2013 SHOT - Poláris kiosztás
  45. AutoCAD 2013 SHOT - Útvonalkiosztás
  46. AutoCAD 2013 SHOT - Egysoros szöveg
  47. AutoCAD 2013 SHOT - Bekezdéses szöveg
  48. AutoCAD 2013 SHOT - Szövegstílus
  49. AutoCAD 2013 SHOT - Távolság, sugár, szög, terület és pont mérés
  50. AutoCAD 2013 SHOT - Terület összeadás és kivonás
  51. AutoCAD 2013 SHOT - Sraffozás
  52. AutoCAD 2013 SHOT - Méretezési alapok
  53. AutoCAD 2013 SHOT - Méretezési stílusok
  54. AutoCAD 2013 SHOT - Méretszöveg kezelés
  55. AutoCAD 2013 SHOT - A blokk rajzelem
  56. AutoCAD 2013 SHOT - Attribútum
  57. AutoCAD 2013 SHOT - Dinamikus blokkok
  58. AutoCAD 2013 SHOT - Raszterkép beillesztése
  59. AutoCAD 2013 SHOT - Felhasználói koordináta-rendszer
  60. AutoCAD 2013 SHOT - Nyomtatás
Remélni tudom csak, hogy sok mindenkinek lesz segítségére ez az oktató anyag. Ha van bármilyen észrevételed azt semmiképen se hagyd magadban!

2012. június 2., szombat

ASP.NET MVC tananyag


Nem is olyan rég elhatároztam, hogy kicsit kikupálom magam ASP.NET MVC-ből, meg munkához is kellene, így elkezdtem kutakodni a neten egy jó kis leírás után. Pár perc guglizás után futottam bele Reiter István szakmai blogjába, ahol magyar nyelvre lefordítva letölthető Jon Galloway ASP.NET MVC MusicStore Tutorial című írása. Háááát… sok mindenre számítottam, de erre nem. Egy nagyszerű anyag, nagyszerű fordítása. Ezer köszönet érte, ritka ez a hozzáállás kis hazánkban. A tananyag innen tölthető le és mindenkinek csak ajánlani tudom:


Tartalmazza a teljes forráskódot, az eredeti angol változatot és nagyjából mindent, amire szükség lehet.
  1. Az MvcMusicStore codeplex oldala:
    http://mvcmusicstore.codeplex.com
  2. Online változat az ASP.NET oldalon (angol):
    http://www.asp.net/mvc/tutorials/mvc-music-store
Köszönjük István!

2011. december 23., péntek

Tao Start 17 - Síkra vetett árnyékok stencil buffer alkalmazásával

Lassan elérkezik az év vége és vele együtt az OpenGL cikksorozat is a végéhez ér. Egy hosszú utat tettünk meg az első háromszögtől egészen a síkra vetített árnyékok megjelenítéséig. A tutoriál háttéranyagát sokszor támogatta Szirmay-Kalos László (2003): Háromdimenziós grafika, animáció és játékfejlesztés című könyve. Ez egy kiváló könyv az érdeklődők számára. A hozzá tartozó egyetemi előadások itt tekinthetőek meg: link

2011. november 5., szombat

Tao Start 16 - Stencil-puffer és árnyékvetés síkra

Nem is olyan rég elkészítettük első árnyékvető algoritmusunkat. Emlékezetül: Tao Start 15 - Síkra vetített árnyékok. A síkra vetített árnyék elég látványos, de akad vele egy fő probléma. Ha a földet szimbolizáló sík véges kiterjedésű, mint a mi esetünkben, előfordulhat, hogy a vetített árnyék egy része „kilóg a semmibe”.

OpenGL Stencil-puffer

Az OpenGL elsajátítása közben számos buffer használatával meg kell ismerkednünk a megfelelő grafikai hatás eléréshez. Ezek közül a legismertebb - és a legtöbbet használt is - a z-buffer, aminek a működését és fontosságát nem kell magyarázni senkinek sem. Mégis akad egy olyan tárrész az OpenGL-ben, amit az elején a legtöbb lelkes tanuló nem tud hova tenni, pedig óriási szerepe van a különféle grafikai hatások elérésben. Ez a tár pedig nem más, mint a stencil-buffer. Mivel számomra is sokáig megfoghatatlan volt, álljon most itt egy kis elméleti ismertető róla. Később persze gyakorlati példákon keresztül is megnézzük a működését.

2011. október 2., vasárnap

WPF Alapok - Bevezetés

Egy ideje már tanulom a WPF-et  (Windows Presentation Foundation) és úgy gondoltam, hogy a kezdeti tapasztalataimat megosztom másokkal is. Mindezt azért teszem, mert ez a technológia teljesen lenyűgözött és szeretnék kis segítséget adni a Windows Forms-ról WPF-re való áttéréshez. Mert ez sajnos a legtöbbünknek nem fájdalommentes és főleg nem küzdelemmentes folyamat.

2011. augusztus 24., szerda

SlimDX9 08 - Fények

Elég régen írtam már a DirectX C#-ban való alkalmazásáról. Ennek egyetlen egy oka volt, mégpedig a lustaság. Persze az is igaz, hogy nem túl sokan érdeklődnek ezen téma iránt. Minden estre én szeretném mindenképpen folytatni a SlimDX tutorialt és később a DirectX11 képességeit is áttekinteni. Most pedig folytassuk ott, ahol valahol régen abbahagytuk, rágjuk át magunkat újra a fényeken...

2011. május 31., kedd

Tao Start 15 - Síkra vetített árnyékok

A háromdimenziós grafikus színterekben egy pixel színét az árnyalás és az árnyékolás együttes hatása alakítja ki. Eddigi OpenGL példáinkban csak az árnyalással foglalkoztunk. Az árnyalás a tárgyak képét alkotó pixelek színének kiszámítása a fényforrások és az objektum felületi színjellemzőinek figyelembevételével. Az árnyékolás alatt a tárgyak által a színtér más objektumaira vetett árnyékainak modellezését értjük. Ezt az OpenGL alapvetően nem támogatja. Ezért magunknak kell kidolgozni a szükséges eljárásokat.

2011. április 25., hétfő

Cg Shaderezés alapok

Ezzel a bejegyzéssel alámerülünk lassacskán a shaderek világába. De mik is azok a shaderek? Röviden a shaderek, olyan programok, melyeket a Graphical Processing Unit (GPU) hajt végre, vertex-eket illetve pixel-eket dolgoznak fel, és műveleteket hajtanak végre rajtuk. Vagyis olyan programocskák, amikkel a rögzített grafikus csővezeték egyes funkciót cserélhetjük le kedvünk szerint. Így sokkal realisztikusabb képet hozhatunk létre, azáltal hogy továbbfejlesztjük ezt az automatikus folyamatot, tehát létrehozzuk a mi shader programunkat.

2011. február 27., vasárnap

Tao Start 14 - Obj megjelenítő

OpenGL alapozó sorozatunk mérföldkőhöz érkezett. Ugyanis minden tudásunk meg van ahhoz, hogy egy összetett 3Ds modellt jelenítsünk meg virtuális világunkban. Ha visszagondolunk, akkor eddig csupán csak kockákat, gömböket és egyéb egyszerű objektumokat ábrázoltunk. Valljuk be őszintén, nem volt túlságosan izgalmas vagy látványos a végeredmény. De most ezzel szakítunk és kész 3D modelleket fogunk betölteni és megjeleníteni.

2010. december 12., vasárnap

Tao Start 13 - Mip-mapping

Sokszor előfordul, hogy ha egy textúrázott objektumhoz közelebb megyünk a virtuális világban, akkor gyakran megjelenítési hibák tűnnek fel. Ezek a problémák abból fakadnak, hogy csak egy rögzített méretű textúrát használunk, amelyek a mozgás hatására az OpenGL az objektum méreteinek megfelelően próbál megjeleníteni. Például, ha egy falra egy kis textúrát teszünk fel, majd a falhoz nagyon közel megyünk, akkor a texeleket külön-külön felismerhetjük. Ha azonban a falhoz közeledve a rögzített méretű textúrát egyre nagyobb felbontásúra cserélnénk le, akkor a textúra kép véges felbontása is kevésbé lenne észrevehető. Ezt a problémát a Mip-mapping használatával oldhatjuk meg, amely a textúrát több felbontásban tárolja el. Két egymást követő textúra felbontásának aránya egy a kettőhöz.

2010. szeptember 2., csütörtök

SlimDX9 04 - Anyagok és fényvisszaverődés

Legutóbb láttuk, hogy megvilágítással és fényhatásokkal jelentősen növelhetjük a megjelenített objektumok valóságosságát. Ehhez most hozzávesszük még az anyagtulajdonságokat, hogy még látványosabb hatásokat érjünk el. Az anyagok segítségével befolyásolhatjuk a beeső fény viselkedését. A SlimDX-ben a Material struktúra használható anyagok létrehozására. Adattagjain keresztül beállíthatjuk az anyag által visszavert fény tulajdonságait. A következő komponensek állnak rendelkezésre:
  • Diffuse (Color4): a terjedő fényvisszaverődést (diffuse light) határozza meg. Szórt megvilágítás és matt felületek esetében találkozhatunk a diffúz fényvisszaverődéssel.
  • Ambient (Color4): ez a komponens a szórt fény visszaverődését határozza meg adott anyaggal bevont felületről . A szórt fény mindenütt jelen van.
  • Specular (Color4): a fényfolt-visszaverődés jellemzőit állíthatjuk be általa. A sima, csiszolt felületekre az jellemző, hogy fény hatására fényes foltok képződnek rajtuk.
  • Emissive (Color4): ezzel az adattaggal a sugárzó felületek jellemzőit állíthatjuk be. A sugárzó felületek saját fényt bocsátanak ki, amelyet a színe jellemez.
  • Power (float, értéke [0,∞]): a komponenssel állíthatjuk be a fényfolt visszaverődés mértékét, minél nagyobb az értéke, annál élesebb és kisebb, míg kisebb értékek esetében elmosottabb és nagyobb kiterjedésű fényfoltot kapunk.
A DirectX-ben az anyag (material) gyakorlatilag a felületek fényvisszaverő, illetve fénykibocsátó tulajdonságait határozza meg. Gyakorlati példánkban három kockát jelenítünk meg, amelyeket a következő, egymástól eltérő anyagokkal látunk el:
  • téglaszerű, vörös, matt anyag, amely csak a terjedő fényt veri vissza;
  • sárga, erősen fényfolt visszaverő anyag;
  • ezüstös anyag, csillogó felület mintázására.
Az anyagokat a következő módon deklarálunk:

Material gRedMaterial, gYellowMaterial, gWhiteMaterial;

Az anyagok inicializálását a void InitMaterials() függvény végzi. Elsőként a téglaszerű anyagot definiáljuk:

gRedMaterial = new Material()
{ Diffuse = new Color4(1.0f, 1.0f, 0.5f, 0.2f) };

Csak a Diffuse komponenst állítjuk, ezáltal az anyag csak a terjedő fényt fogja visszaverni, tehát matt felületű lesz. A következő anyag sárga, erősen fényfolt visszaverő:
          gYellowMaterial = new Material()
{
// terjedő fényvisszaverődés beállítása
Diffuse = new Color4(1.0f, 0.5f, 0.5f, 0.0f),
// szórt fény visszaverődésének beállítása
Ambient = new Color4(1.0f, 1.0f, 1.0f, 0.5f),
// fényfoltvisszaverődés
Specular = new Color4(1.0f, 1.0f, 1.0f, 0.0f),
// éles, kis kiterjedésű fényfolt
Power = 80
};
Mivel minden egyes komponens esetében a vörös és zöld értékei érvényesülnek, végeredményül egy sárga anyagot kapunk. A Power érték kis kiterjedésű fényfoltot eredményez. A harmadik anyag ezüstös, csillogó anyagot, ezért a terjedő és fényfolt képző komponenseken van a hangsúly:
          gWhiteMaterial = new Material()
{
// terjedő fényvisszaverődés beállítása
Diffuse = new Color4(1.0f, 0.3f, 0.3f, 0.3f),
// szórt fény visszaverődésének beállítása,
// ezüstös anyag, teljes mértékben visszaveri a környezet fényeit
Ambient = new Color4(1.0f, 1.0f, 1.0f, 1.0f),
// sugárzó anyag
Emissive = new Color4(1.0f, 0.4f, 0.4f, 0.4f),
// fényfoltvisszaverődés
Specular = new Color4(1.0f, 1.0f, 1.0f, 1.0f),
// kiterjedtebb fényfolt
Power = 15
}
A jobb hatás érdekében ezüstös anyagunk saját fénnyel is rendelkezik, amely fokozza a ragyogást, a Power kisebb értéke pedig nagyobb fényfoltot eredményez. Természetesen a visszavert fényhez szükség van fényforrásra is. Ehhez létrehozunk egy direkcionális fényforrást, továbbá engedélyezzük a fényfolt képződést. Először is definiáljuk a fényforrás irányát, ami egy vektor lesz:

Vector3 dirVec = new Vector3(2.0f, 2.0f, -5.0f);

Normalizáljuk az irányvektort: dirVec.Normalize();

Ezután a Light struktúrát feltöltjük:
          Light light = new Light()
{
Type = LightType.Directional,
Direction = dirVec,
Diffuse = new Color4(1.0f, 1.0f, 1.0f, 1.0f),
Ambient = new Color4(1.0f, 0.1f, 0.1f, 0.1f),
Specular = new Color4(1.0f, 1.0f, 1.0f, 1.0f),
};
Mivel egy direkcionális fényforrást szeretnénk létrehozni – itt az irány és a szín adott, azonban a forrás helyzete nem meghatározott –, ezért kizárólag csak a típusát, az irányát, a diffúz és a fényfolt képző adattagokat kell beállítani. Ahhoz, hogy fényfoltok képződjenek, engedélyezni kell azt a következő hívással:

device.SetRenderState(RenderState.SpecularEnable, true);

A fényfolt képzés elég számításigényes feladat, éppen ezért alapból nincs is engedélyezve. A fenti beállításokon kívül természetesen még be kell kapcsolni a megvilágítást valamint meg kell adni a kockák felületeinek normálvektorait is. Ehhez, most is definiálunk egy saját vertex formátumot, amely a vertex koordinátákon kívül tartalmazza a normálvektorokat is:
  [StructLayout(LayoutKind.Sequential)]
struct Vertex
{
public Vector3 Position;
public Vector3 Normal;
public static readonly VertexFormat Format =
VertexFormat.Position | VertexFormat.Normal;

public Vertex(float vX, float vY, float vZ,
float nX, float nY, float nZ)
{
Position = new Vector3(vX, vY, vZ);
Normal = new Vector3(nX, nY, nZ);
}
}
A kockánkat pedig az OnResourceLoad() metódus fogja megvalósítani, amiben meghívjuk a InitMaterials() metódust és feltöltjük a vertices tömböt:
      private void OnResourceLoad()
{
InitMaterials();

vertices = new Vertex[]
{
// elülső oldal
new Vertex(-1.5f, +1.5f, -1.5f, 0, 0, -1),
new Vertex(+1.5f, +1.5f, -1.5f, 0, 0, -1),
new Vertex(-1.5f, -1.5f, -1.5f, 0, 0, -1),
new Vertex(-1.5f, -1.5f, -1.5f, 0, 0, -1),
new Vertex(+1.5f, +1.5f, -1.5f, 0, 0, -1),
new Vertex(+1.5f, -1.5f, -1.5f, 0, 0, -1),

// jobb oldal
new Vertex(+1.5f, +1.5f, -1.5f, +1, 0, 0),
new Vertex(+1.5f, +1.5f, +1.5f, +1, 0, 0),
new Vertex(+1.5f, -1.5f, -1.5f, +1, 0, 0),
new Vertex(+1.5f, -1.5f, -1.5f, +1, 0, 0),
new Vertex(+1.5f, +1.5f, +1.5f, +1, 0, 0),
new Vertex(+1.5f, -1.5f, +1.5f, +1, 0, 0),

// hátsó oldal
new Vertex(+1.5f, +1.5f, +1.5f, 0, 0, +1),
new Vertex(-1.5f, +1.5f, +1.5f, 0, 0, +1),
new Vertex(+1.5f, -1.5f, +1.5f, 0, 0, +1),
new Vertex(+1.5f, -1.5f, +1.5f, 0, 0, +1),
new Vertex(-1.5f, +1.5f, +1.5f, 0, 0, +1),
new Vertex(-1.5f, -1.5f, +1.5f, 0, 0, +1),

// bal oldal
new Vertex(-1.5f, +1.5f, +1.5f, -1, 0, 0),
new Vertex(-1.5f, +1.5f, -1.5f, -1, 0, 0),
new Vertex(-1.5f, -1.5f, +1.5f, -1, 0, 0),
new Vertex(-1.5f, -1.5f, +1.5f, -1, 0, 0),
new Vertex(-1.5f, +1.5f, -1.5f, -1, 0, 0),
new Vertex(-1.5f, -1.5f, -1.5f, -1, 0, 0),

// kocka teteje
new Vertex(-1.5f, +1.5f, +1.5f, 0, +1, 0),
new Vertex(+1.5f, +1.5f, +1.5f, 0, +1, 0),
new Vertex(-1.5f, +1.5f, -1.5f, 0, +1, 0),
new Vertex(-1.5f, +1.5f, -1.5f, 0, +1, 0),
new Vertex(+1.5f, +1.5f, +1.5f, 0, +1, 0),
new Vertex(+1.5f, +1.5f, -1.5f, 0, +1, 0),

// kocka alja
new Vertex(-1.5f, -1.5f, -1.5f, 0, -1, 0),
new Vertex(+1.5f, -1.5f, -1.5f, 0, -1, 0),
new Vertex(-1.5f, -1.5f, +1.5f, 0, -1, 0),
new Vertex(-1.5f, -1.5f, +1.5f, 0, -1, 0),
new Vertex(+1.5f, -1.5f, -1.5f, 0, -1, 0),
new Vertex(+1.5f, -1.5f, +1.5f, 0, -1, 0),
};
}
Mivel a kocka ezen oldala felénk néz, normálvektora merőleges a képernyő síkjára, és kifelé – azaz a szemlélő felé – mutat, ezért koordinátái (x,y,z) formában (0,0,-1) . Ezt a logikát kell követni a többi oldal esetében is. Végezetül tekintsük meg az eredményt:

Anyagok + fények + transzformációk

A teljes forrás letölthető innen:


A tutoriál írása során nagy hasznomra volt Nyisztor Károly: Grafika és játékprogramozás DirectX-szel c. könyve, amit mindenkinek nyugodt lelkiismerettel ajánlok!

2010. augusztus 28., szombat

2010. augusztus 26., csütörtök

SlimDX9 03 - És legyen világosság…

Legutóbb ott hagytuk abba a SDX leckénket, hogy létrehoztunk egy csonka gúlát a háromdimenziós térben és megforgattuk azt. Mivel nem definiáltunk normál vektorokat ezért a megvilágítást ki kellet kapcsolnunk, hogy valamit lássunk a gúlánkból. Persze fények nélkül a számítógépes grafika semmit sem ér, ezért most egy egyszerű példa segítségével belekóstolunk a fények világába.

Természetesen az első lépés a megvilágítás bekapcsolása, amit a következő sorral tehetünk meg:

device.SetRenderState(RenderState.Lighting, true);

A következő fontos dolog a megfelelő Vertex formátum megadása, ahol már helyet kapnak a normál vektorok is:
  [StructLayout(LayoutKind.Sequential)]
struct Vertex
{
public Vector3 Position;
public Vector3 Normal;
public int Color;
public static readonly VertexFormat Format = VertexFormat.Position | VertexFormat.Normal | VertexFormat.Diffuse;

public Vertex(float vX, float vY, float vZ, float nX, float nY, float nZ, int color)
{
Position = new Vector3(vX, vY, vZ);
Normal = new Vector3(nX, nY, nZ);
Color = color;
}
}
Ezután a modell létrehozása következik, ami ebben az esetben egy kocka. Itt a csúcspontok mellet, a normálvektorokat és a csúcspontok színértékét is megadjuk:
    private void OnResourceLoad()
{
int c1 = Color.Red.ToArgb();
int c2 = Color.Green.ToArgb();
int c3 = Color.Blue.ToArgb();

vertices = new Vertex[]
{
// elülső oldal
new Vertex(-1.5f, +1.5f, -1.5f, 0, 0, -1, c1),
new Vertex(+1.5f, +1.5f, -1.5f, 0, 0, -1, c1),
new Vertex(-1.5f, -1.5f, -1.5f, 0, 0, -1, c1),
new Vertex(-1.5f, -1.5f, -1.5f, 0, 0, -1, c1),
new Vertex(+1.5f, +1.5f, -1.5f, 0, 0, -1, c1),
new Vertex(+1.5f, -1.5f, -1.5f, 0, 0, -1, c1),

// jobb oldal
new Vertex(+1.5f, +1.5f, -1.5f, +1, 0, 0, c2),
new Vertex(+1.5f, +1.5f, +1.5f, +1, 0, 0, c2),
new Vertex(+1.5f, -1.5f, -1.5f, +1, 0, 0, c2),
new Vertex(+1.5f, -1.5f, -1.5f, +1, 0, 0, c2),
new Vertex(+1.5f, +1.5f, +1.5f, +1, 0, 0, c2),
new Vertex(+1.5f, -1.5f, +1.5f, +1, 0, 0, c2),

// hátsó oldal
new Vertex(+1.5f, +1.5f, +1.5f, 0, 0, +1, c1),
new Vertex(-1.5f, +1.5f, +1.5f, 0, 0, +1, c1),
new Vertex(+1.5f, -1.5f, +1.5f, 0, 0, +1, c1),
new Vertex(+1.5f, -1.5f, +1.5f, 0, 0, +1, c1),
new Vertex(-1.5f, +1.5f, +1.5f, 0, 0, +1, c1),
new Vertex(-1.5f, -1.5f, +1.5f, 0, 0, +1, c1),

// bal oldal
new Vertex(-1.5f, +1.5f, +1.5f, -1, 0, 0, c2),
new Vertex(-1.5f, +1.5f, -1.5f, -1, 0, 0, c2),
new Vertex(-1.5f, -1.5f, +1.5f, -1, 0, 0, c2),
new Vertex(-1.5f, -1.5f, +1.5f, -1, 0, 0, c2),
new Vertex(-1.5f, +1.5f, -1.5f, -1, 0, 0, c2),
new Vertex(-1.5f, -1.5f, -1.5f, -1, 0, 0, c2),

// kocka teteje
new Vertex(-1.5f, +1.5f, +1.5f, 0, +1, 0, c3),
new Vertex(+1.5f, +1.5f, +1.5f, 0, +1, 0, c3),
new Vertex(-1.5f, +1.5f, -1.5f, 0, +1, 0, c3),
new Vertex(-1.5f, +1.5f, -1.5f, 0, +1, 0, c3),
new Vertex(+1.5f, +1.5f, +1.5f, 0, +1, 0, c3),
new Vertex(+1.5f, +1.5f, -1.5f, 0, +1, 0, c3),

// kocka alja
new Vertex(-1.5f, -1.5f, -1.5f, 0, -1, 0, c3),
new Vertex(+1.5f, -1.5f, -1.5f, 0, -1, 0, c3),
new Vertex(-1.5f, -1.5f, +1.5f, 0, -1, 0, c3),
new Vertex(-1.5f, -1.5f, +1.5f, 0, -1, 0, c3),
new Vertex(+1.5f, -1.5f, -1.5f, 0, -1, 0, c3),
new Vertex(+1.5f, -1.5f, +1.5f, 0, -1, 0, c3),
};
}
A normálvektor egy olyan vektor, ami az adott háromszögre merőleges. Ennek ellenére normálvektort csak csúcsponthoz rendelhetünk hozzá.

Egy háromszög és a rá merőleges normál vektor

Ezután létre kell hozni egy fényforrást, ami jelen példánkban pontszerű lesz (Point light). A fényforrásokat a Light struktúra segítségével hozhatjuk létre:
           Light light = new Light()
{
Type = LightType.Point,
Range = 10000f,
Attenuation0 = 1f,
Diffuse = Color.White,
Position = new Vector3(-2, -2, +10)
};
A pontszerű fényforrás egyenletesen sugározz, a fény minden irányban haladhat. A pontszerű fényforrás definiálásakor meg kell adni a pozíciót (Position), a fény hatótávolságát (Range), valamint a fény intenzitásának távolsággal arányos csökkenését meghatározó együtthatót (Attenuation0, Attenuation1, Attenuation2). A létrehozott fényforrást be kell állítani:

device.SetLight(0, light);

Az első szám a Light Index, míg a második az index-el hivatkozható Light struktúra. Majd a beállított fényforrást engedélyezni kell:

device.EnableLight(0, true);

A fentieket a SetupLight() metódus foglalja össze:
       private void SetupLight()
{
device.SetRenderState(RenderState.Lighting, true);

Light light = new Light()
{
Type = LightType.Point,
Range = 10000f,
Attenuation0 = 1f,
Diffuse = Color.White,
Position = new Vector3(-2, -2, +10)
};

device.SetLight(0, light);
device.EnableLight(0, true);
}
Természetesen a fényekkel később ennél sokkal részletesebben fogunk foglalkozni, ez csak egy kis ízelítő belőle. A példa többi része már ismerős és könnyedén értelmezhető, eredménye pedig itt látható:


A teljes forrás pedig letölthető innen:


Most is meg kell jegyeznem, hogy Nyisztor Károly könyvei is sokat segítettek a bejegyzés írása közben.

2010. augusztus 22., vasárnap

Cloo kedvcsináló

A most következő írás egy kezdő lecke azok számára, akik valamilyen okból kifolyólag menedzselt környezetből szeretnék használni az OpenCL API-t. Az írás sghc_toma OpenCL kedvcsináló című cikkén alapszik, ezért az azzal való egyezés nem a véletlen műve. Ezer köszönet érte sghc_toma-nak. Kezdjünk is hozzá.

Az OpenCL számolás nagy vonalakban

Az OpenCL eszköz (compute device) az a hardver, amin a párhuzamos feldolgozás történik. Ezekben a hardverekben egy, vagy több számolási egység (compute unit) van, melyek egy, vagy több feldolgozó egységet (processing element) tartalmaznak – tulajdonképpen ezek hajtják végre az utasításokat. Például egy videokártya minden stream processzora, és egy CPU minden magja is egy feldolgozó egység, az nVidia videokártyák egy multiprocesszora pedig egy számolási egység.


A videokártyán futó programrészlet neve kernel kód vagy függvény. Az egyes feldolgozó egységek a kernel kód egy példányát futtatják, mindegyikük más-más adatokon. A kerneleket OpenCL-C nyelven (ISO C99 alapokon) lehet írni, és minden eszközre külön le kell fordítani őket. Az OpenCL eszközre írt kódokat természetesen nem kell egy darab függvénybe sűríteni, a kernel hívhat más függvényeket, illetve lehet több kernel is. A kernelek, a kiegészítő függvények, illetve a kernel által használt konstansok együtt egy programot alkotnak.
A kernelek egy úgynevezett kontextusban (context), környezetben futnak, mely magába foglalja a használható eszközöket, az általuk elérhető memória objektumokat, illetve a kernelek futtatásának ütemezését végző parancslistákat (command queue). A programot, amely létrehozza a kontextusokat, előjegyzi a kernelek futtatását, host programnak nevezzük, az őt futtató hardvert pedig a host eszköznek. A videokártya szempontjából a host (gazda) a CPU. A host kódból lehet memóriát lefoglalni a kártyán, feltölteni adattal, és a kernel kód futása után a host kódból lehet elérni a számolás eredményét. A kernel kódból ez a memória hozzáférhető. A kernel kódot úgy kell elkészíteni, hogy az a feladatnak csak egy részét végzi el; hogy melyik részét, azt beépített paraméterek adják meg. A feladat felosztását mi határozzuk meg, azaz kijelöljük a GPU számára a paraméter tartományokat, amelyekben végre kell hajtani a kernelt, és a GPU saját erőforrásait figyelembe véve maga beosztja, hogy melyik rész mikor és melyik multiprocesszoron fut le.
A fentieket egy egyszerű példán át könnyedén megérthetjük. Tegyük fel, hogy egy mátrixszorzást szeretnénk elvégezni az OpenCL-el. Ehhez a kernel kódban általánosan, sor és oszlop paraméterre leírjuk azt, hogy a szorzatmátrix adott sorában és oszlopában, hogy áll elő az eredmény. A kernel számára kijelöljük, hogy a sor paraméter egytől a sorok számáig, az oszlop paraméter egytől az oszlopok számáig kell, hogy terjedjen. A GPU tehát kap egy feladatot: sor × oszlop-szor végrehajtani a kernelt. A feladat végrehajtását optimálisan a GPU határozza meg magának.

A platform modell

Az OpenCL működése leírható négy modell segítségével:
  1. a platform modell
  2. a futtatási modell
  3. a memória modell
  4. a programozási modell
Az elsővel tulajdonképpen már megismerkedtünk: a host/device kapcsolatról van szó.

A futtatási modell

Mint már említettük, az OpenCL eszköz egy feldolgozó egysége a kernel egy példányát futtatja. Egy kernel-példányt munkaegységnek (work-item) nevezünk. Mikor elindítunk egy számolást, meg kell adnunk, hogy összesen hány munkaegységre lesz szükségünk. A munkaegységek összessége az index tér (index space), mely lehet 1, 2, vagy 3 dimenziós. A munkaegységek munkacsoportokba (work-group) szervezhetők. Ez azért fontos, mert az egy munkacsoportba tartozó munkaegységek között lehetséges szinkronizáció, és mindegyikük hozzáfér a csoport lokális memóriájához (erről később bővebben), míg ez a különböző csoportba tartozó egységekről ez nem mondható el.


Minden munkaegységnek van egy úgynevezett globális azonosítója (global ID), mely egyértelműen meghatározza annak helyét az index térben. Hasonlóan, minden munkacsoportnak is van egy azonosítója (work-group ID). A munkaegységeknek ezen felül van egy helyi azonosítója (local ID), mely a munkacsoporton belüli helyét határozza meg. A fentiekből következik, hogy a munkaegység pozíciója az index térben meghatározható a csoport azonosító és a helyi azonosító kombinációjával. Az index tér dimenzióinak maximális száma, az egyes dimenziókban a maximális méret, illetve egy munkacsoport maximális mérete eszközönként eltérő lehet, ezt figyelembe kell venni programozás közben! Az OpenCL API természetesen lehetőséget nyújt ezen adatok lekérdezésére.

A memória modell

A munkaegységek/kernelek által hozzáférhető memória négy típusra van osztva. A globális memóriához (global memory) az index tér minden egyes munkaegysége hozzáfér, azt írni és olvasni is tudják. Eszköztől függően a globális memória írása/olvasása lehet cache-elt. A konstans memória (constant memory) a globális memória egy olyan része, melynek tartalma nem változik kernelfuttatás közben, azt csak a host módosíthatja.


Minden munkacsoport rendelkezik egy lokális memória (local memory) területtel, melyet minden munkaegység a csoportban képes írni/olvasni. A privát memória (private memory) minden egyes munkaegységnek a sajátja, ő írni/olvasni tudja, de más munkaegység nem fér hozzá. A CUDA-t ismerőknek: érdemes odafigyelni az elnevezésekre, mert bár a két környezet felépítése hasonló, a terminológia nagyon nem. Például a CUDA-féle lokális memória az, amit itt privátnak neveznek, és ami itt lokális memória, az a CUDA-ban megosztott (shared memory).

A programozási modell

OpenCL-t használva kétféle párhuzamosítást érhetünk el: data, illetve task parallel módon programozhatunk. Az első az OpenCL fő profilja, ez jelenti azt, hogy sok kernel példány csinálja ugyanazt az index tér más-más elemein. Ha több, más feladatot végző kernelünk van, betehetjük őket egy parancslistába, és az OpenCL megtesz minden tőle telhetőt, hogy ezek optimálisan használják ki a hardvert. Ez utóbbi a task parallel módszer, hiszen egymástól független folyamatok futnak párhuzamosan.

OpenCL a gyakorlatban

Most hogy átrágtuk magunkat a száraz tényeken, következzen egy konkrét feladat megvalósítása OpenCL-ben. Először is szükségünk van egy wrapperre amivel elérhetjük az OpenCL API függvényeit .NET környezetből. Erre jelenleg az egyik legígéretesebb projekt a Cloo és az OpenCL.NET. Jelen bejegyzésben a Cloo-val fogunk megismerkedni mivel ez objektum orientált szemlélettel készült. Töltsük le a Cloo legfrissebb változatát majd indítsuk el a Visual Studio-t. Hozzunk létre egy konzolos alkalmazást majd a referenciákhoz adjuk hozzá a Cloo.dll szerelvényt. Ezután már csak a szükséges namespace-ket kell elhelyeznünk:

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Runtime.InteropServices;
using System.Diagnostics;
using Cloo;

Első programunk rendkívül egyszerű lesz, összeadunk két vektort és az eredményt eltároljuk egy harmadikban. Persze ezt a műveletet elvégezzük, mondjuk úgy 10 milliószor. Ha nem párhuzamos megvalósításban gondolkodnánk, akkor ezt valószínűleg egy ciklussal oldanánk meg, valahogy így.

for (int index = 0; index < n; index++) c[index]= a[index] + b[index];

A számoláshoz a vektorok i-edik elemeire van szükség - tehát az egyes számolások teljesen függetlenek egymástól, így a probléma szinte felkínálja magát párhuzamosításra. A terv az, hogy írunk egy kernelt, ami elvégzi a fenti műveletet egy vektor egy elemére, majd egy (n elemű vektorokkal számolva) n elemből álló index térre rászabadítjuk ezt a kernelt. Egyszerűen hangzik, mert az is.

A kernel

Első lépés a kernel kód vagy függvény elkészítése:

private static string code = @" kernel void VectorAdd(
global read_only float* a,

global read_only float* b,

global write_only float* c )

{
int index = get_global_id(0);
c[index] = a[index] + b[index]; }";

Ennyi az egész. A kernel kulcsszó - meglepő módon - azt jelzi, hogy az adott függvény egy kernel függvény. Egy kernel argumentumai a private névtérben kell legyenek, és alapértelmezetten oda is kerülnek. Ha egy argumentum mutató, megmondhatjuk, hogy a global, local és constant névterek melyikébe mutasson. A get_global_id(0) függvény az adott munkaegység globális azonosítójának első koordinátáját adja vissza. Erre azért van szükség, hogy tudjuk, hogy egy adott kernelpéldány a vektor hányadik elemének számolásáért felel. Mivel vektorokon dolgozunk, az index terünk egydimenziós, így valóban csak az első koordinátára van szükségünk. Ezután a kernel már csak elvégzi a megfelelő műveleteket a vektorok megfelelő elemein, és az eredményt visszaírja a harmadik vektor megfelelő elemébe.

Platformok

A számoláshoz szükségünk van egy OpenCL eszközre. Ahhoz, hogy létrehozhassunk egy eszközt, szükségünk lesz egy platform ID-re. Az elérhető platformok listáját a következőképpen szerezhetjük meg:

int platformNo = ComputePlatform.Platforms.Count;
ComputePlatform platform = ComputePlatform.Platforms[0];

Ez a kódrészlet az első ComputePlatform.Platforms.Count hívással kideríti az elérhető OpenCL platformok számát. A mellékelt példaprogramban az első elérhető platformot használjuk.

Eszközök

Ha megvan a platformazonosítónk (platform), lekérdezhetjük az elérhető eszközök listáját:

ReadOnlyCollection<ComputeDevice> devices = platform.Devices;

Ez nagyon hasonlít az előző kódrészlethez, a különbség annyi, hogy itt eszközökről szerzünk listát.

Környezet

Most, hogy megvan az eszköz, létre kell hozni egy környezetet a számoláshoz. Ehhez első lépésben készítünk egy környezet tulajdonság listát a platform felhasználásával:

ComputeContextPropertyList properties =
new ComputeContextPropertyList(platform);

Majd a tényleges környezet létrehozása a ComputeContext osztály segítségével történik:

ComputeContext context =
new
ComputeContext(devices, properties, null, IntPtr.Zero);

Az első paraméterben az elérhető eszközöket adjuk át, persze minden adott eszközhöz külön is kreálható környezet. Ilyenkor a ComputeDeviceTypes felsorolás tagjaiból választhatunk: Default, CPU, GPU, Accelerator, All. A következő a környezet tulajdonságait tartalmazó lista. Majd ezután, ha szükségesnek látjuk létrehozhatunk call-back függvényt amivel a hibakódokról értesülhetünk, de mi most ezt nem alkalmazzuk, ezért null.

Parancslista

Most már van környezetünk, amihez már csak hozzá kell adni egy parancslistát, ahová majd a végrehajtandó kerneleket pakoljuk.

ComputeCommandQueue commands =
new ComputeCommandQueue(context, context.Devices[0], ComputeCommandQueueFlags.None);

Ez a hívás a context.Devices[0] azonosítójú eszközhöz hoz létre egy parancslistát. Több parancslistát is létrehozhatnánk, de a kitűzött feladat elég egyszerű, egy lista is elég. Több lista esetén, amennyiben az egyes parancsok használnak közös objektumokat, figyelni kell a szinkronizálásra; erről bővebben olvashattok a OpenCL specifikációban. Még egy dolog a parancslistákkal kapcsolatban: több eszköz esetén mindegyiknek saját listára van szüksége!
Foglaljuk akkor most össze, mink van eddig. Kiválasztottunk egy OpenCL platformot, és erről a platformról egy eszközt, amin számolni fogunk. Ehhez az eszközhöz készítettünk egy környezetet, melyhez létrehoztunk egy parancslistát és írtunk egy kernel függvényt is. Most már csak pár dolgot kell végrehajtanunk: kellenek memória objektumok, amikben átadjuk a kernelnek a két vektort, illetve visszakapjuk az eredményt. A kernelfuttatáshoz szükségünk lesz egy kernel objektumra, amit csak egy, az adott eszközhöz felépített program objektumból nyerhetünk ki. Folytassuk tehát a munkát, készítsük el a program objektumot.

Program objektum

Programot készíthetünk forráskódból, illetve binárisból is. A gyorsabb inicializálás érdekében célszerű az első futtatáskor lefordítani a forrást, majd a kapott binárist (több OpenCL eszköz esetén az eltérő gépi kód miatt binárisokat) elraktározni, s következő alkalommal abból készíteni a program objektumot. Lássuk, hogy forrásból hogyan készítünk programot (a binárisból készítésre most nem térünk ki):

ComputeProgram program =
new ComputeProgram(context, new string[] { code });

Most már van egy programunk, de ez még csak a forráskódot tartalmazza, tehát le kell fordítanunk:

program.Build(null, null, null, IntPtr.Zero);

A fordítás állapotáról a program.GetBuildStatus(devices[0]) tulajdonságon keresztül kaphatunk információt. Ha minden rendben volt a ComputeProgramBuildStatus.Success enumot kapjuk vissza.

Kernel objektum

Miután sikeresen felépítettük a programot, kinyerhetjük belőle a kernel objektumot:

ComputeKernel kernel = program.CreateKernel("VectorAdd");

Mint látható, a kernel objektum gyártás elég egyszerű művelet, csupán egy lefordított programra, és a kernel függvény nevére van szükségünk hozzá.

Memória objektumok

Nos, van már kernelünk, amit tudnánk futtatni, csak egy probléma van: nincsenek adataink, amin dolgozhatunk. Ezért most létre kell hozni memória objektumokat, s feltöltjük őket a vektorainkkal. OpenCL-ben az eszköz memóriáját kétféle objektumon keresztül érhetjük el: puffer és kép (image) objektumok. A pufferekbe bármilyen adatot tehetünk, míg a kép objektumok kettő, vagy háromdimenziós képek tárolására alkalmasak. A kép objektumokkal most nem foglalkozom részletesebben, kitűzött célunk eléréséhez nincs is szükség rájuk, a vektorokat pufferekben tároljuk:

ComputeBuffer<float> a = new ComputeBuffer<float>(context, ComputeMemoryFlags.ReadOnly | ComputeMemoryFlags.CopyHostPointer, arrA);
ComputeBuffer<float> b = new ComputeBuffer<float>(context, ComputeMemoryFlags.ReadOnly | ComputeMemoryFlags.CopyHostPointer, arrB);
ComputeBuffer<float> c = new ComputeBuffer<float>(context, ComputeMemoryFlags.WriteOnly, arrC.Length);

Ez a három függvényhívás létrehozza az adott kontextusban az a, b és c vektoroknak megfelelő puffereket. Az a és b vektort csak olvasni fogjuk, így a ComputeMemoryFlags.ReadOnly flaggel hozzuk létre, míg a c vektorhoz szükség van a ComputeMemoryFlags.WriteOnly flagre, hiszen abba írjuk majd a végeredményt. Lehetőség van a puffer készítésekor megadni egy host pointert (negyedik paraméter), és az adatot, amire mutat, rögtön felmásolni a pufferbe. Ehhez szükséges megadni a ComputeMemoryFlags.CopyHostPointer flaget. Puffert létrehozhatunk a host memóriájában is (ComputeMemoryFlags.AllocateHostPointer), illetve felhasználhatunk már lefoglalt host memóriaterületet is (UseHostPointer).

Kernel futtatás

OK, már majdnem kész vagyunk, már csak futtatnunk kell a kernelt. Egyszerűen hangzik, de azért ez nem csak annyiból áll, hogy meghívunk egy függvényt, aztán fut. Be kell állítani a kernel-argumentumokat, meg kell határozni a munkacsoportok és munkaegységek számát, és elő kell jegyezni a kernelt a parancslistában.

kernel.SetMemoryArgument(0, a);
kernel.SetMemoryArgument(1, b);
kernel.SetMemoryArgument(2, c);

Ezek a függvény hívások állítják be a kernel argumentumokat. Az egyes argumentumokat a kernel függvény fejlécében elfoglalt helyük alapján azonosítjuk, ez a szám a SetMemoryArgument függvény első paramétere.

ICollection<ComputeEvent> events =
new Collection<ComputeEvent>();
commands.Execute(kernel, null, new long[] { count }, null, events);

Most már csak meg kell hívnunk a commands parancslista Execute() metódusát megfelelően felparaméterezve. Az első paraméter a kernel objektum, a második a GlobalWorkOffset amit általában null. A GlobalWorkSize vagyis a munkaegységek száma megegyezik a vektorok méretével. Azt, hogy ez hogyan van felosztva munkacsoportokra (LocalWorkSize), most teljesen mindegy (az egyes vektor elemek teljesen függetlenek egymástól, a munkaegységeknek nincs szüksége közös memóriára), ezért az OpenCL-re bízzuk.
Minden olyan függvény, ami a parancslistához ad elemeket, kér egy ComputeEvent típusú lista pointert (utolsó paraméter). Az így visszaadott ComputeEvent-tel lehet lekérdezni a parancs aktuális állapotát (pl. végzett-e?), illetve más parancsok várólistájához lehet adni a parancsot. A várólistában szereplő összes parancsnak le kell futnia, mielőtt az adott parancs lefuthatna. Nos, ha minden jól ment, ezen hívás után már fut is a kernelünk.

Eredmény olvasása

A számolás eredménye persze az eszköz memóriájában van, tehát ki kell olvasnunk onnan. Ehhez persze biztosnak kell lennünk benne, hogy a számolás befejeződött. Ezt a commands.Finish() függvénnyel biztosíthatjuk, ami csak akkor tér vissza, ha a paraméterként megadott parancslista minden parancsa befejeződött. Ezután még figyelembe kell vennünk azt is, hogy a host memóriájában menedzselt az az objektum amibe bele kívánjuk másolni a számítás eredményét és így meg kell védeni a GC-től.
A menedzselt objektumok nem-menedzselt kódból történő elérésének egyik módja, hogy használjuk a GCHandle struktúrát. A struktúra legfontosabb tulajdonsága, hogy akkor sem engedi a menedzselt objektumot kisöpörni a memóriából, ha már csak egy nem-menedzselt objektum hivatkozik rá (ellenkező esetben a memóriaterület felszabadul). Azt is megakadályozza, hogy az adott cím egy más szegmensre tolódjon a memórián belül. Az azonban lényeges, hogy a nem-menedzselt kódnak rendelkeznie kell a megfelelő jogosultságokkal ehhez, melyet a SecurityPermission osztály segítségével adhatunk meg.

GCHandle arrCHandle = GCHandle.Alloc(arrC, GCHandleType.Pinned);

Az eredmények host memóriába olvasása:

commands.Read(c, false, 0, count,
arrCHandle.AddrOfPinnedObject(), events);
commands.Finish();

Amikor ez lefut, a c memória objektum tartalma a host memóriában lévő arrC vektorba másolódik. Az arrCHandle AddrOfPinnedObject() metódusa kinyeri a szükséges címet. A pufferekbe írásnál kimaradt, most megemlítem: ha a második paraméter true, az olvasás/írás befejeztéig nem tért vissza a függvény, ha false, nem várja meg a műveletek befejeztét. Végül:

arrCHandle.Free();

Ezuán már a számítás eredménye elérhető az arrC tömbben és azt tehetünk vele amit csak akarunk. Végeztünk a számolással, de még nem fejeződött be a dolgunk. Illik eltakarítani magunk után, szabadítsuk fel az OpenCL erőforrásokat:

a.Dispose();
b.Dispose();
c.Dispose();
kernel.Dispose();
program.Dispose();
context.Dispose();
commands.Dispose();

Végszó

Elsőre kicsit bonyolultnak tűnhet a dolog, de higgyük el, hogy megéri a kínlódás mivel így új távlatok nyílhatnak meg előttünk. De még mielőtt elbúcsúznánk érdemes egy kis teljesítmény tesztet végrehajtani a klasszikus .NET for ciklus és az OpenCL/Cloo (GeForce 8800 GTS) páros között. Az eredmények a lenti ábrán láthatóak:

.NET for vs. Cloo

Hát igen, nem valami meggyőző. Nagy elemszámnál már a Cloo átveszi a vezetést, de nem sokkal. Mi lehet ennek az oka?! Először is a .NET nem is olyan lassú, mint hinnénk, másodszor pedig az OpenCL API-t nem tisztán értük el, hanem a Cloo wrapperen keresztül, így egy kis overhead mindig is lesz. Ez főleg akkor megy a teljesítmény rovására, ha a kernel program olyan apró és egyszerű, mint a mi VectorAdd példánk. Jóval nagyobb és összetettebb feladatok esetén már jóval nagyobb különbség is elérhető a CPU és a GPU között. Remélem érthető volt ez kis példa és talán meghozza a kedvet egy kis videokártya-programozáshoz. A teljes forrás letölthető: innen

2010. augusztus 19., csütörtök

SlimDX9 02 - Irány a 3D!

Az első SlimDX-es programunkban egy kétdimenziós háromszöget jelenítettünk meg. Megismerkedtünk a DX alapjaival, ideje hát tovább lépni a harmadik dimenzióba. Ebben a példában egy gúlát fogunk megforgatni a térben.
Fontos megjegyezni, hogy 3D-ben nem lehet csak úgy vaktában renderelni, hiszen meg kell mondanunk, hogy milyen nézőpontból, látószöggel, stb. szeretnénk lefényképezni a modellteret. Hiszen ebben a modelltérben van egy négyzet, mint mondjuk az asztalunkon a valóságban, ezt pedig egy fényképezőgéppel (ezt most mi kamerának fogjuk hívni) akárhonnan lefényképezhetjük.

A virtuális világ "lefényképezése"

De annak a fényképezőgépnek a lencséje állítható, így lehet, hogy a beállításai miatt például egy kört ellipszisnek fényképez, stb. Ezért kell nekünk is megadnunk egy kamerát a modelltérben, az elhelyezkedését, valamint a beállításait. Ezen a kamerán át látható képet fogjuk mi a monitorunkon át látni. A Direct3D ezt úgy oldja meg, hogy tárol három mátrixot: a modelltérbeli transzformáció mátrixot, a nézeti mátrixot illetve a projekciós, azaz vetítési mátrixot. Ezeken a mátrixokon a modelltérbeli pontjaink „végigfolynak”, így kerülnek a képernyőre (persze egyéb effektusok után). Így a ebben a leckében főleg ezekkel a bealításokkal fogunk foglalkozni.
DirectX-ben egy pontot három koordinátájával adjuk meg, tehát ismernünk kell a Direct3D-ben használt koordinátarendszert. Mi most a balkezes (Left-handed) koordinátarendszert fogjuk használni, ami azt jelenti, hogy az X-tengely jobbra mutat, az Y-tengely felfele, a Z-tengely pedig a képernyőbe befelé:

Bal- és jobbsodrású koordináta rendszerek

Természetesen az OpenGL jobbkezes (Right-handed) rendszert alkalmaz, ezt érdemes megjegyezni. Ekkor már csak egy kérdés maradt: hogyan tudjuk ezt a három dimenziót leképezni két dimenzióra?

Projekció-, nézet- és világtranszformáció

Elméletben már ismerjük a képszintézis főbb elemeit, most a helyi, kamera, világ nézet és a vágás végrehajtásához szükséges gyakorlati alapismereteket tekintjük át. A képszintézis szakaszaihoz tartozó transzformációkat a Direct3D mátrixokkal valósítja meg. A mátrixműveletek lehetővé teszik a transzformációk összekapcsolást, így egy mátrixban több transzformáció is megadható. A megfelelő transzformációkat a device SetTransform függvény meghívással valósíthatjuk meg. A State paraméterben megadhatjuk a transzformációt (a képszintézis lépést), a második paraméter pedig maga a transzformációs mátrix:
  • Világ: TransformState.World
  • Kamera: TransformState.View
  • Projekció: TransformState.Projection
Először is nézzük meg mit is jelentenek az említett transzformációk. Világ nézeti transzformáció (World transform), ezzel a transzformációval helyezzük el objektumainkat a jelenetünk szintetikus világában. Nézettranszformáció (View transform), ezzel megadjuk a kamera helyzetét, vagyis azt a pontot ahonnan a jelenetet szemléljük. Projekciós transzformáció (Projection transform), ezzel határozzuk meg a vetítés paramétereit, amelynek eredményeként a képernyőnk kétdimenziós felületén megjelenő objektumok háromdimenziósnak tűnnek.
A fenti transzformációk beállítására a DirectX hasznos függvényeket biztosít. Kezdjük talán a leglátványosabbal, a projekciós transzformációval. Hozzunk létre egy mátrixot, aminek az elemeit a PerspectiveFovLH() függvény segítségével kényelmesen fel tudjuk tölteni.

A nézeti gúla és paramétereinek értelmezése

A paramétereknél megadjuk a látótér szögét (Field of View), méretarányát (Aspect Ratio) valamint az elülső és a hátsó vágósíkok z-koordinátáját (z-front és z-back):

Matrix projection = Matrix.PerspectiveFovLH((float)Math.PI/4,
(float)ClientSize.Width / (float)ClientSize.Height, 1.0f, 100.0f);

Lényeges tudnunk, hogy a látószöget nem fokban, hanem radiánban kell megadni. Használjuk a Math.PI előre definiált értékét, egy elfogadható FoV (Field of View) a Math.PI / 4. A projekciós transzformációt a device.SetTransform() hívással léptethetjük életbe, amelynek első paramétere a TransformState.Projection érték, a második pedig a projekciós transzformációs mátrix:

device.SetTransform(TransformState.Projection, projection);

Az elülső vágósík koordinátája 1.0f – tehát az ennél közelebb helyezkedő objektumok nem láthatóak. Természetesen mindig a kamera pozícióhoz képest kell értelmezni a közeli és a távoli vágósík értékeit.
A háromdimenziós tér legnagyobb előnye, hogy objektumaink mozoghatnak benne, tehát be kell állítani a világ transzformációkat. Példánkban adott tengely körüli elforgatást valósítunk meg, amit szerencsére ugyancsak függvényekkel támogat a DX:

device.SetTransform(TransformState.World, Matrix.RotationAxis(
new Vector3
(
angle / ((float)Math.PI * 2.0f),
angle / ((float)Math.PI * 4.0f),
angle / ((float)Math.PI * 6.0f)
),
angle / (float)Math.PI));

A TransformState.World paraméter megadásával jelezzük, hogy a világtranszformációt akarunk beállítani. A Matrix.RotationAxis() függvény segítségével megkaphatjuk azt a mátrixot aminek a segítésével a tetszőleges tengely körüli elforgatást valósíthatjuk meg. A tengely egy Vector3 típus definiálja, az elforgatás szögét pedig az angle változó tárolja.
Hátra van még a kamera pozíciójának beállítása, amin keresztül a jelentet szemléljük. Ez a kamera a térben bárhol lehet, és bármerre nézhet. Tehát ebből a kamerából látható kétdimenziós képre vagyunk kíváncsiak, azaz a csúcspontban lévő megfigyelő milyen képet lát a kamera síkján. A DirectX a kamerát három jellemzővel adja meg. Ez a három jellemző egy-egy vektor a modelltérben: a kamera helyzete (Eye), az a pont, amire a kamera néz (Target) és a felfele irány (Up). Ebből természetesen ki tudjuk számolni a kamera nézési irányát: Direction=Target-Eye, ami egy egyszerű kivonás vektorok közt. Ezek ismeretében már tudunk egy koordináta-rendszert illeszteni a kamerára is. A DX Matrix.LookAtLH függvény egy bal sodrású (Left Handed, LH) nézeti mátrixot tölt fel a megadott vektorok alapján:

Matrix view = Matrix.LookAtLH(
new Vector3(0, 0, zoom),
new Vector3(),
new Vector3(0, 1, 0));

device.SetTransform(TransformState.View, view);

Számunkara a kamera helyzetét meghatározó mátrix különösen fontos mivel a kamerapocizó módosításával egy fajta zoom hatást valósítunk meg. A zoom értékét a fel és a le billentyűkkel növeljük illetve, csökkentjük. A transzformációkat összefogva a void SetUpCamera(float zoom) metódus valósítja meg amit a Render() metódusban hívunk meg. Az előbbi transzformációkat együttesen a következő ábra mutatja be szemléletesen:

A DX rögzített grafikus csővezetéke

Látható, hogy az egyik oldalon belépnek a vertexek, végigfolynak a transzformációs műveleteken, majd végül a DX gépezet kiköpi a másik oldalon a rasztert, vagyis a képernyőre kerülő képet. Ezt folyamatot szépen grafikus csővezetéknek nevezik aminek csak egy eleme transzformációs modul. A hagyományos (Fixed Function Pipeline) főbb elemei a következők:
  • Transzformáló modul: A modul négy módosítható állapotregisztert tartalmaz: nézeti transzformáció mátrixa, modellezési transzformáció mátrixa, projektív transzformáció mátrixa valamint a képernyő koordináta-rendszerébe vivő transzformáció leírása (viewport). Tetszőleges vetítést (párhuzamos, perspektív) megvalósító mátrixot is támogat. Ahogyan a neve is mutatja, a geometria transzformációival foglalkozik, emiatt geometriai modulnak is hívják.
  • Árnyaló modul: Ez a modul a megvilágítással (árnyalással) és szín információkkal kapcsolatos számításokat végez. Egy veremszerű struktúrát használ fel, az aktuális fényforrások rekordjainak kezeléséhez. Ambiens, irány, pontszerű és szpot fényforrások megvalósítását támogatja.
  • Raszterizáló modul. Ez a modul az előbb említett két modul kimenetét használja fel a virtuális világ megjelenítéséhez. A raszterizáló modul végzi a 3D képszintézist (a színbufferbe rajzolást) a Direct3D-ben. Raszter műveletek, mint huzalváz, tömör kitöltési módok és textúra térképek is ebben a modulban vannak definiálva.
A hagyományos, huzalozott "transformation and lighting" (T&L) motort, rögzített műveleti sorrendű csővezetéknek nevezik, mivel a fejlesztő nem avatkozhat bele, nem módosíthatja a csővezeték működését, csupán vezérelheti. Innen ered a "fixed function pipeline” elnevezés. Felmerülhet a kérdés, hogy mi van akkor, ha a programozó nem elégedett a csővezeték működésével? A válasz egyszerű, semmi, ez van, ezt kell szeretni. Persze ez nem elfogadható válasz egy programozó számára így elég hamar (ez viszonylagos) megjelent a programozható csővezeték. És itt lépnek a képbe a shaderek, de nem menjünk ennyire elébe a dolgoknak. Ma már a merev csővezeték elavultnak számít, de ne higgye azt senki, hogy ettől el kell azt felejteni! Sőt! Igencsak hasznos, főleg kezdők számára.
Most, hogy megismerkedtünk a grafikus csővezeték fogalmával térjünk vissza pár gondolat erejéig az első példaprogramunkhoz. Első művünk egy háromszög volt képernyő koordinátarendszerben (2D) megadva. Itt elegendő lett volna egy vertexet két koordinátával megadni (x,y), de nem azt tettük, hanem négy koordinátát azaz Vector4 struktúrát használtunk. A z koordináta egyértelműen 0, míg a negyedik homogén tag (w) mindenhol 1 volt. Miért bonyolítottuk így meg a dolgokat?
Először is a DirectX alapvetően a 3D-re van felkészítve, gondoljunk csak a csővezetékre. Ezért, ha azt akarjuk, hogy vertex-ünk ne menjen keresztül a transzformációs folyamatokon, akkor azt közölni kell a DX-el. Ezért definiáltuk, úgy a vertex formátumát, hogy: VertexFormat.PositionRhw. A PositionRhw jelentése egyszerűen az, hogy ezek a vertexek már fel vannak dolgozva, azaz transzformáltak, koordinátáik pixelben értendőek. A transzformált vertexek pedig mindig homogén vágási térben helyezkednek el! Ezért szükséges a Vector4 struktúra. A struktúra x és y tagja maga a képernyő koordináta, a z koordináta természetesen teljesen jelentéktelen, csupán egy esetleges z-buffer esetén meghatározza a takarást, végül következik az rhw tag (reciprocal of homogenous w) aminek az értéke 1 szokott lenni. Ezek után jöjjön az új vertex formátumunk amivel már 3D-s vertexeket adhatunk meg:

[StructLayout(LayoutKind.Sequential)]
public struct Vertex
{
public Vector3 Position;
public int Color;
public static readonly VertexFormat Format =
VertexFormat.Position | VertexFormat.Diffuse;
}

Láthatjuk, hogy most már Vector3 struktúrát használunk és a vertex formátum simán Position vagyis térbeli és nem transzformált vertexről van szó! Ebből fogja tudni, a DX hogy milyen műveleteken kell átesnie ezeknek a vertexeknek, hogy a képernyő megjelenjenek. A pozíció mellet a vertex színét is eltároljuk. A VertexFormat.Diffuse azt jelenti, hogy minden vertex kap egy színt is. Ez azt fogja jelenteni, hogy vertex színe a megadott lesz, a primitíven belül pedig a vertexek között folyamatos színátmenet lesz. Látható, hogy több tulajdonságot ’vagy’ kapcsolattal adhatunk meg. A VertexFormat formátumot célszerű magában a Vertex struktúrában megadni Format néven, így nem kell fejben tartanunk annak megadását a renderelés során. A struktúra elején lévő attribútum [StructLayout(LayoutKind.Sequential)] azt mondja meg, hogy az adattagok folytonosan jönnek egymás után.

A jelenet geometriája

A jelenet három dimenziós testekből álló tér. Alapvetően minden testet a pontjaival (vertex) adunk meg. Hogy a megadás módja egyértelmű legyen, ezért egy nagyon egyszerű módszert használunk: minden testet háromszögekre bontunk, ez lesz egy primitív, hiszen tényleg a legegyszerűbb síkidom, aminek területe van. Ezeket a háromszögeket a pontjaival adunk meg, ami egyértelmű, hiszen a három pont akármelyik sorrendben megadva egyértelműen jellemzi a háromszöget. A háromszög pontjainak van egy jellegzetes sorrendje: ha ránézünk a háromszögre, és úgy a pontjait olyan sorrendben adtuk meg, hogy jobb irányban haladnak, akkor pozitívan irányított (DirectX-ben). Azaz pl.:

Irányított vertex megadás

Itt például 0,1,2 pozitívan irányított, viszont 0,2,1 nem. Tehát sikerült megadni egy háromszöget, megismerve az irányítását is. Ha minden háromszöget ennek megfelelően adunk meg akkor bekapcsolhatjuk a hátsó lapok eldobását, hiszen a pozitív z koordinátájú háromszög normálisok (normálvektor, ami ugye merőleges a háromszög síkjára) azt jelentik, hogy az a háromszög a test túloldalán van, azaz nem látszik. Ezért is beszéltünk a pozitív körüljárásról, hiszen csak azok a háromszögek fognak látszani, melyek az aktuális nézőpontból pozitív körüljárásúak. Tehát ez a takarási munka egyszerű:


A hátsó lapok eldobását a következő sorral kapcsolhatjuk be:

device.SetRenderState(RenderState.CullMode, Cull.Counterclockwise);

Ekkor a rendszer minden poligonhoz normálvektort számol és ennek és a nézőpontnak (nézési irány alapján) veszi a közbe zárt szögét és ez alapján dönti el, hogy hátsó lap-e. Nyilván, ha a normálvektor felénk mutat, akkor hegyes szög lesz, ha meg ellenkező irányba, akkor tompaszög. Mivel nagyjából minden második poligon hátsó lap így akár megduplázhatjuk a program sebességét.
Ezek után már csak fel kell tölteni a vertices Vertex tömböt a szükséges adatokkal (gúla csúcspontjai), amit az OnResourceLoad() metódus meg is tesz.
Mielőtt még elkezdenénk a renderelést ne felejtsük el kikapcsolni a fényeket. Ha ezt nem tennénk meg, akkor a gúlából semmi sem látszódna a fekete háttér előtt. A fények kikapcsolását a következő sor engedélyezi:

device.SetRenderState(RenderState.Lighting, false);

Ezek után már nincs más hátra, mint gyönyörködni az első valódi 3D-s DX-es programunkba, aminek az eredménye itt látható:

Hello Pyramid!

Természetesen ebben a leckében sem tértem ki mindenre mivel a forrás tartalmaz megjegyzéseket így a hiányzó részek megértése házi feladat. A példa teljes forrása itt tölthető le:


Most is meg kell jegyeznem, hogy Nyisztor Károly könyvei is sokat segítettek a bejegyzés írása közben. Valamint felhasználtam Crusader DX leckéit is.