mirror of
https://github.com/EsotericSoftware/spine-runtimes.git
synced 2025-12-21 01:36:02 +08:00
Improved AnimationState deform mixing while attachment timelines mix out.
See EsotericSoftware/spine-editor#545 for details. fixes #1292 fixes #1352 fixes EsotericSoftware/spine-editor#545
This commit is contained in:
parent
70e7519813
commit
dd1b3aaa50
@ -170,7 +170,7 @@ public class Animation {
|
|||||||
* apply animations on top of each other (layering).
|
* apply animations on top of each other (layering).
|
||||||
* @param blend Controls how mixing is applied when <code>alpha</code> < 1.
|
* @param blend Controls how mixing is applied when <code>alpha</code> < 1.
|
||||||
* @param direction Indicates whether the timeline is mixing in or out. Used by timelines which perform instant transitions,
|
* @param direction Indicates whether the timeline is mixing in or out. Used by timelines which perform instant transitions,
|
||||||
* such as {@link DrawOrderTimeline} or {@link AttachmentTimeline}. */
|
* such as {@link DrawOrderTimeline} or {@link AttachmentTimeline}, and other such as {@link ScaleTimeline}. */
|
||||||
public void apply (Skeleton skeleton, float lastTime, float time, Array<Event> events, float alpha, MixBlend blend,
|
public void apply (Skeleton skeleton, float lastTime, float time, Array<Event> events, float alpha, MixBlend blend,
|
||||||
MixDirection direction);
|
MixDirection direction);
|
||||||
|
|
||||||
@ -944,18 +944,14 @@ public class Animation {
|
|||||||
|
|
||||||
Slot slot = skeleton.slots.get(slotIndex);
|
Slot slot = skeleton.slots.get(slotIndex);
|
||||||
if (!slot.bone.active) return;
|
if (!slot.bone.active) return;
|
||||||
if (direction == out && blend == setup) {
|
if (direction == out) {
|
||||||
String attachmentName = slot.data.attachmentName;
|
if (blend == setup) setAttachment(skeleton, slot, slot.data.attachmentName);
|
||||||
slot.setAttachment(attachmentName == null ? null : skeleton.getAttachment(slotIndex, attachmentName));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
float[] frames = this.frames;
|
float[] frames = this.frames;
|
||||||
if (time < frames[0]) { // Time is before first frame.
|
if (time < frames[0]) { // Time is before first frame.
|
||||||
if (blend == setup || blend == first) {
|
if (blend == setup || blend == first) setAttachment(skeleton, slot, slot.data.attachmentName);
|
||||||
String attachmentName = slot.data.attachmentName;
|
|
||||||
slot.setAttachment(attachmentName == null ? null : skeleton.getAttachment(slotIndex, attachmentName));
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -965,7 +961,10 @@ public class Animation {
|
|||||||
else
|
else
|
||||||
frameIndex = binarySearch(frames, time) - 1;
|
frameIndex = binarySearch(frames, time) - 1;
|
||||||
|
|
||||||
String attachmentName = attachmentNames[frameIndex];
|
setAttachment(skeleton, slot, attachmentNames[frameIndex]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setAttachment (Skeleton skeleton, Slot slot, String attachmentName) {
|
||||||
slot.setAttachment(attachmentName == null ? null : skeleton.getAttachment(slotIndex, attachmentName));
|
slot.setAttachment(attachmentName == null ? null : skeleton.getAttachment(slotIndex, attachmentName));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1318,8 +1317,8 @@ public class Animation {
|
|||||||
|
|
||||||
Array<Slot> drawOrder = skeleton.drawOrder;
|
Array<Slot> drawOrder = skeleton.drawOrder;
|
||||||
Array<Slot> slots = skeleton.slots;
|
Array<Slot> slots = skeleton.slots;
|
||||||
if (direction == out && blend == setup) {
|
if (direction == out) {
|
||||||
arraycopy(slots.items, 0, drawOrder.items, 0, slots.size);
|
if (blend == setup) arraycopy(slots.items, 0, drawOrder.items, 0, slots.size);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -77,12 +77,12 @@ public class AnimationState {
|
|||||||
* (which affects B and C). Without using D to mix out, A would be applied fully until mixing completes, then snap into
|
* (which affects B and C). Without using D to mix out, A would be applied fully until mixing completes, then snap into
|
||||||
* place. */
|
* place. */
|
||||||
static private final int HOLD_MIX = 3;
|
static private final int HOLD_MIX = 3;
|
||||||
/** 1) An attachment timeline in a subsequent track entry sets the attachment for the same slot as this attachment
|
/** 1) This is the last attachment timeline to set the attachment for a slot.<br>
|
||||||
* timeline.<br>
|
* Result: Do not apply this timeline when mixing out. Attachment timelines that are not last are applied when mixing out so
|
||||||
* Result: This attachment timeline will not use MixDirection.out, which would otherwise show the setup mode attachment (or
|
* any deform timelines are applied and subsequent entries can mix from that deform. */
|
||||||
* none if not visible in setup mode). This allows deform timelines to be applied for the subsequent entry to mix from, rather
|
static private final int LAST = 4;
|
||||||
* than mixing from the setup pose. */
|
|
||||||
static private final int NOT_LAST = 4;
|
static private final int SETUP = 1, CURRENT = 2;
|
||||||
|
|
||||||
private AnimationStateData data;
|
private AnimationStateData data;
|
||||||
final Array<TrackEntry> tracks = new Array();
|
final Array<TrackEntry> tracks = new Array();
|
||||||
@ -92,6 +92,7 @@ public class AnimationState {
|
|||||||
private final IntSet propertyIDs = new IntSet();
|
private final IntSet propertyIDs = new IntSet();
|
||||||
boolean animationsChanged;
|
boolean animationsChanged;
|
||||||
private float timeScale = 1;
|
private float timeScale = 1;
|
||||||
|
private int unkeyedState;
|
||||||
|
|
||||||
final Pool<TrackEntry> trackEntryPool = new Pool() {
|
final Pool<TrackEntry> trackEntryPool = new Pool() {
|
||||||
protected Object newObject () {
|
protected Object newObject () {
|
||||||
@ -193,8 +194,8 @@ public class AnimationState {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Poses the skeleton using the track entry animations. There are no side effects other than invoking listeners, so the
|
/** Poses the skeleton using the track entry animations. The animation state is not changed, so can be applied to multiple
|
||||||
* animation state can be applied to multiple skeletons to pose them identically.
|
* skeletons to pose them identically.
|
||||||
* @return True if any animations were applied. */
|
* @return True if any animations were applied. */
|
||||||
public boolean apply (Skeleton skeleton) {
|
public boolean apply (Skeleton skeleton) {
|
||||||
if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
|
if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
|
||||||
@ -222,8 +223,13 @@ public class AnimationState {
|
|||||||
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 ((i == 0 && 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);
|
Object timeline = timelines[ii];
|
||||||
|
if (timeline instanceof AttachmentTimeline)
|
||||||
|
applyAttachmentTimeline((AttachmentTimeline)timeline, skeleton, animationTime, blend, true);
|
||||||
|
else
|
||||||
|
((Timeline)timeline).apply(skeleton, animationLast, animationTime, events, mix, blend, MixDirection.in);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
int[] timelineMode = current.timelineMode.items;
|
int[] timelineMode = current.timelineMode.items;
|
||||||
|
|
||||||
@ -233,11 +239,13 @@ 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 = (timelineMode[ii] & NOT_LAST - 1) == SUBSEQUENT ? blend : MixBlend.setup;
|
MixBlend timelineBlend = (timelineMode[ii] & LAST - 1) == SUBSEQUENT ? blend : MixBlend.setup;
|
||||||
if (timeline instanceof RotateTimeline) {
|
if (timeline instanceof RotateTimeline) {
|
||||||
applyRotateTimeline((RotateTimeline)timeline, skeleton, animationTime, mix, timelineBlend, timelinesRotation,
|
applyRotateTimeline((RotateTimeline)timeline, skeleton, animationTime, mix, timelineBlend, timelinesRotation,
|
||||||
ii << 1, firstFrame);
|
ii << 1, firstFrame);
|
||||||
} else
|
} else if (timeline instanceof AttachmentTimeline)
|
||||||
|
applyAttachmentTimeline((AttachmentTimeline)timeline, skeleton, animationTime, blend, true);
|
||||||
|
else
|
||||||
timeline.apply(skeleton, animationLast, animationTime, events, mix, timelineBlend, MixDirection.in);
|
timeline.apply(skeleton, animationLast, animationTime, events, mix, timelineBlend, MixDirection.in);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -247,6 +255,20 @@ public class AnimationState {
|
|||||||
current.nextTrackLast = current.trackTime;
|
current.nextTrackLast = current.trackTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set slots attachments to the setup pose, if needed. This occurs if an animation that is mixing out sets attachments so
|
||||||
|
// subsequent timelines see any deform, but the subsequent timelines don't set an attachment (they are also mixing out or
|
||||||
|
// the time is before the first key).
|
||||||
|
int setupState = unkeyedState + SETUP;
|
||||||
|
Object[] slots = skeleton.slots.items;
|
||||||
|
for (int i = 0, n = skeleton.slots.size; i < n; i++) {
|
||||||
|
Slot slot = (Slot)slots[i];
|
||||||
|
if (slot.attachmentState == setupState) {
|
||||||
|
String attachmentName = slot.data.attachmentName;
|
||||||
|
slot.setAttachment(attachmentName == null ? null : skeleton.getAttachment(slot.data.index, attachmentName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unkeyedState += 2;
|
||||||
|
|
||||||
queue.drain();
|
queue.drain();
|
||||||
return applied;
|
return applied;
|
||||||
}
|
}
|
||||||
@ -289,14 +311,10 @@ public class AnimationState {
|
|||||||
MixDirection direction = MixDirection.out;
|
MixDirection direction = MixDirection.out;
|
||||||
MixBlend timelineBlend;
|
MixBlend timelineBlend;
|
||||||
float alpha;
|
float alpha;
|
||||||
switch (timelineMode[i] & NOT_LAST - 1) {
|
switch (timelineMode[i] & LAST - 1) {
|
||||||
case SUBSEQUENT:
|
case SUBSEQUENT:
|
||||||
timelineBlend = blend;
|
|
||||||
if (!attachments && timeline instanceof AttachmentTimeline) {
|
|
||||||
if ((timelineMode[i] & NOT_LAST) == NOT_LAST) continue;
|
|
||||||
timelineBlend = MixBlend.setup;
|
|
||||||
}
|
|
||||||
if (!drawOrder && timeline instanceof DrawOrderTimeline) continue;
|
if (!drawOrder && timeline instanceof DrawOrderTimeline) continue;
|
||||||
|
timelineBlend = blend;
|
||||||
alpha = alphaMix;
|
alpha = alphaMix;
|
||||||
break;
|
break;
|
||||||
case FIRST:
|
case FIRST:
|
||||||
@ -317,14 +335,14 @@ public class AnimationState {
|
|||||||
if (timeline instanceof RotateTimeline) {
|
if (timeline instanceof RotateTimeline) {
|
||||||
applyRotateTimeline((RotateTimeline)timeline, skeleton, animationTime, alpha, timelineBlend, timelinesRotation,
|
applyRotateTimeline((RotateTimeline)timeline, skeleton, animationTime, alpha, timelineBlend, timelinesRotation,
|
||||||
i << 1, firstFrame);
|
i << 1, firstFrame);
|
||||||
|
} else if (timeline instanceof AttachmentTimeline) {
|
||||||
|
// If not showing attachments: do nothing if this is the last timeline, else apply the timeline so
|
||||||
|
// subsequent timelines see any deform, but don't set attachmentState to CURRENT.
|
||||||
|
if (!attachments && (timelineMode[i] & LAST) != 0) continue;
|
||||||
|
applyAttachmentTimeline((AttachmentTimeline)timeline, skeleton, animationTime, timelineBlend, attachments);
|
||||||
} else {
|
} else {
|
||||||
if (timelineBlend == MixBlend.setup) {
|
if (drawOrder && timeline instanceof DrawOrderTimeline && timelineBlend == MixBlend.setup)
|
||||||
if (timeline instanceof AttachmentTimeline) {
|
direction = MixDirection.in;
|
||||||
if (attachments || (timelineMode[i] & NOT_LAST) == NOT_LAST) direction = MixDirection.in;
|
|
||||||
} else if (timeline instanceof DrawOrderTimeline) {
|
|
||||||
if (drawOrder) direction = MixDirection.in;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
timeline.apply(skeleton, animationLast, animationTime, events, alpha, timelineBlend, direction);
|
timeline.apply(skeleton, animationLast, animationTime, events, alpha, timelineBlend, direction);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -338,6 +356,40 @@ public class AnimationState {
|
|||||||
return mix;
|
return mix;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Applies the attachment timeline and sets {@link Slot#attachmentState}.
|
||||||
|
* @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 attachments) {
|
||||||
|
|
||||||
|
Slot slot = skeleton.slots.get(timeline.slotIndex);
|
||||||
|
if (!slot.bone.active) return;
|
||||||
|
|
||||||
|
float[] frames = timeline.frames;
|
||||||
|
if (time < frames[0]) { // Time is before first frame.
|
||||||
|
if (blend == MixBlend.setup || blend == MixBlend.first)
|
||||||
|
setAttachment(skeleton, slot, slot.data.attachmentName, attachments);
|
||||||
|
} else {
|
||||||
|
int frameIndex;
|
||||||
|
if (time >= frames[frames.length - 1]) // Time is after last frame.
|
||||||
|
frameIndex = frames.length - 1;
|
||||||
|
else
|
||||||
|
frameIndex = Animation.binarySearch(frames, time) - 1;
|
||||||
|
setAttachment(skeleton, slot, timeline.attachmentNames[frameIndex], attachments);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If an attachment wasn't set (ie before the first frame or attachments is false), set the setup attachment later.
|
||||||
|
if (slot.attachmentState <= unkeyedState) slot.attachmentState = unkeyedState + SETUP;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setAttachment (Skeleton skeleton, Slot slot, String attachmentName, boolean attachments) {
|
||||||
|
slot.setAttachment(attachmentName == null ? null : skeleton.getAttachment(slot.data.index, attachmentName));
|
||||||
|
if (attachments) slot.attachmentState = unkeyedState + CURRENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 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, MixBlend blend,
|
||||||
float[] timelinesRotation, int i, boolean firstFrame) {
|
float[] timelinesRotation, int i, boolean firstFrame) {
|
||||||
|
|
||||||
@ -772,7 +824,7 @@ public class AnimationState {
|
|||||||
for (int i = 0; i < timelinesCount; i++) {
|
for (int i = 0; i < timelinesCount; i++) {
|
||||||
if (timelines[i] instanceof AttachmentTimeline) {
|
if (timelines[i] instanceof AttachmentTimeline) {
|
||||||
AttachmentTimeline timeline = (AttachmentTimeline)timelines[i];
|
AttachmentTimeline timeline = (AttachmentTimeline)timelines[i];
|
||||||
if (!propertyIDs.add(timeline.slotIndex)) timelineMode[i] |= NOT_LAST;
|
if (propertyIDs.add(timeline.slotIndex)) timelineMode[i] |= LAST;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -47,6 +47,8 @@ public class Slot {
|
|||||||
private float attachmentTime;
|
private float attachmentTime;
|
||||||
private FloatArray deform = new FloatArray();
|
private FloatArray deform = new FloatArray();
|
||||||
|
|
||||||
|
int attachmentState;
|
||||||
|
|
||||||
public Slot (SlotData data, Bone bone) {
|
public Slot (SlotData data, Bone bone) {
|
||||||
if (data == null) throw new IllegalArgumentException("data cannot be null.");
|
if (data == null) throw new IllegalArgumentException("data cannot be null.");
|
||||||
if (bone == null) throw new IllegalArgumentException("bone cannot be null.");
|
if (bone == null) throw new IllegalArgumentException("bone cannot be null.");
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user