Animation Blending

Started by
170 comments, last by haegarr 15 years, 5 months ago
I was thinking about the same thing, but I don't think it's a good idea to let the animation track know anything about the skeleton. I'd rather iterate over all the tracks in a SkeletonAnimation class, so when you add something else than skeletal animation (e.g. particle animation, ...) you just have to add a new animation class.

class AnimTrack{   // only the relevant code is shown here now   public:      // these could also be pure virtual, depending on whether you allow just AnimTracks      virtual int GetInterpolatedPosition(float CurrentTime, Vector3 &v) { return 0; }      virtual int GetInterpolatedRotation(float CurrentTime, Quaternion &q) { return 0; }};class PosTrack : public AnimTrack{   // only the relevant code is shown here now   public:      int GetInterpolatedPosition(float CurrentTime, Vector3 &v)      {          // Interpolate between positions, depending on CurrentTime          // v = output parameter          // ...          return 1;      }};class RotTrack : public AnimTrack{   // only the relevant code is shown here now   public:      int GetInterpolatedRotation(float CurrentTime, Quaternion &q)      {          // Interpolate between rotations, depending on CurrentTime          // q = output parameter          // ...          return 1;      }};void SkeletonAnimation::Update(float FrameDeltaTime){   /* Call the base class update function */   Animation::Update(FrameDeltaTime);   /* Perform specific code for skeleton animation */   // Temporary variables   Vector3 TempPos;   Quaternion TempRot;   // Iterate over all the tracks and let them contribute to the overall pose   for(AnimTrackIter It = m_Tracks.begin(); It < m_Tracks.end(); ++It)   {      int BoneID = It->GetResourceID();      Bone &CurrentBone = m_pSkeleton->GetBone(BoneID); // SkeletalAnimation::m_pSkeleton (this could also be a list of affected skeletons)      if(It->GetInterpolatedPosition(m_CurrentTime, TempPos))  // found a position track          CurrentBone.SetPosition(TempPos);      if(It->GetInterpolatedRotation(m_CurrentTime, TempRot))  // found a rotation track          CurrentBone.SetRotation(TempRot);   }   // Rest of the code   // ...}


Other possible solutions are:
- using RTTI to identify which type of track we are iterating over and then contribute the result as necessary

- store an enumeration-ID in the AnimTrack which we fill in during construction, so we can query for the type of track (I don't think this will work if you derive further from e.g. PosTrack

- it should be possible to solve this using the Visitor Pattern, but it seems overkill for a situation like this

@everyone: let me know what you think about this solution
Advertisement
@godmodder

I think its a good idea to let animation track not know about Skeleton. But what you think about that code:

//cnode.hclass CNode {private://Nothing Inside};//CSkeleton.hclass CSkeleton : private CNode {//Code inside};//CParticle.hclass CParticle : private CNode {//Code Inside};//CAnimation.hclass CAnimationTrack {///////Some Code/////////void Contribute(CNode * node, float time);};


Now we can use there Particle, Skeleton etc. etc.

@godmodder Why we need Keyframe here if we don't use it in anywhere?

Is it good code?
Quote:Now we can use there Particle, Skeleton etc. etc.


How? You still need to identify which kind of node you're contributing to. You'd have to do exactly the same as in my previous post, only now for CNode and it's derived classes. For example: what if you're contibuting a texture track to a bone node? Obviously this doens't make any sense and the operation should be ignored. So you would have to identify that you're dealing with a bone node. This can be done by any of the solution is my last post. In short: I think defining new CNode classes is almost the same as defining new Animation classes.

Second, why do you use private inheritance? Does it provide some benefit here I can't see?

Quote:Why we need Keyframe here if we don't use it in anywhere?

Ofcourse we still need it. We use it to get interpolated values. In your case, you'd use it in the Contribute function.

[Edited by - godmodder on August 29, 2008 5:49:32 AM]
@godmodder

Quote:
Second, why do you use private inheritance? Does it provide some benefit here I can't see?


because its empty i can put private.

Thats my Code:

class CAnimationTrack {	public:		unsigned int trackID;		unsigned int nodeID;		virtual bool GetInterpolatedPosition(float currentTime, CVector3 &v) { return false; }		virtual bool GetInterpolatedRotation(float currentTime, CQuat &q) { return false; }};class CAnimationTrackPos : public CAnimationTrack {	public:		std::vector<CKeyframe<CVector3> *> keys;		bool GetInterpolatedPosition(float currentTime, CVector3 &v) {                         //Some Code Inside (Really Don't know Yet)			return true; 				}};class CAnimationTrackRot : public CAnimationTrack {	public:		std::vector<CKeyframe<CQuat> *> keys;		bool GetInterpolatedRotation(float currentTime, CQuat &q) {                        //Some Code Inside (Really Don't know Yet)			return true;		}};


Is that a right Code?

I need to use Contribute(CSkeleton *pFinalSkel, CAnimationTrack *track, float time); inside CSkeletalAnimation?
Thanks,
Kasya
There is also another solution. Its coarse structure may look like this:
class Animation::Track {public: // handling    virtual void contribute(float localTime,float weight) const =0;};template<typename target_g,typename value_g>class KeyframedAnimationTrack : public Animation::Track {public: // x'tors    KeyframedAnimationTrack(target_g& target) : _target(&target) { ... }public: // handling    void contribute(float localTime,float weight) const {        value_g current = interpolate();        _target->accuBlend(current,weight);    }protected: // key-frames    struct Keyframe {        float moment;        value_g value;    };private: // fields    target_t* _target;    vector<KeyFrame> _keyframes;};

So Animation need not know what a skeleton is, and Animation::Track need not know it, too. The only point of such knowledge is where the animation (instance) is bound to the skeleton (instance).

The trick here is to reduce the view onto the targeted value to just that value: value_g is e.g. a Vector3 for a position or a Quaternion for an orientation or a float for a weight or a RGBA for a color. The mysterious target_g is a structure/class that consists mainly of a field of type value_g (or at least a compatible type), but also a (not necessarily virtual) blend function that performs the actual accumulated blending. Notice please that the target_g may also provide particular channels if value_g is a vector typed value type (err, what a nice word construct ;) ).

Of course, you can set target_g also to your CNode classes, i.e. a value class on a higher level than e.g. a Vector3Target is. But IMHO that is not necessary in most cases.
I see what your intention is, but I don't understand the details of it:

Quote:value_g current = interpolate();

How does this work? Do I have to specialize the interpolation method for every value_g? Would I have to use a template function in this template class? I'm not such a template wizard, so could you please elaborate on this ;)
A simple solution IMO would be to move the interpolation code to the target. But maybe you have an argument against this?

Quote:_target->accuBlend(current,weight);

Suppose that _target is a Vector3Target here and I want to apply the position to a bone. Should a bone be derived from a Vector3Target class then? What if a bone is a QuaternionTarget too then? Would I have to use multiple inheritance? Somehow I don't think this is what you meant, but could you clarify this please.


Overall, your solution seems pretty interesting. This generic programming approach is totally different from the OO approach I was taking and it has the advantage of less runtime overhead. One disadvantage is that template code is more difficult to read IMHO. I don't know if I should be ashamed of myself, but it took some time before I completely understood the code above :)

Jeroen
What you think of that code?

//Animation.hclass CAnimationTrack {	public:		unsigned int trackID;		virtual void Contribute(float localTime, float weight)=0;};template<class value_g, class target_g>class CKeyframeAnimationTrack {	public:		CKeyframeAnimationTrack(target_g target) : m_target(&target) {}		void Contribute(float localTime, float weight) {					value_g current;			Interpolate(current);			BlendTarget(current, weight);		}		virtual void BlendTarget(value_g current, float weight)=0;	protected:		struct Keyframe {						value_g value;			float time;		};	private:		target_g *m_target;		std::vector<Keyframe> keys;};//SkeletalAnimation.htemplate<class value_g> class CSkeletalAnimationTrack : public CKeyframeAnimationTrack<value_g, CBone> {	public:		void BlendTarget(value_g current, float weight);		};


and what is target->accuBlend(current,weight); for?
And how Interpolate function Interpolates without time. maybe you mean current = Interpolate(localTime);

Thanks,
Kasya
@ both godmodder & Kasya:

I've written
value_g current = interpolate();
just to say "insert value computation here". You can of course replace that method invocation with a direct implementation or whatever. And also "interpolation" is only an example, see below. Sorry for not making that clear earlier.

You do not need to specialize the "interpolation" for every value_g. In the first sense, the implementation of "interpolate" depends on the kind of track. E.g. a KeyframeAnimationTrack interpolates linearly between its 2 key-frames; a Bezier spline interpolation computes the result using its polynomial between keys; a FunctionAnimationTrack doesn't interpolate at all. And so on.

So, in general, not the concrete type of value_g but the operation defines "interpolate". However, it may happen that a special type of value_g needs a special implementation, but that are details. But you can see that moving this interpolation code (totally) into the target would be not senseful, since it is the job of the Animation::Track class to hide the exact computation method. Nonetheless, for value_g some basic mathematical operations has to be provided that are used by the implementation of "interpolate" (or whatever) to do its job.

An example (not compiled or tested):
template<...>voidKeyframeAnimationTrack<...>::contribute(float localTime,float weight) const {    // looking up for the left and right frame, dependend on localTime, here expressed as method invocations    Keyframe* left = findKeyframe(localTime);    Keyframe* right = nextKeyFrame(left);    // linear interpolation    float where = ( localTime - left->time )/( right->time - left->time );    value_g current = lerp( left->value, right->value, where );    // (all the above is meant as one possible implementation of "interpolate()")    // blending    _target->accuBlend(current, weight);}

The challange here is how "lerp(...)" is defined. One can argue that it should be a method of value_g. This would work fine for Vector3, but neither for float (because it is no class) nor for Quaternion (since it were not clear whether slerp or nlerp). It could be a method of KeyframeAnimationTrack, but only pure virtual, or all thinkable variations must be foreseen. It could be a method of target_g, but then again forecast is an issue. Concrete instances of KeyframeAnimationTrack<...> could be written to define hand-made contribute routines, but that would be more work and clutters the class repository. IMHO, it would be best if external inlined functions are used. If so, then a more meaningful name like "keyframeLerp" (in this case) or an own namespace like "KeyframeUtils::lerp" would be senseful.

I'm open for discussion of the above issue. :)

Looking at target_g, there is a bit another requirement. An instance of target_g must not only hold an instance of value_g (or at least a compatible value), but it also holds the field to summarize the weights for animation level blending. To hint at this detail, I've written target->accuBlend(...) instead of target->set(...).

For example (not compiled, tested, and not complete):
class QuaternionVariable {public:    Quaternion value;    float totalWeight;    void clear() {        value = Quaternion::IDENTITY;        totalWeight = 0.0f;    }    void accuBlend(const Quaternion& other,float weight) {        totalWeight += weight;        value.nlerp( other, weight/totalWeight );    }};



Quote:Original post by godmodder
Overall, your solution seems pretty interesting. This generic programming approach is totally different from the OO approach I was taking and it has the advantage of less runtime overhead. One disadvantage is that template code is more difficult to read IMHO. I don't know if I should be ashamed of myself, but it took some time before I completely understood the code above :)

You should of course not be ashamed. Understanding code mostly benefits from experience. I'm programming since 1985 now, but that doesn't mean that I don't learn new things the one or other day.


As ever, I only suggest things here. I'd glad to read meanings about it, and perhaps improvement suggestions.
@haegarr

    void accuBlend(const Quaternion& other,float weight) {        totalWeight += weight;        value.nlerp( other, weight/totalWeight );    }


That means my Skeleton needs to have a weight? And if totalWeight = 0, weight / totalWeight = 1.

Thanks,
Kasya
Quote:Original post by Kasya
That means my Skeleton needs to have a weight?

Nope. The animated attributes need a weight each. E.g. the position (if animated anyhow) of each bone, and the orientation of each bone need a weight. And if you want to animate the x, y, and z channels of the position separately, then they need 3 weights in total for that attribute.

The weights we are speaking about here are for both animation blending and animation layering. You can get away of using particular weights if you enforce animations to be always complete w.r.t. the animateable attributes of a skeleton. I.e. each and every animation _must_ then provide a track for each bone position and each orientation. In such case you can easily pre-normalize the weights. But I suggest to not go the way of complete animations in the above sense but to store the weights besides the attributes as the QuaternionVariable class demonstrates in my previous post.

Quote:Original post by Kasya
And if totalWeight = 0, weight / totalWeight = 1.

That is intention. If you think furthur what happens in the sub-sequent blend routine, you got
value := value * ( 1 - weight / totalWeight ) + other * weight / totalWeight = value * ( 1 - 1 ) + other * 1 = other
what is the same as doing an assigment. But notice that this is true only for the first invocation after a clear(). Sub-sequent invocation will find a totalWeight greater than 0, and then weight / totalWeight != 1 will happen.

This topic is closed to new replies.

Advertisement