Animation Blending

Started by
170 comments, last by haegarr 15 years, 6 months ago
Hello,

I started working on CSkeletonInstance and CSkeletonPrototype classes. I really didn't get one thing here:

class CSkeletonPrototype {public://Bone classpublic:CSkeletonPrototype(CSkeletonInstance &backing);//Other functions};


When you use CSkeletonPrototype(CSkeletonInstance &backing); here where it saves that? Doesn't there need to be something like CSkeletonInstance *backing; member variable? Because there is no any std::vector<Bone *> bones.

class CSkeletonPrototype {public://Bone Classpublic:CSkeletonInstance * newInstance();//Other Functions};


What does CSkeletonInstance * newInstance(); do? There is no any CSkeletonInstance::CSkeletonPrototype *backing to put it inside CSkeletonInstance or CSkeletonInstance::std::bones<Bone * bone> to put Prototype bones inside Instance bones.

I really don't understand why SkeletonInstance needed. Because it can't affect to Prototype from anywhere. Please show a little code for connection between them.

Thanks,
Kasya
Advertisement
@Kasya

Why do you mean that SkeletonPrototype should be backed by SkeletonInstance? It is vice-versa (if you follow the usual nomenclature). A prototype is a general, perhaps "global" entity, while an instance is more a specialized or local entity. Hence we have a many-to-one relation from instances to their prototype. In other words, the instances refer to the prototype. Hence, the correct c'tor would be
SkeletonInstance::SkeletonInstance(SkeletonPrototype& backing) ...

Whether overhanding "backing" here at all is questionable. That depends on whether we need it for some task. E.g. in my engine there is the need to lock shared objects that are in use, so that they can't be deleted as long as some other object is referring to it (catchword "smart pointers"). As written above, the prototype is shared by instances, so I lock the prototype from being deleted at least as long as at least one instance is referring to it. Another aspect is that my engine has a build in editor capability, so it is fine if the designer can inspect to which skeleton prototype the instance belongs to. As a conclusion you can drop that parameter until it gets classified as senseful. In the latter case you will of course need some field to store it. As written in my previous posts, the examples I show may be incomplete w.r.t. such things.

Think of the SkeletonInstance as a complex variable that stores an entire pose. The value of the instance is the summary of all joint positions and orientations (as said, it is complex), and there are operators and/or routines to manipulate that value. This is analogously to a simple, say, integer variable like
int a;
where operators like assign, plus, and minus exist for.

From the above it should be clear that the SkeletonPrototype object should never be affected by the runtime system. Instead, the SkeletonInstance objects will be affected. In particular, the instance has bones and the bones has the Vector3Variable and QuaternionVariable. The instance provides this to the outer world, but self does not anything (okay, in fact "not very much") to the content. This is, as said, like a variable, that stores a value but does not alter it. Now, an altering instance is the animation. See my previous lengthy post with the title "Here we go ... Don't shoot me, you've asked for details ;)" in the 5th code snippet the "contribute" routine; there happens the main manipulation stuff.


The SkeletonPrototype::newInstance(...) routine is what is called a "factory" method. Factory methods are e.g. good for generating new objects in dependency on the own state, but without the need to make the state public. So clients need not know about the specialities of the object providing the factory, they only need to know the factory method itself. Instead of letting a client iterate the prototype's bones, creating the instance's bones, and connect them, we sensefully wrap this tasks into the factory.

We have in fact several places where we can build a SkeletonInstance. We can let it be done by a client anywhere. That is often not good because "anywhere" may be needed from several clients (e.g. the AI and scripting). Having a defined place is hence senseful. There is the c'tor of SkeletonInstance (useable when overhanding the backing prototype), a static factory in SkeletonInstance, and a factory method in SkeletonPrototype. Using the c'tor doesn't allow caching. Factory methods, on the other hand, are good in dealing with caches. The static factory is less senseful, since it needs to iterate all cached instances until one is found that belongs to the correct prototype. Hence, asking the particular prototype directly (remembder that the prototype is a kind of singleton) seems me the best way.

Another aspect comes from the edit ability build into my engine, but that is something you can ignore. However, I only show possibilities. You can do something else, of course, whatever fits best into your application.

Now, to give you some clues, the implemention (w/o caching) is like
SkeletonInstance*SkeletonPrototype::newInstance() const {	SkeletonInstance* result = new SkeletonInstance(/* *this */); // parameter according to the needs	forEach( Bone* currentBone in _bones ) {		SkeletonInstance::Bone* companion = new SkeletonInstance::Bone(*currentBone);		result->addBone(companion);	}	return result;}

and with caching (as a forecast ;)) perhaps like
SkeletonInstance*SkeletonPrototype::newInstance() const {	SkeletonInstance* result = fromCache();	if(!result) {		result = new SkeletonInstance(/* *this */); // parameter according to the needs		forEach( Bone* currentBone in _bones ) {			SkeletonInstance::Bone* companion = new SkeletonInstance::Bone(*currentBone);			result->addBone(companion);		}	}	return result;}


Please compare this also with the factory of AnimationInstance I've presented in the post cited already above.
Hello,
@haegarr

Quote:
Why do you mean that SkeletonPrototype should be backed by SkeletonInstance? It is vice-versa (if you follow the usual nomenclature).


Sorry i just put the c'tors in wrong place when i typed the code.

Anyway,

in your code (w/o catching and with catching) you used result->AddBone(companion). That means you have std::vector<CSkeletonInstance::Bone *> bones, right?

My Question before your reply was the same with the above

And why you always use const? Does it change something? Or its just for like anti-cracking thing?

Thanks,
Kasya

P.S I don't have a good writing english. When i write something i write very very mixed. :)
Quote:Original post by Kasya
in your code (w/o catching and with catching) you used result->AddBone(companion). That means you have std::vector<CSkeletonInstance::Bone *> bones, right?

Doing so is one possibility, yes. I intentionally haven't shown this aspect before, just to stay out of discussion about it. Then I've demonstrated the above way since you've asked explicitely for it.

However, I am personally not a fan of allocating masses of tiny objects, especially in cases that burden the smoothness of runtime (i.e. "in game" instanciation). See that the SkeletonInstance::Bone objects are all of the same type (at least as long as we don't diverge from what is developed so far), and furthur that the number of objects is known due to the bones in the skeleton prototype. Hence an array can be used, too, reducing the allocation stuff to just a single (array) object.

One can do something like the following incomplete code:
class SkeletonInstance {public: // x'tors	SkeletonInstance(const SkeletonPrototype& backing)	: _backing(&backing),	  _bones(static_cast<Bone*>(0)) { }	~ SkeletonInstance() {		delete _bones;	}private: // collaborators	const SkeletonPrototype* _backing;public: // bones	void allocateBones() {		delete _bones;		_bones = new Bone*[_backing->numBones()];	}	inline Bone& boneAt(uint32_t index) {		// index checking for non-RELEASE target should be placed here		return _bones[index];	}private: // bones	Bone* _bones;};voidSkeletonPrototype::newInstance() const {	SkeletonInstance* result = new SkeletonInstance(*this);	result->allocateBones();	for(uint32_t index=0; index<numBones(); ++index) {		result->boneAt(index).setup(_bones[index]);	}	return result;}

However, people tend to avoid plain arrays nowadays. So I will not enforce you to do things you don't want to do. You can also use the std::vector with objects, of course, or you stick with the std::vector<Bone*> solution. After all I have already hinted at the possibility of caching ;).

Quote:Original post by Kasya
And why you always use const? Does it change something? Or its just for like anti-cracking thing?

Marking things as const has at least 3 advantages:
(1) It tells others something about the intention of the programmer how to use and how s/he is using the things.
(2) It allows the compiler to detect some programming errors.
(3) It allows the compiler to do some additional optimizations (presumably).


EDIT: Initializer for SkeletonInstance::_bones added to c'tor.

[Edited by - haegarr on September 12, 2008 9:09:38 AM]
Quote:However, I am personally not a fan of allocating masses of tiny objects, especially in cases that burden the smoothness of runtime (i.e. "in game" instanciation). See that the SkeletonInstance::Bone objects are all of the same type (at least as long as we don't diverge from what is developed so far), and furthur that the number of objects is known due to the bones in the skeleton prototype. Hence an array can be used, too, reducing the allocation stuff to just a single (array) object.


The number of allocations is going to be the same for an array or a vector, no? The extra space needed for a vector is neglegible IMHO. The actual allocations are also performed at initialization time and not during runtime, so accessing the elements of the vector should give the same performance as indexing into the array. Vectors also guarantee, just like arrays, that their elements are laid out consecutively in memory. So unlike linked lists, for example, they don't create a lot of memory fragmentation.

@Kasya: I decided to stick with the vector for now. I can always remove it later when I truly see that I won't need it, but I think that extending the animation system is going to be alot easier with it. (e.g. when the bones of an instance need to be adapted at runtime).

@haegarr: About the caching, I understand it is quite important because you can edit the instances with the editing functionality in the engine. However, I don't have that, so the creation of instances is done solely at initialization. I don't suppose I have much use of caching then?
Also, how does this caching technique roughly work? Do you store a couple of recently used prototypes in the cache or perhaps you do something more sofisticated?

Jeroen
Quote:Original post by godmodder
The number of allocations is going to be the same for an array or a vector, no? The extra space needed for a vector is neglegible IMHO.
The actual allocations are also performed at initialization time and not during runtime, so accessing the elements of the vector should give the same performance as indexing into the array.

This is presumably true. Hence my hint at "using std::vector with objects" ;)

To make it clear: The main aspect is definitely whether to allocate a couple of objects and to store pointers to it, or else to allocate the objects as a block. Especially if the animation system will be developed in direction of a blend tree, those many dozen allocations all the time will be noticeable.

As an example, in the indi game "Mount & Blade" now and then enforcement troups are instanciated during a battle. Your machine can be a 4 core Xeon @ 3GHz, it plays no role: The gameplay stops for 3 or 4 seconds. Although I do not really know what happens under the hood, I suspect the instanciation of skeletons and meshes to cause that stop, or at least they are part of it. And, while the game itself is IMHO a Good Thing, that occurances of stops are a big mess.
Quote:Original post by godmodder
@haegarr: About the caching, I understand it is quite important because you can edit the instances with the editing functionality in the engine. However, I don't have that, so the creation of instances is done solely at initialization. I don't suppose I have much use of caching then?
Also, how does this caching technique roughly work? Do you store a couple of recently used prototypes in the cache or perhaps you do something more sofisticated?

The caching is senseful especially for the gameplay, not for editing. The prototypes are like singletons: They are created once at load time (of the game or of a game level) and stay alive. Instead, the cached objects are the instances. And instances are made every now and then. E.g. if you have slain an Orc, its skeleton (not to say the entire model) can be re-used for an Orc of the reinforcement troops waiting behind the next corner.

The caching is as simple as not deleting once created instances but linking them into an internal list. Then, when a new instance is needed, the next instance from the list is unlinked and used. A "new" is performed only if no more instances are available from the list. The only other thing I do is that the caches can be limited to a maximum amount of instances. More sophisticated mechanisms are thinkable, e.g. a deletion of aged instances, but I haven't plans for such things ATM.

And of course, you don't need to implement caching now. If you use the factory method pattern, and return no longer used instance to a "shredder" routine nearby, you're totally okay for now and have the ability to implement caching later when needed.
Hi to everyone!!! I been working for some months on animations (I still do) and I wish I would have had this thread when I started!!! It seems that most of the problems and the solutions you had, are the same I had some months ago (I still have some). I will try to catch up with the discussion and to put my 2 cents. Sorry if I miss something, but reading a 6 pages thread and trying to catch up at once is not easy =o)

Concerning to the interpolators:
Quote:Original post by godmodder
Anyway, as a possible improvement, we could have the best of both worlds and implement the interpolations as static methods in the value classes (like Quaternion). Then, we can use different interpolation functions when working directly with Quaternions, and we can automatically call the desired function in the templatized animation functions.

The problem I found with coupling the interpolation function to the track data type was that I was missing a lot of CPU when using complex functions and a specific animation was not an esential part of the scene (like an animation LOD). For example, you can use an accurate function to interpolate quaternions when a charater is near the camera, but you want to change it in runtime if it is far (strategy design pattern).

So it would be something like:
template<class T>class Interpolator<T>{	virtual void interpolate(T& result, T& t1, T& t2, float t);};class QuaternionSlerp : Interpolator<Quaternion>{	void interpolate(Quaternion& result, Quaternion& t1, Quaternion& t2, float t) { /***/ }};class QuaternionNlerp : Interpolator<Quaternion>{	void interpolate(Quaternion& result, Quaternion& t1, Quaternion& t2, float t) { /***/ }};class Track<T>{	void setInterpolator( Interpolator<T> i );};



Another problem I had was the track target. I first used the solution you proposed:
template<typename target_g, typename value_g>class AnimationTrack<target_g,value_g>

but it lead to a big explosion of derived classes to almost always have the same target and value type, so I gave up with the target template parameter.

This topic leads to value binding (for just one track). Again, I first used the solution you proposed:
void SetTarget(Target *pTarget) { m_Target = static_cast<TargetVar*>(pTarget); }

but this has two problems for me:

1) You don't always have the pointers to the data you want to change (it may be a property wrapped inside set & get methods)

2) I had a specific case when the value type you compute is not the value type you finally want to change (I found only one, but there may be more): quaternions and matrices. I compute the quaternion to transform it to a matrix and use that matrix to do the skinning. I didn't find a clean way to compute the data conversion inside the track and (as mentioned earlier) I used just one data type for the track. Besides that... it was just ONE case!!!

So I decided that the user should do whatever he wants with the value. The track's responsability is just changing a value over time and control playback. The data conversion and use are done at a higher level, so the track system remains generic.

For example:
class TransformationController{	Track<Quaternion> rotTrack;	Track<Vector3> posTrack;	void bind ()	{		Matrix3 mat = rotTrack.ToMatrix();				// now you have the matrix & the vector to 		// compute the final transformation & apply		// it where you want.	}};

On the other side, I think the *Prototype and *Instance classes can be merged together and use a cloning system with some sharing technique (for example, smart pointers), to avoid the class explosion and gain code legibility.

I found this discussion very interesting, so I will try to keep up with the thread. There are lots of topics left to talk about!!! Back to work now!

Just my 2 cents! =o)
Juan Manuel Alvarez - Naicigam
Quote:
You can also use the std::vector with objects, of course, or you stick with the std::vector<Bone*> solution.


What you mean with objects?

objects are without pointers right??

Quote:The problem I found with coupling the interpolation function to the track data type was that I was missing a lot of CPU when using complex functions and a specific animation was not an esential part of the scene (like an animation LOD). For example, you can use an accurate function to interpolate quaternions when a charater is near the camera, but you want to change it in runtime if it is far (strategy design pattern).


That's a very good idea to improve performance. I almost never use SLERP though...

Quote:but it lead to a big explosion of derived classes to almost always have the same target and value type, so I gave up with the target template parameter.


You gave up on the target parameter? What is your alternative then?

But now that I think about it: how big can the explosion be? Almost every animated target I can think of fits in a float, vector3 or quaternion variable. That's 3 classes generated for the specific tracks, 3 for the typed tracks, 3 for the typed bindings, 3 for the target variables and 3 for the keyframes (very small class). This makes for a total of 15 generated classes. (5 generated classes per additional animated variable type)
As you can see, this is just a linear increase in classes and while it might seem much, let's not forget that I can represent almost every kind of animated attribute with these already.

Quote:On the other side, I think the *Prototype and *Instance classes can be merged together and use a cloning system with some sharing technique (for example, smart pointers), to avoid the class explosion and gain code legibility.


Decoupling the classes to avoid excessive memory usage is exactly the goal of the flywheel pattern we've applied to them. I'd be most interested to see how you would accomplish the same with a merged version. How can you avoid all the tracks being copied when a prototype and an instance are the same?

Jeroen

This topic is closed to new replies.

Advertisement