Vertex Format

Vertex attributes stored in buffer objects can be of a surprisingly large number of formats. These tutorials generally used 32-bit floating-point data, but that is far from the best case.

The vertex format specifically refers to the set of values given to the glVertexAttribPointer calls that describe how each attribute is aligned in the buffer object.

Attribute Formats

Each attribute should take up as little room as possible. This is for performance reasons, but it also saves memory. For buffer objects, these are usually one in the same. The less data you have stored in memory, the faster it gets to the vertex shader.

Attributes can be stored in normalized integer formats, just like textures. This is most useful for colors and texture coordinates. For example, to have an attribute that is stored in 4 unsigned normalized bytes, you can use this:

glVertexAttribPointer(index, 4, GLubyte, GLtrue, 0, offset);

If you want to store a normal as a normalized signed short, you can use this:

glVertexAttribPointer(index, 3, GLushort, GLtrue, 0, offset);

There are also a few specialized formats. GL_HALF_FLOAT can be used for 16-bit floating-point types. This is useful for when you need values outside of [-1, 1], but do not need the full

Non-normalized integers can be used as well. These map in GLSL directly to floating-point values, so a non-normalized value of 16 maps to a GLSL value of 16.0.

The best thing about all of these formats is that they cost nothing in performance to use. They are all silently converted into floating-point values for consumption by the vertex shader, with no performance lost.

Interleaved Attributes

Attributes do not all have to come from the same buffer object; multiple attributes can come from multiple buffers. However, where possible, this should be avoided. Furthermore, attributes in the same buffer should be interleaved with one another whenever possible.

Consider an array of structs in C++:

struct Vertex
{
  float position[3];
  GLubyte color[4];
  GLushort texCoord[2];
}

Vertex vertArray[20];

The byte offset of color in the Vertex struct is 12. That is, from the beginning of the Vertex struct, the color variable starts 12 bytes in. The texCoord variable starts 16 bytes in.

If we did a memcpy between vertArray and a buffer object, and we wanted to set the attributes to pull from this data, we could do so using the stride and offsets to position things properly.

glVertexAttribPointer(0, 3, GL_FLOAT, GLfalse, 20, 0);
glVertexAttribPointer(1, 3, GL_UNSIGNED_BYTE, GL_TRUE, 20, 12);
glVertexAttribPointer(3, 3, GL_UNSIGNED_SHORT, GL_TRUE, 20, 16);

The fifth argument is the stride. The stride is the number of bytes from the beginning of one instance of this attribute to the beginning of another. The stride here is set to sizeof(Vertex). C++ defines that the size of a struct represents the byte offset between separate instances of that struct in an array. So that is our stride.

The offsets represent where in the buffer object the first element is. These match the offsets in the struct. If we had loaded this data to a location past the front of our buffer object, we would need to offset these values by the beginning of where we uploaded our data to.

There are certain gotchas when deciding how data gets packed like this. First, it is a good idea to keep every attribute on a 4-byte alignment. This may mean introducing explicit padding (empty space) into your structures. Some hardware will have massive slowdowns if things are not aligned to four bytes.

Next, it is a good idea to keep the size of any interleaved vertex data restricted to multiples of 32 bytes in size. Violating this is not as bad as violating the 4-byte alignment rule, but one can sometimes get sub-optimal performance if the total size of interleaved vertex data is, for example, 48 bytes. Or 20 bytes, as in our example.

Packing Suggestions

If the smallest vertex data size is what you need, consider these packing techniques.

Colors generally do not need to be more than 3-4 bytes in size. One byte per component.

Texture coordinates, particularly those clamped to the [0, 1] range, almost never need more than 16-bit precision. So use unsigned shorts.

Normals should be stored in the signed 2_10_10_10 format whenever possible. Normals generally do not need that much precisions, especially since you're going to normalize them anyway. This format was specifically devised for normals, so use it.

Positions are the trickiest to work with, because the needs vary so much. If you are willing to modify your vertex shaders and put some work into it, you can often use 16-bit signed normalized shorts.

The key to this is a special scale/translation matrix. When you are preparing your data, in an offline tool, you take the floating-point positions of a model and determine the model's maximum extents in all three axes. This forms a bounding box around the model. The center of the box is the center of your new model, and you apply a translation to move the points to this center. Then you apply a non-uniform scale to transform the points from their extent range to the [-1, 1] range of signed normalized values. You save the offset and the scales you used as part of your mesh data (not to be stored in the buffer object).

When it comes time to render the model, you simply reverse the transformation. You build a scale/translation matrix that undoes what was done to get them into the signed-normalized range. Note that this matrix should not be applied to the normals, because the normals were not compressed this way. A fully matrix multiply is even overkill for this transformation; a scale+translation can be done with a simple vector multiply and add.

Fork me on GitHub