2010. november 9., kedd

Textúrák, mapek és az OpenGL

Egy zseniális cikket találtam a jatekfejlesztes.hu oldalon a textúrákról OpenGL környezetben. Mivel ilyen jó írást ebben a témában magyarul még nem olvastam, ezért be is másoltam rögtön ide, remélve, hogy így még több érdeklődőhöz eljut ez az anyag. Tessék olvasni.

Bevezetés

A 3D alkalmazások egyik fő célkitűzése a fotorealisztikus megjelenítés. Ehhez mindenképpen a lehető legrészletesebben kell eltárolni modelljeinket, azonban a részletesség növelése még manapság is hamar korlátokba ütközhet. A felületek összetettségét azonban nagyon jól lehet utánozni a modell vázára húzott „szövetek” felhasználásával. Ezeket a „szöveteket” hívjuk textúráknak. A textúrák legtöbbje általában egy kép, azaz egy síkbeli kétdimenziós tömb, aminek elemei színeket tárolnak. A színtömb elemeit textúra elemeknek ill. texeleknek hívjuk. A felhasználás során a képnek a pontjait rendeljük hozzá a modell vázához. Léteznek textúrák, amelyek mindössze egy dimenziós kiterjedésűek, vagyis vonalak és léteznek térbeli textúrák is, amelyeket kockaként lehet elképzelni. A textúrázott modell megjelenítésekor azonban egy újabb probléma merül fel, ugyanis minél távolabb helyezkedik el a modell adott felületi pontja a nézőponttól, annál gyakrabban fordul elő, hogy a pont képernyőre vetülésekor több texel színét venné fel, amelyek közül azonban csak egy jelenhet meg. Megoldást az jelenti, hogy minél távolabbi a pont a kamerától, annál alacsonyabb részletességi szintű (level of detail) textúrát használunk.

A textúrák és az OpenGL: texture object

Ahhoz, hogy egy képet textúraként tudjunk használni, ismernünk kell, hogyan kezeli ezeket az OpenGL. Az OpenGL ún. kliens-szerver felépítésű ahol a kliens az alkalmazás, míg a szerver maga a grafikus rendszer. Bármilyen adat, amelyet a grafikus rendszernek szeretnénk átadni, átkerül a kliens memóriaterületéről a szerverére. Textúrázáskor az 1.0 szabvány esetén az adatáramlás minden frame idején megtörtént, aminek eredménye a renderelési sebesség drasztikus megnyúlása volt.
Külön figyelmet érdemel az, hogy fragment/pixel adatok mozgatásakor be lehet állítani a tárolási szerkezetet (glPixelStore) és a komponensekhez külön-külön egy scale és egy bias értéket (glPixelTransfer). A kliens memóriaterület felé történő mozgást PACK-nek a szerver memóriaterület felé történőt pedig UNPACK-nek nevezik. A legfontosabb beállítás a pixelek tárolásának határra történő igazítása (alignment). Alapértelmezettként az OpenGL (mind PACK mind UNPACK esetén) ezt négy bytenak veszi azonban sokkal gyakoribb, hogy egy byteos határral dolgozunk.
Az 1.1 szabványtól kezdve a textúrákat már ún. objectként kezeli az OpenGL (1.0 verzióban ez a GL_EXT_texture_object extensionnek felel meg). Maga az object egy általános fogalom, amelyen egy azonosítóval ellátott szerver oldali memóriaterületet értünk. A textúrák esetében ez a tény teszi lehetővé a gyorsabb renderelést, hiszen a képadat felkerül a szerver memóriaterületére.
Egy vagy több azonosítót a glGen parancscsoporttal készíthetünk és a glDelete parancsok valamelyikével, törölhetünk. Az objectek készítéséhez szükséges egy már létező azonosító kötése glBind, a használatához pedig annak engedélyezése glEnable valamint szintén egy kötés létrehozása (ha azonosítóként nullát adunk át az object használatát tiltjuk).
Jelen esetben az azonosítóhoz egy textúra és a hozzá tartozó paraméterek tartoznak, amelyek a textúra használatát befolyásolják és a legelső kötés során kötelező őket megadni (később ezek már nem módosíthatók).

A textúrák paraméterei

Filter
Az egyik legfontosabb a textúra szűrés (a fent említett level of detail gyakorlati alkalmazása). Az OpenGL képes egy textúrának az összes részletességi szintjét tárolni és a mintavételezést az adott távolság alapján az adott szintű textúrából a mipmap elven elvégezni (a mipmapnak ugyanakkor hátránya is, hogy elmossa a távolabb eső pontokat. Ez kiküszöbölhető a későbbiekben tárgyalandó detail texture). Külön megadhatjuk a nagyításkor (GL_MAG_FILTER) és a kicsinyítéskor (GL_MIN_FILTER) alkalmazandó szűrést. Nagyításkor a szűrés legfeljebb lineáris lehet kicsinyítéskor, viszont figyelembe veheti a mipmap szinteket is, amelyek között is interpolálhat. Így jöhet létre a bilineáris szűrés (ilyenkor a GL_MAG_FILTER GL_LINEAR, a GL_MIN_FILTER GL_LINEAR) ill. a trilineáris szűrés (ekkor pedig GL_MAG_FILTER GL_LINEAR, a GL_MIN_FILTER GL_LINEAR_MIPMAP_LINEAR). Létezik még ún. anizotróp szűrés is, amely a négyzetes textúra mintavételezés hibáit javítja fel (GL_EXT_texture_filter_anisotropic).

Wrap
Másik fontos paraméter az ún. wrap mode, amely az adott részletességi szintű textúrán belüli mintavételezést szabályozza pontosabban, azt adja meg, mi történjen, ha a textúra területén kívülre hivatkozunk. A textúránk minden „oldalára” megadhatjuk ezt dimenziótól függően. Ha a wrap mode GL_REPEAT, akkor a textúra végtelenített leképezését kapunk, míg GL_MIRRORED_REPEAT esetén minden páratlan ismétléskor a textúra tükörképét látjuk. Ha a wrap mode GL_CLAMP, akkor kívülre hivatkozás esetén a textúra szélét kapjuk meg végtelenítve. GL_CLAMP esetén bizonyos platformoknál a mintavételezés mintegy elcsúszik a textúra szélénél az ellenoldalra, így amennyiben pontos leképezést szeretnénk GL_CLAMP_TO_EDGE módot kell használnunk. Létezik még ezen kívül a GL_CLAMP_TO_BORDER mód, amikor a textúrán kívül a szegély színe jelenik meg. Mindegyik clamp módnak létezik a mirrored párja is, amely egyszeres tükrözés után vált a megfelelő clamp módra. A fenti paraméterek a glTexParameter függvénycsaládon keresztül módosíthatók.
Az alábbi paramétereket már a kötés létrehozásával egy időben kell megadnunk a glTexImage függvénycsaláddal ill. glu függvényekkel. A két függvénycsoport között az alapvető különbség az, hogy a glu függvények automatikus legenerálják az összes mipmap szintet, míg glTexImage függvényeknél külön-külön kell nekünk azokat feltölteni.

Size
További paraméterek a textúra dimenziójának kiterjedései, amelyek általános esetben mindenképp kettő hatványának kell lenniük (ez csak az ún. POT (power of two) textúrákra igaz. GL_TEXTURE_RECTANGLE esetében tetszőleges lehet a méret, azonban legfeljebb lináris szűrés és clamp wrap mode alkalmazható, míg NPOT textúrák (GL_ARB_texture_non_power_of_two) esetén már a mipmapping és repeat wrap mode is lehetséges).

Internalformat
A legnagyobb változatosság a textúra színkomponensei esetén mutatkozik, amelyek általános esetben vörös (r), zöld (g), kék (b), alfa (a) csatornából állnak és az OpenGL lebegeőpontos számként használja. Beállíthatjuk, hogy az egyes komponensek hány bitből álljanak valamint, milyen komponenseket fogadjon. Ezt a beállítást nevezzük ún. belső formátumnak (internal format). GL_ALPHA esetében csak az a komponenst tölti fel, ilyenkor a r, g, b értéke 0.0. GL_RED, GL_GREEN, GL_BLUE esetén a megfelelő komponenst tölti fel, valamint az a csatornát 1.0 értékkel. GL_RGB használatakor a vörös, zöld és kék komponensek értékeit használja, míg a negyedik csatornába 1.0 kerül, GL_RGBA formátum esetén pedig mind a négy csatornába az általunk megadott szín kerül.
Ha GL_LUMINANCE-t adunk meg formátumnak, akkor az általunk megadott értékek bekerülnek mind a három csatornába (az a komponens felveszi a szokásos 1.0 értékét) míg GL_INTENSITY esetén mind a négy csatornába bekerülnek. Végére maradt a GL_LUMINANCE_ALPHA formátum, amelyben a GL_LUMINANCE eltérően az alfa csatornába is feltöltünk adatot. Ezeken kívül azonban léteznek speciális formátumok is.
Elsőként említsük meg a GL_BGRA/GL_BGR és a GL_ABGR formátumot, amelyekben mindössze a komponensek sorrendje eltérő. Sok platform a memóriában valójában ilyen sorrendben tárolja az adatokat, azaz feltöltéskor egy ún. swizzling játszódik le, azaz a komponensek felcserélődnek. Jelentőségét az adja, hogy amikor dinamikusan változó textúrát használunk, akkor lassítja a hardvert az állandó swizzling (lásd később dinamikus textúrák).
Második speciális formátum a GL_DEPTH_COMPONENT, amely általában a képernyő depth bufferéből származó adatok tárolására szolgál (lásd később shadow map).
A mai hardverek támogatják a GL_ARB_texture_float / GL_ARB_half_float_pixel formátumot, amely esetén a komponensek már lebegőpontos számok (32 vagy 16 biten) ill. a GL_EXT_texture_sRGB formátumot, amely ugyan 8 biten tárol, azonban az értékek nem-lineáris értéket képviselnek, azaz nem 0.0 és 1.0 közé esnek.
A formátumok használatakor az általunk textúraként használni kívánt képnek a fenti formátumnak kell megfelelnie, vagyis a színtömb elemeinek annyi komponensből kell állnia, ahány komponenst igényel a formátum, valamint meg kell adnunk a színtömb elemeinek a méretét (GL_BYTE...GL_FLOAT).

Texture compression
Itt kell még megemlítenünk az ún. textúra tömörítést (texture compression), amely a textúra kisebb helyfoglalását és gyorsabb felhasználását teszi lehetővé. Több algoritmus is létezik, de az alapötlet az S3 cégtől származik bár az óta számos változat megjelent:
  • GL_EXT_texture_compression_s3tc
  • GL_3DFX_texture_compression_FXT1
  • GL_NV_texture_compression_vtc
  • GL_EXT_texture_compression_dxt1
A textúra tömörítés két módon mehet végbe. Az első, amikor az OpenGL-lel végeztetjük el a kötés során. Ilyenkor általában az internalformat paraméter elé kell írnunk a COMPRESSED szót (például GL_RGB-ből GL_COMPRESSED_RGB lesz). Második lehetőség, amikor mi előre elvégezzük a tömörítést és ilyenkor általában a mipmap szinteket is, eltároljuk. A glTexImage parancsok helyett pedig a glCompressedTexImage parancsokat használjuk (ezeket leggyakrabban DirectDraw Surface (.dds) fileokban tárolják).

Border
Manapság ez a legkevésbé használt paramétere a textúráknak (általában szél nélküli textúrákat használunk) viszont egy fontos paraméter a szél színe, hiszen a GL_CLAMP_TO_BORDER wrap mód esetében ezt a színt használja az OpenGL.

Generate mipmap
Létezik egy extension (GL_SGIS_generate_mipmap), amely leváltja a glu függvényeket, mivel egy glTexImage hívással a hardver az összes részletességi szintet automatikus előállítja (a glu mindezt szoftveresen végzi). Erre a később említésre kerülő dinamikus textúrák esetében lesz szükség.

A texture targetek, mapek és a multitexture

Az OpenGL a textúrákat ún. targetek szerint csoportosítja. Ez alapján léteznek egydimenziós (GL_TEXTURE_1D), kétdimenziós (GL_TEXTURE_2D, GL_TEXTURE_RECTANGLE), háromdimenziós (GL_TEXTURE_3D, GL_TEXTURE_CUBE_MAP) és négydimenziós textúrák (GL_TEXTURE_4D)(ezt a textúrát mindössze az Intergraph kártyái ismerik).
Felhasználás szempontjából is csoportosíthatók a textúrák, amelyeket így mapeknek neveznek. A hagyományos textúra a decal map, a bump mappinghez és parallax mappinghez használatos a normal map, relief mappinghoz a relief map, displacement mappginhez a displacement map, tükröződéshez az environment map, a csillogás mértékének rögzítésére a specular map, az átlátszóságra a transparency map szolgál, míg árnyékokat a shadow mapben, fénytérképet pedig a light mapben tárolhatunk.
Manapság a decal mapet ritkán használják önmagában. Sokkal gyakoribb, hogy egy másik mappel együtt vesz részt a renderelésben vagyis egy renderelési fázisban több (legalább kettő) textúrát szükséges egy ponthoz rendelni. Ez az ún. többszörös textúrázás (multitexturing), amely már az OpenGL 1.2 verziójától a mag függvények (core function) között szerepel (olyannyira, hogy a GL_ARB_multitexture extension gyakorlatilag nem létezik).
A multitexture lényege tehát, hogy egy képpont színét több textúra, amelyeknek a neve textúra egység (texture unit) befolyásolhatja (az, hogy a képpont végleges színe hogy alakul a texture environment vagy valamilyen shader alakítja ki). A textúrázás texture unitonként szabályozható, azaz engedélyezhető, a texture environment konfigurálható és bármilyen texture target használható (ha az adott hardver támogatja az adott texture targetet). A szabályozni kívánt texture unit a glActiveTexture paranccsal állítható be.

A textúra koordináták és a textúra mátrix

Amikor a modellünkre „ráhúzunk” egy (vagy több) textúrát, tudnunk kell, hogy a modellünk vázának adott pontjaihoz, azaz a vertexekhez, a textúrá (i) nk melyik pontja (i) legyen (ek) hozzárendelve. Ezek az adatok leggyakrabban lebegőpontos számok (GL_FLOAT), amelyek általános esetben bármilyen értéket felvehetnek azonban a 0.0 és 1.0 tartomány szolgál a textúrán belüli hivatkozásnak (GL_TEXTURE_RECTANGLE esetében a textúra valódi méretei a tartomány határai, és egyedül a GL_TEXTURE_CUBE_MAP esetén -1.0 és 1.0 tartomány határai).
A koordinátákat megadhatjuk mi magunk is a glTexCoord függvénycsoporttal, amelyet a vertex feldolgozó az ún. textúra mátrixszal mindenkor össze fog szorozni. Természetesen ezen a mátrixon is elvégezhetőek a skálázás, eltolás, forgatás műveletek azonban a műveletek hatásai csak a textúra dimenzióiban lesznek hatásosak. A vertex feldolgozó képes akár automatikusan is számolni a koordinátákat, ha kivetítést, tükröződést, fényvisszaverődést szeretnénk létrehozni (természetesen ebben az esetben is megadhatunk textúra mátrixot).
Minden textúra egységhez külön meg kell adnunk vagy generáltatnunk a textúra koordinátákat, valamint külön-külön megadhatjuk a textúra mátrixot is. A felhasznált textúra dimenzióinak számától függően szükséges megadnunk a textúra koordinátákat, amelyek mindig négykomponensűek (s, t, r, q) (ezekkel függenek össze a fentebb említett wrap mode-ok). Ha egy dimenziós a textúra, akkor a koordináta rendszer is egy dimenziós és csak az s és q komponens él. Ha a dimenziók száma kettő, akkor a koordinátarendszer is kétdimenziós, aminek jobbra mutató iránya az s komponens felfelé irány pedig a t. Ha háromdimenziós textúrát használunk, akkor s, t, r alkotta koordinátarendszerben, kell gondolkodnunk, ahol az előre irányt az s komponens, a balra irányt a t komponens, míg a felfelé irányt (mélységet) az r komponens határozza meg. Érdemes jobban megvizsgálni a két 3D textúrát.
A GL_TEXTURE_CUBE_MAP a teljes 360 fokos teret reprezentálja egy „üres”, kétegységnyi hosszúságú kocka hat oldalán (ez a textúra összesen hat textúrából épül fel a tér hat irányának megfelelően). A textúra koordináták ilyenkor egy irányvektort jelölnek a kocka valamelyik lapjának texeléhez (ezért esik a koordináták tartománya -1.0 és 1.0 közé). A GL_TEXTURE_3D egyegységnyi oldalú tömör kockát reprezentál, amelyből a három textúra koordináta alapján kapunk meg egy texelt (itt a tartomány ugyanúgy, mint hagyományos esetben 0.0 és 1.0 közé esik).
Amennyiben valamelyik komponenst nem adjuk meg, akkor az, t és r esetében 0.0, q esetében pedig 1.0 lesz. A negyedik komponens kicsit kilóg a sorból, mert a négydimenziós textúra kivételével közvetlenül nem vesz részt a későbbi mintavételezésben. Az egy háromszöghöz tartozó textúra koordináták és normálvektorok alkotják együttesen az ún. tangent space-t, amelyben az s, t koordináta síkja adja a vízszintes síkot a felfelé vektort, pedig a normálvektor (a koordinátarendszer előre vektora az az irány, amely irányában az s koordináta növekszik, a balra irány pedig az, amerre a t koordináta). Ez a textúra tér az árnyálasnál hasznos ill. speciális esetekben nélkülözhetetlen. A koordináták az OpenGL pipeline-on végighaladva a vertexek között lineáris interpoláción esnek át és így kerülhetnek a texture fetch során felhasználásra.

A texture fetch (a textúra mintavételezés)

A texture fetch a fragment feldolgozás legelső és a legidőigényesebb lépése a raszterizáció után, amely során egy aktív textúrából a texture targetnek megfelelően történik (és csak a szerint történhet) egy texel kiolvasása egy ún. interpolátor és a textúra paraméterei alapján (a mai modern hardvereknek már a vertex feldolgozói is képesek a textúra elérésére).
Az interpolátor általános esetben az interpolált és az aktív textúrához tartozó textúra koordináta (ezen legfeljebb shaderekkel lehet változtatni). Abban az esetben, ha vetített módban történik a texture fetch, akkor az interpolátor első három komponense a negyedik komponenssel történő osztáson esik át (homogén osztás). A mintavételezés végeredménye pedig egy négykomponensű (r, g, b, a) regiszter, amely a leggyakrabban színt tárol (a texture targettől függően).

A texture environment

Hagyományos (shader nélküli) esetben a végeredményt (GL_TEXTURE) a texture environment (ill. a GL_ARB_texture_env_combine) használja fel a művelete során és adja tovább az így kapott végeredményt (GL_PREVIOUS) a következő environmentnek avagy a következő lépésnek. Tehát alapesetben egy texture environment csak a saját textúráját látja, a többit nem (a GL_ARB_texture_env_crossbar teszi lehetővé a többi textúra elérhetőségét).
A texture environmentnek nem csak a műveletét (GL_COMBINE) állíthatjuk be, hanem egy konstans színt (GL_CONSTANT) valamint az environmenthez tartozó textúra részletességi szintjét (GL_TEXTURE_LOD_BIAS).
A GL_ARB_texture_env_combine extensionnek létezik gyártófüggő továbbfejlesztése, amelyben újfajta műveleteket használhatunk (shaderek esetén majdnem teljes a szabadságunk, a fenti szabályt és a texture indirectionök számát betartva bármennyit mintavételezhetünk).

A dinamikus textúrák

Az eddigiekben a textúrákat mindig egy külön fileból olvastuk be ill. saját algoritmussal, állítottuk elő az alkalmazás indításakor. A videokártyák sebességének növelésével lassan megjelentek a valós időben készített ún. dinamikus textúrák (dynamic texture) amikor a textúrát folyamatosan módosítjuk. A módosítás történhet saját adatok felhasználásával, de jóval gyakoribb az az eset, amikor maga a videokártya által renderelt kép szolgáltatja a textúrát (render to texture).
Közös tulajdonságuk, hogy a textúra renderelése nem (vagy legfeljebb szemléltetés céljából) látható majd csak a végeredmény. Szinte az összes megoldás ún. off-screen buffert használ, azaz nem egy látható frame bufferbe rendereli (off-screen rendering) a kívánt képet, hanem a videokártya memóriájának egy másik területére, amelyet nekünk kell kezelnünk.

glTexSubImage
A legelső megoldást a glTexSubImage parancsok jelentették, amelyek az éppen aktív textúra egy adott területét frissítik az általunk megadott adattal. A lehető leglassabb megoldás, hiszen folyamatos kliens-szerver adatfolyamot hoz létre.

glCopyTexSubImage és glCopyTexImage
A következő lépcső a glCopyTexSubImage és a glCopyTexImage utasítások, amelyek már a frame buffer valamelyik bufferéből képesek olvasni az aktív textúrába, azaz már a render to texture kategóriába esnek. Sajnos az adat még itt is kikerül a szerver memóriájából.

WGL_ARB_pbuffer
Az első igazi off-screen módszer az ún. pixel buffer (WGL_ARB_pbuffer). Ez egy másodlagos frame buffert hoz létre a memóriában, amely az elsődleges frame bufferhez hasonlóan viselkedik kivéve, hogy a tartalma sohasem látható, de textúraként (csak GL_TEXTURE_2D és GL_TEXTURE_CUBE_MAP) felhasználható (WGL_ARB_render_texture). Ilyenkor vigyázni kell arra, hogy a pixel buffer kiterjedése kettő hatványa legyen (kivéve, ha az adott platform képes GL_TEXTURE_RECTANGLE texture targetbe renderelni (WGL_NV_render_texture_rectangle, WGL_ATI_render_texture_rectangle)). A textúra adatai természetesen a frame buffer valamelyik bufferéből kerülnek ki attól függően, hogy milyen internalformatokat ismer a videokártya. Egyetlen hátránya, hogy operációs rendszerfüggő (nem véletlen, hogy az extensionök W betűvel kezdődnek).

GL_ARB_pixel_buffer_object
A neve alapján ide tartozik a pixel buffer object, amely inkább a vertex buffer objecttel rokon mintsem a pixel bufferrel OpenGL object (kezelése egy általános objecthez hasonló). Ez az object egy frame buffer adatait képes gyorsan mozgatni a kliens memóriaterület felé (vagyis PACK) ill. a szerver memóriaterület felé (vagyis UNPACK).

GL_EXT_framebuffer_object
Végezetül következzen egy újabb object az ún. framebuffer object, amely a pixel buffer egy újabb, operációs rendszer független megoldása a render to texture módszerekhez. Itt is egy másodlagos frame buffert hozunk létre és ezt objectként tudjuk kezelni ill. a támogatott texture targetek között már szerepel a GL_TEXTURE_3D is.

Megjegyzés:
A textúra paraméterek között van egy speciális darab: GL_NV_texture_expand_normal, amely a textúra szűrés után beiktat egy „kicsomagolást” azaz a hagyományosan előjel nélküli texelekből előjeleset készít (0.0 és 1.0 tartományt átviszi -1.0 és 1.0 tartományba). Ez főleg akkor hasznos, amikor vektorok tárolására használjuk a textúránkat.
Syam (2006.11.29)

0 megjegyzés :

Megjegyzés küldése