14. díl - 3D bludiště v XNA - Hardwarové instancování poprvé

C# .NET XNA game studio 3D bludiště 3D bludiště v XNA - Hardwarové instancování poprvé

Vítejte po dvacáté čtvrté. Jak jsem naznačil posledně, 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ů:

  1. Vymažeme celou obrazovku
  2. 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ě:

  1. Příprava: Vytvoříme si buffer s jedinečnými informacemi o instancovaných objektech (provádíme jen jednou)
  2. Vymažeme celou obrazovku (tohoto kroku se nelze zbavit)
  3. Nastavíme ručně index a vertex buffer instancovaného modelu
  4. Nastavíme ručně druhý vertex buffer, který obsahuje rozdílné vlastnosti
  5. Nastavíme společné efektu vlastnosti (matice view a projection a třeba i texturu)
  6. Nastavíme efekt jako aktivní
  7. 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ě. 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.


 

  Aktivity (1)

Článek pro vás napsal vodacek
Avatar
Vodáček dělá že umí C#, naplno se již pět let angažuje v projektu ŽvB. Nyní studuje na FEI Upa informatiku, ikdyž si připadá spíš na ekonomice. Není mu také cizí PHP a SQL. Naopak cizí mu je Java a Python.

Jak se ti líbí článek?
Celkem (3 hlasů) :
55555


 



 

 

Komentáře

Děláme co je v našich silách, aby byly zdejší diskuze co nejkvalitnější. Proto do nich také mohou přispívat pouze registrovaní členové. Pro zapojení do diskuze se přihlas. Pokud ještě nemáš účet, zaregistruj se, je to zdarma.

Zatím nikdo nevložil komentář - buď první!