440 lines
16 KiB
C#
440 lines
16 KiB
C#
using System.Collections.Generic;
|
|
using UnityEditorInternal;
|
|
using UnityEngine;
|
|
using UnityEngine.U2D;
|
|
using UnityEngine.U2D.Animation;
|
|
|
|
namespace UnityEditor.U2D.Animation
|
|
{
|
|
[CustomEditor(typeof(SpriteSkin))]
|
|
[CanEditMultipleObjects]
|
|
class SpriteSkinEditor : Editor
|
|
{
|
|
static class Contents
|
|
{
|
|
public static readonly GUIContent listHeaderLabel = new GUIContent("Bones", "GameObject Transform to represent the Bones defined by the Sprite that is currently used for deformation.");
|
|
public static readonly GUIContent rootBoneLabel = new GUIContent("Root Bone", "GameObject Transform to represent the Root Bone.");
|
|
public static readonly string spriteNotFound = L10n.Tr($"Sprite not found in {ObjectNames.NicifyVariableName(nameof(SpriteRenderer))}");
|
|
public static readonly string spriteHasNoSkinningInformation = L10n.Tr("Sprite has no Bind Poses");
|
|
public static readonly string spriteHasNoWeights = L10n.Tr("Sprite has no weights");
|
|
public static readonly string rootTransformNotFound = L10n.Tr("Root Bone not set");
|
|
public static readonly string invalidTransformArray = L10n.Tr("Bone list is invalid");
|
|
public static readonly string transformArrayContainsNull = L10n.Tr("Bone list contains unassigned references");
|
|
public static readonly string invalidTransformArrayLength = L10n.Tr("The number of Sprite's Bind Poses and the number of Transforms should match");
|
|
public static readonly string invalidBoneWeights = L10n.Tr("Bone weights are invalid");
|
|
public static readonly GUIContent alwaysUpdate = new GUIContent("Always Update", "Executes deformation of SpriteSkin even when the associated SpriteRenderer has been culled and is not visible.");
|
|
public static readonly GUIContent autoRebind = new GUIContent("Auto Rebind", "When the Sprite in SpriteRenderer is changed, SpriteSkin will try to look for the Transforms that is needed for the Sprite using the Root Bone Tranform as parent.");
|
|
public static readonly GUIContent boundsMode = new GUIContent("Bounds Mode", "Mode used for calculating bounds for culling. Vertex-based provides accurate culling for both CPU and GPU skinning. Bone-based provides aggressive culling and is most beneficial with GPU skinning.");
|
|
}
|
|
|
|
SerializedProperty m_RootBoneProperty;
|
|
SerializedProperty m_BoneTransformsProperty;
|
|
SerializedProperty m_AlwaysUpdateProperty;
|
|
SerializedProperty m_AutoRebindProperty;
|
|
SerializedProperty m_BoundsModeProperty;
|
|
|
|
SpriteSkin[] m_SpriteSkins;
|
|
Sprite[] m_CurrentSprites;
|
|
ReorderableList m_ReorderableList;
|
|
|
|
bool m_NeedsRebind = false;
|
|
bool m_BoneFold = true;
|
|
|
|
void OnEnable()
|
|
{
|
|
List<SpriteSkin> listOfSkins = new List<SpriteSkin>(targets.Length);
|
|
foreach (Object obj in targets)
|
|
{
|
|
if (obj is SpriteSkin skin)
|
|
{
|
|
listOfSkins.Add(skin);
|
|
skin.OnEditorEnable();
|
|
}
|
|
}
|
|
|
|
m_SpriteSkins = listOfSkins.ToArray();
|
|
|
|
m_RootBoneProperty = serializedObject.FindProperty("m_RootBone");
|
|
|
|
m_BoneTransformsProperty = serializedObject.FindProperty("m_BoneTransforms");
|
|
m_AlwaysUpdateProperty = serializedObject.FindProperty("m_AlwaysUpdate");
|
|
m_AutoRebindProperty = serializedObject.FindProperty("m_AutoRebind");
|
|
m_BoundsModeProperty = serializedObject.FindProperty("m_BoundsMode");
|
|
|
|
m_CurrentSprites = new Sprite[m_SpriteSkins.Length];
|
|
|
|
UpdateSpriteCache();
|
|
SetupReorderableList();
|
|
|
|
Undo.undoRedoPerformed += UndoRedoPerformed;
|
|
}
|
|
|
|
void OnDestroy()
|
|
{
|
|
Undo.undoRedoPerformed -= UndoRedoPerformed;
|
|
}
|
|
|
|
void UndoRedoPerformed()
|
|
{
|
|
UpdateSpriteCache();
|
|
}
|
|
|
|
void UpdateSpriteCache()
|
|
{
|
|
for (int i = 0; i < m_SpriteSkins.Length; ++i)
|
|
{
|
|
m_CurrentSprites[i] = m_SpriteSkins[i].sprite;
|
|
}
|
|
}
|
|
|
|
public override void OnInspectorGUI()
|
|
{
|
|
serializedObject.Update();
|
|
|
|
EditorGUILayout.PropertyField(m_AlwaysUpdateProperty, Contents.alwaysUpdate);
|
|
|
|
EditorGUI.BeginChangeCheck();
|
|
EditorGUILayout.PropertyField(m_AutoRebindProperty, Contents.autoRebind);
|
|
if (EditorGUI.EndChangeCheck() && m_AutoRebindProperty.boolValue)
|
|
{
|
|
m_NeedsRebind = true;
|
|
}
|
|
|
|
DoBoundsModeField();
|
|
|
|
EditorGUI.BeginChangeCheck();
|
|
EditorGUILayout.PropertyField(m_RootBoneProperty, Contents.rootBoneLabel);
|
|
if (EditorGUI.EndChangeCheck())
|
|
{
|
|
m_NeedsRebind = true;
|
|
}
|
|
|
|
bool hasSpriteChanged = HasAnySpriteChanged();
|
|
if (m_ReorderableList == null || hasSpriteChanged)
|
|
{
|
|
UpdateSpriteCache();
|
|
InitializeBoneTransformArray();
|
|
SetupReorderableList();
|
|
}
|
|
|
|
DoBoneFoldout();
|
|
|
|
EditorGUILayout.BeginHorizontal();
|
|
GUILayout.FlexibleSpace();
|
|
|
|
EditorGUI.BeginDisabledGroup(!EnableCreateBones());
|
|
DoGenerateBonesButton();
|
|
EditorGUI.EndDisabledGroup();
|
|
|
|
EditorGUI.BeginDisabledGroup(!EnableSetBindPose());
|
|
DoResetBindPoseButton();
|
|
EditorGUI.EndDisabledGroup();
|
|
|
|
GUILayout.FlexibleSpace();
|
|
EditorGUILayout.EndHorizontal();
|
|
|
|
serializedObject.ApplyModifiedProperties();
|
|
|
|
if (m_NeedsRebind)
|
|
Rebind();
|
|
|
|
if (hasSpriteChanged && !m_SpriteSkins[0].ignoreNextSpriteChange)
|
|
{
|
|
ResetBounds(Undo.GetCurrentGroupName());
|
|
m_SpriteSkins[0].ignoreNextSpriteChange = false;
|
|
}
|
|
|
|
DoValidationWarnings();
|
|
}
|
|
|
|
bool HasAnySpriteChanged()
|
|
{
|
|
for (int i = 0; i < m_SpriteSkins.Length; ++i)
|
|
{
|
|
Sprite sprite = m_SpriteSkins[i].sprite;
|
|
if (m_CurrentSprites[i] != sprite)
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void DoBoneFoldout()
|
|
{
|
|
EditorGUILayout.Space();
|
|
|
|
m_BoneFold = EditorGUILayout.Foldout(m_BoneFold, Contents.listHeaderLabel, true);
|
|
if (m_BoneFold)
|
|
{
|
|
EditorGUI.BeginDisabledGroup(m_SpriteSkins[0].rootBone == null || m_BoneTransformsProperty.hasMultipleDifferentValues);
|
|
m_ReorderableList.DoLayoutList();
|
|
EditorGUI.EndDisabledGroup();
|
|
}
|
|
}
|
|
|
|
void InitializeBoneTransformArray()
|
|
{
|
|
bool hasSameNumberOfBones = true;
|
|
int noOfBones = -1;
|
|
for (int i = 0; i < m_SpriteSkins.Length; ++i)
|
|
{
|
|
if (m_SpriteSkins[i] == null || m_CurrentSprites[i] == null)
|
|
continue;
|
|
if (i == 0)
|
|
noOfBones = m_CurrentSprites[i].GetBones().Length;
|
|
else if (m_CurrentSprites[i].GetBones().Length != noOfBones)
|
|
{
|
|
hasSameNumberOfBones = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (hasSameNumberOfBones)
|
|
{
|
|
int elementCount = m_BoneTransformsProperty.arraySize;
|
|
SpriteBone[] bones = m_CurrentSprites[0] != null ? m_CurrentSprites[0].GetBones() : new SpriteBone[0];
|
|
|
|
if (elementCount != bones.Length)
|
|
{
|
|
m_BoneTransformsProperty.arraySize = bones.Length;
|
|
|
|
for (int i = elementCount; i < m_BoneTransformsProperty.arraySize; ++i)
|
|
m_BoneTransformsProperty.GetArrayElementAtIndex(i).objectReferenceValue = null;
|
|
|
|
m_NeedsRebind = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
void SetupReorderableList()
|
|
{
|
|
m_ReorderableList = new ReorderableList(serializedObject, m_BoneTransformsProperty, false, true, false, false);
|
|
m_ReorderableList.headerHeight = 1.0f;
|
|
m_ReorderableList.elementHeightCallback = _ => EditorGUIUtility.singleLineHeight + 6;
|
|
m_ReorderableList.drawElementCallback = (rect, index, _, _) =>
|
|
{
|
|
GUIContent content = GUIContent.none;
|
|
|
|
if (m_CurrentSprites[0] != null)
|
|
{
|
|
SpriteBone[] bones = m_CurrentSprites[0].GetBones();
|
|
if (index < bones.Length)
|
|
content = new GUIContent(bones[index].name);
|
|
}
|
|
|
|
rect.y += 2f;
|
|
rect.height = EditorGUIUtility.singleLineHeight;
|
|
SerializedProperty element = m_BoneTransformsProperty.GetArrayElementAtIndex(index);
|
|
|
|
EditorGUI.showMixedValue = m_BoneTransformsProperty.hasMultipleDifferentValues;
|
|
EditorGUI.PropertyField(rect, element, content);
|
|
};
|
|
}
|
|
|
|
|
|
void Rebind()
|
|
{
|
|
foreach (SpriteSkin skin in m_SpriteSkins)
|
|
{
|
|
if (skin.sprite == null || skin.rootBone == null)
|
|
continue;
|
|
if (!SpriteSkinHelpers.GetSpriteBonesTransforms(skin, out Transform[] transforms, forceCreateCache: true))
|
|
Debug.LogWarning($"Rebind failed for {skin.name}. Could not find all bones required by the Sprite: {skin.sprite.name}.");
|
|
skin.SetBoneTransforms(transforms);
|
|
|
|
if (!m_AutoRebindProperty.boolValue)
|
|
// Clear force created cache if auto rebind is disabled
|
|
skin.hierarchyCache.Clear();
|
|
|
|
ResetBoundsIfNeeded(skin);
|
|
}
|
|
|
|
serializedObject.Update();
|
|
m_NeedsRebind = false;
|
|
}
|
|
|
|
void ResetBounds(string undoName = "Reset Bounds")
|
|
{
|
|
foreach (SpriteSkin skin in m_SpriteSkins)
|
|
{
|
|
if (!skin.isValid)
|
|
continue;
|
|
|
|
Undo.RegisterCompleteObjectUndo(skin, undoName);
|
|
skin.CalculateBounds();
|
|
|
|
EditorUtility.SetDirty(skin);
|
|
}
|
|
}
|
|
|
|
static void ResetBoundsIfNeeded(SpriteSkin spriteSkin)
|
|
{
|
|
if (spriteSkin.isValid && spriteSkin.bounds == new Bounds())
|
|
spriteSkin.CalculateBounds();
|
|
}
|
|
|
|
bool EnableCreateBones()
|
|
{
|
|
foreach (SpriteSkin skin in m_SpriteSkins)
|
|
{
|
|
Sprite sprite = skin.sprite;
|
|
if (sprite != null && skin.rootBone == null)
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool EnableSetBindPose()
|
|
{
|
|
return IsAnyTargetValid();
|
|
}
|
|
|
|
bool IsAnyTargetValid()
|
|
{
|
|
foreach (SpriteSkin skin in m_SpriteSkins)
|
|
{
|
|
if (skin.isValid)
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void DoGenerateBonesButton()
|
|
{
|
|
if (GUILayout.Button("Create Bones", GUILayout.MaxWidth(125f)))
|
|
{
|
|
foreach (SpriteSkin skin in m_SpriteSkins)
|
|
{
|
|
Sprite sprite = skin.sprite;
|
|
if (sprite == null || skin.rootBone != null)
|
|
continue;
|
|
|
|
Undo.RegisterCompleteObjectUndo(skin, "Create Bones");
|
|
|
|
skin.CreateBoneHierarchy();
|
|
|
|
foreach (Transform transform in skin.boneTransforms)
|
|
Undo.RegisterCreatedObjectUndo(transform.gameObject, "Create Bones");
|
|
|
|
ResetBoundsIfNeeded(skin);
|
|
|
|
EditorUtility.SetDirty(skin);
|
|
}
|
|
|
|
serializedObject.SetIsDifferentCacheDirty();
|
|
BoneGizmo.instance.boneGizmoController.OnSelectionChanged();
|
|
}
|
|
}
|
|
|
|
void DoResetBindPoseButton()
|
|
{
|
|
if (GUILayout.Button("Reset Bind Pose", GUILayout.MaxWidth(125f)))
|
|
{
|
|
foreach (SpriteSkin skin in m_SpriteSkins)
|
|
{
|
|
if (!skin.isValid)
|
|
continue;
|
|
|
|
Undo.RecordObjects(skin.boneTransforms, "Reset Bind Pose");
|
|
skin.ResetBindPose();
|
|
}
|
|
}
|
|
}
|
|
|
|
void DoValidationWarnings()
|
|
{
|
|
EditorGUILayout.Space();
|
|
|
|
bool preAppendObjectName = targets.Length > 1;
|
|
|
|
foreach (SpriteSkin skin in m_SpriteSkins)
|
|
{
|
|
SpriteSkinState validationResult = skin.Validate();
|
|
if (validationResult == SpriteSkinState.Ready)
|
|
continue;
|
|
|
|
string text = "";
|
|
switch (validationResult)
|
|
{
|
|
case SpriteSkinState.SpriteNotFound:
|
|
text = Contents.spriteNotFound;
|
|
break;
|
|
case SpriteSkinState.SpriteHasNoSkinningInformation:
|
|
text = Contents.spriteHasNoSkinningInformation;
|
|
break;
|
|
case SpriteSkinState.SpriteHasNoWeights:
|
|
text = Contents.spriteHasNoWeights;
|
|
break;
|
|
case SpriteSkinState.RootTransformNotFound:
|
|
text = Contents.rootTransformNotFound;
|
|
break;
|
|
case SpriteSkinState.InvalidTransformArray:
|
|
text = Contents.invalidTransformArray;
|
|
break;
|
|
case SpriteSkinState.InvalidTransformArrayLength:
|
|
text = Contents.invalidTransformArrayLength;
|
|
break;
|
|
case SpriteSkinState.TransformArrayContainsNull:
|
|
text = Contents.transformArrayContainsNull;
|
|
break;
|
|
case SpriteSkinState.InvalidBoneWeights:
|
|
text = Contents.invalidBoneWeights;
|
|
break;
|
|
}
|
|
|
|
if (preAppendObjectName)
|
|
text = $"{skin.name}:{text}";
|
|
|
|
EditorGUILayout.HelpBox(text, MessageType.Warning);
|
|
}
|
|
}
|
|
|
|
void DoBoundsModeField()
|
|
{
|
|
EditorGUI.BeginChangeCheck();
|
|
EditorGUILayout.PropertyField(m_BoundsModeProperty, Contents.boundsMode);
|
|
|
|
// Show info message if bone-based bounds is selected with CPU skinning
|
|
if (m_BoundsModeProperty.enumValueIndex == (int)BoundsMode.BoneBased)
|
|
{
|
|
bool hasCpuSkinnedSprites = false;
|
|
foreach (SpriteSkin skin in m_SpriteSkins)
|
|
{
|
|
if (skin != null && skin.currentDeformationMethod == DeformationMethods.Cpu)
|
|
{
|
|
hasCpuSkinnedSprites = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (hasCpuSkinnedSprites)
|
|
EditorGUILayout.HelpBox("Vertex-based bounds mode is accurate and efficient with CPU skinning.", MessageType.Info);
|
|
}
|
|
// Show info message if vertex-based bounds is selected with GPU skinning
|
|
else if (m_BoundsModeProperty.enumValueIndex == (int)BoundsMode.VertexBased)
|
|
{
|
|
bool hasGpuSkinnedSprites = false;
|
|
foreach (SpriteSkin skin in m_SpriteSkins)
|
|
{
|
|
if (skin != null && skin.currentDeformationMethod == DeformationMethods.Gpu)
|
|
{
|
|
hasGpuSkinnedSprites = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (hasGpuSkinnedSprites)
|
|
EditorGUILayout.HelpBox("Bone-based bounds mode can improve performance with GPU skinning.", MessageType.Info);
|
|
}
|
|
|
|
if (EditorGUI.EndChangeCheck())
|
|
{
|
|
// Apply changes to all selected SpriteSkins
|
|
serializedObject.ApplyModifiedProperties();
|
|
}
|
|
}
|
|
}
|
|
}
|