pathfinder

Hey everyone,
We created for a game a customizable character, that is divided on different sections to change how it looks (hair, eyes, mouth, etc).

To do this we:
  • Setup placeholders for the attachments
  • Created skins for each category (as seen on the image)
  • Then exported via image folder

The result of this is a folder with the atlas.txt / skeleton.json and finally 1 png per skin.
Then on Unity, when importing we did some quick tests, created skins at runtime based on the documentation and we have a simple system that can swap each part individually.

This works as intended here, and our next step is adding on top of this addressables. Our goal is to support a large number of assets per section (thats why we divided everything into 1 png).

My approach here would be having a "default" addressable group that should hold a initial configuration for the character and then setup different groups that have specific skin sets.
When opening the game then we would have a default character and download/update the other skin sets from a CDN.

The big issue we have right now is that while everything is divided on 1 png/material per skin, we have only 1 skeleton atlas asset that has an array of materials that representates all our skins.
Removing this via inspector generates errors on the console, stating that there are missing materials.

My intention here would be only loading to the atlas the materials I need. I manually tested this with the default skins we have and it works on the editor but it keeps throwing these missing materials errors.

I believe that this should be handled dynamically as we have been preping the assets but im not sure how we can avoid these errors (or if there is a better way to do so).

I checked the forums and found one or two similar questions but no clear solution, thanks for the help!
添付ファイルを見るにはパーミッションが必要です
pathfinder
  • 記事: 4

Harald

We have an issue ticket that deals with implementing delayed (partial) loading of atlas textures:
https://github.com/EsotericSoftware/spine-runtimes/issues/1890
Unfortunately we didn't get to this ticket yet.

You have multiple options to tackle delayed loading and assignment of atlas assets or materials:
  1. You could continue your workflow and disable the error by modifying the spine-unity code. Then you would need to download and assign the materials on demand.
  2. You could unassign any Material references later via a custom build script (only triggered on building your executable) which unlinks references and sets up any adressable links accordingly.
  3. You could also change your workflow from 1 atlas asset with N materials (each downloaded on demand) to N atlas assets (each downloaded on demand) with 1 material each. Then you should not need to modify any code regarding errors upon import.

While it addresses a slightly different goal (having multiple sparse SkeletonDataAssets and assigning a skin from one on-demand-loaded skeleton to another skeleton which is loaded from the start), this forum posting might be interesting for you in case you haven't seen it already:
Memory management of character with many outfits
アバター
Harald

Harri
  • 記事: 3353

pathfinder

Thanks for the help Harald.

Regarding #1, yes I saw the class code and I thought of disabling it, I wanted to be sure that this could be the best way to handle this first.

So with this option it would be valid to leave the mat array empty and modify it on demand as needed? I did a quick test on this and it worked.

--------

If I understood #3 right, instead of doing a single atlas for the whole character you mean doing one per skin (folders?). I also tried this by exporting first the skeleton without packing textures.

The result was a working bone-only character (after skipping fixing atlases), then after searching a bit I found that the only way to pack a specific folder (say bodyskin_0) was by using the sprite packer. This created one atlas/json per skin but I could not find a proper way to add it to the skeleton to render it.

The error we are getting with this approach is that the base skeleton still knows about all skins and continues to mention that there are missing regions. I found that the export cut all bone paths, so I manually created the folder structure to try the packer export (this took time, maybe there is a better way) and the paths were as on the base skeleton.
If up to this point this is fine, what would be the required steps to add the atlases at runtime?

I see that I can get to the SkeletonAnimation.SkeletonDataAsset (the scriptable object) and I could add atlases to its AtlasAssetBase (looks configured on the inspector on runtime) but no render. This also modifies the original asset, so I'm seeing if it could be better to create the Atlas on runtime / clear the asset before ending.

------

Besides this, a little question about exports. Since we are using a folder structure each export png is named after the skeleton (avatar_avatar_1, avatar_avatar_2), for organization purposes, how could we map the names to the folders used for the pngs? I saw that with a post build script could be possible, but I could not find some examples to base on.

Thanks!
pathfinder
  • 記事: 4

Harald

pathfinder さんが書きました:So with this option it would be valid to leave the mat array empty and modify it on demand as needed? I did a quick test on this and it worked.
Yes, it is valid to assign Textures or Materials later. The atlas description file (the .atlas.txt file) holds the names of all pages (of type AtlasPage), which are setup separately from loading Textures and Materials. In spine-unity it is the MaterialTextureLoader.Load() method overload which will link each AtlasPage to a Material, with the Material referencing a single Texture.

The Material is forwarded from page.rendererObject to a MeshGenerator.SubmeshInstruction (approximately a draw call) here. So you just need to make sure the Material is assigned at the AtlasPage before rendering the Skeleton, and that the Material has a Texture assigned.

Note that you could also swap the Material or Texture out with a tiny white placeholder Texture or placeholder Material, which will get rid of any log error messages as well. This way you could avoid modifying the spine-unity code and still remove the error log output.
pathfinder さんが書きました:The error we are getting with this approach is that the base skeleton still knows about all skins and continues to mention that there are missing regions.
..
If up to this point this is fine, what would be the required steps to add the atlases at runtime?
I think I did not share enough of what I had in mind, sorry about that. The assumption here was to have identical atlas attachment names like "customskin/arm" in all downloadable skin atlas assets, and not know every possible skin in advance. This way you could create and deploy a single skeleton once, and then later add downloadable skins which replace the dummy customskin skeleton skin accordingly. This avoids having to re-export the base skeleton when adding skins later. If you always have all your attachments at hand anyway, and would like to generate all atlas textures at the same time as the skeleton, this would not make much sense. Then replacing Materials or Textures at Materials is an easier solution.
pathfinder さんが書きました:Besides this, a little question about exports. Since we are using a folder structure each export png is named after the skeleton (avatar_avatar_1, avatar_avatar_2), for organization purposes, how could we map the names to the folders used for the pngs? I saw that with a post build script could be possible, but I could not find some examples to base on.
If you run the texture packer either via the Spine - Texture Packer menu or via the command line interface (instead of packing textures alongside skeleton export), you can specify the texture name. When using the Spine Command Line Interface, we use this line to specify the atlas and texture name (note that there are multiple exports combined in a single call which starts here).

So you could use a bash script to automatically setup directories and copy your images following the desired directory structure. E.g. for attachment names like "myskin/arm" you would below a top export directory "export" have a directory "myskin" where you place the attachment image "arm". Then you would run the texture packer via the command line interface on the "export" directory. Then you can continue with setup for the next atlas, export it via the CLI, and so on.
アバター
Harald

Harri
  • 記事: 3353

pathfinder

Thanks again for the help! Here is a little update.
Harald さんが書きました:
Yes, it is valid to assign Textures or Materials later. The atlas description file (the .atlas.txt file) holds the names of all pages, which are setup separately from loading Textures and Materials. In spine-unity it is the MaterialTextureLoader.Load() method overload which will link each AtlasPage to a Material, with the Material referencing a single Texture.

The Material is forwarded from page.rendererObject to a MeshGenerator.SubmeshInstruction (approximately a draw call) here. So you just need to make sure the Material is assigned at the AtlasPage before rendering the Skeleton, and that the Material has a Texture assigned.

Im investigating this approach since it should be easier to implement from what I understand.
I have been testing the barebones needed to support changing this on runtime and I have it partially working.
skeletonAnim.SkeletonDataAsset.atlasAssets = new AtlasAssetBase[1];
skeletonAnim.SkeletonDataAsset.atlasAssets[0] = atlas;
skeletonAnim.SkeletonDataAsset.Clear();
skeletonAnim.Initialize(true);
After doing this, im able to create a skin, set it and the slots and the skin renders.

The issue that im still having is that the atlas that im assigning still has all the materials included.
I tried accessing to this for modification but looks like the AtlasAssetBase.Materials is only readable. I could not find any way to modify at runtime this so far.

I see that there is a static method to create at runtime a SkeletonDataAsset, where I need to provide the mats + the .txt. The issue I see here is that I would need to recreate each time I change any skin (because the intention is to provide only the needed materials, by changing the skin I would need others).

Could you provide some more info on how to go to continue from here? The best solution would be of course just changing the materials.

I also saw in another post that after doing that I should call skeletonDataAsset.Clear() and then Init the skeletonAnimator.

---

I have been testing since my previous answer, here is some more info.

  • 1. The skeletonJSON that comes from the export contains all the data, including the skins info.
  • 2. The skeletonData needs to have all the regions from skins referenced (via atlases). (please confirm)
  • 3. I tried exporting manually each skin, getting a more modular organization (1 atlas per skin)
  • 4. Doing a material override doesnt work on this case because each skins possibility also change positions (not just a recolor).

So from this setup I think it would be a good idea to keep 1 atlas per skin, but the main issue still persists.

With our current mix and match, we have 5 customizable sections of our character. Each section has different skins (mouth_0, _1 _2 / costume_0 , _!, _2 / etc).

The skeletonData requires to have all regions covered, meaning that I need to have the atlas array containing all possibilities per each section at the same time (instead of only 1 per section, as it should be).

So, if the blocker is that the skeletonData expects to have all the skins, the only viable solution I see is changing the json and creating a skeletonData at runtime with one containing only the skins I need.

It would be something like this:
  • 1. Export from Spine and manually remove the skins on the json (leave the skin[] empty)
  • 2. Pack and export 1 atlas per skin, an additionally add a json that would be the skin entry (that I previously removed)
  • 3. Get the json with the empty skins, concat each skin I need on the empty array
  • 4. Create the skeletonData at runtime providing the modified json + array of atlases that should cover whats only needed.

This however brings the following questions:
- While this solves the modular / addressable issue, is it viable in terms of performance? Each user input where I need to change any of the skin sections means redoing the steps above.
- This is also going to reset everything (animations, etc)
- I suppose that I can preload some atlases in groups to avoid reloading each time, but still sounds like an issue.


What would be the best approach to take from here?
Is it possible to make a skeletonData work without loading each of the regions (like on the pic)?

Thanks again.
添付ファイルを見るにはパーミッションが必要です
pathfinder
  • 記事: 4

Harald

pathfinder さんが書きました:Im investigating this approach since it should be easier to implement from what I understand.
I have been testing the barebones needed to support changing this on runtime and I have it partially working.
skeletonAnim.SkeletonDataAsset.atlasAssets = new AtlasAssetBase[1];
skeletonAnim.SkeletonDataAsset.atlasAssets[0] = atlas;
skeletonAnim.SkeletonDataAsset.Clear();
skeletonAnim.Initialize(true);
After doing this, im able to create a skin, set it and the slots and the skin renders.
I don't quite understand why you are creating an atlas asset via code here. The use case would rather be to assign an already prepared existing SpineAtlasAsset.

Apart from that you should be creating an instance of SpineAtlasAsset instead of the common base class AtlasAssetBase. Also, depending on what you are intending (editor scripting?), creating an instance of a ScriptableObject subclass should be done via ScriptableObject.CreateInstance instead of via new.
I tried accessing to this for modification but looks like the AtlasAssetBase.Materials is only readable. I could not find any way to modify at runtime this so far.
See above, you should be using SpineAtlasAsset instead of AtlasAssetBase.

If in doubt, you can always have a look at what the spine-unity source code does in the normal import scenario:
https://github.com/EsotericSoftware/spine-runtimes/blob/4.0/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Utility/AssetUtility.cs#L516
I see that there is a static method to create at runtime a SkeletonDataAsset, where I need to provide the mats + the .txt. The issue I see here is that I would need to recreate each time I change any skin (because the intention is to provide only the needed materials, by changing the skin I would need others).

Could you provide some more info on how to go to continue from here?
In general I would not recommend creating an atlas asset programmatically at runtime. You should instead prepare atlas assets ahead of time and then just assign the asset reference.
The best solution would be of course just changing the materials.
Yes, this would be best, if possible.
I also saw in another post that after doing that I should call skeletonDataAsset.Clear() and then Init the skeletonAnimator.
Basically yes, but note that calling skeletonAnimation.Initialize(true) (override == true here) will reload the complete skeleton, which might be tolerable, but costs the full loading time. Only switching Material references is free in comparison.
  1. 1. The skeletonJSON that comes from the export contains all the data, including the skins info.
  2. 2. The skeletonData needs to have all the regions from skins referenced (via atlases). (please confirm)
All references of active attachments need to be setup so that from each RegionAttachment or MeshAttachment it links to the respective atlas array's AtlasRegion, and has its uv coords setup accordingly.

You can have a look at the NewRegionAttachment and NewMeshAttachment code here, which sets up all references needed for both attachment types:
https://github.com/EsotericSoftware/spine-runtimes/blob/4.0/spine-csharp/src/Attachments/AtlasAttachmentLoader.cs#L47
https://github.com/EsotericSoftware/spine-runtimes/blob/4.0/spine-csharp/src/Attachments/AtlasAttachmentLoader.cs#L62
(attachment.RendererObject = region; assigns the reference from skeleton to the atlas here, together with setting up all uv and offset parameters)

You can however create a Skin separately which is not yet assigned at any skeleton. At the time you set this skin as skeletonAnimation.Skeleton.Skin = customSkin, all RegionAttachment and MeshAttachment region references and uv coords need to be setup accordingly beforehand.
  • 3. I tried exporting manually each skin, getting a more modular organization (1 atlas per skin)
  • 4. Doing a material override doesnt work on this case because each skins possibility also change positions (not just a recolor).

    So from this setup I think it would be a good idea to keep 1 atlas per skin, but the main issue still persists.
  • Now I see why you don't want to switch out Materials only. Basically the regions should match up when whitespace stripping is disabled upon atlas export. When whitespace stripping is enabled, differently transparent regions of the same Attachment in two skins will end up being placed at different locations in the respective output atlas, that is correct. This only applies to RegionAttachments, MeshAttachments always use the mesh boundaries for packing, transparent areas within the mesh cannot be cut-off and used by other attachments aparently.
    So, if the blocker is that the skeletonData expects to have all the skins, the only viable solution I see is changing the json and creating a skeletonData at runtime with one containing only the skins I need.

    It would be something like this:

    1. Export from Spine and manually remove the skins on the json (leave the skin[] empty)

    2. Pack and export 1 atlas per skin, an additionally add a json that would be the skin entry (that I previously removed)

    3. Get the json with the empty skins, concat each skin I need on the empty array

    4. Create the skeletonData at runtime providing the modified json + array of atlases that should cover whats only needed.
    This is not recommended. Instead it would be better to just assign pre-built atlas assets at the skeletonData's atlas assets array. You could then either perform a full reload of the SkeletonData via skeletonAnimation.Initialize(true), or perform only reloading of the attachments. The latter would however involve some coding I'm afraid.
    アバター
    Harald

    Harri
    • 記事: 3353

    pathfinder

    Okay so I finally got it working (I edited and cleared my original comment).
    What I'm doing still feels wrong.

    Here are the steps:
    • I created a 1px white tex that I manually assigned to each of the atlases materials.
    • All atlases are loaded on the empty skeleton that I exported before without packing. Everything is rendered white (as expected).
    • To avoid getting an error that didnt recognize the white tex for the atlas, I commented the following on MaterialsTextureLoader:
    //if (other.mainTexture.name == name) {
    material = other;
    //break;
    //}
    This is because the atlas needs a tex with the exact same name, and this would require doing 1px tex per each skin.
    Instead I removed that to reuse the same along all skins.

    • Then on startup, I have as a temporary solution an array of textures (the real ones) paired with another array of strings (the names).
    • I loop the atlasAssets checking if I find the name im iterating. If I do I replace the primaryMaterial.mainTexture with the tex array paired index.
    • Finally I add the skins and apply them and they appear.

    Additional comments:
    • When OnAplicationQuit is called, I reset each mat to the original 1px tex (since its an asset).
    • I tested doing a live change (configured body_1 and replaced body_0) and it worked without reloading the skeleton. This should be because the regions are initialized properly and the only thing I replace is the tex.

    Its really dirty but I wanted to share the current steps to see if there was a way to improve this. Thanks for the help!
    pathfinder
    • 記事: 4

    Harald

    Sorry for the late reply!
    pathfinder さんが書きました:To avoid getting an error that didnt recognize the white tex for the atlas, I commented the following on MaterialsTextureLoader
    ...
    Its really dirty but I wanted to share the current steps to see if there was a way to improve this. Thanks for the help!
    Thanks for pointing that out. You are right, having to comment-out these lines does is not ideal. Unfortunately the other way, creating your own MaterialsTextureLoader subclass and using it in SpineAtlasAsset.GetAtlas() would also require to either change this line where the MaterialsTextureLoader is assigned, or to also create a subclass of SpineAtlasAsset, which however again would just move the necessary code modification to another place where the respective object instantiation happens.

    We will improve the API situation together with implementing the official support for delayed on-demand loading in the aforementioned issue ticket.
    pathfinder さんが書きました:Then on startup, I have as a temporary solution an array of textures (the real ones) paired with another array of strings (the names).

    I loop the atlasAssets checking if I find the name im iterating. If I do I replace the primaryMaterial.mainTexture with the tex array paired index.
    If I understood you right, your solution is completely legit. You either need to create a mapping of a unique texture ID (name, path, UUID) to the texture, or alternatively for each AtlasAsset create only an array of Texture references, e.g. stored at a scriptable object next to each AtlasAsset. This would work by relying on the order of textures (indices) remaining unchanged.

    If you're unhappy with iterating over pairs, it could be implemented via a Dictionary, but this again causes additional work in terms of Unity's serialization.
    アバター
    Harald

    Harri
    • 記事: 3353


    Return to Unity