Vydělávej až 160.000 Kč měsíčně! Akreditované rekvalifikační kurzy s garancí práce od 0 Kč. Více informací.
Hledáme nové posily do ITnetwork týmu. Podívej se na volné pozice a přidej se do nejagilnější firmy na trhu - Více informací.

Lekce 15 - 3D bludiště v XNA - Hardwarové instancování podruhé

Vítejte po dvacáté páté. V minulé lekci, 3D bludiště v XNA - Hardwarové instancování poprvé, jsme se podívali na první optimalizaci a představili si instancování.

V tomto díle si dokončíme rozdělanou komponentu. Nebudu tedy zdržovat, jdeme na to. Kopie si budeme chtít skladovat v Listu, který nám usnadní manipulaci s nimi. Aby to bylo možné udělat, budeme potřebovat učinit třídu generickou. Chceme, aby se daly vkládat jen a pouze instance jednoho typu vertexu. Nestačí tedy pouze používat rozhraní. Z třídy uděláme generickou takto:

public class InstancedModel3D<T> : Component where T : struct, IvertexType

Všechny vertexy musejí implementovat rozhraní IVertexType a zároveň jsou strukturami. Nyní si už můžeme vytvořit List:

protected List<T> PrimitivesList;

A v konstruktoru jej vytvořit:

PrimitivesList = new List<T>(MaxCount);

Přidáme taky metody pro přidání a odebrání kopie, nedělají nic moc specifického, prostě manipulují s daty v listu:

public virtual void AddPrimitive(T obj){
  PrimitivesList.Add(obj);
}
public virtual void RemovePrimitive(T obj){
  PrimitivesList.Remove(obj);
}

Tak sice uděláme potřebné změny, ale na grafická karta o nich zatím neví. Není tedy nic snadnějšího, než vytvořit metodu Apply, která údaje aktualizuje. Tento přístup jsem zvolil, aby se minimalizoval počet komunikace s grafickou kartou. Zavoláme si metodu jen tehdy, když budeme mít všechny změny hotovy. Vypadá následovně:

public virtual void Apply(){
  if (Primitives == null && PrimitivesList.Count > 0){
    Primitives = new DynamicVertexBuffer(Parent.Engine.GraphicsDevice, PrimitivesList[0].VertexDeclaration, MaxCount, BufferUsage.None);
  }
  Primitives.SetData<T>(PrimitivesList.ToArray(), 0, PrimitivesList.Count);
  Count = PrimitivesList.Count;
}

Prvně si zkontrolujeme, zda-li je náš dynamický buffer vytvořen a pokud není a máme co do něj přidávat, tak jej vytvoříme. Pak už do něj jen nastavíme data. Opravdu nic moc složitého.

V úvodu jsem naznačil, že nebudeme moci používat vestavěnou třídu BasicEffect, ale budeme si muset napsat shader vlastní. Jelikož jsme na toto téma zatím nenarazili, tak zde přidám soubor s hotovým shaderem. Bude stačit ho jen do projektu vložit. Přidáme si proměnné:

protected string effectName;
protected Effect Effect;

V konstruktoru si předáme poslední parametr a to název používaného efektu, celý konstruktor tedy bude vypadat následovně:

public InstancedModel3D(string name,int max,string effect){
  ModelName = name;
  MaxCount = max;
  effectName = effect;
  PrimitivesList = new List<T>(MaxCount);
}

A v metodě Load ho stejně jako vše ostatní nahrajeme:

Effect = Parent.Engine.Content.Load<Effect>(effectName);

A teď si dodáme metodu pro vykreslování. Je to poslední střípek skládačky a vlastně dost možná ten nejdůležitější. Přepíšeme si tedy metodu Draw:

public override void Draw(Matrix View, Matrix Projection, Vector3 CameraPosition)

První, co musíme provést, je zkontrolovat, zda-li je vůbec co vykreslovat:

if (Primitives == null || Count == 0) return;

Dále musíme nastavit společné parametry efektu. Není to ale tak pěkné jako u BasicEffectu, ale co naděláme:

Effect.Parameters["View"].SetValue(View);
Effect.Parameters["Projection"].SetValue(Projection);
Effect.Parameters["Texture"].SetValue(Texture);

Pošleme změny do grafické karty:

Effect.CurrentTechnique.Passes[0].Apply();

Nyní potřebujeme nastavit naše buffery jako aktivní. U indexů není žádný problém:

Parent.Engine.GraphicsDevice.Indices = Indicies;

Ale nastavení vertexů už není intuitivní. Použijeme pomocnou strukturu VertexBufferBinding, která obsahuje další parametry jako je offset. A aby nedocházelo k vytváření nepořádku protože tato metoda se volá až 60x za vteřinu, tak si vytvoříme privátní pomocné pole:

private readonly VertexBufferBinding[] binding = new VertexBufferBinding[2];

V metodě Load přidáme jako nultý prvek buffer s vertexy:

binding[0] = new VertexBufferBinding(Verticles);

A do metody Apply při vytváření bufferu pro kopie přidáme další prvek do tohoto pole:

binding[1] = new VertexBufferBinding(Primitives,0,1);

Nyní už můžeme toto pole využít a nastavit oba buffery:

Parent.Engine.GraphicsDevice.SetVertexBuffers(binding);

A konečně se dostáváme k poslednímu kroku a to je samotný příkaz vykresli:

Parent.Engine.GraphicsDevice.DrawInstancedPrimitives(PrimitiveType.TriangleList,0,0, Verticles.VertexCount,0,Indicies.IndexCount/3,Count);

Podívejme se na parametry. První nám stejně jako vždycky říká, že budeme kreslit trojúhelníky. Pak následují dvě nuly značící offset, další parametr je počet vertexů, které budeme vykreslovat. Další nula tentokráte offset pro indexy, pak je tam počet trojúhelníků které vykreslujeme (ano, každý má 3 vrcholy, proto dělíme třemi) a na závěr počet kopií.

Uff. Doufám, že jsem na nic nezapomněl. Nyní si zkusíme komponentu použít. Prvně si musíme vytvořit vlastní vertex, ve kterém budeme mít unikátní parametry pro každou kopii. Já jsem si tuto třídu pojmenoval InstanceDataVertex, ale znáte to, na jménu přece nezáleží. Přidejte si tedy tuto a nebo podobně pojmenovanou třídu do bludiště. Z třídy udělejte strukturu, také by měla být veřejná a navíc musí implementovat rozhraní IVertexType:

public struct InstanceDataVertex: IvertexType{

Klikněte na jméno rozhraní a v seznamu vyberte implement interface explicitly. Pro každou kopii budeme ukládat její matici World a taky barvu. Přidáme si tedy obě proměnné:

public Matrix World;
public Color Color;

Dejte si pozor na pořadí, musí být přesně takové, jaké jsem uvedl, jinak to nebude fungovat. V konstruktoru si je přiřadíme:

public InstanceDataVertex(Matrix world,Color color){
  World = world;
  Color = color;
}

Aby byla struktura kompletní, potřebujeme takzvanou deklaraci vertexu. Co to přesně je a jak to funguje si necháme na později, až se dostaneme ke grafické kartě. Ale pokud by někdo toužil po poznání, odkazuji vás na tento pěkný, byť anglický článek. Takže zatím jako ovečky opisujte:

public readonly static VertexDeclaration VertexDeclaration = new VertexDeclaration(
  new VertexElement(0, VertexElementFormat.Vector4, VertexElementUsage.TextureCoordinate, 3),
  new VertexElement(sizeof(float) * 4,VertexElementFormat.Vector4, VertexElementUsage.TextureCoordinate,4),
  new VertexElement(sizeof(float) * 8,VertexElementFormat.Vector4, VertexElementUsage.TextureCoordinate,5),
  new VertexElement(sizeof(float) * 12,VertexElementFormat.Vector4, VertexElementUsage.TextureCoordinate,6),
  //barva
  new VertexElement(sizeof(float) * 16,VertexElementFormat.Color, VertexElementUsage.Color,0)
);

A ve vygenerovaném getteru si deklaraci vrátíme:

return InstanceDataVertex.VertexDeclaration;

Optimálně by měla naše struktura obsahovat i metodu, která vrátí velikost v bajtech, tak přidáme i ji:

public static readonly int SizeInBytes = sizeof(float)*(16 + 4);

A ta je pokud dobře počítám takováto. Strukturu mám hotovu. Nyní se přesuneme do třídy s mapou. Budeme chtít instanciovat pro začátek pouze podlahy, pro ostatní objekty které hrají roli v kolizích si budeme muset napsat komponentu novou. Konkrétně do metody Nacti, kde si pod volání metody Promaz přidáme:

InstancedModel3D<InstanceDataVertex> podlahy = new InstancedModel3D<InstanceDataVertex>("podlaha", 50, "instancedeffect");

Kde podlaha je název modelu naší podlahy, 50 maximální počet kopií (toto číslo je potřeba vhodně zvolit podle velikosti bludiště) a instancedeffect je jméno shaderu, pomocí kterého budeme vykreslovat. Do switche si do větve pro nulu přidáme:

podlahy.AddPrimitive(new InstanceDataVertex(Utility.CreateWorld(new Vector3(i * 20 + 10, 0, (j-1) * 20 + 10),Matrix.Identity,new Vector3(1.34f)),Color.White));

A naopak zakomentujeme vytváření komponenty podlahy. Poslední, co musíme provést, je komponentu přidat do enginu. To provedeme v metodě úplně dole:

Parent.AddComponent(podlahy);

A zavoláme metodu Apply:

podlahy.Apply();

Hotovo. Ještě si do projektu přidejte soubor se shaderem. Najdete ho v archivu dole pod článkem. Prozatím to bude taková temná neprůhledná krabička, ale není všem dnům konec. Docela si i myslím, že máme jeden z automatických systémů, které se snadno používají. Pro malá bludiště sice nemá moc velký význam, ale jakmile se bludiště rozroste, už je rozdíl velmi citelný. Pro dnešek je to tedy vše, opět jako vždycky se budu těšit na otázky, nápady a dotazy dole pod článkem.

A co bude příště? Dořešíme kolize, 3D bludiště v XNA - Kolize počtvrté a opravdu ne naposled.


 

Měl jsi s čímkoli problém? Stáhni si vzorovou aplikaci níže a porovnej ji se svým projektem, chybu tak snadno najdeš.

Stáhnout

Stažením následujícího souboru souhlasíš s licenčními podmínkami

Staženo 146x (1.78 MB)
Aplikace je včetně zdrojových kódů v jazyce C# XNA

 

Předchozí článek
3D bludiště v XNA - Hardwarové instancování poprvé
Všechny články v sekci
3D bludiště v XNA
Přeskočit článek
(nedoporučujeme)
3D bludiště v XNA - Kolize počtvrté a opravdu ne naposled
Článek pro vás napsal vodacek
Avatar
Uživatelské hodnocení:
Ještě nikdo nehodnotil, buď první!
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