dreamau

Hi guys,

We're trying to implement a character system using Unity and Spine 4beta. The plan is to have an undressed body only, and lots of outfits for it (100s).

The simplest way would be to of course to create all outfits as skins for the body, but doing it the naive way would have us load all geometry / textures into memory, both of which would not be small. I went over similar posts in the forum, but would like to go over some ideas I have in order to help me avoid time trying to implement dead ends.

1) The first idea I had was to export the body as well as each outfit as a separate character (that means lots of assets of type SkeletonDataAsset). I tried quick prototype, then load body and an outfit to Unity and create a new skin for the body, but the outfit does not show. Is there some requirement for Skin to be only created from the same SkeletonDataAsset? I did something like this:
public SkeletonDataAsset Body;
public SkeletonDataAsset Outfit;

SkeletonAnimation _bodySkeletonAnimation = SkeletonAnimation.NewSkeletonAnimationGameObject(Body);
var customSkin = new Skin("custom-skin");
var outfitSkeletonData = Outfit.GetSkeletonData(true);
var outfitSkin = outfitSkeletonData.DefaultSkin;

// add whole skin
customSkin.AddSkin(outfitSkin);

// or add individual attachments (I tried both options)
foreach (var attachement in outfitSkin.Attachments)
{
customSkin.SetAttachment(attachement.SlotIndex, attachement.Name, attachement.Attachment);
}

_bodySkeletonAnimation.Skeleton.SetSkin(customSkin);
_bodySkeletonAnimation.Skeleton.SetSlotsToSetupPose();
Note that skeletons in both files match (completely or mostly), so I was assuming at least some outfit parts would show.

Should this work or not? Alternatively, is there a way to make it work? Can I at runtime clone skin parts from the outfit to the body and create new skin after that or anything similar?

2) My alternative solution is more along the lines mentioned on the forum here. Create a single character, and create all outfits as skins. Have textures in folders, so that separate atlas is created for each outfit. Then before the build / bundle creation, I thought I'd replace the link to the texture by either null or some dummy 4x4 pixels texture. This way when I load SkeletonDataAsset, large textures won't load. Then I'll have to implement custom atlas loader, load the atlas texture and connect it to material .. is there some example on how to do this part please?
The disadvantage here is that I need to load whole SkeletonDataAsset, which contains the meshes and some other data for all outfits, and I suspect this file will not be tiny. Can this be stores as binary to avoid parsing cost at least perhaps?

I would prefer option 1 if possible, but option 2 is likely acceptable as well. Any other good option here?

Thanks so much!
dreamau
  • Posts: 9

Harald

dreamau wrote:Note that skeletons in both files match (completely or mostly), so I was assuming at least some outfit parts would show.
If both files contain the same Slots, it is expected to work. [Edit: Attachments don't need to exist in both, these can only exist in the skin's skeleton data]

I just tested this locally with two exports of the Goblins project (one with all skins deleted), using your script with the adaptation of
var outfitSkin = outfitSkeletonData.FindSkin("goblingirl");
instead of
var outfitSkin = outfitSkeletonData.DefaultSkin;
It successfully displayed the goblingirl skin for me.

What kind of error message or exception do you get? Are you sure that your Slots and Attachments match?
Could you perhaps send us the problematic exported assets (or a minimal Unity project)? You can send it as a zip file to contact@esotericsoftware.com

One more solution (a bit more complex however) would also be to programmatically concatenate json files before loading, or appending content of your Skins skeletonData after loading. This is briefly mentioned here on this forum thread:
多个spine共享一套骨骼动作

Regarding your second solution:
FYI: We have an issue ticket that deals with implementing delayed (partial) loading of atlas textures:
https://github.com/EsotericSoftware/spine-runtimes/issues/1890
The disadvantage here is that I need to load whole SkeletonDataAsset, which contains the meshes and some other data for all outfits, and I suspect this file will not be tiny. Can this be stores as binary to avoid parsing cost at least perhaps?
The skeleton .json or binary .skel.bytes file size should not be the problem usually. You can export it as binary:
Export - Spine User Guide: Data
Removing or replacing texture references from an atlas and loading the textures on-demand would be a viable solution. It basically would require to creating a wrapper around SetSkin calls that ensures to load/unload textures and hook up the references as you described. It should not be necessary to write your own AttachmentLoader, replacing texture references depending on the next skin to be set should be easier to implement and sufficient for many use cases.
User avatar
Harald

Harri
  • Posts: 4194

dreamau

Hi Harald,

Thanks for getting back to me, much appreciated. I tried the case you mentioned, and indeed that works.I suspect the issue with my data file perhaps comes from slots not matching perfectly.

As an example, the body can have these in this order:
"slots": [
{ "name": "footR", "bone": "P_FootR", "attachment": "Body/footR" },
{ "name": "shinR", "bone": "P_KneeR", "attachment": "Body/shinR" },
... some more here
]

and the outfit would have just two:
"slots": [
{ "name": "pantLegR", "bone": "P_hips", "attachment": "Outfit/pantLegR" },
{ "name": "footR", "bone": "P_FootR", "attachment": "Outfit/footL" },
]
Two observations:
1. the slots are not in the same order .. and so slot index does not match. This somehow seems important, as even this function uses slot index:
customSkin.SetAttachment(attachement.SlotIndex, attachement.Name, attachement.Attachment);

2. the names of attachments don't match .. but I assume those don't have to?

So is my issue the fact that the slot names are not in the same positions in the slots arrays? I've noticed you have the SkinKey consisting of slot index and a name (slot name I assume, or is it attachment name? This is unclear to me).

In the ideal case I would have the body containing lots of slots. And each outfit would contain a subset of these slots, in any order. Would this not be all needed to match them up? How strict does the number of slots and their order need to be for this to work?

Thanks again.
dreamau
  • Posts: 9

Harald

dreamau wrote:1. the slots are not in the same order .. and so slot index does not match. This somehow seems important, as even this function uses slot index:
Different slot order is a problem then, since the skin's slotIndex is used to assign the attachment at the base skeleton's slot at this index.


What you can do is find the Slot of the same name in the body skeleton's skeletonData as shown below:
(this is using the Spine 3.8 API, if you are using 4.0-beta you need to slightly adjust the lines accessing the skinEntry in outfitSkin.Attachments, as you posted in your original code in the first posting of this thread)
// instead of customSkin.AddSkin(outfitSkin);

var bodySkeletonData = Body.GetSkeletonData(true);
foreach (var pair in outfitSkin.Attachments) {
var skinEntry = pair.Key;
string slotName = outfitSkeletonData.Slots.Items[skinEntry.SlotIndex].Name;
int bodySlotIndex = bodySkeletonData.FindSlotIndex(slotName);
customSkin.SetAttachment(bodySlotIndex, skinEntry.Name, skinEntry.Attachment);
}
dreamau wrote:2. the names of attachments don't match .. but I assume those don't have to?
Sorry, this was actually a wrong statement, the attachments don't need to match or even exist in the body skeleton data part, only the slots need to. I'll edit my original posting to mention this as well.
User avatar
Harald

Harri
  • Posts: 4194

dreamau

Hi Harald,

That makes perfect sense, but the create skin is still not showing. I emailed you the small repro project to the email you've given me.
For reference and benefit of others, here's the code I've tried:
public class Root : MonoBehaviour
{
public SkeletonDataAsset Body;
SkeletonAnimation _bodySkeletonAnimation;

public SkeletonDataAsset Outfit;

void Start()
{
_bodySkeletonAnimation = SkeletonAnimation.NewSkeletonAnimationGameObject(Body);
var bodySkeletonData = Body.GetSkeletonData(true);

var outfitSkeletonData = Outfit.GetSkeletonData(true);
var outfitSkin = outfitSkeletonData.DefaultSkin;

var customSkin = new Skin("custom-skin");

foreach (var attachement in outfitSkin.Attachments)
{
var slotIndex = attachement.SlotIndex;
var slotName = outfitSkeletonData.Slots.Items[slotIndex].Name;
int bodySlotIndex = bodySkeletonData.FindSlotIndex(slotName);

if (bodySlotIndex >= 0)
{
Debug.Log("attaching " + attachement.Name + " to slot index " + bodySlotIndex + ", slotName: " + slotName + ", outfit slot index: " + slotIndex);
customSkin.SetAttachment(bodySlotIndex, slotName, attachement.Attachment);
}
}

_bodySkeletonAnimation.Skeleton.SetSkin(customSkin);
_bodySkeletonAnimation.Skeleton.SetSlotsToSetupPose();

_bodySkeletonAnimation.AnimationState.Apply(_bodySkeletonAnimation.Skeleton);
}
}
dreamau
  • Posts: 9

Harald

Sorry to hear that it's not working as desired yet. Thanks for sending the reproduction project, we received everything. We will have a look at it and will get back to you once we've figured out what's going wrong.

---

The problem with your project seems that there are missing slots in your body skeleton. You are trying to attach e.g. JuliaArmor/belt to slot named belt, but this slot does not exist in your body skeleton. Same goes for all other slots that fail to be attached (with a returned slotIndex or -1 at FindSlotIndex()).

Are you sure that these slots existed in the original body Spine project? If so, could you please check your export settings or share some screenshots of the export settings you used?
User avatar
Harald

Harri
  • Posts: 4194

dreamau

Hi Harald,

Thanks for this. I agree some slots are missing, and it's expected those attachment do not get attached.
But why are the attachments for existing matching slots not getting attached. I log out the matching slots that exist and I try to attach to it, but it does not render. Example of the log:
attaching JuliaArmor/footL to slot index 0, slotName: footR, outfit slot index: 1
Here is the slot on the body (from Julia_bdy_noX.json) - it's currently at index 0:
"slots": [
{ "name": "footR", "bone": "P_FootR", "attachment": "JuliaNude/footR" },
....
Here is the same slot on the outfit (Julia_Armor_noX.json) - slot as index 1 (that is ok as you mentioned), attached to the bone of the same name P_FootR (but this likely does not matter)
"slots": [
{ "name": "pantLegR", "bone": "P_hips", "attachment": "JuliaArmor/pantLegR" },
{ "name": "footR", "bone": "P_FootR", "attachment": "JuliaArmor/footL" },
and here is the attachement on the outfit, which it's referencing.
"footL": {
"JuliaArmor/footL": {
"type": "mesh",
I would expect some attaachements from the outfit to show up on the body (log has about 10 valid matches), but none do .. The scene only has a single draw call (using body texture), and no draw call using outfit texture.
dreamau
  • Posts: 9

Harald

Oh, I see. I have been misled by your project setup of the missing attachments, and I incorrectly assumed that e.g. JuliaArmor/footL was intended to show the naked foot attachment, but it is instead intended to show an armoured boot. I will have another look at your project and will get back to you when we've figured it out.

---

The problem turned out to be that all your attachments are successfully set, but they are just not shown in setup pose. E.g. you have Slot forearmL and in the setup pose you have attachment JuliaNude/forearmL active. Now you add the attachment JuliaArmor/forearmL at the same slot, but never switch to this attachment at the slot, keeping the setup pose attachment JuliaNude/forearmL active of the two attachments.

A nice tool for testing such issues is opening the Skeleton Debug window (SkeletonAnimation Inspector - Advanced - Debug) and inspect all slots.

---

The difference to the Goblins project, and why it works out-of-the-box there, is that in the Goblins project, a skin placeholder is used at each slot.
E.g. in the goblins base skeleton.json:
"slots": [
{ "name": "left-shoulder", "bone": "left-shoulder", "attachment": "left-shoulder" },
and in the skin goblins.json file this skin placeholder is set:
"slots": [
{ "name": "left-shoulder", "bone": "left-shoulder", "attachment": "left-shoulder" },
"skins":
..
"left-shoulder": {
"left-shoulder": {
"name": "goblin/left-shoulder",
In contrast, in your projects there seems to be no skin placeholder name used, but instead directly the default attachment name is referenced:
"slots": [
{ "name": "footR", "bone": "P_FootR", "attachment": "JuliaNude/footR" },
while in the skin .json file it is referencing JuliaArmor/footL:
"slots": [
{ "name": "footR", "bone": "P_FootR", "attachment": "JuliaArmor/footL" },
...
"skins":
..
"footR": {
"JuliaArmor/footL"
This will then not be replaced by the SetSkin() call, but will instead be added as a second attachment.

So please check whether you are using skin placeholders with the same name in both projects, then it will work without any additional code required.
User avatar
Harald

Harri
  • Posts: 4194

dreamau

Thanks for all the help so far. I got the prototype to an acceptable state, and now I'm exploring the other option, where in the prefabs I strip out the textures (and replace them by 1 pixel white textures). This bit works, and when I load the character at runtime, all is white as expected and no large textures are loaded.

I need to load those original large textures or materials with them at runtime on demand, when I need to use a specific skin. And I'm not sure where to find the materials / textures that are used at runtime, so that I can replace them / swap textures on them. I see that SpineAtlasAsset class stores an array of materials .. but how do I get to its instance at runtime, from the SkeletonData / SkeletonDataAsset?
dreamau
  • Posts: 9

Harald

dreamau wrote:but how do I get to its instance at runtime, from the SkeletonData / SkeletonDataAsset?
skeletonAnimation.SkeletonDataAsset.atlasAssets holds the array of atlas assets of the base-class type AtlasAssetBase. You can iterate over this array and cast each element to SpineAtlasAsset to access its content.
User avatar
Harald

Harri
  • Posts: 4194

dreamau

Hi .. Thanks for that, yes I've seen those, and got this working. So I can at startup load all textures and set them up, and the character renders correctly.

But I am trying to do this on demand. Somewhere later I call:

Skin srcSkin = bodySkeletonData.FindSkin(name);
and before this I need to load the textures for this skin. So from the skin name I need to go to a specific material or materials inside the SpineAtlasAsset.
I looked at how FindSkin works .. and followed it to Skin class, and from there to SkinEntry and Attachment classes, and from there to MeshAttachment and RegionAttachment, but don't see a way to find out which material I need to load.
dreamau
  • Posts: 9

Harald

Ok, thanks for the additional info, now I understand what you are trying to achieve. We have an issue ticket that shall implement on-demand loading and unloading of material and texture assets according to active skins:
https://github.com/EsotericSoftware/spine-runtimes/issues/1890

Unfortunately a fully automatic solution is not trivial enough to just describe a quick implementation in a few lines. If you decide to implement this feature yourself and not wait for the official implementation, I would recommend adding one layer of indirection (e.g. a new SkinLoader component class) around setting and loading any skin. As a preparation you would then remove all but the default skin's assets from any SkeletonDataAsset reference cascades. Then you would set your skin at skinLoader.SetSkin(targetSkeletonAnimation, "skinname") which first checks if it's loaded, loads it on demand and hooks up any references, and then calls targetSkeletonAnimation.Skeleton.SetSkin(skinname). This way it should be doable without having to modify the spine-unity code.
User avatar
Harald

Harri
  • Posts: 4194

dreamau

And I got it to work .. loading textures on demand when adding skin, and all seems to work. Thanks for your help.
At some point when your solution is ready, we'll probably considering switching to it .. but for now, this works well. What is the ETA for the feature? (within couple of months, or longer?)
dreamau
  • Posts: 9

Harald

Very glad to hear you've got a working implementation already! :cooldoge:
dreamau wrote:within couple of months, or longer
Exactly that ;).
User avatar
Harald

Harri
  • Posts: 4194


Return to Unity