81. fejezet
A Quadron VP fénykezelése
(implementációs részletek)


Bármilyen endzsinnek gúnyolt kódhalmaz egy sarkalatos pontja a pongyolán mondva fotoreaisztikus megjelenítés, melynek legfontosabb eleme a fények, illetve megvilágítás kezelése. A korábban írt deferred lighting-os tutoriál az előkészítése volt annak amit most az elmúlt hónapokban az engineben kidolgoztam.

Ebben a cikkben ismertetni fogom a fénykezelés helyét a motorban, a megvalósítás részleteit, illetve árnyékolással kapcsolatos problémákat és azok megoldását.

Régi szép idők

Kezdeném azzal, hogy hogyan nézett ki az engine a fejlesztés előtt. A jelenetet egy jelenetleíró XML fájlban lehet megadni, természetesen ez a hozzám hasonló lusta programozóknak jelent segítséget. Kicsit kockábbak meg tudják csinálni kódból is, de a jövőre nézve mindenképpen célszerűbb az XML-t használni, hiszen a majdani editorral ezen keresztül fog kommunikálni. Na de egy példa:

CODE
<?xml version="1.0" encoding="utf-8"?> <scene> <def id="def_grass"> <material id="grass" file="effects/blinnphong.qfx"> <uniform name="lightPos" value="0;50;-50" /> <texture stages="0" file="textures/grass.jpg" /> </material> </def> <transform scale="1000;10;1000" translate="0;-40;0"> <use href="#def_grass"> <box /> </use> </transform> </scene>

A részlet a homokozóból van (itt szoktam kitesztelni dolgokat). Tipikusan az egész XML fájl ilyenekből áll, illetve lehet részfákat definiálni a <def> taggel, így több objektumnak meg lehet adni pl. ugyanazt a materialt a <use> taggel. Ebből az XML-bl épül fel a színtérgráf. Részletesebben ebbe most nem megyek bele.

Amit fontos elmagyarázni az az engine material fogalma, ez ugyanis nem csak felületi tulajdonságok (diffuse, specular, opacity, stb.) halmaza, hanem shadereket is tartalmazhat. A material tehát megfelel egy effekt fájlnak kiegészítve az említett felületi tulajdonságokkal. Az objektumok egyes részeinek (subset) külön-külön materialja lehet.

Egy még fontosabb dolog, hogy a materialokat két csoportba lehet osztani aszerint, hogy hogyan akarod használni. Világos, hogy vannak esetek, amikor egy effektet sok objektumra akarsz alkalmazni (pl. kirajzolni a normálokat), de azoknak már van saját materialja. Azt a materialt ami objektumhoz tartozik lokálisnak hívom. Nem nehéz kitalálni, hogy akkor van globális is, de mondok egy meglepő dolgot: van parciális is. Ezt az enginebe még nem implementáltam le (máshol igen), amúgymeg elfelejtettem mit csinál (de most pont kéne).

No de, ahhoz hogy ez működjön bevezettem egy új render state-et: material mode. Amikor egy globális material-t akarsz használni a következőt mondod:

CODE
myglobalmat->IsGlobal = true; // ... Graphics->SetMaterialMode(qMM_Global); myglobalmat->Apply(); { Scene->Render(); } myglobalmat->Unapply(); Graphics->SetMaterialMode(qMM_Object); // vissza lokálisba

Ez az új render state annyit csinál, hogy egyrészt csak globális effektet enged beállítani, másrészt minden lokális material felületi tulajdonságait átpasszolja az éppen beállított globális materialnak (amikor beállítódnának). Ez már nagyon régóta benne van az engineben és nagyon sok mindent meg lehet vele csinálni (a mostani fénykezelés is erősen épít rá).

Visszatérve a jelenetre: eddig ha egy materialnak nem volt effektje, akkor a fixed function pipeline-al rajzolódott (ez mindig van, ha máshogy nem, emulálva). Az egyik fontos változás most, hogy ez továbbra is igaz, de csak akkor ha a custom pipeline van bekapcsolva (ezekről később). Tehát eddig minden megvilágításhoz itt kellett megadni a használandó shadert, nem volt általánosan összeszedve. Ez elég problémás volt, mert több hasonló effekt fájlt kellett karbantartani.


Látom a fényt!

Az első lépés az volt, hogy kitaláljam hogyan fognak elhelyezkedni a fények a színtérgráfban. Első ötlet az volt, hogy hierarchikusan, mint a material és transform csúcsok. Ezt azért vetettem el, mert nehéz volt azt leírni, hogy egy fény két totál más helyen (az XML-en belül) levő objektumra hat. Van olyan objektum amire mondjuk három fény hat, egy másikra ebből csak kettő; nagyon hamar összegubancolódsz az XML fában, az implementációról nem is beszélve.

A fények tehát nem hierarchikusak, hanem az XML-en belül akárhova elhelyezhetők (de tipikusan a <scene> tag alatti szintre). Ennek az a következménye, hogy elméletileg minden fény hat minden objektumra, de a gyakorlatban el kell dönteni, hogy ténylegesen hat-e egy objektumra egy adott fény (ez a forward renderernél és árnyékgenerálásnál fontos).

Egy további kérdés volt, hogy a régi funkcionalitást hogyan tartsam meg, illetve hogyan engedjem meg, hogy custom effektet lehessen adni bizonyos objektumoknak. Végül a következő három pipeline-t rögzítettem le:

  • Custom: az ami eddig volt, tehát minden objektumnak saját effektje lehet (avagy FFP-n rajzolódik).
  • Forward: minden olyan objektum aminek nincs effektje megvilágítódik a jelentbeli fényekkel. A megjelenítési stratégia egy multipass forward renderer, tehát minden objektum annyiszor rajzolódik ahány fény hat rá. Ha egy objektumnak van effektje, akkor azzal rajzolódik, illetve ha az emissive tulajdonsága nem nulla, akkor nem fog rá megvilágítás számolódni, hanem kirajzolódik azzal a színnel.
  • Deferred: ugyanaz mint előbb, de deferred lighting-al.
A pipelinet a <scene> tagben lehet megadni, pl. <scene pipeline="forward">. Mobil kütyükön csak a forward érhető el (de az is lassú, nagyon ki kell majd optimalizálni), desktopon a kártya képességei döntik el. A deferred rendererhez kötelező a shader model 3.0, a forwardhoz elég 2.0 is, de némi megszorítás van, ezekről később. A deferred renderer a forwardra épül, tehát bármikor tud fallbackelni rá.


Rendelj előre

Először a forward renderert mutatom be, ugyanis az implementáció oroszlánrésze itt van. A fent emlegetett színtérgráfot nodevisitor-ok segítségével lehet bejárni (látogató tervminta); speciel a renderer is ilyen. Sokáig problémát okozott, hogy miben tároljam a rajzoláshoz szükséges információt, hiszen:

  • ismerni kell egy objektumhoz tartozó transzformációkat (instance)
  • az objektumokat rendezni kell shader szerint
  • a fényeket típus illetve árnyék szerint
  • az átlátszó objektumokat mélység szerint
Ehhez egy gyors adatszerkezetre van szükség, egy megfelelő rendezési algoritmussal. Ez az a pont ahol STL konkténereket nem használok, mert nem engedhetem meg azt a szintű sebességcsökkenést. A konténerek tehát helyben vannak leimplementálva:
  • transzformációs mátrixok (tömb)
  • telített subsetek (tömb): rendezve normal map és objektum ID szerint
  • custom objektumok (tömb): rendezve shader és textúra szerint
  • batchelt átlátszó objektumok (tömb): jelenleg csak egy ilyen lehet, ugyanis baromi nehéz berendezni
Amit itt felsoroltam az a belépőszint, azaz ezeket minden renderer tárolja. Látszólag megkérdőjelezhető, hpgy miért van külön ennyi minden, de gondold meg, hogy pl. a transzformációs mátrixokra igen sok helyen szükség van. Márpedig nem kéne pazarolni a memóriát, ha meg lehet oldani olcsóbban. Nézzük mit ad ehhez hozzá a forward renderer:
  • fények (tömb): fénytípus és árnyék szerint rendezve (van-e vagy nincs)
  • átlátszó subsetek (tömb): z szerint rendezve
  • mikre hat (hashtábla): egy adott fény mely objektumokra hat
  • mik hatnak rá (hashtábla): egy adott instance-ra mely fények hatnak
Az előbbi hashtábla a shadowmap generáláshoz kell, az utóbbi az átlátszó objektumok rajzolásához. A felsorolt táblák és tömbök minden bejáráskor újraépítődnek, tehát nagyon kritikus, hogy hogyan vannak leimplementálva. A memóriaterületük csak indokolt esetben foglalódik újra (pl. megváltozott a fények száma), egyébként a legtöbbnek fix terület áll rendelkezésre (pl. egyszerre 5000 objektum lehet a képernyőn, átlagosan 5 subsettel). Ha ez kevésnek tűnik, akkor egy helyen átírod és használhatsz többet, de őszintén szólva én nem látom indokoltnak.

A renderelés menete a következő:
  • fények begyűjtése; pont és spot fényekre view frustum culling
  • fények rendezése
  • if( renderflags & Cull )
    • minden objektumra VFC
    • ha látható, akkor hozzáadni a megfelelő táblához
    • hozzáadni a fényekhez ("mikre hat")
    • telítetteket rendezni
    • custom objektumokat rendezni
  • if( renderflags & Transparent )
    • ha kell rendezés, akkor átlátszóakat rendezni
    • ha kell rendezés, akkor a batcheket felosztani, majd átlátszóakat újrarendezni
    • ha nem kell rendezés, akkor csak hozzáadni a táblákhoz
  • fényekre ható objektumokat rendezni
  • if( renderflags & Shadow )
    • shadowmapek elkészítése
  • if( renderflags & Opaque )
    • z pass: feltölteni a depth buffert
    • a fényekre 4-es csoportokban:
      • ha kell, akkor shadow mask elkészítése (későbbi bekezdés)
      • depth write disable, additive blend
      • affektált objektumok kirajzolása
  • custom objektumok rajzolása
  • if( renderflags & Transparent )
    • átlátszó objektumok rajzolása (de nem hat rájuk árnyék)
A rendezés minden esetben beszúró rendezés, ugyanis a tömbök általában kicsik, másrészt a sorrend ritkán szokott változni, márpedig ez a fajta algoritmus már rendezett tömbre lineáris futási idejű (míg a gyorsrendezés O(n log n)). A tömbök nem önmagukban vannak rendezve (hiszen tipikusan struktúrákat tárolnak), hanem mindegyikhez tartozik egy index tömb és az rendeződik.

Látható, hogy elég sok mindent művel a renderer, de azt legalább gyorsan teszi. A z pre-pass nagyon fontos, hogy minimalizáljad a fölösleges pixel shader hívásokat. A legtöbb GPU-ban van early z test, azaz ha a fragment takarásban van, akkor a pixel shader nem hívódik meg.

Megemlítendő még a custom objektumok rajzolása, ugyanis azokra a következő szabályok érvényesülnek:
  • ha van shadere, akkor azzal rajzolódik
  • ha nincs shadere, de van textúrája, akkor a degamma effekttel rajzolódik
  • ha egyik sincs neki, akkor ahogy eddig, az FFP-n fog rajzolódni
Amit eddig leírtam az a CPU oldali rész. A renderer többi része a forward effekt fájlban van, erről majd a shadow mask-os bekezdésben írok. A mobilra való optimalizáláshoz ezt a cikket érdemes elolvasni.


Éclairage de Ferred

Mivel a forward renderer minden szükséges információt előállít, a deferred renderernek nincs túl sok dolga. Az objektumok rendezésénél megmelítettem a normal map figyelembevételét, ugyanis az egy külön shader, és azért vannak az objektumok így rendezve, hogy ne kelljen sokszor váltogatni. Hasonlóan a fények is ezért vannak rendezve.

  • a shadowmapek elkészítésével bezárólag ugyanaz mint a forward
  • g-buffer pass; normálok + power (ARGB16F), depth (R32F)
    • normal mapes subsetek rajzolása (a normált world spacebe trafózva)
    • rendes subsetek rajzolása
  • irradiance pass; diffuse (ARGB16F), specular (ARGB16F)
    • additive blend, scissor test bekapcs
    • fényenként egy screenquad
  • forward pass (és szerintem itt lehet bekapcsolni az MSAA-t)
    • telített subsetek kirajzolása, immár materiallal és textúrával
  • custom objektumok rajzolása
  • forward renderer meghívása transzparensekre
A forward renderenél is már képbe kerül, de itt említem meg, hogy a material mode például a normálok kirajzolásakor hasznos. Felvettem a flagek közé néhány újat, például qMM_LockBlend és qMM_LockTextures. Ezek azért kellenek, hogy a blend mode-ot/textúrákat se tudja átállítani egy lokális material (mert egyébként megteszi).

Rögtön kiemelnék egy fontos dolgot DirectX-el kapcsolatban. Gondolom mindenki ismeri a nevezetes cikket, melynek lényege, hogy a screenquadot -0.5-el el kell tolni screen spaceben. Azért fontos ez, mert a harmadik, azaz a forward passban ezt kompenzálni kell:

CODE
float2 ptex = cpos.xy / cpos.w; #ifdef __HLSL ptex.xy += renderParams.zw; // 0.5f / screen size #endif // read irradiance half4 dlight = tex2D(sampler4, ptex); half4 slight = tex2D(sampler5, ptex); // ...

Ha ezt nem teszed meg, akkor egy jól látható csík lesz az objektumok bal oldalán. OpenGL-ben és DX10-től fölfele már nem kell ezzel szórakozni. A deferred rendererre nem is fecsérelnék több szót, mert szinte teljesen ugyanaz mint a tutoriálban.


Unit cube clipping

Szerintem elég hülye elnevezés, de ettől most tekintsünk el. A feladat az, hogy a shadow map a lehető legoptimálisabban legyen kitöltve mindhárom irányban. Tehát a projekciós mátrixot kell ráilleszteni az árnyékvető objektumokra. A pont fény a legegyszerűbb, hiszen ott a far plane legyen a fény sugara és kb. ennyit tudtál tenni. Meg kell említeni a Crysis 2-t, ott ugyanis a pont fény 6 darab különálló fény (árnyék szempontból), és mint ilyen különböző paraméterek vonatkozhatnak mindegyik részre.

Az irányított fény kicsit elgondolkodtatóbb, de még az is viszonylag könnyú, hiszen merőleges vetítést használ. Amit merőleges vetítésről tudni kell, hogy egy téglatestet határoz meg a térben, azaz a [left, right] x [bottom, top] x [near, far] tartományt. Csináljuk a következőt:

CODE
qAABoundingBox casterbox = SZUMMA(<shadow_casterek_boundingboxa>); qVector3 refpoint = casterbox.GetCenter(); qVector3 forward = light->GetDirection(); qVector3 right = qVector3::Cross(<megfelelő_up_vektor>, forward); qVector3 up = qVector3::Cross(forward, right); qPlane leftright = qPlane::FromRay(refpoint, right); qPlane bottomtop = qPlane::FromRay(refpoint, up); qPlane nearfar = qPlane::FromRay(refpoint, forward); float r = casterbox.Farthest(leftright) - leftright.d; float l = -casterbox.Farthest(-leftright) - leftright.d; float t = casterbox.Farthest(bottomtop) - bottomtop.d; float b = -casterbox.Farthest(-bottomtop) - bottomtop.d; float f = casterbox.Farthest(nearfar) - nearfar.d; float n = -casterbox.Farthest(-nearfar) - nearfar.d; view = qMatrix4::ViewBasisLH(qVector3::Zero, forward, right, up); proj = qMatrix4::OrthoLH(l, r, b, t, n, f);

Mint látható a nézeti mátrix egy sima forgatás (merőleges vetítésben nincs kamera pozíció), tehát a projekciós mátrixban szereplő értékek abszolútak (az origótől mérendők), ezért kell levonni a sík távolságát.

directional

Ez még mindig egy viszonylag könnyű dolog volt. A legnehezebb azonban a spotlight, illetve akkor nehéz, ha mindenáron meg akarod csinálni a unit cube clippinget. Gondoljuk végig mit kell csinálni.

A frustum most egy csonkagúla, tegyük fel, hogy a nagy része üres. Tehát valahogy ezt a gúlát kéne úgy megvagdalni (egy nem szimmetrikus gúlára), hogy befoglalja az árnyékvetőket. Az előbb world spaceben számoltam ki a szükséges értékeket, most meg kell fordítani a dolgot: vetítsük le a casterbox-ot screen spacebe és legyen az így kapott két érték ssmin és ssmax.

CODE
ssmin.x = (ssmin.x * n) / proj._11; ssmin.y = (ssmin.y * n) / proj._22; ssmax.x = (ssmax.x * n) / proj._11; ssmax.y = (ssmax.y * n) / proj._22; // create assymetric projection proj = qMatrix4::PerspectiveOffCenterLH(ssmin.x, ssmax.x, ssmin.y, ssmax.y, n, f);

Mi történik itt: NDC-ből visszakonvertál view spacebe. Ha felrajzolod azonnal látszik (proj._11 = 1 / tan(fovy / 2)). A mátrixos függvényt meg tudod nézni a D3DX dokumentációban, teljesen hasonló egyébként mint a fenti ortho, csak perspektív zw-vel.

Megjegyezném, hogy ez nemlineáris mélységet fog csinálni, tehát a vertex shaderben fel kell szorozni a távoli vágósíkkal. Az nem jó, ha átírod a mátrixot 1 / (f - n) és -n / (f - n)-re, mert az nem a [0, 1]-be fogja transzformálni a z-t hanem [0, 1 / f]-be!


A shadow acne probléma

Ettől viszont falra másztam. De mi ez tulajdonképpen? Arra mindenki rájött, hogy a shadow map felbontása és precíziója véges, melynek az a következménye hogy az objektum hibás módon saját magára is vet árnyékot.

acne

A képen is jól látható, hogy a mélység diszkretizációja milyen mintát varázsol az objektum felületén. Ez egy örök probléma, amit teljesen soha nem lehet megoldani. Viszont lényegesen lehet rajta javítani különféle trükkökkel.

Ezek közül többet is kipróbáltam (a Secrets of CryEngine 3 papert bújva):
  • konstans bias: ha túl kicsi, akkor nem szünteti meg az acnet; ha túl nagy, akkor peter pannel az árnyék
  • slope scale depth bias: nem közvetlenül a depth-et írom, ezért nem lehet használni
  • gradient based bias: shader model 3 kell hozzá (ddx, ddy)
  • back faceket renderelni és nincs bias: kétoldalú felületeknél acne; nem zárt felületeknél hiányzó árnyék
Végül syam kollégával közös megegyezés alapján a legutolsót választottam, megbékélve azzal, hogy a modelljeimet javítani kell. Viszont elmagyaráznám a gradiens alapú biasolást, mert kevés anyag van róla, viszont majdnem megoldja a problémát.

Először is mit csinál a ddx, illetve GLSL-ben a dFdx függvény? A dokumentáció ilyen mágikus szavakat ír, mint parciális derivált; hát persze hogy az, de gondoljuk meg, hogy pixeleket raszterizálunk, azaz egy diszkrét függvény deriváltjáról van szó, amit pongyolán nevezhetünk különbségnek.

Ezt azért tudja megtenni a kártya, mert nem egyenként dolgozza fel a pixeleket, hanem 2x2-es blokkokban (de még mindig párhuzamosan!). Egy blokkon belül mindegyik pixelre meg tudja mondani a többitől vett eltérését a paraméterben megadott értéknek.

CODE
// shadowmap készítéskor float dx = abs(ddx(dist)); float dy = abs(ddy(dist)); dist += depthBias.x * (dx + dy) + depthBias.y; color = float4(dist, 0, 0, 1);

Amennyiben nincs ddx, akkor ezt meg lehet tenni a shadow kirajzolásakor is, de akkor háromszor kell olvasni a shadow mapből. A paraméterek megfelelő belövésével majdnem megoldja a problémát, viszont az objektumok szélénél peter pannel. Kis belegondolással adódik, hogy miért: ha a derivált elég nagy, akkor kitolja a depth értékeket a jó tartományból (mint a konstans bias). Márpedig az objektum szélénél annyi lesz amennyit kiszámolt egy érvényes pixelben: nagy. Open for research kérdés, hogy hogyan lehetne jól felhasználni.

Egy fontos dolgot megjegyeznék: a ddx-et nem lehet (dinamikus) elágazásban/ciklusban használni, ugyanis ha a blokkon belül valamelyik pixel más végrehajtási ágra futhat, akkor nem definiált a megadott változó értéke.


Shadow mask

A Crytek nevezetes fóliája alapján csináltam. Miért kellett ez nekik és miért kell nekem? Ugyanazért: hogy SM 2.0-val is menjen az árnyék. Majdnem minden DirectX-es tudja, hogy az ilyen shaderekben maximum 96 utasítás lehet, amiből maximum 64 lehet aritmetikai. A felhasználható konstans regiszterek száma 32 (uniformok).

Szóval nem túl rózsás a helyzet, ezért a Crytek azt mondja, hogy válaszd szét a megvilágítás és az árnyékrajzolás műveletét. Az árnyéknak bőven elég 1 bájt, tehát egy ARGB8 targetbe 4 darab fényárnyékot bele tudsz pakolni. Megvilágításkor pedig a deferred renderer forward pass-ához hasonlóan olvasol ebből a textúrából. Talán annyit említenék meg, hogy a shadow mask készítésekor nem kell semmiféle blending, csak a color write-ot kell buherálni.

shadow mask

Feltűnő, hogy a shadow mask-ban van egy csomó acne, a végleges képen viszont nem látszik (de, sajnos átlátszó objektumoknál igen). Ez azért van, mert az acne pont az objektumok árnyék felőli oldalán van (hát persze, hiszen backfacet rajzoltam).

A Crytek is megemlíti, hogy átlátszó objektumokra ez nem jó; elvileg jó lehet, de akkor objektumonként kellene megcsinálni a maskot, na azt én nem vállaltam be (ők se). Ezért a forward rendererrel átlátszó objektumokra nem hat árnyék, illetve mint mondtam azokon keresztülnézve néha látszik az acne. Az nem megoldás, hogy belerajzolod azokat is a mask-ba, mert akkor az árnyék nem fog átlátszani rajtuk.


Irregular PCF

A shadowmap-nek egy további hátránya, hogy az árnyékrajzoláskor egy shadowmap texel sok pixelhez rendelődik hozzá. Ezt perspective aliasing-nak hívják, és ez sem egy trivi probléma, de szerencsére elég sok algoritmus létezik már az árnyékok puhítására.

  • percentage closer filtering: összehasonlítások átlaga egy környezetben
  • irregular pcf: a pixelesedést elcseréli sűrű zajra
  • variance shadow mapping: Chebyshev nevezetes egyenlőtlenségét felhasználva
  • summed area variance: segít a light bleedinges problémákon
  • exponential shadow maps: hasonló, de az exp függvény által
  • convolution shadow maps: hasonló, Fourier sorfejtéssel
Mindegyik nagyon szimpatikus, de mindegyiknek vannak problémái. Mivel a Crysis-ben a gyakorlatban láttam ezek közül kettőt, ezért végül az irregular pcf mellett döntöttem. Ha leszeded a Crysis demo-ját, akkor egy zip fájlban ott van benne az összes shader. PIX-el megdebugolva még a textúrákat is ki lehet szedni.

De egyébként ez nem egy olyan bonyi algoritmus, hogy sokat kelljen reverse engineeringelni. Az alapötlet az, amit SSAO-hoz is csináltak: a PCF-hez használt kernel nem fix, hanem random változik. Kérféle megközelítés létezik:
  • a randomizálás screen space-ben történik (ez az eredeti)
  • world spaceben + mipmap (ez a Crytek féle)
De egyáltalán mire jó ez? Azt mondják, hogy a szem az ilyen magas frekvenciájú zajt automaikusan kiszűri, tehát ha nem figyelsz oda, akkor fel se tűnik. Messziről tényleg nem. Minkettőt leimplementáltam a tutoriálok között, és amit a Crytek állít azt én is megerősítem: az eredeti megoldás meglehetősen fura mozgás közben.

Az implementációt nem ragozom túl: a random kernelt el kell forgatni a textúrából olvasott szögekkel. A kernel az egységkörön belül Poisson eloszlású értékek; copy-paste.

CODE
const float2 irreg_kernel[8] = { { -0.072, -0.516 }, { -0.105, 0.989 }, { 0.949, 0.258 }, { -0.966, 0.216 }, { 0.784, -0.601 }, { 0.139, 0.230 }, { -0.816, -0.516 }, { 0.529, 0.779 } }; template <int samples> float PCFIrregular2D(sampler shadowmap, sampler noisetex, float3 cpos, float2 texelsize) { const float kernelradius = 2.0f; const float kerneloffset = 30.0f; // radius * 15 // ... float2 stex = cpos.xy * kerneloffset; float2 noise = tex2D(noisetex, stex); noise = normalize(noise * 2.0f - 1.0f); float2 rotmat0 = float2(noise.x, noise.y); float2 rotmat1 = float2(-noise.y, noise.x); for( int i = 0; i < samples; ++i ) { rotated.x = dot(irreg_kernel[i], rotmat0) * kernelradius; rotated.y = dot(irreg_kernel[i], rotmat1) * kernelradius; sd = tex2D(shadowmap, cpos.xy + rotated * texelsize).r; s += ((d > sd) ? 0.0f : 1.0f); } return s * (1.0f / samples); }

Ez a 15 egy mágikus konstans, nem tudom honnan jött, de nekem megfelel így is. A forward renderer 4 samplet vesz, a deferred 8-at. Mindkettő bőven elég. A shadow mask-ot egyébként meg is lehet blurozni még egy bilateral filter-el.


Irregular PCF pont fényekre

Ez szintén egy elég bonyolult dolog, ugyanis tökjó, hogy van cubemap, csak baromi nehéz szűrőket alkalmazni rá. A Crytek mint említettem eintézte ezt úgy, hogy nem használ cubemapet árnyékhoz, helyette egy textúra atlaszba pakolja az összes ilyen fény shadowmapjeit.

A deferred rendererrel ez még elmegy, de a forwarddal semmiképp. Az egyetlen dolog ami akadályoz az a filterelés. Az egyik klasszikus megoldás a ShaderX 2-ben van leírva, én ezt kipróbáltam, nem működik (jól). Helyette mondok két másik megoldást:

  • a ldir vektorból kiszámolsz két merőleges u és v vektort
  • visszafejted a texCUBE implementációját
Egyik sem igazán jó, de ennél jobbat nem tudok mondani. Kezdeném az elsővel, ezt a forward renderer használja. Ezt a bizonyos két vektort könnyű kiszámolni vektoriális szorzattal. Ha megvannak, akkor a megfelelő offsettel felszorozva hozzáadod őket az eredeti vektorhoz.

CODE
float3 l = normalize(ldir); float3 up = { 0, 1, 0 }; if( l.y > 0.98f ) up = up.xzy; // sose tudom merről kell szorozni, de itt oly mindegy u = normalize(cross(l, up)); v = normalize(cross(l, u)); // ezekkel olvasol a cubemapből off2 = l + u * texelsize.x; off3 = l + v * texelsize.y; off4 = off2 + v * texelsize.y;

Világos, hogy miért rossz: ez a gömböt érintő korongon szedett mintának köze nincs a majdani textúra térhez, így ahogy a cubemap széle felé haladsz egyre rondább lesz a PCF. Speciel meg lehetne buherálni úgy, hogy a vektortől függően változtatni a kernel sugarát. Mivel ígyis-úgyis ronda, az is elfogadható ha egyáltalán nem filterelsz (pont fényre úgyis ritkán kell árnyék).

Na de a másik "megoldás". Az ötlet az, hogy a cubemap címzése valójában visszavezethető 2D-s címzésre, ugyanis a vektor leghosszabb komponense megmondja a cubemap egyik oldalát, a másik két komponens pedig onnantól egy mezei 2D koordináta ((másikkettő / leghosszabb) * 0.5 + 0.5). Ha tehát meg tudnám úgy buherálni a vektort, hogy a művelet elvégzése után egy offsetelt texelt adjon meg, akkor működhet is.

Tegyük fel most, hogy x a leghosszabb komponens, ekkor

CODE
float3 CubeOffsetXYZ(float3 swiz, float2 off, float2 texelsize) { float3 ret; ret.yz = swiz.yz + 2.0f * off * texelsize * swiz.x; ret.x = sqrt(1.0f - dot(ret.yz, ret.yz)); if( swiz.x < 0 ) ret.x *= -1.0f; return ret; }

egy jó nagy baromság, de annyira nem ocsmány, mint vártam. Ez a "tegyük fel, hogy" úgy értendő, hogy néhány if-el lekezeled, hogy mit kell meghívni. Egy rondább dolog a zaj textúra megcímzése. Választhatsz, hogy:
  • lepasszolod mind a 6 view mátrixot uniformként
  • valahogy összehackeled shaderben
  • összeehackeled az ldir vektorból (szintén ifekkel kezelve az eseteket)
Nem hiszem, hogy nagy különbségek lennének, úgyhogy lusta módon a harmadikat választottam, ami ilyetén módon elbassza a sűrű zajt erős fejfájásra, merthogy olyan randa helyenként, mint a PCF. Képek lent, az elágazás höfö (annyit segítek, hogy abs(ldir).xyx < abs(ldir).yzz-t kell feldolgozni).


Tone mapping és gamma korrekció

Azt már említettem, hogy ha több fényforrás van, azt FP rendertargetbe szokás akkumulálni, így implicit módon HDR lesz. A képernyő tartományába való visszaskálázást hívják tone mapping-nak. Én ezt egyelőre nem használok, de innen lehet inspirációt meríteni.

A gamma korrekció fontosságát szintén említettem már, anélkül a kép nagyon sötét. Itt említeném meg a Doom 3-at, ugyanis abban egyik sincs. A backbufferbe akkumulál mindent, kihasználva hogy a kártya minden írás után [0, 1]-be clampol (ez is egyfajta tonemap). A gamma korrekció, illetve az ambiens fények teljes hiánya miatt olyan sötét egyébként (de speciel jól néz ki).

no gamma vs gamma

Emlékeztetőül, hogy hogyan is működik ez: a content amit a modellezőtől kapsz, tipikusan srgb color space-ben van, tehát az intenzitása nemlineárisan változik (srgb = pow(rgb, 1 / 2.2)). Ez azért van, hogy optimálisan használja ki a channelenként 8 bitet. Az engineben viszont nem számolhatsz srgb térben, mert a fény fizikai modellje nem arra vonatkozik. Ezért a textúrákat használat előtt konvertálni kell. Erre két lehetőség van:
  • olvasás után pow(x, 2.2)
  • srgb textúrát használsz
Az utóbbinak az az előnye, hogy automatikusan elvégzi a konverziót, de nem minden kártya tudja. Illetve van egy rendertargetekkel kapcsolatos fontos különbség: DirectX-ben az SRGBWRITEENABLE renderstate mindig működik, ha a kártya tudja. OpenGL-ben csak akkor fogja azt csinálni amit akarsz, ha a framebuffer is srgb. Tehát csak úgy lehet ezt API/platform függetlenül megcsinálni, ha shadert használsz. iOS-en szintén ezt csinálom (annak ellenére, hogy baromira nem kéne fullscreen quadokat rajzolgatni).


Babérgyűjtés

Kiraktam a homokozót egy példa jelenettel. A progiban le van rakva egy kamera valahova az origó köré és a z tengelyen néz előre (balkezes koordinátarendszer). Tone mapping és gamma korrekció be van kapcsolva. A jelentleíró fájl (sandbox.xml) szerkeszthető.

  • WASD: mozgás
  • Ctrl: süllyedés
  • Space: emelkedés
  • H: debug rajzolás
  • R: jelenet újratöltése
Maga az XML szerintem nem igényel különösebb magyarázatot, .obj fájlokat be tudsz tölteni úgy ahogy a teáskanna és a tórusz van. Megjegyezném, hogy az .obj fájlok parseolása igen lassú, ha nagyon ráérsz átkonvertálhatod az engine natív formátumába (deferredes tutoriálban van kód). Shadereket is be lehet tölteni, amennyiben ismered a QFX-et. Hehehe... De persze tud GLSL-t is betölteni, csak nem mondom meg hogyan. Hehehe...

sandbox

Vacak gépen nem teszteltem. Ha problémád van OpenGL-el, akkor a settings.ini-ben átírod a renderert d3d9-re. Ha valami miatt elszállna a program akkor egy log-ot küldj emailben :)


Summarum

Azt hiszem ez egy tartalmas cikk lett, sok problémát viszonylag jól meg tudtam oldani (a túrót, még mindig nem javítottam a modelleket, a cubemapos PCF-től meg rosszakat álmodok). Amit nem csináltam meg, az a transzparens objektumok árnyékának gyengítése (de gondold meg, hogy deferred rendererrel ezt nem is nagyon lehet), illetve a világításba való beleszólás egy custom effekttel (ez megint csak orbitálisan nehéz dolog, nem éri meg).

A teljesítményről annyit, hogy a HDR demó a forward rendererrel gyorsabb (Mac-en kétszer annyi), a hellknightos demó a dinamikus plazmafények miatt a deferreddel jobb. A jelenlegi ASUS enGTS 250-emen mindkettő 100-200 fps között mozog (1360x768).

pic1 pic3 pic2

Egyelőre ezt a részt azt hiszem nem nagyon kell fejlesztgetni, illetve amint lesz DX11 kártyám, fogok csinálni egy forward+ renderert is, ugyanis láthatóan erre gyúrnak manapság (ld. Unreal 4 tech videó).


Höfö:
  • Írjál forward+ renderert! (nekem nincs hozzá videókártyám)

back to homepage

Valid HTML 4.01 Transitional Valid CSS!