Semestrální Práce - Visibility Buffer

B4M39PGR2 - Petr Nahodil

Stručný popis zadání

Implementovat metodu odloženého stínování s pamětí viditelnosti (visibility-buffer deferred shading) redukující datové přenosy mezi průchody na minimum.

Dále porovnat škálování výkonu s dopředným stínováním a s odloženým stínováním s pamětí geometrie (g-buffer deferred shading).

Algoritmus

Princip mnou implementovaného algoritmu spočívá v tom při prvním průchodu ukládat co nejmenší počet dat k tomu, aby bylo možné scénu odloženě stínovat v následujícím průchodu. Oproti paměti geometrie (g-buffer), který ukládá o fragmentu několik informací do několika textur (například barvu, normálu, pozici, typicky i další), visibility-buffer ukládá pouze ID objektu a ID trojúhelníka.

Zatímco g-buffer vzorkuje textury v prvním průchodu a využívá pro každý pixel až desítky bajtů, visibility-buffer využívá pouze 4 bajty za pixel, nebo případně 8 bajtů pokud je potřeba ukládat více, než 65536 objektů a/nebo mají některé objekty více, než 65536 trojúhelníků. Například v případě, že by bylo nutné vykreslovat 100 000 objektů, které mají každý 30 000 trojúhelníků, by ale samozřejmě bylo možné upravit visibility-buffer aby neukládal ID objektu a trojúhelníka po 16 bitech, ale 17 a 15, díky čemu by nebylo nutné zvyšovat paměť pro ID z 4 bajtů na 8.

Visibility-buffer má tu nevýhodu, že vzhledem k tomu, že ukládá pro další průchod pouze ID, je nutné několik atributů znovu vygenerovat. Mezi tyto atributy patří pozice vertexu, jeho normála, texturovací souřadnice (UV), gradient texturovacích souřadnic pro filtrování textur atd.

Přes tuto nevýhodu ale není netypické, aby byl visibility-buffer rychlejší, než g-buffer, kvůli tomu, že typicky jsou grafické karty omezeny propustností a latencí globální paměti, ne nutně výpočetním výkonem. Poměr mezi výpočetní rychlostí a propustností paměti je samozřejmě jiný pro každý model grafické karty, ale ve většině případů je tento poměr vychýlený k tomu, že je paměť úzkým hrdlem.

Další pamětová výhoda visibility-bufferu je ta, že za každý pixel bude vzorkovat každou texturu pouze jednou. G-buffer při prvním průchodu typicky musí přepsat všechna navzorkovaná data, když narazí na bližší fragment. Visibility-buffer musí přepsat pouze ID objektu a trojúhelníku.

Implementace

Jako základ práce jsem využil školní kód (specificky podprojekt TiledDeferredShading) ve kterém byla již obsažena vhodná scéna pro testování výkonu algoritmu a dopředné a odložené stínování (g-buffer) bylo již implementované. Má implementace tedy spočívala v tom do tohoto základu přidat třetí algoritmus (visibility-buffer) a také upravit dva existující.

Data, která posílá první průchod dalšímu, ukládám do textury s 32 bitovými barevnými složkami. Záleže na zapnutých přepínatelných možnostech má tato textura jednu až čtyři barevné složky. Ve zbytku textu budu tuto texturu nazývat jednoduše visibility-buffer.

Nadále jsem ke každému algoritmu přidal možnost využívat až 5 (náhodných) textur navíc (více se mi nedařilo zkompilovat). Toto bylo přidáno, aby šlo otestovat škálování výkonu visibility-bufferu s počtem textur oproti g-bufferu. Visibility-buffer je při takto vysokém počtu textur jednoznačně rychlejší.

Implementované přepínatelné možnosti

Ve školním kódu lze jednoduše definovat přepínatelné možnosti, které se v shaderech projeví jako preprocesové #define direktivy.

Osmibajtové ID

Při zapnutí této možnosti se místo dvou 16 bitových ID zabalených do první barevné složky visibility-bufferu začnou ukládat dvě 32 bitové ID odděleně do první a druhé barevné složky. Toto není pro připravenou scénu nutné, ale je to vhodné jakožto způsob, jak testovat dopad na výkon při použití 32 bitových ID.

Ukládání barycentrických souřadnic

Když je tento přepínač vypnutý, barycentrické souřadnice se pro každý pixel dopočítávají ve stínovacím průchodu pomocí výpočtu průsečíku paprsku s trojúhelníkem. Při zapnutí se místo toho do visibility-bufferu, do první volné barevné složky po ID, přidávají barycentrické souřadnice, které vypočítal "za mě" zobrazovací řetězec OpenGL.

Protože pointa tohoto algoritmu je neplýtvat propustností paměti, neukládají se barycentrické souřadnice jako tři čísla datového typu float (3x32 bitů), ale jako dvě čísla typu half (2x16 bitů). Třetí složka se dá dopočítat z prvních dvou, protože barycentrické souřadnice mají vždy sumu 1. K této kompresi jsem využil GLSL funkci packHalf2x16(vec2) -> uint.

Ukládání analytických derivací

Když je tento přepínač vypnutý, analytické derivace (gradient) texturovacích souřadnic se pro každý pixel dopočítávají ve stínovacím průchodu pomocí "manuálního" implementace dat, která by normálně v zobrazovacím řetězci implicitně počítaly helper-invocations (více v sekci Využití funkcí dFdx/y). Při zapnutí se místo toho do visibility-bufferu, do první volné barevné složky po ID a po barycentrických souřadnicích, přidávají tyto derivace stejným způsobem jako barycentrické souřadnice.

Nezarovnané třísložkové vektory

Když je tento přepínač vypnutý, tak se data vektory pozic objektů a jejich polygonální sítě ukládají zarovnané na 16 bajtů. Toto je, alespoň v mé zkušenosti, výchozí chování OpenGL při definování vec3[]. Při zapnutí se místo toho ukládají data se zarovnáním pouze na velikost složky vektoru. Jinými slovy při vypnutí se po 3x32 bitů vektorových dat ukládá 32 bitů ničeho kvůli zarovnání.

Tohoto jsem docílil tím, že při zapnutí definuji pole vektorových dat jako pole čísel, ne vektorů. Je pak nutné přistupovat k datům pomocí jiných indexů.

Využití funkcí dFdx/y

Když je tento přepínač vypnutý, tak každý fragment dopočítává gradient texturovacích souřadnic tak, že pro oba směry vypočítá texturovací souřadnice, které by měl jeho soused kdyby byl ve stejném trojúhelníku, a následně je odečte od těch svých, nebo své od těch souseda, záleže na své pozici v 2x2 čtverci; stejně jak to při dopředném stínování implicitně dělá zobrazovací řetězec OpenGL. Tato operace vyžaduje výpočet barycentrických souřadnic "o pixel vedle", což vyžaduje výpočet průsečíku paprsku s trojúhelníkem.

Při zapnutí se tento postup změní tak, že se dopočítávají texturovací souřadnice souseda, jak je popsáno v předchozím paragrafu, pouze když je to nutné, tj. pouze když soused zavolal discard nebo není ve stejném trojúhelníku. Když to ale nutné není, tj. když soused nezavolal discard a je ve stejném trojúhelníku, tak se použije funkce dFdx nebo dFdy.

Obrázek - Aplikace

application screenshot

Obrázek - Zobrazení využití dFdx/y

application screenshot

Ke stažení