ZimM

I have a character with a single skin, but I need to change its color in a shader. Now, how can I do that? It seems like Spine-Unity runtime always restores whatever material the attachment had originally. Is it really not possible to have a custom material? Per-instance, and, ideally, per-attachment material overrides seem to be an obvious idea. Perhaps I'm just missing something obvious?
User avatar
ZimM
Posts: 25

Pharan

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.
Official Esoteric Assorted Furniture Cleaner and Teahouse | Check out the Spine Users Tumblr Blog: spine-users.tumblr.com
pharan.deviantart.com | pharantriestoanimatestuff.tumblr.com - - - Windows 10 - Spine-Unity.
User avatar
Pharan

Pharan
Posts: 4273

ZimM

UPD: updated script.
Pharan wrote:...
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;
}
}
}
User avatar
ZimM
Posts: 25

Pharan

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?
Official Esoteric Assorted Furniture Cleaner and Teahouse | Check out the Spine Users Tumblr Blog: spine-users.tumblr.com
pharan.deviantart.com | pharantriestoanimatestuff.tumblr.com - - - Windows 10 - Spine-Unity.
User avatar
Pharan

Pharan
Posts: 4273

Mitch

Honestly the best way to deal with this us by using PropertyBlocks... I'm not actually 100% if Unity 5.2+ works correctly w/ Spine in that regard.

http://docs.unity3d.com/ScriptReference/MaterialPropertyBlock.html

But the general idea is that this allows you to swap material textures/properties per-renderer
User avatar
Mitch

Mitch
Posts: 961

Pharan

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?
Official Esoteric Assorted Furniture Cleaner and Teahouse | Check out the Spine Users Tumblr Blog: spine-users.tumblr.com
pharan.deviantart.com | pharantriestoanimatestuff.tumblr.com - - - Windows 10 - Spine-Unity.
User avatar
Pharan

Pharan
Posts: 4273

ZimM

Pharan wrote: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 wrote: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.
User avatar
ZimM
Posts: 25

Mitch

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

Mitch
Posts: 961

SoulGame

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 :D
User avatar
SoulGame
Posts: 83

Pharan

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)
Official Esoteric Assorted Furniture Cleaner and Teahouse | Check out the Spine Users Tumblr Blog: spine-users.tumblr.com
pharan.deviantart.com | pharantriestoanimatestuff.tumblr.com - - - Windows 10 - Spine-Unity.
User avatar
Pharan

Pharan
Posts: 4273

SoulGame

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 :)
User avatar
SoulGame
Posts: 83

Pharan

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.
Official Esoteric Assorted Furniture Cleaner and Teahouse | Check out the Spine Users Tumblr Blog: spine-users.tumblr.com
pharan.deviantart.com | pharantriestoanimatestuff.tumblr.com - - - Windows 10 - Spine-Unity.
User avatar
Pharan

Pharan
Posts: 4273

ZimM

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.
User avatar
ZimM
Posts: 25

Pharan

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.
Official Esoteric Assorted Furniture Cleaner and Teahouse | Check out the Spine Users Tumblr Blog: spine-users.tumblr.com
pharan.deviantart.com | pharantriestoanimatestuff.tumblr.com - - - Windows 10 - Spine-Unity.
User avatar
Pharan

Pharan
Posts: 4273

ZimM

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?
User avatar
ZimM
Posts: 25

Pharan

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).
Official Esoteric Assorted Furniture Cleaner and Teahouse | Check out the Spine Users Tumblr Blog: spine-users.tumblr.com
pharan.deviantart.com | pharantriestoanimatestuff.tumblr.com - - - Windows 10 - Spine-Unity.
User avatar
Pharan

Pharan
Posts: 4273

Nate

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).
User avatar
Nate

Nate
Posts: 7361

Pharan

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.
Official Esoteric Assorted Furniture Cleaner and Teahouse | Check out the Spine Users Tumblr Blog: spine-users.tumblr.com
pharan.deviantart.com | pharantriestoanimatestuff.tumblr.com - - - Windows 10 - Spine-Unity.
User avatar
Pharan

Pharan
Posts: 4273

ZimM

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?
User avatar
ZimM
Posts: 25

Nate

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.
User avatar
Nate

Nate
Posts: 7361

Pharan

@ZimM
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?
Official Esoteric Assorted Furniture Cleaner and Teahouse | Check out the Spine Users Tumblr Blog: spine-users.tumblr.com
pharan.deviantart.com | pharantriestoanimatestuff.tumblr.com - - - Windows 10 - Spine-Unity.
User avatar
Pharan

Pharan
Posts: 4273

brendan.vance

Our current project requires us to do a bunch of fancy nonsense in order to make spine work for us. One such bit of nonsense involves using special cutout shaders to draw spine characters in some contexts (since we position them within 3d environments) and special UI shaders in other contexts (since we also use them in Unity UI hierarchies). Spine is, by necessity perhaps, a little bit bossy about how/when it creates and assigns materials, making it very difficult for us to achieve this!

One thing we can do is iterate through the materials list in LateUpdate() and simply swap shaders where it's necessary, but this suffers from a) update order problems and b) inefficiency, as even inspecting a MeshRenderer's sharedmaterials allocates garbage. We could also do it on one of Unity's render callbacks, though those get called once per camera (which in some cases would be more than we require).

It might be helpful for us to have a way to listen for when SkeletonRenderer's LateUpdate completes (so that we can futz with the meshrender at that point).
brendan.vance
Posts: 5

Nate

I would blame the Unity APIs to be fair, but I digress. :talk:

We are all for improving how things work. Don't be shy about changing SkeletonRenderer. If you have a solution that is useful in general without impacting performance, it makes sense to put it in Git.
User avatar
Nate

Nate
Posts: 7361

Pharan

@brendan.vance
Yeah. If you have a particular need. Just change SkeletonRenderer. And if you think it's useful for the general case, suggest it.

For modifying the material array, SkeletonRenderer actually caches that array as one of its fields before passing it to MeshRenderer. So you can access it without generating garbage if you make it [NonSerialized] public, I guess, and put an appropriate event you can listen to at the end of SkeletonRenderer to change it at the right time.

For Spine in Unity UI, check out SkeletonGraphic. It's part of the latest runtime. It does fancy nonsense to be able to draw in UI and be masked and stuff. It also suffers the same limitations of other Unity UI Graphic classes (only single-material meshes are supported, but it doesn't enforce a material so you can set it to whatever you want that has a shader with a _MainTex.)

The core of what SkeletonRenderer actually needs to do to render correctly is set the correct texture per submesh so it's actually pulling the right images from the right textures. There's no way to set a texture array, or use some kind of MaterialPropertyBlock to set the texture per submesh, so setting Material array was the solution. This is Unity's API working against it, and now against you. But again, you can fix whatever parts of it if you need to.

-- 04 Mar 2016 3:49 am --

Per slot materials have been added to the latest SkeletonRenderer/SkeletonAnimation.

Using the API looks like this:
using UnityEngine;
using System.Collections;

public class SampleClassThatUsesCustomSlotMaterials : MonoBehaviour {

public SkeletonAnimation skeletonAnimation;

[SpineSlot]
public string slotName;

public Material material;

void Start () {
if (material == null)
Debug.LogWarning("Warning!! A null material will cause the attachment to be rendered with the default Unity pink material.");

Spine.Slot slotObject = skeletonAnimation.skeleton.FindSlot(slotName);
skeletonAnimation.CustomSlotMaterials[slotObject] = material;
}

public void RemoveCustomMaterial () {
Spine.Slot slotObject = skeletonAnimation.skeleton.FindSlot(slotName);
skeletonAnimation.CustomSlotMaterials.Remove(slotObject);
}

}
Official Esoteric Assorted Furniture Cleaner and Teahouse | Check out the Spine Users Tumblr Blog: spine-users.tumblr.com
pharan.deviantart.com | pharantriestoanimatestuff.tumblr.com - - - Windows 10 - Spine-Unity.
User avatar
Pharan

Pharan
Posts: 4273

ZimM

Oh, cool, you've done it already :) I guess there are some use-cases for per-attachment overrides, but they are quite marginal.
Unfortunately, this does not solves the problem of changing material per-atlas. Sure, now we can just assign the material to each and every slot, but that would be pretty ugly and slow. In general, I often need to override the character material as a whole (per-atlas), and very occasionally change the material for a slot. Perhaps it'd make sense to add per-atlas override on top of that, with per-slot override having a higher priority? What do you think?
User avatar
ZimM
Posts: 25


Return to Unity