• Unity
  • Per-instance material

Yeah, we have this in a list of things to investigate for Spine-Unity.

Per attachment override is actually currently "easier", but you need to do some kinda-silly things so SkeletonRenderer has what it needs.
RegionAttachment, MeshAttachment and SkinnedMeshAttachment have a System.Object rendererObject field: https://github.com/EsotericSoftware/spine-runtimes/blob/master/spine-unity/Assets/spine-unity/SkeletonRenderer.cs#L229

That's supposed to be a generic reference in Spine-C# but in Spine-Unity, it's specifically a reference to a Spine.AtlasRegion. And it goes down slightly deep in references:

regionAttachment.rendererObject.page.rendererObject;

the first .rendererObject is the AtlasRegion. You need to cast it to get to its members.
.page is an AtlasPage.
the second .rendererObject is a UnityEngine.Material.
So concievably, if you could create a dummy AtlasRegion and AtlasPage, and do a deep-copy only of the data it needs, you can have each attachment have its own material, and SkeletonRenderer will treat it correctly as a material change. (I'm sure you're familiar with that system already)

You'll find this in practice in another form in SpriteAttacher.cs

//create faux-region to play nice with SkeletonRenderer
         atlasRegion = new AtlasRegion();
         AtlasPage page = new AtlasPage();
         page.rendererObject = mat;
         atlasRegion.page = page;

SpriteAttacher is actually an example of how you don't actually need to do a deep copy at all since none of the atlas data is used for actual rendering except for the material reference, since all the rendering-relevant data like UV offsets is already on the Attachment object itself.

For a per-skeleton override, I made that short script which basically just went down the list of sharedMaterials in MeshRenderer and replaced them as needed. I forgot the Unity magic method I used, but it was performed after LateUpdate but before actual rendering happened. I also can't find that script, but it's in the forums somewhere.

Related Discussions
...
5 días más tarde

UPD: updated script.

Pharan escribió

...

Thanks! Here's my script for changing materials, per-atlas, per-instance. It's ugly, but does the job.

using System;
using Spine;
using UnityEngine;

namespace Spine{
    [RequireComponent(typeof(SkeletonRenderer))]
    [RequireComponent(typeof(MeshRenderer))]
    [ExecuteInEditMode]
    public class SpineOverrideAtlasMaterials : MonoBehaviour {
        [SerializeField]
        private SkeletonRenderer _skeletonRenderer;

    [SerializeField]
    private bool _updateEachFrame;

    [SerializeField]
    private AtlasMaterialOverride[] _atlasOverrides;

    private AttachmentOverride[] _attachmentOverrides;

    private void OnWillRenderObject() {
        if (_skeletonRenderer == null || _skeletonRenderer.skeleton == null)
            return;

        Spine.ExposedList<Slot> drawOrder = _skeletonRenderer.skeleton.drawOrder;
        bool mustUpdateAttachmentOverrides = _attachmentOverrides == null || _attachmentOverrides.Length != drawOrder.Count;
        if (mustUpdateAttachmentOverrides) { 
            _attachmentOverrides = new AttachmentOverride[drawOrder.Count];
        } else if (!_updateEachFrame)
            return;

        for (int i = 0; i < drawOrder.Count; i++) {
            Slot slot = drawOrder.Items[i];
            Attachment attachment = slot.attachment;
            if (!mustUpdateAttachmentOverrides) {
                if (_attachmentOverrides[i] == null)
                    continue;

                SetAttachmentRendererObject(attachment, _attachmentOverrides[i].OverrideAtlasRegion);
                continue;
            }

            object rendererObject = GetAttachmentRendererObject(attachment);
            if (rendererObject == null)
                continue;

            Material material = (Material)((AtlasRegion)rendererObject).page.rendererObject;
            for (int j = 0; j < _atlasOverrides.Length; j++) {
                AtlasMaterialOverride atlasMaterialOverride = _atlasOverrides[j];
                for (int k = 0; k < atlasMaterialOverride.OriginalMaterials.Length; k++) {
                    Material overrideMaterial = atlasMaterialOverride.OverrideMaterials[k];
                    if (overrideMaterial == null)
                        continue;

                    Material originalMaterial = atlasMaterialOverride.OriginalMaterials[k];
                    if (originalMaterial != material)
                        continue;

                    AtlasRegion atlasRegion = new AtlasRegion();
                    AtlasPage atlasPage = new AtlasPage();
                    atlasPage.rendererObject = overrideMaterial;
                    atlasRegion.page = atlasPage;
                    _attachmentOverrides[i] = new AttachmentOverride();
                    _attachmentOverrides[i].OverrideAtlasRegion = atlasRegion;

                    SetAttachmentRendererObject(attachment, atlasRegion);

                    goto outerLoop;
                }
            }

            outerLoop: { }
        }
    }

    private void SetAttachmentRendererObject(Attachment attachment, object rendererObject) {
        var regionAttachment = attachment as RegionAttachment;
        if (regionAttachment != null) {
            regionAttachment.RendererObject = rendererObject;
        } else {
            var meshAttachment = attachment as MeshAttachment;
            if (meshAttachment != null) {
                meshAttachment.RendererObject = rendererObject;
            } else {
                var skinnedMeshAttachment = attachment as SkinnedMeshAttachment;
                if (skinnedMeshAttachment != null) {
                    skinnedMeshAttachment.RendererObject = rendererObject;
                }
            }
        }
    }

    private object GetAttachmentRendererObject(Attachment attachment) {
        object rendererObject = null;
        var regionAttachment = attachment as RegionAttachment;
        if (regionAttachment != null) {
            rendererObject = regionAttachment.RendererObject;
        } else {
            if (!_skeletonRenderer.renderMeshes)
                return rendererObject;

            var meshAttachment = attachment as MeshAttachment;
            if (meshAttachment != null) {
                rendererObject = meshAttachment.RendererObject;
            } else {
                var skinnedMeshAttachment = attachment as SkinnedMeshAttachment;
                if (skinnedMeshAttachment != null) {
                    rendererObject = skinnedMeshAttachment.RendererObject;
                } else
                    return rendererObject;
            }
        }

        return rendererObject;
    }

    private void Reset() {
        _skeletonRenderer = GetComponent<SkeletonRenderer>();
        if (_skeletonRenderer == null)
            return;

        SkeletonDataAsset skeletonDataAsset = _skeletonRenderer.skeletonDataAsset;
        _atlasOverrides = new AtlasMaterialOverride[skeletonDataAsset.atlasAssets.Length];
        for (int i = 0; i < _atlasOverrides.Length; i++) {
            _atlasOverrides[i] = new AtlasMaterialOverride();
            _atlasOverrides[i].OriginalMaterials = (Material[]) skeletonDataAsset.atlasAssets[i].materials.Clone();
            _atlasOverrides[i].OverrideMaterials = new Material[skeletonDataAsset.atlasAssets[i].materials.Length];
        }
    }

    [Serializable]
    public class AtlasMaterialOverride {
        public Material[] OriginalMaterials;
        public Material[] OverrideMaterials;
    }

    public class AttachmentOverride {
        public int AttachmentIndex;
        public AtlasRegion OverrideAtlasRegion;
    }
}
}

So it generates its own attachment object per modified attachment for each skeleton so it doesn't affect the others?
In what situation is it helpful for this to run every frame?
And how do other classes interface with it? There doesn't seem to be any publics. Is it an inspector thing?

MaterialPropertyBlock can work if you want to set material properties of an entire skeleton instance, not individual attachments.

Does MaterialPropertyBlock even work for MeshRenderers with multiple materials?

Pharan escribió

So it generates its own attachment object per modified attachment for each skeleton so it doesn't affect the others?
In what situation is it helpful for this to run every frame?
And how do other classes interface with it? There doesn't seem to be any publics. Is it an inspector thing?

Yes, it does generate new attachment objects for each skeleton instance. It isn't actually run each frame by default - the AttachmentOverride[] is generated once and just applied then. Yeah, it's currently set up entirely from Inspector, had no need for interfacing with other classes yet.

Pharan escribió

Does MaterialPropertyBlock even work for MeshRenderers with multiple materials?

It works, but in a way that makes it useless for this case. With MaterialPropertyBlock, you can only override a texture per-Renderer by it's property name in shader, so if you have two materials on the same renderer... Well, nothing good will happen. You'll probably have the same texture on all materials.
And yeah, even if that did work, it'd be pretty limited.

Huh, didn't realize mat prop blocks didn't work for submeshes.... That's utterly useless lol

un mes más tarde

When I came to Unity and convinced myself it would be better than Flash, I didn't think it would require an engineer degree to make a character blink from white to normal when getting hit 😃

And Astrophysics. Don't forget Astrophysics.

(Actually, now that I figure we can use MaterialPropertyBlock for it instead of vertex colors, I realize that this should be easier than that. Will update you in a few days at most, SoulGame lol)

Damn, I forgot Astrophysics, that's probably why I'm so confused.

Looking forwards to hear from you on the topic, I'm moving to another task meanwhile, my brain is blown up 🙂

Open up a new topic and describe what you're hoping to do, and I'll reply there. I don't think this topic is about what you're looking for (blinking character between white and normal).


26 Feb 2016 12:00 am


@SoulGame. Done, by the way.

As it turned out, my script wasn't actually making modifications per skeleton instance. I've assumed that a unique SkeletonData instance is used per SkeletonRenderer, bit it seems like they all use the same shared instance. Do I have to deep copy SkeletonData for each SkeletonRenderer now?..

It's quite astonishing through how many hoops do we have to jump to change the material... I'm still not sure how to execute such a seemingly simple thing.

Nah. SkeletonData is supposed to be stateless and shared across all Skeletons (which are stateful).
Attachments are also supposed to be stateless which makes the material-changing weird 'cause they're the things that eventually point to the Material.

The hoops are endless.

This is why I opted for changing the material on the Renderer end for whole skeleton changes, or making a deep copy of the Attachment for per attachment changes.

Okay, so I've made a simpler script for simple atlas overrides, it doess material replacement post-fast in MeshRenderer, works good enough:

using System;
using Spine;
using UnityEngine;

namespace Raycord {
    [RequireComponent(typeof(SkeletonRenderer))]
    [RequireComponent(typeof(MeshRenderer))]
    [ExecuteInEditMode]
    public class SpineOverrideMaterials : MonoBehaviour {
        [SerializeField]
        private SkeletonRenderer _skeletonRenderer;

    [SerializeField]
    private AtlasMaterialOverride[] _atlasOverrides;

    private Renderer _renderer;

    private void OnEnable() {
        _renderer = GetComponent<Renderer>();
    }

    private void OnWillRenderObject() {
        if (_skeletonRenderer == null || _skeletonRenderer.skeleton == null)
            return;

        Material[] sharedMaterials = _renderer.sharedMaterials;
        foreach (AtlasMaterialOverride atlasOverride in _atlasOverrides) {
            for (int i = 0; i < atlasOverride.OriginalMaterials.Length; i++) {
                for (int j = 0; j < sharedMaterials.Length; j++) {
                    if (sharedMaterials[j] == atlasOverride.OriginalMaterials[i]) {
                        sharedMaterials[j] = atlasOverride.OverrideMaterials[i];
                    }
                }
            }
        }

        _renderer.sharedMaterials = sharedMaterials;
    }

    private void Reset() {
        _skeletonRenderer = GetComponent<SkeletonRenderer>();
        if (_skeletonRenderer == null)
            return;

        SkeletonDataAsset skeletonDataAsset = _skeletonRenderer.skeletonDataAsset;
        _atlasOverrides = new AtlasMaterialOverride[skeletonDataAsset.atlasAssets.Length];
        for (int i = 0; i < _atlasOverrides.Length; i++) {
            _atlasOverrides[i] = new AtlasMaterialOverride();
            _atlasOverrides[i].OriginalMaterials = (Material[]) skeletonDataAsset.atlasAssets[i].materials.Clone();
            _atlasOverrides[i].OverrideMaterials = new Material[skeletonDataAsset.atlasAssets[i].materials.Length];
        }
    }

    [Serializable]
    public class AtlasMaterialOverride {
        public Material[] OriginalMaterials;
        public Material[] OverrideMaterials;
    }
}
}

Per-attachment changes that are shared across Skeleton instances can be done with the script I've posted before. But now what about per-instance per-attachment changes? Even if I do deep copy of Attachments, they will be shared across all Skeleton instances... Is there any way except manually deep copying SkeletonData?

Yeah. Attachments are supposed to be stateless which makes per attachment-for-that-instance weird. So you have to clone that attachment, and give the clone its own AtlasPage and AtlasRegion object and have that point to whatever Material you want.
The problem is when your Animations key that attachment or you call SetToSetupPose.

If you weren't using skins in Spine editor, it'll use SkeletonData's shared defaultSkin Skin object during attachment lookup. And the shared defaultSkin is shared and points to the original attachment.

So you won't have to deep-copy SkeletonData. But you may have to deep-copy defaultSkin and give it your modified attachments and assign that skin clone to your Skeleton instance. (Not sure how well this works. Theoretically, it should).

Don't feel limited just because it works out of the box a certain way. By default, the rendering specific data (atlas and Unity material) is jammed into the attachment rendererObject by AtlasAttachmentLoader. This is convenient for SkeletonRenderer to go from an attachment to a Unity material, but this can be customized by writing your own AttachmentLoader to do something else. You can also customize SkeletonRenderer, which is most likely the easiest way to do what you need. If SkeletonRenderer gets the material from the attachment rendererObject, then yeah you would need to clone/replace attachments throughout the SkeletonData. If you're going that route, it's easiest to just load the SkeletonData twice, memory permitting. Instead, you could change SkeletonRenderer to get the material elsewhere. Eg, the Skeleton could have an attachment to material map which SkeletonRenderer looks in first. If nothing was found in the map, then it uses the attachment rendererObject. Since the map is in Skeleton it is per instance and customizing a per instance material becomes as easy as map.put(attachment, material).

Yeah, giving SkeletonRenderer that extra Dictionary<Attachment, Material> or Dictionary<Slot, Material> to check seems like a clean way to go.
A bit messy if you want an inspector but if you already knew your attachments in code, I'd definitely do it that way.

Yes, extending SkeletonRenderer is the first thing that came to my mind, actually. Thing is, I generally avoid any changes to Spine runtime, so I can safely update to newer versions without having to figure out all the changes in case they interfere with my modifications. Though perhaps this is something that's worth integrating in the official runtime as well?

It might make sense to be in the official runtime. What do you think Pharan? I think the only issue is how much it affects people who aren't using this feature, which is probably not much.

Note if you use Git to pull down the runtimes, then any changes you make will be automatically merged when you update. If that isn't possible, you'll get a conflict and it should be pretty clear how to fix it. I very highly recommend SmartGit.

@[borrado]
There's that. I definitely opt for extensions rather than modifying the core as much as I can unless it just makes sense for everyone.

I think it should be in the official runtime.
But I don't think an inspector in SkeletonRenderer is a good idea at this point. That part (the serialization and inspector part) could be a separate MonoBehaviour though.

What do you think ZimM? Dictionary<Slot, Material>? Or Dictionary<Attachment, Material>?
I'm gravitating towards Slot.

Also, how do we skip the dictionary lookup so it doesn't affect people who aren't using it? if (materialDictionary.Count > 0)? Should we field-initialize the dictionary or lazy-instantiate?