- Home /
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.
Answer by Tenser · Nov 03, 2015 at 05:24 PM
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;
}
}
Tenser, did you forget to post a component called "PoseBlender"? Your code works but with no blending, it doesn't seem be to complete.
The PoseBlender component is the Pose$$anonymous$$anager, i renamed it but forgot to update the comments
Ahh. Well, I ended up figuring it out on my own. $$anonymous$$aybe it's Unity 5.2.2f1 but your code, as-is, wouldn't blend between two poses. I had to change the Start() code slightly:
void Start()
{
_animation = GetComponent<Animation>();
if(_animation == null)
{
Debug.Log("Couldn't find Animation component");
return;
}
foreach(AnimationState s in _animation)
{
s.enabled = false;
s.speed = 0.0f;
s.weight = 0.0f;
}
}
Thanks for posting, I think I can do something of my own, now that this has taught me how to blend 1 frame poses.
It's weird, I tested it ( well, maybe not as much as i should have ) and it was working.
The point of the start method was to be able to fade out of the current pose, which you can't with the fix you made
I'll try to look into it but i think i can make something better without using the animation component and those 1 frame animations so it would be more straight forward
Did you ever get this to work properly? Or do you know of any resources that would help me learn about this technique more in order to implement it myself?