Animation Blending

Started by
170 comments, last by haegarr 15 years, 5 months ago
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
Advertisement
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.
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
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).
Addendum; forgotten to mention this yesterday:

4th, the FindKey(...) routine uses identity comparison == for the time moment. You have to use >= instead.
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
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.
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.
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
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 Kasya
When (*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.

This topic is closed to new replies.

Advertisement