50. fejezet - Fixed function trükkök


Most mindenki meglepödött, hogy hogy lehet ennek a sorszáma 50, hiszen már volt ilyen (és az most mágikus módon 60 lett). A magyarázat az, hogy elbasztam a számozást, ez ugyanis egy OpenGL-es tutoriál lesz, mégpedig a fixed function pipeline-os textúra kombinálókról.

Jogos a kérdés, hogy mégis mi a francért írok erröl, hiszen a mai világban már a Magdi néni is séderrel süti a rántottát...azért, mert meglepöen sok mindent meg lehet vele oldani (és ha netán valaki hasonlóan szerencsés helyzetbe kerülne mint én, akkor ne szivjon vele...sokat...)

Amiröl nem fogok elárulni semmit, az az OpenGL inicializálás, ugyanis ezt mindenki álmából felébresztve keni-vágja. Ha esetleg nem, akkor rossz helyen jársz (javaslom inkább a NeHe tutoriálokat). Ahogy mondani szokták in medias res bele is vágok a közepébe.

Kombinatorikából egyes

Úgy lehet elképzelni ezt a dolgot, mint egy ilyen kitöltendö egyenletet, ahol lehet buherálni az argumentumokat is, és az operátorokat is. Jobb kártyákon van 4 texture unit (nem igaz, új kártyákon több is van, de azok shaderekben használhatóak), mindegyikkel lehet csinálni valami érdekes dolgot. Például:

CODE
outcolor = ((vertexcolor * tex0) + tex1) * constant;

Ez itt egy pseudo kód, ami azt jelenti, hogy semmi értelmes nincs mögötte. Régi kártyákon lehet 4-nél kevesebb stage is (GL_MAX_TEXTURE_UNITS).

Az elsö és legfontosabb dolog, hogy meg kell érteni ezt a vacak OpenGL-t. Míg DirectX-ben csak megmondod, hogy ebbe a stagebe akarod rakni ezt a textúrát, itt kicsit másabb a helyzet, mert az OpenGL olyan mint egy frissen diplomázott autista: lépésenként el kell neki mondani, hogy mi a rákot akarsz csinálni.

CODE
glActiveTexture(GL_TEXTURE0); glEnable(GL_TEXTURE_2D); glBindTexture(GL_TEXTURE_2D, texture1);

A helyzet ennél csak rosszabb lesz, mert 10-böl 11 ember tuti elfelejti visszaállítani az elözö állapotot. Amit rögtön leszögeznék, mert baromi fontos, hogy a textúra kombinátor egy adott stagere csak akkor müködik, ha engedélyezve van és be van bindolva rá textúra (és van hozzá textúra koordináta, bár nekem néha anélkül is megy). A textúra pedig csak akkor müködik, ha adtál neki filtert és address módot.

Az OpenGL jó szokásához híven minden beállításra ugyanazt a függvényt kell meghívni, csak más paraméterrel. Például ha össze akarod szorozni a vertex colort a textúrával, akkor:

CODE
glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE); glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_MODULATE); glTexEnvi(GL_TEXTURE_ENV, GL_SRC0_RGB, GL_PRIMARY_COLOR); glTexEnvi(GL_TEXTURE_ENV, GL_SRC1_RGB, GL_TEXTURE);

Hasonlóság fedezhetö fel a DirectX-el, de sokkal több mindent be lehet állítani, totál fölöslegesen. Nyilván a neveket lecserélve megmondható az alfával történö maszatolás is.


Moduláld magad

Egy flancos elnevezés a szorzásra; aki látott már rádiót az tudja, hogy van rajta egy kapcsoló (AM vagy FM). Elöbbi amplitúdó modulációval kódolja a jelet (tipikusan egy sinus jel a hordozó, ennek az erőssége a küldendö adat), az utóbbi frekvencia modulációval (az adat a sinus oszcillációinak sürüsége). Höfö rádiót építeni.

Ja és persze a szín a fény, mint szignál frekvenciája. Ezért tök logikus, hogy modulációnak hívják...nem? Csináljunk olyat, hogy összeszorzunk két textúrát, mondjuk egy fadarabot az OpenGL logóval, ezzel is szemléltetve, hogy milyen fapados.

CODE
float* vert = &vertices[0]; // ... glClientActiveTexture(GL_TEXTURE0); glEnableClientState(GL_TEXTURE_COORD_ARRAY); glTexCoordPointer(2, GL_FLOAT, sizeof(CustomVertex), vert + 6); glClientActiveTexture(GL_TEXTURE1); glEnableClientState(GL_TEXTURE_COORD_ARRAY); glTexCoordPointer(2, GL_FLOAT, sizeof(CustomVertex), vert + 8); glActiveTexture(GL_TEXTURE0); glEnable(GL_TEXTURE_2D); glBindTexture(GL_TEXTURE_2D, texture1); // itt nem állítok be semmit, mert jó a default glActiveTexture(GL_TEXTURE1); glEnable(GL_TEXTURE_2D); glBindTexture(GL_TEXTURE_2D, texture2); glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE); glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_MODULATE); glTexEnvi(GL_TEXTURE_ENV, GL_SRC0_RGB, GL_PREVIOUS); glTexEnvi(GL_TEXTURE_ENV, GL_SRC1_RGB, GL_TEXTURE); // rajz

Nem csak ezt a GL_COMBINE-t (hehe, kombiné...) lehet megadni, hanem explicite mondjuk GL_MODULATE-t is. A combine viszont extension (GL_ARB_texture_env_combine), úgyhogy ha nincs akkor nem használhatod. Megtiltom.

Még mielött érdekes lenne a fejezet, maszatolok valamit az alfával is, csak hogy ne mondhassátok azt, hogy elhallgatok dolgokat.

CODE
glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_ALPHA, GL_INTERPOLATE); glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE0_ALPHA, GL_PREVIOUS); glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE1_ALPHA, GL_TEXTURE); glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE2_ALPHA, GL_TEXTURE); glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_ALPHA, GL_SRC_ALPHA); glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND1_ALPHA, GL_SRC_ALPHA); glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND2_ALPHA, GL_SRC_ALPHA);

Ez tökre hasonlít a klasszikus alpha blendingre. Elárulnám hogy azért, mert ugyanaz. Söt, ugyanazok a konstansok müködnek, mint ott (pl. GL_ONE_MINUS_SRC_ALPHA). Látható, hogy az operandusok buherálása által rengetegféle effektet meg lehet csinálni.


Szaturálós szatír mágia

Olyan feladatot kaptam, hogy csináljak grayscale képet egy színes modellböl. Ja és futnia kell a legszarabb Intel GMA-n is, ami kb. annyit tud a shaderekröl, mint az OpenGL az OOP-röl. Azt még óvodában tanultam, hogy ha a színkomponensek megfelelöen súlyozott átlagát veszem, akkor az még akár szürke is lehet. Jobb helyeken a skaláris szorzást használják erre, tehát adta magát, hogy a GL_ARB_texture_env_dot3 extensiont használjam.

Hogyaszondja, ez fogja a két értéket, kivon belölük felet, összedotolja, majd beszorozza néggyel. Ezen a ponton én még vártam volna egy inverz kettös integrál számolást is polárkoorinátatérben, de ezt úgylátszik lusták voltak megcsinálni. Na de a viccet félretéve, ezt az extensiönt bump mappinghez találták ki, amit meg is csinálok majd. De elöbb nézzük mi történne:

CODE
result = 4 * <(r, g, b) - 0.5, (0.2, 0.5, 0.1) - 0.5> result = <(r, g, b) * 2 - 1, (0.2, 0.5, 0.1) * 2 - 1>

Ez ismerös a normal mapos tutoriálból. Nyilván most nem kell, úgyhogy kompenzálni kéne. Gondolatban a 4-et be lehet vinni valamelyik tagba, mondjuk a másodikba, tehát akkor a kompenzált grayscale értékek:

CODE
(0.2, 0.5, 0.1) / 4.0 + 0.5 = (0.55, 0.625, 0.525)

A colorhoz pedig hozzáadunk 0.5-öt, mint konstans (mert azt is lehet, GL_ADD és GL_CONSTANT). A meglepö az, hogy némelyik kártyán müködik így is, de helytelen. Ugyanis bizonyos kártyák a nem [0, 1]-beli értékeket visszaclampolják oda és a kép totál szürke lesz (konkrétan mac-en jött elö).

Na akkor nézzük mégegyszer. Vigyük be a 4-et inkább az elsö tagba. Ekkor a grayscale-hez kell hozzáadni a 0.5-öt, viszont két textúra kombinátort is el kell használni a * 0.25 + 0.5 dologra. A jó ebben az, hogy a grayscale értékek szerencsére elég kicsik, hogy még a [0, 1] intervallumban maradjanak az offset után is.

CODE
float scale[] = { 0.25f, 0.25f, 0.25f, 1 }; float offset[] = { 0.5f, 0.5f, 0.5f, 0 }; float gray[] = { 0.299f, 0.5f, 0.114f, 1 }; gray[0] = gray[0] + 0.5f; gray[1] = gray[1] + 0.5f; gray[2] = gray[2] + 0.5f; // GL_TEXTURE1 glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE); glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_MODULATE); glTexEnvi(GL_TEXTURE_ENV, GL_SRC0_RGB, GL_PREVIOUS); glTexEnvi(GL_TEXTURE_ENV, GL_SRC1_RGB, GL_CONSTANT); glTexEnvfv(GL_TEXTURE_ENV, GL_TEXTURE_ENV_COLOR, scale); // GL_TEXTURE2 glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE); glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_ADD); glTexEnvi(GL_TEXTURE_ENV, GL_SRC0_RGB, GL_PREVIOUS); glTexEnvi(GL_TEXTURE_ENV, GL_SRC1_RGB, GL_CONSTANT); glTexEnvfv(GL_TEXTURE_ENV, GL_TEXTURE_ENV_COLOR, offset); // GL_TEXTURE3 (itt történik a grayscale) glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE); glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_DOT3_RGB); glTexEnvi(GL_TEXTURE_ENV, GL_SRC0_RGB, GL_PREVIOUS); glTexEnvi(GL_TEXTURE_ENV, GL_SRC1_RGB, GL_CONSTANT); glTexEnvfv(GL_TEXTURE_ENV, GL_TEXTURE_ENV_COLOR, gray);

A harmadik lehetöség a + 1 / 2 de ez megintcsak rossz ugyanazért, tehát ezt a megoldást kell szeretni... Külön rossz, hogy mivel ilyen sok kombinátort elhasznál, elég limitált, hogy hány réteget tudsz használni a modellen (egyet, de az esetemben a második réteg eleve szürke volt, így tudtam kettöt).


Bump

Nekem sem egyértelmü, hogy most mi a bump mapping és mi a normal mapping mert összevissza hallani mindkettöt. Én elöbbinek azt tekintem, amikor az alfa csatornában van egy szorzó, ami felületi tulajdonságokat mond meg (pl. egy téglafalban a tégla fényes, a malter pedig matt) amivel a fényintenzitást beszorozza. Utóbbi pedig a klasszikus normal mapping, tehát itt most azt fogom megcsinálni.

Most mindenki elökeresi gondolatban a normal mapos tutoriált. Volt ott ilyen állatság hogy tangent space meg valami ldir amit át kellett pakolni, meg volt mátrixszorzás is. Na hát ezt mégis hogy a bánatba lehet megcsinálni? Természetesen csalással. Azt mondom, hogy egy vertexnek van poziciója, normálja, texkoordja és a fényvektor a tangens térben. Minden vertexre. Bár pazarlónak tünik, ez a megoldás.

Elöször is némi okítás arról, hogy hogyan kell kiszámolni a tangent frame-et. A normálokat ki lehet úgy, hogy kiszámolod a háromszögek normálját (mondjuk b - a × c - a), majd minden vertexre átlagolod azokat a háromszögnormálokat, amiknek ö csúcsa. A tangens kiszámolása kicsit nehezebb: figyelembe kell venni mindkét texkoord megváltozását, amit beszorzol az elöbbi b - a és c - a val, majd veszed a különbségüket. A binormál innen már könnyü, mert vektoriálisan szorzod a normált és a tangenst, majd a tangenst újraszámolod a normálból és a binormálból ugyanígy (ugyanis senki nem garantálta, hogy meröleges lesz a normálra). A kódot megnézitek, én Gram-Shmidt ortogonalizációt használtam inkább, mert ilyen ügyi vagyok.

Ez akkor meg is van, elég egy külön memória területen eltárolni, mert a fényvektort a CPU-n transzformálom át a tangens térbe. Ez ugyanaz, mint amit a vertex shader csinálna (csak lassú).

CODE
float ldir[3]; float wpos[3]; float wtan[3]; float wbin[3]; float wnorm[3]; for( int i = 0; i < 24; ++i ) { const AdditionalData& tb = tangentframe[i]; CustomVertex& v = ((CustomVertex*)vertices)[i]; wpos[0] = v.x * world[0] + v.y * world[4] + v.z * world[8] + world[12]; wpos[1] = v.x * world[1] + v.y * world[5] + v.z * world[9] + world[13]; wpos[2] = v.x * world[2] + v.y * world[6] + v.z * world[10] + world[14]; wtan[0] = tb.tx * world[0] + tb.ty * world[4] + tb.tz * world[8]; wtan[1] = tb.tx * world[1] + tb.ty * world[5] + tb.tz * world[9]; wtan[2] = tb.tx * world[2] + tb.ty * world[6] + tb.tz * world[10]; wbin[0] = tb.bx * world[0] + tb.by * world[4] + tb.bz * world[8]; wbin[1] = tb.bx * world[1] + tb.by * world[5] + tb.bz * world[9]; wbin[2] = tb.bx * world[2] + tb.by * world[6] + tb.bz * world[10]; wnorm[0] = v.nx * world[0] + v.ny * world[4] + v.nz * world[8]; wnorm[1] = v.nx * world[1] + v.ny * world[5] + v.nz * world[9]; wnorm[2] = v.nx * world[2] + v.ny * world[6] + v.nz * world[10]; ldir[0] = lightpos[0] - wpos[0]; ldir[1] = lightpos[1] - wpos[1]; ldir[2] = lightpos[2] - wpos[2]; v.u3 = dot(wtan, ldir); v.v3 = dot(wbin, ldir); v.w3 = dot(wnorm, ldir); }

Eddigi prédikálásomat semmibe véve a normált is a world mátrixxal transzformáltam (mea culpa). Az meg, hogy tömböket használok kimondottan eretnekség. Höfö megcsinálni vektorral.

Még egy dolog van amit sejteni lehetett: normalizálni kéne a TBN-beli fényvektort (nem a vertexben!! interpoláció!!). Erre szoktak használni normalization cube map-et, söt még a shaderek höskorában is (nekem mondjuk gyanús, hogy egy olvasás mért olcsóbb, mint egy normalize). Copy-paste.

Na akkor fözzük meg a levest:
  • unit0: normal map
  • unit1: cube map
  • unit2: fa textúra
Az elsö stage olvas a normal mapból. A második stage olvas a cubemapból (méghozzá a fényvektorral) és dotolja az elözövel. A harmadik stage még rászorozza a fadarabot. Gondolom ehhez nem kell bemásolnom a kódot.

Érdekes gondolat, hogy vajon ki lehet-e úgy böviteni, hogy speculart is tudjon? Szerintem nincs elég stage hozzá, úgyhogy nem.


Ríflektáld az invájrönmentet

Elvetelmültebb padawanok most vadul elkezdtek morfondírozni azon, hogy vajon mért ne lehetne valami tükrözödést is csinálni hasonlóan, mint a fényvektorral, csak mondjuk inkább az eye vektorral. Persze, úgy is meg lehet csinálni, de erre van egy sokkal könnyebb megoldás.

Ugyanis van ilyen dolog, hogy az OpenGL tud generálni textúra koordinátákat. Söt, hogy a programozó élete még kényelmesebb legyen, elöre meg van csinálva a sphere mapping és a reflection mapping. Utóbbit majdnem ki is lehet találni, hogy a reflektált wpos - eyepos vektorral megcímez egy cubemapet.

Rajzolás elött meg kell mondani az OpenGL-nek, hogy bizony te most generáltatni fogod vele a texkoordokat. Ez szintén texunitra vonatkozik.

CODE
glActiveTexture(GL_TEXTURE0); glEnable(GL_TEXTURE_2D); glEnable(GL_TEXTURE_GEN_S); glEnable(GL_TEXTURE_GEN_T);

Illetve egy teljesen ártalmatlannak látszó hívással azt is közlöd vele, hogy hogyan. A képleteket megnézitek guglin.

CODE
glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_SPHERE_MAP); glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_SPHERE_MAP);

Az igazi perverzió az, amikor mindegyiknek másmilyet adsz meg. Höfö kitalálni egy szitut, amikor az kell (az én tippem az, hogy nem elhanyagolható mennyiségü feles után programozni próbálsz...).


Summarum

Megmutattam, hogy milyen ádáz dolgokra képes a fixed function pipeline. Bár hamarosan teljesen kihal, régi kártyák egyelöre még vannak. Kód a szokott helyen.

combiners



Höfö:
  • Csináld meg DirectX-el!
  • Igen, mindet. Azt is amit nem lehet.



back to homepage

Valid HTML 4.01 Transitional Valid CSS!