tinySceneGraph



Using generic vertex attributes in VBOs

tinyScenegraph shape nodes are able to manage an arbitrary number of custom vertex attributes by maintaining a map with the attribute location as the key and an array of vectors as the value. Whenever
Procedural bump mapping
Procedural bump mapping in surface local space, using tangent vectors passed as generic attributes to the vertex shader.
this map is changed, the shape is flagged dirty and will rebuild its VBO data in the next frames render traversal.

The image to the right shows csgEdit, having generated tangent vectors for a planes vertices and stored them in generic attribute 6, along with the vertex coordinates (attribute 0), normals (attribute 2) and 2D texture coordinates for texunit 0 (attribute 8). The somewhat strange location assignment is used to avoid aliasing of fixed function attributes with generic attributes with some OpenGL drivers. From tinySG's perspective, it does not really matter what locations are used.

For performance reasons, tinySG organises all vertex data of each shape node in an interleaved layout when copying it into a GL vertex buffer object (VBO). For example, suppose a shape has vertex coordinates (v), per-vertex normals (n), two sets of texture coordinates (t1 and t2) and a custom attribute (a) like a per-vertex tangent vector for bump mapping. The resulting VBO memory layout is v,n,t1,t2,a, v,n,t1,t2,a,... and is generated like this (simplified):

    // Function gets called whenever a shape's geometry is flagged dirty:
    void csgShape::UpdateVBOInterleaved (unsigned short cxtID)
    {
      ... 
      
      // (1) Iterate over attribute map and assign VBO offset to each attribute:
      int stride = 0;                                // total size (bytes) for 1 vertex.
      std::vector< std::pair< int,int > >  attribs;  // ID, offset.

      std::map<short, csgAttribute>::iterator it = attributes.begin();
      while (it!=attributes.end()) {                 // For all existing attributes:
        attribs.push_back (std::pair<int,int>(it->first, stride));
        stride += 4*sizeof (float);
        ++it ;
      }

      // (2) Bind the VBO on given GL context:
      glBindBuffer(GL_ARRAY_BUFFER, vboIDs.vbo[cxtID]);

      // (3) Determine and reserve memory requirements:
      unsigned int size = stride * numVertices;
      glBufferDataARB (GL_ARRAY_BUFFER_ARB, size, NULL, GL_STATIC_DRAW_ARB);

      // (4) Map the currently bound buffer for writing:
      csgVec4f  *buf = (csgVec4f*) glMapBuffer (GL_ARRAY_BUFFER_ARB, GL_WRITE_ONLY);

      // (5) Write vertex attributes to mapped buffer, interleaved:
      size_t  offs= 0;
      for (size_t i=0; i<numVertices; i++)                // for all vertices:
        for (size_t attr=0; attr<attribs.size(); attr++)  // for all vertex attribs:
          buf[offs++] = attributes[attribs[attr].first][i];
     
      // (6) cleanup 
      glUnmapBuffer (GL_ARRAY_BUFFER_ARB);
      glBindBuffer(GL_ARRAY_BUFFER,0);
      
      ... [store VBO layout - see below]
    }
The above code uses four floats for each attribute. Obviously, some attributes like normal vectors or 2D texture coordinates require less than four components, so the code wastes some memory. However, it is simple and fast due to the 16byte alignment of each attribute.

The intermediate attribs array becomes useful again when setting up the attribute data pointers for rendering. Since shapes can have any number of vertex attribute bound to any attribute location, there is a need to somehow remember the layout of the VBO just created. While a shader program can certainly run with just generic pointers, the fixed function pipeline still seems to require the legacy pointers (glVertexPointer and friends) being set. This is quite annoying, because it means that different calls are necessary, based on the attribute to set. tinySG does this by maintaining small functor-like objects that wrap the required calls behind a common interface:

    const GLint attrSize=4;
    for (size_t attr=0; attr<attribs.size(); attr++) { // for all vertex attribs:
      VBOOffset* func = NULL;
      switch (attribs[attr].first) {
        case CSG_VA_VERTEX:
          func = new VBOVertexOffset (stride, attribs[attr].second, attrSize);
          break;
        case CSG_VA_NORMAL:
          func = new VBONormalOffset (stride, attribs[attr].second);
          break;
        case CSG_VA_COLOR:
          func = new VBOColorOffset (stride, attribs[attr].second, attrSize);
          break;
        case CSG_VA_TEXCOORD0:
        case CSG_VA_TEXCOORD1:
        ...
          func = new VBOTextureOffset (attribs[attr].first-CSG_VA_TEXCOORD0, stride, 
                                       attribs[attr].second, attrSize);
          break;
        case default:
          // handle as generic attribute:
          func = new VBOGenericOffset (attribs[attr].first, stride, attribs[attr].second, attrSize);
          break;
       }
      vboIDs.attrOffsets.push_back (func);
    }
Fortunately, this nasty switch() is only executed during VBO updates. When the render traverser triggers a shape to render, it just has to loop through the shapes vboIDs.attrOffsets list to setup pointers for all it's attributes. The render function thus becomes pretty simple. This is what it looks like for a set of triangle strips:
    void csgTristrip::OnRender (csgRenderAction *A)
    {
      if ( !BindBuffers (A) )
        return;  
        
      // Set all attribute pointers:
      for (size_t i=0; i<vboIDs.attrOffsets.size(); i++)
        vboIDs.attrOffsets[i]->Set();
        
      // Render all strips of this triangle strip set in one call:
      glMultiDrawArrays(GL_TRIANGLE_STRIP, &(primStarts[0]), &(numVertices[0]), numVertices.size());

      UnbindBuffers (A);
    }
Finally, the Set() functions look as simple as in this implementation for generic vertex attributes:
    struct VBOGenericOffset : public VBOOffset
    {
      VBOGenericOffset (int attrNo, GLsizei stride, int offset=0, GLint size=4)
      {
        m_attr   = attrNo;    // Attr ID, starting at 0
        m_size   = size;      // number of components, always 1,2,3, or 4
        m_stride = stride;    // total size (coord, normal, tex, generic, ...) of vertex, in byte
        m_offset = offset;    // offset in byte of this attr in vertex (i.e. starting at 
                              // offset=48 of stride=64)
      }
      virtual void Set ()
      {
        glEnableVertexAttribArrayARB (m_attr);
        glVertexAttribPointerARB (m_attr, m_size, GL_FLOAT, GL_FALSE, m_stride, 
                                  reinterpret_cast<void*>(m_offset));
      }
      virtual void Reset ()
      {
        glDisableVertexAttribArrayARB(m_attr);
      }
    };
So, what is all this good for? Well, generic attributes allow to store additional per-vertex information on a mesh, like temperature values, mechanical stress, tangent or bi-normal vectors, etc. These properties can be evaluated in a shader program, realising different views on geometry.

regular texture mapping bump mapping
Plane using regular wood texture. Same scene, using a bumpmap and per-pixel lighting.

The images above shows a small test scene in csgEdit, using a bumpmap shader. The IndexedFaceset node maintains coordinate, normal, texture coordinate and tangent vector for each vertex as vertex attributes. The shader uses normal and tangent vectors to transform light and eye vectors into surface local space for lighting calculations with the perturbed per-pixel normals from the normal map.

Keep rendering,
Christian


Acknowledgements:

  • The bumpmap shader is taken from The GLSL Shading Language orange book. It contains a good chapter on bump mapping and an even better description of the ups and downs of using generic vertex attributes.
  • The normal map texture in the wooden plane sample was created with a gimp plugin.


Copyright by Christian Marten, 2012
Last change: 24.11.2012