AnimationClip Optimization/keys-reduction

Hi everyone :slight_smile:

Animations…
more accurately animationCurves…

I have a ‘record-data’ script for my player ( 1 pos and 2 quats )… All is sent to AnimationClip, and clips are played with an Animation… ( all is legacy )

Everything works like a charm but…
My 18 animationClips are as big as my whole project !!! :o
They are 280MB !!!

Though there’s no problem playing them, i wish i reduce a lot those animations weights.

Therefore i was wondering about the method to be applied to animationClip for keys reduction ( according to an acceptable error ).

I gave some tries to many ideas but all of them reduce things too much and fu*k-up curves…

Therefore i come to get here your knowledge on how to reduce my keys to something more proper…

Thanks to all for your answers :slight_smile:

And happy unitying !

bumpies ?

Anyone has ideas of how to reduce keys of a clip ?

:smiley:

thanks for your answers !

really noone ???

This is quite weird !

Pleaaaase ! :frowning:

You can handle that similar to how Unity’s built in keyframe reduction happens. Compare the keyframe with previous and delete it if the position delta is under some arbitrary value (say .1m) or degrees of rotation between quats is under something like 1 degree. That’s all the built in Rotation Error and Position Error are doing:

Hi, I made a script to cleanup animations some times ago, it could be a good starting point for you:


using System.Collections.Generic;
using UnityEditor;
using UnityEngine;

public static class AnimationCleaner
{
    private const float TOLERANCE = 0.001f;
    
    [MenuItem( "GameObject/Clean Animation",       priority = 2000 )]
    [MenuItem( "Assets/Clean Animation",           priority = 2000 )]
    [MenuItem( "CONTEXT/Animator/Clean Animation", priority = 2000 )]
    private static void CleanAnimation( MenuCommand command )
    {
        var objects = Selection.objects;
        foreach( var obj in objects )
        {
            if( obj is GameObject go )
            {
                Cleanup( go );
            }
            else if( obj is AnimationClip clip )
            {
                Debug.Log( "-----------------------------------------------------------------------------------------------" );
                Debug.Log( "CLEANING " + clip.name );

                var total   = 0;
                var cleaned = 0;
                
                Cleanup( clip, ref total, ref cleaned );
                
                Debug.Log( "DELETED KEYS: " + cleaned + "/" + total );
                Debug.Log( "-----------------------------------------------------------------------------------------------" );
            }
        }
    }
    
    // Show the menu item only if there is a selection in the Editor
    [MenuItem( "GameObject/Clean Animation",       validate = true )]
    [MenuItem( "Assets/Clean Animation",           validate = true )]
    [MenuItem( "CONTEXT/Animator/Clean Animation", validate = true )]
    private static bool CleanAnimationValidate( MenuCommand command )
    {
        var objects = Selection.objects;
        foreach( var obj in objects )
        {
            if( obj is GameObject go )
            {
                var clips = AnimationUtility.GetAnimationClips( go );
                if( clips != null && clips.Length > 0 )
                    return true;
            }
            else if( obj is AnimationClip )
                return true;
        }
        return false;
    }
    
    private static void Cleanup( AnimationClip clip, ref int total, ref int cleaned )
    {
        Debug.Log( " • clip: " + clip.name );
            
        var bindings = AnimationUtility.GetCurveBindings( clip );
        if( bindings == null )
            return;
        
        foreach( var binding in bindings )
        {
            var curve    = AnimationUtility.GetEditorCurve( clip, binding );
            var toremove = new List<int>();
            var cnt      = curve.keys.Length;
            total       += cnt;
            
            if( cnt == 2 )
            {
                if( Mathf.Abs(curve.keys[0].value - curve.keys[1].value) < TOLERANCE )
                {
                    toremove.Add( 0 );
                }
            }
            else if( cnt > 2 )
            {
                for( var i = 1; i < cnt-1; i++ )
                {
                    var first   = curve.keys[0];
                    var prev    = curve.keys[i-1];
                    var key     = curve.keys[i];
                    var next    = curve.keys[i+1];
                    var isFinal = i + 1 == cnt - 1;

                    if( Mathf.Abs(key.value - prev.value) < TOLERANCE && 
                        Mathf.Abs(next.value - key.value) < TOLERANCE )
                    {
                        toremove.Add( i );
                        
                        if( isFinal && Mathf.Abs(key.value - first.value) < TOLERANCE )
                            toremove.Add( i+1 );
                    }
                }
            }

            if( toremove.Count > 0 )
            {
                cleaned += toremove.Count;
                
                for( var i = toremove.Count - 1; i >= 0; i-- )
                {
                    var idx = toremove[i];
                    var key = curve.keys[idx];
                    Debug.Log( "removing: " + binding.propertyName + " " + key.time + " " + key.value );
                    curve.RemoveKey( idx );
                }
                
                AnimationUtility.SetEditorCurve( clip, binding, curve );
            }
        }
    }
    
    private static void Cleanup( GameObject go )
    {
        Debug.Log( "-----------------------------------------------------------------------------------------------" );
        Debug.Log( "CLEANING " + go.name );

        var total   = 0;
        var cleaned = 0;
        
        var clips = AnimationUtility.GetAnimationClips( go );
        if( clips == null )
        {
            Debug.Log( "no Animation in this game object " + go.name );
            return;
        }
        
        foreach( var clip in clips )
        {
            Cleanup( clip, ref total, ref cleaned );
        }
        
        Debug.Log( "DELETED KEYS: " + cleaned + "/" + total );
        Debug.Log( "-----------------------------------------------------------------------------------------------" );
    }
}

Thanks for sharing this! I noticed it doesn’t check curve tangents, so I added that check. This helps prevent subtle animation drift after keyframe cleanup. Sharing here in case anyone else finds it useful :slight_smile:

using System.Collections.Generic;
using UnityEditor;
using UnityEngine;

public static class AnimationCleaner_v2
{
    private const float TOLERANCE         = 0.001f;
    private const float TANGENT_TOLERANCE = 0.01f;
    
    [MenuItem( "GameObject/Clean Animation_v2",       priority = 2000 )]
    [MenuItem( "Assets/Clean Animation_v2",           priority = 2000 )]
    [MenuItem( "CONTEXT/Animator/Clean Animation_v2", priority = 2000 )]
    private static void CleanAnimation( MenuCommand command )
    {
        var objects = Selection.objects;
        foreach( var obj in objects )
        {
            if( obj is GameObject go )
            {
                Cleanup( go );
            }
            else if( obj is AnimationClip clip )
            {
                Debug.Log( "-----------------------------------------------------------------------------------------------" );
                Debug.Log( "CLEANING " + clip.name );
 
                var total   = 0;
                var cleaned = 0;
                
                Cleanup( clip, ref total, ref cleaned );
                
                Debug.Log( "DELETED KEYS: " + cleaned + "/" + total );
                Debug.Log( "-----------------------------------------------------------------------------------------------" );
            }
        }
    }
    
    [MenuItem( "GameObject/Clean Animation_v2",       validate = true )]
    [MenuItem( "Assets/Clean Animation_v2",           validate = true )]
    [MenuItem( "CONTEXT/Animator/Clean Animation_v2", validate = true )]
    private static bool CleanAnimationValidate( MenuCommand command )
    {
        var objects = Selection.objects;
        foreach( var obj in objects )
        {
            if( obj is GameObject go )
            {
                var clips = AnimationUtility.GetAnimationClips( go );
                if( clips != null && clips.Length > 0 )
                    return true;
            }
            else if( obj is AnimationClip )
                return true;
        }
        return false;
    }
    private static bool IsFlatKey( Keyframe prev, Keyframe key, Keyframe next )
    {
        bool valueFlat = Mathf.Abs( key.value - prev.value ) < TOLERANCE &&
                         Mathf.Abs( next.value  - key.value ) < TOLERANCE;
 
        bool tangentFlat = Mathf.Abs( key.inTangent   ) < TANGENT_TOLERANCE &&
                           Mathf.Abs( key.outTangent  ) < TANGENT_TOLERANCE &&
                           Mathf.Abs( prev.outTangent ) < TANGENT_TOLERANCE;
 
        return valueFlat && tangentFlat;
    }
    
    private static void Cleanup( AnimationClip clip, ref int total, ref int cleaned )
    {
        Debug.Log( " • clip: " + clip.name );
            
        var bindings = AnimationUtility.GetCurveBindings( clip );
        if( bindings == null )
            return;
        
        foreach( var binding in bindings )
        {
            var curve    = AnimationUtility.GetEditorCurve( clip, binding );
            var toremove = new List<int>();
            var cnt      = curve.keys.Length;
            total       += cnt;
            
            if( cnt == 2 )
            {
                var k0 = curve.keys[0];
                var k1 = curve.keys[1];
                bool valueFlat   = Mathf.Abs( k0.value - k1.value ) < TOLERANCE;
                bool tangentFlat = Mathf.Abs( k0.outTangent ) < TANGENT_TOLERANCE &&
                                   Mathf.Abs( k1.inTangent  ) < TANGENT_TOLERANCE;
 
                if( valueFlat && tangentFlat )
                    toremove.Add( 0 );
            }
            else if( cnt > 2 )
            {
                for( var i = 1; i < cnt - 1; i++ )
                {
                    var prev    = curve.keys[i - 1];
                    var key     = curve.keys[i];
                    var next    = curve.keys[i + 1];
                    var isFinal = i + 1 == cnt - 1;
 
                    if( IsFlatKey( prev, key, next ) )
                    {
                        toremove.Add( i );
                        
                        if( isFinal )
                        {
                            var first = curve.keys[0];
                            bool lastValueFlat   = Mathf.Abs( next.value - first.value ) < TOLERANCE;
                            bool lastTangentFlat = Mathf.Abs( next.inTangent  ) < TANGENT_TOLERANCE &&
                                                   Mathf.Abs( next.outTangent ) < TANGENT_TOLERANCE;
                            if( lastValueFlat && lastTangentFlat )
                                toremove.Add( i + 1 );
                        }
                    }
                }
            }
 
            if( toremove.Count > 0 )
            {
                cleaned += toremove.Count;
                
                for( var i = toremove.Count - 1; i >= 0; i-- )
                {
                    var idx = toremove[i];
                    var key = curve.keys[idx];
                    Debug.Log( "removing: " + binding.propertyName + " " + key.time + " " + key.value );
                    curve.RemoveKey( idx );
                }
                
                AnimationUtility.SetEditorCurve( clip, binding, curve );
            }
        }
    }
    
    private static void Cleanup( GameObject go )
    {
        Debug.Log( "-----------------------------------------------------------------------------------------------" );
        Debug.Log( "CLEANING " + go.name );
 
        var total   = 0;
        var cleaned = 0;
        
        var clips = AnimationUtility.GetAnimationClips( go );
        if( clips == null )
        {
            Debug.Log( "no Animation in this game object " + go.name );
            return;
        }
        
        foreach( var clip in clips )
        {
            Cleanup( clip, ref total, ref cleaned );
        }
        
        Debug.Log( "DELETED KEYS: " + cleaned + "/" + total );
        Debug.Log( "-----------------------------------------------------------------------------------------------" );
    }
}