Előadás Home Introduction Environment setup Best homeworks

OpenGL

Az OpenGL az nem egy API, hanem egy specifikáció, amelyet a Khronos Group felügyel. A specifikáció célja, hogy pontosan meghatározza, hogy minden egyes függvénynek mi kell legyen az eredménye, kimenete, tehát maga az OpenGL specifikáció nem határozza meg, hogy az adott függvényt hogyan kell implementálni. Az OpenGL egyes függvényeit főleg a különböző grafikus kártya gyártók implementálják (Nvidia, AMD, Intel), ezért minden egyes videó kártya az OpenGL egy specifikus változatát támogatja, amelyet direkt arra a kártya szériára fejlesztettek. Tehát az Apple, Windows, Linux alatt használt Nvidia, AMD, Intel grafikus kártyák egy azon OpenGL specifikációt különböző módon implementálnak.
Modern OpenGL alatt a 3.3 és az a fölötti verziókat értjük, melyek az OpenGL Core Profile-t használják a régi pipeline helyett. A régi OpenGL-t egyszerűbb volt érteni és használni, de elrejtette a felhasználó elől a belső működést, sokkal rugalmatlanabb volt, nem engedett túl sok szabadságot és a működése is sokkal hatékonytalanabb volt. Jelenleg az OpenGL 4.6-os verzió a legfrisebb. Minden egyes jövőbeli verzió új funkcionalitást ad az OpenGL-hez, anélkül, hogy megváltoztatná a Core profile működését. Az új verziók vagy teljesen új funkcionalitást adnak hozzá, vagy egy hatékonyabb implementációját tartalmazzák egy már meglévő függvénynek, de vigyázva arra, hogy az eredmény ugyan az legyen, mint a régi verzióban.
Az OpenGL hasonló egy állapotgéphez, melynek állapotát (context) különböző változók beállításával lehet változtatni. Tehát, ha pl. pontok helyet vonalat szeretnénk rajzolni, akkor a megfelelő változók beállításával megváltoztatjuk az OpenGL context-ét ami egészen addig vonalat fog rajzolni, amíg újra meg nem változtatjuk az állapotát.

Az OpenGL objektum különböző opciók halmaza, melyek az OpenGL állapotát írják le.

In [ ]:
// create object
unsigned int objectId = 0;
glGenObject(1, &objectId);
// bind object to context
glBindObject(GL_WINDOW_TARGET, objectId);
// set options of object currently bound to GL_WINDOW_TARGET
glSetObjectOption(GL_WINDOW_TARGET, GL_OPTION_WINDOW_WIDTH, 800);
glSetObjectOption(GL_WINDOW_TARGET, GL_OPTION_WINDOW_HEIGHT, 600);
// set context target back to default
glBindObject(GL_WINDOW_TARGET, 0);
A fenti kód szemlélteti egy OpenGL objektum használatát. Első lépésben létrehozunk egy objektumot egy adott azonosítóval, majd hozzárendeljük (bind) az adott context egy célpontjához (target). A fenti példában ez a target egy window objektum. Következő lépésben beállítjuk a window target két opcióját (szélesség és magasság). Végül az aktuális objektum id nullára állításával megszüntetjük (un-bind) a kötést a target objektummal. Az adott objektum eltárolja a beállított opciókat, mely a megfelelő id referencián keresztül újra betölthető. Ez a működés lehetővé teszi, hogy számos objektumot hozzunk létre különböző opciókkal, majd mindig az aktuális feladathoz megfelelő objektumot töltsük be a kívánt rajzoló funkciót vagy modellt elérve ezzel.

Graphical Processing Unit (GPU)


A számítógépes grafika legtöbb feladata rengetegszer ismétlődő, nagyon hasonló kis utasításokból áll, melyek általában egyméstól függetlenek. Például, ha egy kép pixeleihez szeretnénk színt hozzárendelni, akkor ugyanazt a műveletet kell a kép pixeleinek számaszor lefuttatni. Ahhoz, hogy egy pixel színét beállítsuk, nincs szükségünk a szomszédos pixelek értékeire, ezért ez a feladat (párhuzamos) parallel módon is könnyen elvégezhető.

Az OpenGL célja, hogy egy absztrakt rétegként funkcionáljon az adott applikáció és a GPU között, mely lehetővé teszi, hogy a felhasználó utasításait hatékony módon parallel futtathassuk az adott GPU architektúrán, kihasználva a modern GPU-k hatalmas számítási teljesítményét. Azért, hogy minél egyszerűbben lehessen a GPU-t használni, a modern OpenGL próbál minél magasabb absztrakciós szinten működni, de egyszerre annyira alacsony szintű lehetőségeket is biztosítani, hogy a programozó minél hatékonyabban kihasználhassa az adott GPU erőforrásait minden egyes specifikus feladat esetén.
Nvidia GeForce RTX 2080 Ti az Nvidia zászlós hajója 11 GB DDR6 memóriát és 4352 shader/cuda processzort, tartalmaz, melyek 13.4 TFLOPs teljesítméyt képesek leadni. Hardware-es sugárkövetést (raytracing) biztosít, továbbá tensor processzorokat tartalmaz a deep learning számítások gyorsításához.

Rendering primitives


A grafikus renderelés alapvető egységei a primitívek. Az OpenGL többféle primitívet támogat, melyek közzül a legfontosabbak a pont (point), a vonal (line) és a háromszög (triangle). Mindent amit a képernyőre kirenderelünk alapvetően, pontok, vonalak és háromszögek összessége. A komplex felületeket, modelleket (surface) rengeteg számú háromszögre bontjuk, melyeket az OpenGL a renderelés során raszterizál. A raszterizáláshoz tartozik egy dedikált hardware ami a háromszögek 3D-s reprezentációját pixelek sorozatára konvertálja, melyeket a képernyőn jelenítünk meg. Mivel a háromszögek mindig konvex alakzatok és egyszerű szabályokkal ki tudjuk őket tölteni és kezelni, ezért a konkáv poligonokat mindig háromszögekre bontjuk. A hardware natívan támogatja a háromszögek renderelését.

Grafikus pipeline [3]

title

Vertex specification

Egy rendezett vertex lista definiálása, melyet a program a pipeline-nak fog elküldeni. A vertex lista a különböző primitívek (point, line, triangle) határait definiálja. Miután definiáltuk a vertex listát a Vertex rendering folyamat során a draw parancs a megfelelő primitívbe rendereli a vertexeket.

Vertex shader

A vertex rendering eredényét fogadja bemenetként és minden egyes vertexen alap műveleteket végezhet el, majd egy kimenő vertexé alakítja az adatot a felhasználó által definiált program alapján.

Tessellation

A primitíveket két shader segítségével tovább finomítjuk. A Tessellation Control Shader meghatározza a "finomítás számát", majd a Tessellation Evaluation Shader valamilyen interpolációt és esetleg további felhasználó által definiált műveleteket alkalmazva elvégzi a finomítást. Azaz egy folytonos felületet úgy közelítünk, hogy az adott felületet, primitívet egyre kisebb háromszögekre bontjuk.

Geometry shader

Felhasználó által definiált program, mely feldolgozza a beérkező primitíveket és nulla vagy több kimenő primitívet küld a pipeline következő szintjére. Ez egy programozható elágazás a pipeline-ban, melyel szabályozhatjuk a kimenő primitívek számát és műveleteket végezhetünk rajtuk.

Vertex post-processing

Számos fixed műveleten mennek keresztül az adott vertexek, például a vágás műveletén, ahol az adott nézőponthoz képest meghatározzuk az objektum window beli helyét.

Primitive assembly

Az a folyamat, ahol a fő lépések kimeneti vertex adatai összegyűlnek és egy rendezett primitív (point, line, triangle) listába szerveződnek.

Rasterization

Azok a primitívek, melyek eddig eljutnak a pipeline-ban a megadott sorrendben raszterizálódnak. Ennek a raszterizációnak az eredménye fragment-ek sorozata. A fragment állapotok halmaza, melyet arra használunk, hogy kiszámítsuk az adott pixel képernyő beli pozícióját.

Fragment shader

A raszterizációs lépés fragment kimeneteit a fragment shader dolgozza fel. A fragment shader kimenete egy lista a megfelelő színekkel és a hozzájuk tartozó mélység információval.


Grafikus processzor

A CPU renderelési parancsokat küld a GPU-nak, ami feldolgozza azokat és az eredméynt megjeleníti a képernyőn, közben a CPU más feladatokat láthat el. Egy adott alkalmazás valamilyen rendering library-n keresztül (OpenGL, DirectX, a mi esetünkben az OpenGL) küldhet parancsokat a GPU-nak. Az OpenGL a grafikus driver-en keresztül képes "beszélni" a GPU-val, vagyis a grafikus driver az egyes OpenGL függvényeket lefordítja a GPU natív nyelvezetére. A következő ábra szemlélteti a CPU és a GPU közötti kommunikációt.

title

A GPU rendelkezik egy saját dedikált memória modullal, amit általánosságban Video Random Acces Memory (VRAM)-nak hívunk. A VRAM tetszőleges információt képes tárolni, azonban van néhány adattípus, amit majdnem minden grafikus renderelés esetén megtalálunk a VRAM-ban:

  • Front és Back image buffers: A Front image buffer azokat a tényleges pixel adatokat tartalmazza, amik a viewport-on keresztül láthatók. A viewport a képernyő azon része, ami tartalmazza a window-t és azon belül a renderelt képet. A Back image buffer az a hely, ahová a GPU ténylegesen rendereli az adatokat (scene). Amikor egy adott scene renderelésével végzett a GPU, akkor a két buffer tartalma kicserélődik (buffer swap) és a Front image buffer tartalma megjelenítődik a képernyőn, közben a GPU egy újabb scene-t renderelhet a Back image buffer-be.
  • Depth buffer vagy z-buffer: Minden egyes pixel értékhez tárol egy depth értéket, ami meghatározza, hogy az adott pixel milyen távol van az adott kamera nézőponthoz képest. Segítségével szűrhetjük azokat a távoli vagy túl közeli pixeleket, amiket nem szeretnénk megjeleníteni.
  • Stencil buffer A stencil buffer egy integer maszkot tartalmaz az image buffer minden pixeléhez, ahol minden pixelhez meghatározhatjuk, hogy meg szeretnénk e jeleníteni vagy nem. Továbbá használják még bonyolult valós idejű árnyékolás kiszámításához is.
  • Texture maps: Tulajdonképpen egy kép, amit arra használunk, hogy az egyes objektumok felületének részletes megjelenítést biztosítsunk.
  • Vertex buffers: A vertex bufferben a primitívek 3D koordinátáit tároljuk. (3D pontok halmaza)

Vertex transzformáció


A GPU egyik feladata, hogy a 3D térben definiált adatokat a 2D képernyő megfelelő pixel koordinátáira alakítsa. Ez a folyamat számos transzformáció kiszámításából áll, amik egyik koordináta rendszerből a másikba visznek át (ld. lenti ábra). A modell vertex értékei tipikusan az objektum térben definiáltak, azaz minden egyes modell (objektum) esetén egyediek. A renderelés során egy scene-t számos modell segítségével állítunk elő, ezért az egyes modelleket egy közös (globális) koordináta rendszerbe (World space) kell transzformálni, hogy meghatározzuk az egyes modellek világ beli pozícióját és orientációját. Mielőtt a scene-t renderelnénk az objektumokat át kell transzformálni a kamera térbe (camera vagy eye space), ahol az x és az y tengelyek a képernyőhöz vannak igazítva a z tengely pedig párhuzamos a néző iránnyal (view direction). A fent leírt folyamatot a modell-view transzformációnak nevezzük.

title

Miután a modellt a kamera térbe transzformáltuk egy perspektív transzformációt hajtunk végre a modellen, aminek az eredménye, hogy a modell geometriája kisebb lesz a kamera távolságának a függvényében. A projekció 4 dimenziós homogén koordináták segítségével történik, a vertex adatok pedig egy ún. homogeneous clip space-be kerülnek. Ebben a térben már csak az adott nézetből a képernyőn ténylegesen látható grafikai primitívek maradnak meg, továbbá a vertex koordináták ún. normalized device koordinátákkal adottak azaz a (-1:1) skálán vannak értelmezve, de tükrözik az objektumok egymáshoz képest elfoglalt pozícióját. Végül a vertex adatok még keresztül mennek egy viewport transzformáción, ahol a normalized device koordinátákat a tényleges pixel koordináták terébe képezzük le. A z koordináta a depth buffer alapján 0-1 tartomány közzé transzformálódik. A viewport transzformáció után azt mondjuk, hogy a vertex adatok a window space-be kerültek.

A transzformációk során számos komplex megvilágítási és árnyékolási érték is kiszámításra kerülhet, továbbá az egyes vertex értékekhez textúra információt is rendelhetünk.

Raszterizáció és fragment műveletek


Miután a modellt a window koordinátarendszerébe vágtuk és transzformáltuk, A GPU-nak meg kell határoznia, hogy az egyes grafikus primitívek, mely valós pixeleket fedik. A folyamatot, ahol az egyes pixeleket hozzárendeljük a primitívekhez raszterizációnak nevezzük. A GPU minden egyes pixelhez kiszámítja a mélységet, az interpolált vertex színt és az interpolált textúra koordinátát. Ezeket a számított információkat és a pixelek helyének az együttesét fragmentnek nevezzük.

title


A fenti ábra illusztrálja a folyamatot, ami alatt a grafikus primitíveket fragmentek sorozatává konvertálja az OpenGL. A face culling a folyamat első lépése, amit csak poligon típusú grafikus primitívekre alkalmazza a pipeline. A pipeline eltávolítja azokat a poligonokat, amik a kamera nézőpontjából az objektum nem látható oldalán vannak, továbbá azokat amelyek a kamera mögött helyezkednek el. Ez egy optimalizáció, hogy gyorsabb legyen a renderelés folyamata. A fragment shading (más néven pixel shading) definiálja, hogy a raszterizáció során hogyan határozzuk meg a végső színt és mélységet az egyes pixelekhez. A végső szín lehet, hogy egyszerűen csak a vertex és a textúra színinformációból áll elő, de előállhat bonyolult árnyékok, megvilágítások és anyag tulajdonságok figyelembe vételével is. A lenti ábra szemlélteti a raszterizáció során generált fragmentek számításánál végbemenő műveleteket.

title


Az első fragment művelet a pixel ownership test és ez az egyetlen amit nem lehet kikapcsolni. Ez a művelet határozza meg, hogy az adott fragment az aktuálisan látható képernyőn található e. Ha egy másik UI window elfedi, akkor nem rajzolódik ki. A scissor test akkor fut le, amikor az applikáción belül definiálva van egy négyszög a viewport-on belül és meghatározza, hogy az adott fragment bele esik e ebbe a területbe. Ha nem, akkor a fragment nem rajzolódik ki. Ha a scissor test lefutott, akkor következik az alpha test. Amikor a fragment végső színe meghatározódik egy alpha érték is kiszámolódik, ami az adott fragment áttetszőződés mértékét definiálja. Ez befolyásolja a végső színt, mert ha az adott fragment alatt másik objektum (fragment) helyezkedik el, akkor a két fragment színéből az alpha érték függvényében áll elő az új szín. A stencil test során a stencil bufferben tárolt értéketet hasonlítja össze a pipeline egy korábban definiált értékkel, ha az összehasonlítás megfelel az előre definiált kritériumoknak akkor sikeresen lefut, ellenkező esetben a fragment nem rajzolódik ki. Ezt főleg a bonyolultabb árnyékok renderelésénél használják ki. A végső fragment teszt a depth test, ahol ha egy fragment számított depth tulajdonsága eltér az előre definiálttól akkor nem rajzolódik ki. Például, ha egy objektum túl messze van a kamerától, ahol már nem szeretnénk, hogy a user ellásson a térképen. Ha az összes teszt lefutott, akkor a blending során a fragment számított színe belekerül az image bufferbe. A blending során egy új szín jön létre, ami a számított fragment színből és a már az image bufferben található színből áll elő.