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.

A GPU árnyalós oldala
Kezdetben a csúcspont- és pixel-árnyalók (shaderek) programozására assembly jellegű nyelvet használtak. Persze később megjelentek (2002-ben) a magasabb szintű árnyaló nyelvek is. Ezek olyan nyelvek, amelyek hasonlítanak a sokak által ismert, C, Pascal stb. programozási nyelvekhez, amelyek már rendelkeznek előre megírt eljárásokkal, melyekkel újabb eljárások készíthetőek és ezt az "eljárásmaszlagot" egy fordító (compiler) alakítja át assembly alapú mikroprogrammá. Az NVIDIA 2002 júniusában jelentette be a Cg (C for Graphics) programnyelv specifikációját, amelynek segítségével a fejlesztők sokkal gyorsabban és könnyebben készíthetnek DirectX és OpenGL kompatibilis grafikus alkalmazásokat. Maga a Cg nyelv az NVIDIA és a Microsoft szoros együttműködésének gyümölcse. A Cg egy C-hez hasonló szintaxissal rendelkező, a grafikus platformtól független magas szintű programnyelv, amely az eddigieknél lényegesen könnyebbé teszi a shader programok fejlesztését. A Cg programok nem csak NVIDIA, hanem az ATI/AMD és más gyártók videokártyáin is futtathatók. Magáról a Cg nyelvről igen hasznos további információkat olvashatunk az ELTE Programozási Nyelvek portálján.



A nyelv Microsoft-féle implementációja a High Level Shading Language, röviden HLSL. Nyelvi elemekben csak kisebb eltérések vannak a két nyelv között, fő különbséget a felhasználhatósági körük jelenti. Míg a HLSL csak DirectX-szel, így Windows-on és Xbox 360-on használható, a Cg OpenGL alól is használható. Emiatt a Cg nyelven írt shader-eket futtató programok használhatók Windows, Linux, Mac OS X operációs rendszereken és Xbox 360 konzolokon is. Az OpenGL Architecture Review Board-nak is létezik GPU-kat célzó programozási nyelve, GLSL néven. Ez a nyelv jobban különbözik a Cg-től, mint az a HLSL-től, valamint csak az OpenGL API-val használható.
Mivel a shader programok minden esetben valamilyen CPU-n futó program segítségével futnak, ezért a parancssori fordítás mellet lehetőség van a főprogram futása közben, a Cg Runtime Library függvényeinek hívásával fordítani a programot. A fordításhoz mindkét esetben meg kell adni a shader belépési pontjaként használandó függvény nevét, és a használni kívánt profilt. A profilok nyelvi szintű használatának igazi előnye a főprogramból való fordításkor jelentkezik, mert a főprogram a Cg Runtime Library segítségével le tudja kérdezni a gépen használható legfejlettebb profilt, és arra tudja fordítani a shader-t. A lefordított program videokártyára töltéséhez, valamint a főprogram és a shader-ek közti adatkapcsolatok meghatározásához szintén a Cg Runtime Library függvényei adnak lehetőséget.


Shader fejlesztés


A shader programok interaktív fejlesztéséhez használható az NVIDIA FX Composer környezet. A FX Composer HLSL nyelven írt shader-ek készítésére szolgál, de a két nyelv közti minimális különbség miatt jól használható Cg programok fejlesztéséhez is.

Olvasott embernek nincs párja

Azt már tudjuk nagyjából, hogy mi is a Cg, de ennyi még édes kevés a sikeres fejlesztéshez. Ezt tudták az NVIDIA-nál is, és a szokásos száraz dokumentációkon túl egy jó kis tutorial könyvet is a piacra dobtak: The Cg Tutorial: The Definitive Guide to Programmable Real-Time Graphics néven. A könyv sajnos nem ingyenes, $50 körül gyűjthetjük be az Amazon.com-on. Vagy magán, oktatási, és egyébként is megvan, de az unokatesóm elkérte célokra elérhető innen is.

A "Cg Biblia"
Elég részletes és jó könyv a shader programozásról, így bőven elegendő információ van benne a sikeres induláshoz. Aki HLSL-t használ azoknak is kötelező. Minimális a két nyelv között a különbség. Igaz nem lehet csak úgy ingyen letölteni, de html formátumban az NVIDIA Developer Zone oldalán szabadon böngészhető. Terveim szerint ennek a könyvnek a példáit fogjuk mi is átvenni. Akinek a fenti könyv túl hosszú, annak ajánlhatom még a Cg in two pages összefoglalót is. Ennyit a szükséges irodalomról.

A Cg API elérése menedzselt környezetben

A Cg API beüzemelésének első lépése, hogy az NVIDIA honlapjáról letöltjük a fejlesztéshez szükséges Cg Toolkit-et:


A letöltéshez lehetséges, hogy be kell jelentkezni az NVIDIA Developer Zone oldalra. Régen erre nem volt szükség. Jelen bejegyzés írásakor a legfrissebb Cg könyvtár verziója 3.0. Az ehhez tatozó dokumentáció is innen érhető el.

Ha sikerült letölteni a kb. 15 MB méretű csomagot, máris indulhat a rendkívül bonyolult telepítés (next-next-ok). A telepítés után a következő nélkülözhetetlen szerelvények kerülnek fel a gépünkre:
  • cg.dll (alapvető Cg függvények)
  • cgD3Dx.dll (Cg csatolás a DirectX API x verziójához)
  • cgGL.dll (Cg csatolás az OpenGL API-hoz)
A második lépés, hogy az előbb felsorolt natív szerelvényeket elérhetővé kell tennünk felügyelt környezetben. Szerencsére a szükséges header fájlok megtalálhatóak az include\cg mappában. Így a Cg OpenGL-ben történő használatához a cg.h és a cgGL.h fájlokból lehet kiindulni.
A Tao Framework nem csak az OpenGL hanem a Cg számára is biztosít wrapper osztályokat. A Tao.Cg névtéren belül érhető el a Cg API 1.2 verziója. Sajnos mára a Tao fejlesztése teljesen megszűnt, így nem célszerű erre a könyvtárra a továbbiakban alapozni. Szerencsére van alternatíva, igaz nem sok. Az egyik ígéretes projekt a Cg.Net ami itt érhető el:
Véleményem szerint ez egy nagyon jó kis könyvtár, így mindenki számára bátran tudom ajánlani. Nagy előnye, hogy sok példát tartalmaz, így használata könnyedén elsajátítható. Ennek ellenére, ebben a bejegyzésben nem ezt, hanem egy másik kiváló wrappert használunk fel és ez pedig nem más, mint a „nagy” OpenCg.
Ezt a könyvtárat lassan egy éve kezdtem el írni a néhai Tao.Cg alaposztályait felhasználva. A wrapper készítésekor fontos cél volt, hogy az IntPtr-ek száma a lehető legkevesebb legyen, valamint, hogy a menedzselt osztályok felépítése a lehető legjobban hasonlítsanak a natív API-hoz. Ezeket a célokat sikerült is elérni, és jelenleg az OpenCg a Cg API 3.0-ás verzióját teszi elérhetővé a .NET rendszerben. Az OpenCg abból az időből származik, amikor még Cg.Net nem volt, így elsősorban saját célra készült, így számtalan hiba lapulhat meg benne, de ez nem jelenti azt, hogy kevésbé lenne használható mint a Cg.Net. Csak tesztelni kellene rengeteget. Az biztos, hogy többet kézzel sose fogok wrappert írni, mert a közel 1500 függvény és enum kötése, dokumentálása és tesztelése eléggé fáradtságos dolog volt… Jelenleg még nem teszem elérhetővé az OpenCg forrását, csak a bináris szerelvényt. A Reflector-ral persze mindenki szabadon turkálhat benne.

Az OpenGL API elérése menedzselt környezetben

Eddig az OpenGL elérésére a Tao Framework-öt használtuk felügyelt környezetben. Ennek a fő oka az volt, hogy a Tao osztályai közel álltak a natív OpenGL API-hoz, így könnyű volt átírni a cpp forrásokat. Sajnos mára már a Tao elavult, így célszerű egy aktívan fejlesztett könyvtárra áttérni. A lehető legjobb választás jelenleg az OpenTK.


Az OpenTK vagy a teljes nevén az Open Toolkit egy C# nyelven írt szabad wrapper ami .NET környezetben (Mono-t is beleértve) teszi elérhetővé az OpenGL, OpenAL és OpenCL API-k használatát. Gyors, platformfüggetlen és típus biztos kötést nyújt az OpenGL 3.2, OpenGL ES 2.0 és OpenAL 1.1 valamint OpenCL 1.0 könyvtárak eléréséhez. Természetesen a bővítmények automatikus betöltése és az inline dokumentáció és hibakezelés sem marad el. Az OpenGL kontroll alkalmazható Windows.Forms, GTK# és WPF alkalmazásokban, sőt még egy nagyteljesítményű GameWindow is a rendelkezésünkre áll. Természetesen az alapvető Vektor és Mátrix adatszerkezeteket is tartalmazza a csomag. A csomag minden a Common Language Specification-t (CLS) megvalósító nyelvvel használható, így pl. a C#, VB.Net, C++ / CLI, IronPython és a Boo programozók sem maradnak ki a jóból. További részletek és a telepítő itt érhető el:


A telepítése legalább olyan bonyolult, mint a Cg Toolkit-é volt, így problémát nem okozhat senkinek sem. A telepítés után a szükséges szerelvények bekerülnek a GAC-ba. A csomagban számos példa található, így könnyű ráhangolódni az OpenTK filozófiájára a Tao után.

Amikor az egyes részek összeállnak

A fenti ismertetők után most térjünk át a gyakorlatra. Azt már tudjuk, hogy az OpenGL-t az OpenTK, míg a Cg-t az OpenCg könyvtár segítségével érjük el menedzselt környezetben. Ne felejtsük el, hogy az OpenGL megfelelő működéséhez a megfelelő videokártya driver, míg a Cg futtatásához a Cg Toolkit telepítése nélkülözhetetlen. Ezek után indítsuk a Visual Studio 2010-et és indulhat is a hárdkódolás. Először is válasszunk egy WinForm projektet (Form1-et nevezzük át MainForm-ra), majd a szükséges referenciák hozzáadása következik:
  1. OpenCg (Cg API elérése)
  2. OpenTK (OpenGL API elérése)
  3. OpenTK.GLControl (OGL kontroll a rajzoláshoz)
Az OpenTK szerelvényei itt találhatóak
Az OpenTK könyvtárban megtalálhatunk még egy fontos szerelvényt, mégpedig az OpenTK.Compatibility.dll. Ebben a Tao és a Glu legfontosabb függvényei találhatóak meg. Fontos, hogy az OpenTK ezeket az osztályokat már elavultnak tekinti, ezért is kerültek külön szerelvénybe. Régebbi projektek kompatibilitását szolgálja.

Az OpenGL használatához szükséges szerelvények
A referenciák után a szükséges névtereket adjuk hozzá a MainForm.cs-hez:

using OpenCg;
using OpenCg.OpenGL;
using OpenTK.Graphics.OpenGL;

Az OpenCg névtérben a Cg API alaptípusai és enum-jai találhatóak meg, míg az OpenCg.OpenGL-ben a Cg rendszer OpenGL specifikus függvényei érhetőek el.
A továbbiakban építsünk fel egy egyszerű OpenGL alkalmazást. Ehhez a Toolbox-ról húzzunk át egy GLControl vezérölt. Ha nem találnánk meg, akkor jobb klikk és Choose Items. A felugró ablakban a .NET Framework Components fülön keressük meg a GLControl-t majd pipa a chekboxba, végül egy kemény OK következik. Ezután drag&drop technikával helyezzük el a MainForm-on.

A GLControl nyakoncsípése
A glControl-nak számos hasznos tulajdonsága és eseménye van, ebből a legfontosabba a Load, a Paint és a Resize. A Load eseménynél a szükséges erőforrásokat tudjuk betölteni. A Resize-nél az ablak átméretezéséhez szükséges OpenGL függvényeket helyezzük el, míg a Paint eseménynél maga a renderelés történik. Lássuk a példát:

   private void glControl_Paint(object sender, PaintEventArgs e)
   {
       Display();
   }
         
   private void glControl_Resize(object sender, EventArgs e)
   {
       Reshape();
       Display();
   }

   private void Reshape()
   {
       GL.MatrixMode(MatrixMode.Projection);
       GL.LoadIdentity();

       GL.Viewport(0, 0, glControl.Width, glControl.Height);
       GL.Ortho(0, 0, (double)glControl.Width, (double)glControl.Height, -1, +1);

       GL.MatrixMode(MatrixMode.Modelview);
       GL.LoadIdentity();
   }

   private void Display()
   {
       GL.ClearColor(0.1f, 0.3f, 0.6f, 0.0f);
       GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);

       GL.Begin(BeginMode.Triangles);
       {
           GL.Vertex2(-0.8f, +0.8f);
           GL.Vertex2(+0.8f, +0.8f);
           GL.Vertex2(+0.0f, -0.8f);
       }
       GL.End();

       glControl.SwapBuffers();
   }

A fenti példában egyelőre maraduk a rögzített csővezetéknél és a képernyőre varázsolunk egy fehér síkbeli háromszöget. Később majd ebből az alapból indulunk ki az első Cg shader futtatásához.

Szimpla OGL háromszög, ezt fogjuk átszínezni
Ok, van egy fehér háromszögünk, de mi ezt át szeretnénk színezni zöldre. Ezt könnyen megtehetnénk a rögzített csővezeték esetén a glColor függvény meghívásával. De mi nem ezt fogjuk tenni. Shadert írunk rá, méghozzá vertex shadert. Shaderek fejlesztésére sok kiváló eszköz található, de mi most a legegyszerűbbet fogjuk használni mind közül, a Jegyzettömböt. A következő legyen a tartalma:

   // This is C2E1v_green from "The Cg Tutorial" (Addison-Wesley, ISBN 0321194969) by Randima Fernando and Mark J. Kilgard.  See page 38.

   struct C2E1v_Output
   {
           float4 position : POSITION;
           float3 color    : COLOR;
   };

   C2E1v_Output C2E1v_green(float2 position : POSITION)
   {  
           C2E1v_Output OUT;

           OUT.position = float4(position,0,1);
           OUT.color = float3(0,1,0);

           return OUT;  
   }

A shader programok minden esetben adatokat dolgoznak fel, amit a szerelőszalag előző lépésétől kapnak, és a következő lépésének adnak tovább. Ezek a legtöbb esetben nem egyetlen változónyi adatok, ezért a programok fogadhatják több paraméterként, vagy egy struktúrában. Ezért először is létrehozunk egy struktúrát (C2E1v_Output) amiben a pozíció és a szín értékeket fogjuk eltárolni. Azért, hogy a fordító megtudja állapítani, hogy a program milyen adatokat vár a GPU-tól a kötési szemantikákkal megadható, hogy melyik változóba, pontosan milyen értéket vár, vagy ad vissza a program. A kötési szemantikák nem csak struktúrák, hanem függvény paraméterek esetén is használhatók.
Ezután létrehozzuk programunk belépési pontját C2E1v_green (ez a main) néven, ami bemenetként pozíciót fogad, és futása után visszatért egy C2E1v_Output típussal. Shaderünk törzsében OUT néven definiáljuk a C2E1v_Output struktúrát. Ennek a position mezőjének a belépési pontban megkapott pozíciót állítjuk be, míg a szín mezőnek a zöld RGB értékeit adjuk meg. Végezetül visszatérünk a feltöltött OUT struktúrával.
Ez a kód az összes vertex esetén lefut. A belépési pontban érkező pozíció nem más, mint a glVertex függvénnyel beállított érték. A mivel a vertexek színét nem állítottuk be a glColor függvényhívással az most jelenleg fehér, egészen addig, amíg a shader be nem állítja a zöld szín RGB értékeit. A shaderből kilépő vertexek már zöld színűek és így haladnak tovább a csővezetéken.
Mivel végeztünk az árnyaló kód megírásával azt most mentsük le egy egyszerű szövegfájlba cg kiterjesztéssel. Most már csak ki kellene próbálni, hogy valóban lefordul-e a kódunk. Erre a lehető legjobb eszköz kezdő Cg programozók számára a CgEddie v0.3.

CgEddie a kis barátunk
A program indítása után töltsük be a szövegfájlból a kódot majd Compile Shader Ctr+F5. Itt valószínű hibaüzenetet kapunk: (0) : error C3001: no program defined. Vagyis meg kell adni a programunk belépési pontját, Set Entry Point… Ctr+F3 és itt a felugró ablakba a „main”-t cseréljük le a „C2E1v_green” szövegre. Ezután már sikeres lesz a fordítás. Vagyis működik, irány vissza a Visual Studo-ba.
A következő kódrészlet OpenGL alatt mutatja be egy vertex shader betöltéséhez szükséges lépéseket. Az első és egyben legfontosabb dolog, hogy létrehozzuk azt a környezetet (kontextust) amiben a Cg programok futni fognak:

private CGcontext cgContext = Cg.CreateContext();

A következő feladat, a támogatott legfejlettebb vertex/fragment vagy geometry profile lekérdezése:

private CGprofile cgVertexProfile = CgGL.GetLatestProfile(CGGLenum.Vertex);

A GetLatestProfile a CgGL osztály metódusa, aminek egy CGGLenum -ot kell átadnunk. A fenti példában a vertex profilt kérdezzük le OpenGL API mellett. Egy profil támogatottságát pedig a CgGL.IsProfileSupported függvénnyel ellenőrizhetjük le. A függvény igaz értéket ad vissza, ha az átadott profil támogatott. Ezek után még be kell állítanunk a shader program belépési pontját, valamint magának a shader forrásnak az elérést:

/* a belépési függvény neve */
private string cgVertexEntryFuncName = "C2E1v_green";

/* a Cg forrás file-jának neve */
private string myVertexProgramFileName = "example.cg";

A kontextus és a profil létrehozása után már csak be kell tölteni és lefordítani a megírt vertex shadert amiből így előáll a vertex program. Létre kell tehát hoznunk egy vertex programot (CGprogram) amit a Cg.CreateProgramFromFile függvény megfelelő paraméterezése mellett feltöltünk:

CGprogram cgVertexProgram = Cg.CreateProgramFromFile(
          cgContext,                // Cg környezet
          CGenum.Source,            // forrás fájlt tölt be, nem binárist
          myVertexProgramFileName,  // forrás fájl név, elérési út
          cgVertexProfile,          // a fordítás cél profilja
          cgVertexEntryFuncName,    // belépési függvény neve
          null);                    // átadandó beállítások a fordítónak

Ha sikerült a fordítás, akkor a vertex programot még be is kell tölteni CgGL.LoadProgram függvénnyel:

CgGL.LoadProgram(cgVertexProgram);

De honnan tudhatjuk, hogy minden rendben történt a fordítás során? Hogyan kaphatunk visszajelzést a hibákról? Először is létrehozunk egy ErrorCallbackFuncDelegate delegáltat:

private static Cg.ErrorCallbackFuncDelegate errorDelegate;

Erre a MainForm konstruktorában feliratkozunk: errorDelegate += CgError;

Ezzel még nem vagyunk készen, hiszen magának a Cg rendszernek is be kell állítanunk a delegáltunkat a Cg.SetErrorCallback(errorDelegate); függvényhívással. A CgError metódus logikája pedig itt olvasható:

   private static void CgError()
   {
       CGerror error = Cg.GetError();
       string errorString = Cg.GetErrorString(error);

       if (error != CGerror.No)
       {
           if (error == CGerror.Compiler)
           {
               Console.WriteLine("{0}", Cg.GetLastListing(cgContext));
               Environment.Exit(0);
           }
       }
   }

Ha a fordítása alatt ide nem ugrunk be akkor simán ment minden, és mindenki boldog. Ezzel fel is épült az első shader programunk váza. Most már csak az OpenGL API-val kell a shader programot összekapcsolni vagyis bindolni. A módosított display metódus a következő:

       private void Display()
       {
           GL.ClearColor(0.1f, 0.3f, 0.6f, 0.0f);
           GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);

           CgGL.BindProgram(cgVertexProgram);
           CgGL.EnableProfile(cgVertexProfile);

           GL.Begin(BeginMode.Triangles);
           {
               GL.Vertex2(-0.8f, +0.8f);
               GL.Vertex2(+0.8f, +0.8f);
               GL.Vertex2(+0.0f, -0.8f);
           }
           GL.End();

           CgGL.DisableProfile(cgVertexProfile);
           CgGL.UnbindProgram(cgVertexProfile);

           glControl.SwapBuffers();
       }

Programunk befejeztével, rendet rakunk magunk mögött és kíméletlenül megsemmisítjük azt, amit meg kell:

Cg.DestroyProgram(cgVertexProgram);
Cg.DestroyContext(cgContext);
Environment.Exit(0);

Ezzel végeztünk is, most már csak „gyönyörködnünk” kell művünkben.

A vertex program eredménye
Tudom nem túlságosan látványos és érdekes dolog, de talán segít megérteni a shaderek lényegét és azok jelentőségét. A teljes forrás letölthető innen:


Bárki csatlakozhat az OpenCg fejlesztéséhez, így aki érzi magában az erőt az nyugodtan dobjon meg egy levéllel. Persze kommentelni sem szégyen :) Remélem tudtam kicsit segíteni az érdeklődök számára.

1 megjegyzés :