Added stretchy IK.

This commit is contained in:
NathanSweet 2018-07-11 04:44:56 +02:00
parent 15b93e0dfd
commit a62e4466dd
6 changed files with 81 additions and 25 deletions

View File

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

View File

@ -42,8 +42,9 @@ public class IkConstraint implements Constraint {
final IkConstraintData data;
final Array<Bone> 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<Bone> 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)

View File

@ -41,6 +41,7 @@ public class IkConstraintData {
final Array<BoneData> 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;

View File

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

View File

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

View File

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