IK constraints for spine-libgdx.

Note Spine doesn't yet export IK data, so spine-libgdx doesn't yet load it. Soon!
This commit is contained in:
NathanSweet 2014-07-22 00:21:27 +02:00
parent 88f805a74e
commit 6444e8e934
11 changed files with 426 additions and 59 deletions

View File

@ -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];
}
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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 {

View File

@ -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. */

View File

@ -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) {

View File

@ -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);

View File

@ -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;