• RuntimesUnity
  • Runtime Attachments with Secondary Textures.

Hi there,

I'm looking to modify my characters item related attachments at runtime.
The aim is to have these attachments update when the players inventory changes.
For example... the item attachment positioned in the characters hand should update at runtime to show the equipped weapon sprite.

My preferred method is to use the "GetRemappedClone" method, and this works great.
(FYI, I've tried bone followers, but this isn't desirable for my use case. Long story short - My game is 2.5d and I'm using custom shaders which distort the vertices of the mesh, so ideally i don't want to have seperate mesh renderers for all of my items)

However, when using the GetRemappedClone method, my sprites lose their secondary textures. To be more precise, their _EmissionMap.

As a quick example, i have two Texture2Ds
weapons-albedo and weapons-emissive.

The weapons-albedo looks like this.
It has several weapon sprites inside it.
It also has a reference to the _EmissionMap secondary texture (weapons-emissive)

and 'weapons-emissive' looks like the following.

Ideally, I want my sprites to have both albedo and emissive.
(For example purposes)

But what I actually get is just the albedo

What is the optimal way to achieve something like this?
I can't really get my head around it.
Any help would be greatly appreciated. Thanks.

[RequireComponent(typeof(SkeletonAnimation))]
public class AttachmentSetter : MonoBehaviour
{
    [SpineSlot] 
    [SerializeField]
    private string slotName;

    [Tooltip("Material whose Main Texture is set to the generated Texture2D of the atlas")]
    [SerializeField]
    private Material spriteMaterial;
    
    [Tooltip("The settings to apply to your new attachment when cloning from the template.")]
    [SerializeField]
    private AttachmentCloneSettings attachmentCloneSettings;
   
    
    private readonly Dictionary<string, Attachment> _attachmentCache = new();
    private SkeletonAnimation _skeletonAnimation;
    private RegionAttachment _originalAttachment;

    // Modify the Awake method to store the original attachment
    private void Awake() {
        _skeletonAnimation = GetComponent<SkeletonAnimation>();
    
        // Store the original attachment.
        Skeleton skeleton = _skeletonAnimation.Skeleton;
        Slot slot = skeleton.FindSlot(slotName);
        _originalAttachment = slot.Attachment as RegionAttachment;
    }

    
    public void SetSprite(string id, Sprite newSprite)
    {
        if (!newSprite || string.IsNullOrEmpty(id))
        {
            ResetAttachment();
            return;
        }

        // Locate the Spine slot and its current RegionAttachment as a template.
        Skeleton skeleton = _skeletonAnimation.Skeleton;
        Slot slot = skeleton.FindSlot(slotName);
        RegionAttachment template = slot.Attachment as RegionAttachment;

        if (template == null) {
            Log.Error($"'{slotName}' slot has no RegionAttachment to use as a template.");
            return;
        }

        // Get or create a remapped clone that swaps in newSprite.
        if (!_attachmentCache.TryGetValue(id, out Attachment attachment))
        {
            // First create the remapped clone
            attachment = template.GetRemappedClone(
                newSprite,
                spriteMaterial,
                premultiplyAlpha: attachmentCloneSettings.premultiplyAlpha,
                cloneMeshAsLinked: attachmentCloneSettings.cloneMeshAsLinked,
                useOriginalRegionSize: attachmentCloneSettings.useOriginalRegionSize,
                pivotShiftsMeshUVCoords: attachmentCloneSettings.pivotShiftsMeshUVCoords,
                useOriginalRegionScale: attachmentCloneSettings.useOriginalRegionScale
            );
            
            _attachmentCache[id] = attachment;
        }

        slot.Attachment = attachment;
    }

    
    public void ResetAttachment()
    {
        Skeleton skeleton = _skeletonAnimation.Skeleton;
        Slot slot = skeleton.FindSlot(slotName);
        if (_originalAttachment != null)
        {
            slot.Attachment = _originalAttachment;
        }
        else
        {
            Log.Warn("Original attachment was not stored properly.");
        }
    }
Related Discussions
...

To achieve the desired effect of having both albedo and emissive textures when using the GetRemappedClone method, you will need to ensure that the secondary texture (emissive map) is also applied to the cloned attachment. The GetRemappedClone method primarily focuses on remapping the main texture, so additional steps are required to handle secondary textures like emissive maps. You can manually set the secondary texture on the material used for the cloned attachment. This involves accessing the material's properties and assigning the emissive texture to the appropriate shader property, typically _EmissionMap. Ensure that your shader supports emission and that the material is set up to use the emissive map.

Here's a basic approach to modify your SetSprite method to include setting the emissive map:

public void SetSprite(string id, Sprite newSprite, Texture2D emissiveTexture)
{
    if (!newSprite || string.IsNullOrEmpty(id))
    {
        ResetAttachment();
        return;
    }

    Skeleton skeleton = _skeletonAnimation.Skeleton;
    Slot slot = skeleton.FindSlot(slotName);
    RegionAttachment template = slot.Attachment as RegionAttachment;

    if (template == null) {
        Log.Error($"'{slotName}' slot has no RegionAttachment to use as a template.");
        return;
    }

    if (!_attachmentCache.TryGetValue(id, out Attachment attachment))
    {
        attachment = template.GetRemappedClone(
            newSprite,
            spriteMaterial,
            premultiplyAlpha: attachmentCloneSettings.premultiplyAlpha,
            cloneMeshAsLinked: attachmentCloneSettings.cloneMeshAsLinked,
            useOriginalRegionSize: attachmentCloneSettings.useOriginalRegionSize,
            pivotShiftsMeshUVCoords: attachmentCloneSettings.pivotShiftsMeshUVCoords,
            useOriginalRegionScale: attachmentCloneSettings.useOriginalRegionScale
        );

        // Set the emissive texture on the material
        spriteMaterial.SetTexture("_EmissionMap", emissiveTexture);

        _attachmentCache[id] = attachment;
    }

    slot.Attachment = attachment;
}

This code assumes that your material and shader are set up to use an emissive map. Adjust the shader property name if your shader uses a different name for the emissive map.

The suggested approach doesn't work, because the UVs of the emissive texture that we pass through do not match the UVs of the materials main texture.

For example...
You can see here our texture correctly represents our weapon, but the emission texture is still sampling the entire 'weapons-emissive' texture

@georgen Thanks for your detailed writeup. Unfortunately the Sprite-based methods like GetRemappedClone(this Attachment o, Sprite sprite,..) do not yet support copying secondary textures of a Sprite.

As accessing secondary textures from a Sprite object is now supported on Unity 2022.2 and newer versions, we've created an issue ticket here for this issue:
EsotericSoftware/spine-runtimes2855
Until this is feature is implemented, if you require it earlier I'm afraid that you will need to write your own version of the GetRemappedClone() method, accessing secondary textures and assigning them to additional material properties e.g. as GetRepackedSkin() does for repacking normalmaps here.