فهرست منبع

[unity] SkeletonGraphic allows custom mesh offset relative to the pivot to keep e.g. the face centered when layout scale downscales the mesh towards the pivot. Closes #2482.

Harald Csaszar 1 سال پیش
والد
کامیت
ff07a01aef

+ 1 - 0
CHANGELOG.md

@@ -159,6 +159,7 @@
   - `SkeletonGraphicRenderTexture` example component now also received a `quadMaterial` property, defaulting to the newly added Material asset `RenderQuadGraphicMaterial` which applies proper premultiplied-alpha blending of the render texture. The `quadMaterial` member variable was moved from `SkeletonRenderTexture` to the common base class `SkeletonRenderTextureBase`.
   - All Spine Outline shaders, including the URP outline shader, now provide an additional parameter `Width in Screen Space`. Enable it to keep the outline width constant in screen space instead of texture space. Requires more expensive computations, so enable only where necessary. Defaults to `disabled` to maintain existing behaviour.
   - Added support for BlendModeMaterials at runtime instantiation from files via an additional method `SkeletonDataAsset.SetupRuntimeBlendModeMaterials`. See example scene `Spine Examples/Other Examples/Instantiate from Script` for a usage example.
+  - SkeletonGraphic: You can now offset the skeleton mesh relative to the pivot via a newly added green circle handle. This allows you to e.g. frame only the face of a skeleton inside a masked frame. Previously offsetting the pivot downwards fails when `Layout Scale Mode` scales the mesh smaller and towards the pivot (e.g. the feet) and thus out of the frame. Now you can keep the pivot in the center of the `RectTransform` while offsetting only the mesh downwards, keeping the desired skeleton area (e.g. the face) centered while resizing. Moving the new larger green circle handle moves the mesh offset, while moving the blue pivot circle handle moves the pivot as usual.
 
 - **Breaking changes**
 

+ 6 - 4
spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/BoneFollowerGraphicInspector.cs

@@ -100,17 +100,19 @@ namespace Spine.Unity.Editor {
 			Transform transform = skeletonGraphicComponent.transform;
 			Skeleton skeleton = skeletonGraphicComponent.Skeleton;
 			float positionScale = skeletonGraphicComponent.MeshScale;
+			Vector2 positionOffset = skeletonGraphicComponent.GetScaledPivotOffset();
 
 			if (string.IsNullOrEmpty(boneName.stringValue)) {
-				SpineHandles.DrawBones(transform, skeleton, positionScale);
-				SpineHandles.DrawBoneNames(transform, skeleton, positionScale);
+				SpineHandles.DrawBones(transform, skeleton, positionScale, positionOffset);
+				SpineHandles.DrawBoneNames(transform, skeleton, positionScale, positionOffset);
 				Handles.Label(tbf.transform.position, "No bone selected", EditorStyles.helpBox);
 			} else {
 				Bone targetBone = tbf.bone;
 				if (targetBone == null) return;
 
-				SpineHandles.DrawBoneWireframe(transform, targetBone, SpineHandles.TransformContraintColor, positionScale);
-				Handles.Label(targetBone.GetWorldPosition(transform, positionScale), targetBone.Data.Name, SpineHandles.BoneNameStyle);
+				SpineHandles.DrawBoneWireframe(transform, targetBone, SpineHandles.TransformContraintColor, positionScale, positionOffset);
+				Handles.Label(targetBone.GetWorldPosition(transform, positionScale, positionOffset),
+					targetBone.Data.Name, SpineHandles.BoneNameStyle);
 			}
 		}
 

+ 9 - 5
spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonGraphicInspector.cs

@@ -552,12 +552,16 @@ namespace Spine.Unity.Editor {
 
 		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);
+
+			if (skeletonGraphic.layoutScaleMode != SkeletonGraphic.LayoutMode.None) {
+				if (skeletonGraphic.EditReferenceRect) {
+					SpineHandles.DrawRectTransformRect(skeletonGraphic, Color.gray);
+					SpineHandles.DrawReferenceRect(skeletonGraphic, Color.green);
+				} else {
+					SpineHandles.DrawReferenceRect(skeletonGraphic, Color.blue);
+				}
 			}
+			SpineHandles.DrawPivotOffsetHandle(skeletonGraphic, Color.green);
 		}
 
 		public static void SetSeparatorSlotNames (SkeletonRenderer skeletonRenderer, string[] newSlotNames) {

+ 62 - 22
spine-unity/Assets/Spine/Editor/spine-unity/Editor/Utility/SpineHandles.cs

@@ -161,27 +161,32 @@ namespace Spine.Unity.Editor {
 			}
 		}
 
-		public static void DrawBoneNames (Transform transform, Skeleton skeleton, float positionScale = 1f) {
+		public static void DrawBoneNames (Transform transform, Skeleton skeleton, float positionScale = 1f,
+			Vector2? positionOffset = null) {
 			if (UnityEngine.Event.current.type != EventType.Repaint) return;
 
+			Vector2 offset = positionOffset == null ? Vector2.zero : positionOffset.Value;
 			GUIStyle style = BoneNameStyle;
 			foreach (Bone b in skeleton.Bones) {
 				if (!b.Active) continue;
-				Vector3 pos = new Vector3(b.WorldX * positionScale, b.WorldY * positionScale, 0) + (new Vector3(b.A, b.C) * (b.Data.Length * 0.5f));
+				Vector3 pos = new Vector3(b.WorldX * positionScale + offset.x, b.WorldY * positionScale + offset.y, 0)
+					+ (new Vector3(b.A, b.C) * (b.Data.Length * 0.5f));
 				pos = transform.TransformPoint(pos);
 				Handles.Label(pos, b.Data.Name, style);
 			}
 		}
 
-		public static void DrawBones (Transform transform, Skeleton skeleton, float positionScale = 1f) {
+		public static void DrawBones (Transform transform, Skeleton skeleton, float positionScale = 1f,
+			Vector2? positionOffset = null) {
 			if (UnityEngine.Event.current.type != EventType.Repaint) return;
 
+			Vector2 offset = positionOffset == null ? Vector2.zero : positionOffset.Value;
 			float boneScale = 1.8f; // Draw the root bone largest;
-			DrawCrosshairs2D(skeleton.Bones.Items[0].GetWorldPosition(transform), 0.08f, positionScale);
+			DrawCrosshairs2D(skeleton.Bones.Items[0].GetWorldPosition(transform, positionScale, offset), 0.08f, positionScale);
 
 			foreach (Bone b in skeleton.Bones) {
 				if (!b.Active) continue;
-				DrawBone(transform, b, boneScale, positionScale);
+				DrawBone(transform, b, boneScale, positionScale, positionOffset);
 				boneScale = 1f;
 			}
 		}
@@ -194,11 +199,13 @@ namespace Spine.Unity.Editor {
 			_boneWireBuffer[4] = _boneWireBuffer[0]; // closed polygon.
 			return _boneWireBuffer;
 		}
-		public static void DrawBoneWireframe (Transform transform, Bone b, Color color, float skeletonRenderScale = 1f) {
+		public static void DrawBoneWireframe (Transform transform, Bone b, Color color, float skeletonRenderScale = 1f,
+			Vector2? positionOffset = null) {
 			if (UnityEngine.Event.current.type != EventType.Repaint) return;
 
+			Vector2 offset = positionOffset == null ? Vector2.zero : positionOffset.Value;
 			Handles.color = color;
-			Vector3 pos = new Vector3(b.WorldX * skeletonRenderScale, b.WorldY * skeletonRenderScale, 0);
+			Vector3 pos = new Vector3(b.WorldX * skeletonRenderScale + offset.x, b.WorldY * skeletonRenderScale + offset.y, 0);
 			float length = b.Data.Length;
 
 			if (length > 0) {
@@ -216,10 +223,12 @@ namespace Spine.Unity.Editor {
 			}
 		}
 
-		public static void DrawBone (Transform transform, Bone b, float boneScale, float skeletonRenderScale = 1f) {
+		public static void DrawBone (Transform transform, Bone b, float boneScale, float skeletonRenderScale = 1f,
+			Vector2? positionOffset = null) {
 			if (UnityEngine.Event.current.type != EventType.Repaint) return;
 
-			Vector3 pos = new Vector3(b.WorldX * skeletonRenderScale, b.WorldY * skeletonRenderScale, 0);
+			Vector2 offset = positionOffset == null ? Vector2.zero : positionOffset.Value;
+			Vector3 pos = new Vector3(b.WorldX * skeletonRenderScale + offset.x, b.WorldY * skeletonRenderScale + offset.y, 0);
 			float length = b.Data.Length;
 			if (length > 0) {
 				Quaternion rot = Quaternion.Euler(0, 0, b.WorldRotationX);
@@ -235,10 +244,12 @@ namespace Spine.Unity.Editor {
 			}
 		}
 
-		public static void DrawBone (Transform transform, Bone b, float boneScale, Color color, float skeletonRenderScale = 1f) {
+		public static void DrawBone (Transform transform, Bone b, float boneScale, Color color, float skeletonRenderScale = 1f,
+			Vector2? positionOffset = null) {
 			if (UnityEngine.Event.current.type != EventType.Repaint) return;
 
-			Vector3 pos = new Vector3(b.WorldX * skeletonRenderScale, b.WorldY * skeletonRenderScale, 0);
+			Vector2 offset = positionOffset == null ? Vector2.zero : positionOffset.Value;
+			Vector3 pos = new Vector3(b.WorldX * skeletonRenderScale + offset.x, b.WorldY * skeletonRenderScale + offset.y, 0);
 			float length = b.Data.Length;
 			if (length > 0) {
 				Quaternion rot = Quaternion.Euler(0, 0, b.WorldRotationX);
@@ -367,9 +378,11 @@ namespace Spine.Unity.Editor {
 			DrawArrowhead(skeletonTransform.localToWorldMatrix * m);
 		}
 
-		public static void DrawConstraints (Transform transform, Skeleton skeleton, float skeletonRenderScale = 1f) {
+		public static void DrawConstraints (Transform transform, Skeleton skeleton, float skeletonRenderScale = 1f,
+			Vector2? positionOffset = null) {
 			if (UnityEngine.Event.current.type != EventType.Repaint) return;
 
+			Vector2 offset = positionOffset == null ? Vector2.zero : positionOffset.Value;
 			Vector3 targetPos;
 			Vector3 pos;
 			bool active;
@@ -381,14 +394,14 @@ namespace Spine.Unity.Editor {
 			handleColor = SpineHandles.TransformContraintColor;
 			foreach (TransformConstraint tc in skeleton.TransformConstraints) {
 				Bone targetBone = tc.Target;
-				targetPos = targetBone.GetWorldPosition(transform, skeletonRenderScale);
+				targetPos = targetBone.GetWorldPosition(transform, skeletonRenderScale, offset);
 
 				if (tc.MixX > 0 || tc.MixY > 0) {
 					if ((tc.MixX > 0 && tc.MixX != 1f) ||
 						(tc.MixY > 0 && tc.MixY != 1f)) {
 						Handles.color = handleColor;
 						foreach (Bone b in tc.Bones) {
-							pos = b.GetWorldPosition(transform, skeletonRenderScale);
+							pos = b.GetWorldPosition(transform, skeletonRenderScale, offset);
 							Handles.DrawDottedLine(targetPos, pos, Thickness);
 						}
 					}
@@ -402,25 +415,25 @@ namespace Spine.Unity.Editor {
 			handleColor = SpineHandles.IkColor;
 			foreach (IkConstraint ikc in skeleton.IkConstraints) {
 				Bone targetBone = ikc.Target;
-				targetPos = targetBone.GetWorldPosition(transform, skeletonRenderScale);
+				targetPos = targetBone.GetWorldPosition(transform, skeletonRenderScale, offset);
 				ExposedList<Bone> bones = ikc.Bones;
 				active = ikc.Mix > 0;
 				if (active) {
-					pos = bones.Items[0].GetWorldPosition(transform, skeletonRenderScale);
+					pos = bones.Items[0].GetWorldPosition(transform, skeletonRenderScale, offset);
 					switch (bones.Count) {
 					case 1: {
 						Handles.color = handleColor;
 						Handles.DrawLine(targetPos, pos);
 						SpineHandles.DrawBoneCircle(targetPos, handleColor, normal);
 						Matrix4x4 m = bones.Items[0].GetMatrix4x4();
-						m.m03 = targetBone.WorldX * skeletonRenderScale;
-						m.m13 = targetBone.WorldY * skeletonRenderScale;
+						m.m03 = targetBone.WorldX * skeletonRenderScale + offset.x;
+						m.m13 = targetBone.WorldY * skeletonRenderScale + offset.y;
 						SpineHandles.DrawArrowhead(transform.localToWorldMatrix * m);
 						break;
 					}
 					case 2: {
 						Bone childBone = bones.Items[1];
-						Vector3 child = childBone.GetWorldPosition(transform, skeletonRenderScale);
+						Vector3 child = childBone.GetWorldPosition(transform, skeletonRenderScale, offset);
 						Handles.color = handleColor;
 						Handles.DrawLine(child, pos);
 						Handles.DrawLine(targetPos, child);
@@ -428,8 +441,8 @@ namespace Spine.Unity.Editor {
 						SpineHandles.DrawBoneCircle(child, handleColor, normal, 0.5f);
 						SpineHandles.DrawBoneCircle(targetPos, handleColor, normal);
 						Matrix4x4 m = childBone.GetMatrix4x4();
-						m.m03 = targetBone.WorldX * skeletonRenderScale;
-						m.m13 = targetBone.WorldY * skeletonRenderScale;
+						m.m03 = targetBone.WorldX * skeletonRenderScale + offset.x;
+						m.m13 = targetBone.WorldY * skeletonRenderScale + offset.y;
 						SpineHandles.DrawArrowhead(transform.localToWorldMatrix * m);
 						break;
 					}
@@ -444,7 +457,8 @@ namespace Spine.Unity.Editor {
 				active = pc.MixX > 0 || pc.MixY > 0 || pc.MixRotate > 0;
 				if (active)
 					foreach (Bone b in pc.Bones)
-						SpineHandles.DrawBoneCircle(b.GetWorldPosition(transform, skeletonRenderScale), handleColor, normal, 1f * skeletonRenderScale);
+						SpineHandles.DrawBoneCircle(b.GetWorldPosition(transform, skeletonRenderScale, offset),
+							handleColor, normal, 1f * skeletonRenderScale);
 			}
 		}
 
@@ -453,6 +467,7 @@ namespace Spine.Unity.Editor {
 
 			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);
@@ -466,6 +481,7 @@ namespace Spine.Unity.Editor {
 
 			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);
@@ -490,6 +506,30 @@ namespace Spine.Unity.Editor {
 			UnityEditor.Handles.color = previousColor;
 		}
 
+		public static void DrawPivotOffsetHandle (SkeletonGraphic skeletonGraphic, Color color) {
+			// Note: not limiting to current.type == EventType.Repaint because the FreeMoveHandle requires interaction.
+
+			float handleSize = HandleUtility.GetHandleSize(skeletonGraphic.transform.position);
+			float controlSize = handleSize * 0.3f;
+			float discSize = handleSize * 0.03f;
+			Vector3 snap = Vector3.zero;
+			Color savedColor = Handles.color;
+
+			Handles.color = color;
+			Vector2 scaledOffset = skeletonGraphic.GetScaledPivotOffset();
+			Vector3 worldSpaceOffset = skeletonGraphic.transform.TransformPoint(scaledOffset);
+			EditorGUI.BeginChangeCheck();
+			Vector3 newWorldSpacePosition = Handles.FreeMoveHandle(worldSpaceOffset, controlSize, snap, Handles.CircleHandleCap);
+			if (EditorGUI.EndChangeCheck()) {
+				Undo.RecordObject(skeletonGraphic, "Change Offset to Pivot");
+				Vector3 localScaledOffset = skeletonGraphic.transform.InverseTransformPoint(newWorldSpacePosition);
+				skeletonGraphic.SetScaledPivotOffset(localScaledOffset);
+				skeletonGraphic.UpdateMeshToInstructions();
+			}
+			Handles.DrawSolidDisc(newWorldSpacePosition, skeletonGraphic.transform.forward, discSize);
+			Handles.color = savedColor;
+		}
+
 		static void DrawCrosshairs2D (Vector3 position, float scale, float skeletonRenderScale = 1f) {
 			if (UnityEngine.Event.current.type != EventType.Repaint) return;
 

+ 5 - 3
spine-unity/Assets/Spine/Runtime/spine-unity/Components/Following/BoneFollowerGraphic.cs

@@ -140,17 +140,19 @@ namespace Spine.Unity {
 			if (thisTransform == null) return;
 
 			float scale = skeletonGraphic.MeshScale;
+			Vector2 offset = skeletonGraphic.MeshOffset;
 
 			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,
-														followXYPosition ? bone.WorldY * scale : thisTransform.localPosition.y,
+				thisTransform.localPosition = new Vector3(followXYPosition ? bone.WorldX * scale + offset.x : thisTransform.localPosition.x,
+														followXYPosition ? bone.WorldY * scale + offset.y : thisTransform.localPosition.y,
 														followZPosition ? 0f : thisTransform.localPosition.z);
 				if (followBoneRotation) thisTransform.localRotation = bone.GetQuaternion();
 			} else {
 				// For special cases: Use transform world properties if transform relationship is complicated
-				Vector3 targetWorldPosition = skeletonTransform.TransformPoint(new Vector3(bone.WorldX * scale, bone.WorldY * scale, 0f));
+				Vector3 targetWorldPosition = skeletonTransform.TransformPoint(
+					new Vector3(bone.WorldX * scale + offset.x, bone.WorldY * scale + offset.y, 0f));
 				if (!followZPosition) targetWorldPosition.z = thisTransform.position.z;
 				if (!followXYPosition) {
 					targetWorldPosition.x = thisTransform.position.x;

+ 59 - 23
spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonGraphic.cs

@@ -77,7 +77,9 @@ namespace Spine.Unity {
 		public float timeScale = 1f;
 		public bool freeze;
 		protected float meshScale = 1f;
+		protected Vector2 meshOffset = Vector2.zero;
 		public float MeshScale { get { return meshScale; } }
+		public Vector2 MeshOffset { get { return meshOffset; } }
 
 		public enum LayoutMode {
 			None = 0,
@@ -88,6 +90,8 @@ namespace Spine.Unity {
 		}
 		public LayoutMode layoutScaleMode = LayoutMode.None;
 		[SerializeField] protected Vector2 referenceSize = Vector2.one;
+		/// <summary>Offset relative to the pivot position, before potential layout scale is applied.</summary>
+		[SerializeField] protected Vector2 pivotOffset = Vector2.zero;
 		[SerializeField] protected float referenceScale = 1f;
 #if UNITY_EDITOR
 		protected LayoutMode previousLayoutScaleMode = LayoutMode.None;
@@ -939,10 +943,19 @@ namespace Spine.Unity {
 			meshScale = (canvas == null) ? 100 : canvas.referencePixelsPerUnit;
 			if (layoutScaleMode != LayoutMode.None) {
 				meshScale *= referenceScale;
-				if (!EditReferenceRect)
-					meshScale *= GetLayoutScale(layoutScaleMode);
+				float layoutScale = GetLayoutScale(layoutScaleMode);
+				if (!EditReferenceRect) {
+					meshScale *= layoutScale;
+				}
+				meshOffset = pivotOffset * layoutScale;
+			} else {
+				meshOffset = pivotOffset;
 			}
-			meshGenerator.ScaleVertexData(meshScale);
+			if (meshOffset == Vector2.zero)
+				meshGenerator.ScaleVertexData(meshScale);
+			else
+				meshGenerator.ScaleAndOffsetVertexData(meshScale, meshOffset);
+
 			if (OnPostProcessVertices != null) OnPostProcessVertices.Invoke(this.meshGenerator.Buffers);
 
 			Mesh mesh = smartMesh.mesh;
@@ -1030,8 +1043,13 @@ namespace Spine.Unity {
 			meshScale = (canvas == null) ? 100 : canvas.referencePixelsPerUnit;
 			if (layoutScaleMode != LayoutMode.None) {
 				meshScale *= referenceScale;
-				if (!EditReferenceRect)
-					meshScale *= GetLayoutScale(layoutScaleMode);
+				float layoutScale = GetLayoutScale(layoutScaleMode);
+				if (!EditReferenceRect) {
+					meshScale *= layoutScale;
+				}
+				meshOffset = pivotOffset * layoutScale;
+			} else {
+				meshOffset = pivotOffset;
 			}
 			// Generate meshes.
 			int submeshCount = currentInstructions.submeshInstructions.Count;
@@ -1052,7 +1070,10 @@ namespace Spine.Unity {
 				meshGenerator.AddSubmesh(submeshInstructionItem);
 
 				Mesh targetMesh = meshesItems[i];
-				meshGenerator.ScaleVertexData(meshScale);
+				if (meshOffset == Vector2.zero)
+					meshGenerator.ScaleVertexData(meshScale);
+				else
+					meshGenerator.ScaleAndOffsetVertexData(meshScale, meshOffset);
 				if (OnPostProcessVertices != null) OnPostProcessVertices.Invoke(this.meshGenerator.Buffers);
 				meshGenerator.FillVertexData(targetMesh);
 				meshGenerator.FillTriangles(targetMesh);
@@ -1351,9 +1372,9 @@ namespace Spine.Unity {
 					SetRectTransformSize(this, rectTransformSize);
 				}
 			}
-			if (editReferenceRect || layoutScaleMode == LayoutMode.None) {
+			if (editReferenceRect || layoutScaleMode == LayoutMode.None)
 				referenceSize = GetCurrentRectSize();
-			}
+
 			previousLayoutScaleMode = layoutScaleMode;
 		}
 
@@ -1374,13 +1395,7 @@ namespace Spine.Unity {
 			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;
-
+			LayoutMode mode = GetEffectiveLayoutMode(previousLayoutScaleMode);
 			if (mode == LayoutMode.WidthControlsHeight)
 				newSize.y = newSize.x / referenceAspect;
 			else if (mode == LayoutMode.HeightControlsWidth)
@@ -1391,17 +1406,22 @@ namespace Spine.Unity {
 		public Vector2 GetReferenceRectSize () {
 			return referenceSize * GetLayoutScale(layoutScaleMode);
 		}
-#endif
 
+		public Vector2 GetPivotOffset () {
+			return pivotOffset;
+		}
+
+		public Vector2 GetScaledPivotOffset () {
+			return pivotOffset * GetLayoutScale(layoutScaleMode);
+		}
+
+		public void SetScaledPivotOffset (Vector2 pivotOffsetScaled) {
+			pivotOffset = pivotOffsetScaled / 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;
-
+			mode = GetEffectiveLayoutMode(mode);
 			if (mode == LayoutMode.WidthControlsHeight) {
 				return currentSize.x / referenceSize.x;
 			} else if (mode == LayoutMode.HeightControlsWidth) {
@@ -1410,6 +1430,22 @@ namespace Spine.Unity {
 			return 1f;
 		}
 
+		/// <summary>
+		/// <c>LayoutMode FitInParent</c> and <c>EnvelopeParent</c> actually result in
+		/// <c>HeightControlsWidth</c> or <c>WidthControlsHeight</c> depending on the actual vs reference aspect ratio.
+		/// This method returns the respective <c>LayoutMode</c> of the two for any given input <c>mode</c>.
+		/// </summary>
+		protected LayoutMode GetEffectiveLayoutMode (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;
+			return mode;
+		}
+
 		private Vector2 GetCurrentRectSize () {
 			return this.rectTransform.rect.size;
 		}

+ 41 - 3
spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonUtility/SkeletonUtility.cs

@@ -159,6 +159,38 @@ namespace Spine.Unity {
 
 			if (skeletonGraphic != null) {
 				positionScale = skeletonGraphic.MeshScale;
+				lastPositionScale = positionScale;
+				if (boneRoot) {
+					positionOffset = skeletonGraphic.MeshOffset;
+					if (positionOffset != Vector2.zero) {
+						boneRoot.localPosition = positionOffset;
+					}
+				}
+			}
+		}
+
+		void UpdateToMeshScaleAndOffset (MeshGeneratorBuffers ignoredParameter) {
+			if (skeletonGraphic == null) return;
+
+			positionScale = skeletonGraphic.MeshScale;
+			if (boneRoot) {
+				positionOffset = skeletonGraphic.MeshOffset;
+				if (positionOffset != Vector2.zero) {
+					boneRoot.localPosition = positionOffset;
+				}
+			}
+
+			// Note: skeletonGraphic.MeshScale and MeshOffset can be one frame behind in Update() above.
+			// Unfortunately update order is:
+			// 1. SkeletonGraphic.Update updating skeleton bones and calling UpdateWorld callback,
+			//    calling SkeletonUtilityBone.DoUpdate() reading hierarchy.PositionScale.
+			// 2. Layout change triggers SkeletonGraphic.Rebuild, updating MeshScale and MeshOffset.
+			// Thus to prevent a one-frame-behind offset after a layout change affecting mesh scale,
+			// we have to re-evaluate the callbacks via the lines below.
+			if (lastPositionScale != positionScale) {
+				UpdateLocal(skeletonAnimation);
+				UpdateWorld(skeletonAnimation);
+				UpdateComplete(skeletonAnimation);
 			}
 		}
 
@@ -170,7 +202,6 @@ namespace Spine.Unity {
 		[System.NonSerialized] public List<SkeletonUtilityBone> boneComponents = new List<SkeletonUtilityBone>();
 		[System.NonSerialized] public List<SkeletonUtilityConstraint> constraintComponents = new List<SkeletonUtilityConstraint>();
 
-
 		public ISkeletonComponent SkeletonComponent {
 			get {
 				if (skeletonComponent == null) {
@@ -197,8 +228,11 @@ namespace Spine.Unity {
 		}
 
 		public float PositionScale { get { return positionScale; } }
+		public Vector2 PositionOffset { get { return positionOffset; } }
 
 		float positionScale = 1.0f;
+		float lastPositionScale = 1.0f;
+		Vector2 positionOffset = Vector2.zero;
 		bool hasOverrideBones;
 		bool hasConstraints;
 		bool needToReprocessBones;
@@ -232,6 +266,8 @@ namespace Spine.Unity {
 			} else if (skeletonGraphic != null) {
 				skeletonGraphic.OnRebuild -= HandleRendererReset;
 				skeletonGraphic.OnRebuild += HandleRendererReset;
+				skeletonGraphic.OnPostProcessVertices -= UpdateToMeshScaleAndOffset;
+				skeletonGraphic.OnPostProcessVertices += UpdateToMeshScaleAndOffset;
 			}
 
 			if (skeletonAnimation != null) {
@@ -250,8 +286,10 @@ namespace Spine.Unity {
 		void OnDisable () {
 			if (skeletonRenderer != null)
 				skeletonRenderer.OnRebuild -= HandleRendererReset;
-			if (skeletonGraphic != null)
+			if (skeletonGraphic != null) {
 				skeletonGraphic.OnRebuild -= HandleRendererReset;
+				skeletonGraphic.OnPostProcessVertices -= UpdateToMeshScaleAndOffset;
+			}
 
 			if (skeletonAnimation != null) {
 				skeletonAnimation.UpdateLocal -= UpdateLocal;
@@ -449,7 +487,7 @@ namespace Spine.Unity {
 
 			if (mode == SkeletonUtilityBone.Mode.Override) {
 				if (rot) goTransform.localRotation = Quaternion.Euler(0, 0, b.bone.AppliedRotation);
-				if (pos) goTransform.localPosition = new Vector3(b.bone.X * positionScale, b.bone.Y * positionScale, 0);
+				if (pos) goTransform.localPosition = new Vector3(b.bone.X * positionScale + positionOffset.x, b.bone.Y * positionScale + positionOffset.y, 0);
 				goTransform.localScale = new Vector3(b.bone.ScaleX, b.bone.ScaleY, 0);
 			}
 

+ 14 - 0
spine-unity/Assets/Spine/Runtime/spine-unity/Mesh Generation/MeshGenerator.cs

@@ -1065,6 +1065,20 @@ namespace Spine.Unity {
 			meshBoundsThickness *= scale;
 		}
 
+		public void ScaleAndOffsetVertexData (float scale, Vector2 offset2D) {
+			Vector3 offset = new Vector3(offset2D.x, offset2D.y);
+			Vector3[] vbi = vertexBuffer.Items;
+			for (int i = 0, n = vertexBuffer.Count; i < n; i++) {
+				vbi[i] = vbi[i] * scale + offset;
+			}
+
+			meshBoundsMin *= scale;
+			meshBoundsMax *= scale;
+			meshBoundsMin += offset2D;
+			meshBoundsMax += offset2D;
+			meshBoundsThickness *= scale;
+		}
+
 		public Bounds GetMeshBounds () {
 			if (float.IsInfinity(meshBoundsMin.x)) { // meshBoundsMin.x == BoundsMinDefault // == doesn't work on float Infinity constants.
 				return new Bounds();

+ 4 - 0
spine-unity/Assets/Spine/Runtime/spine-unity/Utility/SkeletonExtensions.cs

@@ -157,6 +157,10 @@ namespace Spine.Unity {
 			return spineGameObjectTransform.TransformPoint(new Vector3(bone.WorldX * positionScale, bone.WorldY * positionScale));
 		}
 
+		public static Vector3 GetWorldPosition (this Bone bone, UnityEngine.Transform spineGameObjectTransform, float positionScale, Vector2 positionOffset) {
+			return spineGameObjectTransform.TransformPoint(new Vector3(bone.WorldX * positionScale + positionOffset.x, bone.WorldY * positionScale + positionOffset.y));
+		}
+
 		/// <summary>Gets a skeleton space UnityEngine.Quaternion representation of bone.WorldRotationX.</summary>
 		public static Quaternion GetQuaternion (this Bone bone) {
 			float halfRotation = Mathf.Atan2(bone.C, bone.A) * 0.5f;

+ 1 - 1
spine-unity/Assets/Spine/package.json

@@ -2,7 +2,7 @@
 	"name": "com.esotericsoftware.spine.spine-unity",
 	"displayName": "spine-unity Runtime",
 	"description": "This plugin provides the spine-unity runtime core.",
-	"version": "4.2.69",
+	"version": "4.2.70",
 	"unity": "2018.3",
 	"author": {
 		"name": "Esoteric Software",