IT rekvalifikácia. Seniorní programátori zarábajú až 6 000 €/mesiac a rekvalifikácia je prvým krokom. Zisti, ako na to!

14. diel - 3D bludisko v XNA - Hardvérové instancování prvýkrát

Vitajte po dvadsiatej štvrtej. Ako som naznačil posledne, nie som vôbec spokojný s rýchlosťou vykresľovania. Nie je to síce ten pravý ukazovateľ, za ktorým by bolo fajn sa naháňať, ale pre veľké bludisko to problém je celkom veľký. Zoberme si také bludisko s veľkosťou 30x30 polí. To je tvorené z celkom 900-tich modelov. Našťastie, ako už vieme, XNA model nahrá iba raz a my potom dostávame iba odkazy na jednu inštanciu. Z minulého dielu tiež vieme, že dáta sú uložené v grafickej karte v podobe vertex a index bufferu. Zakaždým, keď chceme vykresľovať celé bludisko, opakujeme niekoľko krokov:

  1. Vymastíme celú obrazovku
  2. Pre každý model, ktorý vykresľuje, vykonáme: - Nastavíme spoločné parametre efektu (matica view, projection)
    • Nastavíme jedinečné vlastnosti objektu, treba farba u podláh a alebo pozície modelu (matica world)
    • Nastavíme efekt ako aktívny
    • Nastavíme index a vertex buffer modelu ako práve používaný (toto je súčasťou metódy Draw v triede modelu)
    • Vykeslíme model (opäť súčasťou metódy Draw)
    • Vrátime nastavenia farby pri podlahe späť
Ako je vidieť, je to proces pomerne komplexné. Pomerne dosť krokov sa pre všetky modely zbytočne opakuje. Je to však prístup úplne intuitívne a plne pre malé počty objektov dostačuje. Keď ale je potreba vykonávať túto operáciu treba 900x pri vykresľovaní každej snímky, tak sa počet vykonávaných operácií začína výrazne prejavovať. Je v zásade niekoľko možností ako situáciu riešiť a tou prvou, ktorú si vyskúšame, je takzvané instancování.

To sa hodí najmä na tie miesta, kde je veľa opakujúcich sa modelov. A to je presne náš prípad. Vykresľujú spústy múrov a spústy podláh, preto som dal prednosť tejto technike. Všetkému potom predchádza príprava, kde si vytvoríme jeden nový buffer, v ktorom skladujeme rozdielne parametre skupiny objektov ktoré nainstancujeme. V našom prípade to bude matice World a farba. Proces vykresľovanie potom vyzerá nasledovne:

  1. Príprava: Vytvoríme si buffer s jedinečnými informáciami o instancovaných objektoch (vykonávame len raz)
  2. Vymastíme celú obrazovku (tohto kroku sa nedá zbaviť)
  3. Nastavíme ručne index a vertex buffer instancovaného modelu
  4. Nastavíme ručne druhý vertex buffer, ktorý obsahuje rozdielne vlastnosti
  5. Nastavíme spoločné efektu vlastnosti (matica view a projection a trebárs i textúru)
  6. Nastavíme efekt ako aktívny
  7. Pre všetkých instancované objekty vykonávame - vykresľujú objekty pomocou informácií v druhom vertex bufferu
Je vidieť že počet krokov, ktoré sa opakujú pre každý z modelov, sa zmenšil prakticky len na vykreslenie. Fakticky hovoríme karte: "Chcem to vykresliť tam, tam a tam". Táto technika má ale aj svoje negatíva, nemôžeme použiť pripravený BasicEffect, musíme si teda napísať shader vlastné. Keďže navyše neviete, čo to je shader a ani ako sa vytvára, budete mi musieť veriť, že viem, čo robím :-) Hrozná predstava že, ale myslím, že čoskoro napíšem niečo o tom, ako grafická karta funguje alebo skôr o tom, ako si myslím, že grafická karta a shadery fungujú.

Budeme ich potrebovať pre osvetľovanie a taky pre postprocesorové efekty, ale to opäť predbiehajú. Celý článok je založený na tomto návode: http://sciencefact.co.uk/...ng-in-xna-4/

Dosť bolo rozprávanie, ideme na vec. Napadli ma dve možnosti, ako to napísať tak, aby sa to hodilo do enginu. Môžeme urobiť špeciálne herné okno, kde zakaždým keď pridáme model tak sa správne priradí do skupiny a bude sa instanciovat. A alebo môžeme vytvoriť nový komponent. Nakoniec som zvolil komponent. Síce to nebude tak prívetivé na používanie, ale zas to bude ladiť s okolím. Do enginu, do zložky s komponentmi si pridáme novú triedu, ja som ju nazval InstancedModel3D. Urobíme jej verejnú a opravíme menný priestor. Ako východiskový triedu použijeme triedu Component. Pridáme si premennú s menom súboru s modelom:

public string ModelName{
  get;
  protected set;
}

A tiež premennú, kde si uložíme naložené model:

protected Model Model;

V konstruktoru odovzdáme meno modelu, ktorý budeme instanciovat:

public InstancedModel3D(string name){
  ModelName = name;
}

Prepíšeme si metódu Load od predka, stále nič neznámeho, a v nej načítame model:

protected override void Load(){
  Model = Parent.Engine.Content.Load<Model>(ModelName);
}

Bohužiaľ nie je možné k instanciování používať priamo triedu Model, budeme z nej teda potrebovať dostať vertexy a indexy. Pridáme si pár nových vlastností do našej triedy:

protected VertexBuffer Verticles;
protected IndexBuffer Indicies;

Vertex buffer sa bude hodiť určite, do neho si uložíme vrcholy nášho modelu. A aby bolo navrhované pospájať, pridáme aj buffer s indexmi. Obaja si ich v metóde Load naplníme, ale najprv musíme mať čím. Do metódy Load si pridáme dva pomocné listy, do ktorých budeme vertexy a indexy ukladať:

List<VertexPositionNormalTexture> vert = new List<VertexPositionNormalTexture>();
List<ushort> ind = new List<ushort>();

U indexov použijeme typ ushort, teda unsigned short, čo je 16-bitové celé číslo bez znamienka. Vertexy si uložíme is normálou, môže sa nám neskôr pri tvorbe pokročilých efektov hodiť. Ďalej si tiež nakopírujeme lokálnej transformácie modelu do známeho poľa:

Matrix[] transforms=new Matrix[Model.Bones.Count];
Model.CopyAbsoluteBoneTransformsTo(transforms);

Máme všetko pripravené, môžeme ísť na extrakciu dát. Budeme musieť prejsť všetky súčasti modelu a dáta z nich dostať:

foreach (ModelMesh mesh in Model.Meshes){
  foreach (ModelMeshPart part in mesh.MeshParts){
    //dalsi kod sem
  }
}

Model, ako už viete, sa člení na jednotlivé navzájom hierarchicky usporiadané časti, ktorým hovoríme meshe. Navyše sa každá mesh skladá z niekoľkých častí, aj keď zvyčajne to je len jedna. Ale pri zložitejších modeloch to nemusí platiť, preto musíme prejsť foreach cyklom úplne všetky. Vytvoríme si opäť dve pomocné polia, do ktorých presunieme obsah z bufferov:

VertexPositionNormalTexture[] partVerts = new VertexPositionNormalTexture[part.VertexBuffer.VertexCount];

a

ushort[] partIndices = new ushort[part.IndexBuffer.IndexCount];

Veľkosti polí si berieme z oboch bufferov. Ako prvý si naplníme vertexy:

part.VertexBuffer.GetData(partVerts);

A práve získané vertexy si musíme ešte poupraviť. Teraz ich máme, povedal by som, v surovej podobe. Musíme na ne aplikovať transformácie, ktoré sme si dopredu uložili. Je to rovnaký proces, ktorý robíme, keď model vykresľujú, takže nič moc nové pod slnkom:

for (int i = 0; i < partVerts.Length; i++){
  partVerts[i].Position = Vector3.Transform(partVerts[i].Position, transforms[mesh.ParentBone.Index]);
}

Potom nám už zostáva ich len uložiť do pomocného listu:

vert.AddRange(partVerts);

Obdobne naložíme s indexmi, len je nie je potrebné nijako upravovať, takže je len vyzdvihneme a uložíme do listu:

part.IndexBuffer.GetData(partIndices);
ind.AddRange(partIndices);

A je to, najčernejšie práca je prakticky hotová. Už len stačí vytvoriť zo získaných dát nové buffer:

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());

Obaja buffer zostanú po celú dobu nezmenené. Preto nie je potrebné ich mať ako dynamické. Ešte si z modelu vytiahneme textúru, ktorú používa, a tú si uložíme niekam bokom:

protected Texture2D Texture;
Texture = ((BasicEffect)Model.Meshes[0].Effects[0]).Texture;

Áno má to celé háčik, komplexnejšie modely sa nám takto jednoducho ošéfovat nepodarí. Modely, ktoré používajú viac ako jednu textúru takto nenainstanciujeme. Ale pre jednoduchosť to takto ponecháme.

Gratulujem, máme za sebou asi to najhoršie. Teraz už len tie krajšie veci. Ako je v úvode spomenuté, potrebujeme viac ako jeden buffer s vertexy. Je potrebné ešte jeden ďalší, kde budeme mať odlišné vlastnosti. A keďže sa predpokladá, že ich budeme programovo meniť, tak ich budeme ukladať v dynamickom bufferu:

protected DynamicVertexBuffer Primitives;

Bude potreba taky určiť maximálny počet kópií. Preto si pridáme:

public int MaxCount{
  get;
  protected set;
}

A tiež aktuálny počet kópií:

public int Count{
  get;
  protected set;
}

Do konstruktoru k menu modelu pridáme maximálny počet kópií:

public InstancedModel3D(string name,int max){
  ModelName = name;
  MaxCount = max;
}

Tu sa pre dnešné diel zastavíme. Triedu si dokončíme nabudúce. Zatiaľ očakávam otázky, nápady sťažnosti a priania no však veľmi dobre viete kde - v komentároch.


 

Predchádzajúci článok
3D bludisko v XNA - Vertex a index buffer
Všetky články v sekcii
3D bludisko v XNA
Preskočiť článok
(neodporúčame)
3D bludisko v XNA - Hardvérové instancování druhýkrát
Článok pre vás napísal vodacek
Avatar
Užívateľské hodnotenie:
Ešte nikto nehodnotil, buď prvý!
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.
Aktivity