Skeleton Animation

Because in the final project, one of my main job is animation system developer, so I write my review here.

  • Skeleton animation is a good way to implement the animation in the game engine.
  • Skeleton animation is move the vertices to the correct position based on the animation.
  • For the blend purpose, instead of  one 4×4 matrix, separate the translation, rotation,scale is a better way, it will be more efficiency at CPU.
  • In Maya, I baked the animation to every bone will appear at every keyframe, so I don’t need to handle complicated situation.
  • For efficiency, the same type objects use the same animation data, so every one need a animation temporary data for itself.

 

//while loading, it will store this data structure


struct Bone
{
XMFLOAT3 m_Tran;
XMFLOAT4 m_Rotate;
XMFLOAT3 m_Scale;
};

struct KeyFrame
{
int m_numBones;
float m_Time;
std::vector<Bone> m_World;
};

In one animation state, I need to interpolate between two different key frames, so the animation will be smooth


//Scale , translation use lerp

//rotation use slerp

//use r-value for speed
void Mesh::DoInterpolate(Bone(&_bone)[MAX_BONES_NUM], interpolator* _interpolate)
{
for (int boneIndex = 0; boneIndex < _interpolate->GetAnimation()->GetKeyFrames()[0].m_World.size(); boneIndex++)
{
//scale
XMStoreFloat3(&_bone[boneIndex].m_Scale,
XMVectorLerp(XMLoadFloat3(&_interpolate->GetAnimation()->GetKeyFrames()[_interpolate->GetPreFrame()].m_World[boneIndex].m_Scale),
XMLoadFloat3(&_interpolate->GetAnimation()->GetKeyFrames()[_interpolate->GetNextFrame()].m_World[boneIndex].m_Scale), _interpolate->GetLamda())
);

//rotation
XMStoreFloat4(&_bone[boneIndex].m_Rotate,
XMQuaternionSlerp(XMLoadFloat4(&_interpolate->GetAnimation()->GetKeyFrames()[_interpolate->GetPreFrame()].m_World[boneIndex].m_Rotate),
XMLoadFloat4(&_interpolate->GetAnimation()->GetKeyFrames()[_interpolate->GetNextFrame()].m_World[boneIndex].m_Rotate), _interpolate->GetLamda())
);

//trans
XMStoreFloat3(&_bone[boneIndex].m_Tran,
XMVectorLerp(XMLoadFloat3(&_interpolate->GetAnimation()->GetKeyFrames()[_interpolate->GetPreFrame()].m_World[boneIndex].m_Tran),
XMLoadFloat3(&_interpolate->GetAnimation()->GetKeyFrames()[_interpolate->GetNextFrame()].m_World[boneIndex].m_Tran), _interpolate->GetLamda())
);
}
}


if transition between two animations, do blend two world function


void Mesh::BlendTwoWorldMatrix()
{
calLamda = m_blenderCurrTime / m_blenderTotalTime;

for (int boneIndex = 0; boneIndex < m_CurrInterpolator.GetAnimation()->GetKeyFrames()[0].m_World.size(); boneIndex++)
{
//blend scale
XMStoreFloat3(&m_CurrInterpolator.m_interPos[boneIndex].m_Scale,
XMVectorLerp(
XMLoadFloat3(&m_CurrInterpolator.m_interPos[boneIndex].m_Scale),
XMLoadFloat3(&m_NextInterpolator.m_interPos[boneIndex].m_Scale), calLamda)
);

//blend rotation
XMStoreFloat4(&m_CurrInterpolator.m_interPos[boneIndex].m_Rotate,
XMVectorLerp(
XMLoadFloat4(&m_CurrInterpolator.m_interPos[boneIndex].m_Rotate),
XMLoadFloat4(&m_NextInterpolator.m_interPos[boneIndex].m_Rotate), calLamda)
);

//blend trans
XMStoreFloat3(&m_CurrInterpolator.m_interPos[boneIndex].m_Tran,
XMVectorLerp(
XMLoadFloat3(&m_CurrInterpolator.m_interPos[boneIndex].m_Tran),
XMLoadFloat3(&m_NextInterpolator.m_interPos[boneIndex].m_Tran), calLamda)
);
}
}


For calculate the inverseBind pose, this only need to do one time at beginning, it is used as next function, do in the CPU


void Mesh::SetInvBindPose()
{
for (int i = 0; i < joints.size(); i++)
XMStoreFloat4x4(&InvBindPos[i], (XMMatrixInverse(nullptr, XMLoadFloat4x4(&joints[i].bind_pose_transform))) );
}

For the animation, every vertices is needed to be the correct position, the way to do is based on the joint it attached, the formula is the (vertex data )x(inverseJoint) x(current Pose)


//update joint offset at CPU, it means how many amount the vertex is needed to adjust //in the GPU
void Mesh::UpdateJointsOffset()
{
for (int i = 0; i < joints.size(); i++)
{
//currPos = invBindPos * m_world
XMStoreFloat4x4(&m_CurrentPos.JointOffset[i], XMMatrixMultiply(XMLoadFloat4x4(&InvBindPos[i]), XMLoadFloat4x4(&m_BlendWorld.Joint[i])));
}
}

So in the HLSL , I store the joint offset as a buffer, only need to update every frame,

every vertex will have up to 4 weight,

cbuffer ANIMATION : register(b2)
{
float4x4 jointOffset[MAX_BONES];
}

OUTPUT_VERTEX main(INPUT_VERTEX input)
{
OUTPUT_VERTEX sendOut = (OUTPUT_VERTEX)0;

sendOut.texcoords = input.texcoords.xy;

float4 localPos = mul(float4(input.coordinate, 1.0f), jointOffset[input.indices[0]])*input.weights.x;
localPos += mul(float4(input.coordinate, 1.0f), jointOffset[input.indices[1]])*input.weights.y;
localPos += mul(float4(input.coordinate, 1.0f), jointOffset[input.indices[2]])*input.weights.z;
localPos += mul(float4(input.coordinate, 1.0f), jointOffset[input.indices[3]])*input.weights.w;
float4 pos = mul(float4(localPos.xyz, 1.0f), worldMatrix);

float4 localNorm = mul(float4(input.normals.xyz, 0.0f), jointOffset[input.indices[0]])*input.weights.x;
localNorm += mul(float4(input.normals.xyz, 0.0f), jointOffset[input.indices[1]])*input.weights.y;
localNorm += mul(float4(input.normals.xyz, 0.0f), jointOffset[input.indices[2]])*input.weights.z;
localNorm += mul(float4(input.normals.xyz, 0.0f), jointOffset[input.indices[3]])*input.weights.w;
float3 normWS = normalize(mul(localNorm.xyz, (float3x3)worldMatrix));
sendOut.normals = normWS;

pos = mul(pos, viewMatrix);
pos = mul(pos, projMatrix);
sendOut.coordinate = pos;

return sendOut;
}

So the gun and hand can be run at different animation state and blend right now!