Animation Blending

Started by
170 comments, last by haegarr 15 years, 5 months ago
An addendum to the previous post: One has to differ between a "relative" weighting during blending and an "absolute" weighting during layering. I.e. in general the sums of weights during blending are allowed to not sum-up to 1 when a normalization step is done during processing the "end of animation group". But the sum of weights as is used for layering must not be normalized, of course.

To make the life of the artist a bit easier, one can use distinct weights for blending and layering. E.g. the blending weights are associated with the animations, while the layering weights are associated with the animation groups.
Advertisement
void CAnimation::Blend(CSkeleton *pFinalSkel, CSkeleton *pLastSkel, CSkeleton *pNextSkel, float blendFactor) {	for(unsigned int i = 0; i < pLastSkel->NumBones(); i++) {		pFinalSkel->bones->vPos = pLastSkel->bones->vPos * (1.0f - blendFactor) + pNextSkel->bones->vPos * blendFactor;		pFinalSkel->bones->qRotate.Nlerp(pLastSkel->bones->qRotate, pNextSkel->bones->qRotate, blendFactor);	}}bool loop = true;void CAnimation::Animate(CSkeleton *pFinalSkel) {	unsigned int uiFrame = 0;	CQuat qTmp;	CVector3 vTmp;	static float lastTime = startTime;	float time = m_Timer.GetSeconds();	time += lastTime;	lastTime = time;	if(time > (startTime + length)) {		if(loop) {			m_Timer.Init();			time = startTime;			lastTime = startTime;			uiFrame = 0;		} else {			time = startTime + length;					}	}	while(uiFrame < m_Frames.size() && m_Frames[uiFrame]->time < time) uiFrame++;	if(uiFrame == 0) {		for(unsigned int i = 0; i < m_Frames[0]->NumBones()+1; i++) {			pFinalSkel->bones->qRotate = pFinalSkel->bones->qRotate * m_Frames[0]->bones->qRotate;			pFinalSkel->bones->vPos += m_Frames[0]->bones->vPos;		}	}	else if(uiFrame == m_Frames.size()) {		for(unsigned int i = 0; i < m_Frames[uiFrame-1]->NumBones()+1; i++){			pFinalSkel->bones->qRotate = pFinalSkel->bones->qRotate  * m_Frames[uiFrame-1]->bones->qRotate;			pFinalSkel->bones->vPos += m_Frames[uiFrame-1]->bones->vPos;		}	}	else {		CSkeleton *curFrame = m_Frames[uiFrame];		CSkeleton *prevFrame = m_Frames[uiFrame-1];		float delta = curFrame->time - prevFrame->time;		float blend_factor = (time - prevFrame->time) / delta;		CAnimation::Blend(pFinalSkel, curFrame, prevFrame, blend_factor);	}	}


Thats My New Animation. Its working how needed. But sometimes it slows down. I think its because of CPU. Am i right?

Thanks
Kasya
@ Kasya: Your latest code seems me mainly okay in principle, but I think there are some design quirks. Perhaps I'm wrong because I don't have insight to some implementation and design details. However, here is what I mean:

(1) I wouldn't use a timer but an always running time. When you start using animation blending, several animated objects, and perhaps physics you would otherwise deal with dozens of timers, and you would introduce little slides at each timer restart. A central, always running time would unburden you from taking care, and also gives all animated elements the same time base.

Let's assume there is a central time outside the animation. Whenever you begin you compute and render a new video frame, you freeze the current time (what I have named "the tick" in my large post above).

Reasoning: If you wouldn't do so, then animation A is computed for time t, animation B a bit later, say at time t+0.05s, animation 3 another bit later, and so on. So, your current snapshot of the scene would be no longer a real snapshot since all animations are computed at slightly different time moments.

Hold a relative time in the animation instance. I.e. an animation is instanciated and should start playing at t0. The animation is actually not playing as long as "the tick" is less than t0. Then, when the tick goes behind t0, the relative time will be
t' := tick - t0
Of course, every animation has its own t0 and hence its own t'. If t' exceeds the length T of the animation, the result depends on whether the animation loops or not (you already have seen that, of course).

(2) Animations with loops have to wrap around. That means, there is no "reset" when t' exceeds the length. That has 2 consequences:

First, when t' exceeds the duration, you have to correct t0 (what would be the easiest way of dealing with this) by setting
t0 := t0 + T while t' + t0 > T
Doing so does _not_ drop the time overshooting as would a simple reset do!

Second, when you look at the key-frames, the indices of the key-frames look like
0, 1, 2, 3, ... n-2, n-1, 0, 1, 2, ...
where N is the number of key-frames. Then, if t'[n-1] != t'[0] you have to interpolate between key-frames n-1 and 0 also! You may have another scheme where t'[n-1] == t'[0] so need not handle this stuff specially.

I hint you at this possibility although, or even because, I don't know how you define your key-frames.
Quote:Hold a relative time in the animation instance. I.e. an animation is instanciated and should start playing at t0.


I currently have a global timer in my engine that I query each frame to see how much time has passed since the previous frame. Every model in the world has a CurrentAnimTime variable that is increased with this frame interval. With the help of this CurrentAnimTime, the skeleton is animated. Is this the method you are using?

Quote:(A little excursus: To make things even more interesting, this factor is a float parameter of the animation, and a float modifier can modify it. You see what I mean? Yes, another animation can be used to control it if one wants to do so. So it is even possible to fade animations in and out.)


What do you mean by "fading" an animation in and out? Is it transitioning from a walking to a running animation for example by letting the weight of the walking animation gradually decrease and the weight for the running animation increase?



I've read a thesis here about this subject. It states that animation blending could be implemented as a special case of animation layering, where all the weights off the skeletons from different animations are taken into account, as opposed to just a few joints. This seems like a good approach to me, but I'd like to know what you think of it.

Also, this paper speaks only of blending of the rotations with the SLERP function, but do translations also need to be blended?

Aside from the actual implementation, is this really the technique games like Tomb Raider used in the past to combine animations? I find it a little hard to believe that these games, that didn't even use skinning, used such an advanced animation system. I ask this because I'm currently replicating a Tomb Raider engine and I just need to combine a running and a shooting animation. The rest of the animations can simply be transitioned in between. Maybe there's a simpler technique that I've overlooked?

As you can see, I have quite a few questions before I go and implement this technique. ;) Sorry you had to type that much, BTW!

Jeroen

[Edited by - godmodder on August 17, 2008 8:28:03 AM]
Quote:Original post by godmodder
I currently have a global timer in my engine that I query each frame to see how much time has passed since the previous frame. Every model in the world has a CurrentAnimTime variable that is increased with this frame interval. With the help of this CurrentAnimTime, the skeleton is animated. Is this the method you are using?

I use another way of computing, but in effect it seems me the same with perhaps some minor exceptions.

We both use a global time from which the video frame's tick is fetched. I hope you freeze the tick as I have described in one of the posts above. From your desciption I see that you compute the time elapsed since the last recent video frame
dt := tickn - tickn-1
and increase the animation time by it
t'n := t'n-1 + dt , presumbly with t'0 := 0

My way is
t'n := tickn - t0

Now (if I'm correct with my assumptions), comparing these 2 ways shows that the animation playback is started at a video frame boundary in your case, while it is started at a arbitrary (but defined, of course) time moment t0 in my case. So I can better synchronize with already running animations. But you may also use a t'0==t0!=0, of course, in which my hint is irrelevant.

The other thing is that your way accumulates small inaccuraries in t'n over time, while my way avoids this.

But as said, both things are minor and may never yield in a problem at runtime.

Quote:Original post by godmodder
I've read thesis here about this subject. It states that animation blending could be implemented as a special case of animation layering, where all the weights off the skeletons from different animations are taken into account, as opposed to just a few joints. This seems like a good approach to me, but I'd like to know what you think of it.

Please give me some days to read the thesis.

Quote:Original post by godmodder
Aside from the actual implementation, is this really the technique games like Tomb Raider used in the past to combine animations? I find it a little hard to believe that these games, that didn't even use skinning, used such an advanced animation system. I ask this because I'm currently replicating a Tomb Raider engine and I just need to combine a running and a shooting animation. The rest of the animations can simply be transitioned in between. Maybe there's a simpler technique that I've overlooked?

Well, I don't know how animation is implemented in Tomb Raider. The described system is just the animation system for my engine. If you ignore named binding, symmetry, and instancing, nothing more than a 2 level averaging is left over. Essentially, in a kind of pseudo code, what is done is
for( group=0; group < noofAnimGroups; ++group ) {   for( anim=0; anim < group.noofAnims; ++anim ) {      for( track=0; track < anim.noofTracks; ++ track ) {         value = track.interpolate( tickRelativeToAnim );         track.targetParameter += anim.weight * value;      )   }   skeleton.normalize;   result.addIfTotalWeightIsLessThanOne( skeleton, group.weight );}

As can be seen here, the "for( anim" loop does the blending, while the "for( group" loop does the layering. In fact, blending and layering is mathematically the same with the exception of manipulating the layer's weight factor accordingly to the already applied weight factors. No big deal.


However, I don't claim that the described method is the only one, or even the best one. It is just the method that seems me appropriate to reach the goals I've discussed with my artist, and that fit in my overall engine design, of course. E.g. the timeline and prototype/instancing stuff result from the basic design principles of the engine, and not from a necessity due to the animation system itself. And the usage of tracks is IMHO advantageous because it allows my artits to concentrate on the parameters he want to tweak, and it lowers the amount of parameters the engine will process.
Quote:Original post by godmodder
What do you mean by "fading" an animation in and out? Is it transitioning from a walking to a running animation for example by letting the weight of the walking animation gradually decrease and the weight for the running animation increase?

Yes, that is a possible scenario.

Quote:Original post by godmodder
Also, this paper speaks only of blending of the rotations with the SLERP function, but do translations also need to be blended?

In principle also translations have to be blended, yes, but only if they are animated. The result of blending 2 identical value
vPos * b + vPos * ( 1 - b ) == vPos
is independend on the blending factor and hence need not be done if you _know_ that they are identical. In skeletons the positions are typically constant. That is one of the reasons I use tracks, so the artist _can_ animate (selected) positions.

Using slerp or nlerp is an issue; I've posted something about it anywhere earlier.

Quote:Original post by godmodder
As you can see, I have quite a few questions before I go and implement this technique. ;) Sorry you had to type that much, BTW!

Oh, life is hard ;) Well, my engine is definitely somewhat complex, and offering some details here give the chance of discussion. I don't claim to have found the best way, and so the discussion help also me in perhaps revising design decisions. So don't hesitate to point out weaknesses. I'm glad about every hint making things better.
Quote:Original post by godmodder
Also, this paper speaks only of blending of the rotations with the SLERP function, but do translations also need to be blended?


Use nlerp when blending anims - it's faster and a hell of a lot easier when blending multiple anims. Normalise at the end though, then you have no reason to put constraints on the weights for the layering (additive blending as we call it). The other advantage of using nlerp, is that you can treat all anim tracks as floats, with a special case flag for quat rotations (i.e. normalise float tracks 0,1,2,3 as a quat). It's then trivial to extend the system to handle colours / bland shape weights etc.
Quote:Original post by RobTheBloke
Use nlerp when blending anims - it's faster and a hell of a lot easier when blending multiple anims. Normalise at the end though, then you have no reason to put constraints on the weights for the layering (additive blending as we call it). The other advantage of using nlerp, is that you can treat all anim tracks as floats, with a special case flag for quat rotations (i.e. normalise float tracks 0,1,2,3 as a quat). It's then trivial to extend the system to handle colours / bland shape weights etc.

Seconded, with the exception of dropping constraints on the weights for the layering. How do you prevent lower priority animations from being added if there is no criteria like "the sum is full"?

[Edited by - haegarr on August 18, 2008 6:46:50 AM]
Hello,
What is Animation Track? is it like

class AnimationTrack {public:std::vector<Keyframe> keyframes;unsigned int BoneID;};


?

Thanks,
Kasya
More or less. That track will be fine if you only want skeletal animation, you could simplify it to:

struct KeyFrame{  float time;  float value;};enum AnimTrackType{  kRotateX,  kRotateY,  kRotateZ,  kRotateW,  kTranslateX,  kTranslateY,  kTranslateZ,  kScaleX,  kScaleY,  kScaleZ,  kColorR,  kColorG,  kColorB,  // and any others you might need.... };struct AnimTrack{  int someNodeId;  AnimTrackType type;  std::vector<KeyFrame> keys;};


Though there is a *lot* of optimisation possible in the above code...

This topic is closed to new replies.

Advertisement