English Welcome Kecy Programování 3D - Engine Guestbook Odkazy Downloady O autorovi Napiš mi Mailform
box_cz 3D - Engine 10 - světlo a metody osvětlování 2 (stencil buffer) box_cz

Ahoj !

Tak zase jednou.. ani to moc netrvalo :-) Jak se vám líbil minulej díl ? Shadow-buffer ? Dobrý, ne ? No jo, ale trochu pomalý a ne moc realistický. Dneska se podíváme na jinej způsob jak udělat stíny no a tím je (jak jste mohli zjistit podle seznamu z minulého dílu)


Raytracing

Je to přesně to co to znamená - sledování paprsku. Na raytracingu může být založený i celý engine a dost lidí to tak dělá. Prostě nekreslí objekty, ale vysílá z kamery paprsky do prostoru a hledá průsečíky s objekty, které (jen ty nejbližší) pak zobrazuje. Potom objekty nejsou definované polygony (i když to taky jde ale pomaleji) ale jejich rovnicemi : potom můžete vidět raytracované koule, roviny, válce a kdovíco co lze nějak matematicky definovat. Rovnici roviny už známe :

ax + by + cz + d = 0

Koule je takhle :

(center.x - x)2 + (center.y - y)2 + (center.z - z)2 = radius2

Chápete ? Tomu se říká raytracing, obvykle to je hodně pomalé, ale existují i brutálně optimalizované systémy dosahující velice dobrých výsledků, třeba Trezebees .. Výhoda tohoto systému je, že nemáme hodně malých polygonů, ale třeba jen jednu kouli, nebo rovinu nebo nevímco, takže si můžeme hezky pohrát s vektory a počítat s ambient + diffuse + specular světlem, počítat odrazy objektů v sobě a podobně. Těžší je to zase s texturami. Buď se objekty kreslí jen v barvě, nebo se na ně nanese textura systémem xyz -> uvw. To se vezme normála objektu v tom místě a zjistí se její největší složka. Pokud je to x, potom u = y v tom bodě a v = z v tom bodě. Pokud y, pak u = x, v = z. No a pokud je největší z, pak u = x, v = y. Tohle se dá použít i pro náš model, pokud budete psát nějaký editor 3D, používala to většina editorů pro Quaka I, hlavně proto že textura všude bez obtíží navazuje. Spolu s raytracingem přišel taky pojem procedurální textury, to jsou textury, jejichž barva se vypočítá podle nějakého vzorečku (r = sin(x) * 256, g = sin(y) * 256, b = cos(z) * 256 nebo nějakého jiného) to se používá dodnes a hodí se to na šachovnice, nebe, různé šumy ... Taky si to zkusíme. Raytracing je jako zrozený pro C ++. Existuje třída CGeometry, která má možnost spočítat průsečík s paprskem, normálu v určitém bodě (funkce jsou jen virtuální a prázdné) Potom jsou klony - CSphere, CPlane, CTube, CTorus .. a nemusíte se starat o to s čím se paprsek protíná, C ++ to udělá za vás. Existuje ale ještě jedna metoda, vhodná pro náš polygonální svět a tou je raycasting spojený se stínovými paprsky. Pure raytracing vypadá nějak takhle :

ray_max

ray_trezebees

ray_trezebees 2
Krása, co ?


Raycasting

Raycasting jako takový je taky raytracing bez odrazů. Prostě se vyšle paprsek a kam se zapíchne, to se vykreslí. A něco podobného je ta věc se stínovými paprsky - vystřelíme paprsky z kamery (můžou být předpočítané podle úhlu klipovací pyramidy a rozlišení) a spočítáme průsečíky se světem (budeme používat bounding sphere aby to bylo rychlejší) a přichází řada na stínové paprsky - z každého bodu objektu (těch co jsou vidět) se vystřelí paprsky ke světlům a zjišťuje se jestli je světlo vidět, udělají se kejkle s normálami a můžeme spočítat diffuse (a specular) složku, kterou zmodifikujeme barvu pixelu. Kdyby jsme to dělali takhle pro každý pixel, asi by to bylo dost pomalé (kvůli těm průsečíkům) ale je tu jednoduchá optimalizace : Spočítá se to prostě třeba pro mřížku 64×64 a ta se na obraz roztáhne, na 320×200 bez nějákých kvantovacích problémů v pohodě. A kdo to chce mít přesnější, může ještě kontrolovat rozdíl v hodnotách, a pokud je velký (většinou na hranách objektů, spočítá prostě i to mezi nimi ve větším rozlišení a bude mít i ostré hrany objektů) Říkal jsem že budeme potřebovat rychlý algorytmus pro počítání průsečíků. Dá se použít BSP, jenže s tím by to bylo moc složité. Lepší by byl OcTree, který ještě neumíte, takže použiju bounding sphere, které se lehce vysvětlují.


Bounding sphere algoritmus

Bounding sphere je prostě koule (sphere), která obklopuje objekt (bounding) a když střílím paprsek, kontroluju napřed sphere a až potom vlastní objekt. Sphere má pozici a poloměr (radius) pozice je prostě průměr všech bodů objektu a radius je vzdálenost nejvzdálenějšího bodu od pozice sphere. To snad ani nebudu kreslit. My ale potřebujeme průsečík - na to si vezmeme jako u roviny - rovnici (ne roviny ale koule) :

(center.x - x)2 + (center.y - y)2 + (center.z - z)2 = radius2

Pokud rovnice platí, máme bod [x, y, z], který leží na kouli [center, radius]. My ale potřebujeme průsečík s přímkou (přesněji polopřímkou) - paprskem. Když si to představíte, koule a přímka můžou mít jeden, dva nebo žádný průsečík. To odpovídá výsledkům kvadratické rovnice. Nám stačí (zatím) znát jen jestli průsečík je, nebo není. Pokud je průsečík jen jeden, znamená to že paprsek se koule jen dotýká a to můžeme s klidem zanedbat. Takže nám stačí znát, kdy je determinant ? ne ! denominátor ? ne. jo - diskriminant na to slovo jsem si nemoh vzpomenout, musel sem volat svýmu kámošovi ha ha, no jo, prázdniny. Pokud je diskriminant kladný, bude mít rovnice dva kořeny. Máme situaci :

bounding sphere math

Kde :
A = bod kde je kamera
C = bod kde má koule střed
u = vektor, definující paprsek
v = vektor z kamery do středu koule
r = radius koule
I = průsečík

My teď potřebujeme vyjádřit x, y a z z té rovnice. Tak teď to zase jednou rozepíšu ať se matematici pořádně vyřádí :

(C.x - I.x)2 + (C.y - I.y)2 + (C.z - I.z)2 = r2

a zároveň musí platit :

I = A + t×v (rovnice přímky)

a zároveň t > 0 (paprsek je až před kamerou)

C.x2 - 2C.xI.x + I.x2 + C.y2 - 2C.yI.y + I.y2 + C.z2 - 2C.zI.z + I.z2 = r2
(C.x2 + C.y2 + C.z2 - r2) - 2C.xI.x + I.x2 - 2C.yI.y + I.y2 - 2C.zI.z + I.z2 = 0

I.x = (A.x + tv.x)
I.y = (A.y + tv.y)
I.z = (A.z + tv.z)

(C.x2 + C.y2 + C.z2 - r2) -
2C.x(A.x + tv.x) + (A.x + tv.x)2 -
2C.y(A.y + tv.y) + (A.y + tv.y)2 -
2C.z(A.z + tv.z) + (A.z + tv.z)2 = 0

(C.x2 + C.y2 + C.z2 - r2) -
2C.xA.x - 2C.xtv.x + A.x2 + 2A.xtv.x + t2v.x2 -
2C.yA.y - 2C.ytv.y + A.y2 + 2A.ytv.y + t2v.y2 -
2C.zA.z - 2C.ztv.z + A.z2 + 2A.ztv.z + t2v.z2 = 0

(C.x2 + C.y2 + C.z2 - r2) -
2C.xA.x + 2tv.x(-C.x + A.x) + A.x2 + t2v.x2 -
2C.yA.y + 2tv.y(-C.y + A.y) + A.y2 + t2v.y2 -
2C.zA.z + 2tv.z(-C.z + A.z) + A.z2 + t2v.z2 = 0

(C.x2 + C.y2 + C.z2 - r2 - 2C.xA.x - 2C.yA.y - 2C.zA.z + A.x2 + A.y2 + A.z2) +
2tv.x(-C.x + A.x) + 2tv.y(-C.y + A.y) + 2tv.z(-C.z + A.z) +
t2v.x2 + t2v.y2 + t2v.z2 = 0

(C.x2 + C.y2 + C.z2 - r2 - 2C.xA.x - 2C.yA.y - 2C.zA.z + A.x2 + A.y2 + A.z2) +
2t(v.x(-C.z + A.x) + v.y(-C.z + A.y) + v.z(-C.z + A.z)) +
t2(v.x2 + v.y2 + v.z2) = 0

No a to už vlastně máme rovnici, ze které můžeme vyextrahovat diskriminant.
(kv. rovnice by potom měla tvar _At2 + _Bt + _C = 0)

_A = v.x2 + v.y2 + v.z2
_B = 2(v.x(-C.x + A.x) + v.y(-C.y + A.y) + v.z(-C.z + A.z))
_C = (C.x2 + C.y2 + C.z2 - r2 - 2(C.xA.x + C.yA.y + C.zA.z) + A.x2 + A.y2 + A.z2)

My bychom mohli vypočítat t1 a t2 podle vzorce :

t1, t2 = (_B +- sqrt(_B2 - 4_A_C)) / 2_A

Můžeme to ještě trošku zjednudušit, pokud víme že vektor paprsku je jednotkový:

q = A - C

_A = v.x2 + v.y2 + v.z2 = 1
_B = 2(v.xq.x + v.yq.y + v.zq.z) = 2(v * q)
_C = |C| + |A| - 2(C * A) - r2 = |q|2 - r2

t1, t2 = (_B +- sqrt(_B2 - 4_A_C)) / 2_A
t1, t2 = (_B +- sqrt(_B2 - 4_C)) / 2

_B' = v * q = _B / 2

t1, t2 = (2_B' +- sqrt(4_B'2 - 4_C)) / 2
t1, t2 = (2_B' +- sqrt(4(_B'2 - _C))) / 2
t1, t2 = (2_B' +- 2sqrt(_B'2 - _C)) / 2
t1 = _B' - sqrt(_B'2 - _C)
t2 = _B' + sqrt(_B'2 - _C)

To se učí na všech středních a gymnáziích. Těmi t bysme vynásobili vektor v a přičetli k A (rovnice přímky) a dostali bychom průsečíky. Bližší průsečík (ve směru paprsku) je t1, protože jak víme, odmocnina vyjde vždy kladná, tzn. potřebujeme ji odečíst, aby t bylo co nejmenší. Nás ale zajímá jen ten diskriminant :

D = _B2 - 4_A_C = _B'2 - _C = (v * q)2 - |q|2 + r2

No a pokud to vyjde kladné tak máme průsečík. Huff .. To je hrůza, radši to ještě nechám zkontrolovat v excelu :-) Jo. Nas druhej pokus to vyšlo:


#define _Dot(a,b) ((a).x*(b).x+(a).y*(b).y+(a).z*(b).z)
#define _Len2(a) _Dot(a,a)

float f_IntersectSphere_t(Vector v_ray_org, Vector v_ray,
                          Vector v_sph_org, float f_radius)
{
    float d, b;
    Vector q;

    q.x = v_ray_org.x - v_sph_org.x;
    q.y = v_ray_org.y - v_sph_org.y;
    q.z = v_ray_org.z - v_sph_org.z;

    b = _Dot(v_ray, q);
    d = b * b - _Len2(q) + f_radius * f_radius;

    if(d < 0)
        return -1;
    // žádný průsečík

    return b - (float)sqrt(d);
}

Vector v_IntersectSphere_t(Vector v_ray_org, Vector v_ray,
                           Vector v_sph_org, float f_radius)
{
    float d, b;
    Vector q;

    q.x = v_ray_org.x - v_sph_org.x;
    q.y = v_ray_org.y - v_sph_org.y;
    q.z = v_ray_org.z - v_sph_org.z;

    b = _Dot(v_ray, q);
    d = b * b - _Len2(q) + f_radius * f_radius;

    if(d < 0)
        return v_ray_org;
    // žádný průsečík

    d = b - (float)sqrt(d);
    // vzdálenost

    v_ray_org.x += v_ray.x * d;
    v_ray_org.y += v_ray.y * d;
    v_ray_org.z += v_ray.z * d;
    // bližší průsečík

    return v_ray_org;
}

Je to funkce pro výpočet vzdálenosti bližšího průsečíku a druhá s výpočtem průsečík, vypadá že to bude i fungovat... Je tu ještě jedna možnost, a tou je trojúhelník - vektory u, v a kolmice na v, vedená ze středu koule. Pokud je úhel moc velký, parsek kouli prostě mine. dejme tomu že chybějící bod trojúhelníku bude X. My můžeme lehce spočítat stranu XC podle goniometrických funkcí. Pak stačí zjistit zda je větší nebo menší než poloměr koule a víme že ji mine / strefí.

r / Len(v) = tg(fi)
(fi je úhel který svírá vektor u a v)

Tak, teď už vesele můžeme optimalizovat a odchytávat paprsky... Jen nevíme jak zjistit průsečík paprsku s face :


Průsečíky paprsku s face

Metod je určitě hodně, tady si ukážeme dvě. Ta první je jednodušší - okolo polygonu se vypočítá "ohrádka" z rovin (roviny, ležící na jeho hranách - obrázek) a potom průsečík paprsku s jeho normálovou rovinou (to už známe z klipování polygonů). Pokud průsečík leží uvnitř ohrádky, vyhráli jsme a paprsek protíná face.

ray-face 1
Ty průhledné věci mají být roviny

Ještě vám ukážu jak se ty roviny vytvoří (i když byste na to měli přijít pomalu už sami) Funkce má dva parametry - začátek a konec paprsku, upravit to na začátek a vektor není problém. Vrátí to 1 když je průsečík uvnitř, myslím...


int RayCast(Vector *start, Vector *end, Face *fac)
{
    Vector mid, v1, v2, pl;
    float a, b, t, d;

    a = start.x * fac->normal.a + start.y * fac->normal.b +
        start.z * fac->normal.c + fac->normal.d;
    b = end.x * fac->normal.a + end.y * fac->normal.b +
        end.z * fac->normal.c + fac->normal.d;
    // vzdálenosti od roviny

    if(a * b > 0)
        return 0;
    // pokud jsou oba body na stejné straně

    t = -a / (b - a);
    mid.x = start->x + t * (end->x - start->x);
    mid.y = start->y + t * (end->y - start->y);
    mid.z = start->z + t * (end->z - start->z);
    // spočítá průsečík

    v1.x = -fac->normal.a;
    v1.y = -fac->normal.b;
    v1.z = -fac->normal.c;
    // první vektor (pro všechny roviny stejný)

    v2.x = fac->vertex[1]->x - fac->vertex[2]->x;
    v2.y = fac->vertex[1]->y - fac->vertex[2]->y;
    v2.z = fac->vertex[1]->z - fac->vertex[2]->z;
    // první hranový vektor

    _Cross(v1, v2, pl);
    _Normalize(pl);
    d = - (pl.x * fac->vertex[2]->x +
           pl.y * fac->vertex[2]->y +
           pl.z * fac->vertex[2]->z);
    // spočítá rovinu

    if(mid.x * pl.x + mid.y * pl.y + mid.z * pl.z + d < -1e-3)
        return 0;
    // otestuje průsečík

    v2.x = fac->vertex[0]->x - fac->vertex[1]->x;
    v2.y = fac->vertex[0]->y - fac->vertex[1]->y;
    v2.z = fac->vertex[0]->z - fac->vertex[1]->z;
    // druhý vektor

    _Cross(v1, v2, pl);
    _Normalize(pl);
    d = - (pl.x * fac->vertex[1]->x +
           pl.y * fac->vertex[1]->y +
           pl.z * fac->vertex[1]->z);
    // spočítá rovinu

    if(mid.x * pl.x + mid.y * pl.y + mid.z * pl.z + d < -1e-3)
        return 0;
    // otestuje průsečík

    v2.x = fac->vertex[2]->x - fac->vertex[0]->x;
    v2.y = fac->vertex[2]->y - fac->vertex[0]->y;
    v2.z = fac->vertex[2]->z - fac->vertex[0]->z;
    // treti vektor

    _Cross(v1, v2, pl);
    _Normalize(pl);
    d = - (pl.x * fac->vertex[0]->x +
           pl.y * fac->vertex[0]->y +
           pl.z * fac->vertex[0]->z);
    // spočítá poslední rovinu

    if(mid.x * pl.x + mid.y * pl.y + mid.z * pl.z + d < -1e-3)
        return 0;
    // otestuje průsečík

    return 1;
}

Další, důmyslnější, i když ne tak přesná metoda je počítání úhlů. Vezme se zase průsečík paprsku s rovinou facu a vytvoří se tři vektory od průsečíku k vrcholům. Potom se sčítají úhly mezi těmito vektory (na obrázku psi1 psi2 psi3). Pokud součet bude 180°, je bod uvnitř (je potřeba dát určitou toleranci, která je původcem chyb)

ray-face 2

Výhodou první metody je přesnost a možnost roviny předpočítat. Takže počet operací spadne na několik násobení a tři cmp. Ve druhé metodě jsou to tři arccos, tři cmp, o tři násobení míň a tři odmocniny navíc. Takže ta první je rychlejší a přesnější (pokud je dost paměti na uchování těch rovin) Stejně vám k tomu zase můžu dát zdroják ať jsme komplet.


int RayCast_v2(Vector *start, Vector *end, Face *fac)
{
    Vector mid;
    Vector t[3];
    int i, p;
    float a, b, k, angle;
    float ilen, plen;

    a = start.x * fac->normal.a + start.y * fac->normal.b +
        start.z * fac->normal.c + fac->normal.d;
    b = end.x * fac->normal.a + end.y * fac->normal.b +
        end.z * fac->normal.c + fac->normal.d;
    // vzdalenosti od roviny

    if(a * b > 0)
        return 0;
    // zase zkontroluje jestli nejsou na stejné straně

    k = -a / (b - a);
    mid.x = start->x + k * (end->x - start->x);
    mid.y = start->y + k * (end->y - start->y);
    mid.z = start->z + k * (end->z - start->z);
    // spočítá průsečík

    angle = 0.0;
    p = 2;

    t[p].x = fac->vertex[p]->x - mid.x;
    t[p].y = fac->vertex[p]->y - mid.y;
    t[p].z = fac->vertex[p]->z - mid.z;
    // potřebujeme ještě předchozí vektor

    plen = _Len(t[p]);

    for(i = 0; i < 3; i ++) {
        t[i].x = fac->vertex[i]->x - mid.x;
        t[i].y = fac->vertex[i]->y - mid.y;
        t[i].z = fac->vertex[i]->z - mid.z;
        //
        ilen = _Len(t[i]);
        angle += (float)acos((DOT(t[i], t[p])) / (ilen * plen));
        plen = ilen;
        p = i;
    }
    // počítá úhly

    if(fabs(angle - D_PI) < 0.001)
        return 1;
    // pokud protne, vrátí 1

    return 0;
}

No.. to už by mělo stačit k napsání toho raycasteru ..


Raycasting se shadow paprsky

Tak, můžeme se na to vrhnout. Budeme kreslit scénu jako předtím, ale bude tam navíc jedna vrstva (ta matice 64×64) kde bude uložené (RGB!) diffuse light. Zájemci si můžou připsat i specular vrstvu a mít i lesklé objekty. Možná ... specular vrstva bude určitě v raytraceru. Tady zatím jen diffuse přes Lambertovu rovnici. Na začátku si přepočítáme sadu vektorů, které budeme střílet a při každém snímku tak zjistíme nejbližší průsečík s nějakým objektem. Potom musíme spočítat stínové paprsky ke všem světlům a určít zda něco světlo zastiňuje a podle toho reagovat. Nakonec se spočítá normální obraz a vynásobí se diffuse (a specular) vrstvou a máme další dynamické stíny za relativně nízkou cenu. Další možnost je nepoužívat matici, ale jen interpolovat x, y, z. To je o dost pomalejší, ale taky o dost přesnější. Když používáme S-Buffer a ne Z-Buffer jako většina grafického hardwaru (výjimkou je KYRO, používající tile-based rendering což je pravděpodobně něco podobného), máme možnost zjistit co bude na kterém místě v matici a přímo vypočítat průsečík. Potom ušetříme hodně času při hledání nejbližšího objektu. Ti co používají Z-Buffer / Hardware musí holt hledat. Takže k example. ... no asi vás moc nepotěším, ale nebude. Tahle metoda je už přeci jen trošku out, takže místo ní dáme něco trochu jiného.


Stencil stíny

Stencil buffer je záležitost nových (ne až tak nových, TNT už myslím stencil dávno měly). Jde o další buffer, který se dá použít pro různé účely. Jako takový není vidět, ale je možnost ovlivnit vykreslování scény na základě jeho obsahu. Ale ke stínům.

Základem byly tzv. shadow volumes - stínové objemy. Vypadá to asi takhle : od každého facu se promítne jeho "jakoby stín", který jde teoreticky až do nekonečna. Cokoliv je vevnitř objemu, tvořeného plochami stínu je kupodivu ve stínu :

shadow volume

Stencil stíny fungují tedy tak, že stínový objem se vykreslí (poté, co už je scéna hotová) a když se kreslí plocha, natočená ke kameře rubem, přičte se do stencilu jednička na pozici odpovídajícího pixelu. Pokud je ke kameře lícem, jednička se odečte. Za předpokladu, že náš stencil na začátku vynulujeme, místa kde je hodnota nula jsou osvětlené. (buď tam stín vůbec nesahá, nebo je vidět přední i zadní strana stínu, takže pixel je za volume) Pokud je hodnota nenulová, pixel bude někde uvnitř stínu. Je to docela jednoduché, ale má to jednu chybu : co když je kamera uvnitř stínového objemu ? To je docela průšvih, protože se vynechá jeho první stěna, takže stíny přejdou pravděpodobně do negativních (to co má být stín, bude světlo, co má být světlo, bude stín) Zjišťovat, jestli kamera je uvnitř objemu je jednak neelegantní, ale taky nepřesné na hranách, kdy by to stejně problikávalo. Nejjednodušší nápad je nakreslit všechno bez testování průniků se světem (Z / S-bufferu), ale do stencilu se budou přičítat opačné hodnoty - pro rub mínus jedna, pro líc jedna. Potom vlastně zjistíme, kde se volume můžou kreslit a pokud předek není vidět, bude tam jednička. Když to ale vezmeme kolem a kolem :
  • Nakreslit rub, odečíst od stencilu jedničku
  • Nakreslit líc, přičíst ke stencilu jedničku
  • Nakreslit rub, pokud je vidět, přičíst ke stencilu jedničku
  • Nakreslit líc, pokud je vidět, odečíst od stencilu jedničku
Když to promyslíme, vidíme že se to dá sloučit :
  • Nakreslit rub, pokud není vidět, odečíst od stencilu jedničku
  • Nakreslit líc, pokud není vidět, přičíst ke stencilu jedničku
To už je lepší - zabere to polovinu času :-) Teď můžeme řádit a řádit. Prostě naplníme stencil nulama, vykreslíme si S-Buffer a pak pro každé světlo kreslíme stěny volume, které nejsou vidět do stencilu. Potom určíme jednoduchým testem nulovosti co je a co není světlo, podobně jako ve shadow bufferu. Jenže stencil má oproti shadow bufferu stíny tak ostré, jak to dovolí Z-Buffer, to znamená, že tam nejsou žádné kostky, jako u shadow bbufferu. Pokud kreslíme víc světel, je lepší si udělat rovinu se světlem a k ní přičítat světlost jednotlivých světel (za každým světlem je potřeba vyčistit stencil !) a nakonec při kreslení obrazu pixely prostě pronásobit.

Dá se to optimalizovat tak, že se nekreslí okraje každého facu, ale jen silueta objektu. Ta se dá vypočítat tak, že pro každý face určíte sousedy (přes jeho hrany) a zjišťujete, zda z pohledu světla je vidět face a potom podle sousedních faců (pokud nejsou vidět) se kreslí stěny stínu, promítnuté z odpovídajících hran. viz. obrázek :

silhouette

Budeme zjišťovat, které hrany krychle náleží do siluety. Obrázek je viděný z pohledu světla. Zkoušíme face 1. Je vidět z pohledu světla ? je. Teď - máme čtyři hrany. První hrana napojuje sousední face 4. Ta vidět není, takže hrana je aktivní. Stejně je to s hranou dvě, jejíž soused taky není vidět. Hrana tři spojuje face 2, která je vidět, takže se nic neděje. Stejné to je i s hranou 4 a face 3. Takže z prvního facu povedou dvě stínové plochy. Jen tak na okraj - pohled světla se samozřejmě nekreslí - zjišťuje se jen jestli světlo leží před, nebo za rovinou facu.

stencil silhouette
siluety v naší scéně

Ještě taková věc - je potřeba, aby zadní část volume bylo úplné - tedy se musí kreslit i "čela", nejen stěny. Tím prvním jsou vlastní facy a tím druhým jsou facy, posunuté na konec volume.

stencil shadow volume
bacha na věc - velké polygony budou trošku blíž světlu

Teď už není problém. Jediná věc, která nám trochu stojí v cestě je S-Buffer, který moc dobře nehandluje segmenty, které nejsou vidět. Řešením by bylo nakreslit i Z-Buffer a stěny objemů kontrolovat pomocí něj, ale to by bylo pomalé, takže si musíme dát tu práci a napsat rutinu pro kreslení neviditelných segmentů.

V example jsem to nakonec vyřešil dvěma rutinami, protože S-Buffer opravdu špatně řešil rovnoběžné plochy, takže se volume napřed kreslí celé bez testu hloubky a potom bez přední stěny, kterou by měl normálně zakrýt face, vrhající stín, jenže místo toho se vykreslí šum. Z-Buffer na 3D - kartách to vyřeší líp. Jediným obtížným úkolem může být hledání sousedů, které tu vysvětlím :


struct Face {
    int n_id, n_used;
    int n_material;
    //
    Face *n_neighbour[3];
    //
    Vertex *vertex[3];
    //
    Plane normal;
};

int b_SameVertex(Vertex *p_vertex_a, Vertex *p_vertex_b)
{
    float f, d;

    f = (p_vertex_a->x - p_vertex_b->x);
    f *= f;
    d = (p_vertex_a->y - p_vertex_b->y);
    f += d * d;
    d = (p_vertex_a->z - p_vertex_b->z);
    // vzdálenost mezi vrcholy
    
    return ((f + d * d) < 0.01f);
}

void Find_FaceNeighbours(Object *p_object)
{
    int n_edge_a, n_edge_b;
    int n_edge_c, n_edge_d;
    int i, j, k, l;

    for(i = 0; i < p_object->n_face_num; i ++) {
        p_object->face[i].n_neighbour[0] = NULL;
        p_object->face[i].n_neighbour[1] = NULL;
        p_object->face[i].n_neighbour[2] = NULL;
    }

    for(i = 0; i < p_object->n_face_num; i ++) {
        for(n_edge_a = 0; n_edge_a < 3; n_edge_a ++) {
            n_edge_b = (1 + n_edge_a) % 3;
            l = (1 + n_edge_b) % 3;
            // indexy vertexů hrany (a, b)
            for(j = i + 1; j < p_object->n_face_num; j ++) {
                for(n_edge_c = 0; n_edge_c < 3; n_edge_c ++) {
                    n_edge_d = (n_edge_c + 1) % 3;
                    // indexy vertexů druhé hrany (c, d)

                    if((b_SameVertex(p_object->face[i].vertex[n_edge_a],
                                    p_object->face[j].vertex[n_edge_d]) &&
                       b_SameVertex(p_object->face[i].vertex[n_edge_b],
                                    p_object->face[j].vertex[n_edge_c])) ||
                       (b_SameVertex(p_object->face[i].vertex[n_edge_a],
                                    p_object->face[j].vertex[n_edge_c]) &&
                       b_SameVertex(p_object->face[i].vertex[n_edge_b],
                                    p_object->face[j].vertex[n_edge_d]))) {
                        k = (n_edge_d + 1) % 3;
                        if(p_object->face[i].normal.a * p_object->face[j].vertex[k]->x +
                           p_object->face[i].normal.b * p_object->face[j].vertex[k]->y +
                           p_object->face[i].normal.c * p_object->face[j].vertex[k]->z +
                           p_object->face[i].normal.d > -.01 &&
                           p_object->face[j].normal.a * p_object->face[i].vertex[l]->x +
                           p_object->face[j].normal.b * p_object->face[i].vertex[l]->y +
                           p_object->face[j].normal.c * p_object->face[i].vertex[l]->z +
                           p_object->face[j].normal.d > -.01) {
                            p_object->face[i].n_neighbour[n_edge_a] = &p_object->face[j];
                            p_object->face[j].n_neighbour[n_edge_c] = &p_object->face[i];
                        }
                    }
                    // facy se vzájemně označí za sousedy, pokud
                    // leží odvrácené od sebe (objekt je konvexí)
                }
            }
        }
    }
    // hledá sousední facy
}

Funkce b_SameVertex() slouží jen k zjištění, jestli jsou dva vrcholy na stejném místě. (přibližně) Ve funkci Find_FaceNeighbours() se napřed všechny sousední facy nastaví na NULL a potom se berou další facy a zkouší se, jestli sousedí nějakou hranou a potom se ještě testuje, jestli jsou konvexní. Pokud by byly konkávní, nebudou se uvažovat za sousední. To je třeba příklad místnosti se stěnami přivrácenými dovnitř, samozřejmě. Všechny facy jsou vidět a všechny sousedi by byly vidět, takže podle algoritmu pro optimalizaci siluet by se nenakreslily žádné hrany - stěny místnosti by tedy nevrhaly stín.

Ještě trochu ke světlu - v example se ve 3ds souboru počítá s jedním světlem, se kterým program hýbe po vodorovné kružnici okolo původního umístění světla. Na začátku se obvykle vykreslí ambient část světel, protože je potřeba mít něco v Z-bufferu (v našem případě S), abychom mohli kreslit stencil. Po vykreslení stencilu se vezme obraz a to, co je ve stínu se ztmaví na polovinu (v našem example). Normálně by se ale měl kreslit osvětlený svět, protnutý se stencilem a světlosti přičítat do obrazového bufferu. Pro další světla znovu a znovu, můžete klidně použít jen vertex shading. Tak, tady si to stáhněte a zdar !

stencil engine download

 Stencil engine

V další části budou ty lightmapy.

Nooo ... abych se přiznal tak jsem se při překládání tohodle tutorialu docela styděl, takže jsem to předělal aby to chodilo podle té rychlejší Carmackovy metody a navíc jsem přidal ještě smooth shading aby to vypadalo k světu.

stencil engine 2 download

 Stencil engine 2

... tady ho máte. Měli byste to bez problémů pochopit, kdyžtak pište. Zatím bye!

    -tHE SWINe-

zpět


Valid HTML 4.01!
Valid HTML 4.01