C/C++ & Visual C++

Kurz DirectX (31.)

Úvodem  |  Datové struktury |  Kurz DirectX  |  Downloads  |  Otázky a odpovědi

V minulé lekci jsem slíbil, že se dnes podíváme na vylepšení vykreslovací fronty, protože způsob uvedený minule, byl značně neefektivní. Největší žrout času je totiž funkce SetVertexStream() a tedy časté přepínání vertex bufferů vede ke zbytečným výkonovým ztrátám. Naším úkolem v dnešní lekci je tedy vytvořit postup, jak minimalizovat počet volání metody SetVertexStream().

31.1. Dynamický index buffer

Minule jsme použili jeden index buffer, který byl společný pro všechny listy. Mohli jsme si to dovolit, protože listy se vykreslovaly jeden po druhém. Každý měl svůj vlastní vertex buffer, který se musel před vykreslením KAŽDÉHO listu nastavit. Zde tedy vznikalo nadměrné přepínaní vertex bufferů. Zato index buffer se nastavil jen jednou pomocí metody SetIndices().

Dnes si ukážeme jiný způsob, který je elegantnější ačkoliv složitější na implementaci. Budeme postupovat opačně, čili vytvoříme jeden velký vertex buffer pro celý terén a několik menších index bufferů, jejichž obsah se bude každý snímek měnit. Takový index buffer nazýváme dynamický a vytváří se s příznakem D3DUSAGE_DYNAMIC. Tento příznak zajistí polohu index bufferu v AGP paměti (statické buffery se vytvářejí ve video paměti). Dynamický index buffer musíme zamykat s příznaky D3DLOCK_DISCARD nebo D3DLOCK_NOOVERWRITE. První z nich zajistí, že cely bufferu bude zahozen a funkce Lock() vrátí ukazatel na novou oblast pamětí, což zamezí čekání na vykreslení dat z předchozího snímku. Druhý zmíněný příznak naopak zajistí, že se nepřepíší žádné indexy, které byly vložený předchozím voláním metody Lock(). Tímto způsobem se dají přidávat indexy do bufferu.

Stejným způsobem můžeme pracovat i s vertex bufferem.

31.2. Rozdělení terénu

Náš terén rozdělíme na lokace, které budou mít předem definovanou velikost (například 128x128 políček). Každá taková lokace bude mít vlastní index buffer. Lokaci definujeme strukturou Location:

struct Location
{

// Jednoznacne ID lokace (pouze kvuli vypisum)
WORD m_wID;
// Index buffer ktery ma indexy v ramci jedny lokace
IIndexBuffer *pIB;
// pocet viditelnych listu
int m_iVisQuads;
// Minimalni a maximalni pouzity index
WORD m_iMinIndex;
WORD m_iMaxIndex;
// Docasny pointer na otevreny index buffer
WORD *pIndices;
// Okrajove body
BoundingPoint arBounds[4];

};

Kromě zmíněného IB struktura obsahuje několik dalších parametrů využívaných převážně při vykreslování. Za prvé je to počet viditelných listů. Z toho parametru později zjistíme počet trojúhelníků, které vykreslit. Dále jsou tu atributy MinIndex a MaxIndex, pomocí kterých určíme rozsah použitých vertexů při volání DrawIndexedPrimitive(). Dále je tu pomocný ukazatel na pole indexů, který použijeme při zamykání IB. Nakonec okrajové body lokace, abychom mohli přiřadit listům jejich lokaci.

Upravme tedy strukturu uzlu QTNode:

struct QTNode {

//
// Type of node - NODE, LEAF
NODETYPE ntType;
//
// Bounding points
BoundingPoint arBounds[4];
//
// Each node has four branches/children/kids
// LEAF has not any kids
QTNode *pBranches[4];

//
// Lokace do ktery list patri
Location *pLocation;
//
// leaf's tiles, each LEAF has 25 vertices
// (2 triangles per tile -> 32 triangles per LEAF)
// NODE has not any vertices
WORD *pIndices;
WORD wIndCount;

BOOL bVis;

};

Zde tedy přidáme ukazatel na lokaci, kde se daný list nachází (proměnnou inicializujeme pouze pro listy). Dále již nebudeme potřebovat vertex buffer, ale místo toho uložíme dynamické pole obsahující indexy vertexů listu. Ještě ukládáme pomocnou informaci, počet indexů (abychom toho číslo nemuseli počítat později a ušetřili tak čas).

Hlavní rozdíl bude v tom, že celý terén bude po lokacích uložen v jediném Vertex bufferu. Z toho plyne, že VB bude obsahovat okrajové vrcholy lokací dvakrát. Tím zajistíme jednodušší implementaci, ale zaplatíme vyšší paměťovou složitostí. U každé lokace budeme během rozpoznávání ukládat viditelné listy a protože víme, v jaké lokaci list je, zkopírujeme dané indexy do IB lokace. Při vykreslovaní pak budeme opět postupovat po jednotlivých lokacích. Využijeme všechny parametry, které jsme si ukládali během rozpoznávání viditelných listů. Podrobnosti si uvedeme u konkrétního algoritmu.

Upravíme také samotnou třídu CTerrain. Již nebude potřeba jeden IB, ale místo něj vložíme globální Vertex buffer. Dále bude potřeba dynamické pole lokací a počet lokací:

Location *m_arLoc;
int       m_iLocCount;

Kromě těchto atributů ještě vymažeme funkci FillLeaves(), protože listy se budou plnit zcela jinak.

Nyní se již můžeme podívat na změny v metodách třídy CTerrain. Začneme metodou InternalInit(), kde jsme minule inicializovali také globální index buffer. Dnes ovšem žádný takový buffer nemáme (nový vertex buffer můžeme inicializovat až později, protože ještě nevíme rozměry terénu). Nic nového v této metodě nebude.
Metoda ComputeNormals() zůstane beze změn.
Podstatně se však bude lišit metoda CreateQuadTree(). Tam totiž budeme vytvářet lokace s jejich index buffery a také již můžeme vytvořit vertex buffer. Zbytek metody bude stejný:

Nejdříve tedy určíme počet lokací. Víme jak je velký terén a víme jak je velká lokace (to je určeno konstantou). Poté alokujeme příslušené místo a lokace jednu podruhé nastavíme. Zde vytváříme nový objekt IIndexBuffer. Do index bufferu se musí vejít indexy listů celé lokace, která má v našem případě 128x128 políček. Určíme tedy počet listů v jedné lokaci (víme velikost lokace a velikost listu). Každé políčko má dva trojúhelníky, na každý jsou třeba 3 indexy. Z toho vypočítáme velikost bufferu. Všimněte si, že index buffer vytváříme s příznakem D3DUSAGE_DYNAMIC.
Dále určíme okrajové body lokace a nastavíme zbylé parametry na rozumné hodnoty. MinIndex a MaxIndex vždy musí mít maximální a minimální hodnotu. Protože index buffery jsou dynamické, není třeba je plnit (plní se v každém cyklu aplikace).
V další části vytvoříme a naplníme vertex buffer. Již jsem zmínil, že data ve VB jsou uloženy po lokacích. Budeme tedy opět procházet jednotlivé lokace a vždy přidáme vertexy celé jedné lokace. Jak bude VB velký? Známe počet lokací a víme kolik jedna lokace potřebuje vrcholů. Z toho snadno spočítáme výslednou velikost VB. Nyní tedy pro každou lokaci zkopírujeme příslušné vertexy z velkého pole m_arTerrain. Využijeme k tomu okrajové body lokací. Všimněte si jakým způsobem zamykáme Vertex buffer, vždy zamkneme jen část pro jednu lokaci:

Metody GenerateTerrain() a GenerateTerrainFromFile() zůstanou nezměněny.

Další dost podstatné změny nastanou v metodě CullTerrain(). První část zůstane stejná, lišit se bude pouze část, kde přidáváme nový list do fronty:

Za prvé spočítáme posun v index bufferu dané lokace (zde již mají listy odkaz na svou lokaci - viz. dále). Víme kolik listů je již v tomto IB a také víme kolik indexů jeden list zabere. Překopírujeme indexy funkcí memcpy(). IB již máme otevřený (viz. metoda Render()) a ukazatel je uložen v atributu pIndices. Dále určíme maximální a minimální použitý index. Minimální index má vrchol nejblíže k počátku, maximální je naopak ten nejdál v rámci jednoho listu. Tyto hodnoty platí pro celou lokaci, tudíž je musíme testovat pro každý list, který vkládáme do IB. Nakonec musíme zvýšit hodnotu viditelných listů (aby se další nalezený list vložil do IB nakonec). Záznam, že list je vidět je důležitý jen kvůli mapě, kterou lze zobrazit klávesou M.

Metoda Render() zaznamenala taky spoustu změn. V první části připravíme Index buffery. Tato část lze zcela přeskočit (pak je hezky vidět vykreslovaná oblast). Dále provedeme samotné vykreslení:

Opět zde uvádím jen částečný výpis, zbytek metody je stejný. Jak jsem tedy zmínil, nejdříve uzamkneme IB všech lokací a vynulujeme parametry. Všimněte si jak ukládáme ukazatele na otevřené IB. Tyto ukazatele vzápětí použijeme v metodě CullTerrain(). Nakonec je třeba IB opět odemknout. Před vykreslením nastavíme standardní věci jako te textura, vertex shader a náš nový VB. Metoda SetStreamSource() se tedy volá jen jednou za cyklus. Nyní se podrobně podíváme na způsob vykreslování.
To, že terén vykreslujeme po lokacích je asi celkem jasné. Má smysl kreslit jen ty, které mají nějaký viditelný list. Teď přijde největší finta v nastavení bázové hodnoty indexu v metodě SetIndices() (je to druhý parametr). Tato hodnota se během vykreslovaní přičítá ke všem indexům uložených v IB. Tím dosáhneme na všechny vertexy. Při volání metody DrawIndexedPrimitive() využijeme atributy lokace MinIndex, MaxIndex a iVisLeaves.
Zbytek metody je stejný.

Metoda Restore() nyní nebude obnovovat Index buffer pro list, ale vertex buffer pro terén. Výpis metody zde nebudu uvádět, protože se příliš neliší od inicializace bufferu.

Jako poslední lahůdku zde máme metodu CreateNode(). I tato metoda zůstane vesměs stejná, jen práce s listy bude odlišná:

Metoda má za úkol vytvořit a naplnit pole indexů každého listu a přiřadit list jedné lokaci. Abychom zjistili, ve které lokaci se daný list nachází, použijeme okrajové body lokací a listu. Když najdeme tu správnou lokaci, hledání ukončíme.
Dále alokujeme místo pro indexy. Počet indexů listu snadno určíme z toho, že víme jak je list veliký. Zbývá pole naplnit daty. To je poměrně obtížná operace. Indexy totiž musí mít hodnoty v rámci jedné lokace. My zde využijeme okrajové body listu, to by ale nestačilo, protože například list z druhé lokace by měl indexy mimo rozsah. Takže aby indexy byly ve správném rozsahu, použijeme dělení modulo.

Poznámka: Jistě jste si všimli, že se v kódu vyskytují řádky, které zajistí výpis obsahuj Vertex buffer a index buffer do souboru. Tato volba je volitelná a může se hodit při ladění programu. Výpis těchto dat do výstupního okna vývojového prostředí není vhodný, protože je příliš pomalý.

Kompletní zdrojový kód aplikace si můžete stáhnout v sekci Downloads.

31.3. Závěr

A co nás čeká v příští lekci? Doděláme slíbenou oblohu a pohrajeme se s funkcemi mlhy. Direct3D má docela slušnou podporu mlhy, takže to nebude nic obtížného.

Těším se příště nashledanou.

 

Ondřej Burišin a Jiří Formánek