Jelajahi Sumber

IK constraints for spine-libgdx.

Note Spine doesn't yet export IK data, so spine-libgdx doesn't yet load it. Soon!
NathanSweet 11 tahun lalu
induk
melakukan
6444e8e934

+ 61 - 2
spine-libgdx/src/com/esotericsoftware/spine/Animation.java

@@ -273,7 +273,7 @@ public class Animation {
 			float prevFrameValue = frames[frameIndex - 1];
 			float frameTime = frames[frameIndex];
 			float percent = MathUtils.clamp(1 - (time - frameTime) / (frames[frameIndex + PREV_FRAME_TIME] - frameTime), 0, 1);
-			percent = getCurvePercent(frameIndex / 2 - 1, percent);
+			percent = getCurvePercent((frameIndex >> 1) - 1, percent);
 
 			float amount = frames[frameIndex + FRAME_VALUE] - prevFrameValue;
 			while (amount > 180)
@@ -657,7 +657,7 @@ public class Animation {
 
 			float[] frames = this.frames;
 			if (time < frames[0]) return; // Time is before first frame.
-			
+
 			float[][] frameVertices = this.frameVertices;
 			int vertexCount = frameVertices[0].length;
 
@@ -700,4 +700,63 @@ public class Animation {
 			}
 		}
 	}
+
+	static public class IkConstraintTimeline extends CurveTimeline {
+		static private final int PREV_FRAME_TIME = -2;
+		static private final int FRAME_VALUE = 1;
+
+		int ikConstraintIndex;
+		private final float[] frames; // time, mix, ...
+		private final int[] bendDirections;
+
+		public IkConstraintTimeline (int frameCount) {
+			super(frameCount);
+			frames = new float[frameCount * 2];
+			bendDirections = new int[frameCount];
+		}
+
+		public void setIkConstraintIndex (int ikConstraint) {
+			this.ikConstraintIndex = ikConstraint;
+		}
+
+		public int getIkConstraintIndex () {
+			return ikConstraintIndex;
+		}
+
+		public float[] getFrames () {
+			return frames;
+		}
+
+		/** Sets the time and mix and bend direction of the specified keyframe. */
+		public void setFrame (int frameIndex, float time, float mix, int bendDirection) {
+			bendDirections[frameIndex] = bendDirection;
+			frameIndex *= 2;
+			frames[frameIndex] = time;
+			frames[frameIndex + 1] = mix;
+		}
+
+		public void apply (Skeleton skeleton, float lastTime, float time, Array<Event> events, float alpha) {
+			float[] frames = this.frames;
+			if (time < frames[0]) return; // Time is before first frame.
+
+			IkConstraint ikConstraint = skeleton.ikConstraints.get(ikConstraintIndex);
+
+			if (time >= frames[frames.length - 2]) { // Time is after last frame.
+				ikConstraint.mix += (frames[frames.length - 1] - ikConstraint.mix) * alpha;
+				ikConstraint.bendDirection = bendDirections[bendDirections.length - 1];
+				return;
+			}
+
+			// Interpolate between the previous frame and the current frame.
+			int frameIndex = binarySearch(frames, time, 2);
+			float prevFrameValue = frames[frameIndex - 1];
+			float frameTime = frames[frameIndex];
+			float percent = MathUtils.clamp(1 - (time - frameTime) / (frames[frameIndex + PREV_FRAME_TIME] - frameTime), 0, 1);
+			percent = getCurvePercent((frameIndex >> 1) - 1, percent);
+
+			float mix = prevFrameValue + (frames[frameIndex + FRAME_VALUE] - prevFrameValue) * percent;
+			ikConstraint.mix += (mix - ikConstraint.mix) * alpha;
+			ikConstraint.bendDirection = bendDirections[(frameIndex - 2) >> 1];
+		}
+	}
 }

+ 45 - 4
spine-libgdx/src/com/esotericsoftware/spine/Bone.java

@@ -34,17 +34,18 @@ import static com.badlogic.gdx.math.Matrix3.*;
 
 import com.badlogic.gdx.math.MathUtils;
 import com.badlogic.gdx.math.Matrix3;
+import com.badlogic.gdx.math.Vector2;
 
 public class Bone {
 	final BoneData data;
 	final Bone parent;
 	float x, y;
-	float rotation;
+	float rotation, rotationIK;
 	float scaleX, scaleY;
 
 	float m00, m01, worldX; // a b x
 	float m10, m11, worldY; // c d y
-	float worldRotation;
+	float worldRotation, worldCos, worldSin;
 	float worldScaleX, worldScaleY;
 
 	/** @param parent May be null. */
@@ -64,6 +65,7 @@ public class Bone {
 		x = bone.x;
 		y = bone.y;
 		rotation = bone.rotation;
+		rotationIK = bone.rotationIK;
 		scaleX = bone.scaleX;
 		scaleY = bone.scaleY;
 	}
@@ -71,6 +73,7 @@ public class Bone {
 	/** Computes the world SRT using the parent bone and the local SRT. */
 	public void updateWorldTransform (boolean flipX, boolean flipY) {
 		Bone parent = this.parent;
+		float x = this.x, y = this.y;
 		if (parent != null) {
 			worldX = x * parent.m00 + y * parent.m01 + parent.worldX;
 			worldY = x * parent.m10 + y * parent.m11 + parent.worldY;
@@ -81,16 +84,18 @@ public class Bone {
 				worldScaleX = scaleX;
 				worldScaleY = scaleY;
 			}
-			worldRotation = data.inheritRotation ? parent.worldRotation + rotation : rotation;
+			worldRotation = data.inheritRotation ? parent.worldRotation + rotationIK : rotationIK;
 		} else {
 			worldX = flipX ? -x : x;
 			worldY = flipY ? -y : y;
 			worldScaleX = scaleX;
 			worldScaleY = scaleY;
-			worldRotation = rotation;
+			worldRotation = rotationIK;
 		}
 		float cos = MathUtils.cosDeg(worldRotation);
 		float sin = MathUtils.sinDeg(worldRotation);
+		worldCos = cos;
+		worldSin = sin;
 		m00 = cos * worldScaleX;
 		m10 = sin * worldScaleX;
 		m01 = -sin * worldScaleY;
@@ -110,6 +115,7 @@ public class Bone {
 		x = data.x;
 		y = data.y;
 		rotation = data.rotation;
+		rotationIK = rotation;
 		scaleX = data.scaleX;
 		scaleY = data.scaleY;
 	}
@@ -143,6 +149,7 @@ public class Bone {
 		this.y = y;
 	}
 
+	/** Returns the forward kinetics rotation. */
 	public float getRotation () {
 		return rotation;
 	}
@@ -151,6 +158,15 @@ public class Bone {
 		this.rotation = rotation;
 	}
 
+	/** Returns the inverse kinetics rotation, as calculated by any IK constraints. */
+	public float getRotationIK () {
+		return rotationIK;
+	}
+
+	public void setRotationIK (float rotationIK) {
+		this.rotationIK = rotationIK;
+	}
+
 	public float getScaleX () {
 		return scaleX;
 	}
@@ -205,6 +221,14 @@ public class Bone {
 		return worldRotation;
 	}
 
+	public float getWorldCos () {
+		return worldCos;
+	}
+
+	public float getWorldSin () {
+		return worldSin;
+	}
+
 	public float getWorldScaleX () {
 		return worldScaleX;
 	}
@@ -228,6 +252,23 @@ public class Bone {
 		return worldTransform;
 	}
 
+	public Vector2 worldToLocal (Vector2 world) {
+		float x = world.x - worldX;
+		float y = world.y - worldY;
+		float cos = worldCos;
+		float sin = -worldSin;
+		world.x = (x * cos - y * sin) / worldScaleX;
+		world.y = (x * sin + y * cos) / worldScaleY;
+		return world;
+	}
+
+	public Vector2 localToWorld (Vector2 local) {
+		float x = local.x, y = local.y;
+		local.x = x * m00 + y * m01 + worldX;
+		local.y = x * m10 + y * m11 + worldY;
+		return local;
+	}
+
 	public String toString () {
 		return data.name;
 	}

+ 10 - 0
spine-libgdx/src/com/esotericsoftware/spine/BoneData.java

@@ -98,6 +98,11 @@ public class BoneData {
 		this.y = y;
 	}
 
+	public void setPosition (float x, float y) {
+		this.x = x;
+		this.y = y;
+	}
+
 	public float getRotation () {
 		return rotation;
 	}
@@ -122,6 +127,11 @@ public class BoneData {
 		this.scaleY = scaleY;
 	}
 
+	public void setScale (float scaleX, float scaleY) {
+		this.scaleX = scaleX;
+		this.scaleY = scaleY;
+	}
+
 	public boolean getInheritScale () {
 		return inheritScale;
 	}

+ 132 - 0
spine-libgdx/src/com/esotericsoftware/spine/IkConstraint.java

@@ -0,0 +1,132 @@
+
+package com.esotericsoftware.spine;
+
+import com.badlogic.gdx.math.MathUtils;
+import com.badlogic.gdx.math.Vector2;
+import com.badlogic.gdx.utils.Array;
+
+public class IkConstraint {
+	static private final Vector2 temp = new Vector2();
+
+	final IkConstraintData data;
+	final Array<Bone> bones;
+	Bone target;
+	float mix = 1;
+	int bendDirection;
+
+	public IkConstraint (IkConstraintData data, Skeleton skeleton) {
+		this.data = data;
+		mix = data.mix;
+		bendDirection = data.bendDirection;
+
+		bones = new Array(data.bones.size);
+		if (skeleton != null) {
+			for (BoneData boneData : data.bones)
+				bones.add(skeleton.findBone(boneData.name));
+			target = skeleton.findBone(data.target.name);
+		}
+	}
+
+	/** Copy constructor. */
+	public IkConstraint (IkConstraint ikConstraint) {
+		data = ikConstraint.data;
+		bones = new Array(ikConstraint.bones);
+		target = ikConstraint.target;
+		mix = ikConstraint.mix;
+		bendDirection = ikConstraint.bendDirection;
+	}
+
+	public void apply () {
+		Bone target = this.target;
+		Array<Bone> bones = this.bones;
+		apply(bones.first(), bones.get(1), target.worldX, target.worldY, bendDirection, mix);
+	}
+
+	public Array<Bone> getBones () {
+		return bones;
+	}
+
+	public Bone getTarget () {
+		return target;
+	}
+
+	public void setTarget (Bone target) {
+		this.target = target;
+	}
+
+	public float getMix () {
+		return mix;
+	}
+
+	public void setMix (float mix) {
+		this.mix = mix;
+	}
+
+	public int getBendDirection () {
+		return bendDirection;
+	}
+
+	public void setBendDirection (int bendDirection) {
+		this.bendDirection = bendDirection;
+	}
+
+	public IkConstraintData getData () {
+		return data;
+	}
+
+	public String toString () {
+		return data.name;
+	}
+
+	/** Adjusts the parent and child bone rotations so the tip of the child is as close to the target position as possible. The
+	 * target is specified in the world coordinate system.
+	 * @param child Any descendant bone of the parent. */
+	static public void apply (Bone parent, Bone child, float targetX, float targetY, int bendDirection, float alpha) {
+		float childRotation = child.rotation, parentRotation = parent.rotation;
+		if (alpha == 0) {
+			child.rotationIK = childRotation;
+			parent.rotationIK = parentRotation;
+			return;
+		}
+		Vector2 position = temp;
+		Bone parentParent = parent.parent;
+		if (parentParent != null) {
+			parentParent.worldToLocal(position.set(targetX, targetY));
+			targetX = (position.x - parent.x) * parentParent.worldScaleX;
+			targetY = (position.y - parent.y) * parentParent.worldScaleY;
+		} else {
+			targetX -= parent.x;
+			targetY -= parent.y;
+		}
+		if (child.parent == parent)
+			position.set(child.x, child.y);
+		else
+			parent.worldToLocal(child.parent.localToWorld(position.set(child.x, child.y)));
+		float childX = position.x * parent.worldScaleX, childY = position.y * parent.worldScaleY;
+		float offset = (float)Math.atan2(childY, childX);
+		float len1 = (float)Math.sqrt(childX * childX + childY * childY), len2 = child.data.length * child.worldScaleX;
+		// Based on code by Ryan Juckett with permission: Copyright (c) 2008-2009 Ryan Juckett, http://www.ryanjuckett.com/
+		float cosDenom = 2 * len1 * len2;
+		if (cosDenom < 0.0001f) {
+			child.rotationIK = childRotation
+				+ ((float)Math.atan2(targetY, targetX) * MathUtils.radDeg - parentRotation - childRotation) * alpha;
+			return;
+		}
+		float cos = MathUtils.clamp((targetX * targetX + targetY * targetY - len1 * len1 - len2 * len2) / cosDenom, -1, 1);
+		float childAngle = (float)Math.acos(cos) * bendDirection;
+		float adjacent = len1 + len2 * cos, opposite = len2 * MathUtils.sin(childAngle);
+		float parentAngle = (float)Math.atan2(targetY * adjacent - targetX * opposite, targetX * adjacent + targetY * opposite);
+		float rotation = (parentAngle - offset) * MathUtils.radDeg - parentRotation;
+		if (rotation > 180)
+			rotation -= 360;
+		else if (rotation < -180) //
+			rotation += 360;
+		parent.rotationIK = parentRotation + rotation * alpha;
+		rotation = (childAngle + offset) * MathUtils.radDeg - childRotation;
+		if (rotation > 180)
+			rotation -= 360;
+		else if (rotation < -180) //
+			rotation += 360;
+		child.rotationIK = childRotation + (rotation + parent.worldRotation - child.parent.worldRotation) * alpha;
+	}
+}

+ 52 - 0
spine-libgdx/src/com/esotericsoftware/spine/IkConstraintData.java

@@ -0,0 +1,52 @@
+
+package com.esotericsoftware.spine;
+
+import com.badlogic.gdx.utils.Array;
+
+public class IkConstraintData {
+	final String name;
+	final Array<BoneData> bones = new Array();
+	BoneData target;
+	int bendDirection = 1;
+	float mix = 1;
+
+	public IkConstraintData (String name) {
+		this.name = name;
+	}
+
+	public String getName () {
+		return name;
+	}
+
+	public Array<BoneData> getBones () {
+		return bones;
+	}
+
+	public BoneData getTarget () {
+		return target;
+	}
+
+	public void setTarget (BoneData target) {
+		this.target = target;
+	}
+
+	public int getBendDirection () {
+		return bendDirection;
+	}
+
+	public void setBendDirection (int bendDirection) {
+		this.bendDirection = bendDirection;
+	}
+
+	public float getMix () {
+		return mix;
+	}
+
+	public void setMix (float mix) {
+		this.mix = mix;
+	}
+
+	public String toString () {
+		return name;
+	}
+}

+ 94 - 6
spine-libgdx/src/com/esotericsoftware/spine/Skeleton.java

@@ -39,6 +39,8 @@ public class Skeleton {
 	final SkeletonData data;
 	final Array<Bone> bones;
 	final Array<Slot> slots;
+	final Array<IkConstraint> ikConstraints;
+	private final Array<Array<Bone>> updateBonesCache = new Array();
 	Array<Slot> drawOrder;
 	Skin skin;
 	final Color color;
@@ -65,7 +67,13 @@ public class Skeleton {
 			drawOrder.add(slot);
 		}
 
+		ikConstraints = new Array(data.ikConstraints.size);
+		for (IkConstraintData ikConstraintData : data.ikConstraints)
+			ikConstraints.add(new IkConstraint(ikConstraintData, this));
+
 		color = new Color(1, 1, 1, 1);
+
+		updateCache();
 	}
 
 	/** Copy constructor. */
@@ -82,26 +90,84 @@ public class Skeleton {
 		slots = new Array(skeleton.slots.size);
 		for (Slot slot : skeleton.slots) {
 			Bone bone = bones.get(skeleton.bones.indexOf(slot.bone, true));
-			Slot newSlot = new Slot(slot, this, bone);
-			slots.add(newSlot);
+			slots.add(new Slot(slot, this, bone));
 		}
 
 		drawOrder = new Array(slots.size);
 		for (Slot slot : skeleton.drawOrder)
 			drawOrder.add(slots.get(skeleton.slots.indexOf(slot, true)));
 
+		ikConstraints = new Array(skeleton.ikConstraints.size);
+		for (IkConstraint ikConstraint : skeleton.ikConstraints)
+			ikConstraints.add(new IkConstraint(ikConstraint));
+
 		skin = skeleton.skin;
 		color = new Color(skeleton.color);
 		time = skeleton.time;
+
+		updateCache();
 	}
 
-	/** Updates the world transform for each bone. */
+	/** Caches information about bones and IK constraints. Must be called if bones or IK constraints are added or removed. */
+	public void updateCache () {
+		Array<Array<Bone>> updateBonesCache = this.updateBonesCache;
+		Array<IkConstraint> ikConstraints = this.ikConstraints;
+		int ikConstraintsCount = ikConstraints.size;
+
+		int arrayCount = ikConstraintsCount + 1;
+		updateBonesCache.truncate(arrayCount);
+		for (int i = 0, n = updateBonesCache.size; i < n; i++)
+			updateBonesCache.get(i).clear();
+		while (updateBonesCache.size < arrayCount)
+			updateBonesCache.add(new Array());
+
+		Array<Bone> nonIkBones = updateBonesCache.first();
+
+		outer:
+		for (int i = 0, n = bones.size; i < n; i++) {
+			Bone bone = bones.get(i);
+			Bone current = bone;
+			do {
+				for (int ii = 0; ii < ikConstraintsCount; ii++) {
+					IkConstraint ikConstraint = ikConstraints.get(ii);
+					Bone parent = ikConstraint.bones.first();
+					Bone child = ikConstraint.bones.peek();
+					while (true) {
+						if (current == child) {
+							updateBonesCache.get(ii).add(bone);
+							updateBonesCache.get(ii + 1).add(bone);
+							continue outer;
+						}
+						if (child == parent) break;
+						child = child.parent;
+					}
+				}
+				current = current.parent;
+			} while (current != null);
+			nonIkBones.add(bone);
+		}
+	}
+
+	/** Updates the world transform for each bone and applies IK constraints. */
 	public void updateWorldTransform () {
+		Array<Bone> bones = this.bones;
+		for (int i = 0, nn = bones.size; i < nn; i++) {
+			Bone bone = bones.get(i);
+			bone.rotationIK = bone.rotation;
+		}
 		boolean flipX = this.flipX;
 		boolean flipY = this.flipY;
-		Array<Bone> bones = this.bones;
-		for (int i = 0, n = bones.size; i < n; i++)
-			bones.get(i).updateWorldTransform(flipX, flipY);
+		Array<Array<Bone>> updateBonesCache = this.updateBonesCache;
+		Array<IkConstraint> ikConstraints = this.ikConstraints;
+		int i = 0, last = updateBonesCache.size - 1;
+		while (true) {
+			Array<Bone> updateBones = updateBonesCache.get(i);
+			for (int ii = 0, nn = updateBones.size; ii < nn; ii++)
+				updateBones.get(ii).updateWorldTransform(flipX, flipY);
+			if (i == last) break;
+			ikConstraints.get(i).apply();
+			i++;
+		}
 	}
 
 	/** Sets the bones and slots to their setup pose values. */
@@ -114,6 +180,13 @@ public class Skeleton {
 		Array<Bone> bones = this.bones;
 		for (int i = 0, n = bones.size; i < n; i++)
 			bones.get(i).setToSetupPose();
+
+		Array<IkConstraint> ikConstraints = this.ikConstraints;
+		for (int i = 0, n = ikConstraints.size; i < n; i++) {
+			IkConstraint ikConstraint = ikConstraints.get(i);
+			ikConstraint.bendDirection = ikConstraint.data.bendDirection;
+			ikConstraint.mix = ikConstraint.data.mix;
+		}
 	}
 
 	public void setSlotsToSetupPose () {
@@ -263,6 +336,21 @@ public class Skeleton {
 		throw new IllegalArgumentException("Slot not found: " + slotName);
 	}
 
+	public Array<IkConstraint> getIkConstraints () {
+		return ikConstraints;
+	}
+
+	/** @return May be null. */
+	public IkConstraint findIkConstraint (String ikConstraintName) {
+		if (ikConstraintName == null) throw new IllegalArgumentException("ikConstraintName cannot be null.");
+		Array<IkConstraint> ikConstraints = this.ikConstraints;
+		for (int i = 0, n = ikConstraints.size; i < n; i++) {
+			IkConstraint ikConstraint = ikConstraints.get(i);
+			if (ikConstraint.data.name.equals(ikConstraintName)) return ikConstraint;
+		}
+		return null;
+	}
+
 	public Color getColor () {
 		return color;
 	}

+ 7 - 6
spine-libgdx/src/com/esotericsoftware/spine/SkeletonBinary.java

@@ -69,6 +69,7 @@ public class SkeletonBinary {
 	static public final int TIMELINE_EVENT = 5;
 	static public final int TIMELINE_DRAWORDER = 6;
 	static public final int TIMELINE_FFD = 7;
+	static public final int TIMELINE_IK = 8;
 
 	static public final int CURVE_LINEAR = 0;
 	static public final int CURVE_STEPPED = 1;
@@ -123,7 +124,7 @@ public class SkeletonBinary {
 				boneData.inheritScale = input.readBoolean();
 				boneData.inheritRotation = input.readBoolean();
 				if (nonessential) Color.rgba8888ToColor(boneData.getColor(), input.readInt());
-				skeletonData.addBone(boneData);
+				skeletonData.getBones().add(boneData);
 			}
 
 			// Slots.
@@ -134,19 +135,19 @@ public class SkeletonBinary {
 				Color.rgba8888ToColor(slotData.getColor(), input.readInt());
 				slotData.attachmentName = input.readString();
 				slotData.additiveBlending = input.readBoolean();
-				skeletonData.addSlot(slotData);
+				skeletonData.getSlots().add(slotData);
 			}
 
 			// Default skin.
 			Skin defaultSkin = readSkin(input, "default", nonessential);
 			if (defaultSkin != null) {
 				skeletonData.defaultSkin = defaultSkin;
-				skeletonData.addSkin(defaultSkin);
+				skeletonData.getSkins().add(defaultSkin);
 			}
 
 			// Skins.
 			for (int i = 0, n = input.readInt(true); i < n; i++)
-				skeletonData.addSkin(readSkin(input, input.readString(), nonessential));
+				skeletonData.getSkins().add(readSkin(input, input.readString(), nonessential));
 
 			// Events.
 			for (int i = 0, n = input.readInt(true); i < n; i++) {
@@ -154,7 +155,7 @@ public class SkeletonBinary {
 				eventData.intValue = input.readInt(false);
 				eventData.floatValue = input.readFloat();
 				eventData.stringValue = input.readString();
-				skeletonData.addEvent(eventData);
+				skeletonData.getEvents().add(eventData);
 			}
 
 			// Animations.
@@ -497,7 +498,7 @@ public class SkeletonBinary {
 		}
 
 		timelines.shrink();
-		skeletonData.addAnimation(new Animation(name, timelines, duration));
+		skeletonData.getAnimations().add(new Animation(name, timelines, duration));
 	}
 
 	private void readCurve (DataInput input, int frameIndex, CurveTimeline timeline) throws IOException {

+ 18 - 34
spine-libgdx/src/com/esotericsoftware/spine/SkeletonData.java

@@ -40,23 +40,10 @@ public class SkeletonData {
 	Skin defaultSkin;
 	final Array<EventData> events = new Array();
 	final Array<Animation> animations = new Array();
-
-	public void clear () {
-		bones.clear();
-		slots.clear();
-		skins.clear();
-		defaultSkin = null;
-		events.clear();
-		animations.clear();
-	}
+	final Array<IkConstraintData> ikConstraints = new Array();
 
 	// --- Bones.
 
-	public void addBone (BoneData bone) {
-		if (bone == null) throw new IllegalArgumentException("bone cannot be null.");
-		bones.add(bone);
-	}
-
 	public Array<BoneData> getBones () {
 		return bones;
 	}
@@ -83,11 +70,6 @@ public class SkeletonData {
 
 	// --- Slots.
 
-	public void addSlot (SlotData slot) {
-		if (slot == null) throw new IllegalArgumentException("slot cannot be null.");
-		slots.add(slot);
-	}
-
 	public Array<SlotData> getSlots () {
 		return slots;
 	}
@@ -124,11 +106,6 @@ public class SkeletonData {
 		this.defaultSkin = defaultSkin;
 	}
 
-	public void addSkin (Skin skin) {
-		if (skin == null) throw new IllegalArgumentException("skin cannot be null.");
-		skins.add(skin);
-	}
-
 	/** @return May be null. */
 	public Skin findSkin (String skinName) {
 		if (skinName == null) throw new IllegalArgumentException("skinName cannot be null.");
@@ -144,11 +121,6 @@ public class SkeletonData {
 
 	// --- Events.
 
-	public void addEvent (EventData eventData) {
-		if (eventData == null) throw new IllegalArgumentException("eventData cannot be null.");
-		events.add(eventData);
-	}
-
 	/** @return May be null. */
 	public EventData findEvent (String eventDataName) {
 		if (eventDataName == null) throw new IllegalArgumentException("eventDataName cannot be null.");
@@ -163,11 +135,6 @@ public class SkeletonData {
 
 	// --- Animations.
 
-	public void addAnimation (Animation animation) {
-		if (animation == null) throw new IllegalArgumentException("animation cannot be null.");
-		animations.add(animation);
-	}
-
 	public Array<Animation> getAnimations () {
 		return animations;
 	}
@@ -183,6 +150,23 @@ public class SkeletonData {
 		return null;
 	}
 
+	// --- IK
+
+	public Array<IkConstraintData> getIkConstraints () {
+		return ikConstraints;
+	}
+
+	/** @return May be null. */
+	public IkConstraintData findIkConstraint (String ikConstraintName) {
+		if (ikConstraintName == null) throw new IllegalArgumentException("ikConstraintName cannot be null.");
+		Array<IkConstraintData> ikConstraints = this.ikConstraints;
+		for (int i = 0, n = ikConstraints.size; i < n; i++) {
+			IkConstraintData ikConstraint = ikConstraints.get(i);
+			if (ikConstraint.name.equals(ikConstraintName)) return ikConstraint;
+		}
+		return null;
+	}
+
 	// ---
 
 	/** @return May be null. */

+ 5 - 5
spine-libgdx/src/com/esotericsoftware/spine/SkeletonJson.java

@@ -111,7 +111,7 @@ public class SkeletonJson {
 			String color = boneMap.getString("color", null);
 			if (color != null) boneData.getColor().set(Color.valueOf(color));
 
-			skeletonData.addBone(boneData);
+			skeletonData.getBones().add(boneData);
 		}
 
 		// Slots.
@@ -129,7 +129,7 @@ public class SkeletonJson {
 
 			slotData.additiveBlending = slotMap.getBoolean("additive", false);
 
-			skeletonData.addSlot(slotData);
+			skeletonData.getSlots().add(slotData);
 		}
 
 		// Skins.
@@ -143,7 +143,7 @@ public class SkeletonJson {
 					if (attachment != null) skin.addAttachment(slotIndex, entry.name, attachment);
 				}
 			}
-			skeletonData.addSkin(skin);
+			skeletonData.getSkins().add(skin);
 			if (skin.name.equals("default")) skeletonData.defaultSkin = skin;
 		}
 
@@ -153,7 +153,7 @@ public class SkeletonJson {
 			eventData.intValue = eventMap.getInt("int", 0);
 			eventData.floatValue = eventMap.getFloat("float", 0f);
 			eventData.stringValue = eventMap.getString("string", null);
-			skeletonData.addEvent(eventData);
+			skeletonData.getEvents().add(eventData);
 		}
 
 		// Animations.
@@ -461,7 +461,7 @@ public class SkeletonJson {
 		}
 
 		timelines.shrink();
-		skeletonData.addAnimation(new Animation(name, timelines, duration));
+		skeletonData.getAnimations().add(new Animation(name, timelines, duration));
 	}
 
 	void readCurve (CurveTimeline timeline, int frameIndex, JsonValue valueMap) {

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

@@ -38,7 +38,7 @@ public interface AttachmentLoader {
 
 	/** @return May be null to not load any attachment. */
 	public MeshAttachment newMeshAttachment (Skin skin, String name, String path);
-	
+
 	/** @return May be null to not load any attachment. */
 	public SkinnedMeshAttachment newSkinnedMeshAttachment (Skin skin, String name, String path);
 

+ 1 - 1
spine-libgdx/src/com/esotericsoftware/spine/attachments/RegionAttachment.java

@@ -34,7 +34,7 @@ import com.esotericsoftware.spine.Bone;
 import com.esotericsoftware.spine.Skeleton;
 import com.esotericsoftware.spine.Slot;
 
-import static com.badlogic.gdx.graphics.g2d.SpriteBatch.*;
+import static com.badlogic.gdx.graphics.g2d.Batch.*;
 
 import com.badlogic.gdx.graphics.Color;
 import com.badlogic.gdx.graphics.g2d.TextureAtlas.AtlasRegion;