Advertisement

Handle blending animations

Started by December 20, 2023 07:03 PM
4 comments, last by snoken 11 months ago

I am trying to do some simple animation blending and I am quite confused on how to handle pushing the new animations to the skeleton and then blending it and then stopping the blending so that the original animation continues playing.

This is how I have my set up at the moment:

I have an animation state and I am switching the state using WSAD keys and pushing the relevant animation clip to blend to.

The playerController is of type AnimationData which stores per frame rotations. The AnimationData class stores a std::vector<AnimationData> transitionTo; which I am pushing animations to in order to transition. Quite new to this and would appreciate any insight on how to make this work correctly.

// player skeleton 
playerController.Render(frameNumber, time);

// Switch the animation being rendered based on the current state of the player
switch (m_AnimState)
{
	// If the player is running, render the running animation
	case Running:
		if(playerController.transitionTo.size() < 1)
		{
			playerController.timeStart = std::chrono::high_resolution_clock::now();
			playerController.transitionTo.push_back(runCycle);
		}
		break;
	// If the player is turning left, render the turning left animation
	case TurnLeft:
		if(playerController.transitionTo.size() < 1)
		{
			playerController.timeStart = std::chrono::high_resolution_clock::now();
			playerController.transitionTo.push_back(veerLeftCycle);
		}

		m_AnimState = Running;
		break;
	// // If the player is turning right, render the turning right animation
	// The default state of the player is in rest pose, (Idle)
	default:
		break;
}

This is how I am rendering the joints and blending the two animations currently

void AnimationData::Render(Mat4& viewMatrix, float scale, int frame, double time)
{ 
	RenderJoint(viewMatrix, Mat4::Identity(), &this->root, scale, frame);
} 


// This will render a joint for a single animation frame
void AnimationData::render_joint(Mat4& viewMatrix, Mat4 parentMatrix, Joint* joint, float scale, int frame)
{ 
	// Time since animation started
	auto currentTime = std::chrono::high_resolution_clock::now();
	double nanoseconds = std::chrono::duration_cast<std::chrono::nanoseconds>(currentTime - timeStart).count();
	double time_in_seconds = nanoseconds / 1e+9;

	// Determine updated pose for current joint
	auto f = (frame + 1) % frame_count;
	Quaternion updatedPose = CalculateNewPose(f, time_in_seconds, scale, joint->id);
	Mat4 finalRotationMatrix = updatedPose.ToRotationMatrix();
	
	vec4 offset_from_parent = ...
	auto Offset = Mat4::Translate({offset.x, offset.y, offset.z});
	
	auto global = Mat4::Identity();
	global = parentMatrix * Offset * finalRotationMatrix;

	// multiply offset by parent matrix to get it into the correct space to render
	offset_from_parent = parentMatrix * offset_from_parent;

	// for each child of the current joint, recursively render the joint
	// using start and end position
	for(auto& child : joint->Children)
	{
        auto end = global * vec3(child.joint_offset[0], child.joint_offset[1], child.joint_offset[2], 1.0f);
		auto endpoint = end.toVec3();
		auto start = offset_from_parent.toVec3();
		RenderCylinder(viewMatrix, start, endpoint, finalRotationMatrix);
        // Recursively render the child joint
        RenderJoint(viewMatrix, global, &child, scale, frame);
	}
} 

// Animation blending
Quaternion AnimationData::BlendPose(vec3& a, vec3& b, float time, float slerpAmount)
{
	// Animation A rotation
	Quaternion a_rotX = Quaternion(a.x, vec3(1.0f, 0.0f, 0.0f).unit()); a_rotX.Normalize();
	Quaternion a_rotY = Quaternion(a.y, vec3(0.0f, 1.0f, 0.0f).unit()); a_rotY.Normalize();
	Quaternion a_rotZ = Quaternion(a.z, vec3(0.0f, 0.0f, 1.0f).unit()); a_rotZ.Normalize();
	Quaternion a_animARotQuat = (a_rotZ * a_rotY) * a_rotX;

	// Animation B rotation
	Quaternion b_rotX = Quaternion(b.x, vec3(1.0f, 0.0f, 0.0f).unit()); b_rotX.Normalize();
	Quaternion b_rotY = Quaternion(b.y, vec3(0.0f, 1.0f, 0.0f).unit()); b_rotY.Normalize();
	Quaternion b_rotZ = Quaternion(b.z, vec3(0.0f, 0.0f, 1.0f).unit()); b_rotZ.Normalize();
	Quaternion b_animARotQuat = (b_rotZ * b_rotY) * b_rotX;

	// Determine the slerp amount using time, ensure its 0-1 range.
	double amount_to_lerp = std::fmod(time, slerpAmount) / slerpAmount;

	Quaternion blendedRot = Slerp(a_animARotQuat, b_animARotQuat, amount_to_lerp);

	return blendedRot;
}

Quaternion AnimationData::CalculateNewPose(int frame, float time, float slerpAmount, int jointID)
{
	// Set up pose for animation A and B
	vec4 translation = vec4(0.0f, 0.0f, 0.0f, 1.0f);

	vec3 anim_pose_A = SampleAnimation(frame, jointID);
	vec3 anim_pose_B = SampleAnimation(frame, jointID);

	// if there is a transition animation clip to transition to, set rotation to the animation clips rotation
	// at this current frame 
	if(!transitionTo.empty())
	{
		// Get the first animation clip and sample animation to get current joint pose transforms
		anim_pose_B = transitionTo[transitionTo.size() - 1].SampleAnimation(frame, jointID);
	}

	// Blend the poses
	Quaternion blendedPose = BlendPose(anim_pose_A, anim_pose_B, time, slerpAmount);

	return blendedPose;
}
Run animation to turn left animation but it's repeating itself

Hi Snoken, this is what I've done:

When adding a new animation to a queue, I specify how I want the animation handled when it's finished playing using an enumeration. My options are:

  1. Fade out: The blending weight should be 1.0 at the end of an animation. With the fade out option, the weights are reduced to 0 over a second or so. When the weights reach zero, the animation is flagged for removal. The effect is the previous animations becomes dominant. So if you have a running animation, and add a ‘Wave Hello’ animation with the FadeOut option, you should see a running figure, who raises his arm and waves, then the arm goes back to the running animation over the course of 1 second (or whatever).
  2. Hold: The animation is held at the last key frame indefinitely. This is good for a ‘Sit’ animation, where you want the actor to remain in the final keyframe.
  3. Once: The animation is played, and then removed once it reaches the last keyframe. Examples: jump animation, throw animation.
  4. Loop: When the animation goes past the last keyframe, it resets to the first keyframe. I use this for walking animations.
  5. PingPong: When the animation is finished, it flips direction and runs backwards (Direction *= -1). It keeps going.

I was looking at my code, and there's one more thing I added to my animations: A time where the added animation blending becomes 100%

In the clip below, I have the robot in a running / walking animation as it's pursuing the player. When it gets close enough, it adds a Stab animation (40 seconds in) to its animation queue. There is a transition of ½ second where the arms are blended from 100% running to 100% stabbing. Otherwise it would look really jerky. The stab animation is also faded out , so the arms smoothly transition back to running, and the stab animation is flagged for removal from the queue.

When i said ‘Finished playing’ in my first sentence, I mean when it reaches the last keyframe. If I specified a Loop, Hold, or PingPong it will keep playing until it's removed from the queue.

Hope this helps

Animation Video:

Advertisement

@scott8 Thanks for this incredible response which has provided a lot of insight. I followed what you said and now when the amount to slerp becomes 1.0, I keep it at 1 since this is the weight determining which animation is played. This way the animation im transitioning to is always playing.

Quaternion AnimationData::BlendPose(vec3& a, vec3& b, double time, float slerpAmount)
{
	// Animation A rotation
	Quaternion a_rotX = Quaternion(a.x, vec3(1.0f, 0.0f, 0.0f).unit()); a_rotX.Normalize();
	Quaternion a_rotY = Quaternion(a.y, vec3(0.0f, 1.0f, 0.0f).unit()); a_rotY.Normalize();
	Quaternion a_rotZ = Quaternion(a.z, vec3(0.0f, 0.0f, 1.0f).unit()); a_rotZ.Normalize();
	Quaternion a_animARotQuat = (a_rotZ * a_rotY) * a_rotX;

	// Animation B rotation
	Quaternion b_rotX = Quaternion(b.x, vec3(1.0f, 0.0f, 0.0f).unit()); b_rotX.Normalize();
	Quaternion b_rotY = Quaternion(b.y, vec3(0.0f, 1.0f, 0.0f).unit()); b_rotY.Normalize();
	Quaternion b_rotZ = Quaternion(b.z, vec3(0.0f, 0.0f, 1.0f).unit()); b_rotZ.Normalize();
	Quaternion b_animARotQuat = (b_rotZ * b_rotY) * b_rotX;

	// Determine the slerp amount using time, ensure its 0-1 range.
	double amount_to_lerp = time / slerpAmount;

	if (amount_to_lerp > 1.0) {
		amount_to_lerp = 1.0;
	}

	Quaternion blendedRot = Slerp(a_animARotQuat, b_animARotQuat, amount_to_lerp);

	return blendedRot;
}

This now lets me blend to idle smoothly as seen in the video. I have a few questions regarding some of the scenarios you mentioned. In this scenario, I can smoothly blend to idle but going back to run ends up being a sharp switch instead of a smooth transition which I have shown in the video.

Idle to run is not smooth but run to idle is smooth

Also, you mentioned that there are animations which are transitioned to then removed once the end keyframe is it, how do you handle this in code? currently I'm using a std::vector to push animations clips to.

When I added a stab animation to my robot, the call looks like this:

pRobot→AddAnimation("Stab1", eAnimationLoop::FadeOut,1.0f,0.5f)

The 1.0 is the direction, and the 0.5 is the blend-in TIME. It means the animation (stab) will be blended at full weight at 0.5 seconds. I use the blend-in TIME to calc the blend in RATE when the animation is added: Rate = 1.0f/Blend_Time. So if I want the second animation to be fully blended in at 0.5 seconds, I use a rate of 1.0/0.5 = 2.0.

In the Animate(float DeltaTime) routine, I update the BlendWeight like this:

BlendWeight += BlendRate*DeltaTime;
if (BlendWeight>1.0f)
	BlendWeight = 1.0f; 

So the blendweight reaches 100% at 0.5 seconds and stays there.

It's the same idea as the fadeout.

I also store my animations in a std::vector (great minds think alike 🙂)

My animation structs have a variable that lets me know if they're done playing (bool bFinished = false). I set it to true if the running time is greater than animation time in the last keyframe AND the animation type is Once or FadeOut. Once I run all animations in the vector, I go back and remove any animations where bFinished = true using an iterator.

Disclamer: I am just a hobbyist, so I can't say any of my advice is an ‘industry standard’ way of doing things.

@scott8 Ah I see. This has helped me understand a lot, can't thank you enough!

This topic is closed to new replies.

Advertisement