Explorar o código

[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 hai 1 ano
pai
achega
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`.
   - `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.
   - 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.
   - 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**
 - **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;
 			Transform transform = skeletonGraphicComponent.transform;
 			Skeleton skeleton = skeletonGraphicComponent.Skeleton;
 			Skeleton skeleton = skeletonGraphicComponent.Skeleton;
 			float positionScale = skeletonGraphicComponent.MeshScale;
 			float positionScale = skeletonGraphicComponent.MeshScale;
+			Vector2 positionOffset = skeletonGraphicComponent.GetScaledPivotOffset();
 
 
 			if (string.IsNullOrEmpty(boneName.stringValue)) {
 			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);
 				Handles.Label(tbf.transform.position, "No bone selected", EditorStyles.helpBox);
 			} else {
 			} else {
 				Bone targetBone = tbf.bone;
 				Bone targetBone = tbf.bone;
 				if (targetBone == null) return;
 				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 () {
 		protected void OnSceneGUI () {
 			SkeletonGraphic skeletonGraphic = (SkeletonGraphic)target;
 			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) {
 		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;
 			if (UnityEngine.Event.current.type != EventType.Repaint) return;
 
 
+			Vector2 offset = positionOffset == null ? Vector2.zero : positionOffset.Value;
 			GUIStyle style = BoneNameStyle;
 			GUIStyle style = BoneNameStyle;
 			foreach (Bone b in skeleton.Bones) {
 			foreach (Bone b in skeleton.Bones) {
 				if (!b.Active) continue;
 				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);
 				pos = transform.TransformPoint(pos);
 				Handles.Label(pos, b.Data.Name, style);
 				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;
 			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;
 			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) {
 			foreach (Bone b in skeleton.Bones) {
 				if (!b.Active) continue;
 				if (!b.Active) continue;
-				DrawBone(transform, b, boneScale, positionScale);
+				DrawBone(transform, b, boneScale, positionScale, positionOffset);
 				boneScale = 1f;
 				boneScale = 1f;
 			}
 			}
 		}
 		}
@@ -194,11 +199,13 @@ namespace Spine.Unity.Editor {
 			_boneWireBuffer[4] = _boneWireBuffer[0]; // closed polygon.
 			_boneWireBuffer[4] = _boneWireBuffer[0]; // closed polygon.
 			return _boneWireBuffer;
 			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;
 			if (UnityEngine.Event.current.type != EventType.Repaint) return;
 
 
+			Vector2 offset = positionOffset == null ? Vector2.zero : positionOffset.Value;
 			Handles.color = color;
 			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;
 			float length = b.Data.Length;
 
 
 			if (length > 0) {
 			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;
 			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;
 			float length = b.Data.Length;
 			if (length > 0) {
 			if (length > 0) {
 				Quaternion rot = Quaternion.Euler(0, 0, b.WorldRotationX);
 				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;
 			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;
 			float length = b.Data.Length;
 			if (length > 0) {
 			if (length > 0) {
 				Quaternion rot = Quaternion.Euler(0, 0, b.WorldRotationX);
 				Quaternion rot = Quaternion.Euler(0, 0, b.WorldRotationX);
@@ -367,9 +378,11 @@ namespace Spine.Unity.Editor {
 			DrawArrowhead(skeletonTransform.localToWorldMatrix * m);
 			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;
 			if (UnityEngine.Event.current.type != EventType.Repaint) return;
 
 
+			Vector2 offset = positionOffset == null ? Vector2.zero : positionOffset.Value;
 			Vector3 targetPos;
 			Vector3 targetPos;
 			Vector3 pos;
 			Vector3 pos;
 			bool active;
 			bool active;
@@ -381,14 +394,14 @@ namespace Spine.Unity.Editor {
 			handleColor = SpineHandles.TransformContraintColor;
 			handleColor = SpineHandles.TransformContraintColor;
 			foreach (TransformConstraint tc in skeleton.TransformConstraints) {
 			foreach (TransformConstraint tc in skeleton.TransformConstraints) {
 				Bone targetBone = tc.Target;
 				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.MixY > 0) {
 					if ((tc.MixX > 0 && tc.MixX != 1f) ||
 					if ((tc.MixX > 0 && tc.MixX != 1f) ||
 						(tc.MixY > 0 && tc.MixY != 1f)) {
 						(tc.MixY > 0 && tc.MixY != 1f)) {
 						Handles.color = handleColor;
 						Handles.color = handleColor;
 						foreach (Bone b in tc.Bones) {
 						foreach (Bone b in tc.Bones) {
-							pos = b.GetWorldPosition(transform, skeletonRenderScale);
+							pos = b.GetWorldPosition(transform, skeletonRenderScale, offset);
 							Handles.DrawDottedLine(targetPos, pos, Thickness);
 							Handles.DrawDottedLine(targetPos, pos, Thickness);
 						}
 						}
 					}
 					}
@@ -402,25 +415,25 @@ namespace Spine.Unity.Editor {
 			handleColor = SpineHandles.IkColor;
 			handleColor = SpineHandles.IkColor;
 			foreach (IkConstraint ikc in skeleton.IkConstraints) {
 			foreach (IkConstraint ikc in skeleton.IkConstraints) {
 				Bone targetBone = ikc.Target;
 				Bone targetBone = ikc.Target;
-				targetPos = targetBone.GetWorldPosition(transform, skeletonRenderScale);
+				targetPos = targetBone.GetWorldPosition(transform, skeletonRenderScale, offset);
 				ExposedList<Bone> bones = ikc.Bones;
 				ExposedList<Bone> bones = ikc.Bones;
 				active = ikc.Mix > 0;
 				active = ikc.Mix > 0;
 				if (active) {
 				if (active) {
-					pos = bones.Items[0].GetWorldPosition(transform, skeletonRenderScale);
+					pos = bones.Items[0].GetWorldPosition(transform, skeletonRenderScale, offset);
 					switch (bones.Count) {
 					switch (bones.Count) {
 					case 1: {
 					case 1: {
 						Handles.color = handleColor;
 						Handles.color = handleColor;
 						Handles.DrawLine(targetPos, pos);
 						Handles.DrawLine(targetPos, pos);
 						SpineHandles.DrawBoneCircle(targetPos, handleColor, normal);
 						SpineHandles.DrawBoneCircle(targetPos, handleColor, normal);
 						Matrix4x4 m = bones.Items[0].GetMatrix4x4();
 						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);
 						SpineHandles.DrawArrowhead(transform.localToWorldMatrix * m);
 						break;
 						break;
 					}
 					}
 					case 2: {
 					case 2: {
 						Bone childBone = bones.Items[1];
 						Bone childBone = bones.Items[1];
-						Vector3 child = childBone.GetWorldPosition(transform, skeletonRenderScale);
+						Vector3 child = childBone.GetWorldPosition(transform, skeletonRenderScale, offset);
 						Handles.color = handleColor;
 						Handles.color = handleColor;
 						Handles.DrawLine(child, pos);
 						Handles.DrawLine(child, pos);
 						Handles.DrawLine(targetPos, child);
 						Handles.DrawLine(targetPos, child);
@@ -428,8 +441,8 @@ namespace Spine.Unity.Editor {
 						SpineHandles.DrawBoneCircle(child, handleColor, normal, 0.5f);
 						SpineHandles.DrawBoneCircle(child, handleColor, normal, 0.5f);
 						SpineHandles.DrawBoneCircle(targetPos, handleColor, normal);
 						SpineHandles.DrawBoneCircle(targetPos, handleColor, normal);
 						Matrix4x4 m = childBone.GetMatrix4x4();
 						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);
 						SpineHandles.DrawArrowhead(transform.localToWorldMatrix * m);
 						break;
 						break;
 					}
 					}
@@ -444,7 +457,8 @@ namespace Spine.Unity.Editor {
 				active = pc.MixX > 0 || pc.MixY > 0 || pc.MixRotate > 0;
 				active = pc.MixX > 0 || pc.MixY > 0 || pc.MixRotate > 0;
 				if (active)
 				if (active)
 					foreach (Bone b in pc.Bones)
 					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;
 			RectTransform rectTransform = skeletonGraphic.rectTransform;
 			Vector2 referenceRectSize = skeletonGraphic.GetReferenceRectSize();
 			Vector2 referenceRectSize = skeletonGraphic.GetReferenceRectSize();
+
 			Vector3 position = rectTransform.position;
 			Vector3 position = rectTransform.position;
 			Vector3 right = rectTransform.TransformVector(Vector3.right * referenceRectSize.x);
 			Vector3 right = rectTransform.TransformVector(Vector3.right * referenceRectSize.x);
 			Vector3 up = rectTransform.TransformVector(Vector3.up * referenceRectSize.y);
 			Vector3 up = rectTransform.TransformVector(Vector3.up * referenceRectSize.y);
@@ -466,6 +481,7 @@ namespace Spine.Unity.Editor {
 
 
 			RectTransform rectTransform = skeletonGraphic.rectTransform;
 			RectTransform rectTransform = skeletonGraphic.rectTransform;
 			Vector2 rectTransformSize = skeletonGraphic.RectTransformSize;
 			Vector2 rectTransformSize = skeletonGraphic.RectTransformSize;
+
 			Vector3 position = rectTransform.position;
 			Vector3 position = rectTransform.position;
 			Vector3 right = rectTransform.TransformVector(Vector3.right * rectTransformSize.x);
 			Vector3 right = rectTransform.TransformVector(Vector3.right * rectTransformSize.x);
 			Vector3 up = rectTransform.TransformVector(Vector3.up * rectTransformSize.y);
 			Vector3 up = rectTransform.TransformVector(Vector3.up * rectTransformSize.y);
@@ -490,6 +506,30 @@ namespace Spine.Unity.Editor {
 			UnityEditor.Handles.color = previousColor;
 			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) {
 		static void DrawCrosshairs2D (Vector3 position, float scale, float skeletonRenderScale = 1f) {
 			if (UnityEngine.Event.current.type != EventType.Repaint) return;
 			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;
 			if (thisTransform == null) return;
 
 
 			float scale = skeletonGraphic.MeshScale;
 			float scale = skeletonGraphic.MeshScale;
+			Vector2 offset = skeletonGraphic.MeshOffset;
 
 
 			float additionalFlipScale = 1;
 			float additionalFlipScale = 1;
 			if (skeletonTransformIsParent) {
 			if (skeletonTransformIsParent) {
 				// Recommended setup: Use local transform properties if Spine GameObject is the immediate parent
 				// 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);
 														followZPosition ? 0f : thisTransform.localPosition.z);
 				if (followBoneRotation) thisTransform.localRotation = bone.GetQuaternion();
 				if (followBoneRotation) thisTransform.localRotation = bone.GetQuaternion();
 			} else {
 			} else {
 				// For special cases: Use transform world properties if transform relationship is complicated
 				// 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 (!followZPosition) targetWorldPosition.z = thisTransform.position.z;
 				if (!followXYPosition) {
 				if (!followXYPosition) {
 					targetWorldPosition.x = thisTransform.position.x;
 					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 float timeScale = 1f;
 		public bool freeze;
 		public bool freeze;
 		protected float meshScale = 1f;
 		protected float meshScale = 1f;
+		protected Vector2 meshOffset = Vector2.zero;
 		public float MeshScale { get { return meshScale; } }
 		public float MeshScale { get { return meshScale; } }
+		public Vector2 MeshOffset { get { return meshOffset; } }
 
 
 		public enum LayoutMode {
 		public enum LayoutMode {
 			None = 0,
 			None = 0,
@@ -88,6 +90,8 @@ namespace Spine.Unity {
 		}
 		}
 		public LayoutMode layoutScaleMode = LayoutMode.None;
 		public LayoutMode layoutScaleMode = LayoutMode.None;
 		[SerializeField] protected Vector2 referenceSize = Vector2.one;
 		[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;
 		[SerializeField] protected float referenceScale = 1f;
 #if UNITY_EDITOR
 #if UNITY_EDITOR
 		protected LayoutMode previousLayoutScaleMode = LayoutMode.None;
 		protected LayoutMode previousLayoutScaleMode = LayoutMode.None;
@@ -939,10 +943,19 @@ namespace Spine.Unity {
 			meshScale = (canvas == null) ? 100 : canvas.referencePixelsPerUnit;
 			meshScale = (canvas == null) ? 100 : canvas.referencePixelsPerUnit;
 			if (layoutScaleMode != LayoutMode.None) {
 			if (layoutScaleMode != LayoutMode.None) {
 				meshScale *= referenceScale;
 				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);
 			if (OnPostProcessVertices != null) OnPostProcessVertices.Invoke(this.meshGenerator.Buffers);
 
 
 			Mesh mesh = smartMesh.mesh;
 			Mesh mesh = smartMesh.mesh;
@@ -1030,8 +1043,13 @@ namespace Spine.Unity {
 			meshScale = (canvas == null) ? 100 : canvas.referencePixelsPerUnit;
 			meshScale = (canvas == null) ? 100 : canvas.referencePixelsPerUnit;
 			if (layoutScaleMode != LayoutMode.None) {
 			if (layoutScaleMode != LayoutMode.None) {
 				meshScale *= referenceScale;
 				meshScale *= referenceScale;
-				if (!EditReferenceRect)
-					meshScale *= GetLayoutScale(layoutScaleMode);
+				float layoutScale = GetLayoutScale(layoutScaleMode);
+				if (!EditReferenceRect) {
+					meshScale *= layoutScale;
+				}
+				meshOffset = pivotOffset * layoutScale;
+			} else {
+				meshOffset = pivotOffset;
 			}
 			}
 			// Generate meshes.
 			// Generate meshes.
 			int submeshCount = currentInstructions.submeshInstructions.Count;
 			int submeshCount = currentInstructions.submeshInstructions.Count;
@@ -1052,7 +1070,10 @@ namespace Spine.Unity {
 				meshGenerator.AddSubmesh(submeshInstructionItem);
 				meshGenerator.AddSubmesh(submeshInstructionItem);
 
 
 				Mesh targetMesh = meshesItems[i];
 				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);
 				if (OnPostProcessVertices != null) OnPostProcessVertices.Invoke(this.meshGenerator.Buffers);
 				meshGenerator.FillVertexData(targetMesh);
 				meshGenerator.FillVertexData(targetMesh);
 				meshGenerator.FillTriangles(targetMesh);
 				meshGenerator.FillTriangles(targetMesh);
@@ -1351,9 +1372,9 @@ namespace Spine.Unity {
 					SetRectTransformSize(this, rectTransformSize);
 					SetRectTransformSize(this, rectTransformSize);
 				}
 				}
 			}
 			}
-			if (editReferenceRect || layoutScaleMode == LayoutMode.None) {
+			if (editReferenceRect || layoutScaleMode == LayoutMode.None)
 				referenceSize = GetCurrentRectSize();
 				referenceSize = GetCurrentRectSize();
-			}
+
 			previousLayoutScaleMode = layoutScaleMode;
 			previousLayoutScaleMode = layoutScaleMode;
 		}
 		}
 
 
@@ -1374,13 +1395,7 @@ namespace Spine.Unity {
 			float referenceAspect = referenceSize.x / referenceSize.y;
 			float referenceAspect = referenceSize.x / referenceSize.y;
 			Vector2 newSize = GetCurrentRectSize();
 			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)
 			if (mode == LayoutMode.WidthControlsHeight)
 				newSize.y = newSize.x / referenceAspect;
 				newSize.y = newSize.x / referenceAspect;
 			else if (mode == LayoutMode.HeightControlsWidth)
 			else if (mode == LayoutMode.HeightControlsWidth)
@@ -1391,17 +1406,22 @@ namespace Spine.Unity {
 		public Vector2 GetReferenceRectSize () {
 		public Vector2 GetReferenceRectSize () {
 			return referenceSize * GetLayoutScale(layoutScaleMode);
 			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) {
 		protected float GetLayoutScale (LayoutMode mode) {
 			Vector2 currentSize = GetCurrentRectSize();
 			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) {
 			if (mode == LayoutMode.WidthControlsHeight) {
 				return currentSize.x / referenceSize.x;
 				return currentSize.x / referenceSize.x;
 			} else if (mode == LayoutMode.HeightControlsWidth) {
 			} else if (mode == LayoutMode.HeightControlsWidth) {
@@ -1410,6 +1430,22 @@ namespace Spine.Unity {
 			return 1f;
 			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 () {
 		private Vector2 GetCurrentRectSize () {
 			return this.rectTransform.rect.size;
 			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) {
 			if (skeletonGraphic != null) {
 				positionScale = skeletonGraphic.MeshScale;
 				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<SkeletonUtilityBone> boneComponents = new List<SkeletonUtilityBone>();
 		[System.NonSerialized] public List<SkeletonUtilityConstraint> constraintComponents = new List<SkeletonUtilityConstraint>();
 		[System.NonSerialized] public List<SkeletonUtilityConstraint> constraintComponents = new List<SkeletonUtilityConstraint>();
 
 
-
 		public ISkeletonComponent SkeletonComponent {
 		public ISkeletonComponent SkeletonComponent {
 			get {
 			get {
 				if (skeletonComponent == null) {
 				if (skeletonComponent == null) {
@@ -197,8 +228,11 @@ namespace Spine.Unity {
 		}
 		}
 
 
 		public float PositionScale { get { return positionScale; } }
 		public float PositionScale { get { return positionScale; } }
+		public Vector2 PositionOffset { get { return positionOffset; } }
 
 
 		float positionScale = 1.0f;
 		float positionScale = 1.0f;
+		float lastPositionScale = 1.0f;
+		Vector2 positionOffset = Vector2.zero;
 		bool hasOverrideBones;
 		bool hasOverrideBones;
 		bool hasConstraints;
 		bool hasConstraints;
 		bool needToReprocessBones;
 		bool needToReprocessBones;
@@ -232,6 +266,8 @@ namespace Spine.Unity {
 			} else if (skeletonGraphic != null) {
 			} else if (skeletonGraphic != null) {
 				skeletonGraphic.OnRebuild -= HandleRendererReset;
 				skeletonGraphic.OnRebuild -= HandleRendererReset;
 				skeletonGraphic.OnRebuild += HandleRendererReset;
 				skeletonGraphic.OnRebuild += HandleRendererReset;
+				skeletonGraphic.OnPostProcessVertices -= UpdateToMeshScaleAndOffset;
+				skeletonGraphic.OnPostProcessVertices += UpdateToMeshScaleAndOffset;
 			}
 			}
 
 
 			if (skeletonAnimation != null) {
 			if (skeletonAnimation != null) {
@@ -250,8 +286,10 @@ namespace Spine.Unity {
 		void OnDisable () {
 		void OnDisable () {
 			if (skeletonRenderer != null)
 			if (skeletonRenderer != null)
 				skeletonRenderer.OnRebuild -= HandleRendererReset;
 				skeletonRenderer.OnRebuild -= HandleRendererReset;
-			if (skeletonGraphic != null)
+			if (skeletonGraphic != null) {
 				skeletonGraphic.OnRebuild -= HandleRendererReset;
 				skeletonGraphic.OnRebuild -= HandleRendererReset;
+				skeletonGraphic.OnPostProcessVertices -= UpdateToMeshScaleAndOffset;
+			}
 
 
 			if (skeletonAnimation != null) {
 			if (skeletonAnimation != null) {
 				skeletonAnimation.UpdateLocal -= UpdateLocal;
 				skeletonAnimation.UpdateLocal -= UpdateLocal;
@@ -449,7 +487,7 @@ namespace Spine.Unity {
 
 
 			if (mode == SkeletonUtilityBone.Mode.Override) {
 			if (mode == SkeletonUtilityBone.Mode.Override) {
 				if (rot) goTransform.localRotation = Quaternion.Euler(0, 0, b.bone.AppliedRotation);
 				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);
 				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;
 			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 () {
 		public Bounds GetMeshBounds () {
 			if (float.IsInfinity(meshBoundsMin.x)) { // meshBoundsMin.x == BoundsMinDefault // == doesn't work on float Infinity constants.
 			if (float.IsInfinity(meshBoundsMin.x)) { // meshBoundsMin.x == BoundsMinDefault // == doesn't work on float Infinity constants.
 				return new Bounds();
 				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));
 			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>
 		/// <summary>Gets a skeleton space UnityEngine.Quaternion representation of bone.WorldRotationX.</summary>
 		public static Quaternion GetQuaternion (this Bone bone) {
 		public static Quaternion GetQuaternion (this Bone bone) {
 			float halfRotation = Mathf.Atan2(bone.C, bone.A) * 0.5f;
 			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",
 	"name": "com.esotericsoftware.spine.spine-unity",
 	"displayName": "spine-unity Runtime",
 	"displayName": "spine-unity Runtime",
 	"description": "This plugin provides the spine-unity runtime core.",
 	"description": "This plugin provides the spine-unity runtime core.",
-	"version": "4.2.69",
+	"version": "4.2.70",
 	"unity": "2018.3",
 	"unity": "2018.3",
 	"author": {
 	"author": {
 		"name": "Esoteric Software",
 		"name": "Esoteric Software",