From 91cc6ddfaa51dea0b6ae013b9d767d96233eafc5 Mon Sep 17 00:00:00 2001 From: Harald Csaszar Date: Fri, 24 Mar 2023 14:43:23 +0100 Subject: [PATCH] [unity] SkeletonGraphic now supports automatic scaling based on RectTransform bounds. Closes #1640. --- CHANGELOG.md | 1 + .../Components/SkeletonGraphicInspector.cs | 57 +++++-- .../Editor/Utility/SpineHandles.cs | 36 ++++ .../spine-unity/Components/SkeletonGraphic.cs | 159 ++++++++++++++++-- 4 files changed, 228 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 18da19f89..1186bc31f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -97,6 +97,7 @@ * `SkeletonRenderTexture` and `SkeletonGraphicRenderTexture` components now support automatic down-scaling when required size on screen exceeds `Max Render Texture Size`. * Added `Spine/SkeletonGraphic Fill` shader to provide functionality of `Spine/Skeleton Fill` shader for `SkeletonGraphic`. * Lit Spine URP shaders (`Universal Render Pipeline/Spine/Sprite` and `Universal Render Pipeline/Spine/Skeleton Lit`) now support `Forward+` rendering path as introduced by Unity 2022.2 and URP version 14. + * `SkeletonGraphic` now supports automatic scaling based on its `RectTransform` bounds. Automatic scaling can be enabled by setting the added `Layout Scale Mode` Inspector property to either `Width Controls Height`, `Height Controls Width`, `FitInParent` or `EnvelopeParent`. It is set to `None` by default to keep previous behaviour and avoid breaking existing projects. To modify the reference layout bounds, hit the additional `Edit Layout Bounds` toggle button to switch into edit mode, adjust the bounds or hit `Match RectTransform with Mesh`, and hit the button again when done adjusting. The skeleton will now be scaled accordingly to fit the reference layout bounds to the object's `RectTransform`. * **Breaking changes** * Made `SkeletonGraphic.unscaledTime` parameter protected, use the new property `UnscaledTime` instead. diff --git a/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonGraphicInspector.cs b/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonGraphicInspector.cs index 7e7153387..aaf574906 100644 --- a/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonGraphicInspector.cs +++ b/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonGraphicInspector.cs @@ -57,7 +57,7 @@ namespace Spine.Unity.Editor { SerializedProperty additiveMaterial, multiplyMaterial, screenMaterial; SerializedProperty skeletonDataAsset, initialSkinName; SerializedProperty startingAnimation, startingLoop, timeScale, freeze, - updateTiming, updateWhenInvisible, unscaledTime, tintBlack; + updateTiming, updateWhenInvisible, unscaledTime, tintBlack, layoutScaleMode, editReferenceRect; SerializedProperty initialFlipX, initialFlipY; SerializedProperty meshGeneratorSettings; SerializedProperty allowMultipleCanvasRenderers, separatorSlotNames, enableSeparatorSlots, updateSeparatorPartLocation; @@ -76,8 +76,8 @@ namespace Spine.Unity.Editor { protected bool TargetIsValid { get { if (serializedObject.isEditingMultipleObjects) { - foreach (UnityEngine.Object o in targets) { - SkeletonGraphic component = (SkeletonGraphic)o; + foreach (UnityEngine.Object c in targets) { + SkeletonGraphic component = (SkeletonGraphic)c; if (!component.IsValid) return false; } @@ -129,6 +129,8 @@ namespace Spine.Unity.Editor { freeze = so.FindProperty("freeze"); updateTiming = so.FindProperty("updateTiming"); updateWhenInvisible = so.FindProperty("updateWhenInvisible"); + layoutScaleMode = so.FindProperty("layoutScaleMode"); + editReferenceRect = so.FindProperty("editReferenceRect"); meshGeneratorSettings = so.FindProperty("meshGenerator").FindPropertyRelative("settings"); meshGeneratorSettings.isExpanded = SkeletonRendererInspector.advancedFoldout; @@ -141,6 +143,13 @@ namespace Spine.Unity.Editor { separatorSlotNames.isExpanded = true; } + void OnDisable () { + foreach (UnityEngine.Object c in targets) { + SkeletonGraphic component = (SkeletonGraphic)c; + component.EditReferenceRect = false; + } + } + public override void OnInspectorGUI () { if (UnityEngine.Event.current.type == EventType.Layout) { @@ -299,14 +308,29 @@ namespace Spine.Unity.Editor { EditorGUILayout.PropertyField(raycastTarget); if (maskable != null) EditorGUILayout.PropertyField(maskable); - EditorGUILayout.BeginHorizontal(GUILayout.Height(EditorGUIUtility.singleLineHeight + 5)); - EditorGUILayout.PrefixLabel("Match RectTransform with Mesh"); - if (GUILayout.Button("Match", EditorStyles.miniButton, GUILayout.Width(65f))) { - foreach (UnityEngine.Object skeletonGraphic in targets) { - MatchRectTransformWithBounds((SkeletonGraphic)skeletonGraphic); - } + EditorGUILayout.PropertyField(layoutScaleMode); + + using (new EditorGUI.DisabledGroupScope(layoutScaleMode.intValue == 0)) { + EditorGUILayout.BeginHorizontal(GUILayout.Height(EditorGUIUtility.singleLineHeight + 5)); + EditorGUILayout.PrefixLabel("Edit Layout Bounds"); + editReferenceRect.boolValue = GUILayout.Toggle(editReferenceRect.boolValue, + EditorGUIUtility.IconContent("EditCollider"), EditorStyles.miniButton, GUILayout.Width(40f)); + EditorGUILayout.EndHorizontal(); + } + if (layoutScaleMode.intValue == 0) { + editReferenceRect.boolValue = false; + } + + using (new EditorGUI.DisabledGroupScope(editReferenceRect.boolValue == false && layoutScaleMode.intValue != 0)) { + EditorGUILayout.BeginHorizontal(GUILayout.Height(EditorGUIUtility.singleLineHeight + 5)); + EditorGUILayout.PrefixLabel("Match RectTransform with Mesh"); + if (GUILayout.Button("Match", EditorStyles.miniButton, GUILayout.Width(65f))) { + foreach (UnityEngine.Object skeletonGraphic in targets) { + MatchRectTransformWithBounds((SkeletonGraphic)skeletonGraphic); + } + } + EditorGUILayout.EndHorizontal(); } - EditorGUILayout.EndHorizontal(); if (TargetIsValid && !isInspectingPrefab) { EditorGUILayout.Space(); @@ -320,7 +344,6 @@ namespace Spine.Unity.Editor { } wasChanged |= EditorGUI.EndChangeCheck(); - if (wasChanged) { serializedObject.ApplyModifiedProperties(); slotsReapplyRequired = true; @@ -346,6 +369,18 @@ namespace Spine.Unity.Editor { return false; } + protected void OnSceneGUI () { + SkeletonGraphic skeletonGraphic = (SkeletonGraphic)target; + if (skeletonGraphic.EditReferenceRect) { + SpineHandles.DrawRectTransformRect(skeletonGraphic, Color.gray); + SpineHandles.DrawReferenceRect(skeletonGraphic, Color.green); + } else { + SpineHandles.DrawReferenceRect(skeletonGraphic, Color.blue); + } + + + } + protected void AssignDefaultBlendModeMaterials () { foreach (UnityEngine.Object target in targets) { SkeletonGraphic skeletonGraphic = (SkeletonGraphic)target; diff --git a/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Utility/SpineHandles.cs b/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Utility/SpineHandles.cs index d936d4bfb..ac2810063 100644 --- a/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Utility/SpineHandles.cs +++ b/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Utility/SpineHandles.cs @@ -424,6 +424,42 @@ namespace Spine.Unity.Editor { } } + public static void DrawReferenceRect (SkeletonGraphic skeletonGraphic, Color color) { + RectTransform rectTransform = skeletonGraphic.rectTransform; + Vector2 referenceRectSize = skeletonGraphic.GetReferenceRectSize(); + Vector3 position = rectTransform.position; + Vector3 right = rectTransform.TransformVector(Vector3.right * referenceRectSize.x); + Vector3 up = rectTransform.TransformVector(Vector3.up * referenceRectSize.y); + + Vector3 cornerVertexBL = position - rectTransform.pivot.x * right - rectTransform.pivot.y * up; + DrawRect(cornerVertexBL, right, up, color); + } + + public static void DrawRectTransformRect (SkeletonGraphic skeletonGraphic, Color color) { + RectTransform rectTransform = skeletonGraphic.rectTransform; + Vector2 rectTransformSize = skeletonGraphic.RectTransformSize; + Vector3 position = rectTransform.position; + Vector3 right = rectTransform.TransformVector(Vector3.right * rectTransformSize.x); + Vector3 up = rectTransform.TransformVector(Vector3.up * rectTransformSize.y); + + Vector3 cornerVertexBL = position - rectTransform.pivot.x * right - rectTransform.pivot.y * up; + DrawRect(cornerVertexBL, right, up, color); + } + + public static void DrawRect (Vector3 cornerVertexBL, Vector3 right, Vector3 up, Color color) { + Vector3 v0 = cornerVertexBL; + Vector3 v1 = v0 + right; + Vector3 v2 = v0 + right + up; + Vector3 v3 = v0 + up; + Color previousColor = UnityEditor.Handles.color; + UnityEditor.Handles.color = color; + UnityEditor.Handles.DrawLine(v0, v1); + UnityEditor.Handles.DrawLine(v1, v2); + UnityEditor.Handles.DrawLine(v2, v3); + UnityEditor.Handles.DrawLine(v3, v0); + UnityEditor.Handles.color = previousColor; + } + static void DrawCrosshairs2D (Vector3 position, float scale, float skeletonRenderScale = 1f) { scale *= SpineEditorUtilities.Preferences.handleScale * skeletonRenderScale; Handles.DrawLine(position + new Vector3(-scale, 0), position + new Vector3(scale, 0)); diff --git a/spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonGraphic.cs b/spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonGraphic.cs index db3f12930..1040b6000 100644 --- a/spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonGraphic.cs +++ b/spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonGraphic.cs @@ -68,8 +68,29 @@ namespace Spine.Unity { public float timeScale = 1f; public bool freeze; - /// Update mode to optionally limit updates to e.g. only apply animations but not update the mesh. - public UpdateMode UpdateMode { get { return updateMode; } set { updateMode = value; } } + public enum LayoutMode { + None = 0, + WidthControlsHeight, + HeightControlsWidth, + FitInParent, + EnvelopeParent + } + public LayoutMode layoutScaleMode = LayoutMode.None; + [SerializeField] protected Vector2 referenceSize = Vector2.one; + [SerializeField] protected float referenceScale = 1f; +#if UNITY_EDITOR + protected LayoutMode previousLayoutScaleMode = LayoutMode.None; + [SerializeField] protected Vector2 rectTransformSize = Vector2.zero; + [SerializeField] protected bool editReferenceRect = false; + protected bool previousEditReferenceRect = false; + + public bool EditReferenceRect { get { return editReferenceRect; } set { editReferenceRect = value; } } + public Vector2 RectTransformSize { get { return rectTransformSize; } } +#else + protected const bool EditReferenceRect = false; +#endif +/// Update mode to optionally limit updates to e.g. only apply animations but not update the mesh. +public UpdateMode UpdateMode { get { return updateMode; } set { updateMode = value; } } protected UpdateMode updateMode = UpdateMode.FullUpdate; /// Update mode used when the MeshRenderer becomes invisible @@ -273,6 +294,10 @@ namespace Spine.Unity { Initialize(false); Rebuild(CanvasUpdate.PreRender); } + +#if UNITY_EDITOR + InitLayoutScaleParameters(); +#endif } protected override void OnDestroy () { @@ -297,6 +322,7 @@ namespace Spine.Unity { public virtual void Update () { #if UNITY_EDITOR if (!Application.isPlaying) { + UpdateReferenceRectSizes(); Update(0f); return; } @@ -316,7 +342,6 @@ namespace Spine.Unity { wasUpdatedAfterInit = true; if (updateMode < UpdateMode.OnlyAnimationStatus) return; - UpdateAnimationStatus(deltaTime); if (updateMode == UpdateMode.OnlyAnimationStatus) { @@ -547,15 +572,26 @@ namespace Spine.Unity { 0.5f - (center.y / size.y) ); - this.rectTransform.sizeDelta = size; + SetRectTransformSize(this, size); this.rectTransform.pivot = p; foreach (SkeletonSubmeshGraphic submeshGraphic in submeshGraphics) { - submeshGraphic.rectTransform.sizeDelta = size; + SetRectTransformSize(submeshGraphic, size); submeshGraphic.rectTransform.pivot = p; } } + public static void SetRectTransformSize (Graphic target, Vector2 size) { + Vector2 parentSize = Vector2.zero; + if (target.rectTransform.parent != null) { + RectTransform parentTransform = target.rectTransform.parent.GetComponent(); + if (parentTransform) + parentSize = parentTransform.rect.size; + } + Vector2 anchorAreaSize = target.rectTransform.anchorMax * parentSize - target.rectTransform.anchorMin * parentSize; + target.rectTransform.sizeDelta = size - anchorAreaSize; + } + /// OnAnimationRebuild is raised after the SkeletonAnimation component is successfully initialized. public event ISkeletonAnimationDelegate OnAnimationRebuild; public event UpdateBonesDelegate BeforeApply; @@ -731,7 +767,13 @@ namespace Spine.Unity { meshGenerator.BuildMeshWithArrays(currentInstructions, updateTriangles); } - if (canvas != null) meshGenerator.ScaleVertexData(canvas.referencePixelsPerUnit); + float scale = (canvas == null) ? 100 : canvas.referencePixelsPerUnit; + if (layoutScaleMode != LayoutMode.None) { + scale *= referenceScale; + if (!EditReferenceRect) + scale *= GetLayoutScale(layoutScaleMode); + } + meshGenerator.ScaleVertexData(scale); if (OnPostProcessVertices != null) OnPostProcessVertices.Invoke(this.meshGenerator.Buffers); Mesh mesh = smartMesh.mesh; @@ -803,9 +845,12 @@ namespace Spine.Unity { } protected void UpdateMeshMultipleCanvasRenderers (SkeletonRendererInstruction currentInstructions) { - Canvas c = canvas; - float scale = (c == null) ? 100 : c.referencePixelsPerUnit; - + float scale = (canvas == null) ? 100 : canvas.referencePixelsPerUnit; + if (layoutScaleMode != LayoutMode.None) { + scale *= referenceScale; + if (!EditReferenceRect) + scale *= GetLayoutScale(layoutScaleMode); + } // Generate meshes. int submeshCount = currentInstructions.submeshInstructions.Count; Mesh[] meshesItems = meshes.Items; @@ -893,14 +938,19 @@ namespace Spine.Unity { for (int i = 0; i < submeshCount; i++) { CanvasRenderer canvasRenderer = canvasRenderers[i]; - if (i >= usedRenderersCount) { + if (i >= usedRenderersCount) canvasRenderer.gameObject.SetActive(true); - } - if (canvasRenderer.transform.parent != parent.transform) { + + if (canvasRenderer.transform.parent != parent.transform) canvasRenderer.transform.SetParent(parent.transform, false); - canvasRenderer.transform.localPosition = Vector3.zero; - } + canvasRenderer.transform.SetSiblingIndex(targetSiblingIndex++); + RectTransform dstTransform = submeshGraphics[i].rectTransform; + dstTransform.localPosition = Vector3.zero; + dstTransform.pivot = rectTransform.pivot; + dstTransform.anchorMin = Vector2.zero; + dstTransform.anchorMax = Vector2.one; + dstTransform.sizeDelta = Vector2.zero; SubmeshInstruction submeshInstructionItem = currentInstructions.submeshInstructions.Items[i]; if (submeshInstructionItem.forceSeparate) { @@ -1027,6 +1077,87 @@ namespace Spine.Unity { } } } + + protected void InitLayoutScaleParameters () { + previousLayoutScaleMode = layoutScaleMode; + } + + protected void UpdateReferenceRectSizes () { + if (rectTransformSize == Vector2.zero) + rectTransformSize = GetCurrentRectSize(); + + HandleChangedEditReferenceRect(); + + if (layoutScaleMode != previousLayoutScaleMode) { + if (layoutScaleMode != LayoutMode.None) { + SetRectTransformSize(this, rectTransformSize); + } else { + rectTransformSize = referenceSize / referenceScale; + referenceScale = 1f; + SetRectTransformSize(this, rectTransformSize); + } + } + if (editReferenceRect || layoutScaleMode == LayoutMode.None) { + referenceSize = GetCurrentRectSize(); + } + previousLayoutScaleMode = layoutScaleMode; + } + + protected void HandleChangedEditReferenceRect () { + if (editReferenceRect == previousEditReferenceRect) return; + previousEditReferenceRect = editReferenceRect; + + if (editReferenceRect) { + rectTransformSize = GetCurrentRectSize(); + ResetRectToReferenceRectSize(); + } else { + SetRectTransformSize(this, rectTransformSize); + } + } + + public void ResetRectToReferenceRectSize () { + referenceScale = referenceScale * GetLayoutScale(previousLayoutScaleMode); + float referenceAspect = referenceSize.x / referenceSize.y; + Vector2 newSize = GetCurrentRectSize(); + + LayoutMode mode = previousLayoutScaleMode; + float frameAspect = newSize.x / newSize.y; + if (mode == LayoutMode.FitInParent) + mode = frameAspect > referenceAspect ? LayoutMode.HeightControlsWidth : LayoutMode.WidthControlsHeight; + else if (mode == LayoutMode.EnvelopeParent) + mode = frameAspect > referenceAspect ? LayoutMode.WidthControlsHeight : LayoutMode.HeightControlsWidth; + + if (mode == LayoutMode.WidthControlsHeight) + newSize.y = newSize.x / referenceAspect; + else if (mode == LayoutMode.HeightControlsWidth) + newSize.x = newSize.y * referenceAspect; + SetRectTransformSize(this, newSize); + } + + public Vector2 GetReferenceRectSize () { + return referenceSize * GetLayoutScale(layoutScaleMode); + } #endif + + protected float GetLayoutScale (LayoutMode mode) { + Vector2 currentSize = GetCurrentRectSize(); + float referenceAspect = referenceSize.x / referenceSize.y; + float frameAspect = currentSize.x / currentSize.y; + if (mode == LayoutMode.FitInParent) + mode = frameAspect > referenceAspect ? LayoutMode.HeightControlsWidth : LayoutMode.WidthControlsHeight; + else if (mode == LayoutMode.EnvelopeParent) + mode = frameAspect > referenceAspect ? LayoutMode.WidthControlsHeight : LayoutMode.HeightControlsWidth; + + if (mode == LayoutMode.WidthControlsHeight) { + return currentSize.x / referenceSize.x; + } else if (mode == LayoutMode.HeightControlsWidth) { + return currentSize.y / referenceSize.y; + } + return 1f; + } + + private Vector2 GetCurrentRectSize () { + return this.rectTransform.rect.size; + } } }