[libgdx] Animation/AnimationState additive improvements.

* Replaced MixBlend and MixDirection with booleans.
* MixBlend.first functionality is no longer needed, cases simplified.
* Timelines know if they support additive or use instant transitions.
* When applying an animation with additive, timelines that don't support additive use the hold system to prevent dipping.
* Additive uses setup pose to prevent accumulation across frames. No longer need to reset additive properties.
* Added TrackEntry#setAdditive(boolean).
* Simplified AnimationState code.
This commit is contained in:
Nathan Sweet 2026-03-21 15:36:53 -04:00
parent 063f163d56
commit 4259e86e19
15 changed files with 625 additions and 946 deletions

View File

@ -31,8 +31,6 @@ package com.esotericsoftware.spine;
import com.badlogic.gdx.files.FileHandle;
import com.esotericsoftware.spine.Animation.MixBlend;
import com.esotericsoftware.spine.Animation.MixDirection;
import com.esotericsoftware.spine.attachments.AttachmentLoader;
import com.esotericsoftware.spine.attachments.BoundingBoxAttachment;
import com.esotericsoftware.spine.attachments.ClippingAttachment;
@ -81,7 +79,7 @@ public class BonePlotting {
for (Animation animation : skeletonData.getAnimations()) {
float time = 0;
while (time < animation.getDuration()) {
animation.apply(skeleton, time, time, false, null, 1, MixBlend.first, MixDirection.in, false);
animation.apply(skeleton, time, time, false, null, 1, true, false, false, false);
skeleton.update(fps);
skeleton.updateWorldTransform(Physics.update);

View File

@ -36,7 +36,6 @@ import com.badlogic.gdx.backends.lwjgl3.Lwjgl3ApplicationConfiguration;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.g2d.TextureAtlas;
import com.badlogic.gdx.graphics.g2d.TextureAtlas.AtlasRegion;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Matrix4;
@ -51,8 +50,6 @@ import com.badlogic.gdx.physics.box2d.World;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.ScreenUtils;
import com.esotericsoftware.spine.Animation.MixBlend;
import com.esotericsoftware.spine.Animation.MixDirection;
import com.esotericsoftware.spine.attachments.AtlasAttachmentLoader;
import com.esotericsoftware.spine.attachments.RegionAttachment;
import com.esotericsoftware.spine.attachments.Sequence;
@ -144,7 +141,7 @@ public class Box2DExample extends ApplicationAdapter {
batch.setTransformMatrix(camera.view);
batch.begin();
animation.apply(skeleton, time, time, true, events, 1, MixBlend.first, MixDirection.in, false);
animation.apply(skeleton, time, time, true, events, 1, true, false, false, false);
skeleton.x += 8 * delta;
skeleton.update(delta);
skeleton.updateWorldTransform(Physics.update);

View File

@ -34,8 +34,6 @@ import java.util.Arrays;
import com.badlogic.gdx.utils.Array;
import com.esotericsoftware.spine.Animation.EventTimeline;
import com.esotericsoftware.spine.Animation.MixBlend;
import com.esotericsoftware.spine.Animation.MixDirection;
/** Unit tests to ensure {@link EventTimeline} is working as expected. */
public class EventTimelineTests {
@ -176,7 +174,7 @@ public class EventTimelineTests {
int beforeCount = firedEvents.size;
Array<Event> original = new Array(firedEvents);
timeline.apply(skeleton, lastTimeLooped, timeLooped, firedEvents, 1, MixBlend.first, MixDirection.in, false);
timeline.apply(skeleton, lastTimeLooped, timeLooped, firedEvents, 1, true, false, false, false);
while (beforeCount < firedEvents.size) {
char fired = firedEvents.get(beforeCount).getData().getName().charAt(0);
@ -185,7 +183,7 @@ public class EventTimelineTests {
} else {
if (firedEvents.size > eventsCount) {
if (print) System.out.println(lastTimeLooped + "->" + timeLooped + ": " + fired + " == ?");
timeline.apply(skeleton, lastTimeLooped, timeLooped, original, 1, MixBlend.first, MixDirection.in, false);
timeline.apply(skeleton, lastTimeLooped, timeLooped, original, 1, true, false, false, false);
fail("Too many events fired.");
}
}
@ -193,7 +191,7 @@ public class EventTimelineTests {
System.out.println(lastTimeLooped + "->" + timeLooped + ": " + fired + " == " + events[eventIndex]);
}
if (fired != events[eventIndex]) {
timeline.apply(skeleton, lastTimeLooped, timeLooped, original, 1, MixBlend.first, MixDirection.in, false);
timeline.apply(skeleton, lastTimeLooped, timeLooped, original, 1, true, false, false, false);
fail("Wrong event fired.");
}
eventIndex++;
@ -205,7 +203,7 @@ public class EventTimelineTests {
i++;
}
if (firedEvents.size < eventsCount) {
timeline.apply(skeleton, lastTimeLooped, timeLooped, firedEvents, 1, MixBlend.first, MixDirection.in, false);
timeline.apply(skeleton, lastTimeLooped, timeLooped, firedEvents, 1, true, false, false, false);
if (print) System.out.println(firedEvents);
fail("Event not fired: " + events[eventIndex] + ", " + frames[eventIndex]);
}

View File

@ -42,8 +42,6 @@ import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.graphics.glutils.FrameBuffer;
import com.badlogic.gdx.utils.ScreenUtils;
import com.esotericsoftware.spine.Animation.MixBlend;
import com.esotericsoftware.spine.Animation.MixDirection;
import com.esotericsoftware.spine.utils.TwoColorPolygonBatch;
/** Demonstrates rendering an animation to a frame buffer (FBO) and then rendering the FBO to the screen. */
@ -80,7 +78,7 @@ public class FboTest extends ApplicationAdapter {
// Apply the pose for the first frame of the run animation.
Animation animation = skeleton.getData().findAnimation("run");
animation.apply(skeleton, -1, 0, true, null, 1, MixBlend.first, MixDirection.in, false);
animation.apply(skeleton, -1, 0, true, null, 1, true, false, false, false);
// Compute the world transform for the pose.
skeleton.updateWorldTransform(Physics.update);

View File

@ -36,6 +36,7 @@ import com.badlogic.gdx.backends.headless.HeadlessApplicationConfiguration;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.TextureAtlas;
import com.esotericsoftware.spine.utils.SkeletonSerializer;
public class HeadlessTest implements ApplicationListener {

View File

@ -56,9 +56,6 @@ import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener;
import com.badlogic.gdx.utils.Align;
import com.badlogic.gdx.utils.ScreenUtils;
import com.esotericsoftware.spine.Animation.MixBlend;
import com.esotericsoftware.spine.Animation.MixDirection;
/** Demonstrates simplistic usage of lighting with normal maps.
* <p>
* Note the normals are not rotated when bones are rotated, making lighting incorrect. */
@ -136,7 +133,7 @@ public class NormalMapTest extends ApplicationAdapter {
float lastTime = time;
float delta = Gdx.graphics.getDeltaTime();
time += delta;
if (animation != null) animation.apply(skeleton, lastTime, time, true, null, 1, MixBlend.first, MixDirection.in, false);
if (animation != null) animation.apply(skeleton, lastTime, time, true, null, 1, true, false, false, false);
skeleton.update(delta);
skeleton.updateWorldTransform(Physics.update);

View File

@ -46,8 +46,6 @@ import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.graphics.glutils.FrameBuffer;
import com.badlogic.gdx.utils.ScreenUtils;
import com.esotericsoftware.spine.Animation.MixBlend;
import com.esotericsoftware.spine.Animation.MixDirection;
import com.esotericsoftware.spine.utils.TwoColorPolygonBatch;
/** Demonstrates rendering an animation to a frame buffer (FBO) and then writing each frame as a PNG. */
@ -103,7 +101,7 @@ public class PngExportTest extends ApplicationAdapter {
float fps = 1 / 15f, time = 0;
int frame = 1;
while (time < animation.getDuration()) {
animation.apply(skeleton, time, time, false, null, 1, MixBlend.first, MixDirection.in, false);
animation.apply(skeleton, time, time, false, null, 1, true, false, false, false);
skeleton.update(fps);
skeleton.updateWorldTransform(Physics.update);

View File

@ -37,9 +37,6 @@ import com.badlogic.gdx.graphics.g2d.TextureAtlas;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.ScreenUtils;
import com.esotericsoftware.spine.Animation.MixBlend;
import com.esotericsoftware.spine.Animation.MixDirection;
/** Demonstrates using the timeline API. See {@link SimpleTest1} for a higher level API using {@link AnimationState}.
* <p>
* See: https://esotericsoftware.com/spine-applying-animations */
@ -108,24 +105,23 @@ public class TimelineApiTest extends ApplicationAdapter {
skeleton.setX(-50);
} else if (time > beforeJump + jump) {
// just walk after jump
walkAnimation.apply(skeleton, time, time, true, events, 1, MixBlend.first, MixDirection.in, false);
walkAnimation.apply(skeleton, time, time, true, events, 1, true, false, false, false);
} else if (time > blendOutStart) {
// blend out jump
walkAnimation.apply(skeleton, time, time, true, events, 1, MixBlend.first, MixDirection.in, false);
walkAnimation.apply(skeleton, time, time, true, events, 1, true, false, false, false);
jumpAnimation.apply(skeleton, time - beforeJump, time - beforeJump, false, events, 1 - (time - blendOutStart) / blendOut,
MixBlend.first, MixDirection.in, false);
true, false, false, false);
} else if (time > beforeJump + blendIn) {
// just jump
jumpAnimation.apply(skeleton, time - beforeJump, time - beforeJump, false, events, 1, MixBlend.first, MixDirection.in,
false);
jumpAnimation.apply(skeleton, time - beforeJump, time - beforeJump, false, events, 1, true, false, false, false);
} else if (time > beforeJump) {
// blend in jump
walkAnimation.apply(skeleton, time, time, true, events, 1, MixBlend.first, MixDirection.in, false);
jumpAnimation.apply(skeleton, time - beforeJump, time - beforeJump, false, events, (time - beforeJump) / blendIn,
MixBlend.first, MixDirection.in, false);
walkAnimation.apply(skeleton, time, time, true, events, 1, true, false, false, false);
jumpAnimation.apply(skeleton, time - beforeJump, time - beforeJump, false, events, (time - beforeJump) / blendIn, true,
false, false, false);
} else {
// just walk before jump
walkAnimation.apply(skeleton, time, time, true, events, 1, MixBlend.first, MixDirection.in, false);
walkAnimation.apply(skeleton, time, time, true, events, 1, true, false, false, false);
}
skeleton.update(delta);

View File

@ -1900,8 +1900,8 @@ public class SkeletonSerializer {
json.writeName("mixDuration");
json.writeValue(obj.getMixDuration());
json.writeName("mixBlend");
json.writeValue(obj.getMixBlend().name());
json.writeName("additive");
json.writeValue(obj.getAdditive());
json.writeName("mixingFrom");
if (obj.getMixingFrom() == null) {

View File

@ -41,9 +41,6 @@ import com.badlogic.gdx.utils.SnapshotArray;
import com.esotericsoftware.spine.Animation.AttachmentTimeline;
import com.esotericsoftware.spine.Animation.DrawOrderFolderTimeline;
import com.esotericsoftware.spine.Animation.DrawOrderTimeline;
import com.esotericsoftware.spine.Animation.EventTimeline;
import com.esotericsoftware.spine.Animation.MixBlend;
import com.esotericsoftware.spine.Animation.MixDirection;
import com.esotericsoftware.spine.Animation.RotateTimeline;
import com.esotericsoftware.spine.Animation.Timeline;
@ -213,16 +210,12 @@ public class AnimationState {
if (current == null || current.delay > 0) continue;
applied = true;
// Track 0 animations aren't for layering, so never use current values before the first key.
MixBlend blend = i == 0 ? MixBlend.first : current.mixBlend;
// Apply mixing from entries first.
float alpha = current.alpha;
if (current.mixingFrom != null)
alpha *= applyMixingFrom(current, skeleton);
else if (current.trackTime >= current.trackEnd && current.next == null) //
alpha = 0; // Set to setup pose the last time the entry will be applied.
boolean attachments = alpha >= current.alphaAttachmentThreshold;
// Apply current entry.
float animationLast = current.animationLast, animationTime = current.getAnimationTime(), applyTime = animationTime;
@ -233,33 +226,31 @@ public class AnimationState {
}
int timelineCount = current.animation.timelines.size;
Timeline[] timelines = current.animation.timelines.items;
if ((i == 0 && alpha == 1) || blend == MixBlend.add) {
if (i == 0) attachments = true;
if (i == 0 && alpha == 1) {
for (int ii = 0; ii < timelineCount; ii++) {
Timeline timeline = timelines[ii];
if (timeline instanceof AttachmentTimeline attachmentTimeline)
applyAttachmentTimeline(attachmentTimeline, skeleton, applyTime, blend, false, attachments);
applyAttachmentTimeline(attachmentTimeline, skeleton, applyTime, true, false, true);
else
timeline.apply(skeleton, animationLast, applyTime, applyEvents, alpha, blend, MixDirection.in, false);
timeline.apply(skeleton, animationLast, applyTime, applyEvents, alpha, true, false, false, false);
}
} else {
int[] timelineMode = current.timelineMode.items;
boolean shortestRotation = current.shortestRotation;
boolean attachments = alpha >= current.alphaAttachmentThreshold;
boolean add = current.additive, shortestRotation = add || current.shortestRotation;
boolean firstFrame = !shortestRotation && current.timelinesRotation.size != timelineCount << 1;
if (firstFrame) current.timelinesRotation.setSize(timelineCount << 1);
float[] timelinesRotation = current.timelinesRotation.items;
float[] timelinesRotation = firstFrame ? current.timelinesRotation.setSize(timelineCount << 1)
: current.timelinesRotation.items;
for (int ii = 0; ii < timelineCount; ii++) {
Timeline timeline = timelines[ii];
MixBlend timelineBlend = timelineMode[ii] == SUBSEQUENT ? current.mixBlend : MixBlend.setup;
boolean fromSetup = timelineMode[ii] == FIRST;
if (!shortestRotation && timeline instanceof RotateTimeline rotateTimeline) {
applyRotateTimeline(rotateTimeline, skeleton, applyTime, alpha, timelineBlend, timelinesRotation, ii << 1,
applyRotateTimeline(rotateTimeline, skeleton, applyTime, alpha, fromSetup, timelinesRotation, ii << 1,
firstFrame);
} else if (timeline instanceof AttachmentTimeline attachmentTimeline)
applyAttachmentTimeline(attachmentTimeline, skeleton, applyTime, blend, false, attachments);
applyAttachmentTimeline(attachmentTimeline, skeleton, applyTime, fromSetup, false, attachments);
else
timeline.apply(skeleton, animationLast, applyTime, applyEvents, alpha, timelineBlend, MixDirection.in, false);
timeline.apply(skeleton, animationLast, applyTime, applyEvents, alpha, fromSetup, add, false, false);
}
}
queueEvents(current, animationTime);
@ -309,62 +300,49 @@ public class AnimationState {
else {
if (mix < from.eventThreshold) events = this.events;
}
MixBlend blend = from.mixBlend;
if (blend == MixBlend.add) {
for (int i = 0; i < timelineCount; i++)
timelines[i].apply(skeleton, animationLast, applyTime, events, alphaMix, blend, MixDirection.out, false);
} else {
int[] timelineMode = from.timelineMode.items;
TrackEntry[] timelineHoldMix = from.timelineHoldMix.items;
boolean shortestRotation = from.shortestRotation;
boolean firstFrame = !shortestRotation && from.timelinesRotation.size != timelineCount << 1;
if (firstFrame) from.timelinesRotation.setSize(timelineCount << 1);
float[] timelinesRotation = from.timelinesRotation.items;
from.totalAlpha = 0;
for (int i = 0; i < timelineCount; i++) {
Timeline timeline = timelines[i];
MixBlend timelineBlend;
float alpha;
switch (timelineMode[i]) {
case SUBSEQUENT -> {
if (!drawOrder && timeline instanceof DrawOrderTimeline) continue;
timelineBlend = blend;
alpha = alphaMix;
}
case FIRST -> {
timelineBlend = MixBlend.setup;
alpha = alphaMix;
}
case HOLD_SUBSEQUENT -> {
timelineBlend = blend;
alpha = alphaHold;
}
case HOLD_FIRST -> {
timelineBlend = MixBlend.setup;
alpha = alphaHold;
}
default -> { // HOLD_MIX
timelineBlend = MixBlend.setup;
TrackEntry holdMix = timelineHoldMix[i];
alpha = alphaHold * Math.max(0, 1 - holdMix.mixTime / holdMix.mixDuration);
}
}
from.totalAlpha += alpha;
if (!shortestRotation && timeline instanceof RotateTimeline rotateTimeline) {
applyRotateTimeline(rotateTimeline, skeleton, applyTime, alpha, timelineBlend, timelinesRotation, i << 1,
firstFrame);
} else if (timeline instanceof AttachmentTimeline attachmentTimeline)
applyAttachmentTimeline(attachmentTimeline, skeleton, applyTime, timelineBlend, true,
attachments && alpha >= from.alphaAttachmentThreshold);
else {
MixDirection direction = MixDirection.out;
if (drawOrder && timeline instanceof DrawOrderTimeline && timelineBlend == MixBlend.setup)
direction = MixDirection.in;
timeline.apply(skeleton, animationLast, applyTime, events, alpha, timelineBlend, direction, false);
}
int[] timelineMode = from.timelineMode.items;
TrackEntry[] timelineHoldMix = from.timelineHoldMix.items;
boolean add = from.additive, shortestRotation = add || from.shortestRotation;
boolean firstFrame = !shortestRotation && from.timelinesRotation.size != timelineCount << 1;
float[] timelinesRotation = firstFrame ? from.timelinesRotation.setSize(timelineCount << 1) : from.timelinesRotation.items;
from.totalAlpha = 0;
for (int i = 0; i < timelineCount; i++) {
Timeline timeline = timelines[i];
boolean fromSetup;
float alpha;
switch (timelineMode[i]) {
case SUBSEQUENT -> {
if (!drawOrder && timeline instanceof DrawOrderTimeline) continue;
fromSetup = false;
alpha = alphaMix;
}
case FIRST -> {
fromSetup = true;
alpha = alphaMix;
}
case HOLD_SUBSEQUENT -> {
fromSetup = false;
alpha = alphaHold;
}
case HOLD_FIRST -> {
fromSetup = true;
alpha = alphaHold;
}
default -> { // HOLD_MIX
fromSetup = true;
TrackEntry holdMix = timelineHoldMix[i];
alpha = alphaHold * Math.max(0, 1 - holdMix.mixTime / holdMix.mixDuration);
}
}
from.totalAlpha += alpha;
if (!shortestRotation && timeline instanceof RotateTimeline rotateTimeline) {
applyRotateTimeline(rotateTimeline, skeleton, applyTime, alpha, fromSetup, timelinesRotation, i << 1, firstFrame);
} else if (timeline instanceof AttachmentTimeline attachmentTimeline)
applyAttachmentTimeline(attachmentTimeline, skeleton, applyTime, fromSetup, true,
attachments && alpha >= from.alphaAttachmentThreshold);
else {
boolean out = !drawOrder || !(timeline instanceof DrawOrderTimeline) || !fromSetup;
timeline.apply(skeleton, animationLast, applyTime, events, alpha, fromSetup, add, out, false);
}
}
@ -380,17 +358,14 @@ public class AnimationState {
* @param attachments False when: 1) the attachment timeline is mixing out, 2) mix < attachmentThreshold, and 3) the timeline
* is not the last timeline to set the slot's attachment. In that case the timeline is applied only so subsequent
* timelines see any deform. */
private void applyAttachmentTimeline (AttachmentTimeline timeline, Skeleton skeleton, float time, MixBlend blend, boolean out,
boolean attachments) {
private void applyAttachmentTimeline (AttachmentTimeline timeline, Skeleton skeleton, float time, boolean fromSetup,
boolean out, boolean attachments) {
Slot slot = skeleton.slots.items[timeline.slotIndex];
if (!slot.bone.active) return;
if (out) {
if (blend == MixBlend.setup) setAttachment(skeleton, slot, slot.data.attachmentName, attachments);
} else if (time < timeline.frames[0]) { // Time is before first frame.
if (blend == MixBlend.setup || blend == MixBlend.first)
setAttachment(skeleton, slot, slot.data.attachmentName, attachments);
if (out || time < timeline.frames[0]) {
if (fromSetup) setAttachment(skeleton, slot, slot.data.attachmentName, attachments);
} else
setAttachment(skeleton, slot, timeline.attachmentNames[Timeline.search(timeline.frames, time)], attachments);
@ -405,13 +380,13 @@ public class AnimationState {
/** Applies the rotate timeline, mixing with the current pose while keeping the same rotation direction chosen as the shortest
* the first time the mixing was applied. */
private void applyRotateTimeline (RotateTimeline timeline, Skeleton skeleton, float time, float alpha, MixBlend blend,
private void applyRotateTimeline (RotateTimeline timeline, Skeleton skeleton, float time, float alpha, boolean fromSetup,
float[] timelinesRotation, int i, boolean firstFrame) {
if (firstFrame) timelinesRotation[i] = 0;
if (alpha == 1) {
timeline.apply(skeleton, 0, time, null, 1, blend, MixDirection.in, false);
timeline.apply(skeleton, 0, time, null, 1, fromSetup, false, false, false);
return;
}
@ -419,22 +394,12 @@ public class AnimationState {
if (!bone.active) return;
BoneLocal pose = bone.pose, setup = bone.data.setup;
float[] frames = timeline.frames;
float r1, r2;
if (time < frames[0]) { // Time is before first frame.
switch (blend) {
case setup:
pose.rotation = setup.rotation;
// Fall through.
default:
return;
case first:
r1 = pose.rotation;
r2 = setup.rotation;
}
} else {
r1 = blend == MixBlend.setup ? setup.rotation : pose.rotation;
r2 = setup.rotation + timeline.getCurveValue(time);
if (fromSetup) pose.rotation = setup.rotation;
return;
}
float r1 = fromSetup ? setup.rotation : pose.rotation;
float r2 = setup.rotation + timeline.getCurveValue(time);
// Mix between rotations using the direction of the shortest route on the first frame.
float total, diff = r2 - r1;
@ -727,6 +692,7 @@ public class AnimationState {
entry.loop = loop;
entry.holdPrevious = false;
entry.additive = false;
entry.reverse = false;
entry.shortestRotation = false;
@ -752,7 +718,6 @@ public class AnimationState {
entry.mixDuration = last == null ? 0 : data.getMix(last.animation, animation);
entry.interruptAlpha = 1;
entry.totalAlpha = 0;
entry.mixBlend = MixBlend.replace;
return entry;
}
@ -770,7 +735,6 @@ public class AnimationState {
animationsChanged = false;
// Process in the order that animations are applied.
propertyIds.clear(2048);
int n = tracks.size;
TrackEntry[] tracks = this.tracks.items;
for (int i = 0; i < n; i++) {
@ -779,44 +743,43 @@ public class AnimationState {
while (entry.mixingFrom != null) // Move to last entry, then iterate in reverse.
entry = entry.mixingFrom;
do {
if (entry.mixingTo == null || entry.mixBlend != MixBlend.add) computeHold(entry);
computeHold(entry);
entry = entry.mixingTo;
} while (entry != null);
}
propertyIds.clear(2048);
}
private void computeHold (TrackEntry entry) {
TrackEntry to = entry.mixingTo;
Timeline[] timelines = entry.animation.timelines.items;
int timelinesCount = entry.animation.timelines.size;
int[] timelineMode = entry.timelineMode.setSize(timelinesCount);
entry.timelineHoldMix.clear();
TrackEntry[] timelineHoldMix = entry.timelineHoldMix.setSize(timelinesCount);
ObjectSet<String> propertyIds = this.propertyIds;
if (to != null && to.holdPrevious) {
for (int i = 0; i < timelinesCount; i++) {
boolean first = propertyIds.addAll(timelines[i].getPropertyIds());
if (first && timelines[i] instanceof DrawOrderFolderTimeline && propertyIds.contains(DrawOrderTimeline.propertyID))
first = false; // DrawOrderTimeline changed.
timelineMode[i] = first ? HOLD_FIRST : HOLD_SUBSEQUENT;
}
return;
boolean holdPrevious = false, add = entry.additive;
TrackEntry to = entry.mixingTo;
if (to != null) {
if (to.additive)
to = null;
else
holdPrevious = to.holdPrevious;
}
outer:
for (int i = 0; i < timelinesCount; i++) {
Timeline timeline = timelines[i];
String[] ids = timeline.getPropertyIds();
if (!propertyIds.addAll(ids))
timelineMode[i] = SUBSEQUENT;
else if (timeline instanceof DrawOrderFolderTimeline && propertyIds.contains(DrawOrderTimeline.propertyID))
timelineMode[i] = SUBSEQUENT; // DrawOrderTimeline changed.
else if (to == null || timeline instanceof AttachmentTimeline || timeline instanceof DrawOrderTimeline
|| timeline instanceof DrawOrderFolderTimeline || timeline instanceof EventTimeline
|| !to.animation.hasTimeline(ids)) {
String[] ids = timeline.propertyIds;
boolean first = propertyIds.addAll(ids)
&& !(timeline instanceof DrawOrderFolderTimeline && propertyIds.contains(DrawOrderTimeline.propertyID));
if (add && timeline.additive)
timelineMode[i] = first ? FIRST : SUBSEQUENT;
else if (!first)
timelineMode[i] = holdPrevious ? HOLD_SUBSEQUENT : SUBSEQUENT;
else if (holdPrevious)
timelineMode[i] = HOLD_FIRST;
else if (to == null || timeline.instant || !to.animation.hasTimeline(ids))
timelineMode[i] = FIRST;
} else {
else {
for (TrackEntry next = to.mixingTo; next != null; next = next.mixingTo) {
if (next.animation.hasTimeline(ids)) continue;
if (next.mixDuration > 0) {
@ -909,12 +872,11 @@ public class AnimationState {
@Null TrackEntry previous, next, mixingFrom, mixingTo;
@Null AnimationStateListener listener;
int trackIndex;
boolean loop, holdPrevious, reverse, shortestRotation;
boolean loop, holdPrevious, additive, reverse, shortestRotation;
float eventThreshold, mixAttachmentThreshold, alphaAttachmentThreshold, mixDrawOrderThreshold;
float animationStart, animationEnd, animationLast, nextAnimationLast;
float delay, trackTime, trackLast, nextTrackLast, trackEnd, timeScale;
float alpha, mixTime, mixDuration, interruptAlpha, totalAlpha;
MixBlend mixBlend = MixBlend.replace;
final IntArray timelineMode = new IntArray();
final Array<TrackEntry> timelineHoldMix = new Array(true, 8, TrackEntry[]::new);
@ -1233,26 +1195,18 @@ public class AnimationState {
* entry is looping, its next loop completion is used instead of its duration. */
public void setMixDuration (float mixDuration, float delay) {
this.mixDuration = mixDuration;
if (delay <= 0) {
if (previous != null)
delay = Math.max(delay + previous.getTrackComplete() - mixDuration, 0);
else
delay = 0;
}
if (delay <= 0) delay = previous == null ? 0 : Math.max(delay + previous.getTrackComplete() - mixDuration, 0);
this.delay = delay;
}
/** Controls how properties keyed in the animation are mixed with lower tracks. Defaults to {@link MixBlend#replace}.
* <p>
* The <code>mixBlend</code> can be set for a new track entry only before {@link AnimationState#apply(Skeleton)} is next
* called. */
public MixBlend getMixBlend () {
return mixBlend;
/** When true, timelines in this animation that support additive are added to the setup or current pose. Additive can be set
* for a new track entry only before {@link AnimationState#apply(Skeleton)} is next called. */
public boolean getAdditive () {
return additive;
}
public void setMixBlend (MixBlend mixBlend) {
if (mixBlend == null) throw new IllegalArgumentException("mixBlend cannot be null.");
this.mixBlend = mixBlend;
public void setAdditive (boolean additive) {
this.additive = additive;
}
/** The track entry for the previous animation when mixing from the previous animation to this animation, or null if no
@ -1302,10 +1256,10 @@ public class AnimationState {
/** 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 #getAlpha()} and starting animations on other tracks.
* <p>
* Mixing with {@link MixBlend#replace} involves finding a rotation between two others, which has two possible solutions:
* the short way or the long way around. The two rotations likely change over time, so which direction is the short or long
* way also changes. If the short way was always chosen, bones would flip to the other side when that direction became the
* long way. TrackEntry chooses the short way the first time it is applied and remembers that direction. */
* Mixing involves finding a rotation between two others, which has two possible solutions: the short way or the long way
* around. The two rotations likely change over time, so which direction is the short or long way also changes. If the short
* way was always chosen, bones would flip to the other side when that direction became the long way. TrackEntry chooses the
* short way the first time it is applied and remembers that direction. */
public void resetRotationDirections () {
timelinesRotation.clear();
}

View File

@ -35,7 +35,7 @@ import com.esotericsoftware.spine.AnimationState.AnimationStateListener;
/** Stores the current pose values for an {@link Event}.
* <p>
* See Timeline
* {@link Timeline#apply(Skeleton, float, float, com.badlogic.gdx.utils.Array, float, com.esotericsoftware.spine.Animation.MixBlend, com.esotericsoftware.spine.Animation.MixDirection, boolean)},
* {@link Timeline#apply(Skeleton, float, float, com.badlogic.gdx.utils.Array, float, boolean, boolean, boolean, boolean)},
* AnimationStateListener {@link AnimationStateListener#event(com.esotericsoftware.spine.AnimationState.TrackEntry, Event)}, and
* <a href="https://esotericsoftware.com/spine-events">Events</a> in the Spine User Guide. */
public class Event {

View File

@ -30,8 +30,6 @@
package com.esotericsoftware.spine;
import com.esotericsoftware.spine.Animation.ConstraintTimeline;
import com.esotericsoftware.spine.Animation.MixBlend;
import com.esotericsoftware.spine.Animation.MixDirection;
import com.esotericsoftware.spine.Animation.PhysicsConstraintTimeline;
import com.esotericsoftware.spine.Animation.SlotTimeline;
import com.esotericsoftware.spine.Animation.Timeline;
@ -78,8 +76,7 @@ public class Slider extends Constraint<Slider, SliderData, SliderPose> {
for (int i = 0, n = animation.bones.size; i < n; i++)
bones[indices[i]].applied.modifyLocal(skeleton);
animation.apply(skeleton, p.time, p.time, data.loop, null, p.mix, data.additive ? MixBlend.add : MixBlend.replace,
MixDirection.in, true);
animation.apply(skeleton, p.time, p.time, data.loop, null, p.mix, false, data.additive, false, true);
}
void sort (Skeleton skeleton) {

View File

@ -50,7 +50,6 @@ import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.Null;
import com.badlogic.gdx.utils.viewport.ScreenViewport;
import com.esotericsoftware.spine.Animation.MixBlend;
import com.esotericsoftware.spine.AnimationState.AnimationStateAdapter;
import com.esotericsoftware.spine.AnimationState.TrackEntry;
import com.esotericsoftware.spine.utils.TwoColorPolygonBatch;
@ -216,7 +215,7 @@ public class SkeletonViewer extends ApplicationAdapter {
entry = state.setAnimation(track, ui.animationList.getSelected(), ui.loopCheckbox.isChecked());
entry.setHoldPrevious(track > 0 && ui.holdPrevCheckbox.isChecked());
}
entry.setMixBlend(track > 0 && ui.addCheckbox.isChecked() ? MixBlend.add : MixBlend.replace);
entry.setAdditive(track > 0 && ui.addCheckbox.isChecked());
entry.setReverse(ui.reverseCheckbox.isChecked());
entry.setAlpha(ui.alphaSlider.getValue());
}

View File

@ -67,7 +67,6 @@ import com.badlogic.gdx.utils.Align;
import com.badlogic.gdx.utils.Null;
import com.badlogic.gdx.utils.viewport.ScreenViewport;
import com.esotericsoftware.spine.Animation.MixBlend;
import com.esotericsoftware.spine.AnimationState.TrackEntry;
import java.awt.FileDialog;
@ -572,7 +571,7 @@ class SkeletonViewerUI {
loopCheckbox.setChecked(current.getLoop());
reverseCheckbox.setChecked(current.getReverse());
if (track > 0) {
addCheckbox.setChecked(current.getMixBlend() == MixBlend.add);
addCheckbox.setChecked(current.getAdditive());
holdPrevCheckbox.setChecked(current.getHoldPrevious());
}
}