Search Unity

  1. Unity Asset Manager is now available in public beta. Try it out now and join the conversation here in the forums.
    Dismiss Notice

Sprite resolver & animation keyframe recording issue

Discussion in '2D Experimental Preview' started by OmniDeveloper, Jun 12, 2021.

  1. OmniDeveloper

    OmniDeveloper

    Joined:
    Dec 15, 2018
    Posts:
    2
    I've been toying around with the 2D experimental sprite swapping the past couple of days and trying to make the workflow a bit more automated. Today I was working on a script to automate animation creation and generate key-frames with the "Sprite Resolver.Sprite Key" as an animated property. I believe the script itself to work 'fine', but I am currently running into a bug when modifying the Sprite Key property in my animations. Here is some info on what I am observing

    My script does the following:
    • Takes one of my sprite library assets and extracts the categories and labels for each category
    • For each category, it generates a new key-frame array with the size of the array dependent on the number of labels per each category
    • Creates a new animation curve from the newly create key-frame array
    • Sets the clip tangent to 'broken' and the left & right tangents to constant
    • Create a new animation clip
    • Set the new animation clip curve to the animation curve previously created
    • Sets the name of the clip
    • Creates a new asset
    This process is iterated over the entirety of the sprite library.

    My goal is to set up the animations to decrease the amount of manual creation I have to do. My theory is to create the clips, create the key-frames, and assign the "m_SpriteKey" property to the curve. I would then manually record the key frames using the key-frame recording within Unity to set each Sprite Key.

    The bug I am observing is when recording the Sprite Keys, I have random key-frames where the Sprite Key appears to set properly, but upon exiting key-frame recording and 'scrolling' through the animation, some Sprite Keys are not being saved.

    I have attached a gif to showcase this as well as the code. The code is a bit rough and dirty.

    If I create the clip, forego setting the animation curve via code, manually create the key-frames and then assign the Sprites to each key-frame I do not experience this 'bug'.

    I have a feeling it has something to do with how I am generating my animation curve and/or assigning it to each clip.

    Any help or assistance would be great. Thanks for your time!



    Code (CSharp):
    1. #if UNITY_EDITOR || UNITY_EDITOR_64
    2. using System;
    3. using System.Collections.Generic;
    4. using System.Linq;
    5. using JetBrains.Annotations;
    6. using Sirenix.OdinInspector;
    7. using UnityEditor;
    8. using UnityEngine;
    9. using UnityEngine.Animations;
    10. using UnityEngine.Serialization;
    11. using UnityEngine.U2D.Animation;
    12. using Utility.Utilities;
    13.  
    14. namespace Utility.Generators {
    15.     public class AnimationGenerator : MonoBehaviour {
    16.         [FormerlySerializedAs("_spriteLibrary")] [SerializeField]
    17.                          private SpriteLibraryAsset _spriteLibraryAsset;
    18.         [SerializeField] private SpriteLibrary      _spriteLibrary;                 // not in use
    19.         [SerializeField] private GameObject         _gameObjectToDeriveAnimator;    // not in use
    20.         [SerializeField] private float              _desiredClipLength              = 1;
    21.         [SerializeField] private bool               _canOverwrite                   = false;
    22.  
    23.         [Button]
    24.         private void GenerateClips() {
    25.             if (!HasSpriteLibrary() || _gameObjectToDeriveAnimator == null) return;
    26.  
    27.             // Wait for method to expose sprite key hash generation ///////////////////////////////////////////
    28.             // 2d animations API
    29.             var animationComponent = _gameObjectToDeriveAnimator.GetComponent<Animation>(); // not in use
    30.             var animator = _gameObjectToDeriveAnimator.GetComponent<Animator>();            // not in use
    31.             ///////////////////////////////////////////////////////////////////////////////////////////////////
    32.            
    33.             var categories = GetCategoryNames().ToArray();
    34.  
    35.             for (var i = 0; i < categories.Count(); i++) {
    36.                 var (labels, labelCount) = ExtractLabelData(categories[i]);
    37.                 var keyframes =  GetNewKeyframeArray(labelCount).ToArray();
    38.                 var curve = GetNewAnimCurve(keyframes);
    39.                 var curveClipTangents = SetCurveClipTangents(curve, labelCount);
    40.                 var clip = GetNewAnimClip(curveClipTangents);
    41.                 var clipWithParametersSet = SetClipParameters(clip, categories[i]);
    42.                
    43.                 LogClipInfo(clipWithParametersSet);
    44.                 WriteToAssetDataBase(clipWithParametersSet);
    45.             }
    46.         }
    47.  
    48.         private AnimationClip SetClipParameters(AnimationClip clip, string category) {
    49.             clip.name = category;
    50.             return clip;
    51.         }
    52.  
    53.         private void LogClipInfo(AnimationClip clip) {
    54.             Omni.LogHeader(10, "Logging new clip information:");
    55.             Omni.LogBulleted($"Name: {clip.name}");
    56.             Omni.LogBulleted($"Average Duration: {clip.averageDuration}");
    57.             Omni.LogBulleted($"Legacy Status: {clip.legacy}");
    58.             Omni.LogBulleted($"Frame Rate: {clip.frameRate}");
    59.         }
    60.  
    61.         private void WriteToAssetDataBase([NotNull] AnimationClip clip) {
    62.             Omni.LogHeader(10, $"Saving {clip.name}...");
    63.             var assets = AssetDatabase.FindAssets($"{clip.name}.anim");
    64.             if (assets.Any() && !_canOverwrite) {
    65.                 Omni.LogBulletedWarning($"{clip.name} already exists... please enable 'CanOverwrite' to allow overwriting of animations");
    66.                 return;
    67.             } AssetDatabase.CreateAsset(clip, $"Assets/Sprites/Animations/Alpha/{clip.name}.anim");
    68.         }
    69.  
    70.         private AnimationCurve SetCurveClipTangents(AnimationCurve curve, int keyFrameCount) {
    71.             for (var i = 0; i < keyFrameCount; i++) {
    72.                 AnimationUtility.SetKeyBroken(curve, i, true);
    73.                 AnimationUtility.SetKeyLeftTangentMode(curve, i, AnimationUtility.TangentMode.Constant);
    74.                 AnimationUtility.SetKeyRightTangentMode(curve, i, AnimationUtility.TangentMode.Constant);
    75.             } return curve;
    76.         }
    77.  
    78.         private IEnumerable<string> GetCategoryNames() {
    79.             return _spriteLibraryAsset.GetCategoryNames();
    80.         }
    81.  
    82.         private (string[] labels, int labelCount) ExtractLabelData(string category) {
    83.             var labels = _spriteLibraryAsset.GetCategoryLabelNames(category).ToArray();
    84.             var labelCount = labels.Count();
    85.             return (labels, labelCount);
    86.         }
    87.  
    88.         private IEnumerable<Keyframe> GetNewKeyframeArray(int labelCount) {
    89.             var keyFrames = new Keyframe[labelCount];
    90.             var intervalArray = GetNewIntervalArray(labelCount);
    91.            
    92.             for (var i = 0; i < labelCount; i++) {
    93.                 keyFrames[i] = new Keyframe(intervalArray[i],i + 1);
    94.             } return keyFrames;
    95.         }
    96.  
    97.         private float[] GetNewIntervalArray(int labelCount) {
    98.             var intervalArray = new float[labelCount];
    99.             var counter = 0f;
    100.             var rate = _desiredClipLength / labelCount;
    101.            
    102.             for (var i = 0; i < labelCount; i++) {
    103.                 intervalArray[i] = counter;
    104.                 counter += rate;
    105.             } return intervalArray;
    106.         }
    107.  
    108.         private AnimationCurve GetNewAnimCurve(IEnumerable<Keyframe> keyFrames) {
    109.             return new AnimationCurve(keyFrames.ToArray());
    110.         }
    111.  
    112.         private AnimationClip GetNewAnimClip(AnimationCurve curve) {
    113.             var clip = new AnimationClip {
    114.                 legacy = false, wrapMode = WrapMode.Loop, frameRate = 30f
    115.             };
    116.             clip.SetCurve("", typeof(SpriteResolver), "m_SpriteKey", curve);
    117.             return clip;
    118.         }
    119.  
    120.         private bool HasSpriteLibrary() {
    121.             if (_spriteLibraryAsset != null && _spriteLibraryAsset.GetCategoryNames().ToArray().Length >= 1) return true;
    122.             Omni.LogHeaderWarning(10, "Please assign a sprite library to the Animation Generator");
    123.             return false;
    124.         }
    125.  
    126.         // NOT IN USE
    127.         private EditorCurveBinding[] GetCurveBindings([NotNull] AnimationClip clip) {
    128.             return AnimationUtility.GetCurveBindings(clip);
    129.         }
    130.     }
    131. }
    132.  
    133. #endif
     
    mitaywalle likes this.
  2. Ted_Wikman

    Ted_Wikman

    Unity Technologies

    Joined:
    Oct 7, 2019
    Posts:
    916
    Hello @OmniDeveloper
    Interesting project you have going! I had a brief look at the code you attached, but I can't see it actually injecting the Sprite Key (Categories & Label hash). Are there any changes to the code you have locally that you haven't shared here?
     
  3. frankadoriv

    frankadoriv

    Joined:
    Jul 19, 2017
    Posts:
    14
    @Ted_Wikman excuse me, how the SpriteKey value is calculated? This way?:


    Code (CSharp):
    1. var spriteKey = Animator.StringToHash(categoryName + labelName);
     
  4. Ted_Wikman

    Ted_Wikman

    Unity Technologies

    Joined:
    Oct 7, 2019
    Posts:
    916
    @frankadoriv

    Unity 2020.3 - 2D Animation 5.x
    Category and Label generates their own hash using the following method:
    Code (CSharp):
    1. var hash = Animator.StringToHash(value);
    2. var bytes = BitConverter.GetBytes(hash);
    3. var exponentialBit = BitConverter.IsLittleEndian ? 3 : 1;
    4. if (bytes[exponentialBit] == 0xFF)
    5.     bytes[exponentialBit] -= 1;
    6. return BitConverter.ToInt32(bytes, 0);
    (Code can be found in SpriteLibraryAsset.cs)

    Unity 2021.3 - 2D Animation 7.x
    The Category and Label strings are merged together
    $"{category}_{label}"
    and then using the same hashing method as 2020.3:
    Code (CSharp):
    1. var hash = Animator.StringToHash(value);
    2. var bytes = BitConverter.GetBytes(hash);
    3. var exponentialBit = BitConverter.IsLittleEndian ? 3 : 1;
    4. if (bytes[exponentialBit] == 0xFF)
    5.     bytes[exponentialBit] -= 1;
    6. return BitConverter.ToInt32(bytes, 0);
    (Code can be found in SpriteLibraryAsset.cs)

    Unity 2022.1 - 2D Animation 8.x (and newer)
    The Category and Label strings are merged together
    $"{category}_{label}"
    and then using the following hashing method:
    Code (CSharp):
    1. static int Bit30Hash_GetStringHash(string value)
    2. {
    3.         var hash = Animator.StringToHash(value);
    4.         hash = PreserveFirst30Bits(hash);
    5.         return hash;
    6. }
    7.  
    8. static int PreserveFirst30Bits(int input)
    9. {
    10.         const int mask = 0x3FFFFFFF;
    11.         return input & mask;
    12. }
    (Code can be found in SpriteLibraryUtility.cs)
     
    mitaywalle likes this.