diff --git a/CHANGELOG.md b/CHANGELOG.md index 31b5bf4c2..bd78804f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -381,6 +381,7 @@ * Now all URP (Universal Render Pipeline) and LWRP (Lightweight Render Pipeline) shaders support SRP (Scriptable Render Pipeline) batching. See [Unity SRPBatcher documentation pages](https://docs.unity3d.com/Manual/SRPBatcher.html) for additional information. * Sprite shaders now provide four `Diffuse Ramp` modes as an Inspector Material parameter: `Hard`, `Soft`, `Old Hard` and `Old Soft`. In spine-unity 3.8 it defaults to `Old Hard` to keep the behaviour of existing projects unchanged. From 4.0 on it defaults to `Hard` for newly created materials while existing ones remain unchanged. Note that `Old Hard` and `Old Soft` ramp versions were using only the right half of the ramp texture, and additionally multiplying the light intensity by 2, both leading to brighter lighting than without a ramp texture active. The new ramp modes `Hard` and `Soft` use the full ramp texture and do not modify light intensity, being consistent with lighting without a ramp texture active. * Added **native support for slot blend modes** `Additive`, `Multiply` and `Screen` with automatic assignment at newly imported skeleton assets. `BlendModeMaterialAssets` are now obsolete and replaced by the native properties at `SkeletonDataAsset`. The `SkeletonDataAsset` Inspector provides a new `Blend Modes - Upgrade` button to upgrade an obsolete `BlendModeMaterialAsset` to the native blend modes properties. This upgrade will be performed automatically on imported and re-imported assets in Unity 2020.1 and newer to prevent reported `BlendModeMaterialAsset` issues in these Unity versions. spine-unity 4.0 and newer will automatically perform this upgrade regardless of the Unity version. + * `BoneFollower` and `BoneFollowerGraphic` components now provide better support for following bones when the skeleton's Transform is not the parent of the follower's Transform. Previously e.g. rotating a common parent Transform did not lead to the desired result, as well as negatively scaling a skeleton's Transform when it is not a parent of the follower's Transform. * **Changes of default values** * `SkeletonMecanim`'s `Layer Mix Mode` now defaults to `MixMode.MixNext` instead of `MixMode.MixAlways`. diff --git a/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/BoneFollowerGraphicInspector.cs b/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/BoneFollowerGraphicInspector.cs index 53570bb09..1603385f6 100644 --- a/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/BoneFollowerGraphicInspector.cs +++ b/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/BoneFollowerGraphicInspector.cs @@ -39,7 +39,8 @@ namespace Spine.Unity.Editor { [CustomEditor(typeof(BoneFollowerGraphic)), CanEditMultipleObjects] public class BoneFollowerGraphicInspector : Editor { - SerializedProperty boneName, skeletonGraphic, followXYPosition, followZPosition, followBoneRotation, followLocalScale, followSkeletonFlip; + SerializedProperty boneName, skeletonGraphic, followXYPosition, followZPosition, followBoneRotation, + followLocalScale, followSkeletonFlip, maintainedAxisOrientation; BoneFollowerGraphic targetBoneFollower; bool needsReset; @@ -77,6 +78,7 @@ namespace Spine.Unity.Editor { followZPosition = serializedObject.FindProperty("followZPosition"); followLocalScale = serializedObject.FindProperty("followLocalScale"); followSkeletonFlip = serializedObject.FindProperty("followSkeletonFlip"); + maintainedAxisOrientation = serializedObject.FindProperty("maintainedAxisOrientation"); targetBoneFollower = (BoneFollowerGraphic)target; if (targetBoneFollower.SkeletonGraphic != null) @@ -171,6 +173,11 @@ namespace Spine.Unity.Editor { EditorGUILayout.PropertyField(followZPosition); EditorGUILayout.PropertyField(followLocalScale); EditorGUILayout.PropertyField(followSkeletonFlip); + if ((followSkeletonFlip.hasMultipleDifferentValues || followSkeletonFlip.boolValue == false) && + (followBoneRotation.hasMultipleDifferentValues || followBoneRotation.boolValue == true)) { + using (new SpineInspectorUtility.IndentScope()) + EditorGUILayout.PropertyField(maintainedAxisOrientation); + } //BoneFollowerInspector.RecommendRigidbodyButton(targetBoneFollower); } else { diff --git a/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/BoneFollowerInspector.cs b/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/BoneFollowerInspector.cs index 5416762bb..4221e4c0b 100644 --- a/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/BoneFollowerInspector.cs +++ b/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/BoneFollowerInspector.cs @@ -37,7 +37,8 @@ namespace Spine.Unity.Editor { [CustomEditor(typeof(BoneFollower)), CanEditMultipleObjects] public class BoneFollowerInspector : Editor { - SerializedProperty boneName, skeletonRenderer, followXYPosition, followZPosition, followBoneRotation, followScale, followScaleMode, followSkeletonFlip; + SerializedProperty boneName, skeletonRenderer, followXYPosition, followZPosition, followBoneRotation, + followScale, followScaleMode, followSkeletonFlip, maintainedAxisOrientation; BoneFollower targetBoneFollower; bool needsReset; @@ -87,6 +88,7 @@ namespace Spine.Unity.Editor { followScale = serializedObject.FindProperty("followScale"); followScaleMode = serializedObject.FindProperty("followScaleMode"); followSkeletonFlip = serializedObject.FindProperty("followSkeletonFlip"); + maintainedAxisOrientation = serializedObject.FindProperty("maintainedAxisOrientation"); targetBoneFollower = (BoneFollower)target; if (targetBoneFollower.SkeletonRenderer != null) @@ -183,6 +185,11 @@ namespace Spine.Unity.Editor { } } EditorGUILayout.PropertyField(followSkeletonFlip); + if ((followSkeletonFlip.hasMultipleDifferentValues || followSkeletonFlip.boolValue == false) && + (followBoneRotation.hasMultipleDifferentValues || followBoneRotation.boolValue == true)) { + using (new SpineInspectorUtility.IndentScope()) + EditorGUILayout.PropertyField(maintainedAxisOrientation); + } BoneFollowerInspector.RecommendRigidbodyButton(targetBoneFollower); } else { diff --git a/spine-unity/Assets/Spine/Runtime/spine-unity/Components/Following/BoneFollower.cs b/spine-unity/Assets/Spine/Runtime/spine-unity/Components/Following/BoneFollower.cs index 82fabebc2..5df128479 100644 --- a/spine-unity/Assets/Spine/Runtime/spine-unity/Components/Following/BoneFollower.cs +++ b/spine-unity/Assets/Spine/Runtime/spine-unity/Components/Following/BoneFollower.cs @@ -79,6 +79,16 @@ namespace Spine.Unity { [Tooltip("Follows the target bone's local or uniform world scale. Note: If world scale is non-uniform/skewed, you will receive incorrect results with WorldUniform.")] public ScaleMode followScaleMode = ScaleMode.Local; + public enum AxisOrientation { + XAxis = 1, + YAxis + } + [Tooltip("Applies when 'Follow Skeleton Flip' is disabled but 'Follow Bone Rotation' is enabled." + + " When flipping the skeleton by scaling its Transform, this follower's rotation is adjusted" + + " instead of its scale to follow the bone orientation. When one of the axes is flipped, " + + " only one axis can be followed, either the X or the Y axis, which is selected here.")] + public AxisOrientation maintainedAxisOrientation = AxisOrientation.XAxis; + [UnityEngine.Serialization.FormerlySerializedAs("resetOnAwake")] public bool initializeOnAwake = true; #endregion @@ -151,6 +161,7 @@ namespace Spine.Unity { } Transform thisTransform = this.transform; + float additionalFlipScale = 1; if (skeletonTransformIsParent) { // Recommended setup: Use local transform properties if Spine GameObject is the immediate parent thisTransform.localPosition = new Vector3(followXYPosition ? bone.worldX : thisTransform.localPosition.x, @@ -178,16 +189,24 @@ namespace Spine.Unity { targetWorldPosition.y = thisTransform.position.y; } - float boneWorldRotation = bone.WorldRotationX; - + Vector3 skeletonLossyScale = skeletonTransform.lossyScale; Transform transformParent = thisTransform.parent; - if (transformParent != null) { - Matrix4x4 m = transformParent.localToWorldMatrix; - if (m.m00 * m.m11 - m.m01 * m.m10 < 0) // Determinant2D is negative - boneWorldRotation = -boneWorldRotation; - } - + Vector3 parentLossyScale = transformParent != null ? transformParent.lossyScale : Vector3.one; if (followBoneRotation) { + float boneWorldRotation = bone.WorldRotationX; + + if ((skeletonLossyScale.x * skeletonLossyScale.y) < 0) + boneWorldRotation = -boneWorldRotation; + + if (followSkeletonFlip || maintainedAxisOrientation == AxisOrientation.XAxis) { + if ((skeletonLossyScale.x * parentLossyScale.x < 0)) + boneWorldRotation += 180f; + } + else { + if ((skeletonLossyScale.y * parentLossyScale.y < 0)) + boneWorldRotation += 180f; + } + Vector3 worldRotation = skeletonTransform.rotation.eulerAngles; if (followScale && (followScaleMode == ScaleMode.Local ? @@ -197,13 +216,18 @@ namespace Spine.Unity { } else { thisTransform.position = targetWorldPosition; } + + additionalFlipScale = Mathf.Sign(skeletonLossyScale.x * parentLossyScale.x + * skeletonLossyScale.y * parentLossyScale.y); } Vector3 localScale = followScale ? (followScaleMode == ScaleMode.Local ? new Vector3(bone.scaleX, bone.scaleY, 1f) : new Vector3(bone.WorldScaleX, bone.WorldScaleY, 1f)) : new Vector3(1f, 1f, 1f); - if (followSkeletonFlip) localScale.y *= Mathf.Sign(bone.skeleton.ScaleX * bone.skeleton.ScaleY); + if (followSkeletonFlip) + localScale.y *= Mathf.Sign(bone.skeleton.ScaleX * bone.skeleton.ScaleY) * additionalFlipScale; + thisTransform.localScale = localScale; } } diff --git a/spine-unity/Assets/Spine/Runtime/spine-unity/Components/Following/BoneFollowerGraphic.cs b/spine-unity/Assets/Spine/Runtime/spine-unity/Components/Following/BoneFollowerGraphic.cs index 54bdb8b7d..6d5ea7bef 100644 --- a/spine-unity/Assets/Spine/Runtime/spine-unity/Components/Following/BoneFollowerGraphic.cs +++ b/spine-unity/Assets/Spine/Runtime/spine-unity/Components/Following/BoneFollowerGraphic.cs @@ -33,7 +33,9 @@ using UnityEngine; + namespace Spine.Unity { + using AxisOrientation = BoneFollower.AxisOrientation; #if NEW_PREFAB_SYSTEM [ExecuteAlways] @@ -66,6 +68,11 @@ namespace Spine.Unity { public bool followLocalScale = false; public bool followXYPosition = true; public bool followZPosition = true; + [Tooltip("Applies when 'Follow Skeleton Flip' is disabled but 'Follow Bone Rotation' is enabled." + + " When flipping the skeleton by scaling its Transform, this follower's rotation is adjusted" + + " instead of its scale to follow the bone orientation. When one of the axes is flipped, " + + " only one axis can be followed, either the X or the Y axis, which is selected here.")] + public AxisOrientation maintainedAxisOrientation = AxisOrientation.XAxis; [System.NonSerialized] public Bone bone; @@ -134,6 +141,7 @@ namespace Spine.Unity { if (canvas == null) canvas = skeletonGraphic.GetComponentInParent(); float scale = canvas != null ? canvas.referencePixelsPerUnit : 100.0f; + float additionalFlipScale = 1; if (skeletonTransformIsParent) { // Recommended setup: Use local transform properties if Spine GameObject is the immediate parent thisTransform.localPosition = new Vector3(followXYPosition ? bone.worldX * scale : thisTransform.localPosition.x, @@ -149,25 +157,38 @@ namespace Spine.Unity { targetWorldPosition.y = thisTransform.position.y; } - float boneWorldRotation = bone.WorldRotationX; - + Vector3 skeletonLossyScale = skeletonTransform.lossyScale; Transform transformParent = thisTransform.parent; - if (transformParent != null) { - Matrix4x4 m = transformParent.localToWorldMatrix; - if (m.m00 * m.m11 - m.m01 * m.m10 < 0) // Determinant2D is negative - boneWorldRotation = -boneWorldRotation; - } - + Vector3 parentLossyScale = transformParent != null ? transformParent.lossyScale : Vector3.one; if (followBoneRotation) { + float boneWorldRotation = bone.WorldRotationX; + + if ((skeletonLossyScale.x * skeletonLossyScale.y) < 0) + boneWorldRotation = -boneWorldRotation; + + if (followSkeletonFlip || maintainedAxisOrientation == AxisOrientation.XAxis) { + if ((skeletonLossyScale.x * parentLossyScale.x < 0)) + boneWorldRotation += 180f; + } + else { + if ((skeletonLossyScale.y * parentLossyScale.y < 0)) + boneWorldRotation += 180f; + } + Vector3 worldRotation = skeletonTransform.rotation.eulerAngles; - thisTransform.SetPositionAndRotation(targetWorldPosition, Quaternion.Euler(worldRotation.x, worldRotation.y, skeletonTransform.rotation.eulerAngles.z + boneWorldRotation)); + if (followLocalScale && bone.scaleX < 0) boneWorldRotation += 180f; + thisTransform.SetPositionAndRotation(targetWorldPosition, Quaternion.Euler(worldRotation.x, worldRotation.y, worldRotation.z + boneWorldRotation)); } else { thisTransform.position = targetWorldPosition; } + + additionalFlipScale = Mathf.Sign(skeletonLossyScale.x * parentLossyScale.x + * skeletonLossyScale.y * parentLossyScale.y); } Vector3 localScale = followLocalScale ? new Vector3(bone.scaleX, bone.scaleY, 1f) : new Vector3(1f, 1f, 1f); - if (followSkeletonFlip) localScale.y *= Mathf.Sign(bone.skeleton.ScaleX * bone.skeleton.ScaleY); + if (followSkeletonFlip) + localScale.y *= Mathf.Sign(bone.skeleton.ScaleX * bone.skeleton.ScaleY) * additionalFlipScale; thisTransform.localScale = localScale; }