• Create Account

# Animation Blending

Old topic!

Guest, the last post of this topic is over 60 days old and at this point you may not reply in this topic. If you wish to continue this conversation start a new topic.

171 replies to this topic

### #161Gasim  Members   -  Reputation: 207

Like
0Likes
Like

Posted 01 November 2008 - 08:39 PM

Hello Everyone,
I was very very busy with the school. Sorry for late reply.

I created all the animation system. It can add keyframes, bindings, tracks, skeletons, bones etc.

But about Blending, I got a problem here. And don't know how to fix it.

Thats the blending code:

AnimationUtils.h

class AnimationUtils {public:	template<class T> static T Interpolate(T &v1, T &v2, real factor) {		return Math::Lerp<T>(v1, v2, factor);	}	template<> static Quat Interpolate<Quat>(Quat &q1, Quat &q2, real factor) {		Quat q;		q.Nlerp(q1, q2, factor);		return q;	}};class AnimQuatVariable {public:	Quat value;	real totalWeight;	AnimQuatVariable() : totalWeight(0) {}	void Reset() { totalWeight = 0; }		void Blend(Quat &other, real weight) {		totalWeight += weight;		value.Nlerp(value, other, totalWeight);	}};class AnimVector3Variable {public:	Vector3 value;	real totalWeight;	AnimVector3Variable() : totalWeight(0) {}	void Reset() { totalWeight = 0; }		void Blend(Vector3 &other, real weight) {		totalWeight += weight;		value = Math::Lerp<Vector3>(value, other, totalWeight);	}};enum FourCC {	ORIENTATION = 0,	POSITION};

AnimationTrack.h - Interpolation Part

	Keyframe * FindKey(real time) const {		for(KeyList::const_iterator i = keys.begin(); i != keys.end(); ++i) {			if((*i)->time == time) return *i;		}		return NULL;	} 	Keyframe * NextKey(Keyframe * key) const {		for(KeyList::const_iterator i = keys.begin(); i != keys.end(); ++i) {			if((*i) == key) return *i + 1;		}		return NULL;	}template<class value_g, class target_g> value_g AnimationPrototype::KeyframeAnimationTrack<value_g, target_g>::Interpolate(real time) const {	Keyframe * cur = FindKey(time);	Keyframe * next = NextKey(cur);	if(!cur) cur = new Keyframe; cur->value = 0;	if(!next) next = new Keyframe; cur->value = 0;	return (value_g) AnimationUtils::Interpolate<value_g>(cur->value, next->value, (next->time - cur->time));}

AnimationBinding.h - Contribute:

	void contribute(real localTime, real weight) const {		value_g value = track->Interpolate(localTime);		target->Blend(value, weight);	}

AnimationInstance.h - Animate

void AnimationInstance::Animate(SkeletonInstance *instance, real time) {	instance->preBlending();	for(BindList::iterator i = binds.begin(); i != binds.end(); ++i) {		(*i)->contribute(time, weight);	}	instance->postBlending();}

Testing Code:

SkeletonInstance * skel_instance = new SkeletonInstance;SkeletonPrototype * skel_prototype = new SkeletonPrototype;AnimationPrototype * anim_prototype = new AnimationPrototype;AnimationInstance * anim_instance = new AnimationInstance;Vec3AnimationTrack * track = new Vec3AnimationTrack("RightArm",FourCC::POSITION);void Init() {	skel_prototype->AddBone(1, 0, "RightArm", Quat(), Vector3(0,1,0), 5);	skel_instance = skel_prototype->newInstance();	track->AddKey(Vector3(2,2,1), 0);	track->AddKey(Vector3(3,12,1), 1);	anim_prototype->AddTrack(track);	anim_instance->SetWeight(0.5f);	anim_instance = anim_prototype->create(skel_instance);}real time = 0;void Render() {	if(time < 2) time++;	else time = 0;	anim_instance->Animate(skel_instance, time );	g_Text.PrintText(5,6, "%1.1f", skel_instance->getBoneByID("RightArm")->vLocalPosition.value.y);}

In there it shows RightArm bone's Local Position 1.0, but doesn't change when i animate it.

What can i do?

Thanks,
Kasya

### #162haegarr  Crossbones+   -  Reputation: 7163

Like
0Likes
Like

Posted 01 November 2008 - 11:31 PM

The first thing I've found is that AnimQuatVariable::Blend(...) and AnimVector3Variable::Blend(...) are not correct. You sum up the current weight to the totalWeight and then invoke a lerp with the totalWeight. That is wrong!

Let's look at an example with weight=0.3:
variable->Reset();  => totalWeight := 0variable->Blend( other, weight ) :   totalWeight += weight;  => totalWeight := 0.3   value = Math::Lerp<Vector3>( value, other, totalWeight );  => value := 0.7 * value + 0.3 * other

But what we want is:
variable->Reset();  => totalWeight := 0variable->Blend( other, weight ) :   totalWeight += weight;  => totalWeight := 0.3   value = Math::Lerp<Vector3>( value, other, weight/totalWeight );  => value := 0.0 * value + 1.0 * other

Notice the ratio "weight/totalWeight" as parameter of the lerp. Look at the stuff on the 3rd page of this thread; there we've handled it in great detail.

### #163Gasim  Members   -  Reputation: 207

Like
0Likes
Like

Posted 02 November 2008 - 07:14 AM

Hello,
I changed the totalWeight to weight/totalWeight inside function.
But it doesn't change the value now too. What can i do? Maybe i need to change

return (value_g) AnimationUtils::Interpolate<value_g>(cur->value, next->value, (next->time - cur->time));

into

return (value_g) AnimationUtils::Interpolate<value_g>(cur->value, next->value, time);

Thanks,
Kasya

### #164haegarr  Crossbones+   -  Reputation: 7163

Like
0Likes
Like

Posted 02 November 2008 - 08:14 AM

Next turn ;)

AnimationPrototype::KeyframeAnimationTrack<value_g, target_g>::Interpolate(...) can be made more efficient and also contains several problems as well as an error.

1st, it is inefficient to look up the left and right key-frame independently. Inside FindKey(...) you already iterated the list of key-frames, found the requested (left) key-frame and hence can step to the follower very efficiently. Instead, you use NextKey(...) and do the entire iteration again.

You could define a routine like FindKeys(KeyFrame*& left,KeyFrame*& right, real time) that returns both frames at once. Even better, you should integrate the functionality in the Interpolate(...) routine itself. Moreover, you can exploit temporal coherence: Store the iterator itself as member, and use its most recently value as start position for the next request.

2nd, in the Interpolate(...) routine itself, you allocate new Frame objects on-the-fly if any of FindKey(...) or NextKey(...) doesn't return a Frame instance. However, those allocation are never deleted, hence remaining as memory leaks!

3rd, in the Interpolate(...) routine, the interpolation weight is not calculated correctly. The interpolation weight has to be in the range [0,1] and obviously must depend on the value of time. Both is not given by your implementation.

The correct weight would be
weight = ( time - cur->time ) / ( next->time - cur->time )
This weight will be 0 if time==cur->time, and it will be 1 if time==next->time. For weight==0 the cur->value has to be returned, and for weight==1 the next->value, of course. From this assignment I assume that
AnimationUtils::Interpolate<value_g>(cur->value, next->value, weight);
would be the correct invocation of the interpolator, but it may be the other way around (check you interpolator implementation to clarify this).

There are other issues as well, e.g. to control what happens outside the animation interval and when looping, but that is not of primary interest now (until the other stuff works fine).

### #165haegarr  Crossbones+   -  Reputation: 7163

Like
0Likes
Like

Posted 02 November 2008 - 08:00 PM

Addendum; forgotten to mention this yesterday:

4th, the FindKey(...) routine uses identity comparison == for the time moment. You have to use >= instead.

### #166Gasim  Members   -  Reputation: 207

Like
0Likes
Like

Posted 06 November 2008 - 02:17 AM

Hello,
I changed what you said:

	bool FindKeys(Keyframe *& left, Keyframe *& right, real time) const {		for(KeyList::const_iterator i = keys.begin(); i != keys.end(); ++i) {			if((*i)->time >= time) {				left = *i;				right = *i + 1;				if(left && right) return true;			}		}		return false;	}template<class value_g, class target_g> value_g AnimationPrototype::KeyframeAnimationTrack<value_g, target_g>::Interpolate(real time) const {	Keyframe * cur = NULL;	Keyframe * next = NULL;		if(FindKeys(cur, next, time)) {		real weight = (time - cur->time) / (next->time - cur->time);		return (value_g) AnimationUtils::Interpolate<value_g>(cur->value, next->value, weight);		}	return (value_g) Math::EmptyValue<value_g>();}

class AnimQuatVariable {public:	Quat value;	real totalWeight;	AnimQuatVariable() : totalWeight(0) {}	void Reset() { totalWeight = 0; }		void Blend(Quat &other, real weight) {		totalWeight += weight;		value.Nlerp(value, other, (weight/totalWeight));	}};class AnimVector3Variable {public:	Vector3 value;	real totalWeight;	AnimVector3Variable() : totalWeight(0) {}	void Reset() { totalWeight = 0; }		void Blend(Vector3 &other, real weight) {		totalWeight += weight;		value = Math::Lerp<Vector3>(value, other, (weight/totalWeight));	}};

		template<class M> static M Lerp(const M &v1, const M &v2, T factor) {			return v1 + ( v2 - v1 ) * factor;		}class AnimationUtils {public:	template<class T> static T Interpolate(T &v1, T &v2, real factor) {		return Math::Lerp<T>(v1, v2, factor);	}	template<> static Quat Interpolate<Quat>(Quat &q1, Quat &q2, real factor) {		Quat q;		q.Nlerp(q1, q2, factor);		return q;	}};

But it doesn't change.

Thanks,
Kasya

### #167haegarr  Crossbones+   -  Reputation: 7163

Like
0Likes
Like

Posted 07 November 2008 - 05:16 AM

Are you sure that *i+1 will be interpreted as *(i+1)? I would expect it being interpreted as (*i)+1, what obviously would be problematic if the iterator doesn't happen to point into an array of keyframe pointers. Hmmm.

However, its time to check the implementation step by step, since staring at the whole bunch of code doesn't expose secrets anymore (at least to me). ;)

(1) After adding 3 or 4 keyframes to a track, does the invocation of FinKeys(...) for several different time values return the correct keyframes? Use at least value before, between the both first, between the 2nd and 3rd, and after the last keyframe.

(2) If (1) is okay, does the track's Interpolate(...) routine work okay as well? (BTW: I would detach KeyframeAnimationTrack from AnimationPrototype, making a class by its own; but that is mainly a matter of taste.) Check this for the time values as above, and check it for both quaternions as well as positions.

(3) If (2) is okay, does the Variable:Blend(...) behave fine as well?

(4) ...

You should think of "unit tests". I.e. the tests shown above should not be written for a one-shot run and then be put into waste. Instead, write the tests above in a function (perhaps class) each, write a main(...) that invokes all that tests, and let it live inside your code forever. Then, whenever you have edited the animation or skeleton classes, you can run the test's main(...) and verify that the code still behaves as expected. If it doesn't run, then check whether the code or the (old) tests were erroneous, and correct the failures accordingly. This is good practise in general. Please google for "unit test" to learn more about it. There are also support tools available if you want to do the tests in a more formal manner.

### #168Gasim  Members   -  Reputation: 207

Like
0Likes
Like

Posted 10 November 2008 - 06:36 AM

1) Variable::Blend works fine
2) AnimationUtils::Interpolate works fine (TESTED By Putting values in left, right and blend_factor)
3) Track::Interpolate doesn't work fine:
REASON:

value_g Interpolate(real time) {if(FindKeys(cur, next, time)) {  real weight = (time - cur->time) / (next->time - cur->time);  return (value_g) AnimationUtils::Interpolate<value_g>(cur->value, next->value, weight);	} else {return Math::EmptyValue<value>(); //That returns//For Vector3: Vector3(0,0,0)//For Quat: Quat(0,0,0)//For others: 0}}

Suppose we have 2 Keyframes:

Key1:
Value: Vector3(0,1,2);
Time: 1

Key2:
Value: Vector3(0,-1,-2)
Time: 2

And a local time which is always going in interval (0,2)

if(time > 2) time = 0;
time ++;

Now Interpolation Formula:
real weight = (time - cur->time) / (next->time - cur->time);

1) time = 0
no value for current keyframe

2) time = 1
cur->time = 1

weight = ( time - cur->time) / (...)
time = cur->time;
weight = 0

3) time = 2
cur->time = 2

weight = ( time - cur->time) / (...)
time = cur->time;
weight = 0

---------------
Function FindKeys():

bool FindKeys(Keyframe *& left, Keyframe *& right, real time) const {	for(KeyList::const_iterator i = keys.begin(); i != keys.end(); ++i) {		if((*i)->time >= time) {			left = (*i);			if( (i + 1) != keys.end()) {				++i;				right = (*i);				return true;			}										}	}	return false;}

When (*i)->time >= time, cur->time != time. But i think cur->time = time here too. I think the problem happens from there. Because that place doesn't work.

Thanks,
Kasya

P.S i'll test that track->interpolate thing outside the contribute to see if that works. (Forgot about that)

EDIT:
Tested the track->interpolate outside the contribute. About not changing the value its because of target thing i'll solve it. The only problem i found now is it doesn't read the keyframe in the last time (please look at the FindKeys and Interpolate function). does it need to read that keyframe.

### #169Gasim  Members   -  Reputation: 207

Like
0Likes
Like

Posted 10 November 2008 - 06:57 AM

The only thing that i have found for that is:

value_g KeyframeAnimationTrack<value_g, target_g>::Interpolate(real time) const {	Keyframe * cur = NULL;	Keyframe * next = NULL;		real curT = 0;	real nextT = 0;	value_g curV = Math::EmptyValue<value_g>();	value_g nextV = Math::EmptyValue<value_g>();	if(FindKeys(cur, next, time)) {		curT = cur->time;		nextT = next->time;		curV = cur->value;		nextV = next->value;			} 	else if(cur != NULL) {		curT = cur->time;		curV = cur->value;			}	real weight = (time - curT) / (nextT - cur->time);	return (value_g) AnimationUtils::Interpolate<value_g>(curV, nextV, weight);}

if we need to read last time's cur->time

### #170haegarr  Crossbones+   -  Reputation: 7163

Like
0Likes
Like

Posted 11 November 2008 - 05:54 AM

I'm not able to follow all of your thoughts, Kasya, so I may post something that is already clear. Sorry for that.

(1) If the time overhanded to Interpolate(...,real time) does match a time stored in a keyframe, then ofc the weight becomes computed as 0. Due to
l + ( r - l ) * w = l for w==0
the exact value of the keyframe is returned in such a case, what is correct, isn't it?

The computation
weight = (time - cur->time) / (next->time - cur->time)
is nothing else than transforming the time value into the interval [0,1) if the left and right keyframes are found (_and_ are ordered left-to-right, see below), so making weight a relative time w.r.t. the both involved keyframes.

(2)
Quote:
 Original post by KasyaWhen (*i)->time >= time, cur->time != time. But i think cur->time = time here too.

I don't understand this.

(3) I mentioned already earlier that handling of "off-range" time moments must be defined in greater detail one day, and that integrating FindKeys(...) into Interpolate(...) would have an advantage. Now that day has arrived ;)

There are at least 2 possible ways to offer to an animator how to deal with time moments before the 1st keyframe: Returning a specific constant value or returning the value of the 1st keyframe. For a time behind the last keyframe both ways are possible, too, but also a cycling could be done. Very advanced animation systems may deal not only with a simple cycling but "cue points", and that anywhere in the track. E.g. a cue point may express "jump back to local time 10 sec for the next 5 times when passing here". (I don't say that you should implement this!)

When you extend Track with ins and outs like those above, you'll see that it can still not be guaranteed to find a left and right keyframe, but it can be guaranteed that the animator can specify the behaviour over the full temporal range of the animation. If not both the left and right keyframe could be found, then the resulting value will not be computed by interpolation but from a single value. Moreover, the computation of the weight depends on the method of the active "in" or "out". Hence IMHO it is better to integrate the functionality of FindKeys(...) into Interpolate(...), as said.

(4) Now the handling of the time can be outlined as follows: The global game time defined where the "playhead" is in the timeline. Somewhere at a defined moment in time an animation starts in the timeline. The animations local time is then computed by subtracting its start time from the global time. If the result is negative, i.e. the playhead is _before_ the animation start, then the animation isn't active yet and processing it should terminate. (Next the local time has to be scaled to allow smooth blending, but that comes later when dealing with controlling.) The (scaled) local time is then fed into the Interpolate(...) routine. The track then subtracts its own time reference (that is initially zero but will become different when cycling/jumping is done) and uses this track-local time to search for the next pair of keyframes. If you use cue points, then you must not skip over cue points during this search, of course.

(5) In a comment in your code you initialize a quaternion with (0,0,0). A quaternion has of course 4 values. I mention this to make sure that the quaternion you return at default is a _unit_ quaternion. It can't be seen from the (0,0,0) whether actually (0,0,0,0) or (1,0,0,0) is meant; the former value will not work well.

### #171revel8n  Members   -  Reputation: 205

Like
0Likes
Like

Posted 11 November 2008 - 02:41 PM

Hello,
Are you still using the originally posted test code?

Although if you only have a single track it won't matter (or at least won't see the problem), but to be completely accurate your Blend method would need to know the totalWeight before using the current tracks weight in the division. Adding it up as you go changes the ratio each time a new tracks weight is added.

Also in your test code it appears you add two key values at time 0 and 1 (or 1 nd 2 - depending on the post), yet you increase the time value by using time++ which increments by 1 each time, skipping in between float values of time (try testing with keys at times 0 and 2 if you want to keep it like this).

It's also probably best to ensure keys are within some valid time range regardless of the time passed, and disregard animations by some other means - use curr->value if time < curr->time, use next->value if time > next->time, depends on your needs I guess.

bool FindKeys(Keyframe *& left, Keyframe *& right, real time) const{	if (!keys.empty())	{		KeyList::const_iterator curr = keys.begin();		KeyList::const_iterator next = curr;		for(; next != keys.end(); ++next)		{			if((*next)->time > time)			{				break;			}			curr = next;		}		if (next == keys.end)		{			next = curr;		}		left = (*curr);		right = (*next);		return true;	}	return false;}

Be mindful of any inaccuracies - treat more as a pseudo-code suggestion - depending on how you choose to handle things you may always want the true current and previous/next keys instead of them being the same key when the time is out of range.

Other than that, from the quick glance I did I don't particularly see a direct solution to your issue. May need a few more clarifications on how exactly you are handling things.

----------------------------"Whatever happens, happens..." - Spike"Only the strong survive, if they choose to leave those weaker than themselves behind." - Myself

### #172haegarr  Crossbones+   -  Reputation: 7163

Like
0Likes
Like

Posted 11 November 2008 - 09:13 PM

Quote:
 Original post by AmadeusAlthough if you only have a single track it won't matter (or at least won't see the problem), but to be completely accurate your Blend method would need to know the totalWeight before using the current tracks weight in the division. Adding it up as you go changes the ratio each time a new tracks weight is added.

Interestingly, all you've said in the above citation is correct, but your conclusion of incorrectness of the method is wrong (I must assume that your intention was to hint at an incorrectness of the method). It is definitely intention that the totalWeight increases with each additional blending. Because I'm too lazy to write the proof down again, please look at the 3rd page of this thread into my post where I explain the method in great detail, beginning with "So far you need the a-priori knowledge of all weights to work".

Quote:
 Original post by AmadeusAlso in your test code it appears you add two key values at time 0 and 1 (or 1 nd 2 - depending on the post), yet you increase the time value by using time++ which increments by 1 each time, skipping in between float values of time (try testing with keys at times 0 and 2 if you want to keep it like this).It's also probably best to ensure keys are within some valid time range regardless of the time passed, and disregard animations by some other means - use curr->value if time < curr->time, use next->value if time > next->time, depends on your needs I guess.*** Source Snippet Removed ***

The animation system is intended to be open. One issue is to break the need for simultaneous keyframes in all tracks of an animation. Another is to allow single tracks to have their own behaviour (e.g. off range handling). Animations and their belonging tracks have a defined start, but their end may be open (i.e. the end may be determined at runtime, or may last until the end of the gameplay; notice that this animation system is not restricted to drive character animation, and mechanical devices may for sure have an endless animation w.r.t. the gameplay). If e.g. jumping/cycling is allowed, the tracks will furthur need to deal with their own local time. Considering all this, each track has to deal with a "valid time range" by its own, but finding a valid time range on the animation level w.r.t. track blending seems me senseless. As stated in my post above, a settings dependent search for the valid keyframes, integrated into the Interpolate(...) routine will solve the problem.

Quote:
 Original post by AmadeusBe mindful of any inaccuracies - treat more as a pseudo-code suggestion - depending on how you choose to handle things you may always want the true current and previous/next keys instead of them being the same key when the time is out of range.

Yes, it is senseful to distinguish the different off range situations.

Old topic!

Guest, the last post of this topic is over 60 days old and at this point you may not reply in this topic. If you wish to continue this conversation start a new topic.

PARTNERS