16. díl - 3D bludiště v XNA - Instancujeme kolizní objekty

C# .NET XNA game studio 3D bludiště 3D bludiště v XNA - Instancujeme kolizní objekty

Vítejte po dvacáté sedmé. Posledně jsem sliboval oddychové téma. Ale jelikož se mi zadařilo poměrně hodně postoupit s instancováním, které jsme probírali v dílech 25 a 26, tak ještě jeden díl se na ně podíváme. Obdržel jsem otázku k čemu je to jako dobré a jestli nemám nějaká čísla. Popravdě jsme nevěděl co odpovědět, ale můj včerejší výzkum mi pár čísel přinesl. Takže sem s nimi:

Máme testovací bludiště 30x30 polí, tedy 900 objektů, které musíme pokaždé vykreslovat. Se základním naivním kódem jsem obdržel nelichotivých 40 FPS. Což v překladu znamená že docházelo k vykreslování 40x za vteřinu. Hra se již viditelně hlavně při otáčení kamery sekala. Stačilo instanciovat podlahy a už jsem byl na 160-ti FPS. Když jsem přidal i zdi s použitím kódu, který vytvoříme nyní (zatím to nelze kvůli kolizím) byl jsem na 240-ti a když jsem zainstancoval i díry (kdo hrál bludiště v ostré verzi, tak zná) tak jsem ze svého 3 roky starého počítače vytáhl 360 FPS. Tedy asi 9x rychleji než původní kód a to myslím stojí za trochu námahy.

Instancované kolidovatelné modely

Jak jsem již naznačil výše, budeme potřebovat instanciovat i zdi, přitom ale chceme zachovat funkční kolize. Budeme na to potřebovat novou třídu, já jsme ji nazval CollidableInstan­cedModel3D. Ale znáte moji představivost. Učiníme ji veřejnou. Dědit budeme od třídy InstancedModel3D a opět s použitím genericity:

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

Co vlastně do třídy potřebujeme propašovat? No, asi nějakou kolizní kůži. Nejlepší řešení (podle mě) je vytvořit z brusu novou kolizní kůži, ve které bude seznam krabic. Přidejme si novou třídu MultipleBoxCollisionSkin, dědíme od třídy CollisionSkin. Uvnitř přidáme List pro krabice:

protected List<BoundingBox> Boxes;

V konstruktoru jej vytvoříme:

public MultipleBoxCollisionSkin(){
  Boxes = new List<BoundingBox>();
}

Potřeba budou také dvě metody. Jedna při přidávání krabice a druhá pro odebírání krabice na patřičném indexu. Proč zrovna takto uvidíte za chviličku:

public void AddBox(BoundingBox b){
  Boxes.Add(b);
}

public void RemoveBox(int index){
  Boxes.RemoveAt(index);
}

Potřeba bude také přepsat metodu Intersects, jednoduše projedeme všechny krabice v listu a zkontrolujeme u nich, zda-li nedochází ke kolizi:

public override bool Intersects(BoundingSphere sp){
  foreach (BoundingBox b in Boxes){
    if (b.Intersects(sp)) return true;
  }
  return false;
}

Pokud kolidujeme byť s jednou, tak ke kolizi dochází a cyklus ukončujeme. Stejně naložíme s metodou Draw:

public override void Draw(){
  foreach (BoundingBox b in Boxes){
    BoundingRenderer.Render(b, Manager.Parent.Kamera.View, Manager.Parent.Kamera.Projection, LastCollision ? Color.Red : Color.Black);
  }
}

Jediným problémem je metoda Transform. Jak celou tuto soustavu přesunout na jiné místo si zatím nejsem jistý, proto ji sice přepíšeme, ale ponecháme ji prázdnou. Případně si sem můžete vložit zavolání výjimky:

public override void Transform(Vector3 meritko, Vector3 pozice, Matrix rotace){
}

Kolizní kůži máme tedy hotovou. Vraťme se do třídy, která je naším hlavním zájmem. Přidáme si konstruktor a v něm si naši kůži vytvoříme:

protected MultipleBoxCollisionSkin Skin;

public CollidableInstancedModel3D(string model, int max, string effect) : base(model, max, effect){
  Skin = new MultipleBoxCollisionSkin();
}

Budeme potřebovat také kolizní krabici pro jeden model, právě tu budeme přidávat do nově vytvořené kůže:

protected BoundingBox Box;

A v metodě Load ji z modelu vytáhneme:

protected override void Load(){
  base.Load();
  Matrix[] transformace = new Matrix[Model.Bones.Count];
  Model.CopyAbsoluteBoneTransformsTo(transformace);
  Box = Utility.VypoctiBoundingBox(Model, transformace);
}

Máme vytvořenou kůži, takže už nám stačí ji jen zaregistrovat do kolizního manažeru, uděláme to stejně jako s modelem:

public override void OnAdded(){
  base.OnAdded();
  if (Parent is CollidableGameScreen){
    CollidableGameScreen gs = Parent as CollidableGameScreen;
    gs.CollisionManager.AddBox(Skin);
  }
}

A stejně kód pro odebírání:

public override void OnRemoved(GameScreen okno){
  base.OnRemoved(okno);
  if (okno is CollidableGameScreen){
    CollidableGameScreen gs = okno as CollidableGameScreen;
    gs.CollisionManager.RemoveBox(Skin);
  }
}

Přepíšeme také metodu pro přidávání instanciovaného objektu. Pokaždé, když objekt přidáme, tak s ním přidáme i kůži. Je tu jen pár problémů, které musíme překonat. Nejprve musíme krabici transformovat na požadované místo. Jak známo tuto informaci obsahuje vkládaný vertex, ale jak to z něj dostat? Nad jednotlivými možnostmi jsem přemýšlel poměrně dlouho. Nakonec nejlepším řešením se ukázalo přidat rozhraní, kde předepíšeme metodu pro vrácení matice World. Nazval jsem ho IInstanceVertexType a vypadá takto:

public interface IInstanceVertexType : IVertexType{
  Matrix GetWorld();
}

Nezapomeňme upravit požadavek na typ u genericity u obou tříd:

public class CollidableInstancedModel3D<T>: InstancedModel3D<T> where T: struct, IinstanceVertexType

a

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

Ve struktuře s naším typem vertexu jen přehodíme rozhraní a metodu naimplementujeme:

public Matrix IInstanceVertexType.GetWorld(){
  return World;
}

Ještě upotřebíme metodu pro transformaci krabice s pomocí matice, umístíme ji do třídy Utility. Funguje stejně jako metoda předešlá, vyextrahujeme všechny body krabice a aplikujeme na ně transformaci, potom z nich složíme novou krabici. Jen zde zdůrazním, že nechceme změnit nic v původní krabici:

public static BoundingBox Transform(BoundingBox box, Matrix transform){
  BoundingBox bb;

  Vector3[] body = new Vector3[8];
  box.GetCorners(body);
  for (int i = 0; i < body.Length; i++){
    body[i] = Vector3.Transform(body[i], transform);
  }

  bb = BoundingBox.CreateFromPoints(body);
  return bb;
}

Konečně máme vše připraveno a můžeme kůži přidat:

public override void AddPrimitive(T obj){
  base.AddPrimitive(obj);
  Skin.AddBox(Utility.Transform(Box,obj.GetWorld()));
}

Odebírání kůží je o něco málo složitější, musíme najít index, na kterém se objekt nalézá a na stejném indexu musíme odebrat i z kůží. Proto jsme v kůži implementovali odebírání přes index:

public override void RemovePrimitive(T obj){
  int id = PrimitivesList.IndexOf(obj);
  base.RemovePrimitive(obj);
  Skin.RemoveBox(id);
}

Tím by se dalo říct, že je hotovo. Ovšem není to pravda. Pokud se budeme snažit přidat kopie a nebudeme mít komponentu načtenou, tak nám program spadne. Nebudeme totiž mít načtený model a tedy ani vytvořenou základní krabici. Můžeme to lehce poupravit. Do přidávání kopií vložíme podmínku:

if(!Loaded)Skin.AddBox(Utility.Transform(Box,obj.GetWorld()));

A do metody Load přidáme foreach cyklus:

foreach (T obj in PrimitivesList){
  Skin.AddBox(Utility.Transform(Box, obj.GetWorld()));
}

Nyní zbývá už jen a pouze vše zprovoznit. Otevřete si soubor s mapou a zde stejně jako v případě podlah si vytvoříme proměnnou:

CollidableInstancedModel3D<InstanceDataVertex> zdi = new CollidableInstancedModel3D<InstanceDataVertex>("zed", 50, "instancedeffect");

A do switche do větve, kde jsme přidávali zeď, přidáme stejně jako u podlah novou kopii:

zdi.AddPrimitive(new InstanceDataVertex(Utility.CreateWorld( new Vector3(i * 20 + 10, 0, j * 20 + 10), Matrix.Identity, new Vector3(1.34f)), Color.Green));

Nesmíme zapomenout přidat komponentu do enginu a zdi zobrazit:

Parent.AddComponent(zdi);
zdi.Apply();

Ještě zakomentovat přidání obyčejné zdi a máme hotovo. Gratuluji, právě jsme do enginu přidali instanciované kolizní objekty. A ani to moc doufám nebolelo. Pokud nyní hru spustíte tak... se moc daleko nedostanete. Velikost bufferu je menší, než počet přidávaných zdí. Můžeme tomu čelit dvěma způsoby. Buď nedovolit přidat více než je limit a nebo buffer nafouknout. Já jsem zvolil nafouknutí. Předně přidáme do metody Apply podmínku na nulový počet prvků:

if (PrimitivesList.Count == 0) return;

Na tu jsme zapomněli posledně. Do podmínky při prvním vytvoření bufferu nastavíme velikost na maximum. Bude to vypadat nějak takhle:

if (Primitives == null){
  MaxCount = Math.Max(MaxCount, PrimitivesList.Count);
  Primitives = new DynamicVertexBuffer(Parent.Engine.GraphicsDevice, PrimitivesList[0].VertexDeclaration, MaxCount, BufferUsage.None);
  binding[1] = new VertexBufferBinding(Primitives, 0, 1);
}

A přidáme též podmínku na přetečení bufferu, pokud jej máme již vytvořený:

if (MaxCount < PrimitivesList.Count){
  MaxCount = PrimitivesList.Count;
  Primitives = new DynamicVertexBuffer(Parent.Engine.GraphicsDevice, PrimitivesList[0].VertexDeclaration, MaxCount, BufferUsage.None);
  binding[1] = new VertexBufferBinding(Primitives, 0, 1);
}

Nyní už by vše mělo fungovat tak jak má. Gratuluji. Máme vše co se dalo nainstanciované. Příště si už dáme něco oddychového. S instanciováním jsme doufám skončili. Opět budu čekat na otázky, nápady, no prostě na komentáře.


 

Stáhnout

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

 

  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 (1 hlasů) :
55555


 



 

 

Komentáře

Avatar
magic44
Redaktor
Avatar
magic44:

Ahoj, delam 3D hru a najednou se mi hra zacala v. ob. sekat.. Tak jsem si řekl, že zkusím tohle.. noo možná jsem se měl spokojit s tím co sem měl a to co mi tu hru sekalo tam nedavat :D. Jinak dobrej tutorial.
Akorát mi CollidableInstan­ceModel3D nekoliduje. Zkusil jsem do mé hry dát tvůj kód, ale ani ten tam nefungoval. Nevíš čím by to mohlo být?

Odpovědět 11.3.2014 18:57
Moudrý člověk nechce být lepší než ostatní, ale lepší, než byl sám včera.
Avatar
vodacek
Redaktor
Avatar
Odpovídá na magic44
vodacek:

bez toho aniž bych to viděl tak těžko

 
Odpovědět 11.3.2014 20:35
Avatar
magic44
Redaktor
Avatar
Odpovídá na vodacek
magic44:

Hm.. tak tady je snad vše co je potřeba k instancování, je tohe ale nějak moc.

public struct InstanceDataVertex:InstanceVertexType
    {
        public Matrix World;
        public Color Color;

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

        VertexDeclaration IVertexType.VertexDeclaration
        {
            get { return InstanceDataVertex.VertexDeclaration; }
        }

        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),
            new VertexElement(sizeof(float) * 16, VertexElementFormat.Color, VertexElementUsage.Color, 0));

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

        Matrix InstanceVertexType.GetWorld()
        {
            return World;
        }
    }
public interface InstanceVertexType:IVertexType
    {
        Matrix GetWorld();
    }
public class InstancedModel3D<T>:Component where T : struct, InstanceVertexType
    {
        public string ModelName;
        public Model Model;
        protected VertexBuffer Verticles;
        protected IndexBuffer Indicies;
        protected Texture2D Texture;

        protected DynamicVertexBuffer Primitives;
        public int MaxCount;
        public int Count;

        protected List<T> PrimitiveList;

        protected string effectName;
        protected Effect Effect;

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

        public InstancedModel3D(string nazev, int max, string effect)
        {
            ModelName = nazev;
            MaxCount = max;
            this.effectName = effect;
            PrimitiveList = new List<T>(MaxCount);
        }

        protected override void Load()
        {
            Model = Parent.Engine.ContentManager.Load<Model>(ModelName);
            Effect = Parent.Engine.ContentManager.Load<Effect>(effectName);
            List<VertexPositionNormalTexture> vert = new List<VertexPositionNormalTexture>();
            List<ushort> ind = new List<ushort>();

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

            foreach (ModelMesh mesh in Model.Meshes)
            {
                foreach (ModelMeshPart part in mesh.MeshParts)
                {
                    VertexPositionNormalTexture[] partVerts = new VertexPositionNormalTexture[part.VertexBuffer.VertexCount];
                    part.VertexBuffer.GetData(partVerts);

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

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

                    vert.AddRange(partVerts);
                }
            }

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

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

            binding[0] = new VertexBufferBinding(Verticles);
        }

        public override void Draw(Matrix View, Matrix Projection, Vector3 CameraPosition)
        {
            if (Primitives == null || Count == 0)
                return;
            Effect.Parameters["View"].SetValue(View);
            Effect.Parameters["Projection"].SetValue(Projection);
            //Effect.Parameters["TextureEnabled"].SetValue(Texture != null);
            Effect.Parameters["Texture"].SetValue(Texture);

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

            Parent.Engine.GraphicsDevice.SetVertexBuffers(binding);
            Parent.Engine.GraphicsDevice.Indices = Indicies;

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

        public virtual void Apply()
        {
            if (PrimitiveList.Count == 0)
                return;
            if (Primitives == null)
            {
                MaxCount = Math.Max(MaxCount, PrimitiveList.Count);
                Primitives = new DynamicVertexBuffer(Parent.Engine.GraphicsDevice, PrimitiveList[0].VertexDeclaration, MaxCount, BufferUsage.None);
                binding[1] = new VertexBufferBinding(Primitives, 0, 1);
            }
            if (MaxCount < PrimitiveList.Count)
            {
                MaxCount = PrimitiveList.Count;
                Primitives = new DynamicVertexBuffer(Parent.Engine.GraphicsDevice, PrimitiveList[0].VertexDeclaration, MaxCount, BufferUsage.None);
                binding[1] = new VertexBufferBinding(Primitives, 0, 1);
            }
            /*if (Primitives == null && PrimitiveList.Count > 0)
            {
                Primitives = new DynamicVertexBuffer(Parent.Engine.GraphicsDevice, PrimitiveList[0].VertexDeclaration, MaxCount, BufferUsage.None);
                binding[1] = new VertexBufferBinding(Primitives, 0, 1);
            }*/
            Primitives.SetData<T>(PrimitiveList.ToArray(), 0, PrimitiveList.Count);
            Count = PrimitiveList.Count;
        }

        public virtual void AddPrimitives(T obj)
        {
            PrimitiveList.Add(obj);
        }

        public virtual void RemovePrimitives(T obj)
        {
            PrimitiveList.Remove(obj);
        }
    }
public class CollidableInstanceModel3D<T>: InstancedModel3D<T> where T: struct, InstanceVertexType
    {
        MultipleBoxCillisionSkin Skin;
        protected BoundingBox Box;

        public CollidableInstanceModel3D(string model, int max, string effect)
            : base(model, max, effect)
        {
            Skin = new MultipleBoxCillisionSkin();
        }

        protected override void Load()
        {
            base.Load();
            Matrix[] transformace = new Matrix[Model.Bones.Count];
            Model.CopyAbsoluteBoneTransformsTo(transformace);
            Box = Utility.VypoctiBountingBox(Model, transformace);

            foreach (T obj in PrimitiveList)
            {
                Skin.AddBox(Utility.Transform(Box, obj.GetWorld()));
            }
        }

        public override void OnAdded()
        {
            base.OnAdded();
            if (Parent is CollidableGameScreen)
            {
                CollidableGameScreen cg = Parent as CollidableGameScreen;
                cg.CollisionManager.AddBox(Skin);
            }
        }

        public override void OnRemoved(GameScreen okno)
        {
            base.OnRemoved(okno);
            if (okno is CollidableGameScreen)
            {
                CollidableGameScreen o=okno as CollidableGameScreen;
                o.CollisionManager.RemoveBox(Skin);
            }
        }

        public override void AddPrimitives(T obj)
        {
            base.AddPrimitives(obj);
            if (!Loaded)
                Skin.AddBox(Utility.Transform(Box, obj.GetWorld()));
        }

        public override void RemovePrimitives(T obj)
        {
            int id = PrimitiveList.IndexOf(obj);
            base.RemovePrimitives(obj);
            Skin.RemoveBox(id);
        }
    }
public class MultipleBoxCillisionSkin:CollisionSkin
    {
        protected List<BoundingBox> Boxes;
        BoundingBox Box;

        public MultipleBoxCillisionSkin()
        {
            Boxes = new List<BoundingBox>();
        }

        public void AddBox(BoundingBox b)
        {
            Boxes.Add(b);
        }

        public void RemoveBox(int index)
        {
            Boxes.RemoveAt(index);
        }

        public override bool Intersects(BoundingSphere sp)
        {
            foreach (BoundingBox b in Boxes)
            {
                if (b.Intersects(sp))
                    return true;
            }
            return false;
        }

        public override void Draw()
        {
            foreach (BoundingBox b in Boxes)
            {
                BoundingRenderer.Render(b, Manager.Parent.Kamera.View, Manager.Parent.Kamera.Projection, LastCollision ? Color.Red : Color.Black);
            }
        }

        public override void Transform(Vector3 Meritko, Vector3 Pozice, Matrix Rotace)
        {

        }
    }
Odpovědět 11.3.2014 21:35
Moudrý člověk nechce být lepší než ostatní, ale lepší, než byl sám včera.
Avatar
vodacek
Redaktor
Avatar
Odpovídá na magic44
vodacek:

a vykreslujou se ty kolizní krabice?

 
Odpovědět 11.3.2014 21:40
Avatar
magic44
Redaktor
Avatar
Odpovídá na vodacek
magic44:

Myslíš, to když je kolize, tak jsou červený? Ne ty se mi nezobrazujou, ale mě se nikdy nezobrazovali ani u CollidableModel3D a ten funguje.

Odpovědět 11.3.2014 22:51
Moudrý člověk nechce být lepší než ostatní, ale lepší, než byl sám včera.
Avatar
vodacek
Redaktor
Avatar
Odpovídá na magic44
vodacek:

buď tam prostě nejsou a nebo se nevolá kreslící metoda

 
Odpovědět 12.3.2014 13:13
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.

Zobrazeno 6 zpráv z 6.