瀏覽代碼

Merge branch '3.9-beta' of https://github.com/esotericsoftware/spine-runtimes into 3.9-beta

badlogic 5 年之前
父節點
當前提交
45ef51bc89
共有 32 個文件被更改,包括 1136 次插入121 次删除
  1. 1 0
      CHANGELOG.md
  2. 11 1
      spine-csharp/src/Animation.cs
  3. 13 0
      spine-csharp/src/Slot.cs
  4. 42 0
      spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/AnimationStateTests.java
  5. 20 15
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/AnimationState.java
  6. 2 3
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Bone.java
  7. 1 2
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/BoneData.java
  8. 8 16
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Skeleton.java
  9. 2 4
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonBinary.java
  10. 3 6
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonBounds.java
  11. 14 28
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonData.java
  12. 4 1
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonLoader.java
  13. 1 2
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonRenderer.java
  14. 1 2
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Skin.java
  15. 2 4
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Slot.java
  16. 2 4
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SlotData.java
  17. 6 12
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/AttachmentLoader.java
  18. 2 4
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/VertexAttachment.java
  19. 14 0
      spine-libgdx/spine-skeletonviewer/src/com/esotericsoftware/spine/SkeletonViewer.java
  20. 6 1
      spine-unity/Assets/Spine Examples/Scripts/Sample Components/Legacy/SpriteAttacher.cs
  21. 18 0
      spine-unity/Assets/Spine Examples/Scripts/Sample Components/SkeletonUtility Modules/Editor/spine-unity-examples-editor.asmdef
  22. 7 0
      spine-unity/Assets/Spine Examples/Scripts/Sample Components/SkeletonUtility Modules/Editor/spine-unity-examples-editor.asmdef.meta
  23. 153 0
      spine-unity/Assets/Spine/Editor/spine-unity/Editor/Asset Types/SpineSpriteAtlasAssetInspector.cs
  24. 11 0
      spine-unity/Assets/Spine/Editor/spine-unity/Editor/Asset Types/SpineSpriteAtlasAssetInspector.cs.meta
  25. 178 12
      spine-unity/Assets/Spine/Editor/spine-unity/Editor/Utility/AssetUtility.cs
  26. 170 0
      spine-unity/Assets/Spine/Editor/spine-unity/Editor/Windows/SpriteAtlasImportWindow.cs
  27. 11 0
      spine-unity/Assets/Spine/Editor/spine-unity/Editor/Windows/SpriteAtlasImportWindow.cs.meta
  28. 397 0
      spine-unity/Assets/Spine/Runtime/spine-unity/Asset Types/SpineSpriteAtlasAsset.cs
  29. 11 0
      spine-unity/Assets/Spine/Runtime/spine-unity/Asset Types/SpineSpriteAtlasAsset.cs.meta
  30. 1 2
      spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonMecanim.cs
  31. 19 2
      spine-unity/Assets/Spine/Runtime/spine-unity/Utility/AtlasUtilities.cs
  32. 5 0
      spine-unity/Assets/Spine/Runtime/spine-unity/Utility/MaterialChecks.cs

+ 1 - 0
CHANGELOG.md

@@ -356,6 +356,7 @@
     3) Copy the original material, add *_Outline* to its name and set the shader to `Universal Render Pipeline/Spine/Outline/Skeleton-OutlineOnly`.
     3) Copy the original material, add *_Outline* to its name and set the shader to `Universal Render Pipeline/Spine/Outline/Skeleton-OutlineOnly`.
     4) Assign this *_Outline* material at the `RenderExistingMesh` component under *Replacement Materials*.
     4) Assign this *_Outline* material at the `RenderExistingMesh` component under *Replacement Materials*.
   * Added `Outline Shaders URP` example scene to URP extension module to demonstrate the above additions.
   * Added `Outline Shaders URP` example scene to URP extension module to demonstrate the above additions.
+  * Added support for Unity's [`SpriteAtlas`](https://docs.unity3d.com/Manual/class-SpriteAtlas.html) as atlas provider (as an alternative to `.atlas.txt` and `.png` files) alongside a skeleton data file. There is now an additional `Spine SpriteAtlas Import` tool window accessible via `Window - Spine - SpriteAtlas Import`. Additional information can be found in a new section on the [spine-unity documentation page](http://esotericsoftware.com/spine-unity#Advanced---Using-Unity-SpriteAtlas-as-Atlas-Provider).
 
 
 * **Changes of default values**
 * **Changes of default values**
   * `SkeletonMecanim`'s `Layer Mix Mode` now defaults to `MixMode.MixNext` instead of `MixMode.MixAlways`.
   * `SkeletonMecanim`'s `Layer Mix Mode` now defaults to `MixMode.MixNext` instead of `MixMode.MixAlways`.

+ 11 - 1
spine-csharp/src/Animation.cs

@@ -725,6 +725,7 @@ namespace Spine {
 					slot.g += (slotData.g - slot.g) * alpha;
 					slot.g += (slotData.g - slot.g) * alpha;
 					slot.b += (slotData.b - slot.b) * alpha;
 					slot.b += (slotData.b - slot.b) * alpha;
 					slot.a += (slotData.a - slot.a) * alpha;
 					slot.a += (slotData.a - slot.a) * alpha;
+					slot.ClampColor();
 					return;
 					return;
 				}
 				}
 				return;
 				return;
@@ -758,6 +759,7 @@ namespace Spine {
 				slot.g = g;
 				slot.g = g;
 				slot.b = b;
 				slot.b = b;
 				slot.a = a;
 				slot.a = a;
+				slot.ClampColor();
 			} else {
 			} else {
 				float br, bg, bb, ba;
 				float br, bg, bb, ba;
 				if (blend == MixBlend.Setup) {
 				if (blend == MixBlend.Setup) {
@@ -775,6 +777,7 @@ namespace Spine {
 				slot.g = bg + ((g - bg) * alpha);
 				slot.g = bg + ((g - bg) * alpha);
 				slot.b = bb + ((b - bb) * alpha);
 				slot.b = bb + ((b - bb) * alpha);
 				slot.a = ba + ((a - ba) * alpha);
 				slot.a = ba + ((a - ba) * alpha);
+				slot.ClampColor();
 			}
 			}
 		}
 		}
 	}
 	}
@@ -839,18 +842,22 @@ namespace Spine {
 					slot.g = slotData.g;
 					slot.g = slotData.g;
 					slot.b = slotData.b;
 					slot.b = slotData.b;
 					slot.a = slotData.a;
 					slot.a = slotData.a;
+					slot.ClampColor();
 					slot.r2 = slotData.r2;
 					slot.r2 = slotData.r2;
 					slot.g2 = slotData.g2;
 					slot.g2 = slotData.g2;
 					slot.b2 = slotData.b2;
 					slot.b2 = slotData.b2;
+					slot.ClampSecondColor();
 					return;
 					return;
 				case MixBlend.First:
 				case MixBlend.First:
 					slot.r += (slot.r - slotData.r) * alpha;
 					slot.r += (slot.r - slotData.r) * alpha;
 					slot.g += (slot.g - slotData.g) * alpha;
 					slot.g += (slot.g - slotData.g) * alpha;
 					slot.b += (slot.b - slotData.b) * alpha;
 					slot.b += (slot.b - slotData.b) * alpha;
 					slot.a += (slot.a - slotData.a) * alpha;
 					slot.a += (slot.a - slotData.a) * alpha;
+					slot.ClampColor();
 					slot.r2 += (slot.r2 - slotData.r2) * alpha;
 					slot.r2 += (slot.r2 - slotData.r2) * alpha;
 					slot.g2 += (slot.g2 - slotData.g2) * alpha;
 					slot.g2 += (slot.g2 - slotData.g2) * alpha;
 					slot.b2 += (slot.b2 - slotData.b2) * alpha;
 					slot.b2 += (slot.b2 - slotData.b2) * alpha;
+					slot.ClampSecondColor();
 					return;
 					return;
 				}
 				}
 				return;
 				return;
@@ -893,9 +900,11 @@ namespace Spine {
 				slot.g = g;
 				slot.g = g;
 				slot.b = b;
 				slot.b = b;
 				slot.a = a;
 				slot.a = a;
+				slot.ClampColor();
 				slot.r2 = r2;
 				slot.r2 = r2;
 				slot.g2 = g2;
 				slot.g2 = g2;
 				slot.b2 = b2;
 				slot.b2 = b2;
+				slot.ClampSecondColor();
 			} else {
 			} else {
 				float br, bg, bb, ba, br2, bg2, bb2;
 				float br, bg, bb, ba, br2, bg2, bb2;
 				if (blend == MixBlend.Setup) {
 				if (blend == MixBlend.Setup) {
@@ -919,9 +928,11 @@ namespace Spine {
 				slot.g = bg + ((g - bg) * alpha);
 				slot.g = bg + ((g - bg) * alpha);
 				slot.b = bb + ((b - bb) * alpha);
 				slot.b = bb + ((b - bb) * alpha);
 				slot.a = ba + ((a - ba) * alpha);
 				slot.a = ba + ((a - ba) * alpha);
+				slot.ClampColor();
 				slot.r2 = br2 + ((r2 - br2) * alpha);
 				slot.r2 = br2 + ((r2 - br2) * alpha);
 				slot.g2 = bg2 + ((g2 - bg2) * alpha);
 				slot.g2 = bg2 + ((g2 - bg2) * alpha);
 				slot.b2 = bb2 + ((b2 - bb2) * alpha);
 				slot.b2 = bb2 + ((b2 - bb2) * alpha);
+				slot.ClampSecondColor();
 			}
 			}
 		}
 		}
 
 
@@ -970,7 +981,6 @@ namespace Spine {
 
 
 		public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList<Event> firedEvents, float alpha, MixBlend blend,
 		public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList<Event> firedEvents, float alpha, MixBlend blend,
 							MixDirection direction) {
 							MixDirection direction) {
-			string attachmentName;
 			Slot slot = skeleton.slots.Items[slotIndex];
 			Slot slot = skeleton.slots.Items[slotIndex];
 			if (!slot.bone.active) return;
 			if (!slot.bone.active) return;
 			if (direction == MixDirection.Out) {
 			if (direction == MixDirection.Out) {

+ 13 - 0
spine-csharp/src/Slot.cs

@@ -106,6 +106,13 @@ namespace Spine {
 		/// color tinting.</summary>
 		/// color tinting.</summary>
 		public float A { get { return a; } set { a = value; } }
 		public float A { get { return a; } set { a = value; } }
 
 
+		public void ClampColor() {
+			r = MathUtils.Clamp(r, 0, 1);
+			g = MathUtils.Clamp(g, 0, 1);
+			b = MathUtils.Clamp(b, 0, 1);
+			a = MathUtils.Clamp(a, 0, 1);
+		}
+
 		/// <summary>The dark color used to tint the slot's attachment for two color tinting, ignored if two color tinting is not used.</summary>
 		/// <summary>The dark color used to tint the slot's attachment for two color tinting, ignored if two color tinting is not used.</summary>
 		/// <seealso cref="HasSecondColor"/>
 		/// <seealso cref="HasSecondColor"/>
 		public float R2 { get { return r2; } set { r2 = value; } }
 		public float R2 { get { return r2; } set { r2 = value; } }
@@ -118,6 +125,12 @@ namespace Spine {
 		/// <summary>Whether R2 G2 B2 are used to tint the slot's attachment for two color tinting. False if two color tinting is not used.</summary>
 		/// <summary>Whether R2 G2 B2 are used to tint the slot's attachment for two color tinting. False if two color tinting is not used.</summary>
 		public bool HasSecondColor { get { return data.hasSecondColor; } set { data.hasSecondColor = value; } }
 		public bool HasSecondColor { get { return data.hasSecondColor; } set { data.hasSecondColor = value; } }
 
 
+		public void ClampSecondColor () {
+			r2 = MathUtils.Clamp(r2, 0, 1);
+			g2 = MathUtils.Clamp(g2, 0, 1);
+			b2 = MathUtils.Clamp(b2, 0, 1);
+		}
+
 		public Attachment Attachment {
 		public Attachment Attachment {
 			/// <summary>The current attachment for the slot, or null if the slot has no attachment.</summary>
 			/// <summary>The current attachment for the slot, or null if the slot has no attachment.</summary>
 			get { return attachment; }
 			get { return attachment; }

+ 42 - 0
spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/AnimationStateTests.java

@@ -797,6 +797,48 @@ public class AnimationStateTests {
 			System.exit(0);
 			System.exit(0);
 		}
 		}
 
 
+		setup("looping", // 28
+			expect(0, "start", 0, 0), //
+			expect(0, "event 0", 0, 0), //
+			expect(0, "event 14", 0.5f, 0.5f), //
+			expect(0, "event 30", 1, 1), //
+			expect(0, "complete", 1, 1), //
+			expect(0, "event 0", 1, 1), //
+			expect(0, "event 14", 1.5f, 1.5f), //
+			expect(0, "event 30", 2, 2), //
+			expect(0, "complete", 2, 2), //
+			expect(0, "event 0", 2, 2), //
+			expect(0, "event 14", 2.5f, 2.5f), //
+			expect(0, "end", 2.6f, 2.7f), //
+			expect(0, "dispose", 2.6f, 2.7f) //
+		);
+		state.setAnimation(0, "events0", true).setTrackEnd(2.6f);
+		run(0.1f, 1000, null);
+
+		setup("set next", // 29
+			expect(0, "start", 0, 0), //
+			expect(0, "event 0", 0, 0), //
+			expect(0, "event 14", 0.5f, 0.5f), //
+			expect(0, "event 30", 1, 1), //
+			expect(0, "complete", 1, 1), //
+			expect(0, "interrupt", 1.1f, 1.1f), //
+
+			expect(1, "start", 0.1f, 1.1f), //
+			expect(1, "event 0", 0.1f, 1.1f), //
+
+			expect(0, "end", 1.1f, 1.2f), //
+			expect(0, "dispose", 1.1f, 1.2f), //
+
+			expect(1, "event 14", 0.5f, 1.5f), //
+			expect(1, "event 30", 1, 2), //
+			expect(1, "complete", 1, 2), //
+			expect(1, "end", 1, 2.1f), //
+			expect(1, "dispose", 1, 2.1f) //
+		);
+		state.setAnimation(0, "events0", false);
+		state.addAnimation(0, "events1", false, 0).setTrackEnd(1);
+		run(0.1f, 1000, null);
+
 		System.out.println("AnimationState tests passed.");
 		System.out.println("AnimationState tests passed.");
 	}
 	}
 
 

+ 20 - 15
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/AnimationState.java

@@ -522,6 +522,11 @@ public class AnimationState {
 		queue.drain();
 		queue.drain();
 	}
 	}
 
 
+	/** Removes the {@link TrackEntry#getNext() next entry} and all entries after it for the specified entry. */
+	public void clearNext (TrackEntry entry) {
+		disposeNext(entry.next);
+	}
+
 	private void setCurrent (int index, TrackEntry current, boolean interrupt) {
 	private void setCurrent (int index, TrackEntry current, boolean interrupt) {
 		TrackEntry from = expandToIndex(index);
 		TrackEntry from = expandToIndex(index);
 		tracks.set(index, current);
 		tracks.set(index, current);
@@ -792,8 +797,7 @@ public class AnimationState {
 	}
 	}
 
 
 	/** Returns the track entry for the animation currently playing on the track, or null if no animation is currently playing. */
 	/** Returns the track entry for the animation currently playing on the track, or null if no animation is currently playing. */
-	@Null
-	public TrackEntry getCurrent (int trackIndex) {
+	public @Null TrackEntry getCurrent (int trackIndex) {
 		if (trackIndex < 0) throw new IllegalArgumentException("trackIndex must be >= 0.");
 		if (trackIndex < 0) throw new IllegalArgumentException("trackIndex must be >= 0.");
 		if (trackIndex >= tracks.size) return null;
 		if (trackIndex >= tracks.size) return null;
 		return tracks.get(trackIndex);
 		return tracks.get(trackIndex);
@@ -1031,8 +1035,7 @@ public class AnimationState {
 		 * <p>
 		 * <p>
 		 * A track entry returned from {@link AnimationState#setAnimation(int, Animation, boolean)} is already the current animation
 		 * A track entry returned from {@link AnimationState#setAnimation(int, Animation, boolean)} is already the current animation
 		 * for the track, so the track entry listener {@link AnimationStateListener#start(TrackEntry)} will not be called. */
 		 * for the track, so the track entry listener {@link AnimationStateListener#start(TrackEntry)} will not be called. */
-		@Null
-		public AnimationStateListener getListener () {
+		public @Null AnimationStateListener getListener () {
 			return listener;
 			return listener;
 		}
 		}
 
 
@@ -1086,12 +1089,17 @@ public class AnimationState {
 			this.drawOrderThreshold = drawOrderThreshold;
 			this.drawOrderThreshold = drawOrderThreshold;
 		}
 		}
 
 
-		/** The animation queued to start after this animation, or null. <code>next</code> makes up a linked list. */
-		@Null
-		public TrackEntry getNext () {
+		/** The animation queued to start after this animation, or null if there is none. <code>next</code> makes up a linked list.
+		 * It cannot be set to null, use {@link AnimationState#clearNext(TrackEntry)} instead. */
+		public @Null TrackEntry getNext () {
 			return next;
 			return next;
 		}
 		}
 
 
+		public void setNext (TrackEntry next) {
+			if (next == null) throw new IllegalArgumentException("next cannot be null.");
+			this.next = next;
+		}
+
 		/** Returns true if at least one loop has been completed.
 		/** Returns true if at least one loop has been completed.
 		 * <p>
 		 * <p>
 		 * See {@link AnimationStateListener#complete(TrackEntry)}. */
 		 * See {@link AnimationStateListener#complete(TrackEntry)}. */
@@ -1147,15 +1155,13 @@ public class AnimationState {
 
 
 		/** The track entry for the previous animation when mixing from the previous animation to this animation, or null if no
 		/** The track entry for the previous animation when mixing from the previous animation to this animation, or null if no
 		 * mixing is currently occuring. When mixing from multiple animations, <code>mixingFrom</code> makes up a linked list. */
 		 * mixing is currently occuring. When mixing from multiple animations, <code>mixingFrom</code> makes up a linked list. */
-		@Null
-		public TrackEntry getMixingFrom () {
+		public @Null TrackEntry getMixingFrom () {
 			return mixingFrom;
 			return mixingFrom;
 		}
 		}
 
 
 		/** The track entry for the next animation when mixing from this animation to the next animation, or null if no mixing is
 		/** The track entry for the next animation when mixing from this animation to the next animation, or null if no mixing is
 		 * currently occuring. When mixing to multiple animations, <code>mixingTo</code> makes up a linked list. */
 		 * currently occuring. When mixing to multiple animations, <code>mixingTo</code> makes up a linked list. */
-		@Null
-		public TrackEntry getMixingTo () {
+		public @Null TrackEntry getMixingTo () {
 			return mixingTo;
 			return mixingTo;
 		}
 		}
 
 
@@ -1244,11 +1250,10 @@ public class AnimationState {
 			if (drainDisabled) return; // Not reentrant.
 			if (drainDisabled) return; // Not reentrant.
 			drainDisabled = true;
 			drainDisabled = true;
 
 
-			Object[] objects = this.objects.items;
 			SnapshotArray<AnimationStateListener> listenersArray = AnimationState.this.listeners;
 			SnapshotArray<AnimationStateListener> listenersArray = AnimationState.this.listeners;
 			for (int i = 0; i < this.objects.size; i += 2) {
 			for (int i = 0; i < this.objects.size; i += 2) {
-				EventType type = (EventType)objects[i];
-				TrackEntry entry = (TrackEntry)objects[i + 1];
+				EventType type = (EventType)objects.get(i);
+				TrackEntry entry = (TrackEntry)objects.get(i + 1);
 				int listenersCount = listenersArray.size;
 				int listenersCount = listenersArray.size;
 				Object[] listeners = listenersArray.begin();
 				Object[] listeners = listenersArray.begin();
 				switch (type) {
 				switch (type) {
@@ -1279,7 +1284,7 @@ public class AnimationState {
 						((AnimationStateListener)listeners[ii]).complete(entry);
 						((AnimationStateListener)listeners[ii]).complete(entry);
 					break;
 					break;
 				case event:
 				case event:
-					Event event = (Event)objects[i++ + 2];
+					Event event = (Event)objects.get(i++ + 2);
 					if (entry.listener != null) entry.listener.event(entry, event);
 					if (entry.listener != null) entry.listener.event(entry, event);
 					for (int ii = 0; ii < listenersCount; ii++)
 					for (int ii = 0; ii < listenersCount; ii++)
 						((AnimationStateListener)listeners[ii]).event(entry, event);
 						((AnimationStateListener)listeners[ii]).event(entry, event);

+ 2 - 3
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Bone.java

@@ -168,7 +168,7 @@ public class Bone implements Updatable {
 			b = pa * lb - pb * ld;
 			b = pa * lb - pb * ld;
 			c = pc * la + pd * lc;
 			c = pc * la + pd * lc;
 			d = pc * lb + pd * ld;
 			d = pc * lb + pd * ld;
-			break;
+			return;
 		}
 		}
 		case noScale:
 		case noScale:
 		case noScaleOrReflection: {
 		case noScaleOrReflection: {
@@ -225,8 +225,7 @@ public class Bone implements Updatable {
 	}
 	}
 
 
 	/** The parent bone, or null if this is the root bone. */
 	/** The parent bone, or null if this is the root bone. */
-	@Null
-	public Bone getParent () {
+	public @Null Bone getParent () {
 		return parent;
 		return parent;
 	}
 	}
 
 

+ 1 - 2
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/BoneData.java

@@ -79,8 +79,7 @@ public class BoneData {
 		return name;
 		return name;
 	}
 	}
 
 
-	@Null
-	public BoneData getParent () {
+	public @Null BoneData getParent () {
 		return parent;
 		return parent;
 	}
 	}
 
 

+ 8 - 16
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Skeleton.java

@@ -484,8 +484,7 @@ public class Skeleton {
 
 
 	/** Finds a bone by comparing each bone's name. It is more efficient to cache the results of this method than to call it
 	/** Finds a bone by comparing each bone's name. It is more efficient to cache the results of this method than to call it
 	 * repeatedly. */
 	 * repeatedly. */
-	@Null
-	public Bone findBone (String boneName) {
+	public @Null Bone findBone (String boneName) {
 		if (boneName == null) throw new IllegalArgumentException("boneName cannot be null.");
 		if (boneName == null) throw new IllegalArgumentException("boneName cannot be null.");
 		Object[] bones = this.bones.items;
 		Object[] bones = this.bones.items;
 		for (int i = 0, n = this.bones.size; i < n; i++) {
 		for (int i = 0, n = this.bones.size; i < n; i++) {
@@ -502,8 +501,7 @@ public class Skeleton {
 
 
 	/** Finds a slot by comparing each slot's name. It is more efficient to cache the results of this method than to call it
 	/** Finds a slot by comparing each slot's name. It is more efficient to cache the results of this method than to call it
 	 * repeatedly. */
 	 * repeatedly. */
-	@Null
-	public Slot findSlot (String slotName) {
+	public @Null Slot findSlot (String slotName) {
 		if (slotName == null) throw new IllegalArgumentException("slotName cannot be null.");
 		if (slotName == null) throw new IllegalArgumentException("slotName cannot be null.");
 		Object[] slots = this.slots.items;
 		Object[] slots = this.slots.items;
 		for (int i = 0, n = this.slots.size; i < n; i++) {
 		for (int i = 0, n = this.slots.size; i < n; i++) {
@@ -524,8 +522,7 @@ public class Skeleton {
 	}
 	}
 
 
 	/** The skeleton's current skin. */
 	/** The skeleton's current skin. */
-	@Null
-	public Skin getSkin () {
+	public @Null Skin getSkin () {
 		return skin;
 		return skin;
 	}
 	}
 
 
@@ -573,8 +570,7 @@ public class Skeleton {
 	 * name.
 	 * name.
 	 * <p>
 	 * <p>
 	 * See {@link #getAttachment(int, String)}. */
 	 * See {@link #getAttachment(int, String)}. */
-	@Null
-	public Attachment getAttachment (String slotName, String attachmentName) {
+	public @Null Attachment getAttachment (String slotName, String attachmentName) {
 		SlotData slot = data.findSlot(slotName);
 		SlotData slot = data.findSlot(slotName);
 		if (slot == null) throw new IllegalArgumentException("Slot not found: " + slotName);
 		if (slot == null) throw new IllegalArgumentException("Slot not found: " + slotName);
 		return getAttachment(slot.getIndex(), attachmentName);
 		return getAttachment(slot.getIndex(), attachmentName);
@@ -584,8 +580,7 @@ public class Skeleton {
 	 * attachment name. First the skin is checked and if the attachment was not found, the default skin is checked.
 	 * attachment name. First the skin is checked and if the attachment was not found, the default skin is checked.
 	 * <p>
 	 * <p>
 	 * See <a href="http://esotericsoftware.com/spine-runtime-skins">Runtime skins</a> in the Spine Runtimes Guide. */
 	 * See <a href="http://esotericsoftware.com/spine-runtime-skins">Runtime skins</a> in the Spine Runtimes Guide. */
-	@Null
-	public Attachment getAttachment (int slotIndex, String attachmentName) {
+	public @Null Attachment getAttachment (int slotIndex, String attachmentName) {
 		if (attachmentName == null) throw new IllegalArgumentException("attachmentName cannot be null.");
 		if (attachmentName == null) throw new IllegalArgumentException("attachmentName cannot be null.");
 		if (skin != null) {
 		if (skin != null) {
 			Attachment attachment = skin.getAttachment(slotIndex, attachmentName);
 			Attachment attachment = skin.getAttachment(slotIndex, attachmentName);
@@ -618,8 +613,7 @@ public class Skeleton {
 
 
 	/** Finds an IK constraint by comparing each IK constraint's name. It is more efficient to cache the results of this method
 	/** Finds an IK constraint by comparing each IK constraint's name. It is more efficient to cache the results of this method
 	 * than to call it repeatedly. */
 	 * than to call it repeatedly. */
-	@Null
-	public IkConstraint findIkConstraint (String constraintName) {
+	public @Null IkConstraint findIkConstraint (String constraintName) {
 		if (constraintName == null) throw new IllegalArgumentException("constraintName cannot be null.");
 		if (constraintName == null) throw new IllegalArgumentException("constraintName cannot be null.");
 		Object[] ikConstraints = this.ikConstraints.items;
 		Object[] ikConstraints = this.ikConstraints.items;
 		for (int i = 0, n = this.ikConstraints.size; i < n; i++) {
 		for (int i = 0, n = this.ikConstraints.size; i < n; i++) {
@@ -636,8 +630,7 @@ public class Skeleton {
 
 
 	/** Finds a transform constraint by comparing each transform constraint's name. It is more efficient to cache the results of
 	/** Finds a transform constraint by comparing each transform constraint's name. It is more efficient to cache the results of
 	 * this method than to call it repeatedly. */
 	 * this method than to call it repeatedly. */
-	@Null
-	public TransformConstraint findTransformConstraint (String constraintName) {
+	public @Null TransformConstraint findTransformConstraint (String constraintName) {
 		if (constraintName == null) throw new IllegalArgumentException("constraintName cannot be null.");
 		if (constraintName == null) throw new IllegalArgumentException("constraintName cannot be null.");
 		Object[] transformConstraints = this.transformConstraints.items;
 		Object[] transformConstraints = this.transformConstraints.items;
 		for (int i = 0, n = this.transformConstraints.size; i < n; i++) {
 		for (int i = 0, n = this.transformConstraints.size; i < n; i++) {
@@ -654,8 +647,7 @@ public class Skeleton {
 
 
 	/** Finds a path constraint by comparing each path constraint's name. It is more efficient to cache the results of this method
 	/** Finds a path constraint by comparing each path constraint's name. It is more efficient to cache the results of this method
 	 * than to call it repeatedly. */
 	 * than to call it repeatedly. */
-	@Null
-	public PathConstraint findPathConstraint (String constraintName) {
+	public @Null PathConstraint findPathConstraint (String constraintName) {
 		if (constraintName == null) throw new IllegalArgumentException("constraintName cannot be null.");
 		if (constraintName == null) throw new IllegalArgumentException("constraintName cannot be null.");
 		Object[] pathConstraints = this.pathConstraints.items;
 		Object[] pathConstraints = this.pathConstraints.items;
 		for (int i = 0, n = this.pathConstraints.size; i < n; i++) {
 		for (int i = 0, n = this.pathConstraints.size; i < n; i++) {

+ 2 - 4
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonBinary.java

@@ -312,8 +312,7 @@ public class SkeletonBinary extends SkeletonLoader {
 		return skeletonData;
 		return skeletonData;
 	}
 	}
 
 
-	@Null
-	private Skin readSkin (SkeletonInput input, SkeletonData skeletonData, boolean defaultSkin, boolean nonessential)
+	private @Null Skin readSkin (SkeletonInput input, SkeletonData skeletonData, boolean defaultSkin, boolean nonessential)
 		throws IOException {
 		throws IOException {
 
 
 		Skin skin;
 		Skin skin;
@@ -920,8 +919,7 @@ public class SkeletonBinary extends SkeletonLoader {
 			super(file.read(512));
 			super(file.read(512));
 		}
 		}
 
 
-		@Null
-		public String readStringRef () throws IOException {
+		public @Null String readStringRef () throws IOException {
 			int index = readInt(true);
 			int index = readInt(true);
 			return index == 0 ? null : strings[index - 1];
 			return index == 0 ? null : strings[index - 1];
 		}
 		}

+ 3 - 6
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonBounds.java

@@ -143,8 +143,7 @@ public class SkeletonBounds {
 
 
 	/** Returns the first bounding box attachment that contains the point, or null. When doing many checks, it is usually more
 	/** Returns the first bounding box attachment that contains the point, or null. When doing many checks, it is usually more
 	 * efficient to only call this method if {@link #aabbContainsPoint(float, float)} returns true. */
 	 * efficient to only call this method if {@link #aabbContainsPoint(float, float)} returns true. */
-	@Null
-	public BoundingBoxAttachment containsPoint (float x, float y) {
+	public @Null BoundingBoxAttachment containsPoint (float x, float y) {
 		Object[] polygons = this.polygons.items;
 		Object[] polygons = this.polygons.items;
 		for (int i = 0, n = this.polygons.size; i < n; i++)
 		for (int i = 0, n = this.polygons.size; i < n; i++)
 			if (containsPoint((FloatArray)polygons[i], x, y)) return boundingBoxes.get(i);
 			if (containsPoint((FloatArray)polygons[i], x, y)) return boundingBoxes.get(i);
@@ -174,8 +173,7 @@ public class SkeletonBounds {
 	/** Returns the first bounding box attachment that contains any part of the line segment, or null. When doing many checks, it
 	/** Returns the first bounding box attachment that contains any part of the line segment, or null. When doing many checks, it
 	 * is usually more efficient to only call this method if {@link #aabbIntersectsSegment(float, float, float, float)} returns
 	 * is usually more efficient to only call this method if {@link #aabbIntersectsSegment(float, float, float, float)} returns
 	 * true. */
 	 * true. */
-	@Null
-	public BoundingBoxAttachment intersectsSegment (float x1, float y1, float x2, float y2) {
+	public @Null BoundingBoxAttachment intersectsSegment (float x1, float y1, float x2, float y2) {
 		Object[] polygons = this.polygons.items;
 		Object[] polygons = this.polygons.items;
 		for (int i = 0, n = this.polygons.size; i < n; i++)
 		for (int i = 0, n = this.polygons.size; i < n; i++)
 			if (intersectsSegment((FloatArray)polygons[i], x1, y1, x2, y2)) return boundingBoxes.get(i);
 			if (intersectsSegment((FloatArray)polygons[i], x1, y1, x2, y2)) return boundingBoxes.get(i);
@@ -248,8 +246,7 @@ public class SkeletonBounds {
 	}
 	}
 
 
 	/** Returns the polygon for the specified bounding box, or null. */
 	/** Returns the polygon for the specified bounding box, or null. */
-	@Null
-	public FloatArray getPolygon (BoundingBoxAttachment boundingBox) {
+	public @Null FloatArray getPolygon (BoundingBoxAttachment boundingBox) {
 		if (boundingBox == null) throw new IllegalArgumentException("boundingBox cannot be null.");
 		if (boundingBox == null) throw new IllegalArgumentException("boundingBox cannot be null.");
 		int index = boundingBoxes.indexOf(boundingBox, true);
 		int index = boundingBoxes.indexOf(boundingBox, true);
 		return index == -1 ? null : polygons.get(index);
 		return index == -1 ? null : polygons.get(index);

+ 14 - 28
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonData.java

@@ -67,8 +67,7 @@ public class SkeletonData {
 
 
 	/** Finds a bone by comparing each bone's name. It is more efficient to cache the results of this method than to call it
 	/** Finds a bone by comparing each bone's name. It is more efficient to cache the results of this method than to call it
 	 * multiple times. */
 	 * multiple times. */
-	@Null
-	public BoneData findBone (String boneName) {
+	public @Null BoneData findBone (String boneName) {
 		if (boneName == null) throw new IllegalArgumentException("boneName cannot be null.");
 		if (boneName == null) throw new IllegalArgumentException("boneName cannot be null.");
 		Object[] bones = this.bones.items;
 		Object[] bones = this.bones.items;
 		for (int i = 0, n = this.bones.size; i < n; i++) {
 		for (int i = 0, n = this.bones.size; i < n; i++) {
@@ -87,8 +86,7 @@ public class SkeletonData {
 
 
 	/** Finds a slot by comparing each slot's name. It is more efficient to cache the results of this method than to call it
 	/** Finds a slot by comparing each slot's name. It is more efficient to cache the results of this method than to call it
 	 * multiple times. */
 	 * multiple times. */
-	@Null
-	public SlotData findSlot (String slotName) {
+	public @Null SlotData findSlot (String slotName) {
 		if (slotName == null) throw new IllegalArgumentException("slotName cannot be null.");
 		if (slotName == null) throw new IllegalArgumentException("slotName cannot be null.");
 		Object[] slots = this.slots.items;
 		Object[] slots = this.slots.items;
 		for (int i = 0, n = this.slots.size; i < n; i++) {
 		for (int i = 0, n = this.slots.size; i < n; i++) {
@@ -103,8 +101,7 @@ public class SkeletonData {
 	/** The skeleton's default skin. By default this skin contains all attachments that were not in a skin in Spine.
 	/** The skeleton's default skin. By default this skin contains all attachments that were not in a skin in Spine.
 	 * <p>
 	 * <p>
 	 * See {@link Skeleton#getAttachment(int, String)}. */
 	 * See {@link Skeleton#getAttachment(int, String)}. */
-	@Null
-	public Skin getDefaultSkin () {
+	public @Null Skin getDefaultSkin () {
 		return defaultSkin;
 		return defaultSkin;
 	}
 	}
 
 
@@ -114,8 +111,7 @@ public class SkeletonData {
 
 
 	/** Finds a skin by comparing each skin's name. It is more efficient to cache the results of this method than to call it
 	/** Finds a skin by comparing each skin's name. It is more efficient to cache the results of this method than to call it
 	 * multiple times. */
 	 * multiple times. */
-	@Null
-	public Skin findSkin (String skinName) {
+	public @Null Skin findSkin (String skinName) {
 		if (skinName == null) throw new IllegalArgumentException("skinName cannot be null.");
 		if (skinName == null) throw new IllegalArgumentException("skinName cannot be null.");
 		for (Skin skin : skins)
 		for (Skin skin : skins)
 			if (skin.name.equals(skinName)) return skin;
 			if (skin.name.equals(skinName)) return skin;
@@ -131,8 +127,7 @@ public class SkeletonData {
 
 
 	/** Finds an event by comparing each events's name. It is more efficient to cache the results of this method than to call it
 	/** Finds an event by comparing each events's name. It is more efficient to cache the results of this method than to call it
 	 * multiple times. */
 	 * multiple times. */
-	@Null
-	public EventData findEvent (String eventDataName) {
+	public @Null EventData findEvent (String eventDataName) {
 		if (eventDataName == null) throw new IllegalArgumentException("eventDataName cannot be null.");
 		if (eventDataName == null) throw new IllegalArgumentException("eventDataName cannot be null.");
 		for (EventData eventData : events)
 		for (EventData eventData : events)
 			if (eventData.name.equals(eventDataName)) return eventData;
 			if (eventData.name.equals(eventDataName)) return eventData;
@@ -153,8 +148,7 @@ public class SkeletonData {
 
 
 	/** Finds an animation by comparing each animation's name. It is more efficient to cache the results of this method than to
 	/** Finds an animation by comparing each animation's name. It is more efficient to cache the results of this method than to
 	 * call it multiple times. */
 	 * call it multiple times. */
-	@Null
-	public Animation findAnimation (String animationName) {
+	public @Null Animation findAnimation (String animationName) {
 		if (animationName == null) throw new IllegalArgumentException("animationName cannot be null.");
 		if (animationName == null) throw new IllegalArgumentException("animationName cannot be null.");
 		Object[] animations = this.animations.items;
 		Object[] animations = this.animations.items;
 		for (int i = 0, n = this.animations.size; i < n; i++) {
 		for (int i = 0, n = this.animations.size; i < n; i++) {
@@ -173,8 +167,7 @@ public class SkeletonData {
 
 
 	/** Finds an IK constraint by comparing each IK constraint's name. It is more efficient to cache the results of this method
 	/** Finds an IK constraint by comparing each IK constraint's name. It is more efficient to cache the results of this method
 	 * than to call it multiple times. */
 	 * than to call it multiple times. */
-	@Null
-	public IkConstraintData findIkConstraint (String constraintName) {
+	public @Null IkConstraintData findIkConstraint (String constraintName) {
 		if (constraintName == null) throw new IllegalArgumentException("constraintName cannot be null.");
 		if (constraintName == null) throw new IllegalArgumentException("constraintName cannot be null.");
 		Object[] ikConstraints = this.ikConstraints.items;
 		Object[] ikConstraints = this.ikConstraints.items;
 		for (int i = 0, n = this.ikConstraints.size; i < n; i++) {
 		for (int i = 0, n = this.ikConstraints.size; i < n; i++) {
@@ -193,8 +186,7 @@ public class SkeletonData {
 
 
 	/** Finds a transform constraint by comparing each transform constraint's name. It is more efficient to cache the results of
 	/** Finds a transform constraint by comparing each transform constraint's name. It is more efficient to cache the results of
 	 * this method than to call it multiple times. */
 	 * this method than to call it multiple times. */
-	@Null
-	public TransformConstraintData findTransformConstraint (String constraintName) {
+	public @Null TransformConstraintData findTransformConstraint (String constraintName) {
 		if (constraintName == null) throw new IllegalArgumentException("constraintName cannot be null.");
 		if (constraintName == null) throw new IllegalArgumentException("constraintName cannot be null.");
 		Object[] transformConstraints = this.transformConstraints.items;
 		Object[] transformConstraints = this.transformConstraints.items;
 		for (int i = 0, n = this.transformConstraints.size; i < n; i++) {
 		for (int i = 0, n = this.transformConstraints.size; i < n; i++) {
@@ -213,8 +205,7 @@ public class SkeletonData {
 
 
 	/** Finds a path constraint by comparing each path constraint's name. It is more efficient to cache the results of this method
 	/** Finds a path constraint by comparing each path constraint's name. It is more efficient to cache the results of this method
 	 * than to call it multiple times. */
 	 * than to call it multiple times. */
-	@Null
-	public PathConstraintData findPathConstraint (String constraintName) {
+	public @Null PathConstraintData findPathConstraint (String constraintName) {
 		if (constraintName == null) throw new IllegalArgumentException("constraintName cannot be null.");
 		if (constraintName == null) throw new IllegalArgumentException("constraintName cannot be null.");
 		Object[] pathConstraints = this.pathConstraints.items;
 		Object[] pathConstraints = this.pathConstraints.items;
 		for (int i = 0, n = this.pathConstraints.size; i < n; i++) {
 		for (int i = 0, n = this.pathConstraints.size; i < n; i++) {
@@ -227,8 +218,7 @@ public class SkeletonData {
 	// ---
 	// ---
 
 
 	/** The skeleton's name, which by default is the name of the skeleton data file, if possible. */
 	/** The skeleton's name, which by default is the name of the skeleton data file, if possible. */
-	@Null
-	public String getName () {
+	public @Null String getName () {
 		return name;
 		return name;
 	}
 	}
 
 
@@ -273,8 +263,7 @@ public class SkeletonData {
 	}
 	}
 
 
 	/** The Spine version used to export the skeleton data, or null. */
 	/** The Spine version used to export the skeleton data, or null. */
-	@Null
-	public String getVersion () {
+	public @Null String getVersion () {
 		return version;
 		return version;
 	}
 	}
 
 
@@ -283,8 +272,7 @@ public class SkeletonData {
 	}
 	}
 
 
 	/** The skeleton data hash. This value will change if any of the skeleton data has changed. */
 	/** The skeleton data hash. This value will change if any of the skeleton data has changed. */
-	@Null
-	public String getHash () {
+	public @Null String getHash () {
 		return hash;
 		return hash;
 	}
 	}
 
 
@@ -293,8 +281,7 @@ public class SkeletonData {
 	}
 	}
 
 
 	/** The path to the images directory as defined in Spine. Available only when nonessential data was exported. */
 	/** The path to the images directory as defined in Spine. Available only when nonessential data was exported. */
-	@Null
-	public String getImagesPath () {
+	public @Null String getImagesPath () {
 		return imagesPath;
 		return imagesPath;
 	}
 	}
 
 
@@ -303,8 +290,7 @@ public class SkeletonData {
 	}
 	}
 
 
 	/** The path to the audio directory as defined in Spine. Available only when nonessential data was exported. */
 	/** The path to the audio directory as defined in Spine. Available only when nonessential data was exported. */
-	@Null
-	public String getAudioPath () {
+	public @Null String getAudioPath () {
 		return audioPath;
 		return audioPath;
 	}
 	}
 
 

+ 4 - 1
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonLoader.java

@@ -9,7 +9,10 @@ import com.esotericsoftware.spine.SkeletonJson.LinkedMesh;
 import com.esotericsoftware.spine.attachments.AtlasAttachmentLoader;
 import com.esotericsoftware.spine.attachments.AtlasAttachmentLoader;
 import com.esotericsoftware.spine.attachments.AttachmentLoader;
 import com.esotericsoftware.spine.attachments.AttachmentLoader;
 
 
-/** Base class for loading skeleton data from a file. */
+/** Base class for loading skeleton data from a file.
+ * <p>
+ * See <a href="http://esotericsoftware.com/spine-loading-skeleton-data#JSON-and-binary-data">JSON and binary data</a> in the
+ * Spine Runtimes Guide. */
 abstract public class SkeletonLoader {
 abstract public class SkeletonLoader {
 	final AttachmentLoader attachmentLoader;
 	final AttachmentLoader attachmentLoader;
 	float scale = 1;
 	float scale = 1;

+ 1 - 2
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonRenderer.java

@@ -448,8 +448,7 @@ public class SkeletonRenderer {
 		this.premultipliedAlpha = premultipliedAlpha;
 		this.premultipliedAlpha = premultipliedAlpha;
 	}
 	}
 
 
-	@Null
-	public VertexEffect getVertexEffect () {
+	public @Null VertexEffect getVertexEffect () {
 		return vertexEffect;
 		return vertexEffect;
 	}
 	}
 
 

+ 1 - 2
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Skin.java

@@ -94,8 +94,7 @@ public class Skin {
 	}
 	}
 
 
 	/** Returns the attachment for the specified slot index and name, or null. */
 	/** Returns the attachment for the specified slot index and name, or null. */
-	@Null
-	public Attachment getAttachment (int slotIndex, String name) {
+	public @Null Attachment getAttachment (int slotIndex, String name) {
 		lookup.set(slotIndex, name);
 		lookup.set(slotIndex, name);
 		SkinEntry entry = attachments.get(lookup);
 		SkinEntry entry = attachments.get(lookup);
 		return entry != null ? entry.attachment : null;
 		return entry != null ? entry.attachment : null;

+ 2 - 4
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Slot.java

@@ -95,14 +95,12 @@ public class Slot {
 
 
 	/** The dark color used to tint the slot's attachment for two color tinting, or null if two color tinting is not used. The dark
 	/** The dark color used to tint the slot's attachment for two color tinting, or null if two color tinting is not used. The dark
 	 * color's alpha is not used. */
 	 * color's alpha is not used. */
-	@Null
-	public Color getDarkColor () {
+	public @Null Color getDarkColor () {
 		return darkColor;
 		return darkColor;
 	}
 	}
 
 
 	/** The current attachment for the slot, or null if the slot has no attachment. */
 	/** The current attachment for the slot, or null if the slot has no attachment. */
-	@Null
-	public Attachment getAttachment () {
+	public @Null Attachment getAttachment () {
 		return attachment;
 		return attachment;
 	}
 	}
 
 

+ 2 - 4
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SlotData.java

@@ -74,8 +74,7 @@ public class SlotData {
 
 
 	/** The dark color used to tint the slot's attachment for two color tinting, or null if two color tinting is not used. The dark
 	/** The dark color used to tint the slot's attachment for two color tinting, or null if two color tinting is not used. The dark
 	 * color's alpha is not used. */
 	 * color's alpha is not used. */
-	@Null
-	public Color getDarkColor () {
+	public @Null Color getDarkColor () {
 		return darkColor;
 		return darkColor;
 	}
 	}
 
 
@@ -88,8 +87,7 @@ public class SlotData {
 	}
 	}
 
 
 	/** The name of the attachment that is visible for this slot in the setup pose, or null if no attachment is visible. */
 	/** The name of the attachment that is visible for this slot in the setup pose, or null if no attachment is visible. */
-	@Null
-	public String getAttachmentName () {
+	public @Null String getAttachmentName () {
 		return attachmentName;
 		return attachmentName;
 	}
 	}
 
 

+ 6 - 12
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/AttachmentLoader.java

@@ -39,26 +39,20 @@ import com.esotericsoftware.spine.Skin;
  * Runtimes Guide. */
  * Runtimes Guide. */
 public interface AttachmentLoader {
 public interface AttachmentLoader {
 	/** @return May be null to not load the attachment. */
 	/** @return May be null to not load the attachment. */
-	@Null
-	public RegionAttachment newRegionAttachment (Skin skin, String name, String path);
+	public @Null RegionAttachment newRegionAttachment (Skin skin, String name, String path);
 
 
 	/** @return May be null to not load the attachment. */
 	/** @return May be null to not load the attachment. */
-	@Null
-	public MeshAttachment newMeshAttachment (Skin skin, String name, String path);
+	public @Null MeshAttachment newMeshAttachment (Skin skin, String name, String path);
 
 
 	/** @return May be null to not load the attachment. */
 	/** @return May be null to not load the attachment. */
-	@Null
-	public BoundingBoxAttachment newBoundingBoxAttachment (Skin skin, String name);
+	public @Null BoundingBoxAttachment newBoundingBoxAttachment (Skin skin, String name);
 
 
 	/** @return May be null to not load the attachment. */
 	/** @return May be null to not load the attachment. */
-	@Null
-	public ClippingAttachment newClippingAttachment (Skin skin, String name);
+	public @Null ClippingAttachment newClippingAttachment (Skin skin, String name);
 
 
 	/** @return May be null to not load the attachment. */
 	/** @return May be null to not load the attachment. */
-	@Null
-	public PathAttachment newPathAttachment (Skin skin, String name);
+	public @Null PathAttachment newPathAttachment (Skin skin, String name);
 
 
 	/** @return May be null to not load the attachment. */
 	/** @return May be null to not load the attachment. */
-	@Null
-	public PointAttachment newPointAttachment (Skin skin, String name);
+	public @Null PointAttachment newPointAttachment (Skin skin, String name);
 }
 }

+ 2 - 4
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/VertexAttachment.java

@@ -123,8 +123,7 @@ abstract public class VertexAttachment extends Attachment {
 
 
 	/** Deform keys for the deform attachment are also applied to this attachment.
 	/** Deform keys for the deform attachment are also applied to this attachment.
 	 * @return May be null if no deform keys should be applied. */
 	 * @return May be null if no deform keys should be applied. */
-	@Null
-	public VertexAttachment getDeformAttachment () {
+	public @Null VertexAttachment getDeformAttachment () {
 		return deformAttachment;
 		return deformAttachment;
 	}
 	}
 
 
@@ -136,8 +135,7 @@ abstract public class VertexAttachment extends Attachment {
 	/** The bones which affect the {@link #getVertices()}. The array entries are, for each vertex, the number of bones affecting
 	/** The bones which affect the {@link #getVertices()}. The array entries are, for each vertex, the number of bones affecting
 	 * the vertex followed by that many bone indices, which is the index of the bone in {@link Skeleton#getBones()}. Will be null
 	 * the vertex followed by that many bone indices, which is the index of the bone in {@link Skeleton#getBones()}. Will be null
 	 * if this attachment has no weights. */
 	 * if this attachment has no weights. */
-	@Null
-	public int[] getBones () {
+	public @Null int[] getBones () {
 		return bones;
 		return bones;
 	}
 	}
 
 

+ 14 - 0
spine-libgdx/spine-skeletonviewer/src/com/esotericsoftware/spine/SkeletonViewer.java

@@ -34,6 +34,7 @@ import static com.badlogic.gdx.scenes.scene2d.actions.Actions.*;
 
 
 import java.io.File;
 import java.io.File;
 import java.lang.Thread.UncaughtExceptionHandler;
 import java.lang.Thread.UncaughtExceptionHandler;
+import java.lang.reflect.Field;
 
 
 import com.badlogic.gdx.ApplicationAdapter;
 import com.badlogic.gdx.ApplicationAdapter;
 import com.badlogic.gdx.Gdx;
 import com.badlogic.gdx.Gdx;
@@ -1110,6 +1111,19 @@ public class SkeletonViewer extends ApplicationAdapter {
 	}
 	}
 
 
 	static public void main (String[] args) throws Exception {
 	static public void main (String[] args) throws Exception {
+		try { // Try to turn off illegal access log messages.
+			Class loggerClass = Class.forName("jdk.internal.module.IllegalAccessLogger");
+			Field loggerField = loggerClass.getDeclaredField("logger");
+			Class unsafeClass = Class.forName("sun.misc.Unsafe");
+			Field unsafeField = unsafeClass.getDeclaredField("theUnsafe");
+			unsafeField.setAccessible(true);
+			Object unsafe = unsafeField.get(null);
+			Long offset = (Long)unsafeClass.getMethod("staticFieldOffset", Field.class).invoke(unsafe, loggerField);
+			unsafeClass.getMethod("putObjectVolatile", Object.class, long.class, Object.class) //
+				.invoke(unsafe, loggerClass, offset, null);
+		} catch (Throwable ex) {
+		}
+
 		SkeletonViewer.args = args;
 		SkeletonViewer.args = args;
 
 
 		String os = System.getProperty("os.name");
 		String os = System.getProperty("os.name");

+ 6 - 1
spine-unity/Assets/Spine Examples/Scripts/Sample Components/Legacy/SpriteAttacher.cs

@@ -60,6 +60,8 @@ namespace Spine.Unity.Examples {
 
 
 			if (applyPMA) {
 			if (applyPMA) {
 				try {
 				try {
+					if (sprite == null)
+						return;
 					sprite.texture.GetPixel(0, 0);
 					sprite.texture.GetPixel(0, 0);
 				} catch (UnityException e) {
 				} catch (UnityException e) {
 					Debug.LogFormat("Texture of {0} ({1}) is not read/write enabled. SpriteAttacher requires this in order to work with a SkeletonRenderer that renders premultiplied alpha. Please check the texture settings.", sprite.name, sprite.texture.name);
 					Debug.LogFormat("Texture of {0} ({1}) is not read/write enabled. SpriteAttacher requires this in order to work with a SkeletonRenderer that renders premultiplied alpha. Please check the texture settings.", sprite.name, sprite.texture.name);
@@ -124,7 +126,10 @@ namespace Spine.Unity.Examples {
 
 
 				spineSlot = spineSlot ?? skeletonComponent.Skeleton.FindSlot(slot);
 				spineSlot = spineSlot ?? skeletonComponent.Skeleton.FindSlot(slot);
 				Shader attachmentShader = applyPMA ? Shader.Find(DefaultPMAShader) : Shader.Find(DefaultStraightAlphaShader);
 				Shader attachmentShader = applyPMA ? Shader.Find(DefaultPMAShader) : Shader.Find(DefaultStraightAlphaShader);
-				attachment = applyPMA ? sprite.ToRegionAttachmentPMAClone(attachmentShader) : sprite.ToRegionAttachment(SpriteAttacher.GetPageFor(sprite.texture, attachmentShader));
+				if (sprite == null)
+					attachment = null;
+				else
+					attachment = applyPMA ? sprite.ToRegionAttachmentPMAClone(attachmentShader) : sprite.ToRegionAttachment(SpriteAttacher.GetPageFor(sprite.texture, attachmentShader));
 			}
 			}
 		}
 		}
 
 

+ 18 - 0
spine-unity/Assets/Spine Examples/Scripts/Sample Components/SkeletonUtility Modules/Editor/spine-unity-examples-editor.asmdef

@@ -0,0 +1,18 @@
+{
+    "name": "spine-unity-examples-editor",
+    "references": [
+        "spine-unity",
+        "spine-unity-examples"
+    ],
+    "includePlatforms": [
+        "Editor"
+    ],
+    "excludePlatforms": [],
+    "allowUnsafeCode": false,
+    "overrideReferences": false,
+    "precompiledReferences": [],
+    "autoReferenced": true,
+    "defineConstraints": [],
+    "versionDefines": [],
+    "noEngineReferences": false
+}

+ 7 - 0
spine-unity/Assets/Spine Examples/Scripts/Sample Components/SkeletonUtility Modules/Editor/spine-unity-examples-editor.asmdef.meta

@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 39599136c72c0b64b925d3ff2885aecb
+AssemblyDefinitionImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 153 - 0
spine-unity/Assets/Spine/Editor/spine-unity/Editor/Asset Types/SpineSpriteAtlasAssetInspector.cs

@@ -0,0 +1,153 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated January 1, 2020. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2020, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+using System;
+using System.Collections.Generic;
+using System.Reflection;
+using UnityEditor;
+using UnityEngine;
+using Spine;
+
+namespace Spine.Unity.Editor {
+	using Event = UnityEngine.Event;
+
+	[CustomEditor(typeof(SpineSpriteAtlasAsset)), CanEditMultipleObjects]
+	public class SpineSpriteAtlasAssetInspector : UnityEditor.Editor {
+		SerializedProperty atlasFile, materials;
+		SpineSpriteAtlasAsset atlasAsset;
+
+		static List<AtlasRegion> GetRegions (Atlas atlas) {
+			FieldInfo regionsField = SpineInspectorUtility.GetNonPublicField(typeof(Atlas), "regions");
+			return (List<AtlasRegion>)regionsField.GetValue(atlas);
+		}
+
+		void OnEnable () {
+			SpineEditorUtilities.ConfirmInitialization();
+			atlasFile = serializedObject.FindProperty("spriteAtlasFile");
+			materials = serializedObject.FindProperty("materials");
+			materials.isExpanded = true;
+			atlasAsset = (SpineSpriteAtlasAsset)target;
+
+			if (!SpineSpriteAtlasAsset.AnySpriteAtlasNeedsRegionsLoaded())
+				return;
+			EditorApplication.update -= SpineSpriteAtlasAsset.UpdateWhenEditorPlayModeStarted;
+			EditorApplication.update += SpineSpriteAtlasAsset.UpdateWhenEditorPlayModeStarted;
+		}
+
+		void OnDisable () {
+			EditorApplication.update -= SpineSpriteAtlasAsset.UpdateWhenEditorPlayModeStarted;
+		}
+
+		override public void OnInspectorGUI () {
+			if (serializedObject.isEditingMultipleObjects) {
+				DrawDefaultInspector();
+				return;
+			}
+
+			serializedObject.Update();
+			atlasAsset = atlasAsset ?? (SpineSpriteAtlasAsset)target;
+
+			if (atlasAsset.RegionsNeedLoading) {
+				if (GUILayout.Button(SpineInspectorUtility.TempContent("Load regions by entering Play mode"), GUILayout.Height(20))) {
+					EditorApplication.isPlaying = true;
+				}
+			}
+
+			EditorGUI.BeginChangeCheck();
+			EditorGUILayout.PropertyField(atlasFile);
+			EditorGUILayout.PropertyField(materials, true);
+			if (EditorGUI.EndChangeCheck()) {
+				serializedObject.ApplyModifiedProperties();
+				atlasAsset.Clear();
+				atlasAsset.GetAtlas();
+				atlasAsset.updateRegionsInPlayMode = true;
+			}
+
+			if (materials.arraySize == 0) {
+				EditorGUILayout.HelpBox("No materials", MessageType.Error);
+				return;
+			}
+
+			for (int i = 0; i < materials.arraySize; i++) {
+				SerializedProperty prop = materials.GetArrayElementAtIndex(i);
+				var material = (Material)prop.objectReferenceValue;
+				if (material == null) {
+					EditorGUILayout.HelpBox("Materials cannot be null.", MessageType.Error);
+					return;
+				}
+			}
+
+			if (atlasFile.objectReferenceValue != null) {
+				int baseIndent = EditorGUI.indentLevel;
+
+				var regions = SpineSpriteAtlasAssetInspector.GetRegions(atlasAsset.GetAtlas());
+				int regionsCount = regions.Count;
+				using (new EditorGUILayout.HorizontalScope()) {
+					EditorGUILayout.LabelField("Atlas Regions", EditorStyles.boldLabel);
+					EditorGUILayout.LabelField(string.Format("{0} regions total", regionsCount));
+				}
+				AtlasPage lastPage = null;
+				for (int i = 0; i < regionsCount; i++) {
+					if (lastPage != regions[i].page) {
+						if (lastPage != null) {
+							EditorGUILayout.Separator();
+							EditorGUILayout.Separator();
+						}
+						lastPage = regions[i].page;
+						Material mat = ((Material)lastPage.rendererObject);
+						if (mat != null) {
+							EditorGUI.indentLevel = baseIndent;
+							using (new GUILayout.HorizontalScope())
+							using (new EditorGUI.DisabledGroupScope(true))
+								EditorGUILayout.ObjectField(mat, typeof(Material), false, GUILayout.Width(250));
+							EditorGUI.indentLevel = baseIndent + 1;
+						} else {
+							EditorGUILayout.HelpBox("Page missing material!", MessageType.Warning);
+						}
+					}
+
+					string regionName = regions[i].name;
+					Texture2D icon = SpineEditorUtilities.Icons.image;
+					if (regionName.EndsWith(" ")) {
+						regionName = string.Format("'{0}'", regions[i].name);
+						icon = SpineEditorUtilities.Icons.warning;
+						EditorGUILayout.LabelField(SpineInspectorUtility.TempContent(regionName, icon, "Region name ends with whitespace. This may cause errors. Please check your source image filenames."));
+					} else {
+						EditorGUILayout.LabelField(SpineInspectorUtility.TempContent(regionName, icon));
+					}
+				}
+				EditorGUI.indentLevel = baseIndent;
+			}
+
+			if (serializedObject.ApplyModifiedProperties() || SpineInspectorUtility.UndoRedoPerformed(Event.current))
+				atlasAsset.Clear();
+		}
+	}
+
+}

+ 11 - 0
spine-unity/Assets/Spine/Editor/spine-unity/Editor/Asset Types/SpineSpriteAtlasAssetInspector.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: f063dc5ff6881db4a9ee2e059812cba2
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 178 - 12
spine-unity/Assets/Spine/Editor/spine-unity/Editor/Utility/AssetUtility.cs

@@ -43,6 +43,10 @@
 #define NEW_PREFERENCES_SETTINGS_PROVIDER
 #define NEW_PREFERENCES_SETTINGS_PROVIDER
 #endif
 #endif
 
 
+#if UNITY_2018_2_OR_NEWER
+#define EXPOSES_SPRITE_ATLAS_UTILITIES
+#endif
+
 using UnityEngine;
 using UnityEngine;
 using UnityEditor;
 using UnityEditor;
 using System.Collections.Generic;
 using System.Collections.Generic;
@@ -60,6 +64,7 @@ namespace Spine.Unity.Editor {
 
 
 		public const string SkeletonDataSuffix = "_SkeletonData";
 		public const string SkeletonDataSuffix = "_SkeletonData";
 		public const string AtlasSuffix = "_Atlas";
 		public const string AtlasSuffix = "_Atlas";
+		public const string SpriteAtlasSuffix = "_SpriteAtlas";
 
 
 		/// HACK: This list keeps the asset reference temporarily during importing.
 		/// HACK: This list keeps the asset reference temporarily during importing.
 		///
 		///
@@ -251,6 +256,7 @@ namespace Spine.Unity.Editor {
 			bool reimport = false) {
 			bool reimport = false) {
 
 
 			var atlasPaths = new List<string>();
 			var atlasPaths = new List<string>();
+			var spriteAtlasPaths = new List<string>();
 			var imagePaths = new List<string>();
 			var imagePaths = new List<string>();
 			var skeletonPaths = new List<PathAndProblemInfo>();
 			var skeletonPaths = new List<PathAndProblemInfo>();
 			CompatibilityProblemInfo compatibilityProblemInfo = null;
 			CompatibilityProblemInfo compatibilityProblemInfo = null;
@@ -267,6 +273,9 @@ namespace Spine.Unity.Editor {
 						if (str.EndsWith(".atlas.txt", System.StringComparison.Ordinal))
 						if (str.EndsWith(".atlas.txt", System.StringComparison.Ordinal))
 							atlasPaths.Add(str);
 							atlasPaths.Add(str);
 						break;
 						break;
+					case ".spriteatlas":
+						spriteAtlasPaths.Add(str);
+						break;
 					case ".png":
 					case ".png":
 					case ".jpg":
 					case ".jpg":
 						imagePaths.Add(str);
 						imagePaths.Add(str);
@@ -294,7 +303,6 @@ namespace Spine.Unity.Editor {
 				AtlasAssetBase atlas = IngestSpineAtlas(atlasText, texturesWithoutMetaFile);
 				AtlasAssetBase atlas = IngestSpineAtlas(atlasText, texturesWithoutMetaFile);
 				atlases.Add(atlas);
 				atlases.Add(atlas);
 			}
 			}
-
 			AddDependentSkeletonIfAtlasChanged(skeletonPaths, atlasPaths);
 			AddDependentSkeletonIfAtlasChanged(skeletonPaths, atlasPaths);
 
 
 			// Import skeletons and match them with atlases.
 			// Import skeletons and match them with atlases.
@@ -575,6 +583,164 @@ namespace Spine.Unity.Editor {
 			return (AtlasAssetBase)AssetDatabase.LoadAssetAtPath(atlasPath, typeof(AtlasAssetBase));
 			return (AtlasAssetBase)AssetDatabase.LoadAssetAtPath(atlasPath, typeof(AtlasAssetBase));
 		}
 		}
 
 
+		public static bool SpriteAtlasSettingsNeedAdjustment (UnityEngine.U2D.SpriteAtlas spriteAtlas) {
+		#if EXPOSES_SPRITE_ATLAS_UTILITIES
+			UnityEditor.U2D.SpriteAtlasPackingSettings packingSettings = UnityEditor.U2D.SpriteAtlasExtensions.GetPackingSettings(spriteAtlas);
+			UnityEditor.U2D.SpriteAtlasTextureSettings textureSettings = UnityEditor.U2D.SpriteAtlasExtensions.GetTextureSettings(spriteAtlas);
+
+			bool areSettingsAsDesired =
+				packingSettings.enableRotation == true &&
+				packingSettings.enableTightPacking == false &&
+				textureSettings.readable == true &&
+				textureSettings.generateMipMaps == false;
+			// note: platformSettings.textureCompression is always providing "Compressed", so we have to skip it.
+			return !areSettingsAsDesired;
+		#else
+			return false;
+		#endif
+		}
+
+		public static bool AdjustSpriteAtlasSettings (UnityEngine.U2D.SpriteAtlas spriteAtlas) {
+		#if EXPOSES_SPRITE_ATLAS_UTILITIES
+			UnityEditor.U2D.SpriteAtlasPackingSettings packingSettings = UnityEditor.U2D.SpriteAtlasExtensions.GetPackingSettings(spriteAtlas);
+			UnityEditor.U2D.SpriteAtlasTextureSettings textureSettings = UnityEditor.U2D.SpriteAtlasExtensions.GetTextureSettings(spriteAtlas);
+
+			packingSettings.enableRotation = true;
+			packingSettings.enableTightPacking = false;
+			UnityEditor.U2D.SpriteAtlasExtensions.SetPackingSettings(spriteAtlas, packingSettings);
+
+			textureSettings.readable = true;
+			textureSettings.generateMipMaps = false;
+			UnityEditor.U2D.SpriteAtlasExtensions.SetTextureSettings(spriteAtlas, textureSettings);
+
+			TextureImporterPlatformSettings platformSettings = new TextureImporterPlatformSettings();
+			platformSettings.textureCompression = TextureImporterCompression.Uncompressed;
+			platformSettings.crunchedCompression = false;
+			UnityEditor.U2D.SpriteAtlasExtensions.SetPlatformSettings(spriteAtlas, platformSettings);
+
+			string atlasPath = AssetDatabase.GetAssetPath(spriteAtlas);
+			Debug.Log(string.Format("Adjusted unsuitable SpriteAtlas settings '{0}'", atlasPath), spriteAtlas);
+			return false;
+		#else
+			return true;
+		#endif
+		}
+
+		public static bool GeneratePngFromSpriteAtlas (UnityEngine.U2D.SpriteAtlas spriteAtlas, out string texturePath) {
+			texturePath = System.IO.Path.ChangeExtension(AssetDatabase.GetAssetPath(spriteAtlas), ".png");
+			if (spriteAtlas == null)
+				return false;
+
+			Texture2D tempTexture = SpineSpriteAtlasAsset.AccessPackedTextureEditor(spriteAtlas);
+			if (tempTexture == null)
+				return false;
+
+			byte[] bytes = null;
+			try {
+				bytes = tempTexture.EncodeToPNG();
+			}
+			catch (System.Exception) {
+				// handled below
+			}
+			if (bytes == null || bytes.Length == 0) {
+				Debug.LogError("Could not read Compressed SpriteAtlas. Please enable 'Read/Write Enabled' and ensure 'Compression' is set to 'None' in Inspector.", spriteAtlas);
+				return false;
+			}
+			System.IO.File.WriteAllBytes(texturePath, bytes);
+			AssetDatabase.SaveAssets();
+			AssetDatabase.Refresh();
+			return System.IO.File.Exists(texturePath);
+		}
+
+		public static AtlasAssetBase IngestSpriteAtlas (UnityEngine.U2D.SpriteAtlas spriteAtlas, List<string> texturesWithoutMetaFile) {
+			if (spriteAtlas == null) {
+				Debug.LogWarning("SpriteAtlas source cannot be null!");
+				return null;
+			}
+
+			if (SpriteAtlasSettingsNeedAdjustment(spriteAtlas)) {
+				// settings need to be adjusted via the 'Spine SpriteAtlas Import' window if you want to use it as a Spine atlas.
+				return null;
+			}
+
+			Texture2D texture = null;
+			{ // only one page file
+				string texturePath;
+				GeneratePngFromSpriteAtlas(spriteAtlas, out texturePath);
+				texture = AssetDatabase.LoadAssetAtPath<Texture2D>(texturePath);
+				if (texture == null && System.IO.File.Exists(texturePath)) {
+					EditorUtility.SetDirty(spriteAtlas);
+					return null; // next iteration will load the texture as well.
+				}
+			}
+
+			string primaryName = spriteAtlas.name;
+			string assetPath = Path.GetDirectoryName(AssetDatabase.GetAssetPath(spriteAtlas)).Replace('\\', '/');
+
+			string atlasPath = assetPath + "/" + primaryName + SpriteAtlasSuffix + ".asset";
+
+			SpineSpriteAtlasAsset atlasAsset = AssetDatabase.LoadAssetAtPath<SpineSpriteAtlasAsset>(atlasPath);
+
+			List<Material> vestigialMaterials = new List<Material>();
+
+			if (atlasAsset == null)
+				atlasAsset = SpineSpriteAtlasAsset.CreateInstance<SpineSpriteAtlasAsset>();
+			else {
+				foreach (Material m in atlasAsset.materials)
+					vestigialMaterials.Add(m);
+			}
+
+			protectFromStackGarbageCollection.Add(atlasAsset);
+			atlasAsset.spriteAtlasFile = spriteAtlas;
+
+			int pagesCount = 1;
+			var populatingMaterials = new List<Material>(pagesCount);
+
+			{
+				string pageName = "SpriteAtlas";
+
+				string materialPath = assetPath + "/" + primaryName + "_" + pageName + ".mat";
+				Material mat = AssetDatabase.LoadAssetAtPath<Material>(materialPath);
+
+				if (mat == null) {
+					mat = new Material(Shader.Find(SpineEditorUtilities.Preferences.defaultShader));
+					ApplyPMAOrStraightAlphaSettings(mat, SpineEditorUtilities.Preferences.textureSettingsReference);
+					AssetDatabase.CreateAsset(mat, materialPath);
+				}
+				else {
+					vestigialMaterials.Remove(mat);
+				}
+
+				if (texture != null)
+					mat.mainTexture = texture;
+
+				EditorUtility.SetDirty(mat);
+				// note: don't call AssetDatabase.SaveAssets() since this would trigger OnPostprocessAllAssets() every time unnecessarily.
+				populatingMaterials.Add(mat); //atlasAsset.materials[i] = mat;
+			}
+
+			atlasAsset.materials = populatingMaterials.ToArray();
+
+			for (int i = 0; i < vestigialMaterials.Count; i++)
+				AssetDatabase.DeleteAsset(AssetDatabase.GetAssetPath(vestigialMaterials[i]));
+
+			if (AssetDatabase.GetAssetPath(atlasAsset) == "")
+				AssetDatabase.CreateAsset(atlasAsset, atlasPath);
+			else
+				atlasAsset.Clear();
+
+			atlasAsset.GetAtlas();
+			atlasAsset.updateRegionsInPlayMode = true;
+
+			EditorUtility.SetDirty(atlasAsset);
+			AssetDatabase.SaveAssets();
+
+			Debug.Log(string.Format("{0} :: Imported with {1} material", atlasAsset.name, atlasAsset.materials.Length), atlasAsset);
+
+			protectFromStackGarbageCollection.Remove(atlasAsset);
+			return (AtlasAssetBase)AssetDatabase.LoadAssetAtPath(atlasPath, typeof(AtlasAssetBase));
+		}
+
 		static bool SetDefaultTextureSettings (string texturePath, SpineAtlasAsset atlasAsset) {
 		static bool SetDefaultTextureSettings (string texturePath, SpineAtlasAsset atlasAsset) {
 			TextureImporter texImporter = (TextureImporter)TextureImporter.GetAtPath(texturePath);
 			TextureImporter texImporter = (TextureImporter)TextureImporter.GetAtPath(texturePath);
 			if (texImporter == null) {
 			if (texImporter == null) {
@@ -595,7 +761,7 @@ namespace Spine.Unity.Editor {
 			return true;
 			return true;
 		}
 		}
 
 
-	#if NEW_PREFERENCES_SETTINGS_PROVIDER
+#if NEW_PREFERENCES_SETTINGS_PROVIDER
 		static bool SetReferenceTextureSettings (string texturePath, SpineAtlasAsset atlasAsset, string referenceAssetPath) {
 		static bool SetReferenceTextureSettings (string texturePath, SpineAtlasAsset atlasAsset, string referenceAssetPath) {
 			var texturePreset = AssetDatabase.LoadAssetAtPath<UnityEditor.Presets.Preset>(referenceAssetPath);
 			var texturePreset = AssetDatabase.LoadAssetAtPath<UnityEditor.Presets.Preset>(referenceAssetPath);
 			bool isTexturePreset = texturePreset != null && texturePreset.GetTargetTypeName() == "TextureImporter";
 			bool isTexturePreset = texturePreset != null && texturePreset.GetTargetTypeName() == "TextureImporter";
@@ -613,7 +779,7 @@ namespace Spine.Unity.Editor {
 			AssetDatabase.SaveAssets();
 			AssetDatabase.SaveAssets();
 			return true;
 			return true;
 		}
 		}
-	#else
+#else
 		static bool SetReferenceTextureSettings (string texturePath, SpineAtlasAsset atlasAsset, string referenceAssetPath) {
 		static bool SetReferenceTextureSettings (string texturePath, SpineAtlasAsset atlasAsset, string referenceAssetPath) {
 			TextureImporter reference = TextureImporter.GetAtPath(referenceAssetPath) as TextureImporter;
 			TextureImporter reference = TextureImporter.GetAtPath(referenceAssetPath) as TextureImporter;
 			if (reference == null)
 			if (reference == null)
@@ -642,7 +808,7 @@ namespace Spine.Unity.Editor {
 			AssetDatabase.SaveAssets();
 			AssetDatabase.SaveAssets();
 			return true;
 			return true;
 		}
 		}
-	#endif
+#endif
 
 
 		static void ApplyPMAOrStraightAlphaSettings (Material material, string referenceTextureSettings) {
 		static void ApplyPMAOrStraightAlphaSettings (Material material, string referenceTextureSettings) {
 			bool isUsingPMAWorkflow = string.IsNullOrEmpty(referenceTextureSettings) ||
 			bool isUsingPMAWorkflow = string.IsNullOrEmpty(referenceTextureSettings) ||
@@ -650,9 +816,9 @@ namespace Spine.Unity.Editor {
 
 
 			MaterialChecks.EnablePMAAtMaterial(material, isUsingPMAWorkflow);
 			MaterialChecks.EnablePMAAtMaterial(material, isUsingPMAWorkflow);
 		}
 		}
-		#endregion
+#endregion
 
 
-		#region Import SkeletonData (json or binary)
+#region Import SkeletonData (json or binary)
 		internal static string GetSkeletonDataAssetFilePath(TextAsset spineJson) {
 		internal static string GetSkeletonDataAssetFilePath(TextAsset spineJson) {
 			string primaryName = Path.GetFileNameWithoutExtension(spineJson.name);
 			string primaryName = Path.GetFileNameWithoutExtension(spineJson.name);
 			string assetPath = Path.GetDirectoryName(AssetDatabase.GetAssetPath(spineJson)).Replace('\\', '/');
 			string assetPath = Path.GetDirectoryName(AssetDatabase.GetAssetPath(spineJson)).Replace('\\', '/');
@@ -996,7 +1162,7 @@ namespace Spine.Unity.Editor {
 				return null;
 				return null;
 			}
 			}
 
 
-			string spineGameObjectName = string.Format("Spine GameObject ({0})", skeletonDataAsset.name.Replace("_SkeletonData", ""));
+			string spineGameObjectName = string.Format("Spine GameObject ({0})", skeletonDataAsset.name.Replace(AssetUtility.SkeletonDataSuffix, ""));
 			GameObject go = EditorInstantiation.NewGameObject(spineGameObjectName, useObjectFactory,
 			GameObject go = EditorInstantiation.NewGameObject(spineGameObjectName, useObjectFactory,
 				typeof(MeshFilter), typeof(MeshRenderer), typeof(SkeletonAnimation));
 				typeof(MeshFilter), typeof(MeshRenderer), typeof(SkeletonAnimation));
 			SkeletonAnimation newSkeletonAnimation = go.GetComponent<SkeletonAnimation>();
 			SkeletonAnimation newSkeletonAnimation = go.GetComponent<SkeletonAnimation>();
@@ -1025,19 +1191,19 @@ namespace Spine.Unity.Editor {
 
 
 		/// <summary>Handles creating a new GameObject in the Unity Editor. This uses the new ObjectFactory API where applicable.</summary>
 		/// <summary>Handles creating a new GameObject in the Unity Editor. This uses the new ObjectFactory API where applicable.</summary>
 		public static GameObject NewGameObject (string name, bool useObjectFactory) {
 		public static GameObject NewGameObject (string name, bool useObjectFactory) {
-			#if NEW_PREFAB_SYSTEM
+#if NEW_PREFAB_SYSTEM
 			if (useObjectFactory)
 			if (useObjectFactory)
 				return ObjectFactory.CreateGameObject(name);
 				return ObjectFactory.CreateGameObject(name);
-			#endif
+#endif
 			return new GameObject(name);
 			return new GameObject(name);
 		}
 		}
 
 
 		/// <summary>Handles creating a new GameObject in the Unity Editor. This uses the new ObjectFactory API where applicable.</summary>
 		/// <summary>Handles creating a new GameObject in the Unity Editor. This uses the new ObjectFactory API where applicable.</summary>
 		public static GameObject NewGameObject (string name, bool useObjectFactory, params System.Type[] components) {
 		public static GameObject NewGameObject (string name, bool useObjectFactory, params System.Type[] components) {
-			#if NEW_PREFAB_SYSTEM
+#if NEW_PREFAB_SYSTEM
 			if (useObjectFactory)
 			if (useObjectFactory)
 				return ObjectFactory.CreateGameObject(name, components);
 				return ObjectFactory.CreateGameObject(name, components);
-			#endif
+#endif
 			return new GameObject(name, components);
 			return new GameObject(name, components);
 		}
 		}
 
 
@@ -1075,7 +1241,7 @@ namespace Spine.Unity.Editor {
 				return null;
 				return null;
 			}
 			}
 
 
-			string spineGameObjectName = string.Format("Spine Mecanim GameObject ({0})", skeletonDataAsset.name.Replace("_SkeletonData", ""));
+			string spineGameObjectName = string.Format("Spine Mecanim GameObject ({0})", skeletonDataAsset.name.Replace(AssetUtility.SkeletonDataSuffix, ""));
 			GameObject go = EditorInstantiation.NewGameObject(spineGameObjectName, useObjectFactory,
 			GameObject go = EditorInstantiation.NewGameObject(spineGameObjectName, useObjectFactory,
 				typeof(MeshFilter), typeof(MeshRenderer), typeof(Animator), typeof(SkeletonMecanim));
 				typeof(MeshFilter), typeof(MeshRenderer), typeof(Animator), typeof(SkeletonMecanim));
 
 

+ 170 - 0
spine-unity/Assets/Spine/Editor/spine-unity/Editor/Windows/SpriteAtlasImportWindow.cs

@@ -0,0 +1,170 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated January 1, 2020. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2020, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+using UnityEditor;
+
+namespace Spine.Unity.Editor {
+
+	using Editor = UnityEditor.Editor;
+	using Icons = SpineEditorUtilities.Icons;
+
+	public class SpriteAtlasImportWindow : EditorWindow {
+		const bool IsUtilityWindow = false;
+
+		[MenuItem("Window/Spine/SpriteAtlas Import", false, 5000)]
+		public static void Init (MenuCommand command) {
+			var window = EditorWindow.GetWindow<SpriteAtlasImportWindow>(IsUtilityWindow);
+			window.minSize = new Vector2(284f, 256f);
+			window.maxSize = new Vector2(500f, 256f);
+			window.titleContent = new GUIContent("Spine SpriteAtlas Import", Icons.spine);
+			window.Show();
+		}
+
+		public UnityEngine.U2D.SpriteAtlas spriteAtlasAsset;
+		public TextAsset skeletonDataFile;
+		public SpineSpriteAtlasAsset spineSpriteAtlasAsset;
+
+		SerializedObject so;
+
+		void OnEnable () {
+			if (!SpineSpriteAtlasAsset.AnySpriteAtlasNeedsRegionsLoaded())
+				return;
+			EditorApplication.update -= SpineSpriteAtlasAsset.UpdateWhenEditorPlayModeStarted;
+			EditorApplication.update += SpineSpriteAtlasAsset.UpdateWhenEditorPlayModeStarted;
+		}
+
+		void OnDisable () {
+			EditorApplication.update -= SpineSpriteAtlasAsset.UpdateWhenEditorPlayModeStarted;
+		}
+
+		void OnGUI () {
+			so = so ?? new SerializedObject(this);
+
+			EditorGUIUtility.wideMode = true;
+			EditorGUILayout.LabelField("Spine SpriteAtlas Import", EditorStyles.boldLabel);
+
+			using (new SpineInspectorUtility.BoxScope()) {
+				EditorGUI.BeginChangeCheck();
+				var spriteAtlasAssetProperty = so.FindProperty("spriteAtlasAsset");
+				EditorGUILayout.PropertyField(spriteAtlasAssetProperty, new GUIContent("SpriteAtlas", EditorGUIUtility.IconContent("SpriteAtlas Icon").image));
+				if (EditorGUI.EndChangeCheck()) {
+					so.ApplyModifiedProperties();
+					if (spriteAtlasAsset != null) {
+						if (AssetUtility.SpriteAtlasSettingsNeedAdjustment(spriteAtlasAsset)) {
+							AssetUtility.AdjustSpriteAtlasSettings(spriteAtlasAsset);
+						}
+						GenerateAssetsFromSpriteAtlas(spriteAtlasAsset);
+					}
+				}
+
+				var spineSpriteAtlasAssetProperty = so.FindProperty("spineSpriteAtlasAsset");
+				EditorGUI.BeginChangeCheck();
+				EditorGUILayout.PropertyField(spineSpriteAtlasAssetProperty, new GUIContent("SpineSpriteAtlasAsset", EditorGUIUtility.IconContent("ScriptableObject Icon").image));
+				if (spineSpriteAtlasAssetProperty.objectReferenceValue == null) {
+					spineSpriteAtlasAssetProperty.objectReferenceValue = spineSpriteAtlasAsset = FindSpineSpriteAtlasAsset(spriteAtlasAsset);
+				}
+				if (EditorGUI.EndChangeCheck()) {
+					so.ApplyModifiedProperties();
+				}
+				EditorGUILayout.Space();
+
+				using (new EditorGUI.DisabledScope(spineSpriteAtlasAsset == null)) {
+					if (SpineInspectorUtility.LargeCenteredButton(new GUIContent("Load regions by entering Play mode"))) {
+						GenerateAssetsFromSpriteAtlas(spriteAtlasAsset);
+						SpineSpriteAtlasAsset.UpdateByStartingEditorPlayMode();
+					}
+				}
+
+				using (new SpineInspectorUtility.BoxScope()) {
+					if (spriteAtlasAsset == null) {
+						EditorGUILayout.LabelField(SpineInspectorUtility.TempContent("Please assign SpriteAtlas file.", Icons.warning), GUILayout.Height(46));
+					}
+					else if (spineSpriteAtlasAsset == null || spineSpriteAtlasAsset.RegionsNeedLoading) {
+						EditorGUILayout.LabelField(SpineInspectorUtility.TempContent("Please hit 'Load regions ..' to load\nregion info. Play mode is started\nand stopped automatically.", Icons.warning), GUILayout.Height(54));
+					}
+					else {
+						EditorGUILayout.LabelField(SpineInspectorUtility.TempContent("SpriteAtlas imported\nsuccessfully.", Icons.spine), GUILayout.Height(46));
+					}
+				}
+			}
+
+			bool isAtlasComplete = (spineSpriteAtlasAsset != null && !spineSpriteAtlasAsset.RegionsNeedLoading);
+			bool canImportSkeleton = (spriteAtlasAsset != null && skeletonDataFile != null);
+			using (new SpineInspectorUtility.BoxScope()) {
+
+				using (new EditorGUI.DisabledScope(!isAtlasComplete)) {
+					var skeletonDataAssetProperty = so.FindProperty("skeletonDataFile");
+					EditorGUI.BeginChangeCheck();
+					EditorGUILayout.PropertyField(skeletonDataAssetProperty, SpineInspectorUtility.TempContent("Skeleton json/skel file", Icons.spine));
+					if (EditorGUI.EndChangeCheck()) {
+						so.ApplyModifiedProperties();
+					}
+					EditorGUILayout.Space();
+				}
+				using (new EditorGUI.DisabledScope(!canImportSkeleton)) {
+					if (SpineInspectorUtility.LargeCenteredButton(new GUIContent("Import Skeleton"))) {
+						//AssetUtility.IngestSpriteAtlas(spriteAtlasAsset, null);
+						string skeletonPath = AssetDatabase.GetAssetPath(skeletonDataFile);
+						string[] skeletons = new string[] { skeletonPath };
+						AssetUtility.ImportSpineContent(skeletons, null);
+					}
+				}
+			}
+		}
+
+		void GenerateAssetsFromSpriteAtlas (UnityEngine.U2D.SpriteAtlas spriteAtlasAsset) {
+			AssetUtility.IngestSpriteAtlas(spriteAtlasAsset, null);
+			string texturePath;
+			if (AssetUtility.GeneratePngFromSpriteAtlas(spriteAtlasAsset, out texturePath)) {
+				Debug.Log(string.Format("Generated SpriteAtlas texture '{0}'", texturePath), spriteAtlasAsset);
+			}
+		}
+
+		SpineSpriteAtlasAsset FindSpineSpriteAtlasAsset (UnityEngine.U2D.SpriteAtlas spriteAtlasAsset) {
+			string path = AssetDatabase.GetAssetPath(spriteAtlasAsset).Replace(".spriteatlas", AssetUtility.SpriteAtlasSuffix + ".asset");
+			if (System.IO.File.Exists(path)) {
+				return AssetDatabase.LoadAssetAtPath<SpineSpriteAtlasAsset>(path);
+			}
+			return null;
+		}
+
+		SkeletonDataAsset FindSkeletonDataAsset (TextAsset skeletonDataFile) {
+			string path = AssetDatabase.GetAssetPath(skeletonDataFile);
+			path = path.Replace(".json", AssetUtility.SkeletonDataSuffix + ".asset");
+			path = path.Replace(".skel.bytes", AssetUtility.SkeletonDataSuffix + ".asset");
+			if (System.IO.File.Exists(path)) {
+				return AssetDatabase.LoadAssetAtPath<SkeletonDataAsset>(path);
+			}
+			return null;
+		}
+	}
+}

+ 11 - 0
spine-unity/Assets/Spine/Editor/spine-unity/Editor/Windows/SpriteAtlasImportWindow.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: a5b99b091defeef439a0cb8c99fd8a51
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 397 - 0
spine-unity/Assets/Spine/Runtime/spine-unity/Asset Types/SpineSpriteAtlasAsset.cs

@@ -0,0 +1,397 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated January 1, 2020. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2020, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+#if UNITY_2018_2_OR_NEWER
+#define EXPOSES_SPRITE_ATLAS_UTILITIES
+#endif
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using UnityEngine;
+using Spine;
+using UnityEngine.U2D;
+
+#if UNITY_EDITOR
+using UnityEditor;
+using System.Reflection;
+#endif
+
+namespace Spine.Unity {
+	/// <summary>Loads and stores a Spine atlas and list of materials.</summary>
+	[CreateAssetMenu(fileName = "New Spine SpriteAtlas Asset", menuName = "Spine/Spine SpriteAtlas Asset")]
+	public class SpineSpriteAtlasAsset : AtlasAssetBase {
+		public SpriteAtlas spriteAtlasFile;
+		public Material[] materials;
+		protected Atlas atlas;
+		public bool updateRegionsInPlayMode;
+
+		[System.Serializable]
+		protected class SavedRegionInfo {
+			public float x, y, width, height;
+			public SpritePackingRotation packingRotation;
+		}
+		[SerializeField] protected SavedRegionInfo[] savedRegions;
+
+		public override bool IsLoaded { get { return this.atlas != null; } }
+
+		public override IEnumerable<Material> Materials { get { return materials; } }
+		public override int MaterialCount { get { return materials == null ? 0 : materials.Length; } }
+		public override Material PrimaryMaterial { get { return materials[0]; } }
+
+	#if UNITY_EDITOR
+		static MethodInfo GetPackedSpritesMethod, GetPreviewTexturesMethod;
+		#if !EXPOSES_SPRITE_ATLAS_UTILITIES
+		static MethodInfo PackAtlasesMethod;
+		#endif
+	#endif
+
+		#region Runtime Instantiation
+		/// <summary>
+		/// Creates a runtime AtlasAsset</summary>
+		public static SpineSpriteAtlasAsset CreateRuntimeInstance (SpriteAtlas spriteAtlasFile, Material[] materials, bool initialize) {
+			SpineSpriteAtlasAsset atlasAsset = ScriptableObject.CreateInstance<SpineSpriteAtlasAsset>();
+			atlasAsset.Reset();
+			atlasAsset.spriteAtlasFile = spriteAtlasFile;
+			atlasAsset.materials = materials;
+
+			if (initialize)
+				atlasAsset.GetAtlas();
+
+			return atlasAsset;
+		}
+		#endregion
+
+		void Reset () {
+			Clear();
+		}
+
+		public override void Clear () {
+			atlas = null;
+		}
+
+		/// <returns>The atlas or null if it could not be loaded.</returns>
+		public override Atlas GetAtlas () {
+			if (spriteAtlasFile == null) {
+				Debug.LogError("SpriteAtlas file not set for SpineSpriteAtlasAsset: " + name, this);
+				Clear();
+				return null;
+			}
+
+			if (materials == null || materials.Length == 0) {
+				Debug.LogError("Materials not set for SpineSpriteAtlasAsset: " + name, this);
+				Clear();
+				return null;
+			}
+
+			if (atlas != null) return atlas;
+
+			try {
+				atlas = LoadAtlas(spriteAtlasFile);
+				return atlas;
+			} catch (Exception ex) {
+				Debug.LogError("Error analyzing SpriteAtlas for SpineSpriteAtlasAsset: " + name + "\n" + ex.Message + "\n" + ex.StackTrace, this);
+				return null;
+			}
+		}
+
+		protected void AssignRegionsFromSavedRegions (Sprite[] sprites, Atlas usedAtlas) {
+
+			if (savedRegions == null || savedRegions.Length != sprites.Length)
+				return;
+
+			int i = 0;
+			foreach (var region in usedAtlas) {
+				var savedRegion = savedRegions[i];
+				var page = region.page;
+
+				region.degrees = savedRegion.packingRotation == SpritePackingRotation.None ? 0 : 90;
+				region.rotate = region.degrees != 0;
+
+				float x = savedRegion.x;
+				float y = savedRegion.y;
+				float width = savedRegion.width;
+				float height = savedRegion.height;
+
+				region.u = x / (float)page.width;
+				region.v = y / (float)page.height;
+				if (region.rotate) {
+					region.u2 = (x + height) / (float)page.width;
+					region.v2 = (y + width) / (float)page.height;
+				}
+				else {
+					region.u2 = (x + width) / (float)page.width;
+					region.v2 = (y + height) / (float)page.height;
+				}
+				region.x = (int)x;
+				region.y = (int)y;
+				region.width = Math.Abs((int)width);
+				region.height = Math.Abs((int)height);
+
+				// flip upside down
+				var temp = region.v;
+				region.v = region.v2;
+				region.v2 = temp;
+
+				region.originalWidth = (int)width;
+				region.originalHeight = (int)height;
+
+				// note: currently sprite pivot offsets are ignored.
+				// var sprite = sprites[i];
+				region.offsetX = 0;//sprite.pivot.x;
+				region.offsetY = 0;//sprite.pivot.y;
+
+				++i;
+			}
+		}
+
+		private Atlas LoadAtlas (UnityEngine.U2D.SpriteAtlas spriteAtlas) {
+
+			List<AtlasPage> pages = new List<AtlasPage>();
+			List<AtlasRegion> regions = new List<AtlasRegion>();
+
+			Sprite[] sprites = new UnityEngine.Sprite[spriteAtlas.spriteCount];
+			spriteAtlas.GetSprites(sprites);
+			if (sprites.Length == 0)
+				return new Atlas(pages, regions);
+
+			Texture2D texture = null;
+			#if UNITY_EDITOR
+			if (!Application.isPlaying)
+				texture = AccessPackedTextureEditor(spriteAtlas);
+			else
+			#endif
+				texture = AccessPackedTexture(sprites);
+
+			Material material = materials[0];
+		#if !UNITY_EDITOR
+			material.mainTexture = texture;
+		#endif
+
+			Spine.AtlasPage page = new AtlasPage();
+			page.name = spriteAtlas.name;
+			page.width = texture.width;
+			page.height = texture.height;
+			page.format = Spine.Format.RGBA8888;
+
+			page.minFilter = TextureFilter.Linear;
+			page.magFilter = TextureFilter.Linear;
+			page.uWrap = TextureWrap.ClampToEdge;
+			page.vWrap = TextureWrap.ClampToEdge;
+			page.rendererObject = material;
+			pages.Add(page);
+
+			sprites = AccessPackedSprites(spriteAtlas);
+
+			int i = 0;
+			for ( ; i < sprites.Length; ++i) {
+				var sprite = sprites[i];
+				AtlasRegion region = new AtlasRegion();
+				region.name = sprite.name.Replace("(Clone)", "");
+				region.page = page;
+				region.degrees = sprite.packingRotation == SpritePackingRotation.None ? 0 : 90;
+				region.rotate = region.degrees != 0;
+
+				region.u2 = 1;
+				region.v2 = 1;
+				region.width = page.width;
+				region.height = page.height;
+				region.originalWidth = page.width;
+				region.originalHeight = page.height;
+
+				region.index = i;
+				regions.Add(region);
+			}
+
+			var atlas = new Atlas(pages, regions);
+			AssignRegionsFromSavedRegions(sprites, atlas);
+
+			return atlas;
+		}
+
+#if UNITY_EDITOR
+		public static void UpdateByStartingEditorPlayMode () {
+			EditorApplication.isPlaying = true;
+		}
+
+		public static bool AnySpriteAtlasNeedsRegionsLoaded () {
+			string[] guids = UnityEditor.AssetDatabase.FindAssets("t:SpineSpriteAtlasAsset");
+			foreach (var guid in guids) {
+				string path = UnityEditor.AssetDatabase.GUIDToAssetPath(guid);
+				if (!string.IsNullOrEmpty(path)) {
+					var atlasAsset = UnityEditor.AssetDatabase.LoadAssetAtPath<SpineSpriteAtlasAsset>(path);
+					if (atlasAsset) {
+						if (atlasAsset.RegionsNeedLoading)
+							return true;
+					}
+				}
+			}
+			return false;
+		}
+
+		public static void UpdateWhenEditorPlayModeStarted () {
+			if (!EditorApplication.isPlaying)
+				return;
+
+			EditorApplication.update -= UpdateWhenEditorPlayModeStarted;
+			string[] guids = UnityEditor.AssetDatabase.FindAssets("t:SpineSpriteAtlasAsset");
+			if (guids.Length == 0)
+				return;
+
+			Debug.Log("Updating SpineSpriteAtlasAssets");
+			foreach (var guid in guids) {
+				string path = UnityEditor.AssetDatabase.GUIDToAssetPath(guid);
+				if (!string.IsNullOrEmpty(path)) {
+					var atlasAsset = UnityEditor.AssetDatabase.LoadAssetAtPath<SpineSpriteAtlasAsset>(path);
+					if (atlasAsset) {
+						atlasAsset.atlas = atlasAsset.LoadAtlas(atlasAsset.spriteAtlasFile);
+						atlasAsset.LoadRegionsInEditorPlayMode();
+						Debug.Log(string.Format("Updated regions of '{0}'", atlasAsset.name), atlasAsset);
+					}
+				}
+			}
+
+			EditorApplication.isPlaying = false;
+		}
+
+		public bool RegionsNeedLoading {
+			get { return savedRegions == null || savedRegions.Length == 0 || updateRegionsInPlayMode; }
+		}
+
+		public void LoadRegionsInEditorPlayMode () {
+
+			Sprite[] sprites = null;
+			System.Type T = Type.GetType("UnityEditor.U2D.SpriteAtlasExtensions,UnityEditor");
+			var method = T.GetMethod("GetPackedSprites", BindingFlags.NonPublic | BindingFlags.Static);
+			if (method != null) {
+				object retval = method.Invoke(null, new object[] { spriteAtlasFile });
+				var spritesArray = retval as Sprite[];
+				if (spritesArray != null && spritesArray.Length > 0) {
+					sprites = spritesArray;
+				}
+			}
+			if (sprites == null) {
+				sprites = new UnityEngine.Sprite[spriteAtlasFile.spriteCount];
+				spriteAtlasFile.GetSprites(sprites);
+			}
+			if (sprites.Length == 0) {
+				Debug.LogWarning(string.Format("SpriteAtlas '{0}' contains no sprites. Please make sure all assigned images are set to import type 'Sprite'.", spriteAtlasFile.name), spriteAtlasFile);
+				return;
+			}
+			else if (sprites[0].packingMode == SpritePackingMode.Tight) {
+				Debug.LogError(string.Format("SpriteAtlas '{0}': Tight packing is not supported. Please disable 'Tight Packing' in the SpriteAtlas Inspector.", spriteAtlasFile.name), spriteAtlasFile);
+				return;
+			}
+
+			if (savedRegions == null || savedRegions.Length != sprites.Length)
+				savedRegions = new SavedRegionInfo[sprites.Length];
+
+			int i = 0;
+			foreach (var region in atlas) {
+				var sprite = sprites[i];
+				var rect = sprite.textureRect;
+				float x = rect.min.x;
+				float y = rect.min.y;
+				float width = rect.width;
+				float height = rect.height;
+
+				var savedRegion = new SavedRegionInfo();
+				savedRegion.x = x;
+				savedRegion.y = y;
+				savedRegion.width = width;
+				savedRegion.height = height;
+				savedRegion.packingRotation = sprite.packingRotation;
+				savedRegions[i] = savedRegion;
+
+				++i;
+			}
+			updateRegionsInPlayMode = false;
+			AssignRegionsFromSavedRegions(sprites, atlas);
+			EditorUtility.SetDirty(this);
+			AssetDatabase.SaveAssets();
+		}
+
+		public static Texture2D AccessPackedTextureEditor (SpriteAtlas spriteAtlas) {
+		#if EXPOSES_SPRITE_ATLAS_UTILITIES
+			UnityEditor.U2D.SpriteAtlasUtility.PackAtlases(new SpriteAtlas[] { spriteAtlas }, EditorUserBuildSettings.activeBuildTarget);
+		#else
+			/*if (PackAtlasesMethod == null) {
+				System.Type T = Type.GetType("UnityEditor.U2D.SpriteAtlasUtility,UnityEditor");
+				PackAtlasesMethod = T.GetMethod("PackAtlases", BindingFlags.NonPublic | BindingFlags.Static);
+			}
+			if (PackAtlasesMethod != null) {
+				PackAtlasesMethod.Invoke(null, new object[] { new SpriteAtlas[] { spriteAtlas }, EditorUserBuildSettings.activeBuildTarget });
+			}*/
+		#endif
+			if (GetPreviewTexturesMethod == null) {
+				System.Type T = Type.GetType("UnityEditor.U2D.SpriteAtlasExtensions,UnityEditor");
+				GetPreviewTexturesMethod = T.GetMethod("GetPreviewTextures", BindingFlags.NonPublic | BindingFlags.Static);
+			}
+			if (GetPreviewTexturesMethod != null) {
+				object retval = GetPreviewTexturesMethod.Invoke(null, new object[] { spriteAtlas });
+				var textures = retval as Texture2D[];
+				if (textures.Length > 0)
+					return textures[0];
+			}
+			return null;
+		}
+#endif
+		public static Texture2D AccessPackedTexture (Sprite[] sprites) {
+			return sprites[0].texture;
+		}
+
+
+		public static Sprite[] AccessPackedSprites (UnityEngine.U2D.SpriteAtlas spriteAtlas) {
+			Sprite[] sprites = null;
+#if UNITY_EDITOR
+			if (!Application.isPlaying) {
+
+				if (GetPackedSpritesMethod == null) {
+					System.Type T = Type.GetType("UnityEditor.U2D.SpriteAtlasExtensions,UnityEditor");
+					GetPackedSpritesMethod = T.GetMethod("GetPackedSprites", BindingFlags.NonPublic | BindingFlags.Static);
+				}
+				if (GetPackedSpritesMethod != null) {
+					object retval = GetPackedSpritesMethod.Invoke(null, new object[] { spriteAtlas });
+					var spritesArray = retval as Sprite[];
+					if (spritesArray != null && spritesArray.Length > 0) {
+						sprites = spritesArray;
+					}
+				}
+			}
+#endif
+			if (sprites == null) {
+				sprites = new UnityEngine.Sprite[spriteAtlas.spriteCount];
+				spriteAtlas.GetSprites(sprites);
+				if (sprites.Length == 0)
+					return null;
+			}
+			return sprites;
+		}
+	}
+}

+ 11 - 0
spine-unity/Assets/Spine/Runtime/spine-unity/Asset Types/SpineSpriteAtlasAsset.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: ce59897dd7e6cbc4690a05ebaf975dff
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 1 - 2
spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonMecanim.cs

@@ -417,9 +417,8 @@ namespace Spine.Unity {
 					var interruptingStateInfo = animator.GetNextAnimatorStateInfo(layer);
 					var interruptingStateInfo = animator.GetNextAnimatorStateInfo(layer);
 					layerInfos.isLastFrameOfInterruption = interruptingStateInfo.fullPathHash == 0;
 					layerInfos.isLastFrameOfInterruption = interruptingStateInfo.fullPathHash == 0;
 					if (!layerInfos.isLastFrameOfInterruption) {
 					if (!layerInfos.isLastFrameOfInterruption) {
-						layerInfos.interruptingClipInfoCount = interruptingClipInfos.Count;
-
 						animator.GetNextAnimatorClipInfo(layer, interruptingClipInfos);
 						animator.GetNextAnimatorClipInfo(layer, interruptingClipInfos);
+						layerInfos.interruptingClipInfoCount = interruptingClipInfos.Count;
 						float oldTime = layerInfos.interruptingStateInfo.normalizedTime;
 						float oldTime = layerInfos.interruptingStateInfo.normalizedTime;
 						float newTime = interruptingStateInfo.normalizedTime;
 						float newTime = interruptingStateInfo.normalizedTime;
 						layerInfos.interruptingClipTimeAddition = newTime - oldTime;
 						layerInfos.interruptingClipTimeAddition = newTime - oldTime;

+ 19 - 2
spine-unity/Assets/Spine/Runtime/spine-unity/Utility/AtlasUtilities.cs

@@ -29,7 +29,7 @@
 
 
 using UnityEngine;
 using UnityEngine;
 using System.Collections.Generic;
 using System.Collections.Generic;
-using System.Collections;
+using System;
 
 
 namespace Spine.Unity.AttachmentTools {
 namespace Spine.Unity.AttachmentTools {
 
 
@@ -537,7 +537,24 @@ namespace Spine.Unity.AttachmentTools {
 			bool mipmaps = UseMipMaps, bool linear = false, bool applyPMA = false) {
 			bool mipmaps = UseMipMaps, bool linear = false, bool applyPMA = false) {
 
 
 			var spriteTexture = s.texture;
 			var spriteTexture = s.texture;
-			var r = s.textureRect;
+			Rect r;
+			if (!s.packed || s.packingMode == SpritePackingMode.Rectangle) {
+				r = s.textureRect;
+			}
+			else {
+				r = new Rect();
+				r.xMin = Math.Min(s.uv[0].x, s.uv[1].x) * spriteTexture.width;
+				r.xMax = Math.Max(s.uv[0].x, s.uv[1].x) * spriteTexture.width;
+				r.yMin = Math.Min(s.uv[0].y, s.uv[2].y) * spriteTexture.height;
+				r.yMax = Math.Max(s.uv[0].y, s.uv[2].y) * spriteTexture.height;
+			#if UNITY_EDITOR
+				if (s.uv.Length > 4) {
+					Debug.LogError("When using a tightly packed SpriteAtlas with Spine, you may only access Sprites that are packed as 'FullRect' from it! " +
+						"You can either disable 'Tight Packing' at the whole SpriteAtlas, or change the single Sprite's TextureImporter Setting 'MeshType' to 'Full Rect'." +
+						"Sprite Asset: " + s.name, s);
+				}
+			#endif
+			}
 			var newTexture = new Texture2D((int)r.width, (int)r.height, textureFormat, mipmaps, linear);
 			var newTexture = new Texture2D((int)r.width, (int)r.height, textureFormat, mipmaps, linear);
 			newTexture.CopyTextureAttributesFrom(spriteTexture);
 			newTexture.CopyTextureAttributesFrom(spriteTexture);
 			if (applyPMA)
 			if (applyPMA)

+ 5 - 0
spine-unity/Assets/Spine/Runtime/spine-unity/Utility/MaterialChecks.cs

@@ -38,6 +38,7 @@ namespace Spine.Unity {
 
 
 		static readonly int STRAIGHT_ALPHA_PARAM_ID = Shader.PropertyToID("_StraightAlphaInput");
 		static readonly int STRAIGHT_ALPHA_PARAM_ID = Shader.PropertyToID("_StraightAlphaInput");
 		static readonly string ALPHAPREMULTIPLY_ON_KEYWORD = "_ALPHAPREMULTIPLY_ON";
 		static readonly string ALPHAPREMULTIPLY_ON_KEYWORD = "_ALPHAPREMULTIPLY_ON";
+		static readonly string STRAIGHT_ALPHA_KEYWORD = "_STRAIGHT_ALPHA_INPUT";
 
 
 		public static readonly string kPMANotSupportedLinearMessage =
 		public static readonly string kPMANotSupportedLinearMessage =
 			"Warning: Premultiply-alpha atlas textures not supported in Linear color space!\n\nPlease\n"
 			"Warning: Premultiply-alpha atlas textures not supported in Linear color space!\n\nPlease\n"
@@ -134,6 +135,10 @@ namespace Spine.Unity {
 		public static void EnablePMAAtMaterial (Material material, bool enablePMA) {
 		public static void EnablePMAAtMaterial (Material material, bool enablePMA) {
 			if (material.HasProperty(STRAIGHT_ALPHA_PARAM_ID)) {
 			if (material.HasProperty(STRAIGHT_ALPHA_PARAM_ID)) {
 				material.SetInt(STRAIGHT_ALPHA_PARAM_ID, enablePMA ? 0 : 1);
 				material.SetInt(STRAIGHT_ALPHA_PARAM_ID, enablePMA ? 0 : 1);
+				if (enablePMA)
+					material.DisableKeyword(STRAIGHT_ALPHA_KEYWORD);
+				else
+					material.EnableKeyword(STRAIGHT_ALPHA_KEYWORD);
 			}
 			}
 			else {
 			else {
 				if (enablePMA)
 				if (enablePMA)