Skeletal animation - Jeanmilost/CompactStar GitHub Wiki

Introduction

A skeletal animation, also called rigging, is a technique consisting to animate a model by linking a skeleton, which is in fact a matrix hierarchy (in this context a matrix is named a bone), to a vertex buffer. Basically a such animation contains 3 important concepts: A skeleton, weights and a vertex buffer.

The skeletal animations appeared soon in the 3d engines development. One of the first game supporting a such technique was Half Life, indeed the GoldSrc engine, which was a highly modified version of the both Quake I and Quake II engines, contained several important improvements, among others the facial and skeletal animations for the models.

Today a huge number of popular model files, proprietary or open-source, embed data for skeletal animations, among others the most widely used, as e.g:

Many of these file formats provide their own SDK. However the CompactStar engine contains a standalone (but very basic and incomplete) reader for the DirectX and Collada file formats. The reason was because I needed to remain as independent as possible from third-party SDKs (because the CompactStar Engine may be used in very small embedded compilers like the Mobile C Compiler), and because I was interested to learn how these animation systems were working.

Note that I also wrote several c++ demos, containing a standalone reader, for the following formats: DirectX, Filmbox and MakeHuman.

Basic concepts

A skeletal animation depends on 3 basic concepts: a skeleton, a vertex buffer, and the weights which are linking the skeleton and the vertex buffer together.

The skeleton

A skeleton is a hierarchy of bones. In this context, what is named a bone is simply a 4x4 matrix, which contains a local transformation to apply to a group of vertices in the vertex buffer. When a transformation should be applied, a final matrix is calculated, based on the current bone, its hierarchy, and a special matrix called the bind pose matrix, which should be used to put the bone in the local weight space.

All the model animations are performed through the skeleton. Commonly the animation data contain the state of each bone linked to a time key. This is why skeletal animations allow a great flexibility, once implemented, because create a new animation and rig it to the skeleton is a very easy task, and requires much less resources than a frame animation.

A such animation may also be interpolated, in this case, it's enough to mix the matrices of the 2 closest time keys to get the intermediate one.

The vertex buffer

The vertex buffer contains the model geometry. It is represented in a default pose, commonly a so called T-Pose.

The content of the vertex buffer itself depends on the model format, each file type provides its own standard. Normally the skeleton and the weights are applied only to the vertices, the other values aren't modified.

As for the frame animations and their interpolations, a good 3d engine should apply the mathematics relatives to the skeletal animation on the GPU side, in other words in a Shader program. However the CompactStar Engine applies these calculation on the CPU side, because this code is first of all experimental (i.e. targeted to learn something), and I wanted to keep it as simple and understandable as possible.

Although I will not further develop this last point, there are several documents available on the web about how to implement a such Shader program, like this one, this one, or this one.

The weights

The weights are a kind of relation between the bones and the vertices. They may be described as the force of influence that a bone has on a vertex. The higher the weight value, the more the vertex is influenced by the bone. I represented a such relation in blue on the left image.

A bone may own several weights, and a vertex may have several weights influencing it.

The weights are also commonly associated with a special matrix called the bind pose matrix. It is used to transform the position, orientation and scale of a bone relative to its parent bone or relative to the model space.

Here is a document describing the concepts associated to the skeletal animation, and their descriptions. Also this other document describes in details the basis to implement a skeletal animation.

How the skeletal animations are implemented in CompactStar

The CompactStar Engine contains a very basic reader for the DirectX (.x), the Collada (.dae), and the Inter-Quake (.iqm) model formats. I'll not describe in details how these formats are built, but their source code are contained in the CSR_X.c and the CSR_X.h files for the DirectX format, the CSR_Collada.c and CSR_Collada.h files for the Collada format, and the CSR_Iqm.c and CSR_Iqm.h files for the Inter-Quake Model (.iqm) format. As for the Quake model format, these readers populate a data structure which may be interpreted and rendered by the engine.

These data structures are also built onto some common structures, which are implemented in the CSR_Model.c and the CSR_Model.h files.

The DirectX model

Take a look to the CSR_X structure:

/**
* DirectX (.x) model
*@note Each mesh is connected to its own weights count and skeleton, sorted in the same order in each list
*/
typedef struct
{
    CSR_Mesh*               m_pMesh;               // meshes composing the model
    size_t                  m_MeshCount;           // mesh item count
    CSR_Skin_Weights_Group* m_pMeshWeights;        // mesh skin weights, in the same order as meshes
    size_t                  m_MeshWeightsCount;    // mesh skin weights item count
    CSR_Bone_Mesh_Binding*  m_pMeshToBoneDict;     // mesh to bone dictionary
    size_t                  m_MeshToBoneDictCount; // mesh to bone dictionary item count
    CSR_Bone*               m_pSkeleton;           // model skeleton
    CSR_AnimationSet_Bone*  m_pAnimationSet;       // set of animations to apply to bones
    size_t                  m_AnimationSetCount;   // animation set count
    int                     m_MeshOnly;            // if activated, only the mesh will be drawn. All other data will be ignored
    int                     m_PoseOnly;            // if activated, the model will take the default pose but will not be animated
} CSR_X;

The m_pMesh member contains a collection of geometries, which represent the model in its default position, and the associated m_MeshCount contains the number of items in the collection. The m_pMeshWeights member is the collection of weights associated to each mesh in the m_pMesh member, and the associated m_MeshWeightsCount contains the number of items in the collection, which should be equal to the m_MeshCount member. The m_pMeshToBoneDict member is a collection containing a dictionary of binding between a mesh and its associated bones, and the associated m_MeshToBoneDictCount contains the number of items in the collection.

The m_pSkeleton member is a binary tree containing the skeleton itself. Each node in the tree is a bone.

The m_pAnimationSet member contains a collection of matrices to apply to the bones in order to animate them, associated with time keys, and the associated m_AnimationSetCount contains the number of items in the collection.

And finally, the m_MeshOnly and m_PoseOnly indicate whether the reader should read only the geometry or the default pose, including the skeleton, but without the animations.

The Collada model

Take a look to the CSR_Collada structure:

/**
* Collada (.dae) model
*/
typedef struct
{
    CSR_Mesh*               m_pMesh;               // meshes composing the model
    size_t                  m_MeshCount;           // mesh item count
    CSR_Skin_Weights_Group* m_pMeshWeights;        // mesh skin weights, in the same order as meshes
    size_t                  m_MeshWeightsCount;    // mesh skin weights item count
    CSR_Bone_Mesh_Binding*  m_pMeshToBoneDict;     // mesh to bone dictionary
    size_t                  m_MeshToBoneDictCount; // mesh to bone dictionary item count
    CSR_Skeleton*           m_pSkeletons;          // model skeletons
    size_t                  m_SkeletonCount;       // skeleton count
    CSR_AnimationSet_Bone*  m_pAnimationSet;       // set of animations to apply to bones
    size_t                  m_AnimationSetCount;   // animation set count
    int                     m_MeshOnly;            // if activated, only the mesh will be drawn. All other data will be ignored
    int                     m_PoseOnly;            // if activated, the model will take the default pose but will not be animated
} CSR_Collada;

The m_pMesh member contains a collection of geometries, which represent the model in its default position, and the associated m_MeshCount contains the number of items in the collection. The m_pMeshWeights member is the collection of weights associated to each mesh in the m_pMesh member, and the associated m_MeshWeightsCount contains the number of items in the collection, which should be equal to the m_MeshCount member. The m_pMeshToBoneDict member is a collection containing a dictionary of binding between a mesh and its associated bones, and the associated m_MeshToBoneDictCount contains the number of items in the collection.

The m_pSkeleton member is a binary tree containing the skeleton itself. Each node in the tree is a bone.

The m_pAnimationSet member contains a collection of matrices to apply to the bones in order to animate them, associated with time keys, and the associated m_AnimationSetCount contains the number of items in the collection.

And finally, the m_MeshOnly and m_PoseOnly indicate whether the reader should read only the geometry or the default pose, including the skeleton, but without the animations.

The Inter-Quake model

Take a look to the CSR_IQM structure:

/**
* Inter-Quake Model (.iqm)
*@note Each mesh is connected to its own weights count and skeleton, sorted in the same order in each list
*/
typedef struct
{
    CSR_Mesh*               m_pMesh;               // meshes composing the model
    size_t                  m_MeshCount;           // mesh item count
    CSR_Skin_Weights_Group* m_pMeshWeights;        // mesh skin weights, in the same order as meshes
    size_t                  m_MeshWeightsCount;    // mesh skin weights item count
    CSR_Bone_Mesh_Binding*  m_pMeshToBoneDict;     // mesh to bone dictionary
    size_t                  m_MeshToBoneDictCount; // mesh to bone dictionary item count
    CSR_Bone*               m_pSkeleton;           // model skeleton
    CSR_AnimationSet_Bone*  m_pAnimationSet;       // set of animations to apply to bones
    size_t                  m_AnimationSetCount;   // animation set count
    int                     m_MeshOnly;            // if activated, only the mesh will be drawn. All other data will be ignored
    int                     m_PoseOnly;            // if activated, the model will take the default pose but will not be animated
} CSR_IQM;

The m_pMesh member contains a collection of geometries, which represent the model in its default position, and the associated m_MeshCount contains the number of items in the collection. The m_pMeshWeights member is the collection of weights associated to each mesh in the m_pMesh member, and the associated m_MeshWeightsCount contains the number of items in the collection, which should be equal to the m_MeshCount member. The m_pMeshToBoneDict member is a collection containing a dictionary of binding between a mesh and its associated bones, and the associated m_MeshToBoneDictCount contains the number of items in the collection.

The m_pSkeleton member is a binary tree containing the skeleton itself. Each node in the tree is a bone.

The m_pAnimationSet member contains a collection of matrices to apply to the bones in order to animate them, associated with time keys, and the associated m_AnimationSetCount contains the number of items in the collection.

And finally, the m_MeshOnly and m_PoseOnly indicate whether the reader should read only the geometry or the default pose, including the skeleton, but without the animations.

The common structures

The model common structures are implemented in the CSR_Model.c and the CSR_Model.h files.

These structures are common to each model formats and may be used as a base to work with them.

The first important structures are the CSR_Bone and the CSR_Skeleton ones, which contain all the skeleton data. The CSR_Skeleton structure is a binary tree containing several CSR_Bone structures as nodes.

Take a look to these structures:

/**
* Bone, it's a local transformation to apply to a mesh and belonging to a skeleton
*/
typedef struct CSR_tagBone
{
           char*        m_pName;         // bone name
           CSR_Matrix4  m_Matrix;        // matrix containing the bone transformation to apply
    struct CSR_tagBone* m_pParent;       // bone parent, root bone if 0
    struct CSR_tagBone* m_pChildren;     // bone children
           size_t       m_ChildrenCount; // bone children count
           void*        m_pCustomData;   // additional custom data. Be careful, this data may not be released internally
} CSR_Bone;

The m_Matrix member is the bone matrix, which contains the transformation to apply to the vertices group influenced through the weights. The m_pParent and m_pChildren members contains the node parent and children relationship, and the m_ChildrenCount member indicates how many children the node contains. The m_pCustomData member contains a special data, which depends on the model format implementation. It may or may not be used. Finally the m_pName member is the bone name, as read from the model file, and used to search the bone to link it with the weights and geometries with which it is associated.

/**
* Skeleton, it's a set of local transformations named bones
*/
typedef struct
{
    char*       m_pId;           // skeleton identifier
    char*       m_pTarget;       // target weights identifier
    CSR_Bone*   m_pRoot;         // root bone
    CSR_Matrix4 m_InitialMatrix; // initial matrix
} CSR_Skeleton;

The m_pRoot member contains the root bone of the skeleton, see the CSR_Bone structure above for further details. The m_pId member contains the skeleton identifier, in case the model contains several skeletons (it may be the case e.g. in the Collada files). The m_pTarget member contains the target weights identifier, if the model contains one. The m_InitialMatrix member contains the initial matrix to apply to the skeleton, if any.

The second important structures are the CSR_Bone_Mesh_Binding, the CSR_Skin_Weight_Index_Table, the CSR_Skin_Weights, and the CSR_Skin_Weights_Group ones. These structures contain the data relative to the weights, and their links to the skeleton and the vertex buffer.

Take a look to these structures:

/**
* Binding between a bone and a mesh
*/
typedef struct
{
    CSR_Bone* m_pBone;     // bone binded with mesh
    size_t    m_MeshIndex; // mesh index binded with bone
} CSR_Bone_Mesh_Binding;

This above structure represents a direct link between a mesh and a bone in the skeleton. The m_pBone member is a pointer to the bone, while the m_MeshIndex member is an index to a mesh in the mesh collection of the model.

/**
* Skin weights index table
*/
typedef struct
{
    size_t* m_pData; // indices of the vertices to modify in the source mesh
    size_t  m_Count; // indices count
} CSR_Skin_Weight_Index_Table;

The above skin weight index table structure contains the indices of the vertices impacted in the target vertex buffer. This structure is required, because generally the vertex buffers provided by the readers in CompactStar are expanded from an indices table in the source model file, which points the vertices in a vertex table. As an index may represent several vertices in the final vertex buffer, but is linked to a weight, this structure is required to keep which vertices in the final vertex buffer are impacted by the weight associated to the vertex index in the source indices table.

The m_pData member contains the indices of the vertices impacted by the weight in the target vertex buffer, and the m_Count member contains the indices count.

/**
* Skin weights, it's a group of vertices influenced by a bone
*/
typedef struct
{
    char*                        m_pBoneName;       // linked bone name
    CSR_Bone*                    m_pBone;           // linked bone
    CSR_Matrix4                  m_Matrix;          // matrix to transform the mesh vertices to the bone space
    size_t                       m_MeshIndex;       // source mesh index
    CSR_Skin_Weight_Index_Table* m_pIndexTable;     // table containing the indices of the vertices to modify in the source mesh
    size_t                       m_IndexTableCount; // mesh indices count
    float*                       m_pWeights;        // weights indicating the bone influence on vertices, between 0.0f and 1.0f
    size_t                       m_WeightCount;     // weight count
} CSR_Skin_Weights;

The above structure represents a weight, and its binding with the skeleton and the vertices to modify in the final vertex buffer. The m_MeshIndex member contains the mesh index containing the vertices to modify. The m_pWeights member contains a collection of weights values the bone is owning, and the m_WeightCount member is the item count contained in the collection. The m_pIndexTable member is a collection of indices pointing to the vertices to modify in the target mesh vertex buffer, see the description of the CSR_Skin_Weight_Index_Table structure above for further details. The m_IndexTableCount contains the item count contained in the m_pIndexTable collection. The m_pBone member is the bone owning this group of weights, and the m_pBoneName member contains the bone name, useful to search it in the skeleton. The m_Matrix member contains the bind pose matrix.

/**
* Skin weights group
*@note Generally used to contain all skin weights belonging to a mesh
*/
typedef struct
{
    CSR_Skin_Weights* m_pSkinWeights; // skin weights list
    size_t            m_Count;        // skin weights count
} CSR_Skin_Weights_Group;

The above structure contains a group of weights, which represents all the weights associated with a mesh, and how they influence the vertices in the target vertex buffer. The m_pSkinWeights member is a collection of skin weights, and the m_Count member is the item count in the collection.

The third important structures are the CSR_AnimationKey, the CSR_AnimationKeys, the CSR_Animation_Bone and the CSR_AnimationSet_Bone ones, as well as the CSR_EAnimKeyType enumerator. These structures contain the data required to animate the skeleton.

Take a look to these structures:

/**
* Animation key type
*/
typedef enum
{
    CSR_KT_Unknown  = -1,
    CSR_KT_Rotation =  0,
    CSR_KT_Scale    =  1,
    CSR_KT_Position =  2,
    CSR_KT_Matrix   =  4
} CSR_EAnimKeyType;

The above enumerator just enumerates the possible types an animation key may represents.

/**
* Animation key, may be a rotation, a translation, a scale, a matrix, ...
*/
typedef struct
{
    size_t m_Frame;
    float* m_pValues;
    size_t m_Count;
} CSR_AnimationKey;

The above structure represents an animation key, i.e. the current state of a bone at a given frame. The m_pValues member contains the transformation values, and the m_Count member contains the value count. The m_Frame member contains the frame index.

/**
* Animation key list
*/
typedef struct
{
    CSR_EAnimKeyType  m_Type;
    CSR_AnimationKey* m_pKey;
    size_t            m_Count;
    int               m_ColOverRow;
} CSR_AnimationKeys;

The above structure contains a collection of keys, which represent together a transformation. The m_Type member contains the transformation type. The m_pKey member is a collection of animation keys, and the m_Count member is the animation key count in the collection. The m_ColOverRow member is a flag indicating the matrix arrangement, in case the keys contain one. If set to 1, the columns take precedence over the rows.

/**
* Animation (based on bones)
*/
typedef struct
{
    char*              m_pBoneName;
    CSR_Bone*          m_pBone;
    CSR_AnimationKeys* m_pKeys;
    size_t             m_Count;
} CSR_Animation_Bone;

The above structure represents a complete animation cycle for a bone. The m_pKeys member contains a collection of transformations to apply over the time, and the m_Count member is the item count in the collection. The m_pBone member is the bone on which the animation should be applied, and the m_pBoneName member is the bone name, as read from the model file, useful when the bone should be searched in the skeleton.

/**
* Animation set (based on bones)
*/
typedef struct
{
    CSR_Animation_Bone* m_pAnimation;
    size_t              m_Count;
} CSR_AnimationSet_Bone;

The above structure represents a complete skeletal animation. The m_pAnimation member contains a collection of animation for each bone, and the m_Count member is the item count in the collection.

And this is all for the common structures.

How the animation is applied

In this section I'll explain how the skeletal animations are applied to the vertex buffers, and how the model is rendered in the CompactStar engine.

Calculating the final matrix

The CompactStar engine contains several functions useful to calculate the final matrix to apply to a target vertex, in order to calculate the vertex buffer to render. These functions are commonly located in the CSR_Model.c and the CSR_Model.h files.

Take a look to the following function:

void csrBoneGetMatrix(const CSR_Bone* pBone, CSR_Matrix4* pInitialMatrix, CSR_Matrix4* pMatrix)
{
    CSR_Matrix4 localMatrix;

    // no bone?
    if (!pBone)
        return;

    // no output matrix to write to?
    if (!pMatrix)
        return;

    // set the output matrix as identity
    csrMat4Identity(pMatrix);

    // iterate through bones
    while (pBone)
    {
        // get the previously stacked matrix as base to calculate the new one
        localMatrix = *pMatrix;

        // stack the previously calculated matrix with the current bone one
        csrMat4Multiply(&localMatrix, &pBone->m_Matrix, pMatrix);

        // go to parent bone
        pBone = pBone->m_pParent;
    }

    // initial matrix provided?
    if (pInitialMatrix)
    {
        // get the previously stacked matrix as base to calculate the new one
        localMatrix = *pMatrix;

        // stack the previously calculated matrix with the initial one
        csrMat4Multiply(&localMatrix, pInitialMatrix, pMatrix);
    }
}

This function resolves the bone hierarchy and returns the matrix containing the transformation representing the bone and all its parents. It also applies an optional initial matrix, if one is defined.

The csrBoneGetAnimMatrix function is similar to the previous one, except that it applies the transformation from the animation set instead of the skeleton default pose:

void csrBoneGetAnimMatrix(const CSR_Bone*              pBone,
                          const CSR_AnimationSet_Bone* pAnimSet,
                                size_t                 frameIndex,
                                CSR_Matrix4*           pInitialMatrix,
                                CSR_Matrix4*           pMatrix)
{
    CSR_Matrix4 localMatrix;
    CSR_Matrix4 animMatrix;

    // no bone?
    if (!pBone)
        return;

    // no output matrix to write to?
    if (!pMatrix)
        return;

    // set the output matrix as identity
    csrMat4Identity(pMatrix);

    // iterate through bones
    while (pBone)
    {
        // get the previously stacked matrix as base to calculate the new one
        localMatrix = *pMatrix;

        // get the animated bone matrix matching with frame. If not found use the identity one
        if (!csrBoneAnimGetAnimMatrix(pAnimSet, pBone, frameIndex, &animMatrix))
            animMatrix = pBone->m_Matrix;

        // stack the previously calculated matrix with the current bone one
        csrMat4Multiply(&localMatrix, &animMatrix, pMatrix);

        // go to parent bone
        pBone = pBone->m_pParent;
    }

    // initial matrix provided?
    if (pInitialMatrix)
    {
        // get the previously stacked matrix as base to calculate the new one
        localMatrix = *pMatrix;

        // stack the previously calculated matrix with the initial one
        csrMat4Multiply(&localMatrix, pInitialMatrix, pMatrix);
    }
}

The csrBoneAnimGetAnimMatrix function called internally in the above function is the following:

int csrBoneAnimGetAnimMatrix(const CSR_AnimationSet_Bone* pAnimSet,
                             const CSR_Bone*              pBone,
                                   size_t                 frame,
                                   CSR_Matrix4*           pMatrix)
{
    size_t i;
    size_t j;
    size_t k;

    // no animation set?
    if (!pAnimSet)
        return 0;

    // no bone?
    if (!pBone)
        return 0;

    // no output matrix?
    if (!pMatrix)
        return 0;

    // iterate through animations
    for (i = 0; i < pAnimSet->m_Count; ++i)
    {
        #ifdef _MSC_VER
            size_t         rotFrame;
            size_t         nextRotFrame;
            size_t         posFrame;
            size_t         nextPosFrame;
            size_t         scaleFrame;
            size_t         nextScaleFrame;
            float          frameDelta;
            float          frameLength;
            float          interpolation;
            CSR_Quaternion rotation        = {0};
            CSR_Quaternion nextRotation    = {0};
            CSR_Quaternion finalRotation   = {0};
            CSR_Vector3    position        = {0};
            CSR_Vector3    nextPosition    = {0};
            CSR_Vector3    finalPosition   = {0};
            CSR_Vector3    scaling         = {0};
            CSR_Vector3    nextScaling     = {0};
            CSR_Vector3    finalScaling    = {0};
            CSR_Matrix4    scaleMatrix     = {0};
            CSR_Matrix4    rotateMatrix    = {0};
            CSR_Matrix4    translateMatrix = {0};
            CSR_Matrix4    buildMatrix     = {0};
        #else
            size_t         rotFrame;
            size_t         nextRotFrame;
            size_t         posFrame;
            size_t         nextPosFrame;
            size_t         scaleFrame;
            size_t         nextScaleFrame;
            float          frameDelta;
            float          frameLength;
            float          interpolation;
            CSR_Quaternion rotation;
            CSR_Quaternion nextRotation;
            CSR_Quaternion finalRotation;
            CSR_Vector3    position;
            CSR_Vector3    nextPosition;
            CSR_Vector3    finalPosition;
            CSR_Vector3    scaling;
            CSR_Vector3    nextScaling;
            CSR_Vector3    finalScaling;
            CSR_Matrix4    scaleMatrix;
            CSR_Matrix4    rotateMatrix;
            CSR_Matrix4    translateMatrix;
            CSR_Matrix4    buildMatrix;
        #endif

        // found the animation matching with the bone for which the matrix should be get?
        if (pAnimSet->m_pAnimation[i].m_pBone != pBone)
            continue;

        rotFrame       = 0;
        nextRotFrame   = 0;
        posFrame       = 0;
        nextPosFrame   = 0;
        scaleFrame     = 0;
        nextScaleFrame = 0;

        // iterate through animation keys
        for (j = 0; j < pAnimSet->m_pAnimation[i].m_Count; ++j)
        {
            size_t keyIndex = 0;

            // iterate through animation key items
            for (k = 0; k < pAnimSet->m_pAnimation[i].m_pKeys[j].m_Count; ++k)
                if (frame >= pAnimSet->m_pAnimation[i].m_pKeys[j].m_pKey[k].m_Frame)
                    keyIndex = k;
                else
                    break;

            // search for keys type
            switch (pAnimSet->m_pAnimation[i].m_pKeys[j].m_Type)
            {
                case CSR_KT_Rotation:
                    if (pAnimSet->m_pAnimation[i].m_pKeys[j].m_pKey[keyIndex].m_Count != 4)
                        return 0;

                    // get the rotation quaternion at index
                    rotation.m_W = pAnimSet->m_pAnimation[i].m_pKeys[j].m_pKey[keyIndex].m_pValues[0];
                    rotation.m_X = pAnimSet->m_pAnimation[i].m_pKeys[j].m_pKey[keyIndex].m_pValues[1];
                    rotation.m_Y = pAnimSet->m_pAnimation[i].m_pKeys[j].m_pKey[keyIndex].m_pValues[2];
                    rotation.m_Z = pAnimSet->m_pAnimation[i].m_pKeys[j].m_pKey[keyIndex].m_pValues[3];
                    rotFrame     = pAnimSet->m_pAnimation[i].m_pKeys[j].m_pKey[keyIndex].m_Frame;

                    // get the next rotation quaternion
                    if (keyIndex + 1 >= pAnimSet->m_pAnimation[i].m_pKeys[j].m_Count)
                    {
                        nextRotation.m_W = pAnimSet->m_pAnimation[i].m_pKeys[j].m_pKey[0].m_pValues[0];
                        nextRotation.m_X = pAnimSet->m_pAnimation[i].m_pKeys[j].m_pKey[0].m_pValues[1];
                        nextRotation.m_Y = pAnimSet->m_pAnimation[i].m_pKeys[j].m_pKey[0].m_pValues[2];
                        nextRotation.m_Z = pAnimSet->m_pAnimation[i].m_pKeys[j].m_pKey[0].m_pValues[3];
                        nextRotFrame     = pAnimSet->m_pAnimation[i].m_pKeys[j].m_pKey[0].m_Frame;
                    }
                    else
                    {
                        nextRotation.m_W = pAnimSet->m_pAnimation[i].m_pKeys[j].m_pKey[keyIndex + 1].m_pValues[0];
                        nextRotation.m_X = pAnimSet->m_pAnimation[i].m_pKeys[j].m_pKey[keyIndex + 1].m_pValues[1];
                        nextRotation.m_Y = pAnimSet->m_pAnimation[i].m_pKeys[j].m_pKey[keyIndex + 1].m_pValues[2];
                        nextRotation.m_Z = pAnimSet->m_pAnimation[i].m_pKeys[j].m_pKey[keyIndex + 1].m_pValues[3];
                        nextRotFrame     = pAnimSet->m_pAnimation[i].m_pKeys[j].m_pKey[keyIndex + 1].m_Frame;
                    }

                    continue;

                case CSR_KT_Scale:
                    if (pAnimSet->m_pAnimation[i].m_pKeys[j].m_pKey[keyIndex].m_Count != 3)
                        return 0;

                    // get the scale values at index
                    scaling.m_X = pAnimSet->m_pAnimation[i].m_pKeys[j].m_pKey[keyIndex].m_pValues[0];
                    scaling.m_Y = pAnimSet->m_pAnimation[i].m_pKeys[j].m_pKey[keyIndex].m_pValues[1];
                    scaling.m_Z = pAnimSet->m_pAnimation[i].m_pKeys[j].m_pKey[keyIndex].m_pValues[2];
                    scaleFrame  = pAnimSet->m_pAnimation[i].m_pKeys[j].m_pKey[keyIndex].m_Frame;

                    // get the next rotation quaternion
                    if (keyIndex + 1 >= pAnimSet->m_pAnimation[i].m_pKeys[j].m_Count)
                    {
                        nextScaling.m_X = pAnimSet->m_pAnimation[i].m_pKeys[j].m_pKey[0].m_pValues[0];
                        nextScaling.m_Y = pAnimSet->m_pAnimation[i].m_pKeys[j].m_pKey[0].m_pValues[1];
                        nextScaling.m_Z = pAnimSet->m_pAnimation[i].m_pKeys[j].m_pKey[0].m_pValues[2];
                        nextScaleFrame  = pAnimSet->m_pAnimation[i].m_pKeys[j].m_pKey[0].m_Frame;
                    }
                    else
                    {
                        nextScaling.m_X = pAnimSet->m_pAnimation[i].m_pKeys[j].m_pKey[keyIndex + 1].m_pValues[0];
                        nextScaling.m_Y = pAnimSet->m_pAnimation[i].m_pKeys[j].m_pKey[keyIndex + 1].m_pValues[1];
                        nextScaling.m_Z = pAnimSet->m_pAnimation[i].m_pKeys[j].m_pKey[keyIndex + 1].m_pValues[2];
                        nextScaleFrame  = pAnimSet->m_pAnimation[i].m_pKeys[j].m_pKey[keyIndex + 1].m_Frame;
                    }

                    continue;

                case CSR_KT_Position:
                    if (pAnimSet->m_pAnimation[i].m_pKeys[j].m_pKey[keyIndex].m_Count != 3)
                        return 0;

                    // get the position values at index
                    position.m_X = pAnimSet->m_pAnimation[i].m_pKeys[j].m_pKey[keyIndex].m_pValues[0];
                    position.m_Y = pAnimSet->m_pAnimation[i].m_pKeys[j].m_pKey[keyIndex].m_pValues[1];
                    position.m_Z = pAnimSet->m_pAnimation[i].m_pKeys[j].m_pKey[keyIndex].m_pValues[2];
                    posFrame     = pAnimSet->m_pAnimation[i].m_pKeys[j].m_pKey[keyIndex].m_Frame;

                    // get the next rotation quaternion
                    if (keyIndex + 1 >= pAnimSet->m_pAnimation[i].m_pKeys[j].m_Count)
                    {
                        nextPosition.m_X = pAnimSet->m_pAnimation[i].m_pKeys[j].m_pKey[0].m_pValues[0];
                        nextPosition.m_Y = pAnimSet->m_pAnimation[i].m_pKeys[j].m_pKey[0].m_pValues[1];
                        nextPosition.m_Z = pAnimSet->m_pAnimation[i].m_pKeys[j].m_pKey[0].m_pValues[2];
                        nextPosFrame     = pAnimSet->m_pAnimation[i].m_pKeys[j].m_pKey[0].m_Frame;
                    }
                    else
                    {
                        nextPosition.m_X = pAnimSet->m_pAnimation[i].m_pKeys[j].m_pKey[keyIndex + 1].m_pValues[0];
                        nextPosition.m_Y = pAnimSet->m_pAnimation[i].m_pKeys[j].m_pKey[keyIndex + 1].m_pValues[1];
                        nextPosition.m_Z = pAnimSet->m_pAnimation[i].m_pKeys[j].m_pKey[keyIndex + 1].m_pValues[2];
                        nextPosFrame     = pAnimSet->m_pAnimation[i].m_pKeys[j].m_pKey[keyIndex + 1].m_Frame;
                    }

                    continue;

                case CSR_KT_Matrix:
                {
                    if (pAnimSet->m_pAnimation[i].m_pKeys[j].m_pKey[keyIndex].m_Count != 16)
                        return 0;

                    // get the key matrix
                    for (k = 0; k < 16; ++k)
                        if (pAnimSet->m_pAnimation[i].m_pKeys[j].m_ColOverRow)
                            pMatrix->m_Table[k % 4][k / 4] =
                                    pAnimSet->m_pAnimation[i].m_pKeys[j].m_pKey[keyIndex].m_pValues[k];
                        else
                            pMatrix->m_Table[k / 4][k % 4] =
                                    pAnimSet->m_pAnimation[i].m_pKeys[j].m_pKey[keyIndex].m_pValues[k];

                    return 1;
                }

                default:
                    continue;
            }
        }

        // calculate the frame delta, the frame length and the interpolation for the rotation
        frameDelta    = (float)(frame        - rotFrame);
        frameLength   = (float)(nextRotFrame - rotFrame);
        interpolation = frameDelta / frameLength;

        // interpolate the rotation
        csrQuatSlerp(&rotation, &nextRotation, interpolation, &finalRotation);

        // calculate the frame delta, the frame length and the interpolation for the scaling
        frameDelta    = (float)(frame          - scaleFrame);
        frameLength   = (float)(nextScaleFrame - scaleFrame);
        interpolation = frameDelta / frameLength;

        // interpolate the scaling
        finalScaling.m_X = scaling.m_X + ((nextScaling.m_X - scaling.m_X) * interpolation);
        finalScaling.m_Y = scaling.m_Y + ((nextScaling.m_Y - scaling.m_Y) * interpolation);
        finalScaling.m_Z = scaling.m_Z + ((nextScaling.m_Z - scaling.m_Z) * interpolation);

        // calculate the frame delta, the frame length and the interpolation for the rotation
        frameDelta    = (float)(frame        - posFrame);
        frameLength   = (float)(nextPosFrame - posFrame);
        interpolation = frameDelta / frameLength;

        // interpolate the position
        finalPosition.m_X = position.m_X + ((nextPosition.m_X - position.m_X) * interpolation);
        finalPosition.m_Y = position.m_Y + ((nextPosition.m_Y - position.m_Y) * interpolation);
        finalPosition.m_Z = position.m_Z + ((nextPosition.m_Z - position.m_Z) * interpolation);

        // get the rotation quaternion and the scale and translate vectors
        csrMat4Scale(&finalScaling, &scaleMatrix);
        csrQuatToMatrix(&finalRotation, &rotateMatrix);
        csrMat4Translate(&finalPosition, &translateMatrix);

        // build the final matrix
        csrMat4Multiply(&scaleMatrix, &rotateMatrix,    &buildMatrix);
        csrMat4Multiply(&buildMatrix, &translateMatrix, pMatrix);

        return 1;
    }

    return 0;
}

As you can see, the above function reads the data from the animation set, and rebuild the matrix to apply instead of the default one in the skeleton. If the time is located between 2 frames, an interpolation is applied to the transformation, except if the keys contain a matrix.

Finally, the final matrix is calculated by multiplying the above calculated matrix with the bind pose matrix, as in the following code:

// get the final matrix after bones transform
csrMat4Multiply(&pCollada->m_pMeshWeights[i].m_pSkinWeights[j].m_Matrix,
                &boneMatrix,
                &finalMatrix);

Applying the matrix to the vertices

Once calculated, the transformation matrix should be applied to each vertices influenced by the bone in the target vertex buffer to render. Below is the code I use to achieve that, it is located in the CSR_Renderer_OpenGL.h and CSR_Renderer_OpenGL.c files, if the OpenGL renderer is used, or in the CSR_Renderer_Metal.h and CSR_Renderer_Metal.mm files, if the Metal renderer is used:

// apply the bone and its skin weights to each vertices
for (k = 0; k < pCollada->m_pMeshWeights[i].m_pSkinWeights[j].m_IndexTableCount; ++k)
    for (l = 0; l < pCollada->m_pMeshWeights[i].m_pSkinWeights[j].m_pIndexTable[k].m_Count; ++l)
    {
        #ifdef _MSC_VER
            size_t      iX;
            size_t      iY;
            size_t      iZ;
            CSR_Vector3 inputVertex  = {0};
            CSR_Vector3 outputVertex = {0};
        #else
            size_t      iX;
            size_t      iY;
            size_t      iZ;
            CSR_Vector3 inputVertex;
            CSR_Vector3 outputVertex;
        #endif

        // get the next vertex to which the next skin weight should be applied
        iX = pCollada->m_pMeshWeights[i].m_pSkinWeights[j].m_pIndexTable[k].m_pData[l];
        iY = pCollada->m_pMeshWeights[i].m_pSkinWeights[j].m_pIndexTable[k].m_pData[l] + 1;
        iZ = pCollada->m_pMeshWeights[i].m_pSkinWeights[j].m_pIndexTable[k].m_pData[l] + 2;

        // get input vertex
        inputVertex.m_X = pMesh->m_pVB->m_pData[iX];
        inputVertex.m_Y = pMesh->m_pVB->m_pData[iY];
        inputVertex.m_Z = pMesh->m_pVB->m_pData[iZ];

        // apply bone transformation to vertex
        csrMat4Transform(&finalMatrix, &inputVertex, &outputVertex);

        // apply the skin weights and calculate the final output vertex
        pLocalMesh->m_pVB->m_pData[iX] += (outputVertex.m_X * pCollada->m_pMeshWeights[i].m_pSkinWeights[j].m_pWeights[k]);
        pLocalMesh->m_pVB->m_pData[iY] += (outputVertex.m_Y * pCollada->m_pMeshWeights[i].m_pSkinWeights[j].m_pWeights[k]);
        pLocalMesh->m_pVB->m_pData[iZ] += (outputVertex.m_Z * pCollada->m_pMeshWeights[i].m_pSkinWeights[j].m_pWeights[k]);

        // copy the remaining vertex data
        if (pMesh->m_pVB->m_Format.m_Stride > 3)
        {
            const size_t copyIndex = iZ + 1;

            memcpy(&pLocalMesh->m_pVB->m_pData[copyIndex],
                   &pMesh->m_pVB->m_pData[copyIndex],
                    ((size_t)pMesh->m_pVB->m_Format.m_Stride - 3) * sizeof(float));
        }
    }

Drawing the skeleton

The CompactStar engine contains also a small utility function to draw the skeleton itself, for debug purposes. This function is located in the CSR_DebugHelper.h and CSR_DebugHelper.c files. Below is the function I use to draw the bones:

void csrDebugDrawBone(const CSR_Bone*              pBone,
                      const CSR_OpenGLShader*      pShader,
                      const CSR_AnimationSet_Bone* pAnimationSet,
                            CSR_Matrix4*           pInitialMatrix,
                            size_t                 animSetIndex,
                            size_t                 frameIndex,
                            int                    poseOnly)
{
    size_t i;

    if (!pBone)
        return;

    if (!pShader)
        return;

    if (!poseOnly && !pAnimationSet)
        return;

    for (i = 0; i < pBone->m_ChildrenCount; ++i)
    {
        #ifdef _MSC_VER
            CSR_Matrix4 topMatrix    = {0};
            CSR_Matrix4 bottomMatrix = {0};
            CSR_Line    boneLine     = {0};
            CSR_Bone*   pChild       = &pBone->m_pChildren[i];
        #else
            CSR_Matrix4 topMatrix;
            CSR_Matrix4 bottomMatrix;
            CSR_Line    boneLine;
            CSR_Bone*   pChild = &pBone->m_pChildren[i];
        #endif

        // get the bone top matrix
        if (poseOnly)
            csrBoneGetMatrix(pBone, 0, &topMatrix);
        else
            csrBoneGetAnimMatrix(pBone,
                                 &pAnimationSet[animSetIndex],
                                 frameIndex,
                                 pInitialMatrix,
                                 &topMatrix);

        // get the bone bottom matrix
        if (poseOnly)
            csrBoneGetMatrix(pChild, 0, &bottomMatrix);
        else
            csrBoneGetAnimMatrix(pChild,
                                 &pAnimationSet[animSetIndex],
                                 frameIndex,
                                 pInitialMatrix,
                                 &bottomMatrix);

        boneLine.m_Start.m_X = topMatrix.m_Table[3][0];
        boneLine.m_Start.m_Y = topMatrix.m_Table[3][1];
        boneLine.m_Start.m_Z = topMatrix.m_Table[3][2];

        boneLine.m_End.m_X = bottomMatrix.m_Table[3][0];
        boneLine.m_End.m_Y = bottomMatrix.m_Table[3][1];
        boneLine.m_End.m_Z = bottomMatrix.m_Table[3][2];

        boneLine.m_StartColor.m_R = 0.25f;
        boneLine.m_StartColor.m_G = 0.12f;
        boneLine.m_StartColor.m_B = 0.1f;
        boneLine.m_StartColor.m_A = 1.0f;

        boneLine.m_EndColor.m_R = 0.95f;
        boneLine.m_EndColor.m_G = 0.06f;
        boneLine.m_EndColor.m_B = 0.15f;
        boneLine.m_EndColor.m_A = 1.0f;

        boneLine.m_Width          = 1.0f;
        boneLine.m_Smooth         = 1;
        boneLine.m_CustomModelMat = 1;

        glDisable(GL_DEPTH_TEST);
        csrDrawLine(&boneLine, pShader);
        glEnable(GL_DEPTH_TEST);

        csrDebugDrawBone(pChild, pShader, pAnimationSet, pInitialMatrix, animSetIndex, frameIndex, poseOnly);
    }
}
⚠️ **GitHub.com Fallback** ⚠️