From a62e4466dd5d26235bd71159108fa00be7526f30 Mon Sep 17 00:00:00 2001 From: NathanSweet Date: Wed, 11 Jul 2018 04:44:56 +0200 Subject: [PATCH] Added stretchy IK. --- .../com/esotericsoftware/spine/Animation.java | 43 +++++++++++++----- .../esotericsoftware/spine/IkConstraint.java | 45 ++++++++++++++----- .../spine/IkConstraintData.java | 11 +++++ .../com/esotericsoftware/spine/Skeleton.java | 1 + .../spine/SkeletonBinary.java | 3 +- .../esotericsoftware/spine/SkeletonJson.java | 3 +- 6 files changed, 81 insertions(+), 25 deletions(-) diff --git a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Animation.java b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Animation.java index d4a2ca6dd..6fa9f2d8c 100644 --- a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Animation.java +++ b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Animation.java @@ -37,6 +37,7 @@ import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.math.MathUtils; import com.badlogic.gdx.utils.Array; import com.badlogic.gdx.utils.FloatArray; + import com.esotericsoftware.spine.attachments.Attachment; import com.esotericsoftware.spine.attachments.VertexAttachment; @@ -1294,11 +1295,12 @@ public class Animation { } } - /** Changes an IK constraint's {@link IkConstraint#getMix()} and {@link IkConstraint#getBendDirection()}. */ + /** Changes an IK constraint's {@link IkConstraint#getMix()}, {@link IkConstraint#getBendDirection()}, and + * {@link IkConstraint#getStretch()}. */ static public class IkConstraintTimeline extends CurveTimeline { - static public final int ENTRIES = 3; - static private final int PREV_TIME = -3, PREV_MIX = -2, PREV_BEND_DIRECTION = -1; - static private final int MIX = 1, BEND_DIRECTION = 2; + static public final int ENTRIES = 4; + static private final int PREV_TIME = -4, PREV_MIX = -3, PREV_BEND_DIRECTION = -2, PREV_STRETCH = -1; + static private final int MIX = 1, BEND_DIRECTION = 2, STRETCH = 3; int ikConstraintIndex; private final float[] frames; // time, mix, bendDirection, ... @@ -1328,11 +1330,12 @@ public class Animation { } /** Sets the time in seconds, mix, and bend direction for the specified key frame. */ - public void setFrame (int frameIndex, float time, float mix, int bendDirection) { + public void setFrame (int frameIndex, float time, float mix, int bendDirection, boolean stretch) { frameIndex *= ENTRIES; frames[frameIndex] = time; frames[frameIndex + MIX] = mix; frames[frameIndex + BEND_DIRECTION] = bendDirection; + frames[frameIndex + STRETCH] = stretch ? 1 : 0; } public void apply (Skeleton skeleton, float lastTime, float time, Array events, float alpha, MixBlend blend, @@ -1345,10 +1348,12 @@ public class Animation { case setup: constraint.mix = constraint.data.mix; constraint.bendDirection = constraint.data.bendDirection; + constraint.stretch = constraint.data.stretch; return; case first: constraint.mix += (constraint.data.mix - constraint.mix) * alpha; constraint.bendDirection = constraint.data.bendDirection; + constraint.stretch = constraint.data.stretch; } return; } @@ -1356,11 +1361,19 @@ public class Animation { if (time >= frames[frames.length - ENTRIES]) { // Time is after last frame. if (blend == setup) { constraint.mix = constraint.data.mix + (frames[frames.length + PREV_MIX] - constraint.data.mix) * alpha; - constraint.bendDirection = direction == out ? constraint.data.bendDirection - : (int)frames[frames.length + PREV_BEND_DIRECTION]; + if (direction == out) { + constraint.bendDirection = constraint.data.bendDirection; + constraint.stretch = constraint.data.stretch; + } else { + constraint.bendDirection = (int)frames[frames.length + PREV_BEND_DIRECTION]; + constraint.stretch = frames[frames.length + PREV_STRETCH] != 0; + } } else { constraint.mix += (frames[frames.length + PREV_MIX] - constraint.mix) * alpha; - if (direction == in) constraint.bendDirection = (int)frames[frames.length + PREV_BEND_DIRECTION]; + if (direction == in) { + constraint.bendDirection = (int)frames[frames.length + PREV_BEND_DIRECTION]; + constraint.stretch = frames[frames.length + PREV_STRETCH] != 0; + } } return; } @@ -1373,11 +1386,19 @@ public class Animation { if (blend == setup) { constraint.mix = constraint.data.mix + (mix + (frames[frame + MIX] - mix) * percent - constraint.data.mix) * alpha; - constraint.bendDirection = direction == out ? constraint.data.bendDirection - : (int)frames[frame + PREV_BEND_DIRECTION]; + if (direction == out) { + constraint.bendDirection = constraint.data.bendDirection; + constraint.stretch = constraint.data.stretch; + } else { + constraint.bendDirection = (int)frames[frame + PREV_BEND_DIRECTION]; + constraint.stretch = frames[frame + PREV_STRETCH] != 0; + } } else { constraint.mix += (mix + (frames[frame + MIX] - mix) * percent - constraint.mix) * alpha; - if (direction == in) constraint.bendDirection = (int)frames[frame + PREV_BEND_DIRECTION]; + if (direction == in) { + constraint.bendDirection = (int)frames[frame + PREV_BEND_DIRECTION]; + constraint.stretch = frames[frame + PREV_STRETCH] != 0; + } } } } diff --git a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/IkConstraint.java b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/IkConstraint.java index d6128a332..0a7f8aa36 100644 --- a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/IkConstraint.java +++ b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/IkConstraint.java @@ -42,8 +42,9 @@ public class IkConstraint implements Constraint { final IkConstraintData data; final Array bones; Bone target; - float mix = 1; int bendDirection; + boolean stretch; + float mix = 1; public IkConstraint (IkConstraintData data, Skeleton skeleton) { if (data == null) throw new IllegalArgumentException("data cannot be null."); @@ -51,6 +52,7 @@ public class IkConstraint implements Constraint { this.data = data; mix = data.mix; bendDirection = data.bendDirection; + stretch = data.stretch; bones = new Array(data.bones.size); for (BoneData boneData : data.bones) @@ -69,6 +71,7 @@ public class IkConstraint implements Constraint { target = skeleton.bones.get(constraint.target.data.index); mix = constraint.mix; bendDirection = constraint.bendDirection; + stretch = constraint.stretch; } /** Applies the constraint to the constrained bones. */ @@ -81,10 +84,10 @@ public class IkConstraint implements Constraint { Array bones = this.bones; switch (bones.size) { case 1: - apply(bones.first(), target.worldX, target.worldY, mix); + apply(bones.first(), target.worldX, target.worldY, stretch, mix); break; case 2: - apply(bones.first(), bones.get(1), target.worldX, target.worldY, bendDirection, mix); + apply(bones.first(), bones.get(1), target.worldX, target.worldY, bendDirection, stretch, mix); break; } } @@ -125,6 +128,16 @@ public class IkConstraint implements Constraint { this.bendDirection = bendDirection; } + /** When true, if the target is out of range, the parent bone is scaled on the X axis to reach it. If the parent bone has local + * nonuniform scale, stretching is not applied. */ + public boolean getStretch () { + return stretch; + } + + public void setStretch (boolean stretch) { + this.stretch = stretch; + } + /** The IK constraint's setup pose data. */ public IkConstraintData getData () { return data; @@ -135,7 +148,7 @@ public class IkConstraint implements Constraint { } /** Applies 1 bone IK. The target is specified in the world coordinate system. */ - static public void apply (Bone bone, float targetX, float targetY, float alpha) { + static public void apply (Bone bone, float targetX, float targetY, boolean stretch, float alpha) { if (!bone.appliedValid) bone.updateAppliedTransform(); Bone p = bone.parent; float id = 1 / (p.a * p.d - p.b * p.c); @@ -146,20 +159,25 @@ public class IkConstraint implements Constraint { if (rotationIK > 180) rotationIK -= 360; else if (rotationIK < -180) rotationIK += 360; - bone.updateWorldTransform(bone.ax, bone.ay, bone.arotation + rotationIK * alpha, bone.ascaleX, bone.ascaleY, bone.ashearX, + float sx = bone.ascaleX; + if (stretch) { + float dd = (float)Math.sqrt(tx * tx + ty * ty); + if (dd > bone.data.length * sx) sx *= (dd / (bone.data.length * sx) - 1) * alpha + 1; + } + bone.updateWorldTransform(bone.ax, bone.ay, bone.arotation + rotationIK * alpha, sx, bone.ascaleY, bone.ashearX, bone.ashearY); } /** Applies 2 bone IK. The target is specified in the world coordinate system. * @param child A direct descendant of the parent bone. */ - static public void apply (Bone parent, Bone child, float targetX, float targetY, int bendDir, float alpha) { + static public void apply (Bone parent, Bone child, float targetX, float targetY, int bendDir, boolean stretch, float alpha) { if (alpha == 0) { child.updateWorldTransform(); return; } if (!parent.appliedValid) parent.updateAppliedTransform(); if (!child.appliedValid) child.updateAppliedTransform(); - float px = parent.ax, py = parent.ay, psx = parent.ascaleX, psy = parent.ascaleY, csx = child.ascaleX; + float px = parent.ax, py = parent.ay, psx = parent.ascaleX, sx = psx, psy = parent.ascaleY, csx = child.ascaleX; int os1, os2, s2; if (psx < 0) { psx = -psx; @@ -195,7 +213,7 @@ public class IkConstraint implements Constraint { c = pp.c; d = pp.d; float id = 1 / (a * d - b * c), x = targetX - pp.worldX, y = targetY - pp.worldY; - float tx = (x * d - y * b) * id - px, ty = (y * a - x * c) * id - py; + float tx = (x * d - y * b) * id - px, ty = (y * a - x * c) * id - py, dd = tx * tx + ty * ty; x = cwx - pp.worldX; y = cwy - pp.worldY; float dx = (x * d - y * b) * id - px, dy = (y * a - x * c) * id - py; @@ -203,10 +221,13 @@ public class IkConstraint implements Constraint { outer: if (u) { l2 *= psx; - float cos = (tx * tx + ty * ty - l1 * l1 - l2 * l2) / (2 * l1 * l2); + float cos = (dd - l1 * l1 - l2 * l2) / (2 * l1 * l2); if (cos < -1) cos = -1; - else if (cos > 1) cos = 1; + else if (cos > 1) { + cos = 1; + if (stretch) sx *= ((float)Math.sqrt(dd) / (l1 + l2) - 1) * alpha + 1; + } a2 = (float)Math.acos(cos) * bendDir; a = l1 + l2 * cos; b = l2 * sin(a2); @@ -214,7 +235,7 @@ public class IkConstraint implements Constraint { } else { a = psx * l2; b = psy * l2; - float aa = a * a, bb = b * b, dd = tx * tx + ty * ty, ta = atan2(ty, tx); + float aa = a * a, bb = b * b, ta = atan2(ty, tx); c = bb * l1 * l1 + aa * dd - aa * bb; float c1 = -2 * bb * l1, c2 = bb - aa; d = c1 * c1 - 4 * c2 * c; @@ -266,7 +287,7 @@ public class IkConstraint implements Constraint { if (a1 > 180) a1 -= 360; else if (a1 < -180) a1 += 360; - parent.updateWorldTransform(px, py, rotation + a1 * alpha, parent.ascaleX, parent.ascaleY, 0, 0); + parent.updateWorldTransform(px, py, rotation + a1 * alpha, sx, parent.ascaleY, 0, 0); rotation = child.arotation; a2 = ((a2 + os) * radDeg - child.ashearX) * s2 + os2 - rotation; if (a2 > 180) diff --git a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/IkConstraintData.java b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/IkConstraintData.java index 75b3e49d6..81bceb23b 100644 --- a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/IkConstraintData.java +++ b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/IkConstraintData.java @@ -41,6 +41,7 @@ public class IkConstraintData { final Array bones = new Array(); BoneData target; int bendDirection = 1; + boolean stretch; float mix = 1; public IkConstraintData (String name) { @@ -86,6 +87,16 @@ public class IkConstraintData { this.bendDirection = bendDirection; } + /** When true, if the target is out of range, the parent bone is scaled on the X axis to reach it. If the parent bone has local + * nonuniform scale, stretching is not applied. */ + public boolean getStretch () { + return stretch; + } + + public void setStretch (boolean stretch) { + this.stretch = stretch; + } + /** A percentage (0-1) that controls the mix between the constrained and unconstrained rotations. */ public float getMix () { return mix; diff --git a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Skeleton.java b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Skeleton.java index e267fa1a2..1e3168037 100644 --- a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Skeleton.java +++ b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Skeleton.java @@ -395,6 +395,7 @@ public class Skeleton { for (int i = 0, n = ikConstraints.size; i < n; i++) { IkConstraint constraint = ikConstraints.get(i); constraint.bendDirection = constraint.data.bendDirection; + constraint.stretch = constraint.data.stretch; constraint.mix = constraint.data.mix; } diff --git a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonBinary.java b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonBinary.java index 81884693a..1d5e0959b 100644 --- a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonBinary.java +++ b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonBinary.java @@ -232,6 +232,7 @@ public class SkeletonBinary { data.target = skeletonData.bones.get(input.readInt(true)); data.mix = input.readFloat(); data.bendDirection = input.readByte(); + data.stretch = input.readBoolean(); skeletonData.ikConstraints.add(data); } @@ -660,7 +661,7 @@ public class SkeletonBinary { IkConstraintTimeline timeline = new IkConstraintTimeline(frameCount); timeline.ikConstraintIndex = index; for (int frameIndex = 0; frameIndex < frameCount; frameIndex++) { - timeline.setFrame(frameIndex, input.readFloat(), input.readFloat(), input.readByte()); + timeline.setFrame(frameIndex, input.readFloat(), input.readFloat(), input.readByte(), input.readBoolean()); if (frameIndex < frameCount - 1) readCurve(input, frameIndex, timeline); } timelines.add(timeline); diff --git a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonJson.java b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonJson.java index 904ce4752..3120595b6 100644 --- a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonJson.java +++ b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonJson.java @@ -186,6 +186,7 @@ public class SkeletonJson { if (data.target == null) throw new SerializationException("IK target bone not found: " + targetName); data.bendDirection = constraintMap.getBoolean("bendPositive", true) ? 1 : -1; + data.stretch = constraintMap.getBoolean("stretch", false); data.mix = constraintMap.getFloat("mix", 1); skeletonData.ikConstraints.add(data); @@ -568,7 +569,7 @@ public class SkeletonJson { int frameIndex = 0; for (JsonValue valueMap = constraintMap.child; valueMap != null; valueMap = valueMap.next) { timeline.setFrame(frameIndex, valueMap.getFloat("time"), valueMap.getFloat("mix", 1), - valueMap.getBoolean("bendPositive", true) ? 1 : -1); + valueMap.getBoolean("bendPositive", true) ? 1 : -1, valueMap.getBoolean("stretch", false)); readCurve(valueMap, timeline, frameIndex); frameIndex++; }