Sign in to follow this  
ChristianFrantz

reuse 3d primitives

Recommended Posts

Is there any way to make a primitive and use it over and over again? ex: if I make one cube, can I create 100 and make a 10x10 grid? I've tried using a for loop and updating the x and z coords with each loop thru, but it only moves the one cube thats created in the beginning

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

namespace Cube_Chaser
{
    class Cube
    {
        private GraphicsDevice device;
        private Texture2D texture;

        public Vector3 location;

        private Vector3 position;

        private VertexBuffer cubeVertexBuffer;
        private List<VertexPositionTexture> vertices = new List<VertexPositionTexture>();

        public Cube(GraphicsDevice graphicsDevice, Vector3 playerLocation, float minDistance, Texture2D texture)
        {
            device = graphicsDevice;
            this.texture = texture;

            PositionCube(playerLocation, minDistance);

            BuildFace(new Vector3(0, 0, 0), new Vector3(0, 1, 1));
            BuildFace(new Vector3(0, 0, 1), new Vector3(1, 1, 1));
            BuildFace(new Vector3(1, 0, 1), new Vector3(1, 1, 0));
            BuildFace(new Vector3(1, 0, 0), new Vector3(0, 1, 0));

            BuildFaceHorizontal(new Vector3(0, 1, 0), new Vector3(1, 1, 1));
            BuildFaceHorizontal(new Vector3(0, 0, 1), new Vector3(1, 0, 0));

            cubeVertexBuffer = new VertexBuffer(device, VertexPositionTexture.VertexDeclaration, vertices.Count, BufferUsage.WriteOnly);

            cubeVertexBuffer.SetData<VertexPositionTexture>(vertices.ToArray());

            this.position = position;
        }

        private void BuildFace(Vector3 p1, Vector3 p2)
        {
            vertices.Add(BuildVertex(p1.X, p1.Y, p1.Z, 1, 0));
            vertices.Add(BuildVertex(p1.X, p2.Y, p1.Z, 1, 1));
            vertices.Add(BuildVertex(p2.X, p2.Y, p2.Z, 0, 1));
            vertices.Add(BuildVertex(p2.X, p2.Y, p2.Z, 0, 1));
            vertices.Add(BuildVertex(p2.X, p1.Y, p2.Z, 0, 0));
            vertices.Add(BuildVertex(p1.X, p1.Y, p1.Z, 1, 0));
        }

        private void BuildFaceHorizontal(Vector3 p1, Vector3 p2)
        {
            vertices.Add(BuildVertex(p1.X, p1.Y, p1.Z, 0, 1));
            vertices.Add(BuildVertex(p2.X, p1.Y, p1.Z, 1, 1));
            vertices.Add(BuildVertex(p2.X, p2.Y, p2.Z, 1, 0));
            vertices.Add(BuildVertex(p1.X, p1.Y, p1.Z, 0, 1));
            vertices.Add(BuildVertex(p2.X, p2.Y, p2.Z, 1, 0));
            vertices.Add(BuildVertex(p1.X, p1.Y, p2.Z, 0, 0));
        }

        private VertexPositionTexture BuildVertex(float x, float y, float z, float u, float v)
        {
            return new VertexPositionTexture(new Vector3(x, y, z), new Vector2(u, v));
        }

        public void PositionCube(Vector3 playerLocation, float minDistance)
        {
                location = new Vector3(.5f, .5f, .5f);
        }

        public void Draw(Camera camera, BasicEffect effect)
        {
            effect.VertexColorEnabled = false;
            effect.TextureEnabled = true;
            effect.Texture = texture;

            Matrix center = Matrix.CreateTranslation(new Vector3(-0.5f, -0.5f, -0.5f));
            Matrix scale = Matrix.CreateScale(0.05f);
            Matrix translate = Matrix.CreateTranslation(location);

            effect.World = center * scale * translate;
            effect.View = camera.View;
            effect.Projection = camera.Projection;

            foreach (EffectPass pass in effect.CurrentTechnique.Passes)
            {
                pass.Apply();
                device.SetVertexBuffer(cubeVertexBuffer);
                device.DrawPrimitives(PrimitiveType.TriangleList, 0, cubeVertexBuffer.VertexCount / 3);
            }
        }
    }
}

 

 

Share this post


Link to post
Share on other sites
NightCreature83    5002

Sure, you can create one cube and reuse it.
there are a few options:
- create one cube geometry set (VB + IB (optional) ), and render it n times, setting different world transform matrices as constants for each draw call.
- create one cube geometry set and instance it n times with one draw call ( for example, drawIndexedInstanced in d3d11 ). You can then use SV_InstanceID to index into a cb containing an array of transforms, or just have some other simple shader code to offset each vertex based on instance id.

- You can also use GS to do the instancing, but that will almost certainly be alot slower, especially for large amount of instances. ( en example of this though could be have no vertex geometry, call draw with the number of verts set to the number of cubes you want on screen, and then expand each point into a cube in GS )

Second and last options are out seeing this is XNA so no DX11 support, you can still do instancing but you have to look up DX9 instancing instead. See this link for hardware instancing in XNA 4. Depending on how many cubes you are going to draw instancing is your best bet for realizing a decent FPS. DX doesn't like small vertex buffers and slows down dramatically when it is faced with loads of them.

Share this post


Link to post
Share on other sites
ATEFred    1700

Sure, you can create one cube and reuse it.
there are a few options:
- create one cube geometry set (VB + IB (optional) ), and render it n times, setting different world transform matrices as constants for each draw call.
- create one cube geometry set and instance it n times with one draw call ( for example, drawIndexedInstanced in d3d11 ). You can then use SV_InstanceID to index into a cb containing an array of transforms, or just have some other simple shader code to offset each vertex based on instance id.

- You can also use GS to do the instancing, but that will almost certainly be alot slower, especially for large amount of instances. ( en example of this though could be have no vertex geometry, call draw with the number of verts set to the number of cubes you want on screen, and then expand each point into a cube in GS )

Second and last options are out seeing this is XNA so no DX11 support, you can still do instancing but you have to look up DX9 instancing instead. See this link for hardware instancing in XNA 4. Depending on how many cubes you are going to draw instancing is your best bet for realizing a decent FPS. DX doesn't like small vertex buffers and slows down dramatically when it is faced with loads of them.

Heh, I guess I should have checked that it was xna. 

 

If you are just going to be rendering cubes (rather than looking for a unified solution for instancing / drawing loads of arbitrary meshes ), I believe it would be faster to just have one large VB with loads of cubes inside, storing the cube index in the W component of the position for example (if SV_VertexID or equivalent is not available), and then controlling how many you render by specifying different primitive count in the draw call. You can then use the cube index to index to resolve you final vertex positions as discussed above. 
 

Share this post


Link to post
Share on other sites
Norman Barrows    7179

i'm able to do about 11,000 batches of 8 triangles at 30 fps dx9 fixed function.

 

if your cubes don't change every frame, create a static (NOT dynamic) vb and ib and fill it with cubes, then draw that. i use this trick for ground meshes drawn one quad at a time depending on texture. then on top of that, i draw 11,0000 plants, rocks, and alpha blended clouds (thats 11,000 visible on the screen, after all culling).  and thats all done with just 4 plant meshes, 2 rock meshes, and one billboard quad - drawn over, and over again. the trick is to draw them ordered on texture. that way you set each texture in the scene just once.

 

note that indexed will (usually/almost always?) be faster than non-indexed.

Share this post


Link to post
Share on other sites

I read up on DrawUserIndexedPrimitives and I think its what I want because Im going to be drawing a lot of cubes eventually. But I have no idea how to use it within my cube class. I may just rewrite the class entirely lol. Also, the buffer will only change every time the player interacts with it so hopefully this will improve performance

Edited by burnt_casadilla

Share this post


Link to post
Share on other sites
phil_t    8084

DrawIndexedPrimitives (which uses a vertex buffer) will be significantly faster than DrawUserIndexedPrimitives (which sends all your vertex data to the GPU every time) if your cubes only change when the player interacts with them.

Share this post


Link to post
Share on other sites
Norman Barrows    7179

Also, the buffer will only change every time the player interacts with it so hopefully this will improve performance

 

sounds like a cube world. you'll want some sort of world map to tell you what cube is what and where. you use that to generate the static vb and ib. once at game start, and once every time the player changes the world. basically, you have a cube vb and ib in memory (your source mesh). then you have a big system memory vb and ib (your system mesh) that will hold lots of cubes. i use my own data structures for these. then you will have a big static directx or openGL vb and ib that you've allocated in default memory pool. you copy the source mesh into the system mesh multiple times, applying transforms as you go. then you lock the directx/openGL buffer and memcpy the whole system vb and IB to the directx/ openGL vb and ib. then draw to your heart's content til its time to regenerate.

Share this post


Link to post
Share on other sites

The world will be drawn from a text file:

 

"1, 1, 1, 1, 1, 1"

"1, 1, 1, 1, 1, 1"

"1, 1, 1, 1, 1, 1" 

 

that would be three rows of six cubes one cube high

 

new classes

class Cube
{
    private GraphicsDevice device;
    private VertexBuffer cubeVertexBuffer;

    public Cube(GraphicsDevice graphicsDevice)
    {
        device = graphicsDevice;

        var vertices = new List<VertexPositionTexture>();

        BuildFace(vertices, new Vector3(0, 0, 0), new Vector3(0, 1, 1));
        BuildFace(vertices, new Vector3(0, 0, 1), new Vector3(1, 1, 1));
        BuildFace(vertices, new Vector3(1, 0, 1), new Vector3(1, 1, 0));
        BuildFace(vertices, new Vector3(1, 0, 0), new Vector3(0, 1, 0));

        BuildFaceHorizontal(vertices, new Vector3(0, 1, 0), new Vector3(1, 1, 1));
        BuildFaceHorizontal(vertices, new Vector3(0, 0, 1), new Vector3(1, 0, 0));

        cubeVertexBuffer = new VertexBuffer(device, VertexPositionTexture.VertexDeclaration, vertices.Count, BufferUsage.WriteOnly);

        cubeVertexBuffer.SetData<VertexPositionTexture>(vertices.ToArray());
    }

    private void BuildFace(List<VertexPositionTexture> vertices, Vector3 p1, Vector3 p2)
    {
        vertices.Add(BuildVertex(p1.X, p1.Y, p1.Z, 1, 0));
        vertices.Add(BuildVertex(p1.X, p2.Y, p1.Z, 1, 1));
        vertices.Add(BuildVertex(p2.X, p2.Y, p2.Z, 0, 1));
        vertices.Add(BuildVertex(p2.X, p2.Y, p2.Z, 0, 1));
        vertices.Add(BuildVertex(p2.X, p1.Y, p2.Z, 0, 0));
        vertices.Add(BuildVertex(p1.X, p1.Y, p1.Z, 1, 0));
    }

    private void BuildFaceHorizontal(List<VertexPositionTexture> vertices, Vector3 p1, Vector3 p2)
    {
        vertices.Add(BuildVertex(p1.X, p1.Y, p1.Z, 0, 1));
        vertices.Add(BuildVertex(p2.X, p1.Y, p1.Z, 1, 1));
        vertices.Add(BuildVertex(p2.X, p2.Y, p2.Z, 1, 0));
        vertices.Add(BuildVertex(p1.X, p1.Y, p1.Z, 0, 1));
        vertices.Add(BuildVertex(p2.X, p2.Y, p2.Z, 1, 0));
        vertices.Add(BuildVertex(p1.X, p1.Y, p2.Z, 0, 0));
    }

    private VertexPositionTexture BuildVertex(float x, float y, float z, float u, float v)
    {
        return new VertexPositionTexture(new Vector3(x, y, z), new Vector2(u, v));
    }

    public void Draw( BasicEffect effect)
    {
        foreach (EffectPass pass in effect.CurrentTechnique.Passes)
        {
            pass.Apply();
            device.SetVertexBuffer(cubeVertexBuffer);
            device.DrawPrimitives(PrimitiveType.TriangleList, 0, cubeVertexBuffer.VertexCount / 3);
        }
    }
}

 

cube class

 

public class DrawableList<T> : DrawableGameComponent
{
    private BasicEffect effect;
    private Camera camera;
    private class Entity
    {
        public Vector3 Position { get; set; }
        public Matrix Orientation { get; set; }
        public Texture2D Texture { get; set; }
    }

    private Cube cube;
    private List<Entity> entities = new List<Entity>();

    public DrawableList (Game game, Camera camera, BasicEffect effect) 
        : base( game ) 
    {
        this.effect = effect;
        cube = new Cube (game.GraphicsDevice);
        this.camera = camera;
    }

    public void Add( Vector3 position, Matrix orientation, Texture2D texture )
    {
        entities.Add (new Entity() { 
            Position = position,
            Orientation = orientation,
            Texture = texture
        });
    }

    public override void Draw (GameTime gameTime )
    {
        base.Draw (gameTime);

        foreach (var item in entities) {

            effect.VertexColorEnabled = false;
            effect.TextureEnabled = true;
            effect.Texture = item.Texture;

            Matrix center = Matrix.CreateTranslation(new Vector3(-0.5f, -0.5f, -0.5f));
            Matrix scale = Matrix.CreateScale(0.05f);
            Matrix translate = Matrix.CreateTranslation(item.Position);

            effect.World = center * scale * translate;
            effect.View = camera.View;
            effect.Projection = camera.Projection;

            cube.Draw (effect);
        }
    }
}
camera = new Camera (graphics.GraphicsDevice);
        effect = new BasicEffect (graphics.GraphicsDevice);
        cubes = new DrawableList<Cube> (this, camera, effect);

        Components.Add (cubes);

        for (int i=0 ; i < 50; i++)
        {
            cubes.Add (new Vector3( i*0.5f, 50.0f, 50.0f), Matrix.Identity, logoTexture);
        }

Share this post


Link to post
Share on other sites
NightCreature83    5002

That works but in those cases instancing is a far better solution as you already know how many cubes you are going to render, and you will only issue one drawcall, which means effect setup is only run once as well. And instead of setting the World param on each shader effect you pass this into a VB, removing an object then becomes axing the entity and not building that matrix into the instance data buffer. You can use multiple textures and an index passed through the instance data if you need to modify the texture on particular objects that are drawn through instancing.

 

Another minor optimisation in your code would be to calculate the center and scale combined matrix only once and storing that as a static variable of the DrawableList class if you are not intending to change it.

Share this post


Link to post
Share on other sites
VladR    722

note that indexed will (usually/almost always?) be faster than non-indexed

The reason Indexed rendering is faster than Non-Indexed is due to the post-transform vertex cache.

 

But you do have to make an effort to traverse the vertex buffer in a way that maximizes the usage of the post-transform vertex cache. It's not automatic and this is exactly what gets confused all the time when people say they are using Indices, ergo it is faster.
If your index list does not reuse the same vertices within next ~20 indices, you're not making use of a post-transform cache anyway.

 

What was the last time you benchmarked it, anyway ?

Share this post


Link to post
Share on other sites
VladR    722

I successfully drew a 50x50 area one cube high. FPS is at a whopping 11 -.- so I obviously did something very wrong lol

I don't think so. This is your first working version that is doing something that you wanted. Do not ever delete it or break it. You will break the functionality plenty times later - but it is very important that you have something working anytime you need to go back and figure out how you did it last time.

 

 

Easiest way to do it, is just to copy paste the current render method into another (say, named "Render2 () ) and keep messing with the new one.

 

 

Now, to address the performance, let's check the most obvious issues first:

1. Check that you only bind the VB/IB every frame (not recreate VB/IB every frame).

 

2. What is the number of draw calls ? 50x50x1 would imply 2,500 draw calls - which is a lot, for sure. Can you try 10x10, 20x20 and post the framerate ?

The first thing to do here, is to create a version where you have all cubes in a single VB / IB and render that in one Draw call - and post the framerate then.

Share this post


Link to post
Share on other sites

10x10 works fine, fps stays at 60. How do I check if the vertex buffer is only being called once?

 

public override void Draw(GameTime gameTime)
            {
                base.Draw(gameTime);

                foreach (var item in entities)
                {

                    effect.VertexColorEnabled = false;
                    effect.TextureEnabled = true;
                    effect.Texture = item.Texture;

                    Matrix center = Matrix.CreateTranslation(new Vector3(-0.5f, -0.5f, -0.5f));
                    Matrix scale = Matrix.CreateScale(1f);
                    Matrix translate = Matrix.CreateTranslation(item.Position);

                    effect.World = center * scale * translate;
                    effect.View = camera.View;
                    effect.Projection = camera.Projection;

                    cube.Draw(effect);
                }

 

That method is getting called in my draw method in my game class

 

 public void Draw(BasicEffect effect)
    {
        foreach (EffectPass pass in effect.CurrentTechnique.Passes)
        {
            pass.Apply();
            device.SetVertexBuffer(cubeVertexBuffer);
            device.DrawPrimitives(PrimitiveType.TriangleList, 0, cubeVertexBuffer.VertexCount / 3);
        }
    }

And this method is getting called by the method above. I think this one is the problem. Its setting the VB every time its called I think

Share this post


Link to post
Share on other sites
Norman Barrows    7179

foreach (var item in entities)

...
foreach (EffectPass pass in effect.CurrentTechnique.Passes)
...

device.SetVertexBuffer(cubeVertexBuffer);

 

thats the killer tight there.

 

you want to put all the cubes in one VB and call some sort of draw_my_vb() routine once.

 

so i guess in OO terms, you'll want a vb object, and you'll want a version of your cube draw method that "draws" cubes into your vb object or calls some "add_cube" method of the vb_object. then your vb object has its own draw method that talks to directx, and sends all its cubes at once.

Share this post


Link to post
Share on other sites
VladR    722

10x10 works fine, fps stays at 60.

It looks like you have VSync On. Disable it first at the method where you construct the graphics object (and set the resolution), if you want some reasonable approximation of the performance (a frame time would be more precise). The framerate should skyrocket, depending on your HW configuration,. to 1000's.

 

As I said few posts back, now you need to create a single VB/IB. That means:

- pre-transforming the vertices on CPU when you fill the VB, since you will be able to set the World Matrix only once per VB - thus moving whole terrain at once (not the individual blocks). This is easy, if you don't do any rotation, just translation - just set the vertex position to (CubeX * Spacing, CubeY * Spacing, -CubeZ * Spacing)

- when rendering, you just set the VB/IB once and render it using single Draw command. With Vsync off, the FPS should be really high.

Share this post


Link to post
Share on other sites

I used this line of code

 

graphics.SynchronizeWithVerticalRetrace = false;

 

To disable VSync and it didn't help the fps while running a 50x50. When you're talking about only calling the Vbuffers once, I'm assuming you mean create an object VBuffer and replace it with the foreach loop in each draw method? I'm a little confused on what you mean at your first bullet point

Share this post


Link to post
Share on other sites
VladR    722

Of course, Vsync surely couldn't have helped the framerate when rendering 50x50. But where did the framerate skyrocket when rendering 10x10 or 5x5 ?

 

As for the VB - right now, you keep 50x50 VBs, right ? Each of them has 8 vertices and 24 triangles. To render it all, you need to switch the VB 2,500 times and call a Draw method 2,500 times.

 

With the single VB approach, however, you will fill it with 50x50x8 vertices. Your IB will have 50x50x6 indices. Then, you will have only a single Draw call and all 2,500 cubes will get rendered from that VB/IB.

Share this post


Link to post
Share on other sites
phil_t    8084

foreach (var item in entities)

...
foreach (EffectPass pass in effect.CurrentTechnique.Passes)
...

device.SetVertexBuffer(cubeVertexBuffer);

 

thats the killer tight there.

 

 

Actually, if that's the same vertex buffer each time, XNA will optimize that call out, so it only make the underlying SetStreamSource call once (you can check this in PIX). So I doubt that line is specifically the problem.

 

But separate Draw calls for each cube probably are the killer.

Share this post


Link to post
Share on other sites
VladR    722

I certainly wouldn't try to do the refactoring and optimizing (actually, this is more like a complete new feature) at the same time.

 

First, make it work, even if it will make the code look ugly. You're not going to save any time by doing both things at the same time. Refactoring something that works is a question of 10 minutes in Visual Studio.

 

Trying to figure out whether it's the refactor or something else that broke the rendering (if you try to do both things at the same time), takes way more time than those 10 minutes...

Share this post


Link to post
Share on other sites

I think I need to make an indices buffer, but I have no clue how to pass my vertex buffer to it. When I'm drawing the cubes, I'll be drawing from the indices list so hopefully it'll make the fps jump

 

I changed the class to this so the vb is only being called once but nothing changed

 

    public void setVB()
    {
        device.SetVertexBuffer(cubeVertexBuffer);
    }

    public void Draw(BasicEffect effect)
    {
        setVB();

        foreach (EffectPass pass in effect.CurrentTechnique.Passes)
        {
            pass.Apply();
            device.DrawPrimitives(PrimitiveType.TriangleList, 0, cubeVertexBuffer.VertexCount / 3);
        }
    }

 

 

Share this post


Link to post
Share on other sites
NightCreature83    5002

How much difference is there in the shader setup for each cube? As it is the shader setup that is most likely causing the slow down in your application, especially if the draw call is small.

 

Also start using a Version Control System so that you can easily switch back or diff to a working state, I have my own perforce server at home on the same box as I develop on. Which allows me to revert back to a working state if I manage to mess up my code to badly.

Share this post


Link to post
Share on other sites
Each cube is set up the same, if that's what you're asking. Every cube is drawn with the same texture and vertices, just in different positions. Since nothing I'm trying has been working, could I just exit out of the draw method with a bool variable?

if (Construct == false)
{
draw...
}

construct = true;

EDIT:
What about using a Cube made in blender? Would this be easier to draw and eventually alter in the final game? Edited by burnt_casadilla

Share this post


Link to post
Share on other sites
NightCreature83    5002

It wouldn't be but it would be easier for an artist to work in blender/3ds/maya/or other 3D package. Editing the world position of that cube shouldn't be hard in the engine however but modifying specific verts might be. As there might be more polies in that cube then you expect.

 

If your cubes verts are already transformed, you could do the shader setup only once and then just call draw on all of the cubes, this probably will not give you all of the performance back but you might get some.

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

Sign in to follow this