From 5cd29e55df45000344dc56bb213fef9b01ae34b9 Mon Sep 17 00:00:00 2001 From: NathanSweet Date: Wed, 19 Oct 2016 22:36:04 +0200 Subject: [PATCH 1/2] AnimationState, mixing from multiple animations. #621 --- .../com/esotericsoftware/spine/Animation.java | 6 +- .../spine/AnimationState.java | 112 +++++++----------- 2 files changed, 46 insertions(+), 72 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 465cfb21b..8bc95c640 100644 --- a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Animation.java +++ b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Animation.java @@ -136,8 +136,8 @@ public class Animation { static public interface Timeline { /** Sets the value(s) for the specified time. * @param events May be null to not collect fired events. - * @param setupPose If true, the timeline is mixed with the setup pose, else it is mixed with the current pose. Passing true - * when alpha is 1 is slightly more efficient. + * @param setupPose True when the timeline is mixed with the setup pose, false when it is mixed with the current pose. + * Passing true when alpha is 1 is slightly more efficient. * @param mixingOut True when mixing over time toward the setup or current pose, false when mixing toward the keyed pose. * Irrelevant when alpha is 1. */ public void apply (Skeleton skeleton, float lastTime, float time, Array events, float alpha, boolean setupPose, @@ -916,7 +916,7 @@ public class Animation { if (time < frames[0]) return; // Time is before first frame. // BOZO - Finish timelines handling setupPose and mixingOut from here down. - + IkConstraint constraint = skeleton.ikConstraints.get(ikConstraintIndex); if (time >= frames[frames.length - ENTRIES]) { // Time is after last frame. diff --git a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/AnimationState.java b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/AnimationState.java index f3806240c..b7486c6af 100644 --- a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/AnimationState.java +++ b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/AnimationState.java @@ -52,7 +52,7 @@ public class AnimationState { static private final Animation emptyAnimation = new Animation("", new Array(0), 0); private AnimationStateData data; - private final Array tracks = new Array(); + final Array tracks = new Array(); private final Array events = new Array(); final Array listeners = new Array(); private final EventQueue queue = new EventQueue(); @@ -60,9 +60,6 @@ public class AnimationState { boolean animationsChanged; private float timeScale = 1; - StringBuilder last = new StringBuilder(); - StringBuilder log = new StringBuilder(); - final Pool trackEntryPool = new Pool() { protected Object newObject () { return new TrackEntry(); @@ -112,9 +109,9 @@ public class AnimationState { } continue; } - updateMixingFrom(current, delta); + updateMixingFrom(current, delta, true); } else { - updateMixingFrom(current, delta); + updateMixingFrom(current, delta, true); // Clear the track when there is no next entry, the track end time is reached, and there is no mixingFrom. if (current.trackLast >= current.trackEnd && current.mixingFrom == null) { tracks.set(i, null); @@ -130,11 +127,11 @@ public class AnimationState { queue.drain(); } - private void updateMixingFrom (TrackEntry entry, float delta) { + private void updateMixingFrom (TrackEntry entry, float delta, boolean canEnd) { TrackEntry from = entry.mixingFrom; if (from == null) return; - if (entry.mixTime >= entry.mixDuration && entry.mixTime > 0) { + if (canEnd && entry.mixTime >= entry.mixDuration && entry.mixTime > 0) { queue.end(from); TrackEntry newFrom = from.mixingFrom; entry.mixingFrom = newFrom; @@ -150,7 +147,7 @@ public class AnimationState { from.trackTime += mixingFromDelta; entry.mixTime += mixingFromDelta; - updateMixingFrom(from, delta); + updateMixingFrom(from, delta, canEnd && from.alpha == 1); } /** Poses the skeleton using the track entry animations. There are no side effects other than invoking listeners, so the @@ -161,7 +158,7 @@ public class AnimationState { Array events = this.events; - for (int i = 0; i < tracks.size; i++) { + for (int i = 0, n = tracks.size; i < n; i++) { TrackEntry current = tracks.get(i); if (current == null || current.delay > 0) continue; @@ -171,24 +168,24 @@ public class AnimationState { // Apply current entry. float animationLast = current.animationLast, animationTime = current.getAnimationTime(); - Array timelines = current.animation.timelines; - log("apply current: " + current + ", mix: " + mix + " * " + current.alpha); + int timelineCount = current.animation.timelines.size; + Object[] timelines = current.animation.timelines.items; if (mix == 1) { - for (int ii = 0, n = timelines.size; ii < n; ii++) - timelines.get(ii).apply(skeleton, animationLast, animationTime, events, 1, false, false); + for (int ii = 0; ii < timelineCount; ii++) + ((Timeline)timelines[ii]).apply(skeleton, animationLast, animationTime, events, 1, true, false); } else { boolean firstFrame = current.timelinesRotation.size == 0; - if (firstFrame) current.timelinesRotation.setSize(timelines.size << 1); + if (firstFrame) current.timelinesRotation.setSize(timelineCount << 1); float[] timelinesRotation = current.timelinesRotation.items; + boolean[] timelinesFirst = current.timelinesFirst.items; - for (int ii = 0, n = timelines.size; ii < n; ii++) { - Timeline timeline = timelines.get(ii); + for (int ii = 0; ii < timelineCount; ii++) { + Timeline timeline = (Timeline)timelines[ii]; if (timeline instanceof RotateTimeline) { - applyRotateTimeline((RotateTimeline)timeline, skeleton, animationLast, animationTime, events, mix, - timelinesFirst[ii], false, timelinesRotation, ii << 1, firstFrame); - } else { + applyRotateTimeline(timeline, skeleton, animationTime, mix, timelinesFirst[ii], timelinesRotation, ii << 1, + firstFrame); + } else timeline.apply(skeleton, animationLast, animationTime, events, mix, timelinesFirst[ii], false); - } } } queueEvents(current, animationTime); @@ -197,38 +194,26 @@ public class AnimationState { } queue.drain(); - - if (!log.toString().equals(last.toString())) { - System.out.println(log); - last.setLength(0); - last.append(log); - } - log.setLength(0); - } - - void log (String m) { - log.append(m); - log.append('\n'); } private float applyMixingFrom (TrackEntry entry, Skeleton skeleton, float alpha) { + TrackEntry from = entry.mixingFrom; + if (from.mixingFrom != null) applyMixingFrom(from, skeleton, alpha); + float mix; if (entry.mixDuration == 0) // Single frame mix to undo mixingFrom changes. mix = 1; else { - mix = alpha * entry.mixTime / entry.mixDuration; + mix = entry.mixTime / entry.mixDuration; if (mix > 1) mix = 1; + mix *= alpha; } - TrackEntry from = entry.mixingFrom; - if (from.mixingFrom != null) applyMixingFrom(from, skeleton, alpha); - Array events = mix < from.eventThreshold ? this.events : null; boolean attachments = mix < from.attachmentThreshold, drawOrder = mix < from.drawOrderThreshold; - float animationLast = from.animationLast, animationTime = from.getAnimationTime(); - Array timelines = from.animation.timelines; - int timelineCount = timelines.size; + int timelineCount = from.animation.timelines.size; + Object[] timelines = from.animation.timelines.items; boolean[] timelinesFirst = from.timelinesFirst.items, timelinesLast = from.timelinesLast.items; float alphaFull = from.alpha, alphaMix = alphaFull * (1 - mix); @@ -236,23 +221,19 @@ public class AnimationState { if (firstFrame) from.timelinesRotation.setSize(timelineCount << 1); float[] timelinesRotation = from.timelinesRotation.items; - log("applyMixingFrom: " + entry.mixingFrom + " -> " + entry + ", mix: " + entry.mixTime / entry.mixDuration); - if (timelineCount == 0) log("apply from: " + from + " " + alphaFull + " * " + entry.alpha); - for (int i = 0; i < timelineCount; i++) { - Timeline timeline = timelines.get(i); + Timeline timeline = (Timeline)timelines[i]; boolean setupPose = timelinesFirst[i]; + // If there's a higher timeline for the property, use full alpha to avoid a dip during the mix. float a = timelinesLast[i] ? alphaMix : alphaFull; - log("apply from: " + from + " " + a + " * " + entry.alpha); - if (timeline instanceof RotateTimeline) { - applyRotateTimeline((RotateTimeline)timeline, skeleton, animationLast, animationTime, events, a, setupPose, setupPose, - timelinesRotation, i << 1, firstFrame); - } else { + if (timeline instanceof RotateTimeline) + applyRotateTimeline(timeline, skeleton, animationTime, a, setupPose, timelinesRotation, i << 1, firstFrame); + else { if (!setupPose) { if (!attachments && timeline instanceof AttachmentTimeline) continue; if (!drawOrder && timeline instanceof DrawOrderTimeline) continue; } - timeline.apply(skeleton, animationLast, animationTime, events, a, setupPose, setupPose); + timeline.apply(skeleton, animationLast, animationTime, events, a, setupPose, true); } } @@ -263,18 +244,18 @@ public class AnimationState { return mix; } - /** @param events May be null. */ - private void applyRotateTimeline (RotateTimeline timeline, Skeleton skeleton, float lastTime, float time, Array events, - float alpha, boolean setupPose, boolean mixingOut, float[] timelinesRotation, int i, boolean firstFrame) { + private void applyRotateTimeline (Timeline timeline, Skeleton skeleton, float time, float alpha, boolean setupPose, + float[] timelinesRotation, int i, boolean firstFrame) { if (alpha == 1) { - timeline.apply(skeleton, lastTime, time, events, 1, setupPose, setupPose); + timeline.apply(skeleton, 0, time, null, 1, setupPose, false); return; } - float[] frames = timeline.frames; + RotateTimeline rotateTimeline = (RotateTimeline)timeline; + float[] frames = rotateTimeline.frames; if (time < frames[0]) return; // Time is before first frame. - Bone bone = skeleton.bones.get(timeline.boneIndex); + Bone bone = skeleton.bones.get(rotateTimeline.boneIndex); float r2; if (time >= frames[frames.length - ENTRIES]) // Time is after last frame. @@ -284,7 +265,7 @@ public class AnimationState { int frame = Animation.binarySearch(frames, time, ENTRIES); float prevRotation = frames[frame + PREV_ROTATION]; float frameTime = frames[frame]; - float percent = timeline.getCurvePercent((frame >> 1) - 1, + float percent = rotateTimeline.getCurvePercent((frame >> 1) - 1, 1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime)); r2 = frames[frame + ROTATION] - prevRotation; @@ -293,7 +274,7 @@ public class AnimationState { r2 -= (16384 - (int)(16384.499999999996 - r2 / 360)) * 360; } - // Mix between two 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 = setupPose ? bone.data.rotation : bone.rotation; float total, diff = r2 - r1; if (diff == 0) { @@ -319,7 +300,7 @@ public class AnimationState { if (Math.abs(lastTotal) > 180) lastTotal += 360 * Math.signum(lastTotal); dir = current; } - total = diff + lastTotal - lastTotal % 360; // Keep loops part of lastTotal. + total = diff + lastTotal - lastTotal % 360; // Store loops as part of lastTotal. if (dir != current) total += 360 * Math.signum(lastTotal); timelinesRotation[i] = total; } @@ -405,19 +386,12 @@ public class AnimationState { if (from != null) { queue.interrupt(from); current.mixingFrom = from; - // entry.mixTime = Math.max(0, entry.mixDuration - current.trackTime); - // log("setCurrent mixTime: " + entry.mixDuration + " - " + current.trackTime + " = " + entry.mixTime); current.mixTime = 0; - from.timelinesRotation.clear(); // BOZO - Needed? Recursive? + from.timelinesRotation.clear(); -// float alpha = 1; - float duration = from.animationEnd - from.animationStart; - if (duration > 0) from.alpha *= (from.getAnimationTime() - from.animationStart) / duration; -// do { -// from.alpha *= alpha; -// from = from.mixingFrom; -// } while (from != null); + // If not completely mixed in, set alpha so mixing out happens from current mix to zero. + if (from.mixingFrom != null) from.alpha *= Math.min(from.mixTime / from.mixDuration, 1); } queue.start(current); From e084f7c745c9b526bebe2b09cb76aec960e7ffb0 Mon Sep 17 00:00:00 2001 From: NathanSweet Date: Wed, 19 Oct 2016 22:43:41 +0200 Subject: [PATCH 2/2] Removed crossfade "dip" fix. It worked by applying the mixing from timeline with full alpha, then mix the higher timeline. It wasn't good since it would blow away any pose from before the mixing from timeline. --- .../spine/AnimationState.java | 39 +++---------------- 1 file changed, 5 insertions(+), 34 deletions(-) diff --git a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/AnimationState.java b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/AnimationState.java index b7486c6af..12cec0d18 100644 --- a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/AnimationState.java +++ b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/AnimationState.java @@ -212,10 +212,10 @@ public class AnimationState { Array events = mix < from.eventThreshold ? this.events : null; boolean attachments = mix < from.attachmentThreshold, drawOrder = mix < from.drawOrderThreshold; float animationLast = from.animationLast, animationTime = from.getAnimationTime(); + alpha = from.alpha * (1 - mix); int timelineCount = from.animation.timelines.size; Object[] timelines = from.animation.timelines.items; - boolean[] timelinesFirst = from.timelinesFirst.items, timelinesLast = from.timelinesLast.items; - float alphaFull = from.alpha, alphaMix = alphaFull * (1 - mix); + boolean[] timelinesFirst = from.timelinesFirst.items; boolean firstFrame = from.timelinesRotation.size == 0; if (firstFrame) from.timelinesRotation.setSize(timelineCount << 1); @@ -224,16 +224,14 @@ public class AnimationState { for (int i = 0; i < timelineCount; i++) { Timeline timeline = (Timeline)timelines[i]; boolean setupPose = timelinesFirst[i]; - // If there's a higher timeline for the property, use full alpha to avoid a dip during the mix. - float a = timelinesLast[i] ? alphaMix : alphaFull; if (timeline instanceof RotateTimeline) - applyRotateTimeline(timeline, skeleton, animationTime, a, setupPose, timelinesRotation, i << 1, firstFrame); + applyRotateTimeline(timeline, skeleton, animationTime, alpha, setupPose, timelinesRotation, i << 1, firstFrame); else { if (!setupPose) { if (!attachments && timeline instanceof AttachmentTimeline) continue; if (!drawOrder && timeline instanceof DrawOrderTimeline) continue; } - timeline.apply(skeleton, animationLast, animationTime, events, a, setupPose, true); + timeline.apply(skeleton, animationLast, animationTime, events, alpha, setupPose, true); } } @@ -566,32 +564,6 @@ public class AnimationState { TrackEntry entry = tracks.get(i); if (entry != null) checkTimelinesFirst(entry); } - - // Compute timelinesLast from highest to lowest track entries that have mixingFrom. - propertyIDs.clear(); - int lowestMixingFrom = n; - for (i = 0; i < n; i++) { // Find lowest with a mixingFrom entry. - TrackEntry entry = tracks.get(i); - if (entry == null) continue; - if (entry.mixingFrom != null) { - lowestMixingFrom = i; - break; - } - } - for (i = n - 1; i >= lowestMixingFrom; i--) { - TrackEntry entry = tracks.get(i); - if (entry == null) continue; - - Array timelines = entry.animation.timelines; - for (int ii = 0, nn = timelines.size; ii < nn; ii++) - propertyIDs.add(timelines.get(ii).getPropertyId()); - - entry = entry.mixingFrom; - while (entry != null) { - checkTimelinesUsage(entry, entry.timelinesLast); - entry = entry.mixingFrom; - } - } } /** From last to first mixingFrom entries, sets timelinesFirst to true on last, calls checkTimelineUsage on rest. */ @@ -701,7 +673,7 @@ public class AnimationState { float animationStart, animationEnd, animationLast, nextAnimationLast; float delay, trackTime, trackLast, nextTrackLast, trackEnd, timeScale; float alpha, mixTime, mixDuration, mixAlpha; - final BooleanArray timelinesFirst = new BooleanArray(), timelinesLast = new BooleanArray(); + final BooleanArray timelinesFirst = new BooleanArray(); final FloatArray timelinesRotation = new FloatArray(); public void reset () { @@ -710,7 +682,6 @@ public class AnimationState { animation = null; listener = null; timelinesFirst.clear(); - timelinesLast.clear(); timelinesRotation.clear(); }