English Welcome Kecy Programování 3D - Engine Guestbook Odkazy Downloady O autorovi Napiš mi Mailform |
3D - Engine 11 - lightmapy
No nazdar !
Přišel další tejden (teda vejkend) a zase mám šanci sednout k počítači, kterej nezvládne přehrávat mp3-ku a zároveň mít otevřenej notepad, takže si rozbaluju hudbu do vawů a píšu :-) (ono mi moc jinýho nezbývá - je pravda, že sem moh vyrazit na Citadelu do Brna, ale holt zase plánuju update svýho miláčka, takže nikam nerazím kvůli penězům - mimochodem mě můžete vidět v Pardubicích, jak pobíhám ve slušivým oblečku (oranžová je out :-() městských služeb a dělám ze sebe vola za 150,- na hodinu) ... a za měsíc tradá ! GeForce FX 5900 ! a potom dva 120 GB disky a krásné pole raid0. ... a pak už mám všechno. Dneska máme slíbený ty lightmapy, takže se na to vrhnem, jak nebožtík na útulnou rakev ! Lightmapy jsou, jak název napovídá mapy, obsahující světlo. Je to skoro doslova. Lightmapy jsou textury, nabalené zvlášť na každý face světa (tudíž žádná lightmapa se neopakuje) a do nich se předpočítá světlo i se stíny. Předpokládá to sice bohužel - narozdíl od stencilu statickou geometrii, ale dá se to se stencilem kombinovat. Cílem programu tedy je :
Standardní lightmapovací artefakt - všimněte si, že stín je takový hrbatý (nízké rozlišení lightmapy)
Jak namapovat lightmapu
Jak jsme si řekli, lightmapy budeme mapovat po všech facech světa. Dál by to chtělo, aby texely lightmap měly na všech polygonech zhruba stejnou velikost a byly zhruba čtvercové (to znamená pro větší povrch použít větší lightmapu) Nejjednodušší pro nás bude tzv. planární mapování, které se ve většině případů používá. To se vezme normála facu a určí se její největší složka. Potom se vezme obdélník, který se na normálu jakoby "napíchne" (je na ni kolmý) a v tom obdélníku se promítnou vertexy facu. Jak správně tušíte - podle pozice projekcí se určí souřadnice v lightmapě. Potom se ještě určí velikost lightmapy a máme hotovo. Je tu ale pár věcí na probrání :
Druhý problém je trochu složitější - pokud prostě vnutíme mocninu dvou, texely budou prostě obdélníkové a to nevypadá dobře. Novější 3D karty sice podporují textury, které nemusí mít velikost mocnin dvou, ale dá se předpokládat že to je pomalejší. Řešením je uspořádat lightmapy inteligentně do jedné velké textury (o rozlišení třeba 1024×1024). To má řadu výhod :
Jak lightmapy naplnit Jakkoli. Můžeme použít jakýkoliv stínovací model. My prostě vezmeme normálu na povrchu, vynásobíme s ní paprsek, jdoucí z texelu lightmapy do světla a to ještě vynásobíme barvou světla a viditelností. (viditelnost nemusí být jen 0 / 1, protože lightmapy se nepočítají realtime) Lightmapy se taky můžou počítat pomocí radiosity, photon tracingu, raytracingu, nebo čehokoli. Jak udělat animované lightmapy To už je jednoduché. Vzpomínte si na scénu z Half Life, kde procházíte rozbitým komplexem, nad vámi poblikávají rozbité zářivky a do toho ještě ty zvuky ? Dokonalá atmosféra - a pomocí lightmap je jednoduché ji navodit. Jednoduše spočítáme pro každé světlo lightmapy zvlášť (ne všechny, ale jen ty kam světlo svítí) a ty potom sčítáme do speciální sady lightmap, která se bude renderovat. Nemusíme je tedy sčítat každý snímek, v pohodě to stačí tak desetkrát za sec. Světla se tedy mohou rozsvěcet nebo zhasínat, poblikávat, nebo se postupně ztlumovat a rozsvěcet. Je to sice dost náročné na paměť, ale dneska ... Taky můžete vzít stencil a lightmapy kombinovat s ohledem na stíny (a stíny vrhají jen věci, které nejsou v lightmapách už započítané, tzn. jen pohybující se věci jako hráči, dveře a výtahy ...) Dynamická světla s lightmapami - jednoduše vezmete lightmapy, které používáte pro dočasné součty a do nich napočítáte světla jako normálně. Problém je, že touhle cestou pravděpodobně nespočítáte stíny. Můžete zkusit do lightmapy promítnout stěny, vrhající stín a vykreslit je černě, říká se tomu projection shadows - problém je, že když se to počítá tzakhle, musíte zahrnout geometrii celého světa, nejen pohybujících se věcí, jak by tomu bylo u statických světel se stencilem. Možností je hromada, ale tím se nebudeme zabývat. Jak lightmapy renderovat Když kreslíme face, namapujeme texturu a navíc příslušnou lightmapu a při kreslení pixelů je násobíme. Nevýhodou tohoto systému je, že světlost může být maximálně 1 (255). Takže textura pod světlem má své "původní" barvy. Dnes se ale začíná prosazovat tzv. HDR - high dynamic range. To znamená hodnoty nula až nekonečno. Takže můžeme vidět stěnu pod světlem až bílou, což se víc blíží reálnému světu. HDR používá třeba Half Life 2 :
Krása, co ? (Half - Life 2 teda nepoužívá lightmapy, jak jsem už říkal, ale stencil) Oprava !
Po analýze zdrojáků Half - Life 2 jsem byl zjištěn, že tam jsou taky lightmapy :-( škoda ! Dělá
se to tak, že textury jsou dvě - jedna pro hodnoty 0 - 255 a druhá je vlastně vyšší řád, kterým
se násobí výsledná barva, ale už se nedělí 256. Takže hodnoty jsou opravdu od nuly do "nekonečna".
(nekonečno = 256 - násobek původní textury. Za předpokladu, že textury budou focené, žádný pixel
nebude mít žádnou složku nulovou, takže to úplně stačí na bílou ve všech případech. Možná by to
mohlo být jen trochu přesnější na vyšších řádech, ale tluče nás paměť a (n+1)-násobný počet operací
oproti klasickému RGB, kde n je počet přídavných textur pro vyšší hodnoty barev v naší lightmapě)
Musí se taky hlídat, aby nějaká ze složek barvy nepřetekla 255. To si ukážeme u radiosity, která
výsledky světlosti od "nuly do nekonečna" dává - přesně, jak je tomu v reálu.
Teď se pustím do psaní examplu a za chvíli ho tady popíšu. Chvilku počkejte. Psal jsem to pomalu, protože vím že to budete pomalu chápat (vím, že to je starý ale nemoh sem si prostě pomoct) Podíváme se na funkci, která seskupuje facy podle toho, jak budou spolu ležet v lightmapách.
Je to docela jednoduché. Tak, máme tam strukturu pro group. Group bude ona skupina faců se společnou lightmapou. No a teď co to dělá ? Volá se funkce Group_Faces(), které se předá svět (předpokládá se že souřadnice jsou natransformované) a prochází se face po facu. Pro každý face se procházejí groupy a zjišťuje se, jestli v nich nebude nějaký face, na který by navazovala. Pokud ne, založí se nový group. Pokud navazuje, přidá se již do existujícího groupu a zkontroluje se, jestli právě přidaná face nespojila dva nebo víc groupů dohromady. V tom případě by se spojily. Taky se tam několikrát volá Check_GroupSize(). Ta zjišťuje, jestli je group přípustně velký. Představte si velký dům - celé patro bude mít rovnoběžné polygony podlahy a group by vyšel v podstatě stejně velký jako půdorys domu. My jsme ale omezeni nějakým maximálním rozlišením lightmap, takže musíme dělat jen tak velké groupy aby se do lightmapy bez zmenšování vešly. Check_GroupSize() vypadá takhle:
Tahle funkce používá minimální a maximální souřadnice bodů faců v groupu k určení velikosti. Při zakládání nového groupu se určí mapovací rovina. Už sme si říkali že budeme určovat pro každou skupinu faců obdélník, ležící buď rovnoběžně s rovinou xy, xz nebo yz, jenž se tak stane rovinou mapovací. Jak tuhle rovinu vybereme ? Jednoduše určíme největší složku normály nějaké z faců v groupu (v každém groupu všechny facy leží na jedné rovině) a podle toho i příslušnou rovinu. Z té určíme vektory v_u a v_v, které ukazují kterými směry se zvyšuje souřadnice u a v lightmapy. Potom lze určit velikost pomyslného obdélníku lightmapy, promítnutého do mapovací roviny jako skalární součet vektoru v_max - v_min a v_u, resp. v_v. Ještě se podíváme na funkci, která tyhle hodnoty připraví hned po vložení prvního face do groupu:
Funkce Add_FaceToGroup() slouží jen k přidáváni pointeru na face do seznamu groupu, případně onen seznam zvětší:
Tohle by mělo být jasné. Funkce b_Same_Vertex() už byla v example na stenciů a jen porovnává dva vrcholy a b_Neighbour_Face() je taky jen mírně upravená ze stencil example. Jedinný rozdíl je, že počítá trošku precizněji odchylku rovin dvou faců pomocí rovnoběžné roviny, posunuté na jeden z vertexů testovaného facu:
Výsledkem volání popisovaných funkcí je, že se všechny facy spojí do groupů ve kterých budou mít společnou lightmapu. Jak to vypadá se můžete podívat na obrázku, kde každá barva znamená příslušnost k nějakému groupu: Facy v groupech
Když teď máme facy rozdělený po skupinkách, nebylo by od věci dokončit mapování
lightmap. Co máme ? Máme minimální a maximální souřadnici našeho pomyslného obdélníku, máme vektory
které ukazují kudy půjde osa u a v. Pomocí toho jsme už schopní dopočítat souřadnice
lightmap v jednotlivých facech a spočítat pozici každého texelu lightmapy ve worldspace.
Založíme si další strukturu a hurá na to:
... abych to trošku vysvětlil. Naše funkce Generate_Map_Coords() dostane pole všech groupů a jejich počet a pro každý group vygeneruje strukturu ve které jsou uložené informace o lightmapě, o jejím počátku v_org ve 3D světě (počátek = levý horní roh) a dva vektory v_u a v_v, které ukazují kterými směry se zvyšuje souřadnice u a v, které jsme už spočítali při vytváření groupů. Potom je tam také šířka a výška (f_width a f_height) obdélníku ve worldspace. Potom už jen vezmeme každý vrchol každého face v groupu a jednoduše se jeho souřadnice promítnou do obdélníku lightmapy a určí se u a v. (vzhledem k tomu že obdélník leží rovnoběžně se souřadnými rovinami a není nějak natočený je to triviální úloha, ale ani kdyby nebyl by se už ten kód moc nezměnil) Teď máme svoje obdélníkové textury (on to občas vyjde čtverec, ale opravdu ne moc často), ale my potřebujeme textury o velikosti mocnin dvou, co s tím ? Jednoduše vezmeme větší texturu (třeba 1024x1024) a do ní uložíme všechny textury. Pokud se nevejdou do jedné, uděláme jich víc ... Jak na to ? No, nemusíme nad tím moc přemýšlet. Nejjednodušší nápad je udělat pole hodnot, které bude znázorňovat nejvyšší zaplněný řádek textury. Pokud chceme vložit texturu, musíme najít takové místo, kde součet nejvyššího použitého řádku s výškou lightmapy je menší, než výška textury. Pokud se tam lightmapa vejde, naplníme pole novou hodnotou nejvyššího použitého řádku. Je to jednoduché, ale nějak mi to nejde vysvětlit, třeba to pochopíte ze zdrojáku:
Tak. Máme svoji strukturu, která obsahuje rozměry textury, potom p_frontline, což je pole s nejvyššími použitými řádky (pro každý sloupec rastru jedna hodnota) a nakonec vlastní obrazová data, o která se tady celou dobu snažíme. Potom je tam funkce p_Lightmap(), která jen naalokuje strukturu takže nás nezajímá. Zato další funkce nás zajímat bude. b_InsertTile() se pokusí vložit obdélníček o nějaké výšce a šířce. Pomocí parametrů vrací pozici, kam obdélníček umístila a návratová hodnota funkce znamená true - vešel se a false - nevešel se. Funguje tak, že prochází pole frontline, hledá nejvyšší použitý řádek a kontroluje, jestli se nám sem náš obdélníček ještě vleze. Zároveň odpočítává počet sloupců a kontroluje, jestli nám to už nestačí. Pokud se objeví sloupec, nad který už nevyjdeme, vynuluje počítadlo sloupců a začíná znovu od dalšího. Nakonec buď zjistíme že se nevejdeme nikam a vyskočíme, nebo se vejdeme a máme naše x y souřadnice. Teď přijde na řadu jedna věc - budeme chtít lightmapu filtrovat. Filtr ale potřebuje předchozí (další) texely pro správnou interpolaci, takže uděláme okénko o jeden pixel větší na každé straně (+= 2 na začátku funkce). Vyplníme tedy oblast kde bude naše textura za obsazenou a spočítáme transformační matici pro souřadnice lightmapy. Ona je totiž naše lightmapa namapovaná tak, že souřadnice [0, 0] je v jednom rohu mapovacího obdélníku a [1, 1] je v protějším. Teď ale náš obdélník přenášíme do textury a potřebujeme souřadnice jednak někam posunout a také trochu zmenšit. Pro řešení jsem zvolil matici, protože je to (doufám) pro vás dobře pochopitelné. Touhle maticí tedy později natransformujeme všechny souřadnice lightmap (= u, v) v groupu, který jsme právě přidali. Teď už máme konkrétní souřadnice v rastru takže nám nic nebrání sáhnout po raytraceru a spočítat vlastní lightmapy. Nebudeme vymýšlet žádné složitosti. Napíšeme lambertovu rovnici, vyhodíme jeden paprsek pro zjištění, jestli světlo bude vidět a přičteme ambient. Výsledná funkce vypadá takhle:
Raytrace_Tile() dostane group, mapovací informace, geometrii, pozici v rastru (a šířku rastru, abychom mohli počítat index), vlastní buffer textury a ambientní světlo. Na začátku každé smyčky se spočítá část souřadnice ve worldspace, a to pomocí počátku obdélníku a vektorů v_u a v_v. To je ale pozice právě na tom obdélníku. Pokud si ale dobře pamatujete, některé facy můžou být skloněné, lae obdélník leží vždy rovnoběžně s nejbližší souřadnou rovinou, takže je třeba dopočítat rozdíl pomocí řešení rovnice roviny. Potom už je tam jen smyčka pro každé světlo, kde se spočítá Lambertova rovnice, pokud vyjde nějaká světlost tak se vystřelí paprsek a určí se, jestli je pixel lightmapy ve stínu. Nakonec se hodnoty ořežou do intervalu <0, 255> a uloží se do rastru. Tím máme hotové generování lightmap a pomalu přejdeme na jejich zobrazování. Hotová textura
Než to ale uděláme, ještě musím připomenout abyste nezapomněli vyplnit
jednopixelový rámeček okolo lightmapy hodnotami z ní, aby jsme neměli v rozích zvláštní hodnoty.
Je také dobré lightmapu trošku rozmazat, vypadá potom líp, ale nesmí se to přehánět, jinak začnou
být vidět hrany kde lightmapa kvůli rozmazání nenavazuje.
počítadlo lightmap
Počítadlo pracuje s kódem, který jsem tady popsal, pouští se z příkazové
řádky, kde první parametr musí být název .3ds světa s vloženými světly (nebo i bez, to je jedno
- jen nebude nic vidět), Další parametry můžou být "-axxxxxx", kde "xxxxxx" je hexadecimálí číslo,
určující ambient barvu. Dál "-qx" kde "x" je float násobek velikosti lightmap oproti světu (.5 je dobrá hodnota),
nebo "-rx" kde "x" je rozlišení stránek lightmap (já jsem používal 512) a nakonec "-bx" kde "x" je poloměr
rozmazávání (0 = bez rozmazání) ... Program si chvíli chroustá a potom zapíše lightmapy jako bitmapy
a přepíše kompletně celou geometrii, protože k ní přibyly souřadnice lightmap (.3ds pak už není potřeba).
Teď si ukážeme jak lightmapy kreslit. V zásadě na tom není nic těžkého, ale jsme v software renderingu
a vzhledem k tomu, že lightmapy můžou být dost velké textury a můžou obsahovat dost širokou škálu barev,
rozhodl jsem se že je nemůžeme dát do palety a dělat bilineární filtrování jako u textur ... Proto
se naučíme jeden trik, kterému se říká dithering.
Dithering Dithering je anglicky rozptylování. Co to znamená ? My nebudeme počítat nové barvy pomocí pomalé interpolace, kterou jsme si předtím spočítali do tabulky barev, ale prostě rozházíme podobné barvy do různých vzorů tak, že ve výsledku se bude oku zdát že plocha byla vyplněna novou barvou. Tahle technika je dost jednoduchá a taky rychlá. Výsledek vypadá asi takhle: Dithering lightmapy
Tohle je hodně přiblížené a je vidět, jak se ze dvou sousedních pixelů skládají různé vzory, které
z větší vzdálenosti vypadají jako plynulý barevný přechod. Jak na to ? Potřebujeme znát nějakou
ditherovací tabulku. Tabulek je víc (ony jsou generované stejným způsobem, jen čím větší tabulka,
tím komplikovanější vzory) Já jsem někde vyhrabal tabulku 4x4. Co taková tabulka obsahuje ? Obsahuje
desetinná čísla od nuly do jedné, systematicky rozházená po tabulce. Těmito čísly budeme vychylovat
souřadnici lightmapy. V závislosti na čem ? Na pozici na obrazovce. Slyšíte správně ! Budeme sledovat
pozici každého pixelu na obrazovce a podle toho určíme hodnotu z tabulky. Někdo může říct: "No jo, ale
my máme jednu tabulku a potřebujeme vychylovat dvě souřadnice !" To je pravda. Potřebujeme ještě jednu
tabulku, která vznikne jednoduchým převrácením pořadí hodnot z první tabulky. Myslím že už jsem to
vysvětlil dost, abyste pochopili zdroják:
Na začátku máme svoje tabulky. Našel jsem tabulky s koeficienty 1 - 15, takže jsem je vydělil šestnácti, aby vzniklo desetinné číslo a vynásobil 0x10000 abych měl desetinné číslo ve fixed pointu. Funkce pod tabulkami je naše upravená texturovací smyčka, která bilineárně filtruje texturu, potom spočítá index v ditherovací tabulce a přičte ho k souřadnicím lightmapy. Po určení barvy lightmapy moduluje výslednou barvu pixelu a pokračuje dál ... Example umí přepínat mezi třemi módy filtrování: textura i lightmapa ditheringem (F9), textura bilineárně a lightmapa ditheringem (F10) a nakonec jsem napsal i pomalejší funkci, která bilineárně filtruje jak lightmapu tak texturu (F11). Výsledek ale není tak rozdílný, jak by se dalo očekávat: bilineárně zfiltrované lightmapy ditherované lightmapy ditherované lightmapy s quincunxem - téměř nepoznáte že to není bilineární filtr
Co říct nakonec ? Snad jen že se omlouvám, pokud jsem někde nechal
nějakou chybku. Jsou tři dny před vánocema a já jsem fakt ve skluzu, potřebuju dodělat
ještě dva tutorialy a přeložit tři do angličtiny. Ještě jsem plánoval změnu designu,
ale to by bylo na úkor vzhledu ... Takže si stáhněte ještě engine, kterej vypočítaný lightmapy používá:
S-buffer lightmap 2
no a zase někdy ...
-tHE SWINe- |