animation - ryzom/ryzomcore GitHub Wiki


title: Animation System (2001) description: Design of the NeL low-level animation system — animated values, channels, tracks, keyframers, and skeleton blending published: true date: 2026-03-15T00:00:00.000Z tags: editor: markdown dateCreated: 2026-03-15T00:00:00.000Z

From "en 2001 3d animation" — an internal Nevrax design document describing the low-level animation system in NeL. {.is-info}

1. Low Level Animation System

This is the description of the low level part of NeL's animation system. It includes definitions of animated values, animatable objects, animations, tracks of animation, animation sets, channel mixers, and channels of animation.

Overview of the code design:

Class relationship diagram showing IAnimatable containing IAnimatableValues, connected to CChannelMixer containing CChannels, which connects to CAnimationSet containing CAnimations with ITracks

With this system, NeL can mix together several animations in three cases:

  • Temporal blend of two animations (transitions between animations).
  • Layer blend of two animations (e.g. walk slow 25% and walk fast 75%).
  • Per-bone weight blending using factors on the skeleton, with different weights per animation for each bone. For example: upper body shoot and lower body run (Skeleton Animation Blending).

Animated Value

An animated value (CAnimatedValue) is an abstracted type with blending capabilities. An animated value can blend itself with another one using a blend factor:

virtual void CAnimatedValue::blend(const IAnimatedValue &value, float blendFactor)

An animated value can also be assigned the value of another one:

virtual void CAnimatedValue::affect(const IAnimatedValue &value)

Animated values can have any kind of type: float, vector, quaternion, string, bool, int, etc.

Only animated values with the same type can be blended or affected. Runtime checks are performed in debug mode.

Blending of non-blendable values (like strings or booleans) selects one of the two values depending on the blend factor (<0.5).

Animated values are used directly in NeL's models. There can be a large number of animated values, so it is important to select the fewest number of values to compute (see Channel Mixer).

Any kind of value can be added to a model. This is useful for attaching game data to an object.

Animatable Object

All animatable objects must derive from the IAnimatable interface. This interface manages several animated values for the object. For each animated value, the object must be able to give its name and its default value.

For example, CTransform derives from IAnimatable and has a value for position, two values for rotation (euler and quaternion), a value for the pivot point, and a value for scale.

class CTransform : public IAnimatable
{
	// For animation, Pos, rot scale pivot animated values
	CAnimatedValueVector    _Pos;
	CAnimatedValueVector    _RotEuler;
	CAnimatedValueQuat      _RotQuat;
	CAnimatedValueVector    _Scale;
	CAnimatedValueVector    _Pivot;
protected:
	// For animation, default tracks pointers, must be set by the derived model.
	CTrackDefaultVector     *_PosDefault;
	CTrackDefaultVector     *_RotEulerDefault;
	CTrackDefaultQuat       *_RotQuatDefault;
	CTrackDefaultVector     *_ScaleDefault;
	CTrackDefaultVector     *_PivotDefault;
};

Animations

A CAnimation contains all the tracks of an object's animated values for a given animation.

If an animated value is not animated, it does not have a track in the animation.

Animation Tracks

An ITrack is an animated value's animation. When a value is animated, it has a track with the same name in the animation. The track has an eval() function that evaluates the value at a specific time. This value is managed internally and can be accessed using ITrack::getValue().

This value should have the same type as the channel. Checks are performed in debug.

Internally, ITrack can be a value interpolator of any kind. Implementations for Bezier, TCB, Linear, and Constant keyframers are provided.

A special ITrack for the default value is also implemented.

Animation Set

A CAnimationSet is the set of all the animations for an object. It is used by CChannelMixer to enumerate channels in the object.

Channel Mixer

The CChannelMixer manages animated values, animations, and tracks to efficiently evaluate animations and mix them together.

First you must select the CAnimationSet to work with. Then, the channel mixer allocates a channel array. This is the count of animated channels actually used by animations in the animation set.

The object adds its animated values to the channel mixer.

The animations used by the mixer are selected in 8 slots. Each slot has a weight and a time. Each channel has a weight per slot to perform Skeleton Animation Blending. This weight can be initialized with a Skeleton Weight Template (e.g. upper body, head, lower body, etc.).

When animation slots are modified, a linked list of channels used by the slots is built to speed up evaluation of the animation.

Call eval() at each frame to compute all the channel values.

Channels

A channel is an animated value with a name. It can be a position, a rotation, a color, a blend factor, a string, a boolean, or anything else.

This channel is held internally in the channel mixer.

The name of the channel is used to look up the correct track in an animation.

A default value is computed with a default track when animation tracks are missing for this channel.

In fact, channels are the link between animated values in NeL's models and tracks in animations.

2. CTransform

According to the Model Observer Traversal specifications, the hierarchy graph is a traversal of the scene. The model is a node of this graph, a CTransform. This node has several methods to set up its 3D transformations.

  • It has a pivot point P (a vector).
  • It has a scale S (a vector).
  • It has a rotation R (either an Euler angle or a quaternion).
  • It has a translation T (a vector).
  • It can inherit the local scale of its parent or not.

If the node inherits scale from its parent, the transformation formula is:

Node transformation = T x P x R x S x P^-1

If the node does not inherit scale from its parent, the transformation formula is:

Node transformation = T x (parent->S)^-1 x P x R x S x P^-1

Notes:

  • M^-1 is the inverse matrix of M.
  • Formulas are given in column matrix convention, i.e., the first transformation applied is the rightmost one.

The CTransform has channels for its pivot, scale, rotation, and translation.

3. Skeleton Definition

(Section left empty in the original document.)

4. Inverse Kinematics

(Section left empty in the original document.)

5. Identified Problems and Solutions

CTransform

Different rotation models (Maya and Max)

Max uses quaternions as its rotation model. Maya uses Euler angles. To play the same animations, NeL implements both models in the CTransform interface. Only one model is active at a time. A setMode function chooses the mode (quaternion or Euler) and the Euler rotation mode (order of rotation composition). Each value (quaternion and Euler angles) has its own channel. One restriction is that a CTransform can be animated by quaternion tracks or Euler tracks but not both types at the same time.

Values and Tracks

Linking animated values and tracks using string names

Animated values are managed by objects (such as lights, meshes, materials, textures) and are stored in 3D data files. Tracks are stored in animations and are kept in animation files. To link an animated value with its track, NeL uses names (strings) to find the correct objects.

Name conflicts in the channel mixer

The channel mixer manages all animated values that use the same animations at the same time. So there can be several bones, each with a position value. If the name of this channel is "POS", matching between channels and tracks cannot be done correctly. To avoid this problem, the export tool must create more explicit names for channels and tracks. For example, it can produce LEFT_FOOT.ROT for the rotation channel of the left foot, or SWORD.MAT0.DIFFUSE for the diffuse color channel of material #0 in the object named "sword".

How to deal with a missing track for an animated value

When a track is missing in a CAnimation for an animated value, NeL uses the default track to evaluate the missing value.

Default tracks: evaluation time and space considerations

To avoid the cost of evaluating default tracks as keyframers, NeL does not recommend using keyframers as default tracks. A special object derived from ITrack can be used instead: ITrackDefault. This class has only a default animated value and does nothing in eval(). So its size is the size of an animatable value + 4 bytes (vtable pointer).

Bigger size of animated value than value

The animated value's size is bigger than the raw value's size. The difference is a pointer to the object's virtual table (4 bytes). It is not clear whether this is a real problem.

Keyframer

Abstract keyframer system

The class ITrackKeyFramer can be implemented with any kind of keyframer. NeL implements TCB, Bezier, Linear, and Constant keyframers. The challenge is that each keyframer needs a different number of keys to compute a value. TCB needs two keys before and two keys after the current time. Bezier and Linear need one key before and one after. Constant needs only the key before. To handle this, ITrackKeyFramer has an eval function that always receives 4 keys (as in TCB).

Handling polymorphic keys in keyframers

In a keyframer (ITrackKeyFramer), keys are stored in a map with the time as the map key and a pointer to IKey as the map value. Pointers are needed because keys are polymorphic. To manage these pointers, the map uses std::auto_ptr<IKey> instead of raw IKey* pointers.

Blending

Blending of abstract animated values

NeL must be able to blend two animated values. This is done by the virtual method void IAnimatedValue::blend(const IAnimatedValue &value, float blendFactor). The two animated values must have the same type. Checks are done in debug mode.

Blending multiple values with a simple blend function

Assume we have 3 values to blend (v0, v1, v2) with 3 blend factors (f0, f1, f2) and a destination value dest. We can blend them using a simple blend() method:

dest.affect(v0);
dest.blend(v1, f0 / (f0 + f1));
dest.blend(v2, (f0 + f1) / (f0 + f1 + f2));

Skeleton weight template

The skeleton weight template is used to initialize the weight value of each channel for each slot in the channel mixer.

For example, slot #1 of the mixer is set with a shooting animation. This animation must be set to the upper body template, i.e., upper bones of the skeleton have a higher weight than lower bones. So, the weights of channels for slot #1 will be initialized with the skeleton weight template "upper body".

In practice, a skeleton weight template is a list of (string, weight) pairs where the string is the name of an animated value and the weight is the weight for that animated value.

Dealing with a large number of animated values

Add only animated values that are animated by at least one animation

When the game engine adds an animated value to the channel mixer, the mixer looks up the CAnimationSet for a channel ID with the name of the value. If it finds one, this value will be added to the channel mixer. If not, no animation uses this channel, so there is no need to add it to the mixer.

Selected channels linked in CChannelMixer to compute only needed channels

To speed up computation of animation in the channel mixer (method eval), a linked list of CChannel is built internally. This list links only the channels used by the animations set in the 8 slots. This list is rebuilt each time an animation slot is modified.

The number of animated values in IAnimatable is variable. A flag per channel is needed, implemented as a bit array.

CQuaternion class quality

The CQuaternion class needed significant rework. It could not be used in its original state at the time of writing.

⚠️ **GitHub.com Fallback** ⚠️