#pragma warning disable 0168 // variable declared but not used. using System; using System.Collections.Generic; using UnityEngine.Scripting; using UnityEngine.U2D.Common; using Unity.Collections; using Unity.Collections.LowLevel.Unsafe; using Unity.Profiling; using UnityEngine.Rendering; using UnityEngine.Scripting.APIUpdating; namespace UnityEngine.U2D.Animation { /// /// Represents vertex position. /// internal struct PositionVertex { /// /// Vertex position. /// public Vector3 position; } /// /// Represents vertex position and tangent. /// internal struct PositionTangentVertex { /// /// Vertex position. /// public Vector3 position; /// /// Vertex tangent. /// public Vector4 tangent; } /// /// Deforms the Sprite that is currently assigned to the SpriteRenderer in the same GameObject. /// [Preserve] [ExecuteInEditMode] [DefaultExecutionOrder(-1)] [DisallowMultipleComponent] [RequireComponent(typeof(SpriteRenderer))] [AddComponentMenu("2D Animation/Sprite Skin")] [IconAttribute(IconUtility.IconPath + "Animation.SpriteSkin.png")] [MovedFrom("UnityEngine.U2D.Experimental.Animation")] [HelpURL("https://docs.unity3d.com/Packages/com.unity.2d.animation@9.0/manual/SpriteSkin.html")] public sealed class SpriteSkin : MonoBehaviour, IPreviewable, ISerializationCallbackReceiver { static class Profiling { public static readonly ProfilerMarker cacheCurrentSprite = new ProfilerMarker("SpriteSkin.CacheCurrentSprite"); public static readonly ProfilerMarker cacheHierarchy = new ProfilerMarker("SpriteSkin.CacheHierarchy"); public static readonly ProfilerMarker getSpriteBonesTransformFromGuid = new ProfilerMarker("SpriteSkin.GetSpriteBoneTransformsFromGuid"); public static readonly ProfilerMarker getSpriteBonesTransformFromPath = new ProfilerMarker("SpriteSkin.GetSpriteBoneTransformsFromPath"); } struct TransformData { public string fullName; public Transform transform; } [SerializeField] Transform m_RootBone; [SerializeField] Transform[] m_BoneTransforms = new Transform[0]; [SerializeField] Bounds m_Bounds; [SerializeField] bool m_AlwaysUpdate = true; [SerializeField] bool m_AutoRebind = false; // The deformed m_SpriteVertices stores all 'HOT' channels only in single-stream and essentially depends on Sprite Asset data. // The order of storage if present is POSITION, NORMALS, TANGENTS. NativeByteArray m_DeformedVertices; int m_CurrentDeformVerticesLength = 0; SpriteRenderer m_SpriteRenderer; int m_CurrentDeformSprite = 0; bool m_ForceSkinning; bool m_IsValid = false; int m_TransformsHash = 0; int m_TransformId; NativeArray m_BoneTransformId; int m_RootBoneTransformId; NativeCustomSlice m_SpriteUVs; NativeCustomSlice m_SpriteVertices; NativeCustomSlice m_SpriteTangents; NativeCustomSlice m_SpriteBoneWeights; NativeCustomSlice m_SpriteBindPoses; NativeCustomSlice m_BoneTransformIdNativeSlice; bool m_SpriteHasTangents; int m_SpriteVertexStreamSize; int m_SpriteVertexCount; int m_SpriteTangentVertexOffset; int m_DataIndex = -1; bool m_BoneCacheUpdateToDate = false; Dictionary> m_HierarchyCache = new Dictionary>(); internal Sprite sprite => m_SpriteRenderer != null ? m_SpriteRenderer.sprite : null; internal SpriteRenderer spriteRenderer => m_SpriteRenderer; internal NativeCustomSlice spriteBoneWeights => m_SpriteBoneWeights; /// /// Gets the index of the SpriteSkin in the SpriteSkinComposite. /// internal int dataIndex => m_DataIndex; /// /// Get and set the Auto Rebind property. /// When enabled, Sprite Skin attempts to automatically locate the Transform that is needed for the current Sprite assigned to the Sprite Renderer. /// public bool autoRebind { get => m_AutoRebind; set { m_AutoRebind = value; if (isActiveAndEnabled) { CacheHierarchy(); CacheCurrentSprite(m_AutoRebind); } } } /// /// Returns the Transform Components that is used for deformation. /// Do not modify elements of the returned array. /// /// An array of Transform Components. public Transform[] boneTransforms { get => m_BoneTransforms; internal set { m_BoneTransforms = value; CacheValidFlag(); if(isActiveAndEnabled) OnBoneTransformChanged(); } } /// /// Returns the Transform Component that represents the root bone for deformation. /// /// A Transform Component. public Transform rootBone { get => m_RootBone; internal set { m_RootBone = value; CacheValidFlag(); if (isActiveAndEnabled) { CacheHierarchy(); OnRootBoneTransformChanged(); } } } internal Bounds bounds { get => m_Bounds; set => m_Bounds = value; } /// /// Determines if the SpriteSkin executes even if the associated /// SpriteRenderer has been culled from view. /// public bool alwaysUpdate { get => m_AlwaysUpdate; set => m_AlwaysUpdate = value; } internal bool isValid => this.Validate() == SpriteSkinValidationResult.Ready; #if UNITY_EDITOR internal static Events.UnityEvent onDrawGizmos = new Events.UnityEvent(); void OnDrawGizmos() { onDrawGizmos.Invoke(); } internal bool ignoreNextSpriteChange { get; set; } = true; #endif int GetSpriteInstanceID() { return sprite != null ? sprite.GetInstanceID() : 0; } internal void Awake() { m_SpriteRenderer = GetComponent(); } void OnEnable() { Awake(); m_TransformsHash = 0; CacheCurrentSprite(false); if (m_HierarchyCache.Count == 0) CacheHierarchy(); OnEnableBatch(); } void OnEnableBatch() { m_TransformId = gameObject.transform.GetInstanceID(); UpdateSpriteDeform(); CacheBoneTransformIds(true); SpriteSkinComposite.instance.AddSpriteSkin(this); } void OnResetBatch() { CacheBoneTransformIds(true); SpriteSkinComposite.instance.CopyToSpriteSkinData(this); } void OnDisableBatch() { RemoveTransformFromSpriteSkinComposite(); SpriteSkinComposite.instance.RemoveSpriteSkin(this); } void OnBoneTransformChanged() { CacheBoneTransformIds(true); } void OnRootBoneTransformChanged() { CacheBoneTransformIds(true); } /// /// Called before object is serialized. /// public void OnBeforeSerialize() { OnBeforeSerializeBatch(); } /// /// Called after object is deserialized. /// public void OnAfterDeserialize() { OnAfterSerializeBatch(); } void OnBeforeSerializeBatch() {} void OnAfterSerializeBatch() { #if UNITY_EDITOR m_BoneCacheUpdateToDate = false; #endif } internal void OnEditorEnable() { Awake(); } void CacheValidFlag() { m_IsValid = isValid; if(!m_IsValid) DeactivateSkinning(); } internal bool BatchValidate() { CacheBoneTransformIds(); CacheCurrentSprite(m_AutoRebind); var hasSprite = m_CurrentDeformSprite != 0; return (m_IsValid && hasSprite && spriteRenderer.enabled && (alwaysUpdate || spriteRenderer.isVisible)); } void Reset() { Awake(); if (isActiveAndEnabled) { CacheValidFlag(); OnResetBatch(); } } void CacheBoneTransformIds(bool forceUpdate = false) { if (!m_BoneCacheUpdateToDate || forceUpdate) { SpriteSkinComposite.instance.RemoveTransformById(m_RootBoneTransformId); if (rootBone != null) { m_RootBoneTransformId = rootBone.GetInstanceID(); if (enabled) SpriteSkinComposite.instance.AddSpriteSkinRootBoneTransform(this); } else m_RootBoneTransformId = 0; if (boneTransforms != null) { var boneCount = 0; for (var i = 0; i < boneTransforms.Length; ++i) { if (boneTransforms[i] != null) ++boneCount; } if (m_BoneTransformId.IsCreated) { for (var i = 0; i < m_BoneTransformId.Length; ++i) SpriteSkinComposite.instance.RemoveTransformById(m_BoneTransformId[i]); NativeArrayHelpers.ResizeIfNeeded(ref m_BoneTransformId, boneCount); } else { m_BoneTransformId = new NativeArray(boneCount, Allocator.Persistent); } m_BoneTransformIdNativeSlice = new NativeCustomSlice(m_BoneTransformId); for (int i = 0, j = 0; i < boneTransforms.Length; ++i) { if (boneTransforms[i] != null) { m_BoneTransformId[j] = boneTransforms[i].GetInstanceID(); ++j; } } if (enabled) { SpriteSkinComposite.instance.AddSpriteSkinBoneTransform(this); } } else { if (m_BoneTransformId.IsCreated) NativeArrayHelpers.ResizeIfNeeded(ref m_BoneTransformId, 0); else m_BoneTransformId = new NativeArray(0, Allocator.Persistent); } CacheValidFlag(); m_BoneCacheUpdateToDate = true; SpriteSkinComposite.instance.CopyToSpriteSkinData(this); } } void RemoveTransformFromSpriteSkinComposite() { if (m_BoneTransformId.IsCreated) { for (var i = 0; i < m_BoneTransformId.Length; ++i) SpriteSkinComposite.instance.RemoveTransformById(m_BoneTransformId[i]); m_BoneTransformId.Dispose(); } SpriteSkinComposite.instance.RemoveTransformById(m_RootBoneTransformId); m_RootBoneTransformId = -1; m_BoneCacheUpdateToDate = false; } internal NativeByteArray GetDeformedVertices(int spriteVertexCount) { if (sprite != null) { if (m_CurrentDeformVerticesLength != spriteVertexCount) { m_TransformsHash = 0; m_CurrentDeformVerticesLength = spriteVertexCount; } } else { m_CurrentDeformVerticesLength = 0; } m_DeformedVertices = BufferManager.instance.GetBuffer(GetInstanceID(), m_CurrentDeformVerticesLength); return m_DeformedVertices; } /// /// Returns whether this SpriteSkin has currently deformed vertices. /// /// Returns true if this SpriteSkin has currently deformed vertices. Returns false otherwise. public bool HasCurrentDeformedVertices() { if (!m_IsValid) return false; return m_DataIndex >= 0 && SpriteSkinComposite.instance.HasDeformableBufferForSprite(m_DataIndex); } /// /// Gets a byte array to the currently deformed vertices for this SpriteSkin. /// /// Returns a reference to the currently deformed vertices. This is valid only for this calling frame. /// /// Thrown when there are no currently deformed vertices. /// HasCurrentDeformedVertices can be used to verify if there are any deformed vertices available. /// internal NativeArray GetCurrentDeformedVertices() { if (!m_IsValid) throw new InvalidOperationException("The SpriteSkin deformation is not valid."); if (m_DataIndex < 0) { throw new InvalidOperationException("There are no currently deformed vertices."); } return SpriteSkinComposite.instance.GetDeformableBufferForSprite(m_DataIndex); } /// /// Gets an array of currently deformed position vertices for this SpriteSkin. /// /// Returns a reference to the currently deformed vertices. This is valid only for this calling frame. /// /// Thrown when there are no currently deformed vertices or if the deformed vertices does not contain only /// position data. HasCurrentDeformedVertices can be used to verify if there are any deformed vertices available. /// internal NativeSlice GetCurrentDeformedVertexPositions() { if (!m_IsValid) throw new InvalidOperationException("The SpriteSkin deformation is not valid."); if (sprite.HasVertexAttribute(VertexAttribute.Tangent)) throw new InvalidOperationException("This SpriteSkin has deformed tangents"); if (!sprite.HasVertexAttribute(VertexAttribute.Position)) throw new InvalidOperationException("This SpriteSkin does not have deformed positions."); var deformedBuffer = GetCurrentDeformedVertices(); return deformedBuffer.Slice().SliceConvert(); } /// /// Gets an array of currently deformed position and tangent vertices for this SpriteSkin. /// /// /// Returns a reference to the currently deformed position and tangent vertices. This is valid only for this calling frame. /// /// /// Thrown when there are no currently deformed vertices or if the deformed vertices does not contain only /// position and tangent data. HasCurrentDeformedVertices can be used to verify if there are any deformed vertices available. /// internal NativeSlice GetCurrentDeformedVertexPositionsAndTangents() { if (!m_IsValid) throw new InvalidOperationException("The SpriteSkin deformation is not valid."); if (!sprite.HasVertexAttribute(VertexAttribute.Tangent)) throw new InvalidOperationException("This SpriteSkin does not have deformed tangents"); if (!sprite.HasVertexAttribute(VertexAttribute.Position)) throw new InvalidOperationException("This SpriteSkin does not have deformed positions."); var deformedBuffer = GetCurrentDeformedVertices(); return deformedBuffer.Slice().SliceConvert(); } /// /// Gets an enumerable to iterate through all deformed vertex positions of this SpriteSkin. /// /// Returns an IEnumerable to deformed vertex positions. /// /// Thrown when there is no vertex positions or deformed vertices. /// HasCurrentDeformedVertices can be used to verify if there are any deformed vertices available. /// public IEnumerable GetDeformedVertexPositionData() { if (!m_IsValid) throw new InvalidOperationException("The SpriteSkin deformation is not valid."); var hasPosition = sprite.HasVertexAttribute(VertexAttribute.Position); if (!hasPosition) throw new InvalidOperationException("Sprite does not have vertex position data."); var rawBuffer = GetCurrentDeformedVertices(); var rawSlice = rawBuffer.Slice(sprite.GetVertexStreamOffset(VertexAttribute.Position)); return new NativeCustomSliceEnumerator(rawSlice, sprite.GetVertexCount(), sprite.GetVertexStreamSize()); } /// /// Gets an enumerable to iterate through all deformed vertex tangents of this SpriteSkin. /// /// Returns an IEnumerable to deformed vertex tangents. /// /// Thrown when there is no vertex tangents or deformed vertices. /// HasCurrentDeformedVertices can be used to verify if there are any deformed vertices available. /// public IEnumerable GetDeformedVertexTangentData() { if (!m_IsValid) throw new InvalidOperationException("The SpriteSkin deformation is not valid."); var hasTangent = sprite.HasVertexAttribute(VertexAttribute.Tangent); if (!hasTangent) throw new InvalidOperationException("Sprite does not have vertex tangent data."); var rawBuffer = GetCurrentDeformedVertices(); var rawSlice = rawBuffer.Slice(sprite.GetVertexStreamOffset(VertexAttribute.Tangent)); return new NativeCustomSliceEnumerator(rawSlice, sprite.GetVertexCount(), sprite.GetVertexStreamSize()); } void OnDisable() { DeactivateSkinning(); BufferManager.instance.ReturnBuffer(GetInstanceID()); OnDisableBatch(); } /// /// Used by the animation clip preview window. /// Recommended to not use outside of this purpose. /// public void OnPreviewUpdate() { #if UNITY_EDITOR if(IsInGUIUpdateLoop()) Deform(); #endif } static bool IsInGUIUpdateLoop() => Event.current != null; void Deform() { CacheCurrentSprite(m_AutoRebind); if (isValid && isActiveAndEnabled && (this.alwaysUpdate || this.spriteRenderer.isVisible)) { var transformHash = SpriteSkinUtility.CalculateTransformHash(this); var spriteVertexCount = sprite.GetVertexStreamSize() * sprite.GetVertexCount(); if (spriteVertexCount > 0 && m_TransformsHash != transformHash) { var inputVertices = GetDeformedVertices(spriteVertexCount); SpriteSkinUtility.Deform(sprite, gameObject.transform.worldToLocalMatrix, boneTransforms, inputVertices.array); SpriteSkinUtility.UpdateBounds(this, inputVertices.array); InternalEngineBridge.SetDeformableBuffer(spriteRenderer, inputVertices.array); m_TransformsHash = transformHash; m_CurrentDeformSprite = GetSpriteInstanceID(); } } else if(!InternalEngineBridge.IsUsingDeformableBuffer(spriteRenderer, IntPtr.Zero)) { DeactivateSkinning(); } } void CacheCurrentSprite(bool rebind) { if (m_CurrentDeformSprite != GetSpriteInstanceID()) { using (Profiling.cacheCurrentSprite.Auto()) { DeactivateSkinning(); m_CurrentDeformSprite = GetSpriteInstanceID(); if (rebind && m_CurrentDeformSprite > 0 && rootBone != null) { if (!GetSpriteBonesTransforms(this, out var transforms)) Debug.LogWarning($"Rebind failed for {name}. Could not find all bones required by the Sprite: {sprite.name}."); boneTransforms = transforms; } UpdateSpriteDeform(); CacheValidFlag(); m_TransformsHash = 0; } } } void UpdateSpriteDeform() { if (sprite == null) { m_SpriteUVs = NativeCustomSlice.Default(); m_SpriteVertices = NativeCustomSlice.Default(); m_SpriteTangents = NativeCustomSlice.Default(); m_SpriteBoneWeights = NativeCustomSlice.Default(); m_SpriteBindPoses = NativeCustomSlice.Default(); m_SpriteHasTangents = false; m_SpriteVertexStreamSize = 0; m_SpriteVertexCount = 0; m_SpriteTangentVertexOffset = 0; } else { m_SpriteUVs = new NativeCustomSlice(sprite.GetVertexAttribute(VertexAttribute.TexCoord0)); m_SpriteVertices = new NativeCustomSlice(sprite.GetVertexAttribute(VertexAttribute.Position)); m_SpriteTangents = new NativeCustomSlice(sprite.GetVertexAttribute(VertexAttribute.Tangent)); m_SpriteBoneWeights = new NativeCustomSlice(sprite.GetVertexAttribute(VertexAttribute.BlendWeight)); m_SpriteBindPoses = new NativeCustomSlice(sprite.GetBindPoses()); m_SpriteHasTangents = sprite.HasVertexAttribute(VertexAttribute.Tangent); m_SpriteVertexStreamSize = sprite.GetVertexStreamSize(); m_SpriteVertexCount = sprite.GetVertexCount(); m_SpriteTangentVertexOffset = sprite.GetVertexStreamOffset(VertexAttribute.Tangent); } SpriteSkinComposite.instance.CopyToSpriteSkinData(this); } internal void CopyToSpriteSkinData(ref SpriteSkinData data, int spriteSkinIndex) { CacheBoneTransformIds(); CacheCurrentSprite(m_AutoRebind); data.vertices = m_SpriteVertices; data.boneWeights = m_SpriteBoneWeights; data.bindPoses = m_SpriteBindPoses; data.tangents = m_SpriteTangents; data.hasTangents = m_SpriteHasTangents; data.spriteVertexStreamSize = m_SpriteVertexStreamSize; data.spriteVertexCount = m_SpriteVertexCount; data.tangentVertexOffset = m_SpriteTangentVertexOffset; data.transformId = m_TransformId; data.boneTransformId = m_BoneTransformIdNativeSlice; m_DataIndex = spriteSkinIndex; } internal bool NeedUpdateCompositeCache() { unsafe { var iptr = new IntPtr(sprite.GetVertexAttribute(VertexAttribute.TexCoord0).GetUnsafeReadOnlyPtr()); var rs = m_SpriteUVs.data != iptr; if (rs) { UpdateSpriteDeform(); } return rs; } } void CacheHierarchy() { using (Profiling.cacheHierarchy.Auto()) { m_HierarchyCache.Clear(); if (rootBone == null || !m_AutoRebind) return; var boneCount = CountChildren(rootBone); m_HierarchyCache.EnsureCapacity(boneCount + 1); CacheChildren(rootBone, m_HierarchyCache); foreach (var entry in m_HierarchyCache) { if (entry.Value.Count == 1) continue; var count = entry.Value.Count; for (var i = 0; i < count; ++i) { var transformEntry = entry.Value[i]; transformEntry.fullName = GenerateTransformPath(rootBone, transformEntry.transform); entry.Value[i] = transformEntry; } } } } static void CacheChildren(Transform current, Dictionary> cache) { var nameHash = current.name.GetHashCode(); var entry = new TransformData() { fullName = String.Empty, transform = current }; if (cache.ContainsKey(nameHash)) cache[nameHash].Add(entry); else cache.Add(nameHash, new List(1) { entry }); for (var i = 0; i < current.childCount; ++i) CacheChildren(current.GetChild(i), cache); } static string GenerateTransformPath(Transform rootBone, Transform child) { var path = child.name; if (child == rootBone) return path; var parent = child.parent; do { path = parent.name + "/" + path; parent = parent.parent; } while (parent != rootBone && parent != null); return path; } internal static bool GetSpriteBonesTransforms(SpriteSkin spriteSkin, out Transform[] outTransform) { var rootBone = spriteSkin.rootBone; var spriteBones = spriteSkin.sprite.GetBones(); if(rootBone == null) throw new ArgumentException("rootBone parameter cannot be null"); if(spriteBones == null) throw new ArgumentException("spriteBones parameter cannot be null"); outTransform = new Transform[spriteBones.Length]; var boneObjects = rootBone.GetComponentsInChildren(); if (boneObjects != null && boneObjects.Length >= spriteBones.Length) { using (Profiling.getSpriteBonesTransformFromGuid.Auto()) { var i = 0; for (; i < spriteBones.Length; ++i) { var boneHash = spriteBones[i].guid; var boneTransform = Array.Find(boneObjects, x => (x.guid == boneHash)); if (boneTransform == null) break; outTransform[i] = boneTransform.transform; } if (i >= spriteBones.Length) return true; } } var hierarchyCache = spriteSkin.m_HierarchyCache; if (hierarchyCache.Count == 0) spriteSkin.CacheHierarchy(); // If unable to successfully map via guid, fall back to path return GetSpriteBonesTransformFromPath(spriteBones, hierarchyCache, outTransform); } static bool GetSpriteBonesTransformFromPath(SpriteBone[] spriteBones, Dictionary> hierarchyCache, Transform[] outNewBoneTransform) { using (Profiling.getSpriteBonesTransformFromPath.Auto()) { string[] bonePath = null; var foundBones = true; for (var i = 0; i < spriteBones.Length; ++i) { var nameHash = spriteBones[i].name.GetHashCode(); if (!hierarchyCache.TryGetValue(nameHash, out var children)) { outNewBoneTransform[i] = null; foundBones = false; continue; } if (children.Count == 1) outNewBoneTransform[i] = children[0].transform; else { if (bonePath == null) bonePath = new string[spriteBones.Length]; if (bonePath[i] == null) CalculateBoneTransformsPath(i, spriteBones, bonePath); var m = 0; for (; m < children.Count; ++m) { if (children[m].fullName.Contains(bonePath[i])) { outNewBoneTransform[i] = children[m].transform; break; } } if (m >= children.Count) { outNewBoneTransform[i] = null; foundBones = false; } } } return foundBones; } } static void CalculateBoneTransformsPath(int index, SpriteBone[] spriteBones, string[] paths) { var spriteBone = spriteBones[index]; var parentId = spriteBone.parentId; var bonePath = spriteBone.name; if (parentId != -1) { if (paths[parentId] == null) CalculateBoneTransformsPath(spriteBone.parentId, spriteBones, paths); paths[index] = $"{paths[parentId]}/{bonePath}"; } else paths[index] = bonePath; } internal void DeactivateSkinning() { if (m_SpriteRenderer != null) { var currentSprite = m_SpriteRenderer.sprite; if (currentSprite != null) InternalEngineBridge.SetLocalAABB(m_SpriteRenderer, currentSprite.bounds); m_SpriteRenderer.DeactivateDeformableBuffer(); } m_TransformsHash = 0; } internal void ResetSprite() { m_CurrentDeformSprite = 0; CacheValidFlag(); } static int CountChildren(Transform transform) { var childCount = transform.childCount; var count = childCount; for (var i = 0; i < childCount; ++i) count += CountChildren(transform.GetChild(i)); return count; } } }