Skeletal animation - Jeanmilost/CompactStar GitHub Wiki
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:
- The Autodesk Filmbox (.fbx) format
- The Collada (.dae) format
- The DirectX (.x) format
- The Quake 4 (.md5) format
- The Inter-Quake Model (.iqm) format
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.
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.
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 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 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.
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.
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.
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.
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 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.
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.
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);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));
}
}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);
}
}