Merge branch '3.7-beta' into 3.7-beta-cpp

This commit is contained in:
badlogic 2018-08-23 15:11:16 +02:00
commit 38e5d2f35a
24 changed files with 1941 additions and 201 deletions

File diff suppressed because one or more lines are too long

View File

@ -1210,9 +1210,9 @@ namespace Spine {
} }
public class IkConstraintTimeline : CurveTimeline { public class IkConstraintTimeline : CurveTimeline {
public const int ENTRIES = 3; public const int ENTRIES = 4;
private const int PREV_TIME = -3, PREV_MIX = -2, PREV_BEND_DIRECTION = -1; private const int PREV_TIME = -4, PREV_MIX = -3, PREV_BEND_DIRECTION = -2, PREV_STRETCH = -1;
private const int MIX = 1, BEND_DIRECTION = 2; private const int MIX = 1, BEND_DIRECTION = 2, STRETCH = 3;
internal int ikConstraintIndex; internal int ikConstraintIndex;
internal float[] frames; internal float[] frames;
@ -1230,11 +1230,12 @@ namespace Spine {
} }
/// <summary>Sets the time, mix and bend direction of the specified keyframe.</summary> /// <summary>Sets the time, mix and bend direction of the specified keyframe.</summary>
public void SetFrame (int frameIndex, float time, float mix, int bendDirection) { public void SetFrame (int frameIndex, float time, float mix, int bendDirection, bool stretch) {
frameIndex *= ENTRIES; frameIndex *= ENTRIES;
frames[frameIndex] = time; frames[frameIndex] = time;
frames[frameIndex + MIX] = mix; frames[frameIndex + MIX] = mix;
frames[frameIndex + BEND_DIRECTION] = bendDirection; frames[frameIndex + BEND_DIRECTION] = bendDirection;
frames[frameIndex + STRETCH] = stretch ? 1 : 0;
} }
override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList<Event> firedEvents, float alpha, MixBlend blend, MixDirection direction) { override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList<Event> firedEvents, float alpha, MixBlend blend, MixDirection direction) {
@ -1245,10 +1246,12 @@ namespace Spine {
case MixBlend.Setup: case MixBlend.Setup:
constraint.mix = constraint.data.mix; constraint.mix = constraint.data.mix;
constraint.bendDirection = constraint.data.bendDirection; constraint.bendDirection = constraint.data.bendDirection;
constraint.stretch = constraint.data.stretch;
return; return;
case MixBlend.First: case MixBlend.First:
constraint.mix += (constraint.data.mix - constraint.mix) * alpha; constraint.mix += (constraint.data.mix - constraint.mix) * alpha;
constraint.bendDirection = constraint.data.bendDirection; constraint.bendDirection = constraint.data.bendDirection;
constraint.stretch = constraint.data.stretch;
return; return;
} }
return; return;
@ -1257,11 +1260,19 @@ namespace Spine {
if (time >= frames[frames.Length - ENTRIES]) { // Time is after last frame. if (time >= frames[frames.Length - ENTRIES]) { // Time is after last frame.
if (blend == MixBlend.Setup) { if (blend == MixBlend.Setup) {
constraint.mix = constraint.data.mix + (frames[frames.Length + PREV_MIX] - constraint.data.mix) * alpha; constraint.mix = constraint.data.mix + (frames[frames.Length + PREV_MIX] - constraint.data.mix) * alpha;
constraint.bendDirection = direction == MixDirection.Out ? constraint.data.bendDirection if (direction == MixDirection.Out) {
: (int)frames[frames.Length + PREV_BEND_DIRECTION]; 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 { } else {
constraint.mix += (frames[frames.Length + PREV_MIX] - constraint.mix) * alpha; constraint.mix += (frames[frames.Length + PREV_MIX] - constraint.mix) * alpha;
if (direction == MixDirection.In) constraint.bendDirection = (int)frames[frames.Length + PREV_BEND_DIRECTION]; if (direction == MixDirection.In) {
constraint.bendDirection = (int)frames[frames.Length + PREV_BEND_DIRECTION];
constraint.stretch = frames[frames.Length + PREV_STRETCH] != 0;
}
} }
return; return;
} }

View File

@ -334,10 +334,10 @@ namespace Spine {
// Mix between rotations using the direction of the shortest route on the first frame while detecting crosses. // Mix between rotations using the direction of the shortest route on the first frame while detecting crosses.
float r1 = pose == MixBlend.Setup ? bone.data.rotation : bone.rotation; float r1 = pose == MixBlend.Setup ? bone.data.rotation : bone.rotation;
float total, diff = r2 - r1; float total, diff = r2 - r1;
diff -= (16384 - (int)(16384.499999999996 - diff / 360)) * 360;
if (diff == 0) { if (diff == 0) {
total = timelinesRotation[i]; total = timelinesRotation[i];
} else { } else {
diff -= (16384 - (int)(16384.499999999996 - diff / 360)) * 360;
float lastTotal, lastDiff; float lastTotal, lastDiff;
if (firstFrame) { if (firstFrame) {
lastTotal = 0; lastTotal = 0;

View File

@ -35,15 +35,43 @@ namespace Spine {
internal IkConstraintData data; internal IkConstraintData data;
internal ExposedList<Bone> bones = new ExposedList<Bone>(); internal ExposedList<Bone> bones = new ExposedList<Bone>();
internal Bone target; internal Bone target;
internal float mix;
internal int bendDirection; internal int bendDirection;
internal bool stretch;
internal float mix;
public IkConstraintData Data { get { return data; } } public IkConstraintData Data { get { return data; } }
public int Order { get { return data.order; } } public int Order { get { return data.order; } }
public ExposedList<Bone> Bones { get { return bones; } }
public Bone Target { get { return target; } set { target = value; } } /// <summary>The bones that will be modified by this IK constraint.</summary>
public int BendDirection { get { return bendDirection; } set { bendDirection = value; } } public ExposedList<Bone> Bones {
public float Mix { get { return mix; } set { mix = value; } } get { return bones; }
}
/// <summary>The bone that is the IK target.</summary>
public Bone Target {
get { return target; }
set { target = value; }
}
/// <summary>Controls the bend direction of the IK bones, either 1 or -1.</summary>
public int BendDirection {
get { return bendDirection; }
set { bendDirection = value; }
}
/// <summary>
/// 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 nonuniform scale, stretching is not applied.</summary>
public bool Stretch {
get { return stretch; }
set { stretch = value; }
}
/// <summary>A percentage (0-1) that controls the mix between the constrained and unconstrained rotations.</summary>
public float Mix {
get { return mix; }
set { mix = value; }
}
public IkConstraint (IkConstraintData data, Skeleton skeleton) { public IkConstraint (IkConstraintData data, Skeleton skeleton) {
if (data == null) throw new ArgumentNullException("data", "data cannot be null."); if (data == null) throw new ArgumentNullException("data", "data cannot be null.");
@ -51,6 +79,7 @@ namespace Spine {
this.data = data; this.data = data;
mix = data.mix; mix = data.mix;
bendDirection = data.bendDirection; bendDirection = data.bendDirection;
stretch = data.stretch;
bones = new ExposedList<Bone>(data.bones.Count); bones = new ExposedList<Bone>(data.bones.Count);
foreach (BoneData boneData in data.bones) foreach (BoneData boneData in data.bones)
@ -68,10 +97,10 @@ namespace Spine {
ExposedList<Bone> bones = this.bones; ExposedList<Bone> bones = this.bones;
switch (bones.Count) { switch (bones.Count) {
case 1: case 1:
Apply(bones.Items[0], target.worldX, target.worldY, mix); Apply(bones.Items[0], target.worldX, target.worldY, stretch, mix);
break; break;
case 2: case 2:
Apply(bones.Items[0], bones.Items[1], target.worldX, target.worldY, bendDirection, mix); Apply(bones.Items[0], bones.Items[1], target.worldX, target.worldY, bendDirection, stretch, mix);
break; break;
} }
} }
@ -82,7 +111,7 @@ namespace Spine {
/// <summary>Adjusts the bone rotation so the tip is as close to the target position as possible. The target is specified /// <summary>Adjusts the bone rotation so the tip is as close to the target position as possible. The target is specified
/// in the world coordinate system.</summary> /// in the world coordinate system.</summary>
static public void Apply (Bone bone, float targetX, float targetY, float alpha) { static public void Apply (Bone bone, float targetX, float targetY, bool stretch, float alpha) {
if (!bone.appliedValid) bone.UpdateAppliedTransform(); if (!bone.appliedValid) bone.UpdateAppliedTransform();
Bone p = bone.parent; Bone p = bone.parent;
float id = 1 / (p.a * p.d - p.b * p.c); float id = 1 / (p.a * p.d - p.b * p.c);
@ -92,15 +121,21 @@ namespace Spine {
if (bone.ascaleX < 0) rotationIK += 180; if (bone.ascaleX < 0) rotationIK += 180;
if (rotationIK > 180) if (rotationIK > 180)
rotationIK -= 360; rotationIK -= 360;
else if (rotationIK < -180) rotationIK += 360; else if (rotationIK < -180)
bone.UpdateWorldTransform(bone.ax, bone.ay, bone.arotation + rotationIK * alpha, bone.ascaleX, bone.ascaleY, bone.ashearX, rotationIK += 360;
float sx = bone.ascaleX;
if (stretch) {
float b = bone.data.length * sx, dd = (float)Math.Sqrt(tx * tx + ty * ty);
if (dd > b && b > 0.0001f) sx *= (dd / b - 1) * alpha + 1;
}
bone.UpdateWorldTransform(bone.ax, bone.ay, bone.arotation + rotationIK * alpha, sx, bone.ascaleY, bone.ashearX,
bone.ashearY); bone.ashearY);
} }
/// <summary>Adjusts the parent and child bone rotations so the tip of the child is as close to the target position as /// <summary>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.</summary> /// possible. The target is specified in the world coordinate system.</summary>
/// <param name="child">A direct descendant of the parent bone.</param> /// <param name="child">A direct descendant of the parent bone.</param>
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, bool stretch, float alpha) {
if (alpha == 0) { if (alpha == 0) {
child.UpdateWorldTransform (); child.UpdateWorldTransform ();
return; return;
@ -108,7 +143,7 @@ namespace Spine {
//float px = parent.x, py = parent.y, psx = parent.scaleX, psy = parent.scaleY, csx = child.scaleX; //float px = parent.x, py = parent.y, psx = parent.scaleX, psy = parent.scaleY, csx = child.scaleX;
if (!parent.appliedValid) parent.UpdateAppliedTransform(); if (!parent.appliedValid) parent.UpdateAppliedTransform();
if (!child.appliedValid) child.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; int os1, os2, s2;
if (psx < 0) { if (psx < 0) {
psx = -psx; psx = -psx;
@ -144,17 +179,20 @@ namespace Spine {
c = pp.c; c = pp.c;
d = pp.d; d = pp.d;
float id = 1 / (a * d - b * c), x = targetX - pp.worldX, y = targetY - pp.worldY; 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; x = cwx - pp.worldX;
y = cwy - pp.worldY; y = cwy - pp.worldY;
float dx = (x * d - y * b) * id - px, dy = (y * a - x * c) * id - py; float dx = (x * d - y * b) * id - px, dy = (y * a - x * c) * id - py;
float l1 = (float)Math.Sqrt(dx * dx + dy * dy), l2 = child.data.length * csx, a1, a2; float l1 = (float)Math.Sqrt(dx * dx + dy * dy), l2 = child.data.length * csx, a1, a2;
if (u) { if (u) {
l2 *= psx; 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) if (cos < -1)
cos = -1; cos = -1;
else if (cos > 1) cos = 1; else if (cos > 1) {
cos = 1;
if (stretch && l1 + l2 > 0.0001f) sx *= ((float)Math.Sqrt(dd) / (l1 + l2) - 1) * alpha + 1;
}
a2 = (float)Math.Acos(cos) * bendDir; a2 = (float)Math.Acos(cos) * bendDir;
a = l1 + l2 * cos; a = l1 + l2 * cos;
b = l2 * (float)Math.Sin(a2); b = l2 * (float)Math.Sin(a2);
@ -162,7 +200,7 @@ namespace Spine {
} else { } else {
a = psx * l2; a = psx * l2;
b = psy * l2; b = psy * l2;
float aa = a * a, bb = b * b, dd = tx * tx + ty * ty, ta = (float)Math.Atan2(ty, tx); float aa = a * a, bb = b * b, ta = (float)Math.Atan2(ty, tx);
c = bb * l1 * l1 + aa * dd - aa * bb; c = bb * l1 * l1 + aa * dd - aa * bb;
float c1 = -2 * bb * l1, c2 = bb - aa; float c1 = -2 * bb * l1, c2 = bb - aa;
d = c1 * c1 - 4 * c2 * c; d = c1 * c1 - 4 * c2 * c;
@ -215,7 +253,7 @@ namespace Spine {
if (a1 > 180) if (a1 > 180)
a1 -= 360; a1 -= 360;
else if (a1 < -180) a1 += 360; else if (a1 < -180) a1 += 360;
parent.UpdateWorldTransform(px, py, rotation + a1 * alpha, parent.scaleX, parent.ascaleY, 0, 0); parent.UpdateWorldTransform(px, py, rotation + a1 * alpha, sx, parent.ascaleY, 0, 0);
rotation = child.arotation; rotation = child.arotation;
a2 = ((a2 + os) * MathUtils.RadDeg - child.ashearX) * s2 + os2 - rotation; a2 = ((a2 + os) * MathUtils.RadDeg - child.ashearX) * s2 + os2 - rotation;
if (a2 > 180) if (a2 > 180)

View File

@ -39,6 +39,7 @@ namespace Spine {
internal List<BoneData> bones = new List<BoneData>(); internal List<BoneData> bones = new List<BoneData>();
internal BoneData target; internal BoneData target;
internal int bendDirection = 1; internal int bendDirection = 1;
internal bool stretch;
internal float mix = 1; internal float mix = 1;
/// <summary>The IK constraint's name, which is unique within the skeleton.</summary> /// <summary>The IK constraint's name, which is unique within the skeleton.</summary>
@ -68,6 +69,14 @@ namespace Spine {
set { bendDirection = value; } set { bendDirection = value; }
} }
/// <summary>
/// When true, if the target is out of range, the parent bone is scaled on the X axis to reach it.
/// If the bone has local nonuniform scale, stretching is not applied.</summary>
public bool Stretch {
get { return stretch; }
set { stretch = value; }
}
public float Mix { get { return mix; } set { mix = value; } } public float Mix { get { return mix; } set { mix = value; } }
public IkConstraintData (string name) { public IkConstraintData (string name) {

View File

@ -309,6 +309,7 @@ namespace Spine {
for (int i = 0, n = ikConstraints.Count; i < n; i++) { for (int i = 0, n = ikConstraints.Count; i < n; i++) {
IkConstraint constraint = ikConstraintsItems[i]; IkConstraint constraint = ikConstraintsItems[i];
constraint.bendDirection = constraint.data.bendDirection; constraint.bendDirection = constraint.data.bendDirection;
constraint.stretch = constraint.data.stretch;
constraint.mix = constraint.data.mix; constraint.mix = constraint.data.mix;
} }

View File

@ -210,6 +210,7 @@ namespace Spine {
data.target = skeletonData.bones.Items[ReadVarint(input, true)]; data.target = skeletonData.bones.Items[ReadVarint(input, true)];
data.mix = ReadFloat(input); data.mix = ReadFloat(input);
data.bendDirection = ReadSByte(input); data.bendDirection = ReadSByte(input);
data.stretch = ReadBoolean(input);
skeletonData.ikConstraints.Add(data); skeletonData.ikConstraints.Add(data);
} }
@ -648,7 +649,7 @@ namespace Spine {
IkConstraintTimeline timeline = new IkConstraintTimeline(frameCount); IkConstraintTimeline timeline = new IkConstraintTimeline(frameCount);
timeline.ikConstraintIndex = index; timeline.ikConstraintIndex = index;
for (int frameIndex = 0; frameIndex < frameCount; frameIndex++) { for (int frameIndex = 0; frameIndex < frameCount; frameIndex++) {
timeline.SetFrame(frameIndex, ReadFloat(input), ReadFloat(input), ReadSByte(input)); timeline.SetFrame(frameIndex, ReadFloat(input), ReadFloat(input), ReadSByte(input), ReadBoolean(input));
if (frameIndex < frameCount - 1) ReadCurve(input, frameIndex, timeline); if (frameIndex < frameCount - 1) ReadCurve(input, frameIndex, timeline);
} }
timelines.Add(timeline); timelines.Add(timeline);

View File

@ -182,6 +182,7 @@ namespace Spine {
if (data.target == null) throw new Exception("Target bone not found: " + targetName); if (data.target == null) throw new Exception("Target bone not found: " + targetName);
data.bendDirection = GetBoolean(constraintMap, "bendPositive", true) ? 1 : -1; data.bendDirection = GetBoolean(constraintMap, "bendPositive", true) ? 1 : -1;
data.stretch = GetBoolean(constraintMap, "stretch", false);
data.mix = GetFloat(constraintMap, "mix", 1); data.mix = GetFloat(constraintMap, "mix", 1);
skeletonData.ikConstraints.Add(data); skeletonData.ikConstraints.Add(data);
@ -597,7 +598,8 @@ namespace Spine {
float time = (float)valueMap["time"]; float time = (float)valueMap["time"];
float mix = GetFloat(valueMap, "mix", 1); float mix = GetFloat(valueMap, "mix", 1);
bool bendPositive = GetBoolean(valueMap, "bendPositive", true); bool bendPositive = GetBoolean(valueMap, "bendPositive", true);
timeline.SetFrame(frameIndex, time, mix, bendPositive ? 1 : -1); bool stretch = GetBoolean(valueMap, "stretch", false);
timeline.SetFrame(frameIndex, time, mix, bendPositive ? 1 : -1, stretch);
ReadCurve(valueMap, timeline, frameIndex); ReadCurve(valueMap, timeline, frameIndex);
frameIndex++; frameIndex++;
} }

View File

@ -30,6 +30,7 @@
package com.esotericsoftware.spine; package com.esotericsoftware.spine;
import java.lang.reflect.Field;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import com.badlogic.gdx.Files.FileType; import com.badlogic.gdx.Files.FileType;
@ -803,7 +804,8 @@ public class AnimationStateTests {
expected.addAll(expectedArray); expected.addAll(expectedArray);
stateData = new AnimationStateData(skeletonData); stateData = new AnimationStateData(skeletonData);
state = new AnimationState(stateData); state = new AnimationState(stateData);
state.trackEntryPool = new Pool<TrackEntry>() {
Pool trackEntryPool = new Pool<TrackEntry>() {
public TrackEntry obtain () { public TrackEntry obtain () {
TrackEntry entry = super.obtain(); TrackEntry entry = super.obtain();
entryCount++; entryCount++;
@ -821,6 +823,14 @@ public class AnimationStateTests {
super.free(entry); super.free(entry);
} }
}; };
try {
Field field = state.getClass().getDeclaredField("trackEntryPool");
field.setAccessible(true);
field.set(state, trackEntryPool);
} catch (Exception ex) {
throw new RuntimeException(ex);
}
time = 0; time = 0;
fail = false; fail = false;
log(test + ": " + description); log(test + ": " + description);

View File

@ -1295,15 +1295,15 @@ public class Animation {
} }
} }
/** Changes an IK constraint's {@link IkConstraint#getMix()}, {@link IkConstraint#getBendDirection()}, and /** Changes an IK constraint's {@link IkConstraint#getMix()}, {@link IkConstraint#getBendDirection()},
* {@link IkConstraint#getStretch()}. */ * {@link IkConstraint#getStretch()}, and {@link IkConstraint#getCompress()}. */
static public class IkConstraintTimeline extends CurveTimeline { static public class IkConstraintTimeline extends CurveTimeline {
static public final int ENTRIES = 4; static public final int ENTRIES = 5;
static private final int PREV_TIME = -4, PREV_MIX = -3, PREV_BEND_DIRECTION = -2, PREV_STRETCH = -1; static private final int PREV_TIME = -5, PREV_MIX = -4, PREV_BEND_DIRECTION = -3, PREV_COMPRESS = -2, PREV_STRETCH = -1;
static private final int MIX = 1, BEND_DIRECTION = 2, STRETCH = 3; static private final int MIX = 1, BEND_DIRECTION = 2, COMPRESS = 3, STRETCH = 4;
int ikConstraintIndex; int ikConstraintIndex;
private final float[] frames; // time, mix, bendDirection, ... private final float[] frames; // time, mix, bendDirection, compress, stretch, ...
public IkConstraintTimeline (int frameCount) { public IkConstraintTimeline (int frameCount) {
super(frameCount); super(frameCount);
@ -1324,17 +1324,18 @@ public class Animation {
return ikConstraintIndex; return ikConstraintIndex;
} }
/** The time in seconds, mix, and bend direction for each key frame. */ /** The time in seconds, mix, bend direction, compress, and stretch for each key frame. */
public float[] getFrames () { public float[] getFrames () {
return frames; return frames;
} }
/** Sets the time in seconds, mix, and bend direction for the specified key frame. */ /** Sets the time in seconds, mix, bend direction, compress, and stretch for the specified key frame. */
public void setFrame (int frameIndex, float time, float mix, int bendDirection, boolean stretch) { public void setFrame (int frameIndex, float time, float mix, int bendDirection, boolean compress, boolean stretch) {
frameIndex *= ENTRIES; frameIndex *= ENTRIES;
frames[frameIndex] = time; frames[frameIndex] = time;
frames[frameIndex + MIX] = mix; frames[frameIndex + MIX] = mix;
frames[frameIndex + BEND_DIRECTION] = bendDirection; frames[frameIndex + BEND_DIRECTION] = bendDirection;
frames[frameIndex + COMPRESS] = compress ? 1 : 0;
frames[frameIndex + STRETCH] = stretch ? 1 : 0; frames[frameIndex + STRETCH] = stretch ? 1 : 0;
} }
@ -1348,11 +1349,13 @@ public class Animation {
case setup: case setup:
constraint.mix = constraint.data.mix; constraint.mix = constraint.data.mix;
constraint.bendDirection = constraint.data.bendDirection; constraint.bendDirection = constraint.data.bendDirection;
constraint.compress = constraint.data.compress;
constraint.stretch = constraint.data.stretch; constraint.stretch = constraint.data.stretch;
return; return;
case first: case first:
constraint.mix += (constraint.data.mix - constraint.mix) * alpha; constraint.mix += (constraint.data.mix - constraint.mix) * alpha;
constraint.bendDirection = constraint.data.bendDirection; constraint.bendDirection = constraint.data.bendDirection;
constraint.compress = constraint.data.compress;
constraint.stretch = constraint.data.stretch; constraint.stretch = constraint.data.stretch;
} }
return; return;
@ -1363,15 +1366,18 @@ public class Animation {
constraint.mix = constraint.data.mix + (frames[frames.length + PREV_MIX] - constraint.data.mix) * alpha; constraint.mix = constraint.data.mix + (frames[frames.length + PREV_MIX] - constraint.data.mix) * alpha;
if (direction == out) { if (direction == out) {
constraint.bendDirection = constraint.data.bendDirection; constraint.bendDirection = constraint.data.bendDirection;
constraint.compress = constraint.data.compress;
constraint.stretch = constraint.data.stretch; constraint.stretch = constraint.data.stretch;
} else { } else {
constraint.bendDirection = (int)frames[frames.length + PREV_BEND_DIRECTION]; constraint.bendDirection = (int)frames[frames.length + PREV_BEND_DIRECTION];
constraint.compress = frames[frames.length + PREV_COMPRESS] != 0;
constraint.stretch = frames[frames.length + PREV_STRETCH] != 0; constraint.stretch = frames[frames.length + PREV_STRETCH] != 0;
} }
} else { } else {
constraint.mix += (frames[frames.length + PREV_MIX] - constraint.mix) * alpha; constraint.mix += (frames[frames.length + PREV_MIX] - constraint.mix) * alpha;
if (direction == in) { if (direction == in) {
constraint.bendDirection = (int)frames[frames.length + PREV_BEND_DIRECTION]; constraint.bendDirection = (int)frames[frames.length + PREV_BEND_DIRECTION];
constraint.compress = frames[frames.length + PREV_COMPRESS] != 0;
constraint.stretch = frames[frames.length + PREV_STRETCH] != 0; constraint.stretch = frames[frames.length + PREV_STRETCH] != 0;
} }
} }
@ -1388,15 +1394,18 @@ public class Animation {
constraint.mix = constraint.data.mix + (mix + (frames[frame + MIX] - mix) * percent - constraint.data.mix) * alpha; constraint.mix = constraint.data.mix + (mix + (frames[frame + MIX] - mix) * percent - constraint.data.mix) * alpha;
if (direction == out) { if (direction == out) {
constraint.bendDirection = constraint.data.bendDirection; constraint.bendDirection = constraint.data.bendDirection;
constraint.compress = constraint.data.compress;
constraint.stretch = constraint.data.stretch; constraint.stretch = constraint.data.stretch;
} else { } else {
constraint.bendDirection = (int)frames[frame + PREV_BEND_DIRECTION]; constraint.bendDirection = (int)frames[frame + PREV_BEND_DIRECTION];
constraint.compress = frames[frame + PREV_COMPRESS] != 0;
constraint.stretch = frames[frame + PREV_STRETCH] != 0; constraint.stretch = frames[frame + PREV_STRETCH] != 0;
} }
} else { } else {
constraint.mix += (mix + (frames[frame + MIX] - mix) * percent - constraint.mix) * alpha; constraint.mix += (mix + (frames[frame + MIX] - mix) * percent - constraint.mix) * alpha;
if (direction == in) { if (direction == in) {
constraint.bendDirection = (int)frames[frame + PREV_BEND_DIRECTION]; constraint.bendDirection = (int)frames[frame + PREV_BEND_DIRECTION];
constraint.compress = frames[frame + PREV_COMPRESS] != 0;
constraint.stretch = frames[frame + PREV_STRETCH] != 0; constraint.stretch = frames[frame + PREV_STRETCH] != 0;
} }
} }

View File

@ -38,6 +38,7 @@ import com.badlogic.gdx.utils.IntArray;
import com.badlogic.gdx.utils.IntSet; import com.badlogic.gdx.utils.IntSet;
import com.badlogic.gdx.utils.Pool; import com.badlogic.gdx.utils.Pool;
import com.badlogic.gdx.utils.Pool.Poolable; import com.badlogic.gdx.utils.Pool.Poolable;
import com.esotericsoftware.spine.Animation.AttachmentTimeline; import com.esotericsoftware.spine.Animation.AttachmentTimeline;
import com.esotericsoftware.spine.Animation.DrawOrderTimeline; import com.esotericsoftware.spine.Animation.DrawOrderTimeline;
import com.esotericsoftware.spine.Animation.MixBlend; import com.esotericsoftware.spine.Animation.MixBlend;
@ -62,20 +63,20 @@ public class AnimationState {
/** 1) This is the first timeline to set this property.<br> /** 1) This is the first timeline to set this property.<br>
* 2) The next track entry to be applied does have a timeline to set this property.<br> * 2) The next track entry to be applied does have a timeline to set this property.<br>
* 3) The next track entry after that one does not have a timeline to set this property.<br> * 3) The next track entry after that one does not have a timeline to set this property.<br>
* Result: Mix from the setup pose to the timeline pose, but avoid the "dipping" problem by not using the mix percentage. This * Result: Mix from the setup pose to the timeline pose, but do not mix out. This avoids "dipping" when crossfading animations
* means the timeline pose won't mix out toward the setup pose. A subsequent timeline will set this property using a mix. */ * that key the same property. A subsequent timeline will set this property using a mix. */
static private final int DIP = 2; static private final int HOLD = 2;
/** 1) This is the first timeline to set this property.<br> /** 1) This is the first timeline to set this property.<br>
* 2) The next track entry to be applied does have a timeline to set this property.<br> * 2) The next track entry to be applied does have a timeline to set this property.<br>
* 3) The next track entry after that one does have a timeline to set this property.<br> * 3) The next track entry after that one does have a timeline to set this property.<br>
* 4) timelineDipMix stores the first subsequent track entry that does not have a timeline to set this property.<br> * 4) timelineHoldMix stores the first subsequent track entry that does not have a timeline to set this property.<br>
* Result: This is the same as DIP except the mix percentage from the timelineDipMix track entry is used. This handles when * Result: The same as HOLD except the mix percentage from the timelineHoldMix track entry is used. This handles when more than
* more than 2 track entries in a row have a timeline which sets the same property.<br> * 2 track entries in a row have a timeline that sets the same property.<br>
* Eg, A -> B -> C -> D where A, B, and C have a timeline to set the same property, but D does not. When A is applied, A's mix * Eg, A -> B -> C -> D where A, B, and C have a timeline setting same property, but D does not. When A is applied, to avoid
* percentage is not used to avoid dipping, however a later track entry (D, the first entry without a timeline which sets the * "dipping" A is not mixed out, however D (the first entry that doesn't set the property) mixing in is used to mix out A
* property) is actually mixing out A (which affects B and C). Without using D's mix percentage, A would be applied fully until * (which affects B and C). Without using D to mix out, A would be applied fully until mixing completes, then snap into
* mixed out, causing snapping. */ * place. */
static private final int DIP_MIX = 3; static private final int HOLD_MIX = 3;
private AnimationStateData data; private AnimationStateData data;
final Array<TrackEntry> tracks = new Array(); final Array<TrackEntry> tracks = new Array();
@ -83,11 +84,10 @@ public class AnimationState {
final Array<AnimationStateListener> listeners = new Array(); final Array<AnimationStateListener> listeners = new Array();
private final EventQueue queue = new EventQueue(); private final EventQueue queue = new EventQueue();
private final IntSet propertyIDs = new IntSet(); private final IntSet propertyIDs = new IntSet();
private final Array<TrackEntry> mixingTo = new Array();
boolean animationsChanged; boolean animationsChanged;
private float timeScale = 1; private float timeScale = 1;
Pool<TrackEntry> trackEntryPool = new Pool() { final Pool<TrackEntry> trackEntryPool = new Pool() {
protected Object newObject () { protected Object newObject () {
return new TrackEntry(); return new TrackEntry();
} }
@ -147,6 +147,7 @@ public class AnimationState {
// End mixing from entries once all have completed. // End mixing from entries once all have completed.
TrackEntry from = current.mixingFrom; TrackEntry from = current.mixingFrom;
current.mixingFrom = null; current.mixingFrom = null;
if (from != null) from.mixingTo = null;
while (from != null) { while (from != null) {
queue.end(from); queue.end(from);
from = from.mixingFrom; from = from.mixingFrom;
@ -174,6 +175,7 @@ public class AnimationState {
// Require totalAlpha == 0 to ensure mixing is complete, unless mixDuration == 0 (the transition is a single frame). // Require totalAlpha == 0 to ensure mixing is complete, unless mixDuration == 0 (the transition is a single frame).
if (from.totalAlpha == 0 || to.mixDuration == 0) { if (from.totalAlpha == 0 || to.mixDuration == 0) {
to.mixingFrom = from.mixingFrom; to.mixingFrom = from.mixingFrom;
if (from.mixingFrom != null) from.mixingFrom.mixingTo = to;
to.interruptAlpha = from.interruptAlpha; to.interruptAlpha = from.interruptAlpha;
queue.end(from); queue.end(from);
} }
@ -213,11 +215,11 @@ public class AnimationState {
float animationLast = current.animationLast, animationTime = current.getAnimationTime(); float animationLast = current.animationLast, animationTime = current.getAnimationTime();
int timelineCount = current.animation.timelines.size; int timelineCount = current.animation.timelines.size;
Object[] timelines = current.animation.timelines.items; Object[] timelines = current.animation.timelines.items;
if (mix == 1 || blend == MixBlend.add) { if (i == 0 && (mix == 1 || blend == MixBlend.add)) {
for (int ii = 0; ii < timelineCount; ii++) for (int ii = 0; ii < timelineCount; ii++)
((Timeline)timelines[ii]).apply(skeleton, animationLast, animationTime, events, mix, blend, MixDirection.in); ((Timeline)timelines[ii]).apply(skeleton, animationLast, animationTime, events, mix, blend, MixDirection.in);
} else { } else {
int[] timelineData = current.timelineData.items; int[] timelineMode = current.timelineMode.items;
boolean firstFrame = current.timelinesRotation.size == 0; boolean firstFrame = current.timelinesRotation.size == 0;
if (firstFrame) current.timelinesRotation.setSize(timelineCount << 1); if (firstFrame) current.timelinesRotation.setSize(timelineCount << 1);
@ -225,7 +227,7 @@ public class AnimationState {
for (int ii = 0; ii < timelineCount; ii++) { for (int ii = 0; ii < timelineCount; ii++) {
Timeline timeline = (Timeline)timelines[ii]; Timeline timeline = (Timeline)timelines[ii];
MixBlend timelineBlend = timelineData[ii] == SUBSEQUENT ? blend : MixBlend.setup; MixBlend timelineBlend = timelineMode[ii] == SUBSEQUENT ? blend : MixBlend.setup;
if (timeline instanceof RotateTimeline) { if (timeline instanceof RotateTimeline) {
applyRotateTimeline(timeline, skeleton, animationTime, mix, timelineBlend, timelinesRotation, ii << 1, applyRotateTimeline(timeline, skeleton, animationTime, mix, timelineBlend, timelinesRotation, ii << 1,
firstFrame); firstFrame);
@ -262,14 +264,14 @@ public class AnimationState {
float animationLast = from.animationLast, animationTime = from.getAnimationTime(); float animationLast = from.animationLast, animationTime = from.getAnimationTime();
int timelineCount = from.animation.timelines.size; int timelineCount = from.animation.timelines.size;
Object[] timelines = from.animation.timelines.items; Object[] timelines = from.animation.timelines.items;
float alphaDip = from.alpha * to.interruptAlpha, alphaMix = alphaDip * (1 - mix); float alphaHold = from.alpha * to.interruptAlpha, alphaMix = alphaHold * (1 - mix);
if (blend == MixBlend.add) { if (blend == MixBlend.add) {
for (int i = 0; i < timelineCount; i++) for (int i = 0; i < timelineCount; i++)
((Timeline)timelines[i]).apply(skeleton, animationLast, animationTime, events, alphaMix, blend, MixDirection.out); ((Timeline)timelines[i]).apply(skeleton, animationLast, animationTime, events, alphaMix, blend, MixDirection.out);
} else { } else {
int[] timelineData = from.timelineData.items; int[] timelineMode = from.timelineMode.items;
Object[] timelineDipMix = from.timelineDipMix.items; Object[] timelineHoldMix = from.timelineHoldMix.items;
boolean firstFrame = from.timelinesRotation.size == 0; boolean firstFrame = from.timelinesRotation.size == 0;
if (firstFrame) from.timelinesRotation.setSize(timelineCount << 1); if (firstFrame) from.timelinesRotation.setSize(timelineCount << 1);
@ -280,7 +282,7 @@ public class AnimationState {
Timeline timeline = (Timeline)timelines[i]; Timeline timeline = (Timeline)timelines[i];
MixBlend timelineBlend; MixBlend timelineBlend;
float alpha; float alpha;
switch (timelineData[i]) { switch (timelineMode[i]) {
case SUBSEQUENT: case SUBSEQUENT:
if (!attachments && timeline instanceof AttachmentTimeline) continue; if (!attachments && timeline instanceof AttachmentTimeline) continue;
if (!drawOrder && timeline instanceof DrawOrderTimeline) continue; if (!drawOrder && timeline instanceof DrawOrderTimeline) continue;
@ -291,14 +293,14 @@ public class AnimationState {
timelineBlend = MixBlend.setup; timelineBlend = MixBlend.setup;
alpha = alphaMix; alpha = alphaMix;
break; break;
case DIP: case HOLD:
timelineBlend = MixBlend.setup; timelineBlend = MixBlend.setup;
alpha = alphaDip; alpha = alphaHold;
break; break;
default: default:
timelineBlend = MixBlend.setup; timelineBlend = MixBlend.setup;
TrackEntry dipMix = (TrackEntry)timelineDipMix[i]; TrackEntry holdMix = (TrackEntry)timelineHoldMix[i];
alpha = alphaDip * Math.max(0, 1 - dipMix.mixTime / dipMix.mixDuration); alpha = alphaHold * Math.max(0, 1 - holdMix.mixTime / holdMix.mixDuration);
break; break;
} }
from.totalAlpha += alpha; from.totalAlpha += alpha;
@ -356,10 +358,10 @@ public class AnimationState {
// Mix between rotations using the direction of the shortest route on the first frame. // Mix between rotations using the direction of the shortest route on the first frame.
float r1 = blend == MixBlend.setup ? bone.data.rotation : bone.rotation; float r1 = blend == MixBlend.setup ? bone.data.rotation : bone.rotation;
float total, diff = r2 - r1; float total, diff = r2 - r1;
diff -= (16384 - (int)(16384.499999999996 - diff / 360)) * 360;
if (diff == 0) if (diff == 0)
total = timelinesRotation[i]; total = timelinesRotation[i];
else { else {
diff -= (16384 - (int)(16384.499999999996 - diff / 360)) * 360;
float lastTotal, lastDiff; float lastTotal, lastDiff;
if (firstFrame) { if (firstFrame) {
lastTotal = 0; lastTotal = 0;
@ -448,6 +450,7 @@ public class AnimationState {
if (from == null) break; if (from == null) break;
queue.end(from); queue.end(from);
entry.mixingFrom = null; entry.mixingFrom = null;
entry.mixingTo = null;
entry = from; entry = from;
} }
@ -463,6 +466,7 @@ public class AnimationState {
if (from != null) { if (from != null) {
if (interrupt) queue.interrupt(from); if (interrupt) queue.interrupt(from);
current.mixingFrom = from; current.mixingFrom = from;
from.mixingTo = current;
current.mixTime = 0; current.mixTime = 0;
// Store the interrupted mix percentage. // Store the interrupted mix percentage.
@ -625,6 +629,7 @@ public class AnimationState {
entry.trackIndex = trackIndex; entry.trackIndex = trackIndex;
entry.animation = animation; entry.animation = animation;
entry.loop = loop; entry.loop = loop;
entry.holdPrevious = false;
entry.eventThreshold = 0; entry.eventThreshold = 0;
entry.attachmentThreshold = 0; entry.attachmentThreshold = 0;
@ -662,15 +667,67 @@ public class AnimationState {
animationsChanged = false; animationsChanged = false;
IntSet propertyIDs = this.propertyIDs; IntSet propertyIDs = this.propertyIDs;
propertyIDs.clear(); propertyIDs.clear(2048);
Array<TrackEntry> mixingTo = this.mixingTo;
for (int i = 0, n = tracks.size; i < n; i++) { for (int i = 0, n = tracks.size; i < n; i++) {
TrackEntry entry = tracks.get(i); TrackEntry entry = tracks.get(i);
if (entry != null && (i == 0 || entry.mixBlend != MixBlend.add)) entry.setTimelineData(null, mixingTo, propertyIDs); if (entry == null) continue;
// Move to last entry, then iterate in reverse (the order animations are applied).
while (entry.mixingFrom != null)
entry = entry.mixingFrom;
do {
if (entry.mixingTo == null || entry.mixBlend != MixBlend.add) setTimelineModes(entry);
entry = entry.mixingTo;
} while (entry != null);
} }
} }
private void setTimelineModes (TrackEntry entry) {
TrackEntry to = entry.mixingTo;
Object[] timelines = entry.animation.timelines.items;
int timelinesCount = entry.animation.timelines.size;
int[] timelineMode = entry.timelineMode.setSize(timelinesCount);
entry.timelineHoldMix.clear();
Object[] timelineHoldMix = entry.timelineHoldMix.setSize(timelinesCount);
IntSet propertyIDs = this.propertyIDs;
if (to != null && to.holdPrevious) {
for (int i = 0; i < timelinesCount; i++) {
propertyIDs.add(((Timeline)timelines[i]).getPropertyId());
timelineMode[i] = HOLD;
}
return;
}
outer:
for (int i = 0; i < timelinesCount; i++) {
int id = ((Timeline)timelines[i]).getPropertyId();
if (!propertyIDs.add(id))
timelineMode[i] = SUBSEQUENT;
else if (to == null || !hasTimeline(to, id))
timelineMode[i] = FIRST;
else {
for (TrackEntry next = to.mixingTo; next != null; next = next.mixingTo) {
if (hasTimeline(next, id)) continue;
if (next.mixDuration > 0) {
timelineMode[i] = HOLD_MIX;
timelineHoldMix[i] = next;
continue outer;
}
break;
}
timelineMode[i] = HOLD;
}
}
}
private boolean hasTimeline (TrackEntry entry, int id) {
Object[] timelines = entry.animation.timelines.items;
for (int i = 0, n = entry.animation.timelines.size; i < n; i++)
if (((Timeline)timelines[i]).getPropertyId() == id) return true;
return false;
}
/** 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. */
public TrackEntry getCurrent (int trackIndex) { public TrackEntry getCurrent (int trackIndex) {
if (trackIndex >= tracks.size) return null; if (trackIndex >= tracks.size) return null;
@ -744,74 +801,30 @@ public class AnimationState {
* References to a track entry must not be kept after the {@link AnimationStateListener#dispose(TrackEntry)} event occurs. */ * References to a track entry must not be kept after the {@link AnimationStateListener#dispose(TrackEntry)} event occurs. */
static public class TrackEntry implements Poolable { static public class TrackEntry implements Poolable {
Animation animation; Animation animation;
TrackEntry next, mixingFrom; TrackEntry next, mixingFrom, mixingTo;
AnimationStateListener listener; AnimationStateListener listener;
int trackIndex; int trackIndex;
boolean loop; boolean loop, holdPrevious;
float eventThreshold, attachmentThreshold, drawOrderThreshold; float eventThreshold, attachmentThreshold, drawOrderThreshold;
float animationStart, animationEnd, animationLast, nextAnimationLast; float animationStart, animationEnd, animationLast, nextAnimationLast;
float delay, trackTime, trackLast, nextTrackLast, trackEnd, timeScale; float delay, trackTime, trackLast, nextTrackLast, trackEnd, timeScale;
float alpha, mixTime, mixDuration, interruptAlpha, totalAlpha; float alpha, mixTime, mixDuration, interruptAlpha, totalAlpha;
MixBlend mixBlend = MixBlend.replace; MixBlend mixBlend = MixBlend.replace;
final IntArray timelineData = new IntArray(); final IntArray timelineMode = new IntArray();
final Array<TrackEntry> timelineDipMix = new Array(); final Array<TrackEntry> timelineHoldMix = new Array();
final FloatArray timelinesRotation = new FloatArray(); final FloatArray timelinesRotation = new FloatArray();
public void reset () { public void reset () {
next = null; next = null;
mixingFrom = null; mixingFrom = null;
mixingTo = null;
animation = null; animation = null;
listener = null; listener = null;
timelineData.clear(); timelineMode.clear();
timelineDipMix.clear(); timelineHoldMix.clear();
timelinesRotation.clear(); timelinesRotation.clear();
} }
/** @param to May be null. */
TrackEntry setTimelineData (TrackEntry to, Array<TrackEntry> mixingToArray, IntSet propertyIDs) {
if (to != null) mixingToArray.add(to);
TrackEntry lastEntry = mixingFrom != null ? mixingFrom.setTimelineData(this, mixingToArray, propertyIDs) : this;
if (to != null) mixingToArray.pop();
Object[] mixingTo = mixingToArray.items;
int mixingToLast = mixingToArray.size - 1;
Object[] timelines = animation.timelines.items;
int timelinesCount = animation.timelines.size;
int[] timelineData = this.timelineData.setSize(timelinesCount);
timelineDipMix.clear();
Object[] timelineDipMix = this.timelineDipMix.setSize(timelinesCount);
outer:
for (int i = 0; i < timelinesCount; i++) {
int id = ((Timeline)timelines[i]).getPropertyId();
if (!propertyIDs.add(id))
timelineData[i] = SUBSEQUENT;
else if (to == null || !to.hasTimeline(id))
timelineData[i] = FIRST;
else {
for (int ii = mixingToLast; ii >= 0; ii--) {
TrackEntry entry = (TrackEntry)mixingTo[ii];
if (!entry.hasTimeline(id)) {
if (entry.mixDuration > 0) {
timelineData[i] = DIP_MIX;
timelineDipMix[i] = entry;
continue outer;
}
break;
}
}
timelineData[i] = DIP;
}
}
return lastEntry;
}
private boolean hasTimeline (int id) {
Object[] timelines = animation.timelines.items;
for (int i = 0, n = animation.timelines.size; i < n; i++)
if (((Timeline)timelines[i]).getPropertyId() == id) return true;
return false;
}
/** The index of the track where this track entry is either current or queued. /** The index of the track where this track entry is either current or queued.
* <p> * <p>
* See {@link AnimationState#getCurrent(int)}. */ * See {@link AnimationState#getCurrent(int)}. */
@ -1054,6 +1067,25 @@ public class AnimationState {
return mixingFrom; return mixingFrom;
} }
public void setHoldPrevious (boolean holdPrevious) {
this.holdPrevious = holdPrevious;
}
/** If true, when mixing from the previous animation to this animation, the previous animation is applied as normal instead
* of being mixed out.
* <p>
* When mixing between animations that key the same property, if a lower track also keys that property then the value will
* briefly dip toward the lower track value during the mix. This happens because the first animation mixes from 100% to 0%
* while the second animation mixes from 0% to 100%. Setting <code>holdPrevious</code> to true applies the first animation
* at 100% during the mix so the lower track value is overwritten. Such dipping does not occur on the lowest track which
* keys the property, only when a higher track also keys the property.
* <p>
* Snapping will occur if <code>holdPrevious</code> is true and this animation does not key all the same properties as the
* previous animation. */
public boolean getHoldPrevious () {
return holdPrevious;
}
/** Resets the rotation directions for mixing this entry's rotate timelines. This can be useful to avoid bones rotating the /** Resets the rotation directions for mixing this entry's rotate timelines. This can be useful to avoid bones rotating the
* long way around when using {@link #alpha} and starting animations on other tracks. * long way around when using {@link #alpha} and starting animations on other tracks.
* <p> * <p>
@ -1165,7 +1197,7 @@ public class AnimationState {
start, interrupt, end, dispose, complete, event start, interrupt, end, dispose, complete, event
} }
/** The interface which can be implemented to receive TrackEntry events. /** The interface to implement for receiving TrackEntry events.
* <p> * <p>
* See TrackEntry {@link TrackEntry#setListener(AnimationStateListener)} and AnimationState * See TrackEntry {@link TrackEntry#setListener(AnimationStateListener)} and AnimationState
* {@link AnimationState#addListener(AnimationStateListener)}. */ * {@link AnimationState#addListener(AnimationStateListener)}. */

View File

@ -43,7 +43,7 @@ public class IkConstraint implements Constraint {
final Array<Bone> bones; final Array<Bone> bones;
Bone target; Bone target;
int bendDirection; int bendDirection;
boolean stretch; boolean compress, stretch;
float mix = 1; float mix = 1;
public IkConstraint (IkConstraintData data, Skeleton skeleton) { public IkConstraint (IkConstraintData data, Skeleton skeleton) {
@ -52,6 +52,7 @@ public class IkConstraint implements Constraint {
this.data = data; this.data = data;
mix = data.mix; mix = data.mix;
bendDirection = data.bendDirection; bendDirection = data.bendDirection;
compress = data.compress;
stretch = data.stretch; stretch = data.stretch;
bones = new Array(data.bones.size); bones = new Array(data.bones.size);
@ -71,6 +72,7 @@ public class IkConstraint implements Constraint {
target = skeleton.bones.get(constraint.target.data.index); target = skeleton.bones.get(constraint.target.data.index);
mix = constraint.mix; mix = constraint.mix;
bendDirection = constraint.bendDirection; bendDirection = constraint.bendDirection;
compress = constraint.compress;
stretch = constraint.stretch; stretch = constraint.stretch;
} }
@ -84,7 +86,7 @@ public class IkConstraint implements Constraint {
Array<Bone> bones = this.bones; Array<Bone> bones = this.bones;
switch (bones.size) { switch (bones.size) {
case 1: case 1:
apply(bones.first(), target.worldX, target.worldY, stretch, mix); apply(bones.first(), target.worldX, target.worldY, compress, stretch, data.uniform, mix);
break; break;
case 2: case 2:
apply(bones.first(), bones.get(1), target.worldX, target.worldY, bendDirection, stretch, mix); apply(bones.first(), bones.get(1), target.worldX, target.worldY, bendDirection, stretch, mix);
@ -128,8 +130,17 @@ public class IkConstraint implements Constraint {
this.bendDirection = bendDirection; 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 /** When true and only a single bone is being constrained, if the target is too close, the bone is scaled to reach it. */
* nonuniform scale, stretching is not applied. */ public boolean getCompress () {
return compress;
}
public void setCompress (boolean compress) {
this.compress = compress;
}
/** When true, if the target is out of range, the parent bone is scaled to reach it. If more than one bone is being constrained
* and the parent bone has local nonuniform scale, stretch is not applied. */
public boolean getStretch () { public boolean getStretch () {
return stretch; return stretch;
} }
@ -148,7 +159,8 @@ public class IkConstraint implements Constraint {
} }
/** Applies 1 bone IK. The target is specified in the world coordinate system. */ /** Applies 1 bone IK. The target is specified in the world coordinate system. */
static public void apply (Bone bone, float targetX, float targetY, boolean stretch, float alpha) { static public void apply (Bone bone, float targetX, float targetY, boolean compress, boolean stretch, boolean uniform,
float alpha) {
if (!bone.appliedValid) bone.updateAppliedTransform(); if (!bone.appliedValid) bone.updateAppliedTransform();
Bone p = bone.parent; Bone p = bone.parent;
float id = 1 / (p.a * p.d - p.b * p.c); float id = 1 / (p.a * p.d - p.b * p.c);
@ -158,14 +170,18 @@ public class IkConstraint implements Constraint {
if (bone.ascaleX < 0) rotationIK += 180; if (bone.ascaleX < 0) rotationIK += 180;
if (rotationIK > 180) if (rotationIK > 180)
rotationIK -= 360; rotationIK -= 360;
else if (rotationIK < -180) rotationIK += 360; else if (rotationIK < -180) //
float sx = bone.ascaleX; rotationIK += 360;
if (stretch) { float sx = bone.ascaleX, sy = bone.ascaleY;
if (compress || stretch) {
float b = bone.data.length * sx, dd = (float)Math.sqrt(tx * tx + ty * ty); float b = bone.data.length * sx, dd = (float)Math.sqrt(tx * tx + ty * ty);
if (dd > b && b > 0.0001f) sx *= (dd / b - 1) * alpha + 1; if ((compress && dd < b) || (stretch && dd > b) && b > 0.0001f) {
float s = (dd / b - 1) * alpha + 1;
sx *= s;
if (uniform) sy *= s;
}
} }
bone.updateWorldTransform(bone.ax, bone.ay, bone.arotation + rotationIK * alpha, sx, bone.ascaleY, bone.ashearX, bone.updateWorldTransform(bone.ax, bone.ay, bone.arotation + rotationIK * alpha, sx, sy, bone.ashearX, bone.ashearY);
bone.ashearY);
} }
/** Applies 2 bone IK. The target is specified in the world coordinate system. /** Applies 2 bone IK. The target is specified in the world coordinate system.

View File

@ -41,7 +41,7 @@ public class IkConstraintData {
final Array<BoneData> bones = new Array(); final Array<BoneData> bones = new Array();
BoneData target; BoneData target;
int bendDirection = 1; int bendDirection = 1;
boolean stretch; boolean compress, stretch, uniform;
float mix = 1; float mix = 1;
public IkConstraintData (String name) { public IkConstraintData (String name) {
@ -78,25 +78,6 @@ public class IkConstraintData {
this.target = target; this.target = target;
} }
/** Controls the bend direction of the IK bones, either 1 or -1. */
public int getBendDirection () {
return bendDirection;
}
public void setBendDirection (int bendDirection) {
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. */ /** A percentage (0-1) that controls the mix between the constrained and unconstrained rotations. */
public float getMix () { public float getMix () {
return mix; return mix;
@ -106,6 +87,44 @@ public class IkConstraintData {
this.mix = mix; this.mix = mix;
} }
/** Controls the bend direction of the IK bones, either 1 or -1. */
public int getBendDirection () {
return bendDirection;
}
public void setBendDirection (int bendDirection) {
this.bendDirection = bendDirection;
}
/** When true and only a single bone is being constrained, if the target is too close, the bone is scaled to reach it. */
public boolean getCompress () {
return compress;
}
public void setCompress (boolean compress) {
this.compress = compress;
}
/** When true, if the target is out of range, the parent bone is scaled to reach it. If more than one bone is being constrained
* and the parent bone has local nonuniform scale, stretch is not applied. */
public boolean getStretch () {
return stretch;
}
public void setStretch (boolean stretch) {
this.stretch = stretch;
}
/** When true, only a single bone is being constrained, and {@link #getCompress()} or {@link #getStretch()} is used, the bone
* is scaled on both the X and Y axes. */
public boolean getUniform () {
return uniform;
}
public void setUniform (boolean uniform) {
this.uniform = uniform;
}
public String toString () { public String toString () {
return name; return name;
} }

View File

@ -394,9 +394,10 @@ public class Skeleton {
Array<IkConstraint> ikConstraints = this.ikConstraints; Array<IkConstraint> ikConstraints = this.ikConstraints;
for (int i = 0, n = ikConstraints.size; i < n; i++) { for (int i = 0, n = ikConstraints.size; i < n; i++) {
IkConstraint constraint = ikConstraints.get(i); IkConstraint constraint = ikConstraints.get(i);
constraint.bendDirection = constraint.data.bendDirection;
constraint.stretch = constraint.data.stretch;
constraint.mix = constraint.data.mix; constraint.mix = constraint.data.mix;
constraint.bendDirection = constraint.data.bendDirection;
constraint.compress = constraint.data.compress;
constraint.stretch = constraint.data.stretch;
} }
Array<TransformConstraint> transformConstraints = this.transformConstraints; Array<TransformConstraint> transformConstraints = this.transformConstraints;

View File

@ -232,7 +232,9 @@ public class SkeletonBinary {
data.target = skeletonData.bones.get(input.readInt(true)); data.target = skeletonData.bones.get(input.readInt(true));
data.mix = input.readFloat(); data.mix = input.readFloat();
data.bendDirection = input.readByte(); data.bendDirection = input.readByte();
data.compress = input.readBoolean();
data.stretch = input.readBoolean(); data.stretch = input.readBoolean();
data.uniform = input.readBoolean();
skeletonData.ikConstraints.add(data); skeletonData.ikConstraints.add(data);
} }
@ -307,7 +309,7 @@ public class SkeletonBinary {
data.intValue = input.readInt(false); data.intValue = input.readInt(false);
data.floatValue = input.readFloat(); data.floatValue = input.readFloat();
data.stringValue = input.readString(); data.stringValue = input.readString();
data.audioPath = input.readString(); data.audioPath = input.readString();
skeletonData.events.add(data); skeletonData.events.add(data);
} }
@ -661,7 +663,8 @@ public class SkeletonBinary {
IkConstraintTimeline timeline = new IkConstraintTimeline(frameCount); IkConstraintTimeline timeline = new IkConstraintTimeline(frameCount);
timeline.ikConstraintIndex = index; timeline.ikConstraintIndex = index;
for (int frameIndex = 0; frameIndex < frameCount; frameIndex++) { for (int frameIndex = 0; frameIndex < frameCount; frameIndex++) {
timeline.setFrame(frameIndex, input.readFloat(), input.readFloat(), input.readByte(), input.readBoolean()); timeline.setFrame(frameIndex, input.readFloat(), input.readFloat(), input.readByte(), input.readBoolean(),
input.readBoolean());
if (frameIndex < frameCount - 1) readCurve(input, frameIndex, timeline); if (frameIndex < frameCount - 1) readCurve(input, frameIndex, timeline);
} }
timelines.add(timeline); timelines.add(timeline);

View File

@ -185,9 +185,11 @@ public class SkeletonJson {
data.target = skeletonData.findBone(targetName); data.target = skeletonData.findBone(targetName);
if (data.target == null) throw new SerializationException("IK target bone not found: " + targetName); 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); data.mix = constraintMap.getFloat("mix", 1);
data.bendDirection = constraintMap.getBoolean("bendPositive", true) ? 1 : -1;
data.compress = constraintMap.getBoolean("compress", false);
data.stretch = constraintMap.getBoolean("stretch", false);
data.uniform = constraintMap.getBoolean("uniform", false);
skeletonData.ikConstraints.add(data); skeletonData.ikConstraints.add(data);
} }
@ -569,7 +571,8 @@ public class SkeletonJson {
int frameIndex = 0; int frameIndex = 0;
for (JsonValue valueMap = constraintMap.child; valueMap != null; valueMap = valueMap.next) { for (JsonValue valueMap = constraintMap.child; valueMap != null; valueMap = valueMap.next) {
timeline.setFrame(frameIndex, valueMap.getFloat("time"), valueMap.getFloat("mix", 1), timeline.setFrame(frameIndex, valueMap.getFloat("time"), valueMap.getFloat("mix", 1),
valueMap.getBoolean("bendPositive", true) ? 1 : -1, valueMap.getBoolean("stretch", false)); valueMap.getBoolean("bendPositive", true) ? 1 : -1, valueMap.getBoolean("compress", false),
valueMap.getBoolean("stretch", false));
readCurve(valueMap, timeline, frameIndex); readCurve(valueMap, timeline, frameIndex);
frameIndex++; frameIndex++;
} }

View File

@ -375,6 +375,7 @@ namespace Spine.Unity.Editor {
EditorGUI.BeginChangeCheck(); EditorGUI.BeginChangeCheck();
c.Mix = EditorGUILayout.Slider("Mix", c.Mix, MixMin, MixMax); c.Mix = EditorGUILayout.Slider("Mix", c.Mix, MixMin, MixMax);
c.BendDirection = EditorGUILayout.Toggle(SpineInspectorUtility.TempContent("Bend Clockwise", tooltip: "IkConstraint.BendDirection == 1 if clockwise; -1 if counterclockwise."), c.BendDirection > 0) ? 1 : -1; c.BendDirection = EditorGUILayout.Toggle(SpineInspectorUtility.TempContent("Bend Clockwise", tooltip: "IkConstraint.BendDirection == 1 if clockwise; -1 if counterclockwise."), c.BendDirection > 0) ? 1 : -1;
c.Stretch = EditorGUILayout.Toggle(SpineInspectorUtility.TempContent("Stretch", tooltip: "Stretch the parent bone when the target is out of range. Not applied when parent bone has nonuniform scale."), c.Stretch);
if (EditorGUI.EndChangeCheck()) requireRepaint = true; if (EditorGUI.EndChangeCheck()) requireRepaint = true;
EditorGUILayout.Space(); EditorGUILayout.Space();

View File

@ -905,7 +905,7 @@ namespace Spine.Unity.Editor {
} }
if (!match) if (!match)
Debug.LogWarningFormat("Skeleton '{0}' (exported with Spine {1}) may be incompatible with your runtime version: spine-unity v{2}", asset.name, rawVersion, primaryRuntimeVersionDebugString); Debug.LogWarningFormat("Skeleton '{0}' (exported with Spine {1}) may be incompatible with your runtime version: spine-csharp v{2}", asset.name, rawVersion, primaryRuntimeVersionDebugString);
} }
} }

View File

@ -45,10 +45,9 @@ namespace Spine.Unity.Modules {
public bool ghostingEnabled = true; public bool ghostingEnabled = true;
[Tooltip("The time between invididual ghost pieces being spawned.")] [Tooltip("The time between invididual ghost pieces being spawned.")]
[UnityEngine.Serialization.FormerlySerializedAs("spawnRate")] [UnityEngine.Serialization.FormerlySerializedAs("spawnRate")]
public float spawnInterval = 0.05f; public float spawnInterval = 1f/30f;
[Tooltip("Maximum number of ghosts that can exist at a time. If the fade speed is not fast enough, the oldest ghost will immediately disappear to enforce the maximum number.")] [Tooltip("Maximum number of ghosts that can exist at a time. If the fade speed is not fast enough, the oldest ghost will immediately disappear to enforce the maximum number.")]
public int maximumGhosts = 10; public int maximumGhosts = 10;
[Tooltip("Fadespeed 1 means it will take 1 second for a piece to fade out. 2 means it will take 1/2 second. 10 means it will take 1/10 of a second.")]
public float fadeSpeed = 10; public float fadeSpeed = 10;
[Header("Rendering")] [Header("Rendering")]

View File

@ -35,17 +35,23 @@ using System.Collections;
namespace Spine.Unity.Modules { namespace Spine.Unity.Modules {
public class SkeletonGhostRenderer : MonoBehaviour { public class SkeletonGhostRenderer : MonoBehaviour {
static readonly Color32 TransparentBlack = new Color32(0, 0, 0, 0);
const string colorPropertyName = "_Color";
public float fadeSpeed = 10; float fadeSpeed = 10;
Color32 startColor;
Color32[] colors;
Color32 black = new Color32(0, 0, 0, 0);
MeshFilter meshFilter; MeshFilter meshFilter;
MeshRenderer meshRenderer; MeshRenderer meshRenderer;
MaterialPropertyBlock mpb;
int colorId;
void Awake () { void Awake () {
meshRenderer = gameObject.AddComponent<MeshRenderer>(); meshRenderer = gameObject.AddComponent<MeshRenderer>();
meshFilter = gameObject.AddComponent<MeshFilter>(); meshFilter = gameObject.AddComponent<MeshFilter>();
colorId = Shader.PropertyToID(colorPropertyName);
mpb = new MaterialPropertyBlock();
} }
public void Initialize (Mesh mesh, Material[] materials, Color32 color, bool additive, float speed, int sortingLayerID, int sortingOrder) { public void Initialize (Mesh mesh, Material[] materials, Color32 color, bool additive, float speed, int sortingLayerID, int sortingOrder) {
@ -56,12 +62,9 @@ namespace Spine.Unity.Modules {
meshRenderer.sortingLayerID = sortingLayerID; meshRenderer.sortingLayerID = sortingLayerID;
meshRenderer.sortingOrder = sortingOrder; meshRenderer.sortingOrder = sortingOrder;
meshFilter.sharedMesh = Instantiate(mesh); meshFilter.sharedMesh = Instantiate(mesh);
colors = meshFilter.sharedMesh.colors32; startColor = color;
mpb.SetColor(colorId, color);
if ((color.a + color.r + color.g + color.b) > 0) { meshRenderer.SetPropertyBlock(mpb);
for (int i = 0; i < colors.Length; i++)
colors[i] = color;
}
fadeSpeed = speed; fadeSpeed = speed;
@ -72,19 +75,17 @@ namespace Spine.Unity.Modules {
} }
IEnumerator Fade () { IEnumerator Fade () {
Color32 c; Color32 c = startColor;
for (int t = 0; t < 500; t++) { Color32 black = SkeletonGhostRenderer.TransparentBlack;
bool breakout = true;
for (int i = 0; i < colors.Length; i++) {
c = colors[i];
if (c.a > 0)
breakout = false;
colors[i] = Color32.Lerp(c, black, Time.deltaTime * fadeSpeed); float t = 1f;
} for (float hardTimeLimit = 5f; hardTimeLimit > 0; hardTimeLimit -= Time.deltaTime) {
meshFilter.sharedMesh.colors32 = colors; c = Color32.Lerp(black, startColor, t);
mpb.SetColor(colorId, c);
meshRenderer.SetPropertyBlock(mpb);
if (breakout) t = Mathf.Lerp(t, 0, Time.deltaTime * fadeSpeed);
if (t <= 0)
break; break;
yield return null; yield return null;
@ -95,25 +96,20 @@ namespace Spine.Unity.Modules {
} }
IEnumerator FadeAdditive () { IEnumerator FadeAdditive () {
Color32 c; Color32 c = startColor;
Color32 black = this.black; Color32 black = SkeletonGhostRenderer.TransparentBlack;
for (int t = 0; t < 500; t++) { float t = 1f;
for (float hardTimeLimit = 5f; hardTimeLimit > 0; hardTimeLimit -= Time.deltaTime) {
c = Color32.Lerp(black, startColor, t);
mpb.SetColor(colorId, c);
meshRenderer.SetPropertyBlock(mpb);
bool breakout = true; t = Mathf.Lerp(t, 0, Time.deltaTime * fadeSpeed);
for (int i = 0; i < colors.Length; i++) { if (t <= 0)
c = colors[i];
black.a = c.a;
if (c.r > 0 || c.g > 0 || c.b > 0)
breakout = false;
colors[i] = Color32.Lerp(c, black, Time.deltaTime * fadeSpeed);
}
meshFilter.sharedMesh.colors32 = colors;
if (breakout)
break; break;
yield return null; yield return null;
} }

View File

@ -1,4 +1,4 @@
/****************************************************************************** /******************************************************************************
* Spine Runtimes Software License v2.5 * Spine Runtimes Software License v2.5
* *
* Copyright (c) 2013-2016, Esoteric Software * Copyright (c) 2013-2016, Esoteric Software
@ -49,7 +49,8 @@ namespace Spine.Unity.Modules {
} }
void Start () { void Start () {
if (mirrorOnStart) StartMirroring(); if (mirrorOnStart)
StartMirroring();
} }
void LateUpdate () { void LateUpdate () {
@ -57,13 +58,16 @@ namespace Spine.Unity.Modules {
} }
void OnDisable () { void OnDisable () {
if (restoreOnDisable) RestoreIndependentSkeleton(); if (restoreOnDisable)
RestoreIndependentSkeleton();
} }
/// <summary>Freeze the SkeletonGraphic on this GameObject, and use the source as the Skeleton to be rendered by the SkeletonGraphic.</summary> /// <summary>Freeze the SkeletonGraphic on this GameObject, and use the source as the Skeleton to be rendered by the SkeletonGraphic.</summary>
public void StartMirroring () { public void StartMirroring () {
if (source == null) return; if (source == null)
if (skeletonGraphic == null) return; return;
if (skeletonGraphic == null)
return;
skeletonGraphic.startingAnimation = string.Empty; skeletonGraphic.startingAnimation = string.Empty;

View File

@ -503,6 +503,7 @@ namespace Spine {
ikc = skeleton.ikConstraints.Items[i]; ikc = skeleton.ikConstraints.Items[i];
ikc.mix = ikc.data.mix; ikc.mix = ikc.data.mix;
ikc.bendDirection = ikc.data.bendDirection; ikc.bendDirection = ikc.data.bendDirection;
ikc.stretch = ikc.data.stretch;
break; break;
// TransformConstraint // TransformConstraint