English Welcome Kecy Programování 3D - Engine Guestbook Odkazy Downloady O autorovi Napiš mi Mailform
box_cz 3D - Engine 12 - radiosita box_cz

Radiosita

    Tak, dneska poklábosíme o metodě, zvané radiosita. Zní to sice dost magicky, ale není to až zas tak složité. Napřed trochu historie - v roce 1984 to začlo na Cornellově Universitě na kusu ušmudlaného a mastného papíru s nápisem Modelling the Interaction of Light Between Diffuse Surfaces. Papír měl na svědomí Goral, Torrance ho ušmudlal a zamastil a Greenberg napsal vnitřek. Jak si můžeme přeložit, je to o modelování interakce světla mezi difůzními povrchy. Jak víme, difůzní povrchy jsou "nelesklé" povrchy, tudíž je jedno ze kterého úhlu se díváme a světlost je pořád stejná. Nápad byl jednoduchý, ale mohl být určený pro cokoliv - distribuce světla ze žárovky, tepla z topení a při předpokladu lineárního Brownova pohybu a nehybného vzduchu třeba i smradu z rozkládajícího se nebožtíka.
    Ale jak to fungovalo ? Svět se rozdělil na teoreticky nekonečné množství malých plošek (patchů). Každá ploška reprezentuje část povrchu světa a má svou radiativní energii (vyzařující) a svou světlost. Na začátku mají všechny plošky světlost nula a radiativní energii mají nenulovou jen plošky, reprezentující světla. Radiosita se totiž zajímá plošnými světly. Potom se vytvořila tzv. radiosity matrix - matice o rozměru počet plošek krát počet plošek (ne, opravdu to nemělo být počet plešek :-)) a ta se procházela. V maticích byly uložené tzv. form factory, které říkají kolik energie se přenese z jednoho patche do druhého. (pokud teď dáte pozor, zjistíte že nebude jedno, jestli energie putuje tam, nebo zpátky, jinak by matice byla zbytečně velká) Potom se matice procházela : vzal se první řádek a energie z prvního patche se vyzářila do všech ostatních patchů na řádku podle příslušných form factorů. Část přijaté energie potom putuje do světlosti patche a část do radiativní energie (tj. část, co se odrazí) Výpočet byl hotový ve chvíli, kdy všechna radiativní energie byla nulová. Potom co Torrance snědl tisící plechovku olejovek Goral padl smrady a Greenberg přišel na to, že se pokouší donutit počítač spočítat dvojitou nekonečnou smyčku. (my víme, že by to bylo trochu moc i na sálový počítač z NASA, natož na Torrancovu 486 SX DX 2 XXX !) Takže Torrance odtáhl Gorala a roku 1988 přišel Cohen, Chen, Wallace a Greenberg s ještě ušmudlanějším papírem, protože Chen se neobešel bez indické karí omáčky a stařec Cohen jedl výhradně velké kusy masa :-). Greenberg sice nechtěl, aby Wallace byl mezi autory, ale on tvrdil že jeho skotský dědeček je papírář a papír pochází ze svobodného skotska. Papír, mastný od masa a trochu zažloutlý od kari se jmenoval A Progressive Refinement Approach to Fast Radiosity Image Generation. Tentokrát se to trochu zjednodušilo : Neprocházela se matice (která by mimoto zabrala pár gigabajtů paměti, protže každý prvek je float a při průměrně velkém levelu ...), ale vždycky se našel prvek s největší radiativní energií a ten se "vyzářil". Takhle pořád až dokud nebyla radiativní energie zanedbatelná. (jak by řekl ing. Gandalovič - "tohle tu bude běžet pořád až si pudeš pro maturitu, pokud zapomeneš ukončovací podmínku !" a určitě by dodal "jo, jo - sínusovej tón - ten je jak břitva !") Tohle v podstatě nekonečněkrát zmenšilo dobu výpočtu, protože předtím byla nekonečná a teď už je konečně konečná, takže můžeme programovat.
    Ještě než začneme, měli bychom ale něco vědět : patche by měly být tak malé, aby jejich velikost byla zanedbatelná oproti vzdálenosti mezi nimi (aspoň ve většině případů - pro sousední patche to nefunguje nikdy) Pro dělení patchů se dá použít mnoho metod - buď se vezme svět a řekne se : každá plocha se rozdělí na 8×8 patchů ! No, a vyběhnou výpočetní funkce s pilkami a svět pěkně dvakrát změří a jednou rozřežou. To je sice hezké, ale jak uvidíte, světlost záleží na ploše patche (je to energie a čím větší plocha, tím víc energie na patch napadá) - takže by bylo lepší mít patche stejně velké. Další metodou by bylo rozdělení podle velikosti původního objektu, takže menší plochy by se rozdělily na míň patchů. Ale protože patche se nakonec budou kreslit (protože obsahujou světlost), někdo vymyslel dynamické přerozdělování patchů - patche se přerozdělí jen tam, kde je to potřeba - na hraně stínu atd ... Další věc (která je tedy milá) je, že radiosita dává HDR výsledky (energie patchů může být od nuly do nekonečna), takže teď budeme muset trochu zaonačit výpočet výsledné barvy.
    Taky bych mohl dodat, že radiosita je tzv. metoda globálního osvětlení - není tam žádná energie pro jedno světlo, je tu prostě globální souhrn energií, které na sebe působí. Radiosita je vyvinutá pro difůzní povrchy a dají se s ní počítat interakce difůzní - difůzní a difůzní - spekulární. Ale už neumí spekulární - difůzní a spekulární - spekulární. Radiosita se ve hrách jako taková moc nepoužívá, protože je to právě metoda globálního osvětlení. Jedny dveře se otevřou a změní se osvětlení v téměř celé mapě. Problém je, že k výpočtu osvětlení viditelných částí je potřeba i dost neviditelných částí, takže to nejde udělat tak, že se vezme sada viditelných polygonů a počítá se jen s nimi - to by prostě nefungovalo, protže kdykoli by se změnila sada, světlost by kolísala, což by rušilo. Muselo by se prostě říct : tak, mám okruh 1024 okolo kamery, co je za ním mě nezajímá. To by jakž takž mohlo fungovat. Říkal jsem že ve hrách se nepoužívá jako taková, takže použitá je třeba ve hře Max Payne - světlo je počítané radiositou a vlastní stíny se dělají pomocí stencilu (což je pěst na voko - stencil stíny vostrý jak sínusovej tón a naproti tomu radiosita se stíny jak rozteklá kobliha)

far cry
Far Cry - lightmapový jemný stíny pro statické objekty + stencil pro dynamický (šoupatelný) věci

    Někdo možná slyšel o tzv. monte - carlo metodách - to jsou metody výpočtu za použití náhodných čísel, nebo jejich určených posloupností. Prostě když se střílí energie, nevystřelí se rovnoměrně, ale několik náhodných patchů se vynechá. Pro větší rozlišení to funguje dobře, protože sem tam se vloudí chybička, když patch má smůlu a netrefili ho víckrát. Výstup je potom trochu flekatej, takže se musí zfiltrovat, ale to se hodí opravdu jen pro off-line grafiku (tedy ne pro realtime)


Výpočet form factorů

    Form factory se dají spočítat podle rovnice :
form-factor equation
    Kde i a j jsou indexy patchů. Fi-j je form factor pro energii, jdoucí z i do j (ne naopak !) r je vzdálenost patchů, ty řecký písmenka jsou vyzařovací úhly (úhly, které svírá normála patche s paprskem) Ai a Aj jsou plochy patchů (z angl. area) a dAi a dAj jsou diferenciální plochy (tzn. poměr ploch - pro vyjádření kolik energie se vyzáří a kolik jí obdrží daný patch) Tohle je ale (jak možná pár zasvěcených pozná) první verze radiosity - ty dva integrály (takové ty fajfky) jsou právě ty nekonečné smyčky. Progressive refinement "zní" takhle :
progressive refinement form-factor equation
    To je podobné, jen trochu okousané. Matematicky se samozřejmě obě rovnice nerovnají. Jo, zapoměl bych ještě na Hij, což je viditelnost z jednoho patche do druhého. Obvykle se zase použije víc paprsků, takže číslo není nula nebo jedna, ale nějaká ta desetina. (záleží na tom, jestli chcete vyvádět realtime, nebo ne - i když realtime to taky jde)


A jak to vypadá

    jak jsem říkal - radiosita se používá pro plošná světla (i když pokud je světlo tak malé, že se rozdělí jen na jeden patch, bude se počítat jako bodové) takže způsobuje krásné měkké stíny, věci prosvětlené radiositou jsou hezky světlé, jelikož radiosita počítá i s odrazy. Tady je takový zádrhel - pokud by byl povrch, který světlo naprosto odráží, jeho světlost bude podle radiosity nulová. Ale nesmíme zapomenout, že musíme přičíst odraz okolí, takže černý vlastně taky nebude ;-) (pokud by odrážel míň, odraz okolí už se nezapočítává) Vypadá to takhle pěkně :

cornell box
Cornellova krabička (všimněte si, že se odráží barvy !)


3ds max
Tohle je 3ds max 5 (asi fake, ale dobrej příklad)


movsd - realtime radiosity 2
Demo Realtime radiosity 2 (plochy odvrácené od světla nejsou černé)


movsd - realtime radiosity 2
Taky RR2 (na stole pod světlem jsou hodnoty osvětlení > 1)


Co my s tím ?

    Tak. My známe jednu implementaci - matematickou. Existuje ale ještě jedna - pomocí polokostek. Anglicky se tomu říká hemicube. Vypadá to taglenc :

    hemicube
No a co to je ? Je to docela jednoduché - vezme se rasterizer, kamera se práskne doprostřed patche (kde je taky střed téhle kostky) a vykreslí se jedna a čtyři poloviny obrazovky. (Na obrázku je znázorněná kamera, která kouká nahoru z hemicube) Zase se nekreslí barvy, ale provádíme náš oblíbený poly - id buffer. Podle něj potom přičteme hodnoty každému patchi, který je vidět. Jaké hodnoty ? Přece předpočítané. Do stěn polokrychle (to zní hrozně :-)) prostě předpočítáme část rovnice. A kterou ? Budeme přemýšlet - diferenciální plochu tam máme (když tam bude víc pixelů jednoho patche, potom ten patch dostane víc energie), s tím souvisí i vzdálenost, viditelnost tam máme, no a schází nám cos() / Pi. To Pi klidně nahradíme jedničkou, protože součet všech map dohromady nám musí dát jedničku - tedy ji budeme normalizovat (takže bude určitě uložená jako float) Ono .. vlastně by to nemělo být takhle jednoduché, protože převádíme plochu, která má být viditelná z pohledu patche (totiž promítnutá na kulovou plochu) na krychli. Dá rozum, že pixely uprostřed mají v reálu větší velikost, než pixely u hran přední plochy :

    cube to sphere
Je vidět, že horní plocha má větší projekci, takže i pixel horní plochy by měl mít větší váhu pro přenos energie. Bude ale poměrně jednoduché a relativně přesné vzít každý pixel naší předpočítané mapy a promítnout ho na polokouli (ale pořád to bude čtverec) a tím zjistit jeho plochu. Plochy nakonec normalizujeme a bude to.     Další věc je, že docela s výhodou můžeme použít ambient factor pro shrnutí zbytkové radiativní energie. Bude jej jenom potřeba zprůměrovat podle ploch patchů.     Mluvil jsem taky o odrazech. No jo, ale kde vznikají ? Odrazy vzniknou při distribuci energie, kdy část přijmuté energie se určí jako radiativní (tedy se dál odrazí) a část se uloží jako světlost. Radiativní energie ale nebude prostě procento. Bude určitě záviset na odrazných vlastnostech materiálu, hlavně na jeho barvě ! Takže pokud bude pokoj s bílými stěnami plus jednou červenou, na bílých zdech bude nádech červené ! (pozor ! odrazy jsou difůzní, tzn. nejsou vidět tvary, ale jen barvy - něco jako kouřové sklo v koupelně :-))     Když si to shrneme, musíme pro výpočet osvětlení vynulovat energie všech patchů, tedy kromě světel, které budou mít nějakou radiativní a asi i nějakou světlost danou vlastní sílou světla. Postupně vyzáříme všechna světla a potom začneme hledat patche s největší radiativní energií, které hned vyzařujeme až do té doby, dokud největší radiativní energie v mapě není menší, než nějaká hranice. Potom můžeme sečíst všechny zbylé radiativní energie, zprůměrovat je a máme ambient světlost. Vzhledem k tomu že radiativní energie bude velice pravděpodobně klesat parabolicky (tzn. bude se postupně) přibližovat k nule - může to trvat dost dlouho spočítat. Pokud chceme opravdu kvalitní výsledky, budeme si muset určit hranici nízko a hodnoty světlosti nějak uložit. Tak to je třeba v jedné VRML ukázce od Heliosu :

    
helios32
Helios32 (běží interaktivně ve VRML97)

Tam byly na začátku barevný plochy, při výpočtu se spočítaly světlosti a barvy se přímo "upekly" do souboru se světem, takže prohlížeč nemá o světle ani o radiositě vůbec ponětí. Prostě zobrazuje trojúhelníky s interpolovanou barvou. (barva vertexu se spočítá prostě z průměru barev patchů, které vertex sdílejí)


... a co realtime ?

    Realtime to jde samozřejmě taky. (když už tu máme to demo RR2) V RR2 je to udělaný jednoduše. Zdrojů světla není moc a tím že se zase používá per vertex osvětlování, nemusí se dělat moc operací, aby se světlo distribuovalo. Taky je asi nastavená dost vysoko hranice pro minimální radiativní energii (zčásti proto jsou scény tak tmavé) Pokud byste se chtěli pokusit o lepší výsledky, měli byste sáhnout po nějaké robustnější technice. Pro stíny můžete použít mapy - ve stínové mapě bude pro každý zdroj světla hodnota 0 - 255, vyjadřující jak velká část světla je vidět. Pro radiativní energii můžeme použít další texturu s hodně nízkým rozlišením a iluminativní energii můžeme buď počítat realtime (pokud máme 3D kartu, tak to bude pomocí shaderů) Iluminativní energii stejně dobře můžeme spočítat do textur s trochu vyšším rozlišením, ale nemusí být nutně až tak velké, protože světlost se bude měnit pozvolna, když budou stíny uložené ve speciálních mapách. Potom dostaneme přesné stíny a možnost poměrně přesně spočítat energie, protože na to budeme mít dost času díky tomu, že máme méně patchů. Navíc můžeme použít tzv. surface cache - stínové mapy budou uložené jen pro nejbližší povrchy, zbytek se bude kreslit beze stínů (poměrně dobrá a používaná optimalizace). Povrchy se budou přepočítávat jen pokud se nějaké světlo pohne a nás nebude trápit hromada použité paměti, protože stínové mapy by měly mít rozlišení blízké rozlišení textury - až tak velké. Stejně dobře můžeme vzít okolo kamery bounding sphere a počítat jen s patchi, ležícími uvnitř. Tím zmenšíme okruh patchů k počítání - při velkých scénách (ne jako v RR2, ale třeba průměrný level v Half-Life) se tím dobře zbavíme většiny patchů. To je zhruba model, který používám já.     Další modely jsou třeba dynamické přerozdělování povrchů kudy vedou stíny - na začátku máme opravdu málo patchů a potom až podle rozdílů energií patche přerozdělujeme. Vzdálenější patche nemusíme rozdělovat vůbec, nebude to až zase tolik vidět.     Opravdu běžnou myšlenkou je updatovat osvětlení jedenkrát za n snímků, takže výpočet se rozdělí mezi ně. Tohle je dobré, pokud se světlo moc nemění. Horší by to bylo, pokud bychom měli třeba maják s paprskem - světlo by za paprskem ošklivě poskakovalo.


Jednoduchý radiosity processor

    Tak. Bude to jednoduché. Ve 3ds souboru budeme mít světla rozlišená podle jména objektů. Jméno bude "__light", to bude světlo a podobně jsou i ostatní věci. My nebudeme už svět nějak dělit na patche, to uděláme ve 3D-Maxu. Takže prakticky face = patch. Můžeme si užít i s texturami, protože máme doma pořádný mašiny, který to zvládnou i pod softwarem ;-) Nakonec jsem se rozhodl nedělat hemicube (a potom jsem to předělal do černobílejch světel, ale i tak výsledek stojí za to) Máme jednoduchou funkci, která provede n iterací radiosity :


struct Face {
    int n_id, n_used;
    int n_material;
    //
    Vertex *vertex[3];
    //
    Plane normal;
    //
    float f_rad_energy;
    float f_illumination;
};
// face má navíc radiativní a iluminativní energii ...

void Radiosity_Loop(World *p_world, int n_loops)
{
    Face *p_emitter;
    Vector v_start;
    int i, j, k, l;
    float f_form;
    int n_faces;

    while(n_loops --) {
        for(i = 0, p_emitter = 0; i < p_world->n_object_num; i ++) {
            for(j = 0; j < p_world->object[i].n_face_num; j ++) {
                if(!p_emitter || p_world->object[i].face[j].f_rad_energy >
                   p_emitter->f_rad_energy)
                    p_emitter = &p_world->object[i].face[j];
            }
        }
        // najde face s nejvetsi radiativni energii

        v_start.x = (p_emitter->vertex[0]->x +
            p_emitter->vertex[1]->x + p_emitter->vertex[2]->x) / 3;
        v_start.y = (p_emitter->vertex[0]->y +
            p_emitter->vertex[1]->y + p_emitter->vertex[2]->y) / 3;
        v_start.z = (p_emitter->vertex[0]->z +
            p_emitter->vertex[1]->z + p_emitter->vertex[2]->z) / 3;
        // stred prvniho facu

        for(i = 0; i < p_world->n_object_num; i ++) {
            for(j = 0; j < p_world->object[i].n_face_num; j ++) {
                f_form = f_FormFactor(p_world, v_start, p_emitter,
                    &p_world->object[i].face[j]);
                if(f_form > 0) {
                    p_world->object[i].face[j].f_rad_energy +=
                        p_emitter->f_rad_energy * f_form * .2f;
                    p_world->object[i].face[j].f_illumination +=
                        p_emitter->f_rad_energy * f_form * .8f;
                }
                // 90 % bude svetlost, 10 % se odrazi dal ...
            }
        }

        p_emitter->f_rad_energy = 0;
        // tohle je důležité !!
    }

    for(i = 0; i < p_world->n_object_num; i ++) {
        for(j = 0; j < p_world->object[i].n_vertex_num; j ++){
            n_faces = 0;
            // nastaví počet faců na 0

            p_world->object[i].vertex[j].i = 0;

            for(k = 0; k < p_world->object[i].n_face_num; k ++) {
                for(l = 0; l < 3; l ++) {
                    if(p_world->object[i].face[k].vertex[l] ==
                       &p_world->object[i].vertex[j]) {
                        n_faces ++;
                        p_world->object[i].vertex[j].i +=
                            p_world->object[i].face[k].f_illumination;
                    }
                }
            }
            // přičítá svetlost

            p_world->object[i].vertex[j].i /= (float)n_faces;
        }
    }
    // spočítá osvětlení per vertex
}

Je to dost jednoduché ... Na začátku to vybere face s největší energií, vyzáří ho a nastaví jeho radiativní energii na nulu. K tomu vyzáření potřebujeme znát výpočet form factoru, kterej vám hned ukážu. Dál už to jen vezme vertexy a zprůměruje to pro každý vertex světlost sousedících faců, protože světlost jako taková je zatím vypočítaná per face a my chceme per vertex, abysme ji mohli pěkně interpolovat. Tady je f_FormFactor() :


float f_FormFactor(World *p_world, Vector v_start, Face *p_emitter, Face *p_receiver)
{
    Vector v_end, v_ray;
    float f_form;

    v_end.x = (p_receiver->vertex[0]->x + p_receiver->vertex[1]->x +
        p_receiver->vertex[2]->x) / 3;
    v_end.y = (p_receiver->vertex[0]->y + p_receiver->vertex[1]->y +
        p_receiver->vertex[2]->y) / 3;
    v_end.z = (p_receiver->vertex[0]->z + p_receiver->vertex[1]->z +
        p_receiver->vertex[2]->z) / 3;
    // stred druheho facu

    if(v_end.x * p_emitter->normal.a + v_end.y * p_emitter->normal.b +
       v_end.z * p_emitter->normal.c + p_emitter->normal.d > 0.1)
        return 0.0;
    // jen přivrácené facy ...

    v_ray.x = v_end.x - v_start.x;
    v_ray.y = v_end.y - v_start.y;
    v_ray.z = v_end.z - v_start.z;
    // paprsek

    f_form = f_Visibility(p_world, v_start, v_end, v_ray);
    // zakladem je viditelnost

    f_form /= Pi * _Len(v_ray);
    // vydelime to pi * vzdalenost

    _Normalize(v_ray);
    return f_form * _PlDot(v_ray, p_emitter->normal) *
        (- _PlDot(v_ray, p_receiver->normal));
    // a zavrsime dvema skalarnimi souciny (nepocitame s diferencialni
    // plochou, budeme si tise myslet ze je plocha vsech patchu 1 ...)
}

Tohle je jen přepis té druhé rovnice ... Spočítá si to střed druhého facu (patche) a určí, jestli vůbec leží před vysílajícím facem, spočítá paprsek a zkouší, jestli je patch opravdu vidět. Potom se dopočítá dělení Pi a dělení vzdáleností (r), nakonec se to vynásobí dvěma skalárními součiny. Je důležité normalizovat paprsek a nezapomenout, že jeden skalární součin vyjde záporně (to proto, že paprsek k jednomu patchi přichází a ke druhému odchází, ale my potřebujeme kosinus úhlu, takže v obou případech musí paprsek odcházet -> násobení -1) Funkce f_Visibility() vrací naši hodnotu viditelnosti patche. Měla by to být hodnota 0 - 1, v našem případě je to buď 0, nebo 1 - protože to má běžet více méně realtime, ale pokud byste psali nějaký renderer - měli byste pustit několik paprsků a mít tím pádem desetinné číslo 0 - 1, podle toho, kolik paprsků prošlo. Funkci na chytání paprsků nebudu popisovat, protože je úplně stejná jako všude předtím.

    ... a jak to složit dohromady ? To je jednoduché - nahrajeme svět, podle jmen objektů přiřadíme energie, potom provedeme několik iterací radiosity a můžeme kreslit ... Ale jak ? Máme přece naše krásné HDR hodnoty. Co bude potřeba změnit ? No, za předpokladu světlostí 0 - 65535 nám stačí jedna hodnota pro interpolaci (při 16:16 fixed pointu), takže světlost nemusíme půlit na dvě hodnoty, jen musíme trochu jinak modulovat barvu :


n_color = (((n_color >> 16) * (i1 >> 16) > 0xff00)? 0xff0000 :
          (((n_color >> 16) * (i1 >> 16)) >> 8) << 16) |
          (((n_color & 0x00ff00) * (i1 >> 16) > 0x00ff0000)? 0x00ff00 :
          (((n_color & 0x00ff00) * (i1 >> 16)) >> 8) & 0x00ff00) |
          (((n_color & 0x0000ff) * (i1 >> 16) > 0x0000ff00)? 0x0000ff :
          (((n_color & 0x0000ff) * (i1 >> 16)) >> 8) & 0x0000ff);

Kde n_color je transformovaná barva a i1 je 16:16 FP světlost. Pokaždé ji posuneme o 16 doprava, což by se, mimochodem, nemělo - protže ztrácíme přesnost, ale přesnější výpočet by trval trochu déle ... Pokaždé vezmeme barvu, zkusíme, jestli nepřetekla, pokud ano tak tam dáme maximum a pokud ne, spočítáme skutečnou barvu. Můžete říct, že se tam opakují výpočty - a já řeknu, že každý optimalizér by se s tímhle měl vypořádat.

    Tak, to by mělo být všechno, co potřebujete vědět, nakonec to nebylo tak složité. Uvidíte, že výsledek je celkem pomalý, je to způsobeno pomalým vrháním paprsků a tím, že kód je celkově neoptimalizovaný, aby byl dobře pochopitelný. Nedával jsem tam ambient factor, protože je krásné dívat se, jak se scéna postupně osvětluje a konverguje k výsledku ... romantika ;-)     Při prohlížení xample přijdete na to, že to je vlastně modifikovaný smooth engine, takže jedinné nové je ta radiosita. V programu můžete zapnout a vypnout HDR (vypnutí = oříznout hodnoty světlosti na 256, takže to není rychlejší, ale je vidět ten rozdíl) a zapnout vypnout textury. Všimněte si, že pokud máte textury, není ani moc vidět takové ty "kolečka", vzniklé při přechodu barvy, protože je zakryje šum textury - takže přesnost transformace barvy je dostatečná ...

textured

hdr

clamped
Tady neni HDR

 Radiosity example !!

    ... ty textury se nakonec ukázaly jako špatně zvolený, protože jsou dost tmavý ... ale i tak to není špatný. Ovládá se to pomocí "L" jako loop - zastavuje / pokračuje počítání radiosity, "T" - vypíná a zapíná textury, "H" - vypíná a zapíná HDR. Bavte se !


... A to je všechno ?

    Ne, to zdaleka není všechno. Ono ... k tomu HDR - ono se obraz vyrenderuje do float bufferu (pro r, g a b složku je float hodnota), nebo do dvou bufferů, z nichž jeden tvoří spodních 8 bitů a druhý vrchních 8 bitů barevných složek. Buffery se potom zprůměrují a tím se spočítá průměrná světlost. Podle ní se potom upraví kontrast a jas a výsledný obraz se ořízne do normálního 32bpp bufferu.
    Občas se ještě dělá to, že se vezme kopie obrazu, kde zůstanou jen hodnoty RGB přesahují určitou mez. Tenhle buffer se potom prožene nějakým blurem a přičte se k obrazu. Vznikne potom krásný efekt, kdy světlé věci září i do okolí ... Asi si to potom zkusíme s OpenGL ...
    HDR používá po zpatchování třeba taky Far Cry (obrázek), a když už jsem u Far Cry, měl bych připomenout že při počítání průměrné světlosti by se potom měly zachovat také nějaké meze, jinak dopadnete jako FarCry, kde malá žárovička v tmavé místnosti vypadá jako sluníčko :o)

HDR far cry
s HDR

HDR far cry
a bez HDR



Konec ..? To zrovna !

    Tak, a ještě nekončíme ! při překládání jsem se rozhodl, že to nemůžu takhle nechat. Takže jsem napsal ještě jeden example, kterej počítá s hemicube, umí i něco-jako-HDR (nakonec jsem už nepsal exposure control - to s tím počítáním průměrné světlosti a kontrolou světlosti celého obrazu), ale výsledek i tak rozhodně stojí za to. Napřed se podíváme, jak takovou hemicube spočítat:


static const int n_hemicube_size = 256;

static unsigned __int32 p_s_hemicube_buffer[n_hemicube_size * n_hemicube_size];
static unsigned __int32 p_s_hemicube_buffer_side[4][n_hemicube_size *
                                                    n_hemicube_size / 2];
static float p_hemicube_formfactor[n_hemicube_size * n_hemicube_size];
static float p_hemicube_formfactor_side[n_hemicube_size * n_hemicube_size / 2];

static float f_hemicube_max = 1;

void Calc_HemicubeFormFactors()
{
    float f_half_pixel_width;
    float f_pixel_area;
    float dx, dy;
    int x, y;
    float f;

    f_half_pixel_width = (1.0f / n_hemicube_size);
    //
    f_pixel_area = (2.0f / n_hemicube_size);
    f_pixel_area *= f_pixel_area;
    // 2 je velikost hemicube

    for(x = 0; x < n_hemicube_size; x ++) {
        for (y = 0; y < n_hemicube_size; y ++) {
            dx = ((x - n_hemicube_size / 2) / (n_hemicube_size / 2.0f)) +
                f_half_pixel_width;
            dy = ((y - n_hemicube_size / 2) / (n_hemicube_size / 2.0f)) +
                f_half_pixel_width;

            f = (dx * dx + dy * dy + 1);
            f *= f * Pi;

            p_hemicube_formfactor[x + y * n_hemicube_size] = f_pixel_area / f;
        }
    }
    // vrchni ...

    for(x = 0; x < n_hemicube_size; x ++) {
        for(y = 0; y < n_hemicube_size / 2; y ++) {
            dx = (x - n_hemicube_size / 2) / (n_hemicube_size / 2.0f) +
                f_half_pixel_width;
            dy = (n_hemicube_size / 2 - 1 - y) / (n_hemicube_size / 2.0f) +
                f_half_pixel_width;

            f = (dx * dx + dy * dy + 1);
            f *= f * Pi;

            p_hemicube_formfactor_side[x + y * n_hemicube_size] =
                (f_pixel_area * (dy + f_half_pixel_width)) / f;
        }
    }
    // strana

    for(x = 0, f_hemicube_max = 0; x < n_hemicube_size; x ++) {
        for(y = 0; y < n_hemicube_size / 2; y ++) {
            if(f_hemicube_max < p_hemicube_formfactor_side[x + y * n_hemicube_size])
                f_hemicube_max = p_hemicube_formfactor_side[x + y * n_hemicube_size];
        }
    }
    //
    for(x = 0; x < n_hemicube_size; x ++) {
        for(y = 0; y < n_hemicube_size; y ++) {
            if(f_hemicube_max < p_hemicube_formfactor[x + y * n_hemicube_size])
                f_hemicube_max = p_hemicube_formfactor[x + y * n_hemicube_size];
        }
    }
    // hledá největší položku ...

#define __SUM_TEST
#ifdef __SUM_TEST
        float f_sum;

        for(y = 0, f_sum = 0; y < n_hemicube_size; y ++) {
            for(x = 0; x < n_hemicube_size / 2; x ++)
                f_sum += p_hemicube_formfactor_side[y + x * n_hemicube_size];
        }
        // strana

        f_sum *= 4;
        // strany jsou 4

        for(x = 0; x < n_hemicube_size; x ++) {
            for(y = 0; y < n_hemicube_size; y ++)
                f_sum += p_hemicube_formfactor[x + y * n_hemicube_size];
        }
        // vrch

        // tady musi byt f_sum == 1, mě to vyjde 1.00435f
#endif
}

    Teď abych to trochu vysvětlil ... Napřed tam máme nějké globální proměnné. První konstanta je rozlišení hemicube. Potom tam méme nějaká pole. p_s_hemicube_buffer a p_s_hemicube_buffer_side jsou buffery, do kterých budeme kreslit to, co hemicube vidí. (_side je čtyřikrát, protože budeme chtít kreslit hemicube na obrazovku, takže ji chceme uchovat celou) Potom tam jsou p_hemicube_formfactor a p_hemicube_formfactor_side - form-factory jednotlivých pixelů v hemicube. (tentokrát nám _side stačí jedna, protože bude pro všechny čtyři strany stejná. Pro lidi kdo nemluví anglicky bych měl poznamenat že "side" znamená "strana". Teď se můžeme podívat na funkci, jež bude počítat buffery, obsahující form-factory - Calc_HemicubeFormFactors():
    Na začátku spočítáme f_half_pixel_width - polovinu velikosti (šířky) pixelu v hemicube. Velikost hemicube (ve worldspace) je 1 do hloubky a 2 na šířku a výšku. (jsou to jakoby čtyři jednotkové krychle, narovnané okolo normály) Potom ještě spočítáme f_pixel_area - plochu pixelu a můžeme se vrhnout na počítání form-factorů přední stěny hemicube. V cyklu pro každý pixel spočítáme vzdálenost od středu hemicube (v pixelech), převedeme na vzdálenost ve worldspace, posuneme ji tak, aby platila pro střed texelu (přičtením poloviny jeho velikosti) a spočítáme výsledný form-factor jako:

      f_pixel_area / (Pi * (dx2 + dy2 + 1)2)

Obdobně postupujeme i u boku hemicube, jen musíme započítat ještě vzdálenost od začátku. Výsledkem je hezky vypadající hemicube, kterou můžete pomocí funkce p_Hemicube_Test() převést na bitmapu. Výsledek by měl vypadat nějak takhle:

hemicube

(tohle je trochu upravené, z programu vám vypadne jen prostřední díl a jedna strana pod ním)

    Teď, když už máme hemicube, měli bysme si ukázat, jak ji máme vykreslit. Budeme znát střed patche (texelu lightmapy) a normálu jeho povrchu. To nám stačí ke konstrukci kamery, koukající dopředu a jak uvidíte, tak i těch čtyř bočních. Podíváme se na funkci:


void Render_Hemicube(Vector v_pos, Vector v_normal, Mesh *p_svet, Vector v_radiative)
{
    Vector v_up, v_right;
    Matrix m_camera;
    int i, j;

    Matrix_Init(&m_camera);

    if(fabs(v_normal.x) < .01 &&
       fabs(v_normal.y) - 1 < .01 && fabs(v_normal.z) < .01) {
        v_up.x = 0; 
        v_up.y = 0; 
        v_up.z = 1;
    } else {
        v_up.x = 0; 
        v_up.y = 1; 
        v_up.z = 0;
    }
    // najde smysluplný vektor nahoru ...

    _Normalize(v_normal);
    _Cross(v_up, v_normal, v_right);
    _Cross(v_right, v_normal, v_up);
    _Normalize(v_right);
    _Normalize(v_up);

    Matrix_Init(&m_camera);
    //
    m_camera[0][0] = v_right.x;
    m_camera[0][1] = v_right.y;
    m_camera[0][2] = v_right.z;
    m_camera[1][0] = v_up.x;
    m_camera[1][1] = v_up.y;
    m_camera[1][2] = v_up.z;
    m_camera[2][0] = v_normal.x;
    m_camera[2][1] = v_normal.y;
    m_camera[2][2] = v_normal.z;
    m_camera[3][0] = v_pos.x;
    m_camera[3][1] = v_pos.y;
    m_camera[3][2] = v_pos.z;
    // kamera, která se dívá dopředu

    Set_FormFactors(p_hemicube_formfactor, v_radiative);

    SubWindow(n_hemicube_size, n_hemicube_size);
    Clipper(n_hemicube_size, n_hemicube_size);
    Init_Screen();
    for(i = 0; i < p_svet->n_object_num; i ++) {
        DrawObject(&p_svet->p_object[i].m_object, &m_camera,
            &p_svet->p_object[i], p_svet->p_material, p_svet->p_lightmap);
    }
    Release_Screen_LinearBuff(p_s_hemicube_buffer);
    // vykreslí pohled patche do přední roviny ...

    Matrix_Rotate_X(&m_camera, -Pi / 2);
    // otočí kameru tak, aby byla rovnoběžně s rovinou ...

    Set_FormFactors(p_hemicube_formfactor_side, v_radiative);

    SubWindow(n_hemicube_size, n_hemicube_size);
    Clipper(n_hemicube_size, n_hemicube_size / 2);
    for(i = 0; i < 4; i ++) {
        Init_Screen();
        for(j = 0; j < p_svet->n_object_num; j ++) {
            DrawObject(&p_svet->p_object[j].m_object, &m_camera,
                &p_svet->p_object[j], p_svet->p_material, p_svet->p_lightmap);
        }
        Release_Screen_LinearBuff(p_s_hemicube_buffer_side[i]);
        // vykreslí pohled patche do boční roviny ...

        Matrix_Rotate_Y(&m_camera, -Pi / 2);
        // otočí kameru k další rovině ...
    }
}

    Tak. Funkce dostane parametry v_pos (vektor pozice středu patche), v_normal (normálu), p_svet (geometrii světa) a v_radiative (radiativní barvu, je to normálně RGB, ale uložené jako floaty s tím, že není limitovaná do mezí <0, 1>) Na začátku se vytvoří kamera tak, jak už jsme to mockrát dělali, jen se klade důraz na to, aby vektor "nahoru" nebyl stejný s normálou, což by způsobilo výpočet vektoru, který není kolmý na normálu (a kamera by se dívala jinam, tím pádem by se osvětlilo něco co nemá ...) Potom se voláním Set_FormFactors() nastaví v rasterizéru příslušná tabulka s form-faktory a radiativní barva. Potom se volají další dvě nové funkce rasterizéru, a to SubWindow(), jež nastaví kreslení jen podokna v obraze (hemicube bude mít menší rozlišení, než obraz) a Clipper(), jež nastaví oříznutí tohoto okna (to je důležité, protože když volám SubWindow(), změní se prostě rozlišení a střed obrazu bude zároveň bodem, kde je kamera, ale když volám Clipper(), prostě se obraz jen ořízne - to je užitečné když krelíme boční hrany, kdy potřebujeme aby kamera ležela na hraně obrazu) Potom vykreslíme celý svět jako vždycky a nakonec zavoláme funkci Release_Screen() Tím vykreslíme přední stěnu hemicube. Potom kameru otočíme o 90 stupňů po ose x, takže teď její osa pohledu leží na rovině patche. Nastavíme ořezávání okna na poloviční výšku, takže se bude kreslit jen to, co je nad rovinou patche (tedy to, co nás zajímá) a znovu kreslíme. Po vykreslení každé stěny rotujeme kameru o 90 stupňů po ose y, čímž zajistíme její otočení na další bok hemicube. Pro renderování bočních stěn nesmíme zapomenout nastavit odpovídající form-faktory. Tohle by mělo být jasné. Jak to zkontrolujeme ? Jednoduše můžeme zkopírovat obdélníky jednotlivých stěn na obrazovku, jako tady:

hemicube contents

    Existuje ale i jiný způsob - hemicube můžeme promítnout na polokouli. Je to vcelku jednoduchá transformace. Představte si, že máte polokouli, jež leží na nějaké rovné ploše. Každý bod polokoule odpovídá vektoru z jejího středu do toho bodu. Když polokouli něčím zmáčknete tak, že je z ní kruh, ležící na povrchu, pořád můžeme vidět její vektory z jejího středu. Není složité je vygenerovat. Prostě vezmete kde bude její střed v rastru a potom pro každý pixel zjišťujete vzdálenost od tohoto středu. Tím získáte x a y souřadnice vektoru. Pokud tyto souřadnice vydělíte poloměrem koule (spíš kruhu) v rastru, získáme x a y souřadnice normalizovaného vektoru. Potom už není těžké určit z, pokud víme že pro jednotkový vektor platí tento vztah:

      x2 + y2 + z2 = 1

(Není potřeba vkládat odmocninu, protože každý ví že odmocnina z jedné je jedna. Takže výsledné z bude jednoduše:

      z2 = 1 - x2 - y2
      z = sqrt(1 - x2 - y2)

Teď už můžeme vzít náš vektor, zjistit která jeho část je největší, případně její znaménko a víme na kterou stěnu hemicube vektor ukazuje. Potom jen musíme z vektoru v prostoru zjistit souřadnice v rastru a můžeme vykreslit příslušný pixel. Tu transformaci známe už dlouho a to je prespektivní korekce. Konstantu perspektivního zkreslení necháme 1, jen nesmíme zapomenout že vektor má délku 1 (tzn. je potřeba ho vynásobit velikostí rastru) a jeho počátek je uprostřed (tzn. je třeba k němu přičíst 0.5 - a nenásobit ho velikostí rastru, ale jen její polovinou) Kód vypadá takhle:


void Copy_HemicubeToScreen2(unsigned __int32 *p_buffer, int n_buffer_w)
{
    Vector v;
    int w, h;
    int x, y;
    float f;

    //Modulate_HemicubeBuffers();

    w = (int)(2 * n_hemicube_size);
    h = w;

    for(y = 0; y < h; y ++) {
        for(x = 0; x < w; x ++) {
            v.x = -(w - x * 2) / (float)w;
            v.y = -(h - y * 2) / (float)h;
            if((f = v.x * v.x + v.y * v.y) > 1)
                continue;
            v.z = (float)sqrt(1 - f);
            // sestrojí vektor, koukající do hemicube ...

            if(v.z > fabs(v.x) && v.z > fabs(v.y)) {
                p_buffer[x + y * n_buffer_w] =
                    p_s_hemicube_buffer[
                        (int)((v.x / v.z * .5f + .5f) *
                        n_hemicube_size) +
                        (int)((v.y / v.z * .5f + .5f) *
                        n_hemicube_size) * n_hemicube_size];
                // největší je z - přední strana
            } else if(fabs(v.x) > fabs(v.y)) {
                if(v.x < 0) {
                    p_buffer[x + y * n_buffer_w] =
                        p_s_hemicube_buffer_side[1]
                        [(int)((-v.y / v.x * .5f + .5f) *
                         (n_hemicube_size - 1)) +
                         (int)((v.z / v.x * .5f + .5f) *
                         (n_hemicube_size - 1)) * n_hemicube_size];
                    // 1
                } else {
                    p_buffer[x + y * n_buffer_w] =
                        p_s_hemicube_buffer_side[3]
                        [(int)((-v.y / v.x * .5f + .5f) *
                         (n_hemicube_size - 1)) +
                         (int)((-v.z / v.x * .5f + .5f) *
                         (n_hemicube_size - 1)) * n_hemicube_size];
                    // 3
                }
                // největší je x - strana 1 nebo 3
            } else {
                if(v.y < 0) {
                    p_buffer[x + y * n_buffer_w] =
                        p_s_hemicube_buffer_side[2]
                        [(int)((v.x / v.y * .5f + .5f) *
                         (n_hemicube_size - 1)) +
                         (int)((v.z / v.y * .5f + .5f) *
                         (n_hemicube_size - 1)) * n_hemicube_size];
                    // 2
                } else {
                    p_buffer[x + y * n_buffer_w] =
                        p_s_hemicube_buffer_side[0]
                        [(int)((v.x / v.y * .5f + .5f) *
                         (n_hemicube_size - 1)) +
                         (int)((-v.z / v.y * .5f + .5f) *
                         (n_hemicube_size - 1)) * n_hemicube_size];
                    // 0
                }
                // největší je y - strana 0 nebo 2
            }
        }
    }
}

Pokud smažete komentář před Modulate_HemicubeBuffers();, uvidíte obraz hemicube navíc pronásobený form-faktory. Já jsem to nechal takhle, protože mi šlo o rychlost. Výsledek vypadá takhle hezky: (je to pohled z jiného patche, než minule !)

hemicube contents

    Teď vás ale asi zajímá, jak bude vypadat funkce, která "střílí" energii. Normálně by se to řešilo kreslením patch-id bufferu a potom by se procházel každý jeho pixel a k odpovídajícímu patchi by se za každý pixel přičetla odpovídající energie. My ale máme lightmapy, takže je trochu obtížné vytvořit id pixelu. Pokud vezmeme maximální rozlišení stránky 512x512, tak bychom měli id, skládající se z dvakrát devíti (pro x a y souřadnici texelu, 512 = 29), což je osmnáct a zbývá nám 14 (32 - 18) bitů na identifikaci lightmapy, což zaručuje 16k lightmap. (16k = 16384) My ale využijeme výhody s-bufferu, jíž je nulové překreslování a budeme energii střílet přímo při kreslení obrazu pro hemicube. K tomu trochu upravíme formát lightmap, kde bude pro každý texel šest hodnot typu float, první tři jsou RGB iluminativní energie a další tři jsou RGB radiativní energie. K tomu potřebujeme upravit funkci, která lightmapy alokuje:


TLightmapTex *p_Lightmap(int n_width, int n_height)
{
    TLightmapTex *p_lightmap;

    if(!(p_lightmap = (TLightmapTex*)malloc(sizeof(TLightmapTex))))
        return 0;
    // naalokuje data pro lightmapu ...

    p_lightmap->n_width = n_width;
    p_lightmap->n_height = n_height;
    // nastaví rozměry lightmapy ...

    if(!(p_lightmap->p_frontline = (int*)malloc(n_width * sizeof(int))))
        return 0;
    memset(p_lightmap->p_frontline, 0, n_width * sizeof(int));
    // naalokuje a vynuluje data pro vkládání dílků ...

    if(!(p_lightmap->p_bytes = (float*)malloc(n_width *
       n_height * sizeof(float) * 6)))
        return 0;
    memset(p_lightmap->p_bytes, 0, n_width * n_height * sizeof(float) * 6);
    // naalokuje buffer pro texely a vymaže ho ...

    return p_lightmap;
}

Tohle je stejná funkce, jako byla minule v lightmapovací utilitě. Jedinná změna je, že alokuje 6 floatů místo jednoho __int32. Další změna je ve funkci pro vytváření obsahu lightmapy:


void Raytrace_Tile(Group *p_group, Mapping_Info *p_mapping_info, World *p_world,
                   int n_x, int n_y, int n_h, int n_w, int n_p_w,
                   float *p_buffer)
{
    float f_radiative[3];
    Vector v_texel_pos;
    Vector v_pos_v;
    int b_contain;
    //float r, g, b;
    //Vector v_ray;
    int x, y, i;
    //float a;
    //int j;

    for(y = 0; y < n_h; y ++) {
        v_pos_v = p_mapping_info->v_org;
        v_pos_v.x += p_mapping_info->v_v.x * (float)y /
            (float)n_h * p_mapping_info->f_height;
        v_pos_v.y += p_mapping_info->v_v.y * (float)y /
            (float)n_h * p_mapping_info->f_height;
        v_pos_v.z += p_mapping_info->v_v.z * (float)y /
            (float)n_h * p_mapping_info->f_height;
        //
        for(x = 0; x < n_w; x ++) {
            v_texel_pos.x = p_mapping_info->v_u.x * (float)x /
                (float)n_w * p_mapping_info->f_width + v_pos_v.x;
            v_texel_pos.y = p_mapping_info->v_u.y * (float)x /
                (float)n_w * p_mapping_info->f_width + v_pos_v.y;
            v_texel_pos.z = p_mapping_info->v_u.z * (float)x /
                (float)n_w * p_mapping_info->f_width + v_pos_v.z;
            // spočítá pozici texelu lightmapy ve worldspace

            switch(p_mapping_info->n_plane) {
            case Plane_xy:
                v_texel_pos.z = -(v_texel_pos.x * p_group->p_face[0]->normal.a +
                                  v_texel_pos.y * p_group->p_face[0]->normal.b +
                                  p_group->p_face[0]->normal.d) /
                                  p_group->p_face[0]->normal.c;
                break;
            case Plane_xz:
                v_texel_pos.y = -(v_texel_pos.x * p_group->p_face[0]->normal.a +
                                  v_texel_pos.z * p_group->p_face[0]->normal.c +
                                  p_group->p_face[0]->normal.d) /
                                  p_group->p_face[0]->normal.b;
                break;
            case Plane_yz:
                v_texel_pos.x = -(v_texel_pos.y * p_group->p_face[0]->normal.b +
                                  v_texel_pos.z * p_group->p_face[0]->normal.c +
                                  p_group->p_face[0]->normal.d) /
                                  p_group->p_face[0]->normal.a;
                break;
            }
            // promítne pozici texelu z našeho
            // obdélníku do roviny, ve které leží facy ...

            f_radiative[0] = 0;
            f_radiative[1] = 0;
            f_radiative[2] = 0;

            b_contain = false;
            for(i = 0; i < p_group->n_face_used; i ++) {
                if(b_Contain_Point(v_texel_pos, p_group->p_face[i])) {
                    if(p_group->p_face[i]->f_radiative_r > 0.01 ||
                       p_group->p_face[i]->f_radiative_g > 0.01 ||
                       p_group->p_face[i]->f_radiative_b > 0.01) {
                        f_radiative[0] = p_group->p_face[i]->f_radiative_r;
                        f_radiative[1] = p_group->p_face[i]->f_radiative_g;
                        f_radiative[2] = p_group->p_face[i]->f_radiative_b;
                    }
                    b_contain = true;
                    break;
                }
            }
            // zkontroluje, jestli face obsahuje bod ..

            p_buffer[ ((x + n_x) + (y + n_y) * n_p_w) * 6     ] = f_radiative[0];
            p_buffer[(((x + n_x) + (y + n_y) * n_p_w) * 6) + 1] = f_radiative[1];
            p_buffer[(((x + n_x) + (y + n_y) * n_p_w) * 6) + 2] = f_radiative[2];
            // HDR illumination color

            p_buffer[(((x + n_x) + (y + n_y) * n_p_w) * 6) + 3] = f_radiative[0];
            p_buffer[(((x + n_x) + (y + n_y) * n_p_w) * 6) + 4] = f_radiative[1];
            p_buffer[(((x + n_x) + (y + n_y) * n_p_w) * 6) + 5] = f_radiative[2];
            // HDR radiative color

            // převede barvu do RGB a uloží do příslušného dílku v textuře ...
        }
    }
}

Tady taky není moc nového, jen se zjišťuje, jestli příslušný texel leží na nějakém face a pokud ten face má nějakou radiativní energii, přiřadí se ta radiativní energie tomu texelu. (ta se zkopíruje i do iluminativní, aby světla nezůstala černá ...)
    Teď už si jen ukážeme, jak vykreslíme segment s takovým formátem lightmapy. Je to mírně upravená funkce z minulého engine. Vypadá takhle:


static void _fastcall Interpolate_Segment_Bilerp(int u1, int du, int v1, int dv,
    int lu1, int ldu, int lv1, int ldv,
    unsigned __int32 *video, unsigned __int32 *v2, int x, int y)
{
    register int frac, tex, col;

    y = (y & 3) << 2;
    do {
        tex = ((u1 >> 16) & A_1) + ((v1 >> B_1) & A_2);
        // adresa texelu

        frac = ((u1 >> 10) & 0x3f) + ((v1 >> 4) & 0xfc0);
        // desetinna cast adresy (6 bitu z V + 6 bitu z U)

        col = _palette[(_texture[tex] << 6) + multab[0][frac]] +
              _palette[(_texture[(tex + 1) & A_3] << 6) + multab[1][frac]] +
              _palette[(_texture[(tex + W_0) & A_3] << 6) + multab[2][frac]] +
              _palette[(_texture[(tex + W_1) & A_3] << 6) + multab[3][frac]];
        // bilinearni interpolace

        tex = (((lu1 + dither_u[frac = (y | (x & 3))]) >> 16) & LA_1) +
              (((lv1 + dither_v[frac]) >> LB_1) & LA_2);
        tex = (tex << 1) + (tex << 2);

        int b = (int)(((col & 0x0000ff) * _lightmap[tex + 2]) / 256);
        b = (b > 0x0000ff)? 0x0000ff : b & 0x0000ff;
        col >>= 8;
        int r = (col & 0x00ff00);
        r = ((r * _lightmap[tex]) > 0xff0000)? 0xff0000 :
            (int)(r * _lightmap[tex]) & 0xff0000;
        int g = (col & 0x0000ff);
        g = ((g * _lightmap[tex + 1]) > 0x00ff00)? 0x00ff00 :
            (int)(g * _lightmap[tex + 1]) & 0x00ff00;

        *video ++ = r | g | b;
        // osvětlí ...

        x ++;
        // nova pozice

        u1 += du;
        v1 += dv;
        lu1 += ldu;
        lv1 += ldv;
        // interpolace souradnic
    } while(video < v2);
}

Tady normálně určíme barvu texelu z textury, potom určíme index texelu v lightmapě, vynásobíme ho šesti (násobení šesti je rychlejší jako součet násobení dvěma a čtyřmi pomocí bitového posuvu) A potom přičítáním k indexu získáme svoje iluminativní RGB. Musíme modulovat barvu textury a zapíšeme ho do videopaměti. Textura je bilineárně filtrovaná, lightmapy jsou ditherované.
    Nakonec vám ukážu ještě funkci, jež kreslí segmenty hemicube a zároveň při tom "střílí" energii:


static float *p_cur_form_factors;
// pointer na odpovídající form-faktory hemicube

static float f_radiative_r;
static float f_radiative_g;
static float f_radiative_b;
// radiativní energie


static void _fastcall Interpolate_Segment_Radiate(int u1, int du, int v1, int dv,
    int lu1, int ldu, int lv1, int ldv,
    unsigned __int32 *video, unsigned __int32 *v2, int x, int y)
{
    register int frac, tex, col;

    do {
        tex = ((u1 >> 16) & A_1) + ((v1 >> B_1) & A_2);
        // adresa texelu

        frac = ((u1 >> 10) & 0x3f) + ((v1 >> 4) & 0xfc0);
        // desetinna cast adresy (6 bitu z V + 6 bitu z U)

        col = _palette[(_texture[tex] << 6) + multab[0][frac]] +
              _palette[(_texture[(tex + 1) & A_3] << 6) + multab[1][frac]] +
              _palette[(_texture[(tex + W_0) & A_3] << 6) + multab[2][frac]] +
              _palette[(_texture[(tex + W_1) & A_3] << 6) + multab[3][frac]];
        // bilinearni interpolace

        tex = (((lu1 + dither_u[frac = (((y & 3) << 2) | (x & 3))]) >> 16) & LA_1) +
              (((lv1 + dither_v[frac]) >> LB_1) & LA_2);
        tex = (tex << 1) + (tex << 2);
        // určí index texelu v lightmapě

        float f_form_factor = p_cur_form_factors[x + y * n_Width];

        _lightmap[tex + 3] += f_radiative_r * f_form_factor * __reflectance__ *
            ((float)((col >> 16) & 0xff) / 256 + (__bleeding__ - 1)) / __bleeding__;
        _lightmap[tex + 4] += f_radiative_g * f_form_factor * __reflectance__ *
            ((float)((col >> 8) & 0xff) / 256 + (__bleeding__ - 1)) / __bleeding__;
        _lightmap[tex + 5] += f_radiative_b * f_form_factor * __reflectance__ *
            ((float)(col & 0xff) / 256 + (__bleeding__ - 1)) / __bleeding__;
        //
        _lightmap[tex    ] += f_radiative_r * f_form_factor * (1 - __reflectance__);
        _lightmap[tex + 1] += f_radiative_g * f_form_factor * (1 - __reflectance__);
        _lightmap[tex + 2] += f_radiative_b * f_form_factor * (1 - __reflectance__);
        // radiate (pocitam s tim ze nikdy nepretece)

        int b = (int)(((col & 0x0000ff) * _lightmap[tex + 2]) / 256);
        b = (b > 0x0000ff)? 0x0000ff : b & 0x0000ff;
        col >>= 8;
        int r = (col & 0x00ff00);
        r = ((r * _lightmap[tex]) > 0xff0000)? 0xff0000 :
            (int)(r * _lightmap[tex]) & 0xff0000;
        int g = (col & 0x0000ff);
        g = ((g * _lightmap[tex + 1]) > 0x00ff00)? 0x00ff00 :
            (int)(g * _lightmap[tex + 1]) & 0x00ff00;

        *video ++ = r | g | b;
        // osvětlí ...

        x ++;
        // nova pozice

        u1 += du;
        v1 += dv;
        lu1 += ldu;
        lv1 += ldv;
        // interpolace souradnic
    } while(video < v2);
}

Tady zase napřed určíme difůzní barvu pixelu, potom určíme index texelu v lightmapě, potom přečteme odpovídající form-faktor (... a najednou se nám hodí x a y, používané pro dithering) a přičteme odpovídající energii k iluminativní energii a k radiativní energii. Když přičítáme k radiativní energii, smícháme přijatou barvu s barvou textury, takže máme i color bleeding. Potom normálně vykreslíme osvětlený otexturovaný segment, takže můžeme zkopírovat obsah hemicube na obrazovku.
    Teď už byste pomalu měli pochopit jak example funguje. Už jen načrtnu postup kroků při inicializaci programu:
  • vytvoříme okno, inicializuje se direct-x atd ...
  • voláme Calc_HemicubeFormFactors(); - spočítáme form-faktory hemicube
  • nahrajeme svět a dáme na něj lightmapy Create_Lightmaps("sshock_room_1obj.3ds", &t_svet)
    (tady se přiřadí facům jejich radiativní energie, barva je podle barvy nejbližšího světla)
  • nahrajou se textury


  • kreslící smyčka
    • najdeme texel s největší radiativní energií (+ jeho worldspace pozici a normálu)
    • vykreslíme hemicube (a zároveň vyzáříme radiativní energii patche)
    • zkopírujeme jednopixelové rámečky okolo lightmap
    • kreslíme obraz
    • kreslíme obraz s menší světlostí, rozmažeme ho a přidáme k původnímu obrazu
    • zobrazíme výsledek
    Ještě by vám vlastně nemusela být jasná funkce pro hledání patche (texelu) s největší radiativní energií:


float f_Find_GreatestRadiativeEnergy(Mesh *p_mesh, Vector *v_texel_pos,
    Vector *v_texel_normal, int *n, Vector *v_rad_color)
{
    float f_max_energy, f_energy;
    int n_max_j, n_max_page;
    Group *p_group;
    int x, y;
    int i, j;

    while(1) {
        f_max_energy = -1;
        n_max_j = -1;
        n_max_page = -1;
        for(i = 0; i < p_mesh->n_lightmap_num; i ++) {
            for(j = 0; j < p_mesh->p_lightmap[i].n_width *
               p_mesh->p_lightmap[i].n_height; j ++) {
                if((f_energy = f_Energy(
                   p_mesh->p_lightmap[i].p_buffer[(j * 6) + 3],
                   p_mesh->p_lightmap[i].p_buffer[(j * 6) + 4],
                   p_mesh->p_lightmap[i].p_buffer[(j * 6) + 5])) > f_max_energy) {
                    f_max_energy = f_energy;
                    n_max_j = j;
                    n_max_page = i;
                }
            }
        }
        // najde maximální radiativní energii v nějaké lightmapě ...

        if(n_max_j == -1)
            return -1;

        for(i = 0; i < p_mesh->p_g_data->n_group_num; i ++) {
            if(p_mesh->p_g_data->p_group[i].t_rect.n_page != n_max_page)
                continue;

            x = n_max_j % p_mesh->p_lightmap[n_max_page].n_width;
            y = n_max_j / p_mesh->p_lightmap[n_max_page].n_width;
            // souřadnice pixelu ...

            p_group = &p_mesh->p_g_data->p_group[i];
            //
            x -= p_group->t_rect.x;
            y -= p_group->t_rect.y;
            //
            if(x >= 0 && x <= p_group->t_rect.w &&
               y >= 0 && y <= p_group->t_rect.h) {
                // našli jsme group, do kterého pixel patří ...

                v_texel_pos->x = p_mesh->p_m_info[i].v_org.x +

                    p_mesh->p_m_info[i].v_v.x * (float)(y + .5f) /
                    (float)p_group->t_rect.h *
                    p_mesh->p_m_info[i].f_height +

                    p_mesh->p_m_info[i].v_u.x * (float)(x + .5f) /
                    (float)p_group->t_rect.w *
                    p_mesh->p_m_info[i].f_width;
                //
                v_texel_pos->y = p_mesh->p_m_info[i].v_org.y +

                    p_mesh->p_m_info[i].v_v.y * (float)(y + .5f) /
                    (float)p_group->t_rect.h *
                    p_mesh->p_m_info[i].f_height +

                    p_mesh->p_m_info[i].v_u.y * (float)(x + .5f) /
                    (float)p_group->t_rect.w *
                    p_mesh->p_m_info[i].f_width;
                //
                v_texel_pos->z = p_mesh->p_m_info[i].v_org.z +

                    p_mesh->p_m_info[i].v_v.z * (float)(y + .5f) /
                    (float)p_group->t_rect.h *
                    p_mesh->p_m_info[i].f_height +

                    p_mesh->p_m_info[i].v_u.z * (float)(x + .5f) /
                    (float)p_group->t_rect.w *
                    p_mesh->p_m_info[i].f_width;
                //
                switch(p_mesh->p_m_info[i].n_plane) {
                case Plane_xy:
                    v_texel_pos->z =
                        -(v_texel_pos->x * p_group->p_face[0]->normal.a +
                        v_texel_pos->y * p_group->p_face[0]->normal.b +
                        p_group->p_face[0]->normal.d) /
                        p_group->p_face[0]->normal.c;
                    break;
                case Plane_xz:
                    v_texel_pos->y =
                        -(v_texel_pos->x * p_group->p_face[0]->normal.a +
                        v_texel_pos->z * p_group->p_face[0]->normal.c +
                        p_group->p_face[0]->normal.d) /
                        p_group->p_face[0]->normal.b;
                    break;
                case Plane_yz:
                    v_texel_pos->x =
                        -(v_texel_pos->y * p_group->p_face[0]->normal.b +
                        v_texel_pos->z * p_group->p_face[0]->normal.c +
                        p_group->p_face[0]->normal.d) /
                        p_group->p_face[0]->normal.a;
                    break;
                }
                // spočítá pozici texelu ve světě ...

                *n = p_group->p_face[0]->n_id;
                // id facu

                v_texel_normal->x = p_group->p_face[0]->normal.a;
                v_texel_normal->y = p_group->p_face[0]->normal.b;
                v_texel_normal->z = p_group->p_face[0]->normal.c;
                // normála texelu

                *v_rad_color = v_Energy(
                    p_mesh->p_lightmap[n_max_page].p_buffer[(n_max_j * 6) + 3],
                    p_mesh->p_lightmap[n_max_page].p_buffer[(n_max_j * 6) + 4],
                    p_mesh->p_lightmap[n_max_page].p_buffer[(n_max_j * 6) + 5]);
                // vrátí radiativní energii ...

                p_mesh->p_lightmap[n_max_page].p_buffer[(n_max_j * 6) + 3] = 0;
                p_mesh->p_lightmap[n_max_page].p_buffer[(n_max_j * 6) + 4] = 0;
                p_mesh->p_lightmap[n_max_page].p_buffer[(n_max_j * 6) + 5] = 0;
                // odstraní radiativní energii ...

                return f_max_energy;
            }
        }

        p_mesh->p_lightmap[n_max_page].p_buffer[(n_max_j * 6) + 3] = 0;
        p_mesh->p_lightmap[n_max_page].p_buffer[(n_max_j * 6) + 4] = 0;
        p_mesh->p_lightmap[n_max_page].p_buffer[(n_max_j * 6) + 5] = 0;
        // vznikla chyba rasterizéru a radiace se vykreslila kousek mimo face,
        // takže nikam nepatří ... odstraníme ji a zkusíme to s další ...
    }

    return -1;
}

Na začátku najdeme jednoduše texel s největší radiativní energií v jedné z lightmap, potom postupně procházíme všechny groupy (mapovací skupiny faců) a zjišťujeme ke kterému z nich texel patří. Potom máme normálu, najdeme si id příslušné face a spočítáme pozici ve worldspace. Vrátíme radiativní energii a v lightmapě ji vynulujeme. Občas se stane, že se přiřadí nenulová radiativní hodnota texelu, který neleží na žádném facu - proto je celý kód ve smyčce, aby se vždycky buď našel nějaký texel, nebo se vrátilo -1 = radiativní energie je nulová.
    Example po spuštění začne vyzařovat energie, to se dá zapnout / vypnout klávesou 'I', klávesou 'P' se dá zobrazit aktivní vyzařující patch, klávesou 'F' se vypne zobrazení hemicube, klávesou 'G' se zapne jednodušší zobrazení a 'H' sférické zobrazení hemicube. Pomocí 'R' se dá zobrazit radiativní energie a pomocí 'Q' se dá zobrazit obraz se zářícími světlými věcmi. Ukázalo se, že lightmapy mají trochu handicap, že texely na hranách jsou vidět třeba jen z půlky a jsou tmavší. K tomu jsem přidal ještě funkci pro rozmazání iluminativní složky pomocí 'B'. To ale nedělejte mockrát, jinak se vám rozmažou stíny. A výsledek ? Nakreslil jsem scénu v duchu System Shock, po nějaké té půlhodině se krásně prosvětlí:

illumination

illumination

illumination

... a ještě na závěr ukázka jak funguje color bleeding: (zaměnil jsem dvě textury za bílou a červenou texturu a výsledkem je červený nádech jednak na bílé textuře, ale docela dobře je vidět třeba i na bedně)

color bleeding

    Tak, stáhněte si náš malý

 Radiosity processor 2

... a můžete si hrát ! Příště uděláme nějakou tu dodělávku a pak už se vrhneme na OpenGL !

    -tHE SWINe-




Valid HTML 4.01!
Valid HTML 4.01