Monday, March 19, 2012

OpenGL Buffer Packing

This weekend I felt a drain to my creativity brought about by a combination of not feeling well, disappointment in the ending of Mass Effect 3, and actually getting decent at call of duty. Drop-box replacing some important code files with emptiness didn't help either. So I've decided to start from scratch, rather than trying to repair my existing code base.

Starting from square one usually involves a trip to good old fashioned C. If I can code it in C, I can take it anywhere. I had gotten quite good at doing OpenGL from inside OpenTK, so my idea was to take some of the  modern concepts learned from there, and apply them back in C.

Modern OpenGL consists really of two things, cramming things in to buffers, and making pretties with shaders.

So, let's talk about array buffers, no, wait, let's talk about vertexes first.

In a mathematical sense, a vertex buffer is an intersection of two or more lines in space. We use vertexes to define the bounds of our geometry instead of lines or rays. However, in OpenGL, vertexes do not just represent points in space that define boundaries of geometry. Color, texture, lighting, and other information can also be attached to a vertex as well.

There are two ways of packing data into buffers. The first way is to give each type of element it's own buffer. 

So in case A:

* vertexes would be in their own vertex buffer
* colors would be in their own color buffer
* texture coordinates would be in their own buffer
* normals would be in their own normal buffer

Case B: (Eeach vertex would contain the following)

* x, y, z (position)
* s, t (texture)
* r, g, b, a (color)
* nx, ny, nz (normal)

In general, case B is the better way to go. It only requires one bind buffer operation, vs one for each component type. 

For case A, you would do your drawing like this:

glEnableClientState(GL_VERTEX_ARRAY);
glEnableClientState(GL_COLOR_ARRAY);

glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer_handle);
glVertexPointer(3, GL_FLOAT, 0, 0);

glBindBuffer(GL_ARRAY_BUFFER, color_buffer_handle);
glColorPointer(4, GL_FLOAT, 0, 0);

glDrawArrays(GL_TRIANGLES, 0, number_triangles);
...

for case B, you would draw like this:

typedef struct {

float x, y, z;
float r, g, b, a;

}custom_vertex_format;

...

glEnableClientState(GL_VERTEX_ARRAY);
glEnableClientState(GL_COLOR_ARRAY);

glBindBuffer(GL_ARRAY_BUFFER, buffer_handle);
glVertexPointer(3, GL_FLOAT, sizeof(custom_vertex_format), 
    offsetof(custom_vertex_format, x);

glColorPointer(4, GL_FLOAT, sizeof(custom_vertex_format), 
    offsetof(custom_vertex_format, r);

glDrawArrays(GL_TRIANGLES, 0, number_triangles);

offsetof is a macro defined in stddef.h http://linux.die.net/man/3/offsetof

Again, in most circumstances, method B is going to be faster. The reasoning for this is as follows.

Fewer cache misses, since all the data is tightly packed together. In case A, it might be conceivable that the vertex buffer and the color buffer might be in completely different areas in memory. In case B, each vertex should be adjacent to each other. It's also especially important when working with element arrays.

What's an element array?

Consider the following. Say you have a mesh, and in this mesh, you have multiple adjacent triangles. These triangles, 'share' a vertex. As in, the vertex has the exact same position, color, normal and texture attributes. The more attributes you have packed in your vertex, the more expensive it becomes to have duplicates in your vertex buffer.

Say you have the following vertexes 
V = {A, B, C, D, E, F, G, H, I, J}

Say your triangle mesh 
M = {A, B, C, B, C, D, C, D, E, F, G, H}

So, B is an element twice, C is an element 3 times, D is in there twice, etc. At this point, each vertex is composed of at least 11 floating point elements. If only there was a way to send each vertex to the buffer exactly once, and then just draw by index... There is!

Say V = {A, B, C, D, E, F, G, H, I, J}
and M = {A, B, C, B, C, D, C, D, E, F, G, H}

Let Mi = {1,2,3,2,3,4,3,4,5,6,7,8}

So we would bind V as our vertex buffer, and Mi as our element buffer. we would use glDrawElements instead of glDrawArrays. http://www.opengl.org/sdk/docs/man/xhtml/glDrawElements.xml

glEnableClientState(GL_VERTEX_ARRAY);
glEnableClientState(GL_COLOR_ARRAY);

glBindBuffer(GL_ARRAY_BUFFER, buffer_handle);
glVertexPointer(3, GL_FLOAT, sizeof(custom_vertex_format), 
    offsetof(custom_vertex_format, x);

glColorPointer(4, GL_FLOAT, sizeof(custom_vertex_format), 
    offsetof(custom_vertex_format, r);

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, element_buffer_handle);

glDrawElements(GL_TRIANGLES, number_triangles, GL_SHORT, 0);

Take note that we only had to bind the element array buffer in order to use glDrawElements, we didn't have to specify an element pointer, or enable the client state for it.

Also, notice that there isn't a separate element buffer for each attribute of the vertex. It sounds like it could be a great idea, but the reality is that it would be incredibly slow. It's the same reason why non-power of two textures are such a problem. It's just a constraint we have to live with.

I hope this has helped distinguish some of the differences in how OpenGL can process buffers for you.

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.