Skip to content

Animation & Rigging: Non-Linear Animation System

Todo: Brief intro to the NLA

NLA Stack Evaluation

Root function: ( animsys_calculate_nla()

NLA: rewrite evaluation channel data structures (D4120)

For a given frame, there are several strips that may be evaluated and blended. We begin with an empty NlaEvalSnapshot. Strips are then blended from the bottom-most track to the top-most track, storing results within an NlaEvalChannelSnapshot. If the channel snapshot did not exist already, then its allocated and filled with default values based on the underlying channel's RNA property type. If already existing, then we overwrite relevant NlaEvalChannelSnapshot values with the blended result. In the end the NlaEvalSnapshot contains the fully blended NLA stack. A domain() pass afterwards adds default channels for those affected by the NLA at some point but were not currently evaluated. Effectively, the domain() pass forces such channels to evaluate to default. Channels that are never touched by the NLA evaluation remain untouched.

There are two special cases for whether a strip is evaluated and blended, described below.

Special Case: Action Track

Root function: ( nonstrip_action_fill_strip_data() (refactor)

Special Case: Tweaked Strip

Root function: ( animsys_append_tweaked_strip() (refactor)

  • Animated Time: Currently there is no proper UI support when the tweaked strip has animated strip time. Thus we evaluate it as if it's not animated with an Extrapolation of Hold. The evaluation time is also independent of the strip's start frame ( nlastrip_evaluate_controls() and unclamped.
  • Synced Action Length: If active, then the tweaked strip will evaluate according to the actions bounds instead of the strip's bounds. (NLA: Evaluate Tweak Strip Within Synced Action Bounds (D7533))
  • No other strips in the same track will evaluate.

Blending Strips

Related functions: ( nla_blend_value(), nla_combine_value(), nla_combine_quaternion()

We blend lower NLA stack snapshot result (lower_value) with the next strip's evaluated value (fcurve_value) accordingly:

  • Replace:

blended_value = lower_value * (1.0f - influence) + (fcurve_value * influence);

  • Combine: Depends on the underlying type:
case default:
  blended_value = lower_value + (fcurve_value - base_value) * influence;
case Proportional Properties:
  blended_value = lower_value * powf(fcurve_value / base_value, influence);
case Quaternion:
   blended_value = lower_values @ fcurve_values^(influence)
  • Add:
/* Simply add the scaled value on to the stack. */
blended_value = lower_value + (fcurve_value * influence);
  • Subtract:
/* Simply subtract the scaled value from the stack. */
blended_value = lower_value - (fcurve_value * influence);
  • Multiply:
/* Multiply the scaled value with the stack. */
blended_value = influence* (lower_value * fcurve_value) + (1 - influence) * lower_value;


Keyframing to Action Track

Root function: ( nonstrip_action_fill_strip_data() (refactor)

  • Ignores Extrapolation property to allow keyframing anywhere.

Keyframing to Tweaked Strip

Root function: ( animsys_append_tweaked_strip() (refactor)

  • If strip bounds is synced to action bounds, then we ignore the NlaStrip's Extrapolation property to allow keyframing anywhere. (D7533)
  • If strip has animated strip time, then we allow keyframing anywhere.

Keyframe Remapping

Root function: ( BKE_animsys_nla_remap_keyframe_values()

NLA: insert keyframes correctly for strips with non-Replace mode (D3927)

NLA: Feature: NLA: Evaluate Whole NLA Stack in Tweak Mode (D8296)

When the user keyframes through the viewport, they expect their pose to be preserved. What you see is what you get. The general implementation follows. We view the result of the NLA system as:

\[ final\_nla\_result = f_N(f_{N-1}(\dots f_2(f_1(default\_values,\ strip_1\ values),\ strip_2\ values),\ \dots),\ tweaked\ strip's\ values), \dots),\ strip_N) \]

To find tweaked strip's values, we effectively have to apply each function's inverse sequentially. We do this for each blended strip above the tweaked strip, getting blend_result_after_tweak:

\[ {blend\_result\_after\_tweak = f_{tweak}(f_1(...(f_{1}(default\ values,\ strip_{1}\ values),\ \dots),\ strip_{tweak-1}\ values),\ tweak\ strip's\ values)} \]
\[ {blend\_result\_after\_tweak = f_{tweak}(lower\ stack\ result,\ tweak\ strip's\ values)} \]

where we also know the value of the lower stack result. Thus we apply the inverse of \(f_{tweak}()\) to solve for tweak strip's values. It's important to note that this value is what the fcurve should evaluate to. It does not mean it's the keyframe co value that should be inserted because fmodifiers may change the fcurve value. Currently only cyclic fmodifiers are properly remapped through (Add an option to do keyframe insertion in a cycle-aware fashion). Solving this is not the job of the NLA system.

We currently only properly support keyframe remapping when the tweaked strip's underlying action occurs once in the current frame. Keyframing through some transitions are problematic (Quaternion Combine strip to other Quaternion non-Combine strips). Until Feature: NLA: Evaluate Whole NLA Stack in Tweak Mode (D8296), tweak mode would only evaluate from the first strip up to the tweaked strip and exclude the strips above it. So before, we would only have the second equation.