From 0ecef9b2c5186f8e4e05af5f211d9b98aa54589e Mon Sep 17 00:00:00 2001 From: NathanSweet Date: Sat, 3 Sep 2016 22:47:05 +0200 Subject: [PATCH] Multiple mixingFrom animations. --- ...tateTest.java => AnimationStateTests.java} | 13 +- .../spine/AnimationState.java | 240 ++++++++++-------- .../spine/SkeletonViewer.java | 11 +- 3 files changed, 142 insertions(+), 122 deletions(-) rename spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/{AnimationStateTest.java => AnimationStateTests.java} (99%) diff --git a/spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/AnimationStateTest.java b/spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/AnimationStateTests.java similarity index 99% rename from spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/AnimationStateTest.java rename to spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/AnimationStateTests.java index 8098e6e75..ca488f373 100644 --- a/spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/AnimationStateTest.java +++ b/spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/AnimationStateTests.java @@ -43,7 +43,7 @@ import com.esotericsoftware.spine.attachments.MeshAttachment; import com.esotericsoftware.spine.attachments.PathAttachment; import com.esotericsoftware.spine.attachments.RegionAttachment; -public class AnimationStateTest { +public class AnimationStateTests { final SkeletonJson json = new SkeletonJson(new AttachmentLoader() { public RegionAttachment newRegionAttachment (Skin skin, String name, String path) { return null; @@ -112,7 +112,7 @@ public class AnimationStateTest { boolean fail; int test; - AnimationStateTest () { + AnimationStateTests () { skeletonData = json.readSkeletonData(new LwjglFileHandle("test/test.json", FileType.Internal)); TrackEntry entry; @@ -645,11 +645,12 @@ public class AnimationStateTest { expect(-1, "start", 0, 0.7f), // expect(-1, "complete", 0.1f, 0.8f), // - expect(-1, "end", 0.1f, 0.9f), // - expect(-1, "dispose", 0.1f, 0.9f), // expect(0, "end", 0.8f, 0.9f), // - expect(0, "dispose", 0.8f, 0.9f) // + expect(0, "dispose", 0.8f, 0.9f), // + + expect(-1, "end", 0.1f, 0.9f), // + expect(-1, "dispose", 0.1f, 0.9f) // ); state.addAnimation(0, "events1", false, 0); run(0.1f, 10, new TestListener() { @@ -779,6 +780,6 @@ public class AnimationStateTest { } static public void main (String[] args) throws Exception { - new AnimationStateTest(); + new AnimationStateTests(); } } 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 38be6fe8d..d96bf29d7 100644 --- a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/AnimationState.java +++ b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/AnimationState.java @@ -95,47 +95,62 @@ public class AnimationState { current.delay = 0; } - TrackEntry next = current.next, mixingFrom = current.mixingFrom; + TrackEntry next = current.next; if (next != null) { - // When the next entry's delay is passed, change to the next entry. + // When the next entry's delay is passed, change to the next entry, preserving leftover time. float nextTime = current.trackLast - next.delay; if (nextTime >= 0) { next.delay = 0; next.trackTime = nextTime + delta * next.timeScale; current.trackTime += currentDelta; setCurrent(i, next); - if (next.mixingFrom != null) next.mixTime += currentDelta; + while (next.mixingFrom != null) { + next.mixTime += currentDelta; + next = next.mixingFrom; + } + continue; + } + updateMixingFrom(current, delta); + } else { + updateMixingFrom(current, delta); + // 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); + queue.end(current); + disposeNext(current); continue; } - } else if (current.trackLast >= current.trackEnd) { - // Clear the track when the track end time is reached and there is no next entry. - tracks.set(i, null); - queue.end(current); - disposeNext(current); - if (mixingFrom != null) queue.end(mixingFrom); - continue; } current.trackTime += currentDelta; - - // Update mixing from entry. - if (mixingFrom != null) { - if (current.mixTime >= current.mixDuration && current.mixTime > 0) { - current.mixingFrom = null; - queue.end(mixingFrom); - } else { - mixingFrom.animationLast = mixingFrom.nextAnimationLast; - mixingFrom.trackLast = mixingFrom.nextTrackLast; - float mixingFromDelta = delta * mixingFrom.timeScale; - mixingFrom.trackTime += mixingFromDelta; - current.mixTime += mixingFromDelta; - } - } } queue.drain(); } + private void updateMixingFrom (TrackEntry entry, float delta) { + TrackEntry from = entry.mixingFrom; + if (from == null) return; + + if (entry.mixTime >= entry.mixDuration && entry.mixTime > 0) { + queue.end(from); + TrackEntry newFrom = from.mixingFrom; + entry.mixingFrom = newFrom; + if (newFrom == null) return; + entry.mixTime = from.mixTime; + entry.mixDuration = from.mixDuration; + from = newFrom; + } + + from.animationLast = from.nextAnimationLast; + from.trackLast = from.nextTrackLast; + float mixingFromDelta = delta * from.timeScale; + from.trackTime += mixingFromDelta; + entry.mixTime += mixingFromDelta; + + updateMixingFrom(from, delta); + } + /** Poses the skeleton using the track entry animations. There are no side effects other than invoking listeners, so the * animation state can be applied to multiple skeletons to pose them identically. */ public void apply (Skeleton skeleton) { @@ -146,21 +161,11 @@ public class AnimationState { for (int i = 0; i < tracks.size; i++) { TrackEntry current = tracks.get(i); - if (current == null) continue; - if (current.delay > 0) continue; + if (current == null || current.delay > 0) continue; + // Apply mixing from entries first. float mix = current.alpha; - - // Apply mixing from entry first. - if (current.mixingFrom != null) { - if (current.mixDuration == 0) - mix = 1; - else { - mix *= current.mixTime / current.mixDuration; - if (mix > 1) mix = 1; - } - applyMixingFrom(current.mixingFrom, skeleton, mix); - } + if (current.mixingFrom != null) mix = applyMixingFrom(current, skeleton, mix); // Apply current entry. float animationLast = current.animationLast, animationTime = current.getAnimationTime(); @@ -189,46 +194,68 @@ public class AnimationState { } queue.drain(); + + System.out.println(); } - private void applyMixingFrom (TrackEntry entry, Skeleton skeleton, float mix) { - Array events = mix < entry.eventThreshold ? this.events : null; - boolean attachments = mix < entry.attachmentThreshold, drawOrder = mix < entry.drawOrderThreshold; + private float applyMixingFrom (TrackEntry entry, Skeleton skeleton, float alpha) { + float mix; + if (entry.mixDuration == 0) // Single frame mix to undo mixingFrom changes. + mix = 1; + else { + mix = alpha * entry.mixTime / entry.mixDuration; + if (mix > 1) mix = 1; + } - float animationLast = entry.animationLast, animationTime = entry.getAnimationTime(); - Array timelines = entry.animation.timelines; + 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; - boolean[] timelinesFirst = entry.timelinesFirst.items, timelinesLast = entry.timelinesLast.items; - float alphaFull = entry.alpha, alphaMix = alphaFull * (1 - mix); + boolean[] timelinesFirst = from.timelinesFirst.items, timelinesLast = from.timelinesLast.items; + float alphaFull = from.alpha, alphaMix = alphaFull * (1 - mix); - boolean firstFrame = entry.timelinesRotation.size == 0; - if (firstFrame) entry.timelinesRotation.setSize(timelineCount << 1); - float[] timelinesRotation = entry.timelinesRotation.items; + boolean firstFrame = from.timelinesRotation.size == 0; + if (firstFrame) from.timelinesRotation.setSize(timelineCount << 1); + float[] timelinesRotation = from.timelinesRotation.items; + + System.out.println(entry.mixingFrom + " -> " + entry + ": " + entry.mixTime / entry.mixDuration); for (int i = 0; i < timelineCount; i++) { Timeline timeline = timelines.get(i); boolean setupPose = timelinesFirst[i]; - float alpha = timelinesLast[i] ? alphaMix : alphaFull; - if (timeline instanceof RotateTimeline && alpha < 1) { - applyRotateTimeline((RotateTimeline)timeline, skeleton, animationLast, animationTime, events, alpha, setupPose, - setupPose, timelinesRotation, i << 1, firstFrame); + float a = timelinesLast[i] ? alphaMix : alphaFull; + if (timeline instanceof RotateTimeline) { + applyRotateTimeline((RotateTimeline)timeline, skeleton, animationLast, animationTime, events, a, setupPose, 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, alpha, setupPose, setupPose); + timeline.apply(skeleton, animationLast, animationTime, events, a, setupPose, setupPose); } } - queueEvents(entry, animationTime); - entry.nextAnimationLast = animationTime; - entry.nextTrackLast = entry.trackTime; + queueEvents(from, animationTime); + from.nextAnimationLast = animationTime; + from.nextTrackLast = from.trackTime; + + 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) { + if (alpha == 1) { + timeline.apply(skeleton, lastTime, time, events, 1, setupPose, setupPose); + return; + } + float[] frames = timeline.frames; if (time < frames[0]) return; // Time is before first frame. @@ -342,10 +369,13 @@ public class AnimationState { disposeNext(current); - TrackEntry mixingFrom = current.mixingFrom; - if (mixingFrom != null) { - current.mixingFrom = null; - queue.end(mixingFrom); + TrackEntry entry = current; + while (true) { + TrackEntry from = entry.mixingFrom; + if (from == null) break; + queue.end(from); + entry.mixingFrom = null; + entry = from; } tracks.set(current.trackIndex, null); @@ -358,21 +388,10 @@ public class AnimationState { tracks.set(index, entry); if (current != null) { - TrackEntry mixingFrom = current.mixingFrom; - current.mixingFrom = null; - queue.interrupt(current); - - // If a mix is in progress, mix from the closest animation. - if (mixingFrom != null && mixingFrom.animation != emptyAnimation - && (current.mixDuration == 0 || current.mixTime / current.mixDuration < 0.5f)) { - entry.mixingFrom = mixingFrom; - mixingFrom = current; - } else - entry.mixingFrom = current; - entry.mixingFrom.timelinesRotation.clear(); - - if (mixingFrom != null) queue.end(mixingFrom); + entry.mixingFrom = current; + entry.mixTime = Math.max(0, entry.mixDuration - current.trackTime); + current.timelinesRotation.clear(); // BOZO - Needed? Recursive? } queue.start(entry); @@ -531,31 +550,27 @@ public class AnimationState { private void animationsChanged () { animationsChanged = false; - // Compute timelinesFirst. + IntSet propertyIDs = this.propertyIDs; + + // Compute timelinesFirst from lowest to highest track entries. int i = 0, n = tracks.size; propertyIDs.clear(); - for (; i < n; i++) { + for (; i < n; i++) { // Find first non-null entry. TrackEntry entry = tracks.get(i); if (entry == null) continue; - if (entry.mixingFrom != null) { - setTimelineUsage(entry.mixingFrom, entry.mixingFrom.timelinesFirst); - checkTimelineUsage(entry, entry.timelinesFirst); - } else - setTimelineUsage(entry, entry.timelinesFirst); + setTimelinesFirst(entry); i++; break; } - for (; i < n; i++) { + for (; i < n; i++) { // Rest of entries. TrackEntry entry = tracks.get(i); - if (entry == null) continue; - if (entry.mixingFrom != null) checkTimelineUsage(entry.mixingFrom, entry.mixingFrom.timelinesFirst); - checkTimelineUsage(entry, entry.timelinesFirst); + if (entry != null) checkTimelinesFirst(entry); } - // Compute timelinesLast from highest to lowest track that has mixingFrom. + // Compute timelinesLast from highest to lowest track entries that have mixingFrom. propertyIDs.clear(); int lowestMixingFrom = n; - for (i = 0; i < n; i++) { + 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) { @@ -566,34 +581,43 @@ public class AnimationState { for (i = n - 1; i >= lowestMixingFrom; i--) { TrackEntry entry = tracks.get(i); if (entry == null) continue; - if (entry.mixingFrom != null) { - addTimelineUsage(entry); - checkTimelineUsage(entry.mixingFrom, entry.mixingFrom.timelinesLast); - } else - addTimelineUsage(entry); - i--; - break; - } - for (; i >= lowestMixingFrom; i--) { - TrackEntry entry = tracks.get(i); - if (entry == null) continue; - addTimelineUsage(entry); - if (entry.mixingFrom != null) checkTimelineUsage(entry.mixingFrom, entry.mixingFrom.timelinesLast); + + 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; + } } } - private void setTimelineUsage (TrackEntry entry, BooleanArray usageArray) { + /** From last to first mixingFrom entries, sets timelinesFirst to true on last, calls checkTimelineUsage on rest. */ + private void setTimelinesFirst (TrackEntry entry) { + if (entry.mixingFrom != null) { + setTimelinesFirst(entry.mixingFrom); + checkTimelinesUsage(entry, entry.timelinesFirst); + return; + } IntSet propertyIDs = this.propertyIDs; Array timelines = entry.animation.timelines; int n = timelines.size; - boolean[] usage = usageArray.setSize(n); + boolean[] usage = entry.timelinesFirst.setSize(n); for (int i = 0; i < n; i++) { propertyIDs.add(timelines.get(i).getPropertyId()); usage[i] = true; } } - private void checkTimelineUsage (TrackEntry entry, BooleanArray usageArray) { + /** From last to first mixingFrom entries, calls checkTimelineUsage. */ + private void checkTimelinesFirst (TrackEntry entry) { + if (entry.mixingFrom != null) checkTimelinesFirst(entry.mixingFrom); + checkTimelinesUsage(entry, entry.timelinesFirst); + } + + private void checkTimelinesUsage (TrackEntry entry, BooleanArray usageArray) { IntSet propertyIDs = this.propertyIDs; Array timelines = entry.animation.timelines; int n = timelines.size; @@ -602,13 +626,6 @@ public class AnimationState { usage[i] = propertyIDs.add(timelines.get(i).getPropertyId()); } - private void addTimelineUsage (TrackEntry entry) { - IntSet propertyIDs = this.propertyIDs; - Array timelines = entry.animation.timelines; - for (int i = 0, n = timelines.size; i < n; i++) - propertyIDs.add(timelines.get(i).getPropertyId()); - } - /** Returns the track entry for the animation currently playing on the track, or null. */ public TrackEntry getCurrent (int trackIndex) { if (trackIndex >= tracks.size) return null; @@ -740,8 +757,9 @@ public class AnimationState { } /** The track time in seconds when this animation will be removed from the track. Defaults to the animation duration for - * non-looping animations and to {@link Integer#MAX_VALUE} for looping animations. If the track end time is reached and no - * other animations are queued for playback then the track is cleared, leaving skeletons in their last pose. + * non-looping animations and to {@link Integer#MAX_VALUE} for looping animations. If the track end time is reached, no + * other animations are queued for playback, and mixing from any previous animations is complete, then the track is cleared, + * leaving skeletons in their last pose. *

* It may be desired to use {@link AnimationState#addEmptyAnimation(int, float, float)} to mix the skeletons back to the * setup pose, rather than leaving them in their last pose. */ diff --git a/spine-libgdx/spine-skeletonviewer/src/com/esotericsoftware/spine/SkeletonViewer.java b/spine-libgdx/spine-skeletonviewer/src/com/esotericsoftware/spine/SkeletonViewer.java index 4eac15514..181a26685 100644 --- a/spine-libgdx/spine-skeletonviewer/src/com/esotericsoftware/spine/SkeletonViewer.java +++ b/spine-libgdx/spine-skeletonviewer/src/com/esotericsoftware/spine/SkeletonViewer.java @@ -329,9 +329,9 @@ public class SkeletonViewer extends ApplicationAdapter { List skinList = new List(skin); CheckBox loopCheckbox = new CheckBox("Loop", skin); CheckBox premultipliedCheckbox = new CheckBox("Premultiplied", skin); - Slider mixSlider = new Slider(0f, 2, 0.01f, false, skin); + Slider mixSlider = new Slider(0, 4, 0.01f, false, skin); Label mixLabel = new Label("0.3", skin); - Slider speedSlider = new Slider(0.1f, 3, 0.01f, false, skin); + Slider speedSlider = new Slider(0, 3, 0.01f, false, skin); Label speedLabel = new Label("1.0", skin); CheckBox flipXCheckbox = new CheckBox("X", skin); CheckBox flipYCheckbox = new CheckBox("Y", skin); @@ -359,12 +359,13 @@ public class SkeletonViewer extends ApplicationAdapter { loopCheckbox.setChecked(true); scaleSlider.setValue(1); - scaleSlider.setSnapToValues(new float[] {1}, 0.1f); + scaleSlider.setSnapToValues(new float[] {1, 1.5f, 2, 2.5f, 3, 3.5f}, 0.01f); mixSlider.setValue(0.3f); + mixSlider.setSnapToValues(new float[] {1, 1.5f, 2, 2.5f, 3, 3.5f}, 0.1f); speedSlider.setValue(1); - speedSlider.setSnapToValues(new float[] {1}, 0.1f); + speedSlider.setSnapToValues(new float[] {0.5f, 0.75f, 1, 1.25f, 1.5f, 2, 2.5f}, 0.1f); window.setMovable(false); window.setResizable(false); @@ -621,7 +622,7 @@ public class SkeletonViewer extends ApplicationAdapter { prefs.putInteger("x", skeletonX); prefs.putInteger("y", skeletonY); if (animationList.getSelected() != null) prefs.putString("animationName", animationList.getSelected()); - if (skinList.getSelected() != null)prefs.putString("skinName", skinList.getSelected()); + if (skinList.getSelected() != null) prefs.putString("skinName", skinList.getSelected()); prefs.flush(); }