Animation Blending

Started by
170 comments, last by haegarr 15 years, 6 months ago
Hello,
Thats my last Updated Code:

Animation Componenets:

class CKeyframe {	public:		float time;		float value;};enum CAnimationTrackType {	TRANSLATEX = 0,	TRANSLATEY,	TRANSLATEZ,	ROTATEX,	ROTATEY,	ROTATEZ};class CAnimationTrack {	public:		unsigned int trackID;		unsigned int nodeID;		CAnimationTrackType trackType;		std::vector<CKeyframe *> keys;};


Animation Class:
class CAnimation {	protected:		std::vector<CAnimationTrack *> m_Tracks;		float startTime, endTime;		char name[32];			public:		CAnimation();		~CAnimation();		CAnimationTrack * FindTrackByID(unsigned int trackID);		CKeyframe * FindKeyframeByTime(unsigned int trackID, float keyframe_time);		void AddTrack(unsigned int trackID, unsigned int nodeID, CAnimationTrackType trackType);		void AddKeyframe(unsigned int trackID, float value, float time);		void SetTime(float start_time, float end_time);		CAnimationTrack * operator[](unsigned int index);		CAnimationTrack *AnimationTrack(unsigned int index);		unsigned int NumAnimationTracks();	};


CBlend.cpp

void CBlend::Blend(CVector3 &vPos, CQuat &qRotate, CKeyframe *curKf, CKeyframe *prevKf, float blend_factor, CAnimationTrackType trackType) {	CVector3 vTmpPrev, vTmpCur;	CQuat qTmpPrev, qTmpCur;	if(trackType == TRANSLATEX) {		vTmpPrev.x = prevKf->value;		vTmpCur.x = curKf->value;			}	if(trackType == TRANSLATEY) {		vTmpPrev.y = prevKf->value;		vTmpCur.y = curKf->value;		}	if(trackType == TRANSLATEZ) {		vTmpPrev.z = prevKf->value;		vTmpCur.z = curKf->value;	}	if(trackType == ROTATEZ) {		qTmpPrev.SetAxis(prevKf->value, 1, 0, 0);		qTmpCur.SetAxis(curKf->value, 1, 0, 0);	}		if(trackType == ROTATEZ) {		qTmpPrev.SetAxis(prevKf->value, 0, 1, 0);		qTmpCur.SetAxis(curKf->value, 0, 1, 0);	}	if(trackType == ROTATEZ) {		qTmpPrev.SetAxis(prevKf->value, 0, 0, 1);		qTmpCur.SetAxis(curKf->value, 0, 0, 1);	}	vPos = vTmpCur * (1.0f - blend_factor) + vTmpPrev * blend_factor;	qRotate.Slerp(qTmpCur, qTmpPrev, blend_factor);}void CBlend::Blend(CAnimation *pFinalAnim, CAnimation *pCurAnimation, CAnimation *pNextAnimation, float blend_factor) {	CVector3 vTmp;	CQuat qTmp;	for(unsigned int i = 0; i < pCurAnimation->NumAnimationTracks(); i++) {		for(unsigned int n = 0; n < pNextAnimation->NumAnimationTracks(); n++) {						if(pCurAnimation->AnimationTrack(i)->nodeID == pNextAnimation->AnimationTrack(n)->nodeID && pCurAnimation->AnimationTrack(i)->trackType == pNextAnimation->AnimationTrack(n)->trackType) {				for(unsigned int j = 0; j < pCurAnimation->AnimationTrack(i)->keys.size(); j++) {					for(unsigned int k = 0; k < pNextAnimation->AnimationTrack(n)->keys.size(); k++) {						Blend(vTmp, qTmp, pCurAnimation->AnimationTrack(i)->keys[j], pNextAnimation->AnimationTrack(n)->keys[j], blend_factor, pNextAnimation->AnimationTrack(n)->trackType);						for(unsigned int v = 0; v < pFinalAnim->NumAnimationTracks(); v++) {							if(pFinalAnim->AnimationTrack(v)->nodeID == pCurAnimation->AnimationTrack(i)->nodeID && pFinalAnim->AnimationTrack(i)->trackType == pCurAnimation->AnimationTrack(n)->trackType) {									//What to write here??								}							}						}					}				}			}		}}


Animate Skeleton:

void CSkeletalAnimation::Animate(CSkeleton *pFinalSkel) {	float time = g_Timer.GetSeconds();        //Haven't written loop yet. :)	for(unsigned int i = 0; i < m_Tracks.size(); i++) {		unsigned int uiFrame = 0;		CBone * bone = pFinalSkel->FindBone(m_Tracks->nodeID);		CAnimationTrackType trackType = m_Tracks->trackType;		CVector3 vTmp;		CQuat qTmp;		while(uiFrame < m_Tracks->keys.size() && m_Tracks->keys[uiFrame]->time < time) uiFrame++;		if(uiFrame == 0) {			if(trackType == TRANSLATEX) {				vTmp.x = m_Tracks->keys[0]->value;			}			if(trackType == TRANSLATEY) {				vTmp.y = m_Tracks->keys[0]->value;			}			if(trackType == TRANSLATEZ) {				vTmp.z = m_Tracks->keys[0]->value;			}			if(trackType == ROTATEX) {				qTmp.SetAxis(m_Tracks->keys[0]->value, 1, 0, 0);							}			if(trackType == ROTATEY) {				qTmp.SetAxis(m_Tracks->keys[0]->value, 0, 1, 0);			}			if(trackType == ROTATEZ) {				qTmp.SetAxis(m_Tracks->keys[0]->value, 0, 0, 1);			}							}		if(uiFrame == m_Tracks->keys.size()) {			if(trackType == TRANSLATEX) {				vTmp.x = m_Tracks->keys[uiFrame-1]->value;			}			if(trackType == TRANSLATEY) {				vTmp.y = m_Tracks->keys[uiFrame-1]->value;			}			if(trackType == TRANSLATEZ) {				vTmp.z = m_Tracks->keys[uiFrame-1]->value;			}			if(trackType == ROTATEX) {				qTmp.SetAxis(m_Tracks->keys[uiFrame-1]->value, 1, 0, 0);							}			if(trackType == ROTATEY) {				qTmp.SetAxis(m_Tracks->keys[uiFrame-1]->value, 0, 1, 0);			}			if(trackType == ROTATEZ) {				qTmp.SetAxis(m_Tracks->keys[uiFrame-1]->value, 0, 0, 1);			}			}		else {			CKeyframe *prevKf = m_Tracks->keys[uiFrame-1];			CKeyframe *curKf = m_Tracks->keys[uiFrame];			float delta = curKf->time - prevKf->time;			float blend = (time - prevKf->time) / delta;			CBlend::Blend(vTmp, qTmp, curKf, prevKf, blend, trackType);			}		bone->qRotate = bone->qRotate * qTmp;		bone->vPos = bone->vPos * vTmp;	}}


Is that code right?? And please answer my question inside Blend::Blend function.

Thanks,
Kasya

P.S. Timer is temporary i'll change it anyway. :)
Advertisement
IMHO there are several problems within the code.

(1) You use 3 times ROTATIONZ but no ROTATIONX and ROTATIONY in the CBlend::Blend for 2 key-frames.

(2) The implementation of the same CBlend::Blend as above is very inefficient. My main grumbling is that the trackType is exactly 1 of the possible values, but you ever process all of them. Consider at least to choose a structure like
// either a rotation ...if( trackType>=ROTATEX ) {   CQuat qTmpPrev, qTmpCur;   if( trackType==ROTATIONX ) {      qTmpPrev.SetAxis(prevKf->value, 1, 0, 0);       qTmpCur.SetAxis(curKf->value, 1, 0, 0);   }   else if( trackType==ROTATIONY ) {      /// insert appropriate code here   }   else {      /// insert appropriate code here   }   qRotate.Slerp(qTmpCur, qTmpPrev, blend_factor);}// ... or else a translation ...else {   /// insert appropriate code here, similarly to the part above}

As you can see, this snippet tries to avoid senseless computations. You can use a switch statement for the inner decisions, of course. (Yes, you can say that this is a kind of pre-optimization, if you like. But choosing another implementation here has an impact on the invoking code, so changing it later can IMO introduce more problems than expected so far.)

(3) I would not choose the way of CBlend::Blend of 2 animations as you've done. Your way is appropriate for blending 2 animations, but not necessarily for blending more than 2 animations. Instead of blending animations pairwise, I would use the skeleton instance as an accumulator for the blending. I.e. preset the skeleton instance when entering the blending group, process each animation of the group without knowledge of the other animations, and post-process the skeleton instance when exiting the group.


I'm not sure what kind of animation system you try to implement. So I cannot really evaluate the code at the higher logical levels (this is strictly speaking already relevant for point (3) above). You may consider to tell us exactly what your goals are when we should discuss them.
Hello,

it there a problem inside
CSkeletalAnimation::Animate(CSkeleton *pFinalSkel); except Time

And Thats my new Blending:

void CBlend::Blend(CVector3 &vPos, CQuat &qRotate, CKeyframe *curKf, CKeyframe *prevKf, float blend_factor, CAnimationTrackType trackType) {	if(trackType >= TRANSLATEX && trackType <= TRANSLATEZ) {			CVector3 vTmpPrev, vTmpCur;		if(trackType == TRANSLATEX) {			vTmpPrev.x = prevKf->value;			vTmpCur.x = curKf->value;					}		else if(trackType == TRANSLATEY) {			vTmpPrev.y = prevKf->value;			vTmpCur.y = curKf->value;				}		else if(trackType == TRANSLATEZ) {			vTmpPrev.z = prevKf->value;			vTmpCur.z = curKf->value;		}		vPos = vTmpCur * (1.0f - blend_factor) + vTmpPrev * blend_factor;	}	else if(trackType >= ROTATEX && trackType <= ROTATEZ) {		CQuat qTmpPrev, qTmpCur;		if(trackType == ROTATEX) {			qTmpPrev.SetAxis(prevKf->value, 1, 0, 0);			qTmpCur.SetAxis(curKf->value, 1, 0, 0);		}				else if(trackType == ROTATEY) {			qTmpPrev.SetAxis(prevKf->value, 0, 1, 0);			qTmpCur.SetAxis(curKf->value, 0, 1, 0);		}		else if(trackType == ROTATEZ) {			qTmpPrev.SetAxis(prevKf->value, 0, 0, 1);			qTmpCur.SetAxis(curKf->value, 0, 0, 1);		}		qRotate.Nlerp(qTmpCur, qTmpPrev, blend_factor);	}}void CBlend::Blend(CSkeleton *pFinalSkel, CSkeleton *pLastSkel, CSkeleton *pNextSkel, float blend_factor) {	for(unsigned int i = 0; i < pLastSkel->NumBones(); i++) {		pFinalSkel->bones->vPos = pLastSkel->bones->vPos * ( 1.0 - blend_factor) + pNextSkel->bones->vPos * blend_factor;		pFinalSkel->bones->qRotate.Nlerp(pLastSkel->bones->qRotate, pNextSkel->bones->qRotate, blend_factor);	}}


Thanks,
Kasya
Okay; although the implementation of CBlend::Blend for 2 key-frames is still away from being optimal, it is good enough until the animation system works as expected. I here only hint at the potential to optimize it and urge you to come back to this topic at appropriate time.

In CSkeletalAnimation::Animate there is IMO an "else" being missed. The structure should probably be
if(uiFrame == 0) {   ...}else if(uiFrame == m_Tracks->keys.size()) { // <-- notice the else at the beginning   ...else {   ...}


Next, vTmp is a QVector3, and I assume vPos to be one, too. You are multiplying the both in CSkeletalAnimation::Animate
bone->vPos = bone->vPos * vTmp;
but that isn't the correct operation. If you don't want to do animation blending, then addition would be the correct operation. If, on the other hand, you want to do animation blending, then blending would be the correct operation. In the latter case also bone->qRotate must be blended, of course.

I can't tell you about a working structure if you don't tell us what you want to do. So please:
(a) Do you want to integrate animation blending?
(b) Are the tracks of a particular animation unique w.r.t. the affected bone attribute, or else is blending already necessary at this level? (I would suggest the former.)
(c) What about layering? What kind of layer blending do you prefer, if any?
Hello,

(a) I want ot use animation blending
(b) The Tracks are animating Bones
(c) I have no Layering no animation groups. I want to do that too. but after blending.

i changed lots of things here in CSkeleton::Animate(CSkeleton *pFinalSkel); functions.

void CSkeletalAnimation::Animate(CSkeleton *pFinalSkel) {	float time = g_Timer.GetSeconds() * 0.01f;	static float lastTime = startTime;	time += lastTime;	lastTime = time;	sprintf(t, "%f", time);	SetWindowText(GetHWND(), t);	if(time >= endTime) {		if(loop) {			lastTime = startTime;			time = startTime;		}	}	for(unsigned int i = 0; i < m_Tracks.size()-1; i++) {		unsigned int uiFrame = 0;		CBone * bone = pFinalSkel->FindBone(m_Tracks->nodeID);		CAnimationTrackType trackType = m_Tracks->trackType;		CVector3 vTmp;		CQuat qTmp;		while(uiFrame < m_Tracks->keys.size() && m_Tracks->keys[uiFrame]->time < time) uiFrame++;		if(uiFrame == 0) {			if(trackType >= TRANSLATEX && trackType <= TRANSLATEY) {							if(trackType == TRANSLATEX) {					vTmp.x = m_Tracks->keys[0]->value;				}				else if(trackType == TRANSLATEY) {					vTmp.y = m_Tracks->keys[0]->value;				}				else if(trackType == TRANSLATEZ) {					vTmp.z = m_Tracks->keys[0]->value;				}			} 			else if(trackType >= ROTATEX && trackType <= ROTATEZ) {				if(trackType == ROTATEX) {					qTmp.SetAxis(m_Tracks->keys[0]->value, 1, 0, 0);									}				else if(trackType == ROTATEY) {					qTmp.SetAxis(m_Tracks->keys[0]->value, 0, 1, 0);				}				else if(trackType == ROTATEZ) {					qTmp.SetAxis(m_Tracks->keys[0]->value, 0, 0, 1);				}								}		}		else if(uiFrame == m_Tracks->keys.size()) {			if(trackType >= TRANSLATEX && trackType <= TRANSLATEY) {							if(trackType == TRANSLATEX) {					vTmp.x = m_Tracks->keys[uiFrame-1]->value;				}				else if(trackType == TRANSLATEY) {					vTmp.y = m_Tracks->keys[uiFrame-1]->value;				}				else if(trackType == TRANSLATEZ) {					vTmp.z = m_Tracks->keys[uiFrame-1]->value;				}			} 			else if(trackType >= ROTATEX && trackType <= ROTATEZ) {				if(trackType == ROTATEX) {					qTmp.SetAxis(m_Tracks->keys[uiFrame-1]->value, 1, 0, 0);									}				else if(trackType == ROTATEY) {					qTmp.SetAxis(m_Tracks->keys[uiFrame-1]->value, 0, 1, 0);				}				else if(trackType == ROTATEZ) {					qTmp.SetAxis(m_Tracks->keys[uiFrame-1]->value, 0, 0, 1);				}								}		}		else {			CKeyframe *prevKf = m_Tracks->keys[uiFrame-1];			CKeyframe *curKf = m_Tracks->keys[uiFrame];			float delta = curKf->time - prevKf->time;			float blend = (time - prevKf->time) / delta;			CBlend::Blend(vTmp, qTmp, curKf, prevKf, blend, trackType);			}		bone->qRotate = bone->qRotate * qTmp;		bone->vPos += vTmp;	}}


I only have time problem. But im gettings rid of it.

Thanks,
Kasya
Quote:Original post by Kasya
(a) I want ot use animation blending
(b) The Tracks are animating Bones
(c) I have no Layering no animation groups. I want to do that too. but after blending.

Okay. Answer (b) isn't complete w.r.t. my question, but I assume trackIDs being unique per animation until you constradict explicitely. I furthur assume that more than 2 animations should be able to be blended.

Coming to the timing. I suggest you the following:

How is g_Timer.GetSeconds() advanced? It is a timer provides by the OS? As mentioned earlier, the time used should be freezed for the current video frame. Due to this behaviour, I would expect the current time being overhanded like in
void CSkeletalAnimation::Animate(float currentTime,CSkeleton *pFinalSkel) ...
instead of being fetched inside that routine.

Next, what is lastTime being good for? Especially declaring it as static is probably a bad idea. I suggest something like this:
CSkeletalAnimation:: CSkeletalAnimation( float startTime, bool loop ):  m_startTime( startTime ),   m_loop( loop ) ...void CSkeletalAnimation::Animate( CSkeleton *pFinalSkel, float currentTime ) {   // relating current time to animation start   currentTime -= m_startTime;   // handling looping if necessary ...   if( currentTime>=m_duration && m_loop ) {      do {         currentTime -= m_duration;         m_startTime += m_duration;      } while( currentTime>=m_duration );   }   // pre-processing the skeleton   //    (nothing to do yet)   // iterating tracks   for( unsigned int i=0; i<m_Tracks.size()-1; ++i ) {      m_Tracks->contribute( pFinalSkel, currentTime );   }   // post-processing the skeleton   //    (nothing to do yet)}

What do you think about that? Notice that the animation doesn't care here that tracks are build of key-frames.
Hello,

What you mean

Quote:
Notice that the animation doesn't care here that tracks are build of key-frames.


?

Does it Care on mine? Where?

Doesn't m_Tracks->Contribute(pFinalSkel, currentTime);'s implementation like mine but inside function.

And for blending more than one animation, i need to do like that:

CSkeleton *pFinalSkel;CSkeleton tempSkel1, tempSkel2, tempSkel3;CSkeleton tempFinal;m_Animation[0].Animate(&tempSkel1, currentTime);m_Animation[1].Animate(&tempSkel2, currentTime);m_Animation[2].Animate(&tempSkel3, currentTime);CBlend::Blend(&tempFinal, &tempSkel1, &tempSkel2, blend_factor);CBlend::Blend(pFinalSkel, &tempFinal, &tempSkel3, blend_factor);


Thanks,
Kasya



Quote:Original post by Kasya
What you mean

Quote:
Notice that the animation doesn't care here that tracks are build of key-frames.


?

Does it Care on mine? Where?

Your CSkeletalAnimation::Animate method executes the entire logic of tracks. Hence it also executes the look-up for the surrounding key-frames of the track. So yes, your solution requires the tracks being key-frame tracks. That is not an error; it is not even not a real problem if you stick with key-frame tracks only. But it is away from a clean OOP solution, IMHO. And you'll get into trouble if you decide to allow other kinds of tracks. At least, externalizing that functionality slims CSkeletalAnimation::Animate.

Quote:Original post by Kasya
Doesn't m_Tracks->Contribute(pFinalSkel, currentTime);'s implementation like mine but inside function.

Mostly.

Quote:Original post by Kasya
And for blending more than one animation, i need to do like that:
...

Looking at your code snippets shows me a hardcoded handling of an anknown amount of animations. Well, I assume you don't really meant that but gave that as an equivalent example, did you? However, the real problems are others.

First, look at the total weightings. What you suggest is something like
f1 := p2 * w12 + p1 * ( 1 - w12 )
f2 := p3 * w23 + f1 * ( 1 - w23 )
== p3 * w23 + ( p2 * w12 + p1 * ( 1 - w12 ) ) * ( 1 - w23 )
== p3 * w23 + p2 * w12 * ( 1 - w23 ) + p1 * ( 1 - w12 ) * ( 1 - w23 )
Do you notice the double weighting of p2 and p1? If you don't take countermeasures then animations incorporated at the beginning are more and more supressed due to their multiple weightings.

The 2nd problem is that, if you don't have complete animations w.r.t. the count of tracks (i.e. not all skeleton attributes are animated), then you have the need to lately set those attributes to the state of the bind pose. So you have the need to detect at the end of processing all animations, whether or not an attribute has been touched.

Both problems can be handled by using a sum of weights for each attribute. It has an additional advantage: It allows weights to be independent; it is sufficient if each weight is greater than 0 (an animation with weight 0 plays no role by definition, and negative weights are disallowed anyway).

An animation has a track that influences f. Since this is the first animation doing so, the value returned by the track is used as is, but the weight is remembered:
s = w1
f = p1
If no other animation has a track bound to the same attribute, then nothing more happens. If, on the other hand, another animation has a track bound to that attribute, then a blending happens but with an adapted weight:
s += w2
f = blend( f, p2, w2 / s )
So what happens? The adapted weight is
w2 / s == w2 / ( w1 + w2 )
so that the blending is actually computed as
p1 * ( 1 - w2 / ( w1 + w2 ) ) + p2 * w2 / ( w1 + w2 )
== p1 * w1 / ( w1 + w2 ) + p2 * w2 / ( w1 + w2 )
Assuming a 3rd animation track comes into play, then again
s += w3
f = blend( f, p3, w3 / s )
resulting in
p1 * w1 / ( w1 + w2 + w3 ) + p2 * w2 / ( w1 + w2 + w3 ) + p3 * w3 / ( w1 + w2 + w3 )

You see that the weights gets normalized automatically, and each track has an influence with just the weight defined by the animation rather than a mix of weights of various animations!

Moreover, when the animations are all processed, you can investigate the sum of weights and determine whether _any_ track ahd influence; if not, then set the value of the attribute to those of the bind pose.
Addendum:

Notice please how the above scheme of blending animations fits perfectly with the track->contribute thingy. It is so because the said scheme blends animations one-by-one, so it needs only to know a single animation at a time. In other words, the blending can be done by the animation (or its track, in this case) itself. That is an advantage from the implementation's point of view.

To do so, you need to transport the animations weight to the track, of course, like so
   // iterating tracks   for( unsigned int i=0; i<m_Tracks.size()-1; ++i ) {      m_Tracks->contribute( pFinalSkel, currentTime, m_weight );   }


And you can see why the comments "pre-processing" and "post-processing" in one of my previous posts are senseful...
Hello,

You said

Quote:
the blending can be done by the animation (or its track, in this case) itself


and used

m_Tracks->contribute( pFinalSkel, currentTime, m_weight );


That means i need to calculate weight. But when its inside keyframe like

			CKeyframe *prevKf = m_Tracks->keys[uiFrame-1];			CKeyframe *curKf = m_Tracks->keys[uiFrame];			float delta = curKf->time - prevKf->time;			float blend = (time - prevKf->time) / delta;			CBlend::Blend(vTmp, qTmp, curKf, prevKf, blend, trackType);	


i calculated weight in float blend;.

How can i calculate it before m_Tracks->contribute loop

Thanks,
Kasya

This topic is closed to new replies.

Advertisement