Search Unity

How to change individual spritesheet in TextureImporter object without regenerating all spriteIDs

Discussion in '2D' started by chilon, Feb 18, 2019.

  1. chilon

    chilon

    Joined:
    Feb 10, 2019
    Posts:
    9
    I'm simply trying to edit the spritesheet elements within a TextureImporter's spritesheet field with an Editor Script.

    The following code doesn't work:

    Code (CSharp):
    1. var textureImporter = AssetImporter.GetAtPath(AssetDatabase.GUIDToAssetPath(someAssetGuid)) as TextureImporter;
    2. textureImporter.spritesheet[0].pivot = new Vector2(3, 4);
    3. EditorUtility.SetDirty(textureImporter);
    4. textureImporter.SaveAndReimport();
    The change to the spritesheet object is lost.

    If I change it to the following:

    Code (CSharp):
    1. var textureImporter = AssetImporter.GetAtPath(AssetDatabase.GUIDToAssetPath(someAssetGuid)) as TextureImporter;
    2. textureImporter.spritesheet[0].pivot = new Vector2(3, 4);
    3.  
    4. // this line is new:
    5. textureImporter.spritesheet = textureImporter.spritesheet;
    6.  
    7. EditorUtility.SetDirty(textureImporter);
    8. textureImporter.SaveAndReimport();
    Now the spritesheet changes are saved, but the `spriteID` for every single sprite within the sheet gets updated. Not sure why. I just want to be able to change the spritesheet elements that are different without regenerating the `spriteID` of every sprite within the sheet.

    I also see that the SpriteMetaData struct (`spritesheet` has type `SpriteMetaData[]`) is lacking properties for things like `physicsShape` so can those not be changed via editor scripts?

    Is there any way to do what I want?
     
    Last edited: Feb 18, 2019
    neonblitzer likes this.
  2. chilon

    chilon

    Joined:
    Feb 10, 2019
    Posts:
    9
    I don't suppose anybody saw this? I sort of ended up abandoning my game because I hit this deadend, especially when it came to being able to update the physics shape.

    I'd really like to pick it back up without having to switch to a different game engine. I have all my physics shapes defined in a yaml file already, having to use the editor to set each one manually will be such a huge waste of time.
     
  3. Lo-renzo

    Lo-renzo

    Joined:
    Apr 8, 2018
    Posts:
    1,511
    You could modify the texture's meta text file directly, editing the custom physics shape. This is just string manipulation. The hard part might be translating the physic shape's positions in your yaml file to whatever the Sprite Editor is expecting; these positions may be normalized or in pixel space or something annoying. Idk how they work.
     
  4. Ted_Wikman

    Ted_Wikman

    Unity Technologies

    Joined:
    Oct 7, 2019
    Posts:
    914
    Hello @chilon
    Sorry that we missed this question. This territory is unfortunately not very well documented. I'll bring this back to the team to see what we can do in future versions. While we wait for that, I took the time to write you a sample that can help you along towards your goal.

    [Written and tested in Unity 2020.3]

    Code (CSharp):
    1. using UnityEditor;
    2. using UnityEditor.U2D.Sprites;
    3. using UnityEngine;
    4.  
    5. public static class PivotUpdater
    6. {
    7.     [MenuItem("Custom/Update Sprite Pivots")]
    8.     static void UpdatePivot()
    9.     {
    10.         foreach (var obj in Selection.objects)
    11.         {
    12.             if (obj is Texture2D)
    13.             {
    14.                 var factory = new SpriteDataProviderFactories();
    15.                 factory.Init();
    16.                 var dataProvider = factory.GetSpriteEditorDataProviderFromObject(obj);
    17.                 dataProvider.InitSpriteEditorDataProvider();
    18.  
    19.                 SetPivot(dataProvider, new Vector2(3, 4));
    20.                 SetPhysicsShape(dataProvider);
    21.              
    22.                 dataProvider.Apply();
    23.              
    24.                 // Optional: If you want to auto save your changes
    25.                 AutoSaveChanges(dataProvider);
    26.             }
    27.         }
    28.     }
    29.  
    30.     static void SetPivot(ISpriteEditorDataProvider dataProvider, Vector2 pivot)
    31.     {
    32.         var spriteRects = dataProvider.GetSpriteRects();
    33.         foreach (var rect in spriteRects)
    34.         {
    35.             rect.pivot = pivot;
    36.             rect.alignment = SpriteAlignment.Custom;
    37.         }
    38.         dataProvider.SetSpriteRects(spriteRects);
    39.     }
    40.  
    41.     static void SetPhysicsShape(ISpriteEditorDataProvider dataProvider)
    42.     {
    43.         var physicsOutlineDataProvider = dataProvider.GetDataProvider<ISpritePhysicsOutlineDataProvider>();
    44.         var rects = dataProvider.GetSpriteRects();
    45.         foreach (var rect in rects)
    46.         {
    47.             var outlines = physicsOutlineDataProvider.GetOutlines(rect.spriteID);
    48.             // Do changes
    49.             physicsOutlineDataProvider.SetOutlines(rect.spriteID, outlines);
    50.         }
    51.     }
    52.  
    53.     static void AutoSaveChanges(ISpriteEditorDataProvider dataProvider)
    54.     {
    55.         var assetImporter = dataProvider.targetObject as AssetImporter;
    56.         assetImporter.SaveAndReimport();
    57.     }
    58. }
    So the main gist is that you need to work through the different data providers. These data providers will help you update the Sprite Rect data in a valid way so that it can be stored back into the .meta file with the same format.

    For physics, you need to use the
    ISpritePhysicsOutlineDataProvider
    , you can find a bit more info about it here. In this script reference link, you can also see more data providers that exist, to be used to modify other pieces of Sprite Rect data.

    Let me know how it goes!
     
    Last edited: Mar 25, 2022
  5. Lo-renzo

    Lo-renzo

    Joined:
    Apr 8, 2018
    Posts:
    1,511
    The SpriteEditor is definitely an area that should be added to your documentation backlog. Custom outlines, physics shapes, etc. Here's how I formed my understanding.

    It would be nice if it were a bit more extensible... for example, if it had an API that allows you to add a custom button, with access to the currently selected sprite if in a spritesheet.
     
  6. Ted_Wikman

    Ted_Wikman

    Unity Technologies

    Joined:
    Oct 7, 2019
    Posts:
    914
    Thank you for the feedback @Lo-renzo
    The UI and UI interactions in the Sprite Editor Window is a different system which makes use of these data providers from my example. Which part more exactly would benefit from increased extensibility? Do you have any examples of what you would like to do, which you cannot do today?
     
  7. Lo-renzo

    Lo-renzo

    Joined:
    Apr 8, 2018
    Posts:
    1,511
    I meant "Sprite Editor API" - that's where I agree with you that there's a need for more docs. Those providers were entirely unknown to me and, as evidenced by the linked thread, other Unity devs as well.

    I've wanted to edit/modify custom outlines. That seems possible with the providers you cited, but not on a fine tuned basis. For example, I currently would like a way to create for myself a custom Sprite Editor button for use with multiple mode spritesheets. It would resize the selected rect, add and adjust some custom outline nodes. I'm not aware of a way to get /set the selected sprite rect of the Sprite Editor window.
     
  8. Ted_Wikman

    Ted_Wikman

    Unity Technologies

    Joined:
    Oct 7, 2019
    Posts:
    914
    I see, thanks for the clarification. Yeah, so the SpriteEditorWindow is fully internal and currently does not allow any external functionality to be built on-top of it. The way you would be able to accomplish this task is to create your own editor tool with the data providers.
    It is good to know that you would like to see us adding ways to extend/build on top of the existing editor window. I will bring this feedback, and your previous feedback, to the team and we'll use it as a base for discussions on future improvements of the editor window.

    Let us know if you have any other ideas on what could be improved/what is missing.
     
    Lo-renzo likes this.
  9. ZenTeapot

    ZenTeapot

    Joined:
    Oct 19, 2014
    Posts:
    65
    If I were to use data provider, how do I change sprite names? There's not a name provider it seems?
     
  10. Ted_Wikman

    Ted_Wikman

    Unity Technologies

    Joined:
    Oct 7, 2019
    Posts:
    914
    @ZenTeapot I put together a small sample script to demonstrate how you would go about changing a Sprites name using the data providers. Note the additional step you need to perform in Unity 2021.2 and newer.

    Let me know if it answers your question.

    [Tested with Unity 2022.2]
    Code (CSharp):
    1. using UnityEditor;
    2. using UnityEditor.U2D.Sprites;
    3. using UnityEngine;
    4.  
    5. public class SpriteName : MonoBehaviour
    6. {
    7.     [MenuItem("Custom/Update Sprite Name")]
    8.     static void UpdateName()
    9.     {
    10.         foreach (var obj in Selection.objects)
    11.         {
    12.             if (obj is Texture2D)
    13.             {
    14.                 var factory = new SpriteDataProviderFactories();
    15.                 factory.Init();
    16.                 var dataProvider = factory.GetSpriteEditorDataProviderFromObject(obj);
    17.                 dataProvider.InitSpriteEditorDataProvider();
    18.  
    19.                 SetSpriteName(dataProvider);
    20.  
    21.                 dataProvider.Apply();
    22.                
    23.                 var assetImporter = dataProvider.targetObject as AssetImporter;
    24.                 assetImporter.SaveAndReimport();
    25.             }
    26.         }
    27.     }
    28.  
    29.     static void SetSpriteName(ISpriteEditorDataProvider dataProvider)
    30.     {
    31.         var spriteRects = dataProvider.GetSpriteRects();
    32.         for (var i = 0; i < spriteRects.Length; ++i)
    33.             spriteRects[i].name = GetRandomName();
    34.         dataProvider.SetSpriteRects(spriteRects);
    35.        
    36.         // Additional step for Unity 2021.2 and newer
    37.         var nameFileIdDataProvider = dataProvider.GetDataProvider<ISpriteNameFileIdDataProvider>();
    38.         var pairs = nameFileIdDataProvider.GetNameFileIdPairs();
    39.         foreach (var pair in pairs)
    40.         {
    41.             var spriteRect = System.Array.Find(spriteRects, x => x.spriteID == pair.GetFileGUID());
    42.             pair.name = spriteRect.name;
    43.         }
    44.        
    45.         nameFileIdDataProvider.SetNameFileIdPairs(pairs);
    46.         // End
    47.     }
    48.  
    49.     static string GetRandomName()
    50.     {
    51.         return GUID.Generate().ToString();
    52.     }
    53. }
    54.  
     
    Ronnie_0 and ZenTeapot like this.
  11. ZenTeapot

    ZenTeapot

    Joined:
    Oct 19, 2014
    Posts:
    65
    Thanks. Would be nice to have examples in the documentation. I wasn't aware SpriteRect is an all emcompassing SpriteMetadata replacement.
     
    neonblitzer likes this.
  12. Ted_Wikman

    Ted_Wikman

    Unity Technologies

    Joined:
    Oct 7, 2019
    Posts:
    914
  13. Midiphony-panda

    Midiphony-panda

    Joined:
    Feb 10, 2020
    Posts:
    243

    I used parts of your sample, without the additionnal step for Unity 2021.2 and newer.
    It seems to work nevertheless on Unity 2021.2.5f1.

    Could you give some clarification on what this step does, and why it is needed ? :)
     
  14. Ted_Wikman

    Ted_Wikman

    Unity Technologies

    Joined:
    Oct 7, 2019
    Posts:
    914
    Sure! It all comes down to retaining the reference to a Sprite in different consumers of Sprites (such as SpriteRenderer). When you are working in the Sprite Editor Window, you can do many different operations on a Sprite, and the reference to that Sprite tends to remain. Only tracking a Sprite by its name is not enough to keep the reference intact, we also need an ID to the Sprite as a backup, in case we cannot resolve the Sprite only by its name.

    In Unity 2021.2, we updated the way we track these Name + IDs, and introduced the
    ISpriteNameFileIdDataProvider
    interface. We did this to introduce more flexibility in how a Sprite reference could be resolved.

    The reason why I am adding this extra step in my examples is because without it, you run the chance of losing the reference to a Sprite when combining your custom Sprite edit tool with the Sprite Editor Window. If you are not relying on the Sprite Editor Window when authoring new Sprites, this step can be dropped, as you would be responsible for keeping track of the Sprites IDs between iterations.
     
    Midiphony-panda likes this.
  15. Ted_Wikman

    Ted_Wikman

    Unity Technologies

    Joined:
    Oct 7, 2019
    Posts:
    914
  16. Midiphony-panda

    Midiphony-panda

    Joined:
    Feb 10, 2020
    Posts:
    243
    All good (how to get the different providers, numerous welcomed examples), but I would add a section about renaming sprites :)
    With the doc you provided, I guess I wouldn't have found easily how to do it (whereas your above post was very clear!!).
     
    Ted_Wikman likes this.
  17. Ted_Wikman

    Ted_Wikman

    Unity Technologies

    Joined:
    Oct 7, 2019
    Posts:
    914
    Thanks for the feedback! Let me see if we can add a renaming sample as well.
     
    Midiphony-panda likes this.
  18. ZenTeapot

    ZenTeapot

    Joined:
    Oct 19, 2014
    Posts:
    65
    I have to comment that this is the only time I ask something that Unity dev gave a direct answer and then actually put the missing piece in. I wish all problems are treated this way.
     
    Midiphony-panda likes this.
  19. Ted_Wikman

    Ted_Wikman

    Unity Technologies

    Joined:
    Oct 7, 2019
    Posts:
    914
    Thank you for your kind words, @ZenTeapot. I'm happy we could help out, and be sure that we are trying our best to help our users, but sometimes it takes a bit of time before we are able to roll out the changes.

    Happy developing!
     
  20. ShervinM

    ShervinM

    Joined:
    Sep 16, 2017
    Posts:
    67
    Just following up on this thread...

    After doing some testing where I iterate over the sprite rects (using the provider), update their names, and call
    Code (CSharp):
    1. dataProvider.SetSpriteRects(spriteRects);
    to update them, it looks like the `NameFileIdPairs` in the meta file have their names also updated just fine.

    So it looks like the extra step to call
    Code (CSharp):
    1. SetNameFileIdPairs
    is no longer required as its being done under the hood? Is that correct @Ted_Wikman ?
     
  21. Ted_Wikman

    Ted_Wikman

    Unity Technologies

    Joined:
    Oct 7, 2019
    Posts:
    914
    In the TextureImporter's SetSpriteRects implementation, we do not write the data back to the Name/FileId map, so the data stored in the Sprite Rects vs. the data stored in the Name/FileId map will diverge if you skip the SetFileIdPairs-call. This means that if there is a contention for either the name or an ID, whatever is stored in the Name/FileId map will win over what's stored in a Sprite Rect, which in turn will most likely lead to a broken reference.
     
  22. ShervinM

    ShervinM

    Joined:
    Sep 16, 2017
    Posts:
    67
    I see, thank you for the confirmation! Not sure what was updating the pairs then, but I'll be sure to make the SetNameFileIdPairs call to be safe. Thanks @Ted_Wikman

    On that note... I've been working on a utility to copy over sprite rect data from one Texture2D to another (hence why I've been interested in this thread). In my first implementation, I was quite literally taking the SpriteRects and the FileID pairs from texture A, and using the dataproviders to set the rects and pairs on texture B.

    This means that texture A and B would have identical Name - FileId pairs in their metafiles. So my question is: is there any circumstance that could be problem down the road? Or otherwise is this considered bad practice?

    Obviously texture A and texture B have different GUIDs themselves, so if one object was making a reference to texture A's sprite 1, it would look different than if were referencing texture B's sprite 1 (even though both sprite 1's have the same FileId):

    fileID: <same sprite1 fileID>, guid: <texture A GUID>
    fileID: <same sprite1 fileID>, guid: <texture B GUID>

    I've since changed my code for this utility to create brand new SpriteRect objects that copy the corresponding SpriteRects from Texture A's data, but then they generate different spriteID and internalIDs (which I also make sure that the new corresponding FileIdPair has the same ID). Is this better practice? Or is it unnecessary since the two texture assets have different GUIDs anyways?
     
  23. Ted_Wikman

    Ted_Wikman

    Unity Technologies

    Joined:
    Oct 7, 2019
    Posts:
    914
    Sounds like a neat tool!

    The FileIds in the Name/FileId map are local to the texture and are not being used to compare across multiple textures, so copying this data over shouldn't cause any issues.

    Regarding the suggested implementation, I would go for a new ID for any copied data. The reason is that in the rare occasion that you need to debug this data, you have a smaller chance to have the same ID used for two Sprites in different textures, which makes for clearer debugging.
     
    ShervinM likes this.
  24. ShervinM

    ShervinM

    Joined:
    Sep 16, 2017
    Posts:
    67
    Ted_Wikman likes this.
  25. Ted_Wikman

    Ted_Wikman

    Unity Technologies

    Joined:
    Oct 7, 2019
    Posts:
    914
    Nice one, thanks for sharing your solution with the community!
     
  26. tonytopper

    tonytopper

    Joined:
    Jun 25, 2018
    Posts:
    226
    What kind of object do I need to pass to GetSpriteEditorDataProviderFromObject to be able to get a ISpriteNameFileIdDataProvider back from it?

    I tried passing a PSDImporter object, which gives me the ISpriteEditorDataProvider just fine, but then that data provider won't give me an ISpriteNameFileIdDataProvider.

    I am guessing maybe the PSDImporter needs to implement this or I am missing something about the nature of this API.
     
  27. Ted_Wikman

    Ted_Wikman

    Unity Technologies

    Joined:
    Oct 7, 2019
    Posts:
    914
    It is up to each importer how they would like to generate and store the ids of the individual objects. PsdImporter does not make use of ISpriteNameFileIdDataProvider, since its primary use is to import layers from Photoshop. These layers have a GUID (in most cases, depending on the DCC tool creating the .psd/.psb file) which is being used to identify a layer and keep its id intact even though the name may change outside of Unity.
     
    tonytopper likes this.
  28. tonytopper

    tonytopper

    Joined:
    Jun 25, 2018
    Posts:
    226
    That's useful to know, thanks. For the PSDImporter I ended up using "sub-assets" and the AssetDatabase API for now.
     
    Ted_Wikman likes this.