• RuntimesUnity
  • How to Export Skins in their Own Skeletons

First of all, what we want to achieve:
We have a shared Skeleton with multiple Skins.
In our game, we do not want to load all of those Skins and Textures at the same time: we want to load only what players will use.

Our current situation:
We export each skin 1 by 1, generating multiple SkeletonDataAssets.
This way skins are compatible with each other and we can even achieve Skin transition seamlessly.

There are some problems with this approach:

  • We must export each skin 1 by 1, manually enabling/disabling them in the editor.
  • When there are updates to the main skeleton, we must update ALL OF THEM. If we forget to update some, bugs will happen and they are hard to preemptively detect them. Updating all of the skin skeletons is a very error-prone task.

What we want:
We want to be able to automatic export multiple Skins in their separate files in a way that we can easily load them separately and add them to the main Skeleton.
I tried repacking all the skins from the Skeleton in the Unity Editor and exporting the generated material and texture but I can't save the SkeletonData since it's loaded from the Skeleton JSON.

If I could just create and save a SkeletonBinary from any given SkeletonData in the Unity Editor it would be a good solution.
Another good one is just a smart export that exports all Skins to self-packed files.

NOTE: I know about Addressables Spine module, but that is not very useful, since it ends up loading the original "full of skins" textures.

  • Harald がこの投稿に返信しました。

    To achieve exporting skins in their own skeletons while maintaining a shared skeleton, you can consider a few approaches. One method is to manually pack the attachments of each skin into separate texture atlases, ensuring only the necessary atlases are loaded at runtime. This can be done by creating a script that copies only the images for each skin to a separate folder and then packs that folder. At runtime, you can load the appropriate atlas for the skin being used. Another approach is to use the Spine CLI to automate the export process, potentially writing a tool that processes the JSON to separate skins and then converts them to binary format. This would allow you to maintain a single skeleton file while loading only the necessary images for each skin. Unfortunately, Spine does not natively support exporting each skin as a separate skeleton automatically, so some custom scripting will be necessary.

    The usual way to do this is to customize which textures are loaded. Our APIs are designed with that in mind.

    I've never seen loading all the skin data be a problem. A Spine skeleton is not intended to be broken into multiple skeletons or otherwise do a "partial export".

    leonardo.bilck When there are updates to the main skeleton, we must update ALL OF THEM. If we forget to update some, bugs will happen and they are hard to preemptively detect them. Updating all of the skin skeletons is a very error-prone task.

    In general it's always highly recommended to automate your exports using bash (or batch) scripts.
    This is even more true for more error-prone tasks. You can use the Spine Command Line Interface (CLI) or use our provided scripts to export all your skeletons in one go. Then another script would copy the assets into your respective Unity project directories.

    In general as Nate already mentioned, it's not a good idea to separately export skins as separate skeletons. Just export your skins to multiple atlas textures, and group them by folder structure if you need certain attachments on certain atlas page textures.

    leonardo.bilck NOTE: I know about Addressables Spine module, but that is not very useful, since it ends up loading the original "full of skins" textures.

    What do you mean by "full of skins" textures? Do you mean that you dislike how the attachments were grouped on the atlas page textures? If so, see packing by folder structure above.

    In general the Addressables module should be the perfect fit for your scenario, where skins end up on separate textures and these textures are only loaded when the skin is set active.

    • Nate がこの投稿に返信しました。

      Harald In general it's always highly recommended to automate your exports using bash (or batch) scripts.

      @Harald, Spine doesn't support partial export, so they can't export a skeleton per skin from the CLI. They could export to JSON and then write a script to hack that up, but I don't recommend that.

      • Harald が「いいね」しました。

      Thanks for the feedback!

      The solution to having all textures in separate folders still have the following problems:
      1) Shared attachments must be either duplicated per Skin (😢) or added to a separate folder. This will generate extra draw calls per skeleton, which will require multiple materials.

      2) A solution to item #1 is repacking the texture in runtime after loading, which does not work well for Addressables module (I tried it and it mess up the skin)

      I have a few questions about the Addressable module:
      1) If all skin attachments are contained in a single separate directory, will only that skin be loaded from the Addressable?
      2) What happens when we change skins? Will the unused skin be unloaded from memory?
      3) What happens if multiple characters sharing the same skeleton use different Skins?

      I will conduct more tests here and will get back to you.

      • Harald がこの投稿に返信しました。

        Answering #1:
        Based in my own tests using Unity's Memory Profiler, I can see that loaded Addressables are not unloaded.

        This should not be very hard to fix, but it is a problem that must be considered.

        It would also be very nice if Skin.GetRepackedSkin worked with Addressables module:

        • Non-Repacked Skin (build):

        • Repacked Skin (build):

        • Repacked Skin (Unity Editor):

        • Harald がこの投稿に返信しました。

          leonardo.bilck The solution to having all textures in separate folders still have the following problems:
          1) Shared attachments must be either duplicated per Skin (😢) or added to a separate folder. This will generate extra draw calls per skeleton, which will require multiple materials.

          That's unfortunately a general theoretical tradeoff.

          Your screenshot shows a very heavily alternating sequence of used atlas textures though. Please consider whether it's an option to rearrange your draw-order to have fewer switches between grouped shared attachments from one atlas page vs unique attachments from another.

          leonardo.bilck 2) A solution to item #1 is repacking the texture in runtime after loading,

          Yes, runtime repacking is the recommended solution if you can't re-arrange your draw order to have few atlas texture switches.

          leonardo.bilck which does not work well for Addressables module (I tried it and it mess up the skin)

          How did you perform runtime repacking with addressables?
          Could you please share some reproduction steps and code so that we can understand what's going wrong?
          Did you wait for the newly activated skin textures to be completely downloaded before repacking the skins? Coud you show the code how you are switching skins, waiting for the download and performing the repack operation?

          leonardo.bilck I have a few questions about the Addressable module:
          1) If all skin attachments are contained in a single separate directory, will only that skin be loaded from the Addressable?

          I'm not quite sure I understand what you mean by that. You're still responsible for assigning the high-resolution textures to whatever Addressable packages and groups that you think fits your scenario. The Addressables module only performs downloading and replacement of low-resolution placeholder textures at active materials with the high resolution texture versions. Whenever a low-res placeholder texture is found, the high-res version is downloaded.

          leonardo.bilck 2) What happens when we change skins? Will the unused skin be unloaded from memory?

          Note that it's never about skins, but about textures. The AddressableTextureLoader asset (suffix _Addressable) Inspector provides a property "Unload After Seconds Unused". Did you try setting this property according to your needs? If you don't want automatic unloading, you can trigger the unload earlier whenever you need.

          leonardo.bilck 3) What happens if multiple characters sharing the same skeleton use different Skins?

          It's about textures. The textures of active skins are loaded in their high-res version via Addressables.

          leonardo.bilck Answering #1:
          Based in my own tests using Unity's Memory Profiler, I can see that loaded Addressables are not unloaded.

          This should not be very hard to fix, but it is a problem that must be considered.

          See my posting above, did you check "Unload After Seconds Unused" or trigger an unload yourself?
          Of course it's by default not unloading currently unused textures immediately after you've switched to a different skin. Imagine switching between two weapon skins frequently, having to re-download the alternating skin's textures all the time would be terrible.

          leonardo.bilck It would also be very nice if Skin.GetRepackedSkin worked with Addressables module:

          Please see my questions in my previous posting above.

          I have posted the repository and a Windows build here.

          The main class for this POC is SpineSkinChangerScreen.

          I'm repacking on demand (check the button on the UI) through this method:

          private void RepackSkin()
              {
                  const string repackedSuffix = "-repacked";
                  string skinName = skeletonAnimation.skeleton.Skin.Name;
                  
                  if(skinName.EndsWith(repackedSuffix, StringComparison.Ordinal)) return;
                  
                  if(repackedSkinDictionaries.TryGetValue(skinName, out var existingSkin))
                  {
                      skeletonAnimation.Skeleton.Skin = existingSkin;
                      skeletonAnimation.Skeleton.SetSlotsToSetupPose();
                      skeletonAnimation.AnimationState.Apply(skeletonAnimation.Skeleton);
                      return;
                  }
                  
                  // https://esotericsoftware.com/spine-unity-main-components#Runtime-Repacking
                  
                  Skin skin = skeletonAnimation.skeleton.Skin;
                  
                  // Create a repacked skin.
                  Skin repackedSkin = skin.GetRepackedSkin($"{skinName}{repackedSuffix}", skeletonAnimation.SkeletonDataAsset.atlasAssets[0].PrimaryMaterial, out var runtimeMaterial, out var runtimeAtlas);
                  // Use the repacked skin.
                  skeletonAnimation.Skeleton.Skin = repackedSkin;
                  skeletonAnimation.Skeleton.SetSlotsToSetupPose();
                  skeletonAnimation.AnimationState.Apply(skeletonAnimation.Skeleton); // skeletonMecanim.Update() for SkeletonMecanim
          
                  // You can optionally clear the cache after multiple repack operations.
                  AtlasUtilities.ClearCache();
              }

          More info coming in the next post.

          I have profiled the memory from the build, follow along:

          1) Using the default skin, only the texture Aria_27 is loaded.

          2) Then I load another skin, which loads an additional texture: Aria_28. Please note that Aria_27 is used by most skins.

          3) I load another skin, which loads Aria_15 texture.

          4) I wait quite a while (I had the app opened while I write this) and the Aria_28 texture is still in memory.

          5) Bonus: I thought maybe in some way my repack of skin in item #2 (I did it to test repacking, it failed) might have hold a texture reference, but I changed Skin again, waited a few minutes and all textures are still in memory.

          @leonardo.bilck Thanks for sharing a reproduction project. We'll have a look at it and will get back to you as soon as we've figured out what's going wrong.

          Thanks, looking forward to your feedback!

          @leonardo.bilck The problems with your project setup were as follows:

          Addressables not unloading
          The Addressable Textures were not unloaded because you've added all Textures to the same Addressable group (i.e. the same asset bundle) and in the Addressable Groups settings of this group set Bundle Mode to Pack Together. Thus the asset bundle will only be unloaded once none of the group are used, which does not happen since you keep using Textures from the same group.

          So to fix this: Assign them to separete groups, or change Bundle Mode to Pack Separately or Pack Together by Label and assign the respective labels in the Addressable Groups panel. Once I've changed it to Pack Separately the profiler showed the desired outcome after switching forward and backward through your skins in the built executable:

          Please note that this is not specific to the spine-unity runtime but general Unity behaviour. Please make sure to be familiar with Unity Addressables before adding a layer of complexity by using the spine-unity runtime with it:
          https://docs.unity3d.com/Packages/com.unity.addressables@1.20/manual/AddressableAssetsDevelopmentCycle.html
          https://docs.unity3d.com/Packages/com.unity.addressables@1.20/manual/PackingGroupsAsBundles.html
          You might also find the following thirdparty writeup on asset unloading helpful:
          https://thegamedev.guru/unity-performance/memory-management-unloading/

          Repacking skins failing with white rectangles

          The problem's cause is Read/Write is disabled and can be found in the top of the Runtime Repacking documentation, in the box Important Note: If repacking fails or creates unexpected results, it is most likely due to any of the following causes".
          It's also listed in the FAQ with text "Why does my repacked skin display in the Editor but shows white polygons in the built executable?" and an image showing the same issue
          https://esotericsoftware.com/spine-unity-faq#Visual.
          Once I've enabled Read/Write and rebuilt Addressables, the issue was resolved.
          Please make sure to first check the FAQ and the general documenation when encountering any issues, likely you will find helpful information there.

          6日 後

          @Harald Thanks for your tips!

          It worked nicely in our project.
          I also noticed that the texture also gets unloaded when you repack a skin, which is quite nice (probably because the original Skin is not in use anymore, right?).

          One last question about memory optimization: to use compressed atlases for our Spine objects and to support repacking, is it enough to:

          • Change the target texture format when repacking
          • Ensure that all attachments follow the rules required by the target compression. For example: all with dimensions multiple of 4.

          Is that it?

          • Harald がこの投稿に返信しました。

            Very glad to hear you've got it worked out, thanks for letting us know!

            leonardo.bilck One last question about memory optimization: to use compressed atlases for our Spine objects and to support repacking, is it enough to:
            Change the target texture format when repacking
            Ensure that all attachments follow the rules required by the target compression. For example: all with dimensions multiple of 4.

            Is that it?

            An additional requirement is unfortunately that the attachment region would also need to be on multiples of 4 texels to be copyable. So your attachments each need to be of size multiple of 4, and each must be placed aligned at the 4x4 grid inside the atlas texture. That's due to the copy operation only being able to copy 4x4 grid cells as a whole from source to destination texture.

            I'm not 100% sure I got this:

            each must be placed aligned at the 4x4 grid inside the atlas texture.

            How can I guarantee that when exporting our Skeleton from Spine?

            5日 後

            @leonardo.bilck Sorry for the late reply. The alignment will automatically end up at the 4x4 grid if the attachment size is multiples of 4x4 and packing mode is not Polygons.

            So to let your attachments be located on the 4x4 grid, the following must be met:

            • All your attachments need to be of size multiple of 4
            • Strip whitespace x/y` must be disabled (so that the size is not reduced to anything other than 4x4)
            • Region Padding must be set to 0 or a multiple of 4.
            • Packing mode needs to be Rectangles (or you could also use Grid, but this would be quite wastful most of the time).