Lekce 14 - 3D bludiště v XNA - Hardwarové instancování poprvé
Vítejte po dvacáté čtvrté. Jak jsem naznačil posledně, 3D bludiště v XNA - Vertex a index buffer, nejsem vůbec spokojen s rychlostí vykreslování.
Není to sice ten pravý ukazatel, za kterým by bylo fajn se honit, ale pro velká bludiště to problém je docela velký. Vezměme si takové bludiště o velikosti 30x30 polí. To je tvořeno z celkem 900-ti modelů. Naštěstí, jak už víme, XNA model nahraje pouze jednou a my pak dostáváme pouze odkazy na jednu instanci. Z minulého dílu také víme, že data jsou uložená v grafické kartě v podobě vertex a index bufferu. Pokaždé, když chceme vykreslovat celé bludiště, opakujeme několik kroků:
- Vymažeme celou obrazovku
- Pro každý model, který vykreslujeme, provedeme:
- Nastavíme společné parametry efektu (matice view, projection)
- Nastavíme jedinečné vlastnosti objektu, třeba barva u podlah a nebo pozice modelu (matice world)
- Nastavíme efekt jako aktivní
- Nastavíme index a vertex buffer modelu jako právě používaný (toto je součástí metody Draw ve třídě modelu)
- Vykeslíme model (opět součástí metody Draw)
- Vrátíme nastavení barvy u podlahy zpět
Jak je vidět, je to proces poměrně komplexní. Poměrně dost kroků se pro všechny modely zbytečně opakuje. Je to však přístup zcela intuitivní a plně pro malé počty objektů dostačuje. Když ale je potřeba provádět tuto operaci třeba 900x při vykreslování každého snímku, tak se počet prováděných operací začíná výrazně projevovat. Je v zásadě několik možností jak situaci řešit a tou první, kterou si vyzkoušíme, je takzvané instancování.
To se hodí zejména na ta místa, kde je spousta opakujících se modelů. A to je přesně náš případ. Vykreslujeme spousty zdí a spousty podlah, proto jsem dal přednost této technice. Všemu pak předchází příprava, kde si vytvoříme jeden nový buffer, ve kterém skladujeme rozdílné parametry skupiny objektů které nainstancujeme. V našem případě to bude matice World a barva. Proces vykreslování pak vypadá následovně:
- Příprava: Vytvoříme si buffer s jedinečnými informacemi o instancovaných objektech (provádíme jen jednou)
- Vymažeme celou obrazovku (tohoto kroku se nelze zbavit)
- Nastavíme ručně index a vertex buffer instancovaného modelu
- Nastavíme ručně druhý vertex buffer, který obsahuje rozdílné vlastnosti
- Nastavíme společné efektu vlastnosti (matice view a projection a třeba i texturu)
- Nastavíme efekt jako aktivní
- Pro všechny instancované objekty provádíme
- vykreslujeme objekty pomocí informací ve druhém vertex bufferu
Je vidět že počet kroků, které se opakují pro každý z modelů, se
zmenšil prakticky jen na vykreslení. Fakticky říkáme kartě: "chci to
vykreslit tam, tam a tam". Tato technika má ale i svoje negativa, nemůžeme
použít připravený BasicEffect, musíme si tedy napsat shader vlastní.
Jelikož navíc nevíte, co to je shader a ani jak se vytváří, budete mi
muset věřit, že vím, co dělám Hrozná představa že, ale myslím, že brzy napíši něco o tom,
jak grafická karta funguje nebo spíš o tom, jak si myslím, že grafická
karta a shadery fungují.
Budeme je potřebovat pro osvětlování a taky pro postprocesorové efekty, ale to opět předbíhám. Celý článek je založený na tomto návodu: http://sciencefact.co.uk/…ng-in-xna-4/
Dost bylo povídání, jdeme na věc. Napadly mě dvě možnosti, jak to
napsat tak, aby se to hodilo do enginu. Můžeme udělat speciální herní
okno, kde pokaždé když přidáme model tak se správně přiřadí do skupiny
a bude se instanciovat. A nebo můžeme vytvořit novou komponentu. Nakonec jsem
zvolil komponentu. Sice to nebude tak přívětivé na používání, ale zas to
bude ladit s okolím. Do enginu, do složky s komponentami si přidáme novou
třídu, já jsem ji nazval InstancedModel3D
. Učiníme jí
veřejnou a opravíme jmenný prostor. Jako výchozí třídu použijeme třídu
Component
. Přidáme si proměnnou se jménem souboru s
modelem:
public string ModelName{ get; protected set; }
A taky proměnnou, kde si uložíme načtený model:
protected Model Model;
V konstruktoru předáme jméno modelu, který budeme instanciovat:
public InstancedModel3D(string name){ ModelName = name; }
Přepíšeme si metodu Load
od předka, stále nic neznámého,
a v ní načteme model:
protected override void Load(){ Model = Parent.Engine.Content.Load<Model>(ModelName); }
Bohužel není možné k instanciování používat přímo třídu Model, budeme z ní tedy potřebovat dostat vertexy a indexy. Přidáme si pár nových vlastností do naší třídy:
protected VertexBuffer Verticles; protected IndexBuffer Indicies;
Vertex buffer se bude hodit určitě, do něj si uložíme vrcholy našeho
modelu. A aby bylo jak je pospojovat, přidáme i buffer s indexy. Oba si je v
metodě Load
naplníme, ale nejdřív musíme mít čím. Do metody
Load
si přidáme dva pomocné listy, do kterých budeme vertexy a
indexy ukládat:
List<VertexPositionNormalTexture> vert = new List<VertexPositionNormalTexture>(); List<ushort> ind = new List<ushort>();
U indexů použijeme typ ushort
, tedy
unsigned short
, což je 16-ti bitové celé číslo bez znaménka.
Vertexy si uložíme i s normálou, může se nám později při tvorbě
pokročilých efektů hodit. Dále si také nakopírujeme lokální transformace
modelu do známého pole:
Matrix[] transforms=new Matrix[Model.Bones.Count];
Model.CopyAbsoluteBoneTransformsTo(transforms);
Máme vše připraveno, můžeme jít na extrakci dat. Budeme muset projít všechny součásti modelu a data z nich dostat:
foreach (ModelMesh mesh in Model.Meshes){ foreach (ModelMeshPart part in mesh.MeshParts){ //dalsi kod sem } }
Model, jak už víte, se člení na jednotlivé navzájem hierarchicky uspořádané části, kterým říkáme meshe. Navíc se každá mesh skládá z několika částí, i když obvykle to je pouze jedna. Ale u složitějších modelů to nemusí platit, proto musíme projít foreach cyklem úplně všechny. Vytvoříme si opět dvě pomocná pole, do kterých přesuneme obsah z bufferů:
VertexPositionNormalTexture[] partVerts = new VertexPositionNormalTexture[part.VertexBuffer.VertexCount];
a
ushort[] partIndices = new ushort[part.IndexBuffer.IndexCount];
Velikosti polí si bereme z obou bufferů. Jako první si naplníme vertexy:
part.VertexBuffer.GetData(partVerts);
A právě získané vertexy si musíme ještě poupravit. Nyní je máme, řekl bych, v syrové podobě. Musíme na ně aplikovat transformace, které jsme si dopředu uložili. Je to stejný proces, který děláme, když model vykreslujeme, takže nic moc nového pod sluncem:
for (int i = 0; i < partVerts.Length; i++){ partVerts[i].Position = Vector3.Transform(partVerts[i].Position, transforms[mesh.ParentBone.Index]); }
Pak nám už zbývá je jen uložit do pomocného listu:
vert.AddRange(partVerts);
Obdobně naložíme s indexy, jen je není potřeba nijak upravovat, takže je jen vyzvedneme a uložíme do listu:
part.IndexBuffer.GetData(partIndices); ind.AddRange(partIndices);
A je to, nejčernější práce je prakticky hotova. Už jen stačí vytvořit ze získaných dat nové buffery:
Verticles = new VertexBuffer(Parent.Engine.GraphicsDevice, VertexPositionNormalTexture.VertexDeclaration,vert.Count, BufferUsage.WriteOnly); Verticles.SetData<VertexPositionNormalTexture>(vert.ToArray()); Indicies = new IndexBuffer(Parent.Engine.GraphicsDevice, IndexElementSize.SixteenBits, ind.Count, BufferUsage.WriteOnly); Indicies.SetData<ushort>(ind.ToArray());
Oba buffery zůstanou po celou dobu nezměněné. Proto není potřeba je mít jako dynamické. Ještě si z modelu vytáhneme texturu, kterou používá, a tu si uložíme někam stranou:
protected Texture2D Texture; Texture = ((BasicEffect)Model.Meshes[0].Effects[0]).Texture;
Ano má to celé háček, komplexnější modely se nám takto jednoduše ošéfovat nepovede. Modely, které používají více jak jednu texturu takto nenainstanciujeme. Ale pro jednoduchost to takto ponecháme.
Gratuluji, máme za sebou asi to nejhorší. Teď už jen ty krásnější věci. Jak je v úvodu zmíněno, potřebujeme víc než jeden buffer s vertexy. Je potřeba ještě jeden další, kde budeme mít odlišné vlastnosti. A jelikož se předpokládá, že je budeme programově měnit, tak je budeme ukládat v dynamickém bufferu:
protected DynamicVertexBuffer Primitives;
Bude potřeba taky určit maximální počet kopií. Proto si přidáme:
public int MaxCount{ get; protected set; }
A taky aktuální počet kopií:
public int Count{ get; protected set; }
Do konstruktoru ke jménu modelu přidáme maximální počet kopií:
public InstancedModel3D(string name,int max){ ModelName = name; MaxCount = max; }
Tady se pro dnešní díl zastavíme.
Třídu si dokončíme příště, 3D bludiště v XNA - Hardwarové instancování podruhé. Prozatím očekávám otázky, nápady stížnosti a přání no však moc dobře víte kde – v komentářích.