Replicating Wolfire's Procedural Animation Approach in Unity?

What’s the best way to blend between single animation keyframes with custom curves in Unity?

I’m trying to see how feasible it would be to use the procedural animation approach described here: http://www.gdcvault.com/play/1020583/Animation-Bootcamp-An-Indie-Approach inside Unity, and I haven’t been able to find a way to blend between individual keyframes using arbitrary curves for the influence of each keyframe. However, I could easily be missing something because I’m not very familiar with Unity’s animation facilities and the legacy animation system (which I would probably need to use instead of Mecanim?) is poorly documented.

Does Unity’s animation system support this usecase at all, or would it be necessary to write an animation importer and then use that as the basis for a fully-custom animation blending system that manually moves bones in LateUpdate? This option might work, but it seems horribly inefficient (is it?) and duplicates much of Unity’s existing animation functionality.

I know this thread is one year old, but i recently tried to do the same thing and i found a possible solution.

It might not be perfect, but it can help someone who is trying to do something similar

( Note : This uses the Legacy Animation system and you need to create 1 frame long static animations for each pose )

The PoseSequence class used below :

using UnityEngine;


[System.Serializable]
public class PoseSequence
{

    public string Name = "";
    public string[] Poses = new string[0];


    public AnimationCurve Curve;

}

The PoseManager Class :

    using UnityEngine;
using System.Collections.Generic;

// This component manages pose transitions.
// A pose is a 1 frame long static animation, it must be added to the 'Animation' component to be usable.
// The curves used by this component can be any length but must start at time 0 and the weight must stay between 0 and 1.
public class PoseManager : MonoBehaviour
{

    // The Legacy Animation component, note that the BoseBlender component must be on the same GameObject
    Animation _animation;

    // A pose sequence array for sequences that will be used often and don't need to be tweaked.
    // It's totally optionnal so you can remove it, the corresponding 'StartSequence' and 'GetPreset' methods if you don't need it.
    public PoseSequence[] Presets = new PoseSequence[ 0 ];

    // The current timer in the sequence
    float _timer;

    // The duration of the sequence and the curve and a boolean indicating if they differ.
    float _sequenceDuration, _curveDuration;
    bool _isDurationOverriden;

    // Stored weight to check if the curve changed direction since last frame, indicating that a new transition started
    float _lastWeight;

    // The index of the fading in pose
    int _index;
    // The scale of the weight to apply to the currently fading in pose
    float _weightScale;

    // Stored boolean representing the current direction of the curve
    int _curveDirection;

    // Is the current sequence looping
    bool _looping;

    // The curve used by the current sequence. The curve should start at time 0 for and its weight must stay between 0 and 1. Note that if you are trying to loop the sequence, the curve should be looping too ( ending at the same weight it starts )
    AnimationCurve _curve;

    // The animations representing the poses used by the current sequence
    AnimationState[] _states;

    List<AnimationState> _fadingOutStates;
    AnimationState _fadingInState;

    public delegate void DelegateOnPoseChanged ( string poseName );
    // An event called every time a pose reached its maximum weight. 
    // It's optionnal, use it if you want to react to a pose that has finished fading in.
    public event DelegateOnPoseChanged OnPoseChanged;


    // Fetching the Animation component
    void Start ()
    {
        _animation = GetComponent<Animation> ();
    }

    void Update ()
    {
        if ( _states == null ) return;

        _timer += Time.deltaTime;

        float deltaWeight = EvaluateCurve ();

        // Updates the weight if the sequences is still running
        if ( _states != null )
            UpdateWeight ( deltaWeight );

        if ( _timer >= _sequenceDuration )
            NextTransition (); 
    }


    // Remove this if you removed the Presets array
    public void StartPresetSequence ( string name )
    {
        StartPresetSequence ( name, false, 0f );
    }

    // Remove this if you removed the Presets array
    public void StartPresetSequence ( string name, float overrideDuration )
    {
        StartPresetSequence ( name, false, overrideDuration );
    }

    // Remove this if you removed the Presets array
    public void StartLoopingPresetSequence ( string name )
    {
        StartPresetSequence ( name, true, 0f );
    }

    // Remove this if you removed the Presets array
    public void StartLoopingPresetSequence ( string name, float overrideDuration )
    {
        StartPresetSequence ( name, true, overrideDuration );
    }

    // The poses strings represent the AnimationClip name, just like in the Animation component
    public void StartSequence ( AnimationCurve curve, params string[] poses )
    {
        StartSequence ( poses, curve, false, 0f );
    }

    public void StartSequence ( AnimationCurve curve, float overrideDuration, params string[] poses )
    {
        StartSequence ( poses, curve, false, overrideDuration );
    }

    public void StartLoopingSequence ( AnimationCurve curve, params string[] poses )
    {
        StartSequence ( poses, curve, true, 0f );
    }

    public void StartLoopingSequence ( AnimationCurve curve, float overrideDuration, params string[] poses )
    {
        StartSequence ( poses, curve, true, overrideDuration );
    }

    // Remove this if you removed the Presets array
    void StartPresetSequence ( string name, bool looping, float newDuration )
    {
        PoseSequence preset = GetPreset ( name );

        if ( preset == null ) return;

        StartSequence ( preset.Poses, preset.Curve, looping, newDuration );
    }

    void StartSequence ( string[] poses, AnimationCurve curve, bool looping, float overrideDuration )
    {
        if ( curve.keys.Length != poses.Length + 1 )
            throw new System.Exception ( "The curve must have n+1 keys where n is the number of poses. Keys = " + curve.keys.Length + ", poses : " + poses.Length );

        _states = new AnimationState[ poses.Length ];

        // Get AnimationStates from the Animation component
        for ( int i = 0 ; i < poses.Length ; i++ )
            _states[ i ] = _animation[ poses[ i ] ];

        _curve = curve;
        _looping = looping;

        // Get the curve's last key time
        _curveDuration = GetNativeCurveLength ( curve );

        // Check if the override duration makes sense.
        if ( overrideDuration > 0f && overrideDuration != _curveDuration )
        {
            _sequenceDuration = overrideDuration;
            _isDurationOverriden = true;
        }
        else
        {
            _sequenceDuration = _curveDuration;
            _isDurationOverriden = false;
        }

        _timer = 0f;

        FirstTransition ();
    }

    // Remove this if you removed the Presets array
    PoseSequence GetPreset ( string name )
    {
        for ( int i = 0 ; i < Presets.Length ; i++ )
            if ( Presets[ i ].Name == name )
                return Presets[ i ];

        return null;
    }

    float GetNativeCurveLength ( AnimationCurve curve )
    {
        return curve.keys[ curve.keys.Length - 1 ].time;
    }

    // Shortcut to evaluate the curve at the current timer
    float EvaluateCurve ()
    {
        float clampedTimer = Mathf.Clamp ( _isDurationOverriden ? ( _timer * _curveDuration / _sequenceDuration ) : _timer, 0f, _curveDuration );

        return EvaluateCurve ( clampedTimer, true );
    }

    // Check direction and weight variations since last evaluation
    // If the curve changed direction, start transitionning to next pose in the array.
    float EvaluateCurve ( float time, bool recordWeight )
    {
        float currentWeight = _curve.Evaluate ( time );

        float deltaWeight = Mathf.Abs ( currentWeight - _lastWeight );

        int direction;

        if ( currentWeight > _lastWeight )
            direction = 1;
        else if ( currentWeight < _lastWeight )
            direction = -1;
        else direction = 0;

        if ( recordWeight )
        _lastWeight = currentWeight;

        if ( CheckIfCurveChangedDirection ( direction ) )
        {
            _curveDirection = direction;
            NextTransition ();
        }

        return deltaWeight;
    }


    bool CheckIfCurveChangedDirection ( int currentDirection )
    {
        // if the current direction has not been found, it just accepts the new one
        if ( _curveDirection == 0 )
        {
            _curveDirection = currentDirection;
            return false;
        }

        // if the curve is currently flat, the direction does not change
        if ( currentDirection == 0 || _curveDirection == currentDirection )
            return false;

        return true;
    }

    void GetCurveInitialDirection ()
    {
        float predictionTimer = .05f;

        // Try to find the first slope in the curve, in case it's starting flat
        // If no slope is found, an exception is thrown
        while ( _curveDirection == 0 )
        {
            EvaluateCurve ( predictionTimer, false );
            predictionTimer += .05f;

            if ( predictionTimer > _curveDuration )
                throw new System.Exception ( "Flat Curves are not supported." );
        }
    }

    void FirstTransition ()
    {
        _index = 0;

        _curveDirection = 0;

        // Get the starting weight of the first pose according to the curve initial weight
        var weight = _curve.Evaluate ( 0f );
        _lastWeight = weight;

        GetCurveInitialDirection ();

        _fadingOutStates = new List<AnimationState> ();

        // Get all the currently active state in the Animation component to fade them out.
        foreach ( AnimationState state in _animation )
            if ( state.enabled )
                _fadingOutStates.Add ( state );

        DoTransition ();
    }

    // Goes to the next transition. If the current transition was the last one, stops the sequence or resets it if the sequence is looping
    void NextTransition ()
    {
        // Call the OnPoseChanged event if a listener is registered.
        if ( OnPoseChanged != null )
            OnPoseChanged ( _fadingInState.name );

        for ( int i = 0 ; i < _fadingOutStates.Count ; i++ )
        {
            _fadingOutStates[ i ].enabled = false;
            _fadingOutStates[ i ].weight = 1f;
        }

        _fadingOutStates = new List<AnimationState> ();
        _fadingOutStates.Add ( _fadingInState );

        _index++;

        if ( _index >= _states.Length )
        {
            if ( _looping )
            {
                _timer -= _sequenceDuration;
                FirstTransition ();
            }
            else
            {
                Stop ();
                return;
            }
        }

        DoTransition ();
    }

    void DoTransition ()
    {
        _fadingInState = _states[ _index ];

        _fadingInState.enabled = true;
        _fadingInState.weight = 0f;

        _weightScale = _curveDirection > 0 ? 1f - _lastWeight : _lastWeight;
    }

    void UpdateWeight ( float deltaWeight )
    {
        // Add weight to the fading in pose factored by the weight scale since the weight of the curve when the pose started might not be 0 or 1
        _fadingInState.weight += deltaWeight * _weightScale;

        float sharedWeight = ( 1f - _fadingInState.weight ) / _fadingOutStates.Count;

        // Remove weight from all the fading out poses. If their weight reach 0, disable them.
        for ( int i = 0 ; i < _fadingOutStates.Count ; i++ )
        {
            var state = _fadingOutStates[ i ];

            // No need to remove weight if the state is disabled or if, for some reason, it's the currently fading in pose.
            if ( !state.enabled || state == _fadingInState ) continue;

            state.weight -= deltaWeight;

            if ( state.weight <= 0f )
            {
                Debug.Log ( "State faded off : " + state.name );
                state.enabled = false;
                state.weight = 1f;
                _fadingOutStates.Remove ( state );
            }
        }
    }

    void Stop ()
    {
        _states = null;
    }

}