#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 { /// <summary> /// Represents vertex position. /// </summary> internal struct PositionVertex { /// <summary> /// Vertex position. /// </summary> public Vector3 position; } /// <summary> /// Represents vertex position and tangent. /// </summary> internal struct PositionTangentVertex { /// <summary> /// Vertex position. /// </summary> public Vector3 position; /// <summary> /// Vertex tangent. /// </summary> public Vector4 tangent; } /// <summary> /// Deforms the Sprite that is currently assigned to the SpriteRenderer in the same GameObject. /// </summary> [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<int> m_BoneTransformId; int m_RootBoneTransformId; NativeCustomSlice<Vector2> m_SpriteUVs; NativeCustomSlice<Vector3> m_SpriteVertices; NativeCustomSlice<Vector4> m_SpriteTangents; NativeCustomSlice<BoneWeight> m_SpriteBoneWeights; NativeCustomSlice<Matrix4x4> m_SpriteBindPoses; NativeCustomSlice<int> m_BoneTransformIdNativeSlice; bool m_SpriteHasTangents; int m_SpriteVertexStreamSize; int m_SpriteVertexCount; int m_SpriteTangentVertexOffset; int m_DataIndex = -1; bool m_BoneCacheUpdateToDate = false; Dictionary<int, List<TransformData>> m_HierarchyCache = new Dictionary<int, List<TransformData>>(); internal Sprite sprite => spriteRenderer.sprite; internal SpriteRenderer spriteRenderer => m_SpriteRenderer; internal NativeCustomSlice<BoneWeight> spriteBoneWeights => m_SpriteBoneWeights; /// <summary> /// 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. /// </summary> public bool autoRebind { get => m_AutoRebind; set { m_AutoRebind = value; CacheHierarchy(); CacheCurrentSprite(m_AutoRebind); } } /// <summary> /// Returns the Transform Components that is used for deformation. /// Do not modify elements of the returned array. /// </summary> /// <returns>An array of Transform Components.</returns> public Transform[] boneTransforms { get => m_BoneTransforms; internal set { m_BoneTransforms = value; CacheValidFlag(); OnBoneTransformChanged(); } } /// <summary> /// Returns the Transform Component that represents the root bone for deformation. /// </summary> /// <returns>A Transform Component.</returns> public Transform rootBone { get => m_RootBone; internal set { m_RootBone = value; CacheValidFlag(); CacheHierarchy(); OnRootBoneTransformChanged(); } } internal Bounds bounds { get => m_Bounds; set => m_Bounds = value; } /// <summary> /// Determines if the SpriteSkin executes even if the associated /// SpriteRenderer has been culled from view. /// </summary> 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<SpriteRenderer>(); } 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() { if (enabled) CacheBoneTransformIds(true); } void OnRootBoneTransformChanged() { if (enabled) CacheBoneTransformIds(true); } /// <summary> /// Called before object is serialized. /// </summary> public void OnBeforeSerialize() { OnBeforeSerializeBatch(); } /// <summary> /// Called after object is deserialized. /// </summary> 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<int>(boneCount, Allocator.Persistent); } m_BoneTransformIdNativeSlice = new NativeCustomSlice<int>(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<int>(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; } /// <summary> /// Returns whether this SpriteSkin has currently deformed vertices. /// </summary> /// <returns>Returns true if this SpriteSkin has currently deformed vertices. Returns false otherwise.</returns> public bool HasCurrentDeformedVertices() { if (!m_IsValid) return false; return m_DataIndex >= 0 && SpriteSkinComposite.instance.HasDeformableBufferForSprite(m_DataIndex); } /// <summary> /// Gets a byte array to the currently deformed vertices for this SpriteSkin. /// </summary> /// <returns>Returns a reference to the currently deformed vertices. This is valid only for this calling frame.</returns> /// <exception cref="InvalidOperationException"> /// Thrown when there are no currently deformed vertices. /// HasCurrentDeformedVertices can be used to verify if there are any deformed vertices available. /// </exception> internal NativeArray<byte> 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); } /// <summary> /// Gets an array of currently deformed position vertices for this SpriteSkin. /// </summary> /// <returns>Returns a reference to the currently deformed vertices. This is valid only for this calling frame.</returns> /// <exception cref="InvalidOperationException"> /// 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. /// </exception> internal NativeSlice<PositionVertex> 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<PositionVertex>(); } /// <summary> /// Gets an array of currently deformed position and tangent vertices for this SpriteSkin. /// </summary> /// <returns> /// Returns a reference to the currently deformed position and tangent vertices. This is valid only for this calling frame. /// </returns> /// <exception cref="InvalidOperationException"> /// 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. /// </exception> internal NativeSlice<PositionTangentVertex> 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<PositionTangentVertex>(); } /// <summary> /// Gets an enumerable to iterate through all deformed vertex positions of this SpriteSkin. /// </summary> /// <returns>Returns an IEnumerable to deformed vertex positions.</returns> /// <exception cref="InvalidOperationException"> /// Thrown when there is no vertex positions or deformed vertices. /// HasCurrentDeformedVertices can be used to verify if there are any deformed vertices available. /// </exception> public IEnumerable<Vector3> 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<Vector3>(rawSlice, sprite.GetVertexCount(), sprite.GetVertexStreamSize()); } /// <summary> /// Gets an enumerable to iterate through all deformed vertex tangents of this SpriteSkin. /// </summary> /// <returns>Returns an IEnumerable to deformed vertex tangents.</returns> /// <exception cref="InvalidOperationException"> /// Thrown when there is no vertex tangents or deformed vertices. /// HasCurrentDeformedVertices can be used to verify if there are any deformed vertices available. /// </exception> public IEnumerable<Vector4> 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<Vector4>(rawSlice, sprite.GetVertexCount(), sprite.GetVertexStreamSize()); } void OnDisable() { DeactivateSkinning(); BufferManager.instance.ReturnBuffer(GetInstanceID()); OnDisableBatch(); } /// <summary> /// Used by the animation clip preview window. /// Recommended to not use outside of this purpose. /// </summary> public void OnPreviewUpdate() { #if UNITY_EDITOR if(IsInGUIUpdateLoop()) Deform(); #endif } static bool IsInGUIUpdateLoop() => Event.current != null; void Deform() { CacheCurrentSprite(m_AutoRebind); if (isValid && this.enabled && (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<Vector2>.Default(); m_SpriteVertices = NativeCustomSlice<Vector3>.Default(); m_SpriteTangents = NativeCustomSlice<Vector4>.Default(); m_SpriteBoneWeights = NativeCustomSlice<BoneWeight>.Default(); m_SpriteBindPoses = NativeCustomSlice<Matrix4x4>.Default(); m_SpriteHasTangents = false; m_SpriteVertexStreamSize = 0; m_SpriteVertexCount = 0; m_SpriteTangentVertexOffset = 0; } else { m_SpriteUVs = new NativeCustomSlice<Vector2>(sprite.GetVertexAttribute<Vector2>(VertexAttribute.TexCoord0)); m_SpriteVertices = new NativeCustomSlice<Vector3>(sprite.GetVertexAttribute<Vector3>(VertexAttribute.Position)); m_SpriteTangents = new NativeCustomSlice<Vector4>(sprite.GetVertexAttribute<Vector4>(VertexAttribute.Tangent)); m_SpriteBoneWeights = new NativeCustomSlice<BoneWeight>(sprite.GetVertexAttribute<BoneWeight>(VertexAttribute.BlendWeight)); m_SpriteBindPoses = new NativeCustomSlice<Matrix4x4>(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<Vector2>(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; m_HierarchyCache.EnsureCapacity(rootBone.hierarchyCount); 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<int, List<TransformData>> 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<TransformData>(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<Bone>(); 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<int, List<TransformData>> 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() { var currentSprite = spriteRenderer.sprite; if (currentSprite != null) InternalEngineBridge.SetLocalAABB(spriteRenderer, currentSprite.bounds); spriteRenderer.DeactivateDeformableBuffer(); m_TransformsHash = 0; } internal void ResetSprite() { m_CurrentDeformSprite = 0; CacheValidFlag(); } } }