Catering for multiple vertex definitions

Started by
14 comments, last by Hodgman 11 years, 5 months ago
This is the format I use for VBO data, in a plain C application:
[source lang="cpp"]
#define POS_COUNT 3
#define NORMAL_COUNT 3
#define TEXCOORD_COUNT 2
#define COLOR_COUNT 4


struct vertex_data_s {
int this_size; //size of this structure
float* databuffer; //holds all data

//pointers into data buffer:
float* pos; //pointer to pos for 0th vertex
float* normal; //pointer to normal for 0th vertex
float* color; //pointer to color for 0th vertex
float* texcoord; //pointer to 0th texcoord for 0th vertex
int texcoord_count;
int stride; //number of floats to next vertex
int count; //number of vertices
} vertex_data_t;

//In the above, pos, normal, color, texcoord are all staggered pointers into the same databuffer. To access a particular type of data, its just:
// assume 'd' is a vertex_data_t*

d->pos[ d->stride * n]; //pointer to nth vertex position
d->normal[ d->stride * n]; //pointer to nth normal

Suppose I have data that has position, and normal, but no color or texcoords. d->color and d->texcoord will be null, and d->stride will be set to NORMAL_COUNT + POSITION_COUNT;

What if I want 3 textures? Then d->stride can be NORMAL_COUNT + POSITION_COUNT + 3* TEXCOORD_COUNT. And the three texcoords are:

d->texcoord[ d->stride * n]
d->texcoord[ d->stide *n + TEXCOORD_COUNT ]
d->texcoord[d->stride * n +TEXCOORD_COUNT *2]

in general :

d->texcoord[stride * n + TEXCOORD_COUNT * tn ] for the 's'
d->texcoord[stride * n + TEXCOORD_COUNT * tn +1 ] for the 't'


[/source]

These all get simplified by some macros, so I don't have to think about it. I just have macros like vertex_position( my_vbo, n) to get a pointer to the nth 3d vector (pointer to 3 floats).

With macros, it makes it easy for my to change the underlying structure and recompile, without rewriting everything else. For C++, you could use accessor functions instead of macros. You can also return the correct data types if you do this. For instance, when I use the vertex_position macro, the macro casts the float* into a vertex_3*, which is my app is just 3 floats. So I can do vertex_position(my_vbo, n)->y or vertex_texcoord(my_vbo, n, tn)->y if I want. But in C++, you can do all kinds of other things with accessor functions, such as check for valud values of n, tn, etc, so you can use the same idea, but refine it a bit and make it more crash proof.

This structure also translates data easy to opengl: Call glBufferData using a pointer to d->databuffer, size of d->stride * sizeof(float) * d->count. All the data is transfered to gl (and probably the gfx card vram) in one quick call. Then when its time to draw, just call glVertexPointer / glAttribPointer with these pointers, give it d->stide for each one, and then draw. Fast and easy. If a particular buffer has no normals, colors, etc, its d->normal or d->colors pointer will be null and stride will be smaller: no wasted space.
Advertisement

This is the format I use for VBO data, in a plain C application:


Looks very cool, might give that a shot actually.
I have an alternate form that does a structure-of-arrays, but since it uses different accessor macros (because access is d->position[POSITION_COUNT*n] instead of d->position[d->stride*n]), but I don't have much use for it anymore because having two layouts is confusing. The idea was if all the positions are continuous in memory they can be updated by the cpu without having to retransfer everything else (like texcoords). But it didn't take long to realize that it was better just to have two vertex_data_t structs, one with positions/ normals updated with GL_DRAW_DYNAMIC, then the other with everything else as GL_DRAW_STATIC. All I needed to do was allow multiple of these to be bound to one model, which was not hard at all.
Wouldn't this be an ideal situation for vertex streams?
Wouldn't this be an ideal situation for vertex streams?[/quote]

I suppose it is. When I was implementing this stuff, I considered many options:

[ ] denotes one VBO, P = position N = normal T = texcoord

interleaved: [PNTPNTPNTPNTPNT] (if model is static, I suspect this is ideal)

separate: [PPPPP] [NNNNN] [TTTTT] (I think this is the worst. advantage is T can be kept static, and P or N can be dynamic/streamed. Don't know when you would want to change P without changing N though) (This was also my first VBO implementation, just to get things working!)

sequential: [ PPPPP NNNNN TTTTT] (not sure about this one. I would guess it's the same or worse than interleaved. Not sure how it could be better.)

'twin' [PNPNPNPNPNP] [TTTTTTTTTTT] (If PN are dynamic and T is static, I suspect this is the best for CPU animated models)

Are there any other formats anyone uses? I suppose everyone has some custom attributes they like to pass, but you can just shove those into their own VBO, or extend my struct to allow custom0, custom1, custom2, etc to be part of the stride.

I suppose another interesting question is: Is the cost of having to bind 2 or more VBOs per model almost always less than the saving produced by tagging one VBO as static and the other as dynamic? For instance, given the choice between:

A. dynamic/streaming: [PNPNPNPNPNPNP] static:[TTTTTTTTTTT] ; each render needs to bind 2 VBOs

versus:

B. dynamic/streaming: [PNPNPNPNPNPNPNPTTTTTTTTTTTTTTTT] ; only update PNPNPN area with glBufferSubData; render only binds 1 VBO

will A almost always beat-out B? I think so, unless the VBOs being drawn are so short (only handfuls of triangles), that you're spending all your time thrashing and binding; in which case you should reevaluate what's going on.
will A almost always beat-out B?
Depends how your GL driver implements buffer management, but probably wink.png

Another reason to use split (non-interleaved) streams is when you need to use the same mesh data with different vertex shaders.
e.g. your render-to-shadow-map shader only requires positions, not normals or tex-coords.
In that case, you might want to use [PPPPPPPP][NTNTNTNT] so that the shadow-shader doesn't unnecessarily have to skip over wasted normal/tex-coord bytes.
I've also seen other engines that simply export [PPPPPPPP] and [PNTPNTPNTPNTPNTPNTPNTPNT] -- so that both shaders are optimal, at the cost of memory ;)


In my engine, the model/shader compilers read in a Lua configuration file, describing their options for laying out vertex data. For any sub-mesh, it first determines which vertex shaders might be used on that sub-mesh, and collects a list of "vertex formats" (i.e. vertex shader input structures) that the sub-mesh needs to be compatible with. It then uses that list, along with the list of attributes that the artist has authored (e.g. have they actually authored tex-coords?) and selects an appropriate stream storage format for the data to be exported in.
e.g.
StreamFormat("basicStream",
{
{--stream 0
{ Float, 3, Position },
},
{--stream 1
{ Half, 3, Normal },
{ Half, 2, TexCoord, 0 },
},
})

--these are also used to auto-generate the 'struct Vertex {...}' HLSL code, etc..
VertexFormat("basicVertex",
{
{ "position", float3, Position },
{ "texcoord", float2, TexCoord, 0 },
{ "normal", float3, Normal },
})
VertexFormat("shadowVertex",
{
{ "position", float3, Position },
})

--which stream formats are allowed to be used with which vertex formats
--A.K.A. VertexDeclaration, VAO format descriptor...
InputLayout( "basicStream", "basicVertex" )
InputLayout( "basicStream", "shadowVertex" )

This topic is closed to new replies.

Advertisement