Először is hogyan kell kiejteni: frenel. Ugyanis Augustin-Jean Fresnel francia fizikusról van szó, aki leginkább a fény viselkedésével kapcsolatos egyenleteiről ismert. Konkrétan arról szólnak ezek az egyenletek, hogy
mi történik a fénnyel akkor amikor egyik közegből áthalad egy másikba (pl. levegőből vízbe). 3D grafika szempontjából két fontos dolog történik vele: egy része visszaverődik, a másik része megtörve továbbhalad (bármilyen viccesen is hangzik).
Emlékeztető általános iskolából
Mondhatnám azt, hogy ott a wikipédia, de akkor nem lenne túl sok értelme cikket írni. Mindenesetre annak könnyebb megértését elősegítendő átrombolnék a fontosabb dolgokon.
Ezek után A Fresnel-egyenletek két esetet vesznek figyelembe:
A jó hír az, hogy 3D grafikában csak polarizálatlan fénnyel foglalkozunk, ahol is R = (Rs + Rp) / 2 és a megtört fény energiája pedig T = 1 - R. Ez a két érték (R és T) grafika szempontjából egy szorzót jelent, mint majd látni fogjátok. A még jobb hír, hogy a törésmutató lehet komplex szám is (fémek). Sok sikert kiszámolni, akkor én itt be is fejeztem.
Ez az approximáció sikk
Valójában a fenti képlet már egy az egyben át is írható shaderbe, amennyiben meg tudod határozni a visszavert és megtört vektorokat (szegény vektorok, mindenki csak bántja őket). Kezdjük az egyszerűbb esettel: tükröződés.
CODE
float3 n = normalize(wnorm); // world space normal
float3 reflray = reflect(incray, n);
float3 refrray = refract(incray, n, n1 / n2);
Javaslom normalizálni őket, hiszen a fenti egyenletekben akkor a koszinuszok egy dot-al elő is állnak: CODE
float cosA = dot(normalize(reflray), norm);
float cosG = -dot(normalize(refrray), norm);
float Rs = (n1 * cosA - n2 * cosG) / (n1 * cosA + n2 * cosG);
float Rp = (n1 * cosG - n2 * cosA) / (n1 * cosG + n2 * cosA);
float R = (Rs * Rs + Rp * Rp) * 0.5f;
Ahonnan a végső szín egy R szerinti lineáris interpolációval áll elő az objektum színe és a cubemapból (a reflektált vektorral) olvasott szín között. Gondolom ezen nincs mit részletezni (ha mégis akkor megnézed a kódot). Na de...ez a számolás elég soknak tűnik. Van egy olcsóbb módszer, amit Schlick-féle közelítésnek hívnak. CODE
float3 n = normalize(wnorm);
float3 i = normalize(incray);
float R0 = (n1 - n2) / (n1 + n2);
R0 = R0 * R0;
R = saturate(R0 + (1 - R0) * pow(1 + dot(i, n), 5));
R0 itt a Fresnel egyenlet értéke akkor amikor a beeső fénysugár pont a normálvektor irányából találja el a felületet (azaz α = 0). Mindenki végiggondolja, hogy miért így írtam meg a shadert (mert bizony ugyanaz van leírva, mint a wikipédián). Bár az eredeti képletben a félvektor szerepel, a megfordított logika miatt az pont a normálvektor. Ha valaki azon aggodalmának kívánna hangot adni, miszerint a tükröződés alig látszik, annak javaslom, hogy HDR-ben rendereljen. Milyen jó ötlet, legyen is höfö.
Megfogyva bár, de törve nem
A fénytörés kódján senki nem fog meglepődni. CODE
float4 reflcol = texCUBE(mytex0, reflray);
float4 refrcol = texCUBE(mytex0, refrray);
color = lerp(refrcol, reflcol, R);
Mivel ez elég rövidke kód ahhoz, hogy egy egész bekezdést eltöltsek vele, megcsinálom az itt leírt normal mapped effektet is. Mit lehet erről elmondani: aki csinált már normal mappinget az valószínűleg ki tudja találni. Egy fontos dolog azonban mégis van: zöldfülűek nekiállnának a beeső fénysugarat textúra térbe trafózni (ahogy szoktuk), aztán csodálkoznak, hogy miért nem azt látják amit kéne. Persze, a számolást meg lehet ott is csinálni, de a cubemapből world spaceben kell olvasni. Célszerűbb tehát a normal mapból olvasott vektort átpakolni world spacebe: CODE
float3 tnorm = tex2D(normalmap, tex).xyz * 2 - 1;
float3 t = normalize(wtan);
float3 b = normalize(wbin);
float3 n = normalize(wnorm);
float3x3 tbn = { t, -b, n }; // továbbra se tudom miért így számolja a DX
n = mul(tnorm, tbn);
Innentől kezdve pedig ugyanaz a buli, mint eddig. Höfö megcsinálni dupla fénytöréssel.
Breaking bad
Most jön egy elszomorító hír: a fenti fénytörés és tükröződés is hibás. Először is azért mert a fénytörés sosem egyszer történik meg, hanem minimum kétszer (hiszen ki is megy az objektumból).
Ezt majd később megmutatom hogyan kell csinálni. A másik
problémával viszont már kevesebben vannak tisztában:
Ugyanis a cubemap címzése mindig az origóból indul. Felmerül a kérdés, hogy hogyan lehet kiszámolni a helyes pontot. Rossz hírem van: csak kereséssel. Ezt nem vázolnám fel, mert egyrészt tüköződés esetén lenyelhető a béka, másrészt fénytöréshez vannak egyéb megoldások is, ahol legalább nem cubemapben kell keresni (később). A linken megtalálható a kód a komplex törésmutatós R0 kiszámolásához is, de én marhára nem látok különbséget.
Friss és nedves
Ha a fénytörés szóba kerül, akkor nem lehet elmenni a víz megjelenítése mellett, ami bár egy bonyolult matematikai modellen alapul, ügyes trükkökkel össze lehet buherálni. A víz animációját továbbra is a normalmap tekerésével oldom meg, a tükröződéshez és töréshez viszont az már említett cikkben leírt módszert fogom alkalmazni.
CODE
float4 perturbed_refr = tex2D(refractionmap, ptex + distort.xy * 0.02f);
float4 masked_refr = tex2D(refractionmap, ptex);
float4 final_refr = lerp(perturbed_refr, masked_refr, perturbed_refr.a);
Ahol ptex projektív textúrázással címzi meg a textúrát, ahogy az első shaderes cikkben már említettem. Vigyázat, nem írtam el a lerp-et! Nem az érdekel, hogy az adott pixelben mi van, hanem az ami megtörik a vizen! Ha ott a maszk fehér, akkor az a pixel a víz fölött van, tehát nem szabad használni.
Chromatic dispersion
Magyarul szétszóródásnak hívják, ez szerintem elég pongyola fordítás, hiszen arról van szó amikor egy fénysugár felbomlik a színkomponenseire.
Ilyet mindenki látott már aki nem négy fal között él: úgy hívják, hogy szivárvány.
Négy fal között élők talán láttak már CD lemezt, az is ezt csinálja.
CODE
float n3 = refractiveindices.y + 0.1f;
float n4 = refractiveindices.y + 0.25f;
float3 refrray1 = refract(i, n, n1 / n2);
float3 refrray2 = refract(i, n, n1 / n3);
float3 refrray3 = refract(i, n, n1 / n4);
// ugyanaz a számolás mint előbb, mindhárom vektorral
color.r = lerp(refrcol1.r, reflcol.r, Rr);
color.g = lerp(refrcol2.g, reflcol.g, Rg);
color.b = lerp(refrcol3.b, reflcol.b, Rb);
Ehhez már SM 3.0 illendő. Persze lehet csalni, például a fresnel érték (R) lehet mindháromra ugyanaz. Egyszeres fénytörésnél nem néz ki egetrengetően, duplával meg nem próbáltam. Höfö megcsinálni szépre.
Rhapszódia
A dupla fénytöréshez nyilván szükség lesz valamilyen formában az objektum belsejére, azon belül is a normálokra, de a feladat megoldásához a pozíciókra is.
Az említett cikk az objektum belsejét és a környezetet is cubemapbe rendereli, és abban szekáns kereséssel találja meg a szükséges dolgokat.
Először én is ezzel próbálkoztam (jó régen...), de tök fölösleges és rengeteg memória. Szóval jobb ha elfelejted.
Tehát elindulok egy tetszőleges d-vel (1 eddig jó volt), és lépek a megadott irányban (p0 + d * v). Ezt a pontot levetítve olvasok a pozíciókat tartalmazó textúrából, megkapva p1-et. A következő becslés d-re p1 és p0 távolsága. Ezzel ugyanez történik, mint előbb, megkapva p2-t és így tovább. Az ábrából is látszik, hogy igen gyorsan konvergál, tehát 3-4 iteráció bőven elég. Annyit hozzátennék, hogy ciklussal implementálva problémák adódtak, nem világos, hogy miért. De ilyen kevés iterációra nem is kell ciklus. Na de akkor hogyan tovább: nevezzük el ezt a függvényt NewtonRhapson2D(outp, outn, p0, v)-nek, ami visszaadja a keresett normált (és a pozíciót is, de az most nem fog kelleni). CODE
float3 v0 = normalize(x0 - eyePos); // incident ray
float3 v1 = refract(v0, n0, ratio); // refracted ray
float3 x1, n1;
NewtonRhapson2D(x1, n1, x0, v1);
v2 = refract(v1, -n1, 1 / ratio); // secondary refracted ray
if( dot(v2, v2) > 0 )
refrcolor = texCUBE(mytex0, v2); // hit sky
else
refrcolor = texCUBE(mytex0, reflect(v1, -n1)); // total internal reflection
Bejött egy új fogalom, amit teljes belső visszaverődésnek hívnak. Ahogy a nevében is benne van, a fény nem tud áthatolni a felületen, mert olyan szögben sikerül eltalálnia. Ez akkor fordulhat elő, amikor n2 < n1. Persze lehetne tovább pattogtatni a sugarat, de minek. A különbség az egyszeres fénytöréshez képest ordít. Eltekintve persze az artifactoktól és attól, hogy ennél bőven többször meg kéne törjön a fény (pl. teáscsésze füle). De más szempontból is leegyszerűsített eset ez, mivel az objektumon kívül nem raktam be mást a jelenetbe, de a következő bekezdésből kiderül, hogy mit kellene akkor csinálni.
Caustic Armand
Most mindenki utálni fog, de ezt már lusta voltam leimplementálni. Hehe. Amúgy sem egy trivi probléma, mivel a caustic minősége erősen függ a fotonok számától. A rosszabb hír, hogy nem tudsz annyi fotont kipakolni amennyi már jól nézne ki
mert diavetítő lesz a programból.
Summarum
Érdekes módon ha valaki azzal akarja reklámozni a cuccát, hogy az milyen valósághű, akkor valamilyen tükröződő effektet rak bele. Persze az nem az engine érdeme, hanem a tükrözött környezeté, ami tipikusan egy fényképezővel készített HDR kép.
Úgy meg naná, hogy valósághű...el is nevezték image based lighting-nak, erről már volt szó a HDR cikkben. Mindenesetre az itt leírt dolgok felhasználhatóak nem csak IBL-ben, hiszen egy specular lightingra ugyanezek az egyenletek alkalmazhatóak.
Megjegyzés: a chromatic dispersion nem keverendő össze a chromatic abberation-al, ami bár ugyanazon ok miatt jön létre de egy zavaró effekt. Sok irodalom mégis ezt az elnevezést használja. Kód github-on (jobb oldalt van "download zip" gomb). Höfö:
Irodalomjegyzék http://en.wikipedia.org/wiki/Fresnel_equations - Fresnel egyenletek (wikipédia) http://en.wikipedia.org/wiki/List_of_refractive_indices - Néhány anyag törésmutatója (wikipédia) http://http.developer.nvidia.com/GPUGems2/gpugems2_chapter19.html - Egyszeres fénytörés (GPU Gems) http://http.developer.nvidia.com/GPUGems3/gpugems3_ch17.html - Többszörös fénytörés (GPU Gems) http://sirkan.iit.bme.hu/~szirmay/gpucaust5.pdf - Caustic (BME) |