53. fejezet - Physically based rendering


A 3D megjelenítés örök vesszőparipája az úgynevezett "fotorealizmus", amit azért tettem idézőjelbe mert szubjektíven ítélik meg: 2004-ben a Half-Life 2 volt fotorealisztikus, 2007-ben meg a Crysis. Mostanra már egyiket sem mondanánk annak. A physically based rendering (PBR) szakít ezzel a szubjektivitással és egy objektívabb, fizikai modellre alapozva minősíti az előállított képet.

Hogy ki kezdte, arra nem sikerült rájönnöm, az viszont elég egyértelmű, hogy a legtöbb mainstream játékmotort beszippantotta az új trend. Nem túl jó hír ez a garázsenginek fejlesztőinek, pláne hogy a marketing modellben is drasztikus változás állt be, így gyakorlatilag ingyen elérhetőek olyan nagy nevek, mint az Unreal 4. A disznók...

Az irodalmakról mondanék pár szót, ugyanis a PBR tipikusan olyan, aminek megértéséhez sokat kell kutatni különféle helyekről. A legalapabb forrás, amit én is beszereztem ez a könyv, ami olyan súlyos, hogy ha véletlenül ráejted a macskára, abból atomnyi vékonyságú palacsinta lesz. Tartalom szempontjából az elméleti háttérhez elég jó, mert minden egy helyen van, de egy offline raytracert (meg sok egyebet) implementál. Én viszont valós időben akarom használni a technikát, szóval a könyvet csak a többi irodalomban leírt képletek és fogalmak megértéséhez használtam. Valósidejű megjelenítéshez a legátfogóbb forrás a SIGGRAPH 2014-es kurzusa, különösen a DICE prezentációja.



Na de mit is jelent ez a PBR? Egyáltalán hogyan kell lefordítani magyarra? Én úgy fordítom magamnak, hogy (fény)fizikán alapuló megjelenítés, mert ez rögtön meg is magyarázza a jelentését. Eddig a megjelenítő enginek ad-hoc jellegűek voltak, tehát egy jelenet adott (fény)beállításokkal tök szép volt, más környezetben viszont a hackelések miatt tipikusan rosszul nézett ki (nappal vs. este, belső tér vs. külső tér). A PBR legfontosabb jellemzője tehát, hogy vizuális minőség helyett mérhető fizikai mennyiségek alapján validálja az előállított képet. Összefoglalva:

  • a fény valós fizikai modelljén alapul
  • valós (mérhető) adatokkal dolgozik ("50 W-os villanykörte")
  • könnyíti a munkát ("ami jól néz ki a nappaliban, az jól néz ki a kertben is")
Ezek a mindenki számára érthető alapelvek. Fejlesztői szempontból ehhez hozzájön két fontos tulajdonság:
  • minden felület valamilyen szinten tükrözi a beérkező fényt
  • nem ver vissza több fényt, mint amennyit kapott (energiamegmaradás)
Ami persze drasztikus változásokat hoz az anyagok leírásában is. Kétféle anyagleírás terjedt el, az egyik a specular workflow, a másik a metalness workflow. A két modell eredménye persze ugyanaz, de nekem az utóbbi szimpatikusabb:
  • baseColor: alapszín
  • roughness: a felület durvasága ("rücskössége"), ezzel lehet variálni a tükröződés élességét
  • metalness: fém (vezető)-e vagy nem (szigetelő)
Természetesen ezek textúrák is lehetnek. Ha a baseColor textúra, akkor van rá megkötés is: csak albedó lehet, azaz minden megvilágítás és egyéb dolgot (pl. AO) ki kell belőle szedni. Ez megtehető például az AwesomeBump nevű programmal, ami akár még normal- és heightmapot is képes generálni hozzá. Az általánosan igaz minden paraméterre, hogy ha sRGB, akkor OpenGL-ben linearizálni kell.

Ez az alap anyagmodell, és az egyszerűségéhez képest meglepően sokféle anyag közelíhető vele. Persze a mainstream enginek ennél többet is megengednek; pl. az UE4 blueprintjeivel tetszőlegesen komplex anyagokat lehet létrehozni, de ezekkel nem foglalkozom. Szintén nem foglalkozom átlátszó anyagok megjelenítésével, hiszen már írtam róla cikket, amiből kiderül, hogy fizikailag korrekten igen nehézkes ezeket megjeleníteni. Ha a korrektségtől (fénytörés) eltekintünk, akkor üvegszerű dolgokat össze lehet buherálni premultiplied alpha blending-el. A 2015-ös SIGGRAPH-on azóta megjelent a Disney új jegyzete, ami kiterjeszti a módszert átlátszó anyagokra is.

Designereknek ajánlott elolvasni ezt meg ezt (regisztrálni kell).


Először is egy kis emlékeztető differenciál- és integrálszámításból. Az f: R → R függvény differenciáljának nevezzük a

pic003

függvényt. A differenciál konyhanyelven "f(x) megváltozása, amikor x megváltozása dx". Azért foglalkozunk ezzel, mert magasabb dimenziókban könnyebben lehet vele számolni, mint a deriválttal (amit esetleg nem is ismerünk). A differenciálás inverz művelete az integrálás:

pic004

amin senki nem lepődik meg. Az [a, b] intervallumon vett Riemann integrálra gondolhatunk úgy, hogy az intervallumot felosztjuk végtelenül kicsi dx részekre, ezeken kiértékeljük az integrandust, majd az eredményeket összeadjuk. Ahogy dx tart 0-hoz, úgy az összeg is tart az integrálhoz. A továbbiakban elszakadok a Riemann integráltól és egy általánosabb fogalommal, a felületi integrállal fogok dolgozni.

A következő fontos fogalom a térszög, ami a konyhanyelven értett 2D-s szög kiterjesztése 3D-be. Először gondoljuk végig, hogy a konyhanyelven értett szög mit jelent: egy α szög radiánban kifejezve megfelel a szög által az egységkörből kimetszett körív hosszával. Két szög különbsége egy differenciális szög.

Ha analóg módon gondolkozunk, akkor 3D-ben egy Ω térszög megfelel az egységgömb felületén általa kimetszett területtel, szteradiánban (sr). Ahogy 2D-ben a szög, úgy 3D-ben a térszög is meghatároz egy irányvektort, amit két rendes szöggel lehet megadni (φ ∈ [0, 2π] és θ ∈ [0, π]). Kicsit zavaró, hogy az irodalmak ezt az irányt szokták (aláhúzás nélkül)

pic005

-val jelölni és az ebben az irányban értelmezett (differenciális) térszöget -val. A differenciális térszöget úgy kell érteni, hogy "φ és θ megváltozása által meghatározott térszög". Mivel ω egyben paraméterezése is az egységgömbnek:

pic001

Vajon mi történik, ha integráljuk?

pic002

(megjegyzés: integráljelből ahol lehet, ott mostantól csak egyet fogok kiírni)

Bal oldalon az Ω térszögön vett felületi integrál van, azaz az Ω térszöget felosztjuk végtelenül kicsi differenciális térszögekre, majd ezeket összeadjuk és (mily meglepő) visszakapjuk Ω-t. Jobb oldalon ugyanez egy dupla Riemann integrálként kifejezve. Ha Ω a teljes gömbnek megfelelő térszög (S), akkor φ = 2π és θ = π, így az eredmény szteradián. Ha Ω csak a félgömbnek megfelelő térszög (H), akkor az eredmény szteradián. Az integráljel alatt a továbbiakban az Ω jelölést fogom használni, de tipikusan félgömböt fogok érteni alatta. Ha esetleg más térszögön integrálok, azt külön jelzem.

Ez eddig könnyű volt, de most jön valami nehezebb: egy tetszőleges felület által kifeszített térszög, ami a felületnek az egységgömbre való vetületének a területe (tehát ezt már nem mindig lehet Riemann integrálként felírni). Ha most veszünk egy végtelenül kicsi dA felületdarabkát, akkor az általa kifeszített (differenciális) térszög kiszámítható az ábrán látható módon:

pic01

Ugye mindenki látja, hogy ez itt θ'? Az irodalmak ilyen szempontból elég szórakozottak, hogy nem képesek valami más szimbólumot találni. Egy konkrét felület által kifeszített térszög az ilyen felületdarabkákon vett felületi integrál. Ezzel most nem foglalkozok tovább, de a képlet fontos lesz később.

(megjegyzés: ω alatt innentől én is ω-t értem, alatt pedig egy végtelenül kicsi térszöget körülötte)


A fény egy elektromágneses hullám, azaz van elektromos és mágneses mezője, sőt ezek a haladási irányra merőlegesen oszcillálnak (transzverzális hullám). Két hullámhegy távolsága a hullámhossz, ezt λ-val jelölik. A számunkra látható fény hullámhossza 380 és 780 nanométer között lehet. Az anyag fényre gyakorolt hatását az anyag törésmutatója mondja meg, ez változhat attól függően, hogy milyen hullámhosszú fénnyel találkozik.

Amíg a fény homogén közegben halad (a törésmutató állandó), addig a haladási iránya egyenes vonal és nem változik, de maga a fény elnyelődhet. Ha viszont olyan közegbe kerül, ahol a törésmutató elég gyorsan változik (a fény hullámhosszánál rövidebb távolságon belül), akkor a fény szétszóródik (scattering), azaz több irányban folytatja az útját, de összeszámolva nem nőhet az energiája. Az itt tárgyalt anyagoknál mindig ez az eset áll fent, ennek is egy leegyszerűsített formája (optikailag sima felületek): a fény egy része visszaverődik (reflection), a többi része megtörik (refraction). Az ezek közötti összefüggést a Fresnel egyenletek írják le.

Egy fontos különbség, hogy fémek esetén a megtört fénysugár azonnal el is nyelődik (a fémeknek tehát nincs diffúz fénye), míg szigetelő anyagokban egy része elnyelődik, a maradék pedig kiszóródik a felületből (subsurface scattering). Például a paradicsom elnyeli a kék és zöld hullámhosszú fényt, de kiszórja a pirosat.

Az elektromágneses sugárzás mérésével a radiometria foglalkozik. Erről a területről néhány fontos mennyiséget illik megemlíteni, mert a későbbieknek ez az alapja. A mennyiségekre az angol nevükkel fogok hivatkozni.

  • radiant flux (Φ, sugárzási teljesítmény): egy tartományban egységnyi idő alatt átáramló sugárzási energia (W)
  • irradiance (E, besugárzott teljesítmény): egységnyi területre beérkező sugárzási teljesítmény (W / m2)
  • radiant intensity (I, sugárerősség): egységnyi térszögben terjedő sugárzási teljesítmény (W / sr)
  • radiance (L, sugársűrűség): egységnyi vetített területre egységnyi térszögben beérkező sugárzási teljesítmény (W / m2sr)
A legutolsó fogalom a legfontosabb, ugyanis azt kell kiszámolni, azaz megjelenítendő felület egy pontját adott irányban elhagyó radianciát. A definíciók képlettel:

pic02

(megjegyzés: mivel a radiancia egy adott irány mentén halad egy adott pontból vagy pont felé, ezért L(x, ω)-nak kéne írni, de a paramétereket lustaságból el szokták hagyni)

Ahol θ a pontbeli normálvektor és ω által bezárt szög. Most rögzítsünk le egy ωi bejövő irányt, a körülötte levő i térszöggel. Az ebből az irányból beérkező radianciát jelölje Lii), ekkor az első képletet átrendezve:

pic03

Ha esetleg integráljuk a pont körüli félgömbön, akkor megkapjuk az irradianciát (ez később fontos lesz). Most viszont a kimenő iránybeli radianciát szeretnénk tudni, tehát Loo)-t. Most jön egy meglepő feltevés: dLoo) arányos dE(ωi)-hez, és ezt az arányt nevezzük BRDF-nek (bidirectional reflectance distribution function), ami tehát:

pic3

Ez egy definíció; akinek megvan az eredeti dolgozat az utánaolvashat, hogy miért ilyen. Mindenesetre behelyettesítve az előbbit, illetve kifejezve dLoo)-t:

pic4

És mondjuk integráljuk a pont körüli félgömbnek megfelelő térszögön:

pic5

Ez az úgynevezett tükröződési egyenlet (reflectance equation, de máshol radiance equation-nak is mondják). Itt a könyv jelöléseivel voltam konzisztens, de egy kicsit kibogozva a szálakat: ωi az eddig megszokott fényirány (l), ωo a nézési irány (v), cos θi pedig a szokásos n·l. Nem kell megrettenni az integráltól sem: analitikus fényforrások (pl. pont fény) esetén meglepő módon van analitikus megoldása, szélsőséges esetekben pedig Monte Carlo integrálással közelíthető.


Először is a legszimpatikusabb tulajdonsága, hogy mérhető. Az interneten található MERL adatbázis 100 féle anyag BRDF-jét tárolja, 3D tömb formában. A Disney készített egy programot, ami meg tudja jeleníteni ezeket, de a PBR-hez szükséges paramétereket próba-szerencse alapon kell kikövetkeztetni...

Kevésbé szimpatikus tulajdonság, hogy nem minden függvény jó BRDF-nek. Három olyan dolog van, amit teljesíteni kell, mégpedig:

  • pozitív:
fr(v, l) ≥ 0
  • szimmetrikus:
fr(v, l) = fr(l, v)
  • energiamegmaradás:
Ω fr(v, l) cos θl dl ≤ 1

Ez alapján most mutatnám be a Lambert-féle diffúz BRDF-et, ami rettentően bonyolult:

pic6

A π-vel való osztás az energiamegmaradás miatt kell, ugyanis a cos integrálja a félgömbön pont π. Mily meglepő, hogy az eddigi cikkeimben én sem, de mások sem vették ezt figyelembe. Máris egy kicsit fizikaibbnak érezhetjük ettől magunkat, csak az a szépséghiba, hogy Lambert felületek nincsenek a valóságban (viszont a matt felületeket jól modellezi).

Az eddig megszokott módon a BRDF-et most is két részre osztjuk: egy diffúz (fd) és egy spekuláris (fs) komponensre. Előbbinek a Lambert teljesen jó, bár a Disney és a DICE ezzel nem ért egyet (a Disney-féle diffúz BRDF figyelembe veszi a durvaságot is). A Crytek szintén nem, ők az Oren-Nayar modellt használják.

Az igazi buli a spekuláris komponenssel van, ugyanis arra a Cook-Torrance-féle általánosított mikrofelület modellt fogom használni (mint ahogy majdnem mindenki más is). Ez egy több tagból álló függvény, így a végeredménynek több variációja is lehet, az egyes részegységek megválasztásától függően. De előbb egy rajz: mit jelent a felület durvasága? (tipp: nem azt, hogy beszól neked a villamoson):

pic8

Ahogy mondtam, "rücskös", tehát a beérkező fénysugár mindenféle véletlenszerű irányban törik meg a mikrofelületek miatt, ettől a tükröződés mosottas lesz. Természetesen ezeket a mikrofelületeket nem szimuláljuk explicit, csak egy eloszlásfüggvényt adunk meg a félvektor (h = normalize(l + v)) alapján.

pic7

A részegységek magyarázata:

  • D(h):
a mikrofelületek normálvektorainak eloszlásfüggvénye
  • F(l, h):
a Fresnel függvény
  • G(l, v, h):
annak a valószínűsége, hogy egy mikrofelület látható a fény és a kamera által is

A három közül a legegyszerűbb F, mert erre szinte mindenki a Schlick-féle közelítést használja (az UE4 kicsit módosít rajta):

pic9

Ahol F0 a Fresnel-egyenlet értéke 0 fokos beesési szögnél: szigetelő anyagokra konzisztenesen 0.04 (mert amúgyis ekörül mozog), fémekre pedig a fém színe (pl. arany esetén (1.022, 0.782, 0.344)).

Az irodalmak közötti eltérés leginkább D és G megválasztása. A már említett BRDF Explorer-ben sokfélét ki lehet próbálni, de én lusta módon azokat választottam, amiket az irodalmak propagálnak, tehát D-nek a GGX függvényt:

pic10

Ahol α = rougness2 (a Disney javaslata alapján). Hátravan még G, erre az UE4 által preferált Smith-Schlick közelítést használtam:

pic11

Ahol k = (roughness + 1)2 / 8 analitikus fényekre (szintén Disney javaslata), de k = roughness2 / 2 light probe-okra. Kipróbáltam még a DICE-féle Smith-GGX megoldást is, de végül nem használtam (nem azért mert ronda, hanem CSAK). Ha ránézünk erre a függvényre, akkor majdnem kiejti a Cook-Torrance nevezőjét, ezért optimalizációs célból egy Vis nevű függvénynek szokták definiálni, amiben ez az egyszerűsítés már benne van.

Ezekkel a függvényekkel igazából nem szükséges ennél részletesebben foglalkozni, hacsak nem utánaszámolás céljából.


Amit még tisztába kell tenni az a fénykezelés, illetve hogy milyen fényekkel foglalkozik egyáltalán a PBR és hogyan lehet ezeket paraméterezni. Néhányat felsorolnék ezek közül:

  • pontszerű fények (pont fény, spotlámpa)
  • területi fények (gömb, korong, kapszula alakú fényforrások)
  • kép alapú fények (IBL; globális/lokális light probe-ok)
  • fotometrikus fények (IES vagy EULUMDAT formátumban adott profilok)
  • emisszív felületek (fényt kibocsátó felületek, pl. egy digitális óra háttérvilágítása)
Ezek közül én csak analitikus (pont/területi) és kép alapú fényekkel foglalkoztam, a többiről lehet olvasni a DICE jegyzetében. Bár sok fontos kikötés van, én a leglényegesebbnek azt a kettőt tartom, hogy
  • minden egyes BRDF helyesen működjön együtt az összes fénytípussal
  • a fényekhez használt mennyiségek konzisztensek legyenek
A fotometria területe a radiometriától abban különbözik, hogy csak az emberi szem által érzékelhető fénnyel foglalkozik. Így tehát megfeleltethető fogalmak találhatók itt is:
  • luminous fluxv, fényáram): mértékegysége lumen (lm)
  • illuminance (Ev, megvilágítás): felületre beérkező fényáram; mértékegysége lux (lx)
  • luminous intensity: (Iv, fényerősség): fényáram egységi térszögben; mértékegysége candela (cd)
  • luminance (Lv, fénysűrűség): egységnyi vetített területre egysényi térszögben beérkező fényáram (cd / m2)
(megjegyzés: mivel a szimbólumok radiometriában és fotometriában is ugyanazok, alsó indexben jelölik, hogy éppen melyikről van szó)

A szem fényérzékelését a CIE fotometrikus görbe (V(λ)) írja le, ami minden hullámhosszhoz hozzárendel egy [0, 1]-beli súlyt. A görbe a zöld fénynél (555 nm) éri el az 1-et, ez azt jelenti, hogy a szem a zöld fényt látja legjobban (100%-os hatékonysággal). A radiometriai mennyiségekből (Xe) fotometriai mennyiségekbe (Xv) való váltás az alábbi integrállal írható le:

pic12

Ahol Km = 683, röviden hívjuk most maximális hatásfoknak (angolul baromi hosszú kifejezés). Ha konyhanyelven akarjuk megfogalmazni, akkor azt lehet mondani, hogy "egy 1 wattos zöld fény fényáramja 683 lumen". Természetesen 3D grafikában megelégszünk egy közelítő képlettel, ami:

pic13

Ahol η a fény hatásfokát jelöli. Ha ez az infó nem áll rendelkezésre, akkor η = 683-at feltételezünk és ekkor a többi mennyiség is így számolható ki. A kérdés, hogy a shadereknek mi legyen a bemenő paramétere: watt vagy lumen? A rövid válasz az, hogy lumen (mert nem foglalkozunk spektrumokkal). A képletek ugyanazok, tehát ha esetleg radianciát írok, akkor nyugodtan lehet érteni helyette fénysűrűséget is.

Amivel nem foglalkoznak különösebben az a fények színe. Alapvetően nem is színt kellene mondani, hanem hőmérsékletet (feketetest-sugárzás és társai). A hőmérsékletből RGB színeket előállító képlet bonyolult, de megtalálható itt.


A feladat a tükröződési egyenlet analitikus megoldása. Kezdjük a pont fénnyel, mert az a legegyszerűbb, hiszen egyetlen olyan irány van, ahonnan fény érkezhet. Viszont van egy kis gond: mivel nincs területe, a radianciát nem lehet értelmezni. Induljunk ki akkor abból, amit könnyen ki lehet számolni. Jelöljük a pont fény körüli egységgömböt S-el, ekkor a térszögeknél megbeszéltek alapján:

pic14

Tehát I-t már ki tudjuk számolni, ami nagyon jó, mert akkor (alkalmazva a korábban felírt képleteket):

pic15

És már csak egy picit kell küzdeni és:

pic16

Ez az amit úgy hívnak, hogy inverz négyzet törvény, tehát az irradiancia a fényforrástól való távolság négyzetével csökken. A pont fény általában a pozíciójával és az intenzitásával (I) adott. Vigyázat, az UE4 a fluxust hívja intenzitásnak, ami elég félrevezető!

Az inverz négyzet törvénnyel annyi baj van, hogy sosem éri el a 0-t. Ez nem hangzik túl jól, mert optimalizációs célból jó lenne ha a fényeket ki lehetne vágni ha nem látszanak (gondoljunk a forward+ -ra). Ezért a pont fényhez hozzá szoktak venni egy sugarat is és a képlet attenuation = I / dist2 * max(0, 1 - dist / r)-re módosul.

A spotlámpa se sokkal nehezebb, hiszen hasonlóan kell gondolkodni, de gömb helyett kúpon vesszük az integrált:

pic17

ahol θouter a spotlámpa külső nyitási (fél)szöge. A DICE azt mondja, hogy nekik ez nem tetszik, mert ahogy csökken ez a szög, úgy erősödik az illumináció és ezt a designereknek nehéz kompenzálni. Ezért ők Φ = πI-vel számolnak (én nem).

A spotlámpához hozzá szoktak venni még egy paramétert, a belső nyitási szöget (θinner). A két szög között a fény fokozatosan elhalványodik. Ehhez nem írok képletet, ott a kód (meg amúgyis mindenki álmából felébresztve keni-vágja).


Gyakran alkalmazott módszer, mert egy egyszerű módja a globális megvilágítás szimulálásának. De mivel a rücskösség által a felület nem feltétlenül sima, a tükröződési egyenletnek nincs analitikus megoldása. Emlékeztetőül:

pic18

Ahol most Li(l) az environment mapból olvasott érték. Azt mondtam, hogy az integrálra lehet úgy gondolni, mint végtelenül kicsi környezeteken vett összegzésre. Most ez a "végtelenül kicsi" legyen egyszerűen csak "kicsi", tehát osszuk fel a félgömböt egyenletesen N részre és cseréljük le az integrált egy szummára (Monte Carlo integrálás):

pic19

Ahol pr az lk (mint valószínűségi változó) sűrűségfüggvénye (probability density function, PDF). Mivel most egyenletesen osztottam fel a félgömböt, annak a valószínűsége, hogy egy adott pontot választok rajta pr = 1 / 2π. Ahogy egyre nagyobb N-ig megy a szumma, úgy javul a közelítés pontossága is.

Ez a képlet a probléma megoldásához jó lesz, az egyenletes eloszlás viszont nem. Mondok egy triviális példát: egy tökéletesen tükröző felület (roughness = 0) egy pontjában egyetlen olyan irány van, ahonnan fény jöhet, tehát bármilyen olyan eloszlás, ami nem ezt az irányt választja, pazarolja az időt.

A konvergencia tehát gyorsítható, ha az adott BRDF szerint választjuk meg lk-t és pr-t (importance sampling). Kezdjük először a Lambert-féle BRDF-el: milyen mintavételezési stratégiával gyorsítható a konvergencia?

Annyit kapásból észrevehetünk, hogy amikor cos θ = 0, akkor nem jutunk közelebb a megoldáshoz, azaz olyan irányokban fölösleges mintavételezni. Mi lenne ha pr kiejtené cos θ-t, hogy ne is zavarjon? Ehhez pr = (cos θ) / π kellene (a sűrűségfüggvény 1-hez kell integráljon). A levezetést nem írom le, de ennek analógiájára megcsináltam (aki nem hiszi, számoljon utána):

CODE
float3 CosineSample(float xi1, float xi2) { float phi = 2 * PI * xi1; float costheta = sqrt(1 - xi2); // mert P(θ) = 1 - cos2 θ float sintheta = sqrt(1 - costheta * costheta); // mert sin2 x + cos2 x = 1 // PDF = costheta / PI return float3(sintheta * cos(phi), sintheta * sin(phi), costheta); }

Tehát ha így választjuk meg az irányt, akkor cos θ is és a Lambert BRDF-ben a π is kiesik. Vigyázat, ez nem jelenti azt, hogy eltűnik a diffúz árnyék! Csak n·l-t cseréltük el úgy, hogy az árnyékos részen egyszerűen nem veszünk mintát!

Most akkor valami hasonló kéne a fent bemutatott Cook-Torrance-féle BRDF-re is. Szerencsére ezen se a DICE se az Epic nem veszett össze:

CODE
float3 GGXSample(float xi1, float xi2, float roughness) { float alpha2 = pow(roughness, 4.0); // eml.: Disney-féle α = roughness2 float phi = 2 * PI * xi1; float costheta = sqrt((1 - xi2) / (1 + (alpha2 - 1) * xi2)); float sintheta = sqrt(1 - costheta * costheta); // PDF = (D(h) * dot(n, h)) / (4 * dot(v, h)) return float3(sintheta * cos(phi), sintheta * sin(phi), costheta); }

Tök jó, ez meg kiejti majd D(h)-t. Megjegyezném, hogy az egyszerűsítéseket célszerű kézzel elvégezni, ugyanis nem nagyon lehet abban bízni, hogy a GPU-n a * (b / b) == a ... Ez már majdnem tökéletes, path tracinghez. Valósidejű megjelenítéshez még mindig lassúcska, mert ahogy a rücskösséget növeled egyre több mintavételezés kell. Azt mondja erre az Epic, hogy:

pic20

Vigyázat, ez egyébként baromira nem igaz! (lenyeljük) Az Epic felhívja a figyelmet, hogy ez a fajta közelítés az n = v = r feltételezés miatt látható hibákat okoz, például le kell mondani a "megnyúlt" tükröződésekről. Egy 2015-ös jegyzet próbál segíteni ezen, több-kevesebb sikerrel.

Az első tagot a cubemap mip szintjeibe számoljuk ki, különböző roughness értékekhez. Ha esetleg realtime kellene előállítani ezt a cubemapet, az sem vészes: ha egyszer lefordult, akkor már gyors (meg nem muszáj N = 2048-ig futtatni; cserébe kicsit zsizsás, amit úgysem veszel észre). Mivel itt konkrétan az irradianciát számoltuk ki, a becsületes neve irradiance cubemap.

A második tagot még tovább kell egyszerűsíteni, ugyanis jelenlegi formájában három paramétertől függ (F0, roughness, n·v), tehát legjobb esetben is 3D textúrában lehetne eltárolni. Az F0-tól való függés azonban megszüntethető:

pic21

Vigyázat, legalább 16 bites float textúrába kell pakolni, különben nem lesz jó! Ha ezek elkészültek, akkor megjelenítéskor két darab textúra olvasással megúszható az egész buli, ami azért picit hatékony.

CODE
float miplevel = roughness * (NUM_MIPS - 1); float3 r = 2 * dot(v, n) * n - v; float3 spec_irrad = irradcube.SampleLevel(linearSampler, r, miplevel).rgb; float ndotv = saturate(dot(n, v)); float2 AB = brdfLUT.Sample(linearSampler, float2(ndotv, roughness)).rg; color = spec_irrad * (F0 * AB.x + AB.y); // megj.: fémeknél F0 = baseColor

Az előszűrés miatt a cubemap alsóbb mip szintjein seam-ek keletkeznek (pl. minden oldal különböző színű), ennek megoldásához be kell kapcsolni az oldalakon keresztüli szűrést (glEnable(GL_TEXTURE_CUBE_MAP_SEAMLESS)).

Hasonlóan lehet gondolkodni a diffúz esetben is, az említett cos mintavételezéssel; ezt még speciel a CubeMapGen is meg tudja csinálni. A neten elérhető egyéb programok, amik papíron tudják a fentit, nem működnek jól.

pic22

Na és akkor mi legyen a sík felületekkel? A DICE azt mondja, hogy oda nem kell textúra, ők helyben raytracelik a tükröződést (a gbufferben). Persze, amikor pont merőlegesen nézel a felületre, akkor nincs semmi információ, úgyhogy ebben az esetben diszkréten eltűnik a tükröződés. Szigetelőknél ez Fresnel miatt nem baj, fémeknél meg lenyelhető béka...

Höfö kitalálni egy olyan mintavételezést ami egyszerre segít a diffúz és spekulár eseten is (path tracinghez jól jön).


A pontszerű fényekkel könnyű dolgunk volt, mert csak egy irányból jöhetett fény. A területi fények esetében viszont ez már nem igaz, így valamilyen módon muszáj megoldani a tükröződési integrált. Ez a téma még aktív kutatási terület, ezért csak annyit írok róla, amennyit jelenleg meg lehet csinálni.

Először is határozzuk meg a területi fény egy pontja által kisugárzott radianciát. Ehhez a radiancia definíciójából fejezzük ki d2Φ-t és integráljuk:

pic29

A radianciát egy átrendezéssel megkapjuk. Nézzük most a tükröződési egyenletet. Az envmappal ellentétben a területi fénynél nem szükséges a teljes félgömbön integrálni, hiszen sok helyről nem érkezik fény. Elég tehát a területi fény által bezárt térszögön elvégezni az integrálást, legyen ez Ωlight. Ha így írjuk fel az egyenletet és alkalmazzuk a térszögeknél megbeszélt képletet:

pic30

Tehát térszög helyett integrálhatunk a területi fény (felületi) területén. A képlet path tracingben különösen hasznos, mert ezek szerint az importance sampling-ot elvégezhetjük a fényforráson is (explicit light sampling). Ez bizonyos esetekben sokkal gyorsabban konvergál, mintha a BRDF-et mintavételeznéd!

Valós időben viszont lassú, és értelmesen nem is lehet előre kiszámolni (túl sok paramétertől függhet). Marad tehát egy közelítő analitikus megoldás és itt kezdődnek a gondok, ugyanis eddig még nem sikerült olyat kitalálni, ami minden esetben jó lenne (különösen az energiamegmaradással vannak bajok). Egy külön nehezítő körülmény, hogy a területi fény részben a pont horizontja alatt lehet. A jelenleg elfogadott megoldásokat levezetés nélkül adom meg.

Két fajta területi fénnyel foglalkoztam: (egyoldalú) téglalap és kapszula. A (részben) horizont alatti esetet egyiknél sem kezeltem le, csak annyit csináltam meg, hogy legalább ne világítson a fény olyan helyekre ahova nem kéne (pl. a téglalap mögé).

Az összességében igaz, hogy az integrált néhány darab jól megválasztott iránnyal próbálják megoldani (most representative point). Vegyük először a diffúz (Lambert) esetet. Azt mondja a DICE, hogy konstans Li mellett:

pic31

Tehát kellene a térszög és néhány okosan megválasztott lk fényirány. Téglalapra a térszög kiszámolható ennek megfelelően, a választott irányok pedig legyenek (a pontból) a téglalap négy csúcsába illetve a közepébe mutató vektorok.

A kapszula egy szakasszal (A, B) és a sugarával (radius) adott. A megvilágítás egy adott p pontban közelíthető úgy, mintha a kapszula egy a pont felé néző téglalap és a ponthoz (a szakaszon) legközelebb eső gömb lenne. Mivel a horizont alatti esetekkel nem foglalkoztam, erre adható egy közelítő képlet:

CODE
float DiffuseIlluminanceCapsule(float3 p, float3 n) { float illum_rect = ...; // DICE jegyzetéből float3 closest = ClosestPointOnSegment(p, A, B); float3 dl = normalize(closest - p); float dist2 = dot(closest - p, closest - p); float illum_sphere = PI * saturate(dot(dl, n)) * ((radius * radius) / dist2); return luminance * (illum_rect + illum_sphere); }

Nem néz ki tökéletesen, de ez a néhány pontatlanság lenyelhető.

pic32

Ha valaki eddig szúrós szemmel nézett, akkor most még szúrósabban fog nézni, mert a spekuláris tag ennél még egetrengetőbb. Ugyanis GGX eloszláshoz egyelőre nincs elfogadható módszer (Phong-hoz van, na de...), így mindenki az UE4-et másolja. Azt mondják ők, hogy "keresd meg a tükrözött vektorhoz legközelebbi pontot a fényforráson és számolj azzal".

Kapszulához van implementáció, de nem kezeli le a horizont alatti esetet, így artifactos. A téglalapot az UE4 meg se csinálja, a DICE meg bevallja, hogy hát nem találtak hozzá megfelelő energiamegmaradási képletet. Szerintem egy ilyen patthelyzetben mondhatom azt, hogy bocsi, de kihagytam legyen höfö.

Árnyékokat területi fényekhez is shadow mapping-al lehet csinálni úgy, hogy a fókuszpontot kijjebb tolod és a frustum közrefogja a fényt. Hogy ne kelljen cubemapet használni az ilyen fényeket úgy rakd le, hogy elég legyen egy irányban árnyékot számolni (pl. a Nap ilyen).


Korábbi cikkekben is hangoztattam már, hogy mennyire fontos a gamma korrekció. Mostantól viszont nem csak ez kötelező, hanem a tone mapping is (magyarul színleképezés-nek fordítják).

Miért kellenek ezek? Először is a szem a fényt nemlineárisan érzékeli (kétszer annyi fény → picit világosabb), tehát a sötétebb színek között könnyebben tud különbséget tenni. A fényképezőgépek viszont lineárisan érzékelik (kétszer annyi fény → kétszer olyan világos). Ha a lineáris értékeket tárolod a tipikusan 24 bites képbe, akkor sok hely elmegy a világos színeknek, amik között a szem amúgy se tud különbséget tenni, a sötét színeknek pedig nem marad elég precízió.

pic23

Ezt kompenzálandó a gép úgy menti el a képet, hogy felemeli 0.45-re, így optimálisabban tölti ki a 24 bitet (így lesz a kép sRGB). A visszakonvertálást a monitor végzi el (felemeli 2.2-re). A megvilágításra vonatkozó egyenletek és képletek viszont lineáris értékeket feltételeznek; ez mindaddig nem okoz gondot, amíg hülyeséget számolsz a shaderben (tipp: a PBR nem ilyen).

Mit jelent ez OpenGL szempontból? Ha betöltesz egy textúrát és tudod róla, hogy sRGB, akkor olyan formátumot adj meg (GL_SRGB8_ALPHA8). Kivételek a normal map, specular map és hasonlók, mert ezek tipikusan lineárisan vannak tárolva. Másrészt a rajzolás legvégén az egész framebufferre pow(color, 0.45)-öt mondasz (vagy sRGB framebuffert csinálsz, de az nekem sose működik jól).

Önmagában viszont nem elég a gamma korrekció, mert amellett, hogy lineáris térben kell dolgozni, a 24 bites korlátot is el kell, hogy felejtsük. A szem érzékelése ugyanis nem 1:256 hanem nagyjából 1:10000 (a valóság ennél még nagyobb). Ezt hívják high dynamic range-nek; ha pontosan akarjuk modellezni a valóságot, akkor legalább ilyen precízióban kell tárolni a kirenderelt képet (ARGB16F vagy A2R10G10B10 elég).

Amit persze végül vissza kell képezni a backbuffer 24 bitjébe, ezt csinálja a tone mapping. Ha nem teszed ezt meg, akkor már egy 40 W-os villanykörte is kiégeti a képet. Mutatom inkább:

pic24

Ha tehát egy engineben beállítasz egy fényt és a bal oldalihoz hasonló képet kapsz, akkor az nem azt jelenti, hogy a fényed intenzitását kell csökkenteni, hanem hogy az engine nem tone mappel. Amit itt elmondtam az részletesebben elolvasható az Uncharted 2 prezentációjában (és maga a tone mapping függvény is).

Kell viszont még egy harmadik dolog is, amit expozíciónak hívnak (exposure). A szem ugyanis folyamatosan adaptálódik a beérkező fénysűrűséghez: egy sötét szobában a pupilla kitágul, így több fényt enged be. Ha innen kimész a napfénybe, akkor a pupilla azonnal összeszűkül, de egy rövid ideig még nem látsz semmit, mert a retina lassabban adaptálódik a megváltozott fényviszonyokhoz, így esetleg felrúgod a macskát. Ezt is fontos szimulálni, különben a 40 W-os villanykörte mellett egy másik szoba túl sötétnek látszódhat és kínodban megint elkezdesz hackelni.

Ez utóbbi az, amivel picit kell foglalkozni. Először is vegyél egy (digitális) fényképezőgépet (tipp: a mobilodban jó eséllyel van) és próbálj meg lefotózni valamit. A gép automatikusan elvégzi az expozíciót, ami három dologtól függ (részletesen itt):
  • blende mérete (N, aperture): f-stop, 1.4 és 16 között; depth of field
  • záridő (t, shutter speed): mennyi ideig engedje be a fényt; motion blur
  • ISO/érzékenység (S, sensor sensitivity): én 100-al számolok; analóg gépeknél ez a film érzékenysége
Ebből a három paraméterből áll össze az ún. expozíciós érték (exposure value), amit konvencionálisan S = 100-hoz definiálnak:

pic25

Választhatsz, hogy a három paramétert beállítod kézzel (erre léteznek módszerek, például a Sunny 16 szabály), vagy pedig megcsinálod (fél)automatikusra. Utóbbi esetben a fényképezőgéphez hasonlóan egy fénysűrűség mérést kell elvégezni, például Erik Reinhard módszerével:

pic26

Ahol M a pixelek száma, δ pedig egy pici érték a szingularitás elkerüléséhez. Ezt a klasszikusnak mondható módszerrel valósítottam meg, tehát egy 64x64-es textúrába átlagoltam a képet (elvégezve a log-ot), majd ezt átlagoltam tovább 16x16, 4x4 és végül 1x1-be, ezen utolsó lépésben végezve el az exp-et. Ebből az expozíciós érték a következőképpen kapható meg (S = 100):

pic27

Az expozíció pedig a DICE levezetésének megfelelően:

pic28

Tonemap előtt a fénysűrűséget felszorozva ezzel automatikusan adaptálódik a kép. Ezt az adaptációt az eddig megszokott módon lehet késleltetni (ld. korábbi cikkem).


A fejlesztés eredménye végül három program lett. Egy irradiance cubemap generátor (DX10), egy egyszerű realtime path tracer (DX10) és egy bonyolultabb OpenGL 3.2-es demó. Path tracingről majd külön cikket fogok írni, úgyhogy arról nem mondok most semmit. OpenGL-hez a core profile fontos, mert sok PBR-hez szükséges dolog része lett a szabványnak.

A hivatalos álláspont az, hogy a PBR-t deferred shadinggel illik leimplementálni (material ID-t tárolva valamelyik bufferben). Én bepróbálkoznék azért deferred lightinggal is, de a demóban végül egy sima forward renderert csináltam. Az árnyékok kezelésében nincs különösebb változás, a demóban variance shadow mapping-ot használtam, mert a hibái ellenére elég egyszerű. Kép alapú fényekre viszont nincs értelme direkt árnyékot számolni, ezért egy SAO-t csináltam csak (scalable ambient obscurance).

Amire különösen oda kell figyelni, hogy a framebufferbe ne kerüljön negatív fénysűrűség. A pontatlanságok miatt ez könnyen megtörténhet, ilyenkor az adaptáció szingularitásba kerül. Két helyen jött ez nekem elő: a GGX eloszlásban, ha n·h = 1, és a területi fényeknél ha a térszög közel nulla.

A módszer egyik célja a hackelések megszüntetése, de a valósidejű megjelenítés miatt még mindig muszáj kerülő megoldásokhoz folyamodni. Például az envmap nem világíthatja meg a szobán belüli tükröző objektumokat, oda tehát egy lokális lightprobe-ot tettem le. Ez szintén problematikus, mert mint tudjuk a cubemap címzése az origóból indul, így minden objektumon ugyanaz tükröződik. Erre itt található egy megoldás.

A fejezet végén felsorolnék még néhány témakört, amikkel időhiány miatt nem foglalkoztam, de egy teljesértékű PB rendererhez hozzátartoznak:

Lightmap generálás: például radiosity-vel, de fontos, hogy minden fény benne legyen. HDR-ben célszerű kiszámolni ezt is.

Material layering: valamilyen felületre rétegeket lehet tenni (rozsda, karcolások). Sokat dob a látványon.

Subsurface scattering: a fény úgy hatol át a felületen, hogy az alatta levő felület láthatóvá válik (világíts át a kezeden egy lámpával).

Clear coat shading: az alap anyag fölött egy vékony áttetsző réteg van (lakkozott felületek vagy autófesték).

Anti-aliasing: deferred renderinggel az MSAA kiesik, tehát maradnak a postprocess AA effektek (pl. SMAA T2x).

Specular anti-aliasing: normal map-ek használata esetén jöhet elő az a probléma, hogy távolabbról nézve az objektumot a tükrözött fény aliasol. Egy megoldás erre a LEADR mapping.

Kamera effektek: bloom, star, lens flare, ezekről már írtam cikket.

Depth of field, motion blur: egy becsületes kamera modellnek ezek is részei a megbeszélt paraméterek által vezérelve.

Color grading: az előállított kép színeire alkalmazol valamilyen leképezést, így például naplementekor kicsit vörösesebb a kép.

Light propagation volumes, VXGI: realtime global illumination-hoz. A Maxwell architektúrájú nVidia kártyákban hardveresen implementálva van.

Mitől fizikán alapuló egy megjelenítő? A pongyola válasz az, hogy fizikailag korrekt, de nem feltétel, hogy fotorealisztikus legyen. Azonban a mainstream enginek közül van amelyik azt állítja magáról, hogy PB, de mégsem teljesen az. Leveleztem erről a DICE egyik fejlesztőjével és szerinte három olyan dolog van, amit teljesíteni kell ahhoz, hogy egy engine PB legyen:

  • fizikailag korrekt anyagok
  • fizikailag korrekt fények
  • fizikailag korrekt kamera
Ebből a háromból sok engine csak az elsőt csinálja meg. Unity-ben például a fények paraméterezése valami teljesen idióta elméleten alapul (1-8-ig lehet megadni egy intenzitást). Az egy dolog, hogy alapból nem tonemap-el, de mégcsak nem is gamma korrektál (csak a fizetős verzió).

Az Unreal 4 már lényegesen jobb, de a fények paraméterezése el van rontva. Például a spot fény állításuk szerint lumenben várja az értéket, ehhez képest 1700 lumen alig világít meg egy 36 négyzetméteres szobát (auto exposure-t direkt kikapcsoltam). A hiba valószínűleg abból adódik, hogy centiméterben számolnak, pedig méterben kellene...

A DICE-féle Frostbite mindhárom dolgot implementálja, de ezt nem sikerült kipróbálnom. A CryEngine 3-at szintén nem.

Offline rendererek közül a könyvhöz tartozó pbrt (nem próbáltam) és az azon alapuló Mitsuba helyesen implementálja a dolgokat, így összehasonlítási alapnak az utóbbit használtam.

Zárszónak annyit, hogy ez a technológia nagyon is aktuális és még mindig fejlődés alatt áll (pl. a területi fények jobb analitikus közelítése). Véleményem szerint a hardver fejlődésével egyre többször fog előkerülni a Monte Carlo integrálközelítés, így végső soron a realtime path tracing felé haladunk.

Kód a szokott helyen, akiknek nincs számítógépe azoknak meg videó.


Höfö:
  • Kezdj el mélyebben foglalkozni a PBR-el!
  • A legegyszerűbb az, ha írsz egy ray- vagy path tracert!

Irodalomjegyzék

https://seblagarde.files.wordpress.com/... - Moving Frostbite to Physically Based Rendering (DICE, 2014)
http://www.crytek.com/download/2014_03_25_CRYENGINE_GDC_... - The Rendering Technology of Ryse (Crytek, 2014)
http://blog.selfshadow.com/publications/s2013-shading-course/karis/... - Real Shading in Unreal Engine 4 (Epic, 2013)
http://blog.selfshadow.com/publications/s2013-shading-course/hoffman/... - Physics and Math of Shading (Naty Hoffman, 2013)
http://www.crytek.com/download/fmx2013_c3_art_tech_... - The Art and Technology behind Crysis 3 (Crytek, 2013)
https://mediatech.aalto.fi/~jaakko/T111-5310/K2013/... - Reflectance Equation, Rendering Equation (Aalto University, 2013)
http://blog.selfshadow.com/publications/s2012-shading-course/burley/... - Physically Based Shading at Disney (Disney, 2012)
http://renderwonk.com/publications/s2010-shading-course/... - Practical Implementation at tri-Ace (tri-Ace, 2010)
http://www.pbrt.org/ - Physically Based Rendering From Theory to Implementation (2010)

back to homepage

Valid HTML 4.01 Transitional Valid CSS!