Skip to main content
GameDev.net gamedev.net
🔒 Locked

The benefits of index buffers

Started by matt77hias Jan 24, 2017 at 8:13 PM 15 replies 3.5k views
Original Post
matt77hias
matt77hias

If vertices are only represented by a position, or a position + normal, or a position + texture coordinates, one can clearly see the benefits of an index buffer. But does a single index buffer still pay off and even make sense if vertices are represented by a position + normal + texture coordinates (.obj files)?

Alternatively, do multiple buffer textures (via offsetting) pay off the performance penalty due to all the look ups (especially with regard to D3D11)?

🧙
phil_t
phil_t



But does a single index buffer still pay off and even make sense if vertices are represented by a position + normal + texture coordinates (.obj files)?

Why wouldn't it? If the vertices are re-used for many triangles, that's less memory usage/bandwidth and less vertex processing.

21st Century Moose
21st Century Moose

In addition to potential memory saving don't forget that index buffers allow you to concatenate primitives: if your model is composed of multiple strips and fans, using an index buffer allows you to draw that model in a single draw call rather than in multiple draw calls. As we all know, draw calls are expensive and the fewer of them we make the better.

Direct3D has need of instancing, but we do not. We have plenty of glVertexAttrib calls. 
matt77hias
matt77hias

Worst case (not very rare): the texture coordinate pairs are all different, making every vertex unique?


In addition to potential memory saving don't forget that index buffers allow you to concatenate primitives: if your model is composed of multiple strips and fans, using an index buffer allows you to draw that model in a single draw call rather than in multiple draw calls. As we all know, draw calls are expensive and the fewer of them we make the better.

Isn't this possible with just a single vertex buffer as well?

🧙
phil_t
phil_t



Worst case (not very rare): the texture coordinate pairs are all different, making every vertex unique?

Well, you didn't specifically call that out in your original post. If every vertex of your geometry is truly unique (e.g. tetrahedron with normals), index buffers don't serve much purpose.

In the end, the things to think about are and measure are:

- how much data am I sending to the GPU each frame?

- how much space does the data for each vertex take? (pre and post transform)

- how much space does the data for an index buffer take?

- how many times will the vertex shader run?

- are my vertices organized to take advantage of the vertex cache (potentially minimizing the number of times the VS needs to run)?

It's not possible to really give specific recommendations without targeting a specific scenario (and measuring).

C0lumbo
C0lumbo



Worst case (not very rare): the texture coordinate pairs are all different, making every vertex unique?

That's extremely rare in my experience. I struggle to imagine any content generated by a 3D artist where every vertex is only used once. Even most pathological cases I can think of like cubes with separate textures on each face, or 2D tiles can benefit from indices because each quad can be made of 4 vertices instead of 6.

21st Century Moose
21st Century Moose

Isn't this possible with just a single vertex buffer as well?


If you add degenerate triangles ... But indices don't require you to add degenerate triangles and an index is much smaller than a vertex.
Direct3D has need of instancing, but we do not. We have plenty of glVertexAttrib calls. 
cozzie
cozzie
Not sure if this applied in your question, but there's also the advantage that you can have submeshes with their own offsets within the index (and vertex)buffer
Crealysm game & engine development: http://www.crealysm.com

Looking for a passionate, disciplined and structured producer? PM me
montify
montify

The IndexBuffer defindes primitives by referencing the vertex data stored in vertexBuffer.

So without a IndexBuffer you need 6 Vertices to generate this "mesh":

without.png

That means you submit 6 Vertices to the GPU - for this case that is not much, but imagine you have a fully Scene with lots of meshes....

With a IndexBuffer in your pocket the Picture look like:

with.png

you only need 4 Vertices in the VertexBuffer for generating the same result.

IA.png

So you use the IndexBuffer as a reference how the InputAssembler read the VertexBuffer to generate (in this case) 2 Triangles.

So the IndexBuffer tell to the InputAssembler like:

Hey, here you have a VertexBuffer with 4 Vertices, i gave you a reference how you can assembly 2 triangles with this 4 vertices, look:

For the first, you need

the First Item from the VertexBuffer (0)

than you grab the second Item (1)

and at least you need the fourth(3) Item from the buffer to form the First Triangle.

For the second Triangle, you need

The fourth Vertex(3).

the second Vertex (1)

and the third Vertex (2) to form the second Triangle.

So you must think, the IndexBuffer is more like a Blueprint to gave the IA the reference how connect Vertices - the benefit is you send less Data to the GPU

Sorry for the drawing, im not a professional, hope you can understand what i mean, feel free to ask.

For the pro´s, if this is complete garbage than GC.Collect() and tell me what i said wrong :) - always learning..

matt77hias
matt77hias

...

Sorry for the drawing, im not a professional, hope you can understand what i mean, feel free to ask.

For the pro´s, if this is complete garbage than GC.Collect() and tell me what i said wrong :) - always learning..

Drawings are ok :) but I understand the concept of index buffers.

The problem was just about what happens if you add more data to a single vertex like texture coordinates and vertex normal which makes more vertices unique.

But for most .obj files (and based on the above comments), I start to see some v/vt/vn pairs which are reused, so I just use the index buffer approach by default (or let the user decide).

🧙
Norman Barrows
Norman Barrows



But for most .obj files (and based on the above comments), I start to see some v/vt/vn pairs which are reused, so I just use the index buffer approach by default (or let the user decide).

the proof is in the pudding.

write a test routine that times drawing meshes both ways. you'll find indexed to be faster in almost all cases -sometimes much faster.

Norm Barrows Rockland Software Productions "Building PC games since 1989"</
montify
montify

...

Sorry for the drawing, im not a professional, hope you can understand what i mean, feel free to ask.

For the pro´s, if this is complete garbage than GC.Collect() and tell me what i said wrong :) - always learning..

Drawings are ok :) but I understand the concept of index buffers.

The problem was just about what happens if you add more data to a single vertex like texture coordinates and vertex normal which makes more vertices unique.

But for most .obj files (and based on the above comments), I start to see some v/vt/vn pairs which are reused, so I just use the index buffer approach by default (or let the user decide).

Normaly, when you use a IndexBuffer you try to avoid to submit two Vertices at the exact same Location.

So without a VertexBuffer it is nonsense that 2 vertices at the same Position have different Texture Coordinate, Color data.

Hope i understand your question at the right spot.

Sry, im not good in English ;)

21st Century Moose
21st Century Moose

Isn't this possible with just a single vertex buffer as well?


If you add degenerate triangles ... But indices don't require you to add degenerate triangles and an index is much smaller than a vertex.


Just to clarify - assume that you have a model that's composed of 20 triangle strips. It has a single vertex buffer which we'll call "vb". To draw it without indices you would use (D3D9-ish syntax):

device->SetStreamSource (0, vb, offset, stride);

device->DrawPrimitive (D3DPT_TRIANGLESTRIP, ...);

device->DrawPrimitive (D3DPT_TRIANGLESTRIP, ...);

device->DrawPrimitive (D3DPT_TRIANGLESTRIP, ...);

device->DrawPrimitive (D3DPT_TRIANGLESTRIP, ...);

device->DrawPrimitive (D3DPT_TRIANGLESTRIP, ...);

device->DrawPrimitive (D3DPT_TRIANGLESTRIP, ...);

device->DrawPrimitive (D3DPT_TRIANGLESTRIP, ...);

device->DrawPrimitive (D3DPT_TRIANGLESTRIP, ...);

device->DrawPrimitive (D3DPT_TRIANGLESTRIP, ...);

device->DrawPrimitive (D3DPT_TRIANGLESTRIP, ...);

device->DrawPrimitive (D3DPT_TRIANGLESTRIP, ...);

device->DrawPrimitive (D3DPT_TRIANGLESTRIP, ...);

device->DrawPrimitive (D3DPT_TRIANGLESTRIP, ...);

device->DrawPrimitive (D3DPT_TRIANGLESTRIP, ...);

device->DrawPrimitive (D3DPT_TRIANGLESTRIP, ...);

device->DrawPrimitive (D3DPT_TRIANGLESTRIP, ...);

device->DrawPrimitive (D3DPT_TRIANGLESTRIP, ...);

device->DrawPrimitive (D3DPT_TRIANGLESTRIP, ...);

device->DrawPrimitive (D3DPT_TRIANGLESTRIP, ...);

device->DrawPrimitive (D3DPT_TRIANGLESTRIP, ...);


That's 20 draw calls to draw your model, and we all know that draw calls are expensive so you want to minimize them.

Let's add an index buffer (we'll call it "ib") and draw it:

device->SetStreamSource (0, vb, offset, stride);

device->SetIndices (ib);

device->DrawIndexedPrimitive (D3DPT_TRIANGLES, ...);


One draw call - much better.

The whole point of this is that you should not assume that volume of memory usage is the sole arbiter of performance. We see this time and again when programming to a 3D API: burn a little extra memory and get an order of magnitude performance increase.

Direct3D has need of instancing, but we do not. We have plenty of glVertexAttrib calls. 
matt77hias
matt77hias

Isn't this possible with just a single vertex buffer as well?


If you add degenerate triangles ... But indices don't require you to add degenerate triangles and an index is much smaller than a vertex.


Just to clarify - assume that you have a model that's composed of 20 triangle strips. It has a single vertex buffer which we'll call "vb". To draw it without indices you would use (D3D9-ish syntax):

device->SetStreamSource (0, vb, offset, stride);

device->DrawPrimitive (D3DPT_TRIANGLESTRIP, ...);

device->DrawPrimitive (D3DPT_TRIANGLESTRIP, ...);

device->DrawPrimitive (D3DPT_TRIANGLESTRIP, ...);

device->DrawPrimitive (D3DPT_TRIANGLESTRIP, ...);

device->DrawPrimitive (D3DPT_TRIANGLESTRIP, ...);

device->DrawPrimitive (D3DPT_TRIANGLESTRIP, ...);

device->DrawPrimitive (D3DPT_TRIANGLESTRIP, ...);

device->DrawPrimitive (D3DPT_TRIANGLESTRIP, ...);

device->DrawPrimitive (D3DPT_TRIANGLESTRIP, ...);

device->DrawPrimitive (D3DPT_TRIANGLESTRIP, ...);

device->DrawPrimitive (D3DPT_TRIANGLESTRIP, ...);

device->DrawPrimitive (D3DPT_TRIANGLESTRIP, ...);

device->DrawPrimitive (D3DPT_TRIANGLESTRIP, ...);

device->DrawPrimitive (D3DPT_TRIANGLESTRIP, ...);

device->DrawPrimitive (D3DPT_TRIANGLESTRIP, ...);

device->DrawPrimitive (D3DPT_TRIANGLESTRIP, ...);

device->DrawPrimitive (D3DPT_TRIANGLESTRIP, ...);

device->DrawPrimitive (D3DPT_TRIANGLESTRIP, ...);

device->DrawPrimitive (D3DPT_TRIANGLESTRIP, ...);

device->DrawPrimitive (D3DPT_TRIANGLESTRIP, ...);


That's 20 draw calls to draw your model, and we all know that draw calls are expensive so you want to minimize them.

Let's add an index buffer (we'll call it "ib") and draw it:

device->SetStreamSource (0, vb, offset, stride);

device->SetIndices (ib);

device->DrawIndexedPrimitive (D3DPT_TRIANGLES, ...);


One draw call - much better.

The whole point of this is that you should not assume that volume of memory usage is the sole arbiter of performance. We see this time and again when programming to a 3D API: burn a little extra memory and get an order of magnitude performance increase.


...


m_device_context2->IASetVertexBuffers(0, 1, &vertex_buffer, &stride, &offset);
m_device_context2->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);


...


m_device_context2->Draw(nb_vertices, 0);
m_swap_chain2->Present(0, 0);
Isn't this one draw call? (D3D11)
🧙
21st Century Moose
21st Century Moose

...


m_device_context2->IASetVertexBuffers(0, 1, &vertex_buffer, &stride, &offset);
m_device_context2->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);


...


m_device_context2->Draw(nb_vertices, 0);
m_swap_chain2->Present(0, 0);

Isn't this one draw call? (D3D11)


Assuming that your model is represented by a single triangle list, yes, but that's a very big assumption. Most models aren't.

If we stop talking theory and start talking actual practice....

The "stripified" Stanford Bunny has 34834 vertices, 69451 triangles and is composed of 1129 triangle strips.

Because a triangle uses 3 vertices, doing it in a single draw call as a single triangle list without indices requires 208353 vertices.

Assume a vertex is 32 bytes and an index is 4 bytes.

That single draw call is approx. 6.5mb of data.

Drawing it with multiple triangle strips requires 1129 draw calls. That's not going to run fast.

So we add an index buffer instead. Vertex cost is now 1.1mb and index cost is 277kb - total about 1.4mb.

So for about 20% of the storage cost we get to do it in a single draw call.

I'd encourage you to examine your data sets. Are you absolutely certain that you have no repeated vertices? Because the only scenarios in which I would imagine that to be the case would be if you have either grossly simplified models (cubes) or if you're using per-face normals (which will give you flat shading so you almost certainly don't want that). Otherwise your objections to indexing seem theoretical at best.

Direct3D has need of instancing, but we do not. We have plenty of glVertexAttrib calls. 
ErnieDingo
ErnieDingo
You guys have given me some food for thought on my own game. I abandoned indicies a while ago but i feel i should revisit.

The original thought was that per triangle i needed to have control over per texture. The mesh is uniform but basically at the same vertex I have 6 triangles with their own copy. Just not necessary and if i need extra the texture detail i can do that a number of ways but its just too inefficient at the moment. I could bake extra geometry in where needed or use decals. I can live without the memory foot print and also added more geometric information in such as tangent so i can add more gpu based functionality such as tessellation. Lots to think about.
Indie game developer - Game WIP    Strafe (Working Title) - Currently in need of another developer and modeler/graphic artist (professional & amateur's artists welcome)   Insane Software Facebook
ErnieDingo
ErnieDingo
Worst case (not very rare): the texture coordinate pairs are all different, making every vertex unique?

That's extremely rare in my experience. I struggle to imagine any content generated by a 3D artist where every vertex is only used once. Even most pathological cases I can think of like cubes with separate textures on each face, or 2D tiles can benefit from indices because each quad can be made of 4 vertices instead of 6.

The other thing, 4 vertices instead of 6 is just a starter for savings. If you have say 8 vertices making up 12 triangles imagine laid out in a grid, to do the same thing with just vertices on your own, you're going to need 18 individual vertices. Where as with indices you will have 18 but only process 8 vertices. Which I would argue is a greater than 50% saving, probably more as VS is only called 8 times, rather than 18 (and matrix multiplication etc, so its got to be faster on that front alone).

I did this quickly, apologies for any mistakes in it. If any clarifications then I will fix etc. I make some broad assumptions here. Caching is perfect etc, I am not using Triangle fans either, by rights that will speed up index component but the number of Verts should be the same. The saving Calc% is based on the number of times you would visit between just a raw Vertex Buffer vs another which is optimized with an associated index buffer. Im not sure of impacts down stream on geometry shader. Numbers seem to work out.


  Vertex List   Count (Draw Call DX11)                                             DrawCall Indexed       
  Quad Count (2 tris) / Full Vertex List (32 bytes) / Total Memory   /  Vertices 32 bytes / Index count 4 bytes / Total Memory     / Saving Calc % 
            1                          6               192                   4                       6                   152         33.33 
            4                         24               768                  10                      24                   416         58.33 
           10                         60              1920                  22                      60                   944         63.33 
          100                        600             19200                 202                     600                  8864         66.33 
          800                       4800            153600                1602                    4800                 70464         66.63

I chose 800 because I actually have some geometry in a buffer at the moment which I can test this on. Will code it over the next day and see what impact it has.

Edit: Cleaned up table and spelling

Indie game developer - Game WIP    Strafe (Working Title) - Currently in need of another developer and modeler/graphic artist (professional & amateur's artists welcome)   Insane Software Facebook

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.