Skeletal Animation System

Built from scratch | Personal custom C++ engine
Key features
  1. Linear Interpolation (LERP) blending
  2. Animation crossfading
  3. Root motion data manipulation
  4. Raycast based obstacle detection
  5. Character controller (movement & animation)
  6. FBX data parsing
1. Linear Interpolation (LERP) Blending
General formula for LERP
Vec3 LERP
Quaternion LERP

I implemented Linear Interpolation (LERP) blending to blend between two different keyframes. Animation data is stored in the form of keyframes, where each keyframe denotes the local transform of each joint.

An animation file can contain animation that was sampled at different interval, but it is not likely that the game will sample them at the same interval.

2. Animation Crossfading

To make sure the smooth transition of animation when character goes from one movement state to another, I used animation crossfading to achieve the result.

Essentially, when the transition happens from one state (P) to another state (N), then for a certain period, defined as crossfade duration, both the previous animation (Pa), and the next animation (Na) are sampled. The results of these two animations are then blended using the linear interpolation (LERP) blending.

3. Root Motion Data Manipulation

Removing root motion data allows character movement to be governed by game's physics or character control system, rather than fixed animation scripts.

In my skeletal animation system root motion data removal happens in two stages:

  1. Engine parsing stage
  2. Runtime handling stage
3.a. Removal of Root Motion at Engine Parsing

In my engine when loading the animation file, 'remove root motion' attribute can be set to notify the animation controller to ignore the root motion data of the animation clip.

3.b. Root Motion Data Manipulation at Runtime
  • Get root motion data
  • Pre-processing:
    Curve editing
  • Selective sampling
  • Change global position
  • Camera independent lerp
  • Fix global position:
    match with next animation

Manipulation of root motion data at runtime happens at three different stages. The first is when a movement state starts, the second is when the animation is being played, and finally the third is when the movement state ends.

3.c. Selectively Sampling the Root Motion Data

To give my character the ability to vault over an arbitrary distance, I perform selective sampling on the root motion data.

The length of the vault is calculated based on the obstacle length. This data, combined with the forward root motion of the character, updates the global position of the character. Whereas, the upward and lateral root motion is left in the animation controller, which samples this data to update the root joint in model space for upward and lateral movement only.

3.d. Editing the Root Motion Data Curves

The curve editing happens at the start of the movement state. This step is custom handled differently for each animation that is manipulated at runtime.

The prime example of curve editing usage is when two animations that needs to be played after each other do not match with respect to their root motion data.

Unedited curve

Edited curve

  • Split the curve into 3 segments
  • First section: reposition to start at previous animation’s last value
  • Replace middle section with a custom Cubic Bezier curve
  • The third section matches with next animation, do nothing
Without Curve Editing
With Curve Editing
3.e. Updating the Global Position of the Character

When stitching multiple animations together, if the end of the root motion data of the previous animation doesn't match the start of the next animation, another trick to solve the mismatching data is repositioning the global position of the character after the end of each animation in the stitching process.

Mismatching Z root motion curves of consecutive animations

The world position of the character changes after each animation. The above sequence is the result of 4 animations stitched one after the other.

3.f. Updating the Camera During Animation

To ensure smooth camera movement, the camera is smoothly LERPed over the delta translation of the animation curve.

Upward smooth camera movement

Lateral smooth camera movement

4. Raycast Based World Detection

The obstacles of the world were detected by having multiple raycasts from the player in different directions.

I made a raycast vs Axis Aligned Bounding Box 3D (AABB3) visual testcase to test the algorithm in an isolated environment to make sure it is working properly.

5. Character Controller
Movement Controller
Animation Controller
  • Movement state
  • Physics movement of the character
  • Keyboard input
  • Root motion data manipulation
  • Obstacle detection
  • Animation state
  • Animation blending
  • Animation crossfading
  • Mesh rendering
  • Skinning & Texturing

I architectured a custom character controller to handle the animation and physics movement of my character. It is subdivided into movement controller and character controller.

5.a. Character Controller Pipeline for Vault
5.b. XML Data Driven Animation States

The Animation states are defined in the XML file which is parsed at the startup. For each animation state, I decoupled the in-game name of that animation and the file name of that animation clip. I also specified explicitly what animation transitions are possible from a given state.

6. FBX SDK Data Parsing

The Animation data was parsed from FBX file format. I used FBX because it's an industry standard.

6.a. Breakdown of One Animation Curve in FBX