Sunday, March 4, 2012

Matrix (palette) skinning for OpenGL ES 2.0 (GLSL, GPU, hardware) - applying bone transformation to mesh in vertex shader


When starting this post, the intention is to explain how I implemented GPU skinning in previous post, sort of a tutorial on how to port CPU skinning based code (often the approach in desktop code) to OpenGL ES.



1) Use buffers for vertices, normals (m_posnoVertexVboIds) and vertex indices (m_staticIndexVboIds). 
Here, also used for vertex colors (m_staticVertexVboIds). Call this once in init context - e.g. in UIViewController::awakeFromNib implementation - called when application is brought to foreground.

glBindBuffer(GL_ARRAY_BUFFER, m_posnoVertexVboIds[i]);
glBufferData(GL_ARRAY_BUFFER, nv*posnodatas, posnodata, GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, m_staticVertexVboIds[i]);
glBufferData(GL_ARRAY_BUFFER, nv*staticdatas, staticdata, GL_STATIC_DRAW);


glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_staticIndexVboIds[i]);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, ni*(idatas/2), indexData2UShort, GL_STATIC_DRAW);

No need to read this paragraph further, if you are familiar with stride and glBufferData. Note that n-th value in indexData2UShort, defines index in posnodata, staticdata related to n-th vertex. e.g. when later "drawing" 56th vertex,  if indexData2UShort[55] = 13, means that vertex and normals for 56th vertex are defined in posnodata[13] and color is defined in staticdata[13]. This is quite simplified explanation but 55 is used for 56th element, since counting starts from 0. 

2) Stuff bone index and bone weight data to buffers. 
Use the same approach as 1) for bone weight and bone indices data. This means that you need to prepare data blob packed like:

boneIndexData:       <vertex1_bone1_index><vertex1_bone2_index><vertex1_bone3_index><vertex1_bone4_index><vertex2_bone1_index><vertex2_bone2_index> ….  

boneWeightsData:  <vertex1_bone1_weight><vertex1_bone2_weight><vertex1_bone3_weight><vertex1_bone4_weight><vertex2_bone1_weight><vertex2_bone2_weight> 


            if (isMeshDeformedBySkeleton()) {
                void *boneIndexData = sub->getBoneIndexDataPtr();
                void *boneWeightsData = sub->getBoneWeightsDataPtr();
                
                glBindBuffer(GL_ARRAY_BUFFER_ARB, m_boneIndexVboIds[i]);
                glBufferData(GL_ARRAY_BUFFER_ARB, nv*4*sizeof(UTuint8), boneIndexData, GL_STATIC_DRAW);
                
                glBindBuffer(GL_ARRAY_BUFFER_ARB, m_boneWeightVboIds[i]);
                glBufferData(GL_ARRAY_BUFFER_ARB, nv*4*sizeof(float), boneWeightsData, GL_STATIC_DRAW);
            }

In example bellow, <vertexA_boneB_index> is 1 byte (unsigned char), <vertexA_boneB_weight> is 4 byte (float), though 1 or 2 bytes should suffice too.
<vertexA_boneB_index> defines which bone index affects position of vertex A, <vertexA_boneB_weight> defines how much it affects. Note that every vertex position can be affected by up to 4 bones. If <vertexA_boneB_weight> == 0, means that bone number <vertexA_boneB_index> doesn't affect vertex A.
As explained in 1), index in m_staticIndexVboIds maps also to index of data related to vertex in boneIndexData and boneWeightsData arrays. e.g. bytes boneIndexData[13*4],boneIndexData[13*4+1],boneIndexData[13*4+2] and boneIndexData[13*4+3] defines index of bones 1-4 affecting position of 56th vertex (from example in step 1), while 4 byte floats on byte positions boneWeightsData[13*4*4 … 13*4*4+3],…, boneWeightsData[13*4*4+3*4 … 13*4*4+3*4+3] define "weight" of corresponding bones.

Vertex shader "receives" this data in:

attribute mediump vec4 a_BoneIndices;
attribute mediump vec4 a_BoneWeights;

Data in step 2), like data in step 1), needs to be fed only in init().

3) update matrix palette with simulation progress - prepare matrix palette by stepping simulation 
    
    m_animengine->stepTime(time);
    // fill bone transformation to m_matrixPalette structure
    pose->fillMatrixPalette(m_matrixPalette);
        
4) when rendering every frame, upload updated matrix palette to shader:

    glUniformMatrix4fv(PiperGL20::instance()->currentShader()->BoneMatricesHandle, m_matrixPalette.size(), GL_FALSE, (float*)matrixData);

where:
    BoneMatricesHandle = glGetUniformLocation(uiProgramObject, "u_BoneMatrices[0]");

and u_BoneMatrices is defined in the vertex shader to support 8 bones (in this example), as:

uniform highp   mat4    u_BoneMatrices[8];

5) Draw (glDrawElements) - use prepared vertex buffers from step 2) when drawing.

                        
        glBindBuffer(GL_ARRAY_BUFFER, m_boneWeightVboIds[j]);
                        glEnableVertexAttribArray(GL_BONEWEIGHT_ARRAY);
                        glVertexAttribPointer(GL_BONEWEIGHT_ARRAY, 4, GL_FLOAT, GL_FALSE, weightsbuf->stride, (GLvoid*) weightsbuf->getOffset());
                        
        glBindBuffer(GL_ARRAY_BUFFER, m_boneIndexVboIds[j]);
                        glEnableVertexAttribArray(GL_BONEINDEX_ARRAY);
                        glVertexAttribPointer(GL_BONEINDEX_ARRAY, 4, GL_UNSIGNED_BYTE, GL_FALSE, indicesbuf->stride, (GLvoid*) indicesbuf->getOffset());

                    }
                }

if (!useGPUSkinning && PiperGL20::instance()) {
                    // since we use the same shader, turing off matrix skinning this way
                    glUniform1i(PiperGL20::instance()->currentShader()->BoneCountHandle, 0);
                }
#endif                
                glBindBuffer(GL_ELEMENT_ARRAY_BUFFER_ARB, m_staticIndexVboIds[j]);
                Piper::instance()->glDrawElements(GL_TRIANGLES, tot, GL_UNSIGNED_SHORT, (GLvoid*)idxbuf->getOffset());


I believe that there is a constraint on number of bones in matrix, but don't know what the number is for iPhone 3GS. If you need more bones then supported by the platform, you could try to split the mesh to sub meshes (and subskeletons) and render submeshes separately.

6) vertex shader does the rest - for every vertex apply bones transformation matrix.

SkinnedCharacter.vert:

    if (u_BoneCount > 0) {
        highp mat4 mat = u_BoneMatrices[indexOfBone.x] * weightOfBone.x;
        for (int i = 1; i < u_BoneCount; i++) {
            // rotate to use indexOfBone.x in the loop
            indexOfBone = indexOfBone.yzwx;
            weightOfBone = weightOfBone.yzwx;
            if (weightOfBone.x > 0.0) {
                mat = mat + (u_BoneMatrices[indexOfBone.x] * weightOfBone.x);
            }
             
        }
        
        // resulting position after applying skinning
        vertex = mat * a_Vertex;
        normal = mat * a_Normal;

Complete source code is available here.

No comments:

Post a Comment