Introduction

Before I moved to Stockholm to become a game programmer, I spent my free time learning game development by getting to know software like Blender and game engines like Unity and Unreal Engine. While working on my little projects, the thing I loved to do most (second to programming) was animating characters.

A game project I was working on in Unity before The Game Assembly. I created, rigged and animated the model in Blender.

So when the time came to implement animation in my graphics engine I was excited! An animation system is a delicate machine and there are a lot of things that can go wrong. I spent many late hours and a lot of trying, failing, and trying again. But eventually I succeeded! When I finally got it to work it was easy for me to transfer it from my engine to the group’s engine, since I co-created the graphics engine with another member of the group.


Implementation

Initially I had to create data structures for the data FBX SDK would give me from the .fbx file.

Our ModelData class will contain a skeleton which contains all the bones. Each bone has a bind pose, the pose in which the model is created and rigged. These are essential when calculating the final transform of the bones later.

struct Bone
{
		std::string myName = "None";
		DirectX::XMMATRIX myBindPose = {};
		int myParentIndex = -1;
};
	
struct Skeleton
{
		int myRootBoneIndex = -1;
		std::vector<Bone> myBones = {};
		std::vector<std::string> myBoneNames = {};
};

An animation clip contains keyframes, consisting of transform matrices for each bone relative to its parent. This decides which pose a skeleton should have at a certain frame.

struct Keyframe
{
		std::vector<DirectX::XMMATRIX> myBoneTransforms{};
};

struct AnimationClip
{
		std::string myName = "None";
		float myFps = 0.0f;
		float myDuration = 0.0f;
		std::vector<Keyframe> myKeyframes = {};

		std::vector<AnimationEvent*> myEvents = {};
};

Playback

An animation is made up of keyframes. If your animation have 24 frames per second (FPS) and you play it on a lower speed without interpolation, the bones will appear to jump in place from one keyframe to another like this.

When interpolating, the animation player will calculate the pose between keyframe N and N+1, lerping between them to make a smooth transition between the frames.

In this code, we interpolate between keyframes and apply the pose to a buffer that we pass to the model’s vertex shader:

// Calculate the current frame and delta
const float frameRate = 1.0f / aOutAnimation.myClip->fps;
const float result = aOutAnimation.myTime / frameRate;
const int frame = static_cast<int>(std::floor(result)); // Current frame
const float delta = result - static_cast<float>(frame); // Progress to the next frame

aOutAnimation.UpdateEvents(frame);

// Interpolate between current and next frame
for (size_t i = 0; i < skeleton->myBones.size(); i++)
{
    DirectX::XMMATRIX currentFramePose = aOutAnimation.myClip->keyframes[frame].boneTransforms[i];
		DirectX::XMMATRIX nextFramePose = aOutAnimation.myClip->keyframes[(frame + 1) % 
		                                  aOutAnimation.myClip->keyframes.size()].boneTransforms[i];
    // Interpolate between current and next frame using delta
    DirectX::XMMATRIX blendedPose = currentFramePose + delta * (nextFramePose - currentFramePose);

		const int parentIndex = skeleton->myBones[i].myParentIndex;

		if (parentIndex >= 0)
		{
				// Accumulate relative transformation
				aOutAnimation.myCombinedTransforms[i] = blendedPose * 
				                                        aOutAnimation.myCombinedTransforms[parentIndex];
		}
		else
		{
		    // Root bone, use absolute transformation
		    aOutAnimation.myCombinedTransforms[i] = blendedPose;
		}

		aOutAnimation.myFinalTransforms[i] = skeleton->myBones[i].myBindPose * 
		                                     aOutAnimation.myCombinedTransforms[i];
}

Blending

Initially, blending between animations was a bit tricky to achieve the expected results. What helped blending poses correctly was, instead of directly applying an animation clip on a character, I created an Animation class:

struct Animation
{
		AnimationClip* myClip = nullptr;
		float myTime = 0.0f;
		float mySpeed = 1.0f;
		bool isPlaying = false;
		bool isLooping = true;

		std::vector<DirectX::XMMATRIX> myCombinedTransforms = {};
		std::vector<DirectX::XMMATRIX> myFinalTransforms = {};	
}

I could then play an animation clip directly to an animation object without even having a character. Blending poses was then just a simple linear interpolation between two animations. The final animation is then applied to the character.

for (size_t i = 0; i < skeleton->myBones.size(); i++)
{
	  {
		    const DirectX::XMMATRIX fromFramePose = aFromAnimation.myCombinedTransforms[i];
		    const DirectX::XMMATRIX toFramePose = aToAnimation.myCombinedTransforms[i];
		
		    // Blended pose needs to be multiplication of decomposed matrices
		    DirectX::XMVECTOR fromScale, fromRotation, fromTranslation;
		    DirectX::XMVECTOR toScale, toRotation, toTranslation;
		    DirectX::XMMatrixDecompose(&fromScale, &fromRotation, &fromTranslation, fromFramePose);
		    DirectX::XMMatrixDecompose(&toScale, &toRotation, &toTranslation, toFramePose);
		    
		    DirectX::XMVECTOR rotationOrigin = DirectX::XMVectorSet(0.0f, 0.0f, 0.0f, 1.0f);
		    
		    DirectX::XMMATRIX blendedPose = DirectX::XMMatrixAffineTransformation(
			      DirectX::XMVectorLerp(fromScale, toScale, aBlendFactor),
			      rotationOrigin,
			      DirectX::XMQuaternionSlerp(fromRotation, toRotation, aBlendFactor),
			      DirectX::XMVectorLerp(fromTranslation, toTranslation, aBlendFactor));
			      
		    modelData->myFinalTransforms[i] = skeleton->myBones[i].myBindPose * blendedPose;
	  }
}

Animation events

struct AnimationEvent : ES::Event
{
		AnimationEvent() = default;
		~AnimationEvent() = default;

		std::string myName = "None";
		bool myIsTriggeredThisFrame = false;
		unsigned int myFrameIndex = 0;
};

void UpdateEvents(const unsigned aFrame) const
{
		for (const auto& event : myClip->myEvents)
  	{
	  		if (event->myIsTriggeredThisFrame)
				{
		  			continue;
				}
				if (event->myFrameIndex > aFrame)
				{
			  		event->myIsTriggeredThisFrame = true;
				  	ES::EventSystem::GetInstance().QueueEvent(event);
				}
		}
}

Next post