[libgdx] Separate pose and applied for bones, slots, and constraints, sliders WIP.

This commit is contained in:
Nathan Sweet 2025-04-12 20:14:28 -04:00
parent 9424018102
commit 71ef2d5f98
28 changed files with 1044 additions and 719 deletions

View File

@ -76,14 +76,14 @@ public class BonePlotting {
SkeletonData skeletonData = json.readSkeletonData(new FileHandle("assets/spineboy/spineboy-ess.json"));
Skeleton skeleton = new Skeleton(skeletonData);
Bone bone = skeleton.findBone("gun-tip");
BoneApplied bone = skeleton.findBone("gun-tip").getApplied();
// Pose the skeleton at regular intervals throughout each animation.
float fps = 1 / 15f;
for (Animation animation : skeletonData.getAnimations()) {
float time = 0;
while (time < animation.getDuration()) {
animation.apply(skeleton, time, time, false, null, 1, MixBlend.first, MixDirection.in);
animation.apply(skeleton, time, time, false, null, 1, MixBlend.first, MixDirection.in, false);
skeleton.update(fps);
skeleton.updateWorldTransform(Physics.update);

View File

@ -149,7 +149,7 @@ public class Box2DExample extends ApplicationAdapter {
batch.setTransformMatrix(camera.view);
batch.begin();
animation.apply(skeleton, time, time, true, events, 1, MixBlend.first, MixDirection.in);
animation.apply(skeleton, time, time, true, events, 1, MixBlend.first, MixDirection.in, false);
skeleton.x += 8 * delta;
skeleton.update(delta);
skeleton.updateWorldTransform(Physics.update);
@ -162,9 +162,10 @@ public class Box2DExample extends ApplicationAdapter {
if (!(slot.getAttachment() instanceof Box2dAttachment)) continue;
Box2dAttachment attachment = (Box2dAttachment)slot.getAttachment();
if (attachment.body == null) continue;
float x = slot.getBone().getWorldX();
float y = slot.getBone().getWorldY();
float rotation = slot.getBone().getWorldRotationX();
BoneApplied bone = slot.getBone().getApplied();
float x = bone.getWorldX();
float y = bone.getWorldY();
float rotation = bone.getWorldRotationX();
attachment.body.setTransform(x, y, rotation * MathUtils.degRad);
}

View File

@ -177,7 +177,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);
timeline.apply(skeleton, lastTimeLooped, timeLooped, firedEvents, 1, MixBlend.first, MixDirection.in, false);
while (beforeCount < firedEvents.size) {
char fired = firedEvents.get(beforeCount).getData().getName().charAt(0);
@ -186,7 +186,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);
timeline.apply(skeleton, lastTimeLooped, timeLooped, original, 1, MixBlend.first, MixDirection.in, false);
fail("Too many events fired.");
}
}
@ -194,7 +194,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);
timeline.apply(skeleton, lastTimeLooped, timeLooped, original, 1, MixBlend.first, MixDirection.in, false);
fail("Wrong event fired.");
}
eventIndex++;
@ -206,7 +206,7 @@ public class EventTimelineTests {
i++;
}
if (firedEvents.size < eventsCount) {
timeline.apply(skeleton, lastTimeLooped, timeLooped, firedEvents, 1, MixBlend.first, MixDirection.in);
timeline.apply(skeleton, lastTimeLooped, timeLooped, firedEvents, 1, MixBlend.first, MixDirection.in, false);
if (print) System.out.println(firedEvents);
fail("Event not fired: " + events[eventIndex] + ", " + frames[eventIndex]);
}

View File

@ -107,7 +107,7 @@ public class IKTest extends ApplicationAdapter {
Bone crosshair = skeleton.findBone("crosshair"); // Should be cached.
boneCoords.set(cameraCoords.x, cameraCoords.y);
crosshair.getParent().worldToLocal(boneCoords); // camera space to local bone space
crosshair.getParent().getApplied().worldToLocal(boneCoords); // camera space to local bone space
crosshair.setPosition(boneCoords.x, boneCoords.y); // override the crosshair position
// Calculate final world transform with the crosshair bone set to the mouse cursor position. Update physics this time.

View File

@ -137,7 +137,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);
if (animation != null) animation.apply(skeleton, lastTime, time, true, null, 1, MixBlend.first, MixDirection.in, false);
skeleton.update(delta);
skeleton.updateWorldTransform(Physics.update);

View File

@ -104,7 +104,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);
animation.apply(skeleton, time, time, false, null, 1, MixBlend.first, MixDirection.in, false);
skeleton.update(fps);
skeleton.updateWorldTransform(Physics.update);

View File

@ -48,7 +48,7 @@ public class SkeletonAttachmentTest extends ApplicationAdapter {
Skeleton spineboy, goblin;
AnimationState spineboyState, goblinState;
Bone attachmentBone;
BoneApplied attachmentBone;
public void create () {
camera = new OrthographicCamera();
@ -87,7 +87,7 @@ public class SkeletonAttachmentTest extends ApplicationAdapter {
skeletonAttachment.setSkeleton(goblin);
Slot slot = spineboy.findSlot("front-upper-arm");
slot.setAttachment(skeletonAttachment);
attachmentBone = slot.getBone();
attachmentBone = slot.getBone().getApplied();
}
}

View File

@ -109,23 +109,24 @@ 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);
walkAnimation.apply(skeleton, time, time, true, events, 1, MixBlend.first, MixDirection.in, false);
} else if (time > blendOutStart) {
// blend out jump
walkAnimation.apply(skeleton, time, time, true, events, 1, MixBlend.first, MixDirection.in);
walkAnimation.apply(skeleton, time, time, true, events, 1, MixBlend.first, MixDirection.in, false);
jumpAnimation.apply(skeleton, time - beforeJump, time - beforeJump, false, events, 1 - (time - blendOutStart) / blendOut,
MixBlend.first, MixDirection.in);
MixBlend.first, MixDirection.in, false);
} else if (time > beforeJump + blendIn) {
// just jump
jumpAnimation.apply(skeleton, time - beforeJump, time - beforeJump, false, events, 1, MixBlend.first, MixDirection.in);
jumpAnimation.apply(skeleton, time - beforeJump, time - beforeJump, false, events, 1, MixBlend.first, MixDirection.in,
false);
} else if (time > beforeJump) {
// blend in jump
walkAnimation.apply(skeleton, time, time, true, events, 1, MixBlend.first, MixDirection.in);
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);
MixBlend.first, MixDirection.in, false);
} else {
// just walk before jump
walkAnimation.apply(skeleton, time, time, true, events, 1, MixBlend.first, MixDirection.in);
walkAnimation.apply(skeleton, time, time, true, events, 1, MixBlend.first, MixDirection.in, false);
}
skeleton.update(delta);

View File

@ -96,7 +96,7 @@ public class Animation {
/** Applies the animation's timelines to the specified skeleton.
* <p>
* See Timeline {@link Timeline#apply(Skeleton, float, float, Array, float, MixBlend, MixDirection)}.
* See Timeline {@link Timeline#apply(Skeleton, float, float, Array, float, MixBlend, MixDirection, boolean)}.
* @param skeleton The skeleton the animation is being applied to. This provides access to the bones, slots, and other skeleton
* components the timelines may change.
* @param lastTime The last time in seconds this animation was applied. Some timelines trigger only at specific times rather
@ -115,7 +115,7 @@ public class Animation {
* @param direction Indicates whether the timelines are mixing in or out. Used by timelines which perform instant transitions,
* such as {@link DrawOrderTimeline} or {@link AttachmentTimeline}. */
public void apply (Skeleton skeleton, float lastTime, float time, boolean loop, @Null Array<Event> events, float alpha,
MixBlend blend, MixDirection direction) {
MixBlend blend, MixDirection direction, boolean appliedPose) {
if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
if (loop && duration != 0) {
@ -125,7 +125,7 @@ public class Animation {
Object[] timelines = this.timelines.items;
for (int i = 0, n = this.timelines.size; i < n; i++)
((Timeline)timelines[i]).apply(skeleton, lastTime, time, events, alpha, blend, direction);
((Timeline)timelines[i]).apply(skeleton, lastTime, time, events, alpha, blend, direction, appliedPose);
}
/** The animation's name, which is unique across all animations in the skeleton. */
@ -140,7 +140,7 @@ public class Animation {
/** Controls how timeline values are mixed with setup pose values or current pose values when a timeline is applied with
* <code>alpha</code> < 1.
* <p>
* See Timeline {@link Timeline#apply(Skeleton, float, float, Array, float, MixBlend, MixDirection)}. */
* See Timeline {@link Timeline#apply(Skeleton, float, float, Array, float, MixBlend, MixDirection, boolean)}. */
static public enum MixBlend {
/** Transitions from the setup value to the timeline value (the current value is not used). Before the first frame, the
* setup value is set. */
@ -168,7 +168,7 @@ public class Animation {
/** Indicates whether a timeline's <code>alpha</code> is mixing out over time toward 0 (the setup or current pose value) or
* mixing in toward 1 (the timeline's value). Some timelines use this to decide how values are applied.
* <p>
* See Timeline {@link Timeline#apply(Skeleton, float, float, Array, float, MixBlend, MixDirection)}. */
* See Timeline {@link Timeline#apply(Skeleton, float, float, Array, float, MixBlend, MixDirection, boolean)}. */
static public enum MixDirection {
in, out
}
@ -239,9 +239,10 @@ public class Animation {
* apply animations on top of each other (layering).
* @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,
* such as {@link DrawOrderTimeline} or {@link AttachmentTimeline}, and others such as {@link ScaleTimeline}. */
* such as {@link DrawOrderTimeline} or {@link AttachmentTimeline}, and others such as {@link ScaleTimeline}.
* @param appliedPose True to to modify the applied pose. */
abstract public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha,
MixBlend blend, MixDirection direction);
MixBlend blend, MixDirection direction, boolean appliedPose);
/** Linear search using a stride of 1.
* @param time Must be >= the first value in <code>frames</code>.
@ -557,10 +558,13 @@ public class Animation {
}
public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
MixDirection direction) {
MixDirection direction, boolean appliedPose) {
Bone bone = skeleton.bones.get(boneIndex);
if (bone.active) bone.rotation = getRelativeValue(time, alpha, blend, bone.rotation, bone.data.rotation);
if (bone.active) {
if (appliedPose) bone = bone.applied;
bone.rotation = getRelativeValue(time, alpha, blend, bone.rotation, bone.data.rotation);
}
}
}
@ -580,10 +584,11 @@ public class Animation {
}
public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
MixDirection direction) {
MixDirection direction, boolean appliedPose) {
Bone bone = skeleton.bones.get(boneIndex);
if (!bone.active) return;
if (appliedPose) bone = bone.applied;
float[] frames = this.frames;
if (time < frames[0]) {
@ -650,10 +655,13 @@ public class Animation {
}
public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
MixDirection direction) {
MixDirection direction, boolean appliedPose) {
Bone bone = skeleton.bones.get(boneIndex);
if (bone.active) bone.x = getRelativeValue(time, alpha, blend, bone.x, bone.data.x);
if (bone.active) {
if (appliedPose) bone = bone.applied;
bone.x = getRelativeValue(time, alpha, blend, bone.x, bone.data.x);
}
}
}
@ -671,10 +679,13 @@ public class Animation {
}
public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
MixDirection direction) {
MixDirection direction, boolean appliedPose) {
Bone bone = skeleton.bones.get(boneIndex);
if (bone.active) bone.y = getRelativeValue(time, alpha, blend, bone.y, bone.data.y);
if (bone.active) {
if (appliedPose) bone = bone.applied;
bone.y = getRelativeValue(time, alpha, blend, bone.y, bone.data.y);
}
}
}
@ -694,10 +705,11 @@ public class Animation {
}
public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
MixDirection direction) {
MixDirection direction, boolean appliedPose) {
Bone bone = skeleton.bones.get(boneIndex);
if (!bone.active) return;
if (appliedPose) bone = bone.applied;
float[] frames = this.frames;
if (time < frames[0]) {
@ -803,10 +815,13 @@ public class Animation {
}
public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
MixDirection direction) {
MixDirection direction, boolean appliedPose) {
Bone bone = skeleton.bones.get(boneIndex);
if (bone.active) bone.scaleX = getScaleValue(time, alpha, blend, direction, bone.scaleX, bone.data.scaleX);
if (bone.active) {
if (appliedPose) bone = bone.applied;
bone.scaleX = getScaleValue(time, alpha, blend, direction, bone.scaleX, bone.data.scaleX);
}
}
}
@ -824,10 +839,13 @@ public class Animation {
}
public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
MixDirection direction) {
MixDirection direction, boolean appliedPose) {
Bone bone = skeleton.bones.get(boneIndex);
if (bone.active) bone.scaleY = getScaleValue(time, alpha, blend, direction, bone.scaleY, bone.data.scaleY);
if (bone.active) {
if (appliedPose) bone = bone.applied;
bone.scaleY = getScaleValue(time, alpha, blend, direction, bone.scaleY, bone.data.scaleY);
}
}
}
@ -847,10 +865,11 @@ public class Animation {
}
public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
MixDirection direction) {
MixDirection direction, boolean appliedPose) {
Bone bone = skeleton.bones.get(boneIndex);
if (!bone.active) return;
if (appliedPose) bone = bone.applied;
float[] frames = this.frames;
if (time < frames[0]) {
@ -917,10 +936,13 @@ public class Animation {
}
public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
MixDirection direction) {
MixDirection direction, boolean appliedPose) {
Bone bone = skeleton.bones.get(boneIndex);
if (bone.active) bone.shearX = getRelativeValue(time, alpha, blend, bone.shearX, bone.data.shearX);
if (bone.active) {
if (appliedPose) bone = bone.applied;
bone.shearX = getRelativeValue(time, alpha, blend, bone.shearX, bone.data.shearX);
}
}
}
@ -938,10 +960,13 @@ public class Animation {
}
public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
MixDirection direction) {
MixDirection direction, boolean appliedPose) {
Bone bone = skeleton.bones.get(boneIndex);
if (bone.active) bone.shearY = getRelativeValue(time, alpha, blend, bone.shearY, bone.data.shearY);
if (bone.active) {
if (appliedPose) bone = bone.applied;
bone.shearY = getRelativeValue(time, alpha, blend, bone.shearY, bone.data.shearY);
}
}
}
@ -975,10 +1000,11 @@ public class Animation {
}
public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
MixDirection direction) {
MixDirection direction, boolean appliedPose) {
Bone bone = skeleton.bones.get(boneIndex);
if (!bone.active) return;
if (appliedPose) bone = bone.applied;
if (direction == out) {
if (blend == setup) bone.inherit = bone.data.inherit;
@ -1029,10 +1055,11 @@ public class Animation {
}
public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
MixDirection direction) {
MixDirection direction, boolean appliedPose) {
Slot slot = skeleton.slots.get(slotIndex);
if (!slot.bone.active) return;
if (appliedPose) slot = slot.applied;
float[] frames = this.frames;
Color color = slot.color;
@ -1118,10 +1145,11 @@ public class Animation {
}
public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
MixDirection direction) {
MixDirection direction, boolean appliedPose) {
Slot slot = skeleton.slots.get(slotIndex);
if (!slot.bone.active) return;
if (appliedPose) slot = slot.applied;
float[] frames = this.frames;
Color color = slot.color;
@ -1197,10 +1225,11 @@ public class Animation {
}
public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
MixDirection direction) {
MixDirection direction, boolean appliedPose) {
Slot slot = skeleton.slots.get(slotIndex);
if (!slot.bone.active) return;
if (appliedPose) slot = slot.applied;
float[] frames = this.frames;
Color color = slot.color;
@ -1267,10 +1296,11 @@ public class Animation {
}
public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
MixDirection direction) {
MixDirection direction, boolean appliedPose) {
Slot slot = skeleton.slots.get(slotIndex);
if (!slot.bone.active) return;
if (appliedPose) slot = slot.applied;
float[] frames = this.frames;
Color light = slot.color, dark = slot.darkColor;
@ -1393,10 +1423,11 @@ public class Animation {
}
public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
MixDirection direction) {
MixDirection direction, boolean appliedPose) {
Slot slot = skeleton.slots.get(slotIndex);
if (!slot.bone.active) return;
if (appliedPose) slot = slot.applied;
float[] frames = this.frames;
Color light = slot.color, dark = slot.darkColor;
@ -1518,10 +1549,11 @@ public class Animation {
}
public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
MixDirection direction) {
MixDirection direction, boolean appliedPose) {
Slot slot = skeleton.slots.get(slotIndex);
if (!slot.bone.active) return;
if (appliedPose) slot = slot.applied;
if (direction == out) {
if (blend == setup) setAttachment(skeleton, slot, slot.data.attachmentName);
@ -1636,11 +1668,12 @@ public class Animation {
}
public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
MixDirection direction) {
MixDirection direction, boolean appliedPose) {
Slot slot = skeleton.slots.get(slotIndex);
if (!slot.bone.active || !(slot.attachment instanceof VertexAttachment vertexAttachment)
|| vertexAttachment.getTimelineAttachment() != attachment) return;
if (appliedPose) slot = slot.applied;
FloatArray deformArray = slot.deform;
if (deformArray.size == 0) blend = setup;
@ -1838,7 +1871,7 @@ public class Animation {
/** Fires events for frames > <code>lastTime</code> and <= <code>time</code>. */
public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> firedEvents, float alpha,
MixBlend blend, MixDirection direction) {
MixBlend blend, MixDirection direction, boolean appliedPose) {
if (firedEvents == null) return;
@ -1846,7 +1879,7 @@ public class Animation {
int frameCount = frames.length;
if (lastTime > time) { // Apply after lastTime for looped animations.
apply(skeleton, lastTime, Integer.MAX_VALUE, firedEvents, alpha, blend, direction);
apply(skeleton, lastTime, Integer.MAX_VALUE, firedEvents, alpha, blend, direction, appliedPose);
lastTime = -1f;
} else if (lastTime >= frames[frameCount - 1]) // Last time is after last frame.
return;
@ -1899,7 +1932,7 @@ public class Animation {
}
public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
MixDirection direction) {
MixDirection direction, boolean appliedPose) {
if (direction == out) {
if (blend == setup) arraycopy(skeleton.slots.items, 0, skeleton.drawOrder.items, 0, skeleton.slots.size);
@ -1963,10 +1996,11 @@ public class Animation {
}
public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
MixDirection direction) {
MixDirection direction, boolean appliedPose) {
IkConstraint constraint = skeleton.ikConstraints.get(constraintIndex);
if (!constraint.active) return;
if (appliedPose) constraint = constraint.applied;
float[] frames = this.frames;
if (time < frames[0]) {
@ -2072,10 +2106,11 @@ public class Animation {
}
public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
MixDirection direction) {
MixDirection direction, boolean appliedPose) {
TransformConstraint constraint = skeleton.transformConstraints.get(constraintIndex);
if (!constraint.active) return;
if (appliedPose) constraint = constraint.applied;
float[] frames = this.frames;
if (time < frames[0]) {
@ -2171,11 +2206,13 @@ public class Animation {
}
public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
MixDirection direction) {
MixDirection direction, boolean appliedPose) {
PathConstraint constraint = skeleton.pathConstraints.get(constraintIndex);
if (constraint.active)
if (constraint.active) {
if (appliedPose) constraint = constraint.applied;
constraint.position = getAbsoluteValue(time, alpha, blend, constraint.position, constraint.data.position);
}
}
}
@ -2195,11 +2232,13 @@ public class Animation {
}
public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
MixDirection direction) {
MixDirection direction, boolean appliedPose) {
PathConstraint constraint = skeleton.pathConstraints.get(constraintIndex);
if (constraint.active)
if (constraint.active) {
if (appliedPose) constraint = constraint.applied;
constraint.spacing = getAbsoluteValue(time, alpha, blend, constraint.spacing, constraint.data.spacing);
}
}
}
@ -2238,10 +2277,11 @@ public class Animation {
}
public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
MixDirection direction) {
MixDirection direction, boolean appliedPose) {
PathConstraint constraint = skeleton.pathConstraints.get(constraintIndex);
if (!constraint.active) return;
if (appliedPose) constraint = constraint.applied;
float[] frames = this.frames;
if (time < frames[0]) {
@ -2314,7 +2354,7 @@ public class Animation {
}
public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
MixDirection direction) {
MixDirection direction, boolean appliedPose) {
PhysicsConstraint constraint;
if (constraintIndex == -1) {
@ -2323,12 +2363,17 @@ public class Animation {
Object[] constraints = skeleton.physicsConstraints.items;
for (int i = 0, n = skeleton.physicsConstraints.size; i < n; i++) {
constraint = (PhysicsConstraint)constraints[i];
if (constraint.active && global(constraint.data))
if (constraint.active && global(constraint.data)) {
if (appliedPose) constraint = constraint.applied;
set(constraint, getAbsoluteValue(time, alpha, blend, get(constraint), setup(constraint), value));
}
}
} else {
constraint = skeleton.physicsConstraints.get(constraintIndex);
if (constraint.active) set(constraint, getAbsoluteValue(time, alpha, blend, get(constraint), setup(constraint)));
if (constraint.active) {
if (appliedPose) constraint = constraint.applied;
set(constraint, getAbsoluteValue(time, alpha, blend, get(constraint), setup(constraint)));
}
}
}
@ -2532,7 +2577,7 @@ public class Animation {
/** Resets the physics constraint when frames > <code>lastTime</code> and <= <code>time</code>. */
public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> firedEvents, float alpha,
MixBlend blend, MixDirection direction) {
MixBlend blend, MixDirection direction, boolean appliedPose) {
PhysicsConstraint constraint = null;
if (constraintIndex != -1) {
@ -2543,20 +2588,24 @@ public class Animation {
float[] frames = this.frames;
if (lastTime > time) { // Apply after lastTime for looped animations.
apply(skeleton, lastTime, Integer.MAX_VALUE, null, alpha, blend, direction);
apply(skeleton, lastTime, Integer.MAX_VALUE, null, alpha, blend, direction, appliedPose);
lastTime = -1f;
} else if (lastTime >= frames[frames.length - 1]) // Last time is after last frame.
return;
if (time < frames[0]) return;
if (lastTime < frames[0] || time >= frames[search(frames, lastTime) + 1]) {
if (constraint != null)
if (constraint != null) {
if (appliedPose) constraint = constraint.applied;
constraint.reset();
else {
} else {
Object[] constraints = skeleton.physicsConstraints.items;
for (int i = 0, n = skeleton.physicsConstraints.size; i < n; i++) {
constraint = (PhysicsConstraint)constraints[i];
if (constraint.active) constraint.reset();
if (constraint.active) {
if (appliedPose) constraint = constraint.applied;
constraint.reset();
}
}
}
}
@ -2601,10 +2650,12 @@ public class Animation {
}
public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
MixDirection direction) {
MixDirection direction, boolean appliedPose) {
Slot slot = skeleton.slots.get(slotIndex);
if (!slot.bone.active) return;
if (appliedPose) slot = slot.applied;
Attachment slotAttachment = slot.attachment;
if (slotAttachment != attachment) {
if (!(slotAttachment instanceof VertexAttachment vertexAttachment)

View File

@ -238,8 +238,10 @@ public class AnimationState {
Object timeline = timelines[ii];
if (timeline instanceof AttachmentTimeline attachmentTimeline)
applyAttachmentTimeline(attachmentTimeline, skeleton, applyTime, blend, attachments);
else
((Timeline)timeline).apply(skeleton, animationLast, applyTime, applyEvents, alpha, blend, MixDirection.in);
else {
((Timeline)timeline).apply(skeleton, animationLast, applyTime, applyEvents, alpha, blend, MixDirection.in,
false);
}
}
} else {
int[] timelineMode = current.timelineMode.items;
@ -258,7 +260,7 @@ public class AnimationState {
} else if (timeline instanceof AttachmentTimeline attachmentTimeline)
applyAttachmentTimeline(attachmentTimeline, skeleton, applyTime, blend, attachments);
else
timeline.apply(skeleton, animationLast, applyTime, applyEvents, alpha, timelineBlend, MixDirection.in);
timeline.apply(skeleton, animationLast, applyTime, applyEvents, alpha, timelineBlend, MixDirection.in, false);
}
}
queueEvents(current, animationTime);
@ -313,7 +315,7 @@ public class AnimationState {
if (blend == MixBlend.add) {
for (int i = 0; i < timelineCount; i++)
((Timeline)timelines[i]).apply(skeleton, animationLast, applyTime, events, alphaMix, blend, MixDirection.out);
((Timeline)timelines[i]).apply(skeleton, animationLast, applyTime, events, alphaMix, blend, MixDirection.out, false);
} else {
int[] timelineMode = from.timelineMode.items;
Object[] timelineHoldMix = from.timelineHoldMix.items;
@ -363,7 +365,7 @@ public class AnimationState {
else {
if (drawOrder && timeline instanceof DrawOrderTimeline && timelineBlend == MixBlend.setup)
direction = MixDirection.in;
timeline.apply(skeleton, animationLast, applyTime, events, alpha, timelineBlend, direction);
timeline.apply(skeleton, animationLast, applyTime, events, alpha, timelineBlend, direction, false);
}
}
}
@ -409,7 +411,7 @@ public class AnimationState {
if (firstFrame) timelinesRotation[i] = 0;
if (alpha == 1) {
timeline.apply(skeleton, 0, time, null, 1, blend, MixDirection.in);
timeline.apply(skeleton, 0, time, null, 1, blend, MixDirection.in, false);
return;
}

View File

@ -29,41 +29,45 @@
package com.esotericsoftware.spine;
import static com.badlogic.gdx.math.Matrix3.*;
import static com.esotericsoftware.spine.utils.SpineUtils.*;
import com.badlogic.gdx.math.Matrix3;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.Null;
import com.esotericsoftware.spine.BoneData.Inherit;
import com.esotericsoftware.spine.Skeleton.Physics;
/** Stores a bone's current pose.
* <p>
* A bone has a local transform which is used to compute its world transform. A bone also has an applied transform, which is a
* local transform that can be applied to compute the world transform. The local transform and applied transform may differ if a
* constraint or application code modifies the world transform after it was computed from the local transform. */
public class Bone implements Updatable {
public class Bone {
final BoneData data;
final Skeleton skeleton;
@Null final Bone parent;
final Array<Bone> children = new Array();
final Array<Bone> children;
BoneApplied applied;
float x, y, rotation, scaleX, scaleY, shearX, shearY;
float ax, ay, arotation, ascaleX, ascaleY, ashearX, ashearY;
float a, b, worldX;
float c, d, worldY;
Inherit inherit;
boolean sorted, active;
Bone (Bone bone) {
this.data = bone.data;
this.skeleton = bone.skeleton;
this.parent = bone.parent;
this.children = bone.children;
}
public Bone (BoneData data, Skeleton skeleton, @Null Bone parent) {
if (data == null) throw new IllegalArgumentException("data cannot be null.");
if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
this.data = data;
this.skeleton = skeleton;
this.parent = parent;
children = new Array();
applied = new BoneApplied(this);
setToSetupPose();
}
@ -73,6 +77,7 @@ public class Bone implements Updatable {
if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
this.skeleton = skeleton;
this.parent = parent;
children = new Array();
data = bone.data;
x = bone.x;
y = bone.y;
@ -84,131 +89,6 @@ public class Bone implements Updatable {
inherit = bone.inherit;
}
/** Computes the world transform using the parent bone and this bone's local applied transform. */
public void update (Physics physics) {
updateWorldTransform(ax, ay, arotation, ascaleX, ascaleY, ashearX, ashearY);
}
/** Computes the world transform using the parent bone and this bone's local transform.
* <p>
* See {@link #updateWorldTransform(float, float, float, float, float, float, float)}. */
public void updateWorldTransform () {
updateWorldTransform(x, y, rotation, scaleX, scaleY, shearX, shearY);
}
/** Computes the world transform using the parent bone and the specified local transform. The applied transform is set to the
* specified local transform. Child bones are not updated.
* <p>
* See <a href="https://esotericsoftware.com/spine-runtime-skeletons#World-transforms">World transforms</a> in the Spine
* Runtimes Guide. */
public void updateWorldTransform (float x, float y, float rotation, float scaleX, float scaleY, float shearX, float shearY) {
ax = x;
ay = y;
arotation = rotation;
ascaleX = scaleX;
ascaleY = scaleY;
ashearX = shearX;
ashearY = shearY;
Bone parent = this.parent;
if (parent == null) { // Root bone.
Skeleton skeleton = this.skeleton;
float sx = skeleton.scaleX, sy = skeleton.scaleY;
float rx = (rotation + shearX) * degRad;
float ry = (rotation + 90 + shearY) * degRad;
a = cos(rx) * scaleX * sx;
b = cos(ry) * scaleY * sx;
c = sin(rx) * scaleX * sy;
d = sin(ry) * scaleY * sy;
worldX = x * sx + skeleton.x;
worldY = y * sy + skeleton.y;
return;
}
float pa = parent.a, pb = parent.b, pc = parent.c, pd = parent.d;
worldX = pa * x + pb * y + parent.worldX;
worldY = pc * x + pd * y + parent.worldY;
switch (inherit) {
case normal -> {
float rx = (rotation + shearX) * degRad;
float ry = (rotation + 90 + shearY) * degRad;
float la = cos(rx) * scaleX;
float lb = cos(ry) * scaleY;
float lc = sin(rx) * scaleX;
float ld = sin(ry) * scaleY;
a = pa * la + pb * lc;
b = pa * lb + pb * ld;
c = pc * la + pd * lc;
d = pc * lb + pd * ld;
return;
}
case onlyTranslation -> {
float rx = (rotation + shearX) * degRad;
float ry = (rotation + 90 + shearY) * degRad;
a = cos(rx) * scaleX;
b = cos(ry) * scaleY;
c = sin(rx) * scaleX;
d = sin(ry) * scaleY;
}
case noRotationOrReflection -> {
float sx = 1 / skeleton.scaleX, sy = 1 / skeleton.scaleY;
pa *= sx;
pc *= sy;
float s = pa * pa + pc * pc, prx;
if (s > 0.0001f) {
s = Math.abs(pa * pd * sy - pb * sx * pc) / s;
pb = pc * s;
pd = pa * s;
prx = atan2Deg(pc, pa);
} else {
pa = 0;
pc = 0;
prx = 90 - atan2Deg(pd, pb);
}
float rx = (rotation + shearX - prx) * degRad;
float ry = (rotation + shearY - prx + 90) * degRad;
float la = cos(rx) * scaleX;
float lb = cos(ry) * scaleY;
float lc = sin(rx) * scaleX;
float ld = sin(ry) * scaleY;
a = pa * la - pb * lc;
b = pa * lb - pb * ld;
c = pc * la + pd * lc;
d = pc * lb + pd * ld;
}
case noScale, noScaleOrReflection -> {
rotation *= degRad;
float cos = cos(rotation), sin = sin(rotation);
float za = (pa * cos + pb * sin) / skeleton.scaleX;
float zc = (pc * cos + pd * sin) / skeleton.scaleY;
float s = (float)Math.sqrt(za * za + zc * zc);
if (s > 0.00001f) s = 1 / s;
za *= s;
zc *= s;
s = (float)Math.sqrt(za * za + zc * zc);
if (inherit == Inherit.noScale && (pa * pd - pb * pc < 0) != (skeleton.scaleX < 0 != skeleton.scaleY < 0)) s = -s;
rotation = PI / 2 + atan2(zc, za);
float zb = cos(rotation) * s;
float zd = sin(rotation) * s;
shearX *= degRad;
shearY = (90 + shearY) * degRad;
float la = cos(shearX) * scaleX;
float lb = cos(shearY) * scaleY;
float lc = sin(shearX) * scaleX;
float ld = sin(shearY) * scaleY;
a = za * la + zb * lc;
b = za * lb + zb * ld;
c = zc * la + zd * lc;
d = zc * lb + zd * ld;
}
}
a *= skeleton.scaleX;
b *= skeleton.scaleX;
c *= skeleton.scaleY;
d *= skeleton.scaleY;
}
/** Sets this bone's local transform to the setup pose. */
public void setToSetupPose () {
BoneData data = this.data;
@ -246,8 +126,6 @@ public class Bone implements Updatable {
return active;
}
// -- Local transform
/** The local x translation. */
public float getX () {
return x;
@ -336,309 +214,11 @@ public class Bone implements Updatable {
this.inherit = inherit;
}
// -- Applied transform
/** The applied local x translation. */
public float getAX () {
return ax;
/** Returns the bone for applied pose. */
public BoneApplied getApplied () {
return applied;
}
public void setAX (float ax) {
this.ax = ax;
}
/** The applied local y translation. */
public float getAY () {
return ay;
}
public void setAY (float ay) {
this.ay = ay;
}
/** The applied local rotation in degrees, counter clockwise. */
public float getARotation () {
return arotation;
}
public void setARotation (float arotation) {
this.arotation = arotation;
}
/** The applied local scaleX. */
public float getAScaleX () {
return ascaleX;
}
public void setAScaleX (float ascaleX) {
this.ascaleX = ascaleX;
}
/** The applied local scaleY. */
public float getAScaleY () {
return ascaleY;
}
public void setAScaleY (float ascaleY) {
this.ascaleY = ascaleY;
}
/** The applied local shearX. */
public float getAShearX () {
return ashearX;
}
public void setAShearX (float ashearX) {
this.ashearX = ashearX;
}
/** The applied local shearY. */
public float getAShearY () {
return ashearY;
}
public void setAShearY (float ashearY) {
this.ashearY = ashearY;
}
/** Computes the applied transform values from the world transform.
* <p>
* If the world transform is modified (by a constraint, {@link #rotateWorld(float)}, etc) then this method should be called so
* the applied transform matches the world transform. The applied transform may be needed by other code (eg to apply another
* constraint).
* <p>
* Some information is ambiguous in the world transform, such as -1,-1 scale versus 180 rotation. The applied transform after
* calling this method is equivalent to the local transform used to compute the world transform, but may not be identical. */
public void updateAppliedTransform () {
Bone parent = this.parent;
if (parent == null) {
ax = worldX - skeleton.x;
ay = worldY - skeleton.y;
float a = this.a, b = this.b, c = this.c, d = this.d;
arotation = atan2Deg(c, a);
ascaleX = (float)Math.sqrt(a * a + c * c);
ascaleY = (float)Math.sqrt(b * b + d * d);
ashearX = 0;
ashearY = atan2Deg(a * b + c * d, a * d - b * c);
return;
}
float pa = parent.a, pb = parent.b, pc = parent.c, pd = parent.d;
float pid = 1 / (pa * pd - pb * pc);
float ia = pd * pid, ib = pb * pid, ic = pc * pid, id = pa * pid;
float dx = worldX - parent.worldX, dy = worldY - parent.worldY;
ax = (dx * ia - dy * ib);
ay = (dy * id - dx * ic);
float ra, rb, rc, rd;
if (inherit == Inherit.onlyTranslation) {
ra = a;
rb = b;
rc = c;
rd = d;
} else {
switch (inherit) {
case noRotationOrReflection -> {
float s = Math.abs(pa * pd - pb * pc) / (pa * pa + pc * pc);
pb = -pc * skeleton.scaleX * s / skeleton.scaleY;
pd = pa * skeleton.scaleY * s / skeleton.scaleX;
pid = 1 / (pa * pd - pb * pc);
ia = pd * pid;
ib = pb * pid;
}
case noScale, noScaleOrReflection -> {
float r = rotation * degRad, cos = cos(r), sin = sin(r);
pa = (pa * cos + pb * sin) / skeleton.scaleX;
pc = (pc * cos + pd * sin) / skeleton.scaleY;
float s = (float)Math.sqrt(pa * pa + pc * pc);
if (s > 0.00001f) s = 1 / s;
pa *= s;
pc *= s;
s = (float)Math.sqrt(pa * pa + pc * pc);
if (inherit == Inherit.noScale && pid < 0 != (skeleton.scaleX < 0 != skeleton.scaleY < 0)) s = -s;
r = PI / 2 + atan2(pc, pa);
pb = cos(r) * s;
pd = sin(r) * s;
pid = 1 / (pa * pd - pb * pc);
ia = pd * pid;
ib = pb * pid;
ic = pc * pid;
id = pa * pid;
}
}
ra = ia * a - ib * c;
rb = ia * b - ib * d;
rc = id * c - ic * a;
rd = id * d - ic * b;
}
ashearX = 0;
ascaleX = (float)Math.sqrt(ra * ra + rc * rc);
if (ascaleX > 0.0001f) {
float det = ra * rd - rb * rc;
ascaleY = det / ascaleX;
ashearY = -atan2Deg(ra * rb + rc * rd, det);
arotation = atan2Deg(rc, ra);
} else {
ascaleX = 0;
ascaleY = (float)Math.sqrt(rb * rb + rd * rd);
ashearY = 0;
arotation = 90 - atan2Deg(rd, rb);
}
}
// -- World transform
/** Part of the world transform matrix for the X axis. If changed, {@link #updateAppliedTransform()} should be called. */
public float getA () {
return a;
}
public void setA (float a) {
this.a = a;
}
/** Part of the world transform matrix for the Y axis. If changed, {@link #updateAppliedTransform()} should be called. */
public float getB () {
return b;
}
public void setB (float b) {
this.b = b;
}
/** Part of the world transform matrix for the X axis. If changed, {@link #updateAppliedTransform()} should be called. */
public float getC () {
return c;
}
public void setC (float c) {
this.c = c;
}
/** Part of the world transform matrix for the Y axis. If changed, {@link #updateAppliedTransform()} should be called. */
public float getD () {
return d;
}
public void setD (float d) {
this.d = d;
}
/** The world X position. If changed, {@link #updateAppliedTransform()} should be called. */
public float getWorldX () {
return worldX;
}
public void setWorldX (float worldX) {
this.worldX = worldX;
}
/** The world Y position. If changed, {@link #updateAppliedTransform()} should be called. */
public float getWorldY () {
return worldY;
}
public void setWorldY (float worldY) {
this.worldY = worldY;
}
/** The world rotation for the X axis, calculated using {@link #a} and {@link #c}. */
public float getWorldRotationX () {
return atan2Deg(c, a);
}
/** The world rotation for the Y axis, calculated using {@link #b} and {@link #d}. */
public float getWorldRotationY () {
return atan2Deg(d, b);
}
/** The magnitude (always positive) of the world scale X, calculated using {@link #a} and {@link #c}. */
public float getWorldScaleX () {
return (float)Math.sqrt(a * a + c * c);
}
/** The magnitude (always positive) of the world scale Y, calculated using {@link #b} and {@link #d}. */
public float getWorldScaleY () {
return (float)Math.sqrt(b * b + d * d);
}
public Matrix3 getWorldTransform (Matrix3 worldTransform) {
if (worldTransform == null) throw new IllegalArgumentException("worldTransform cannot be null.");
float[] val = worldTransform.val;
val[M00] = a;
val[M01] = b;
val[M10] = c;
val[M11] = d;
val[M02] = worldX;
val[M12] = worldY;
val[M20] = 0;
val[M21] = 0;
val[M22] = 1;
return worldTransform;
}
/** Transforms a point from world coordinates to the bone's local coordinates. */
public Vector2 worldToLocal (Vector2 world) {
if (world == null) throw new IllegalArgumentException("world cannot be null.");
float det = a * d - b * c;
float x = world.x - worldX, y = world.y - worldY;
world.x = (x * d - y * b) / det;
world.y = (y * a - x * c) / det;
return world;
}
/** Transforms a point from the bone's local coordinates to world coordinates. */
public Vector2 localToWorld (Vector2 local) {
if (local == null) throw new IllegalArgumentException("local cannot be null.");
float x = local.x, y = local.y;
local.x = x * a + y * b + worldX;
local.y = x * c + y * d + worldY;
return local;
}
/** Transforms a point from world coordinates to the parent bone's local coordinates. */
public Vector2 worldToParent (Vector2 world) {
if (world == null) throw new IllegalArgumentException("world cannot be null.");
return parent == null ? world : parent.worldToLocal(world);
}
/** Transforms a point from the parent bone's coordinates to world coordinates. */
public Vector2 parentToWorld (Vector2 world) {
if (world == null) throw new IllegalArgumentException("world cannot be null.");
return parent == null ? world : parent.localToWorld(world);
}
/** Transforms a world rotation to a local rotation. */
public float worldToLocalRotation (float worldRotation) {
worldRotation *= degRad;
float sin = sin(worldRotation), cos = cos(worldRotation);
return atan2Deg(a * sin - c * cos, d * cos - b * sin) + rotation - shearX;
}
/** Transforms a local rotation to a world rotation. */
public float localToWorldRotation (float localRotation) {
localRotation = (localRotation - rotation - shearX) * degRad;
float sin = sin(localRotation), cos = cos(localRotation);
return atan2Deg(cos * c + sin * d, cos * a + sin * b);
}
/** Rotates the world transform the specified amount.
* <p>
* After changes are made to the world transform, {@link #updateAppliedTransform()} should be called and
* {@link #update(Physics)} will need to be called on any child bones, recursively. */
public void rotateWorld (float degrees) {
degrees *= degRad;
float sin = sin(degrees), cos = cos(degrees);
float ra = a, rb = b;
a = cos * ra - sin * c;
b = cos * rb - sin * d;
c = sin * ra + cos * c;
d = sin * rb + cos * d;
}
// ---
public String toString () {
return data.name;
}

View File

@ -0,0 +1,372 @@
package com.esotericsoftware.spine;
import static com.badlogic.gdx.math.Matrix3.*;
import static com.esotericsoftware.spine.utils.SpineUtils.*;
import com.badlogic.gdx.math.Matrix3;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.utils.Null;
import com.esotericsoftware.spine.BoneData.Inherit;
import com.esotericsoftware.spine.Skeleton.Physics;
public class BoneApplied extends Bone implements Updatable {
final Bone pose;
@Null final BoneApplied parentApplied;
float a, b, worldX;
float c, d, worldY;
BoneApplied (Bone bone) {
super(bone);
pose = bone;
parentApplied = parent == null ? null : parent.applied;
}
/** Computes the world transform using the parent bone and this bone's local applied transform. */
public void updateWorldTransform () {
updateWorldTransform();
}
/** Computes the world transform using the parent bone and this bone's local transform.
* <p>
* See {@link #updateWorldTransform(float, float, float, float, float, float, float)}. */
/** Computes the world transform using the parent bone and the specified local transform. The applied transform is set to the
* specified local transform. Child bones are not updated.
* <p>
* See <a href="https://esotericsoftware.com/spine-runtime-skeletons#World-transforms">World transforms</a> in the Spine
* Runtimes Guide. */
public void update (Physics physics) {
BoneApplied parent = parentApplied;
if (parent == null) { // Root bone.
Skeleton skeleton = this.skeleton;
float sx = skeleton.scaleX, sy = skeleton.scaleY;
float rx = (rotation + shearX) * degRad;
float ry = (rotation + 90 + shearY) * degRad;
a = cos(rx) * scaleX * sx;
b = cos(ry) * scaleY * sx;
c = sin(rx) * scaleX * sy;
d = sin(ry) * scaleY * sy;
worldX = x * sx + skeleton.x;
worldY = y * sy + skeleton.y;
return;
}
float pa = parent.a, pb = parent.b, pc = parent.c, pd = parent.d;
worldX = pa * x + pb * y + parent.worldX;
worldY = pc * x + pd * y + parent.worldY;
switch (inherit) {
case normal -> {
float rx = (rotation + shearX) * degRad;
float ry = (rotation + 90 + shearY) * degRad;
float la = cos(rx) * scaleX;
float lb = cos(ry) * scaleY;
float lc = sin(rx) * scaleX;
float ld = sin(ry) * scaleY;
a = pa * la + pb * lc;
b = pa * lb + pb * ld;
c = pc * la + pd * lc;
d = pc * lb + pd * ld;
return;
}
case onlyTranslation -> {
float rx = (rotation + shearX) * degRad;
float ry = (rotation + 90 + shearY) * degRad;
a = cos(rx) * scaleX;
b = cos(ry) * scaleY;
c = sin(rx) * scaleX;
d = sin(ry) * scaleY;
}
case noRotationOrReflection -> {
float sx = 1 / skeleton.scaleX, sy = 1 / skeleton.scaleY;
pa *= sx;
pc *= sy;
float s = pa * pa + pc * pc, prx;
if (s > 0.0001f) {
s = Math.abs(pa * pd * sy - pb * sx * pc) / s;
pb = pc * s;
pd = pa * s;
prx = atan2Deg(pc, pa);
} else {
pa = 0;
pc = 0;
prx = 90 - atan2Deg(pd, pb);
}
float rx = (rotation + shearX - prx) * degRad;
float ry = (rotation + shearY - prx + 90) * degRad;
float la = cos(rx) * scaleX;
float lb = cos(ry) * scaleY;
float lc = sin(rx) * scaleX;
float ld = sin(ry) * scaleY;
a = pa * la - pb * lc;
b = pa * lb - pb * ld;
c = pc * la + pd * lc;
d = pc * lb + pd * ld;
}
case noScale, noScaleOrReflection -> {
rotation *= degRad;
float cos = cos(rotation), sin = sin(rotation);
float za = (pa * cos + pb * sin) / skeleton.scaleX;
float zc = (pc * cos + pd * sin) / skeleton.scaleY;
float s = (float)Math.sqrt(za * za + zc * zc);
if (s > 0.00001f) s = 1 / s;
za *= s;
zc *= s;
s = (float)Math.sqrt(za * za + zc * zc);
if (inherit == Inherit.noScale && (pa * pd - pb * pc < 0) != (skeleton.scaleX < 0 != skeleton.scaleY < 0)) s = -s;
rotation = PI / 2 + atan2(zc, za);
float zb = cos(rotation) * s;
float zd = sin(rotation) * s;
shearX *= degRad;
shearY = (90 + shearY) * degRad;
float la = cos(shearX) * scaleX;
float lb = cos(shearY) * scaleY;
float lc = sin(shearX) * scaleX;
float ld = sin(shearY) * scaleY;
a = za * la + zb * lc;
b = za * lb + zb * ld;
c = zc * la + zd * lc;
d = zc * lb + zd * ld;
}
}
a *= skeleton.scaleX;
b *= skeleton.scaleX;
c *= skeleton.scaleY;
d *= skeleton.scaleY;
}
/** Computes the applied transform values from the world transform.
* <p>
* If the world transform is modified (by a constraint, {@link #rotateWorld(float)}, etc) then this method should be called so
* the applied transform matches the world transform. The applied transform may be needed by other code (eg to apply another
* constraint).
* <p>
* Some information is ambiguous in the world transform, such as -1,-1 scale versus 180 rotation. The applied transform after
* calling this method is equivalent to the local transform used to compute the world transform, but may not be identical. */
public void updateAppliedTransform () {
BoneApplied parent = parentApplied;
if (parent == null) {
x = worldX - skeleton.x;
y = worldY - skeleton.y;
float a = this.a, b = this.b, c = this.c, d = this.d;
rotation = atan2Deg(c, a);
scaleX = (float)Math.sqrt(a * a + c * c);
scaleY = (float)Math.sqrt(b * b + d * d);
shearX = 0;
shearY = atan2Deg(a * b + c * d, a * d - b * c);
return;
}
float pa = parent.a, pb = parent.b, pc = parent.c, pd = parent.d;
float pid = 1 / (pa * pd - pb * pc);
float ia = pd * pid, ib = pb * pid, ic = pc * pid, id = pa * pid;
float dx = worldX - parent.worldX, dy = worldY - parent.worldY;
x = (dx * ia - dy * ib);
y = (dy * id - dx * ic);
float ra, rb, rc, rd;
if (inherit == Inherit.onlyTranslation) {
ra = a;
rb = b;
rc = c;
rd = d;
} else {
switch (inherit) {
case noRotationOrReflection -> {
float s = Math.abs(pa * pd - pb * pc) / (pa * pa + pc * pc);
pb = -pc * skeleton.scaleX * s / skeleton.scaleY;
pd = pa * skeleton.scaleY * s / skeleton.scaleX;
pid = 1 / (pa * pd - pb * pc);
ia = pd * pid;
ib = pb * pid;
}
case noScale, noScaleOrReflection -> {
float r = rotation * degRad, cos = cos(r), sin = sin(r);
pa = (pa * cos + pb * sin) / skeleton.scaleX;
pc = (pc * cos + pd * sin) / skeleton.scaleY;
float s = (float)Math.sqrt(pa * pa + pc * pc);
if (s > 0.00001f) s = 1 / s;
pa *= s;
pc *= s;
s = (float)Math.sqrt(pa * pa + pc * pc);
if (inherit == Inherit.noScale && pid < 0 != (skeleton.scaleX < 0 != skeleton.scaleY < 0)) s = -s;
r = PI / 2 + atan2(pc, pa);
pb = cos(r) * s;
pd = sin(r) * s;
pid = 1 / (pa * pd - pb * pc);
ia = pd * pid;
ib = pb * pid;
ic = pc * pid;
id = pa * pid;
}
}
ra = ia * a - ib * c;
rb = ia * b - ib * d;
rc = id * c - ic * a;
rd = id * d - ic * b;
}
shearX = 0;
scaleX = (float)Math.sqrt(ra * ra + rc * rc);
if (scaleX > 0.0001f) {
float det = ra * rd - rb * rc;
scaleY = det / scaleX;
shearY = -atan2Deg(ra * rb + rc * rd, det);
rotation = atan2Deg(rc, ra);
} else {
scaleX = 0;
scaleY = (float)Math.sqrt(rb * rb + rd * rd);
shearY = 0;
rotation = 90 - atan2Deg(rd, rb);
}
}
/** Part of the world transform matrix for the X axis. If changed, {@link #updateAppliedTransform()} should be called. */
public float getA () {
return a;
}
public void setA (float a) {
this.a = a;
}
/** Part of the world transform matrix for the Y axis. If changed, {@link #updateAppliedTransform()} should be called. */
public float getB () {
return b;
}
public void setB (float b) {
this.b = b;
}
/** Part of the world transform matrix for the X axis. If changed, {@link #updateAppliedTransform()} should be called. */
public float getC () {
return c;
}
public void setC (float c) {
this.c = c;
}
/** Part of the world transform matrix for the Y axis. If changed, {@link #updateAppliedTransform()} should be called. */
public float getD () {
return d;
}
public void setD (float d) {
this.d = d;
}
/** The world X position. If changed, {@link #updateAppliedTransform()} should be called. */
public float getWorldX () {
return worldX;
}
public void setWorldX (float worldX) {
this.worldX = worldX;
}
/** The world Y position. If changed, {@link #updateAppliedTransform()} should be called. */
public float getWorldY () {
return worldY;
}
public void setWorldY (float worldY) {
this.worldY = worldY;
}
/** The world rotation for the X axis, calculated using {@link #a} and {@link #c}. */
public float getWorldRotationX () {
return atan2Deg(c, a);
}
/** The world rotation for the Y axis, calculated using {@link #b} and {@link #d}. */
public float getWorldRotationY () {
return atan2Deg(d, b);
}
/** The magnitude (always positive) of the world scale X, calculated using {@link #a} and {@link #c}. */
public float getWorldScaleX () {
return (float)Math.sqrt(a * a + c * c);
}
/** The magnitude (always positive) of the world scale Y, calculated using {@link #b} and {@link #d}. */
public float getWorldScaleY () {
return (float)Math.sqrt(b * b + d * d);
}
public Matrix3 getWorldTransform (Matrix3 worldTransform) {
if (worldTransform == null) throw new IllegalArgumentException("worldTransform cannot be null.");
float[] val = worldTransform.val;
val[M00] = a;
val[M01] = b;
val[M10] = c;
val[M11] = d;
val[M02] = worldX;
val[M12] = worldY;
val[M20] = 0;
val[M21] = 0;
val[M22] = 1;
return worldTransform;
}
/** Transforms a point from world coordinates to the bone's local coordinates. */
public Vector2 worldToLocal (Vector2 world) {
if (world == null) throw new IllegalArgumentException("world cannot be null.");
float det = a * d - b * c;
float x = world.x - worldX, y = world.y - worldY;
world.x = (x * d - y * b) / det;
world.y = (y * a - x * c) / det;
return world;
}
/** Transforms a point from the bone's local coordinates to world coordinates. */
public Vector2 localToWorld (Vector2 local) {
if (local == null) throw new IllegalArgumentException("local cannot be null.");
float x = local.x, y = local.y;
local.x = x * a + y * b + worldX;
local.y = x * c + y * d + worldY;
return local;
}
/** Transforms a point from world coordinates to the parent bone's local coordinates. */
public Vector2 worldToParent (Vector2 world) {
if (world == null) throw new IllegalArgumentException("world cannot be null.");
return parent == null ? world : parentApplied.worldToLocal(world);
}
/** Transforms a point from the parent bone's coordinates to world coordinates. */
public Vector2 parentToWorld (Vector2 world) {
if (world == null) throw new IllegalArgumentException("world cannot be null.");
return parent == null ? world : parentApplied.localToWorld(world);
}
/** Transforms a world rotation to a local rotation. */
public float worldToLocalRotation (float worldRotation) {
worldRotation *= degRad;
float sin = sin(worldRotation), cos = cos(worldRotation);
return atan2Deg(a * sin - c * cos, d * cos - b * sin) + rotation - shearX;
}
/** Transforms a local rotation to a world rotation. */
public float localToWorldRotation (float localRotation) {
localRotation = (localRotation - rotation - shearX) * degRad;
float sin = sin(localRotation), cos = cos(localRotation);
return atan2Deg(cos * c + sin * d, cos * a + sin * b);
}
/** Rotates the world transform the specified amount.
* <p>
* After changes are made to the world transform, {@link #updateAppliedTransform()} should be called and
* {@link #update(Physics)} will need to be called on any child bones, recursively. */
public void rotateWorld (float degrees) {
degrees *= degRad;
float sin = sin(degrees), cos = cos(degrees);
float ra = a, rb = b;
a = cos * ra - sin * c;
b = cos * rb - sin * d;
c = sin * ra + cos * c;
d = sin * rb + cos * d;
}
}

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)},
* {@link Timeline#apply(Skeleton, float, float, com.badlogic.gdx.utils.Array, float, com.esotericsoftware.spine.Animation.MixBlend, com.esotericsoftware.spine.Animation.MixDirection, 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

@ -42,13 +42,20 @@ import com.esotericsoftware.spine.Skeleton.Physics;
* See <a href="https://esotericsoftware.com/spine-ik-constraints">IK constraints</a> in the Spine User Guide. */
public class IkConstraint implements Updatable {
final IkConstraintData data;
final Array<Bone> bones;
Bone target;
final Array<BoneApplied> bones;
BoneApplied target;
IkConstraint applied;
boolean active;
int bendDirection;
boolean compress, stretch;
float mix = 1, softness;
boolean active;
private IkConstraint (IkConstraintData data, Array<BoneApplied> bones, BoneApplied target) {
this.data = data;
this.bones = bones;
this.target = target;
}
public IkConstraint (IkConstraintData data, Skeleton skeleton) {
if (data == null) throw new IllegalArgumentException("data cannot be null.");
@ -57,9 +64,11 @@ public class IkConstraint implements Updatable {
bones = new Array(data.bones.size);
for (BoneData boneData : data.bones)
bones.add(skeleton.bones.get(boneData.index));
bones.add(skeleton.bones.get(boneData.index).applied);
target = skeleton.bones.get(data.target.index);
target = skeleton.bones.get(data.target.index).applied;
applied = new IkConstraint(data, bones, target);
setToSetupPose();
}
@ -82,17 +91,18 @@ public class IkConstraint implements Updatable {
/** Applies the constraint to the constrained bones. */
public void update (Physics physics) {
if (mix == 0) return;
Bone target = this.target;
BoneApplied target = this.target;
Object[] bones = this.bones.items;
switch (this.bones.size) {
case 1 -> apply((Bone)bones[0], target.worldX, target.worldY, compress, stretch, data.uniform, mix);
case 1 -> apply((BoneApplied)bones[0], target.worldX, target.worldY, compress, stretch, data.uniform, mix);
case 2 -> //
apply((Bone)bones[0], (Bone)bones[1], target.worldX, target.worldY, bendDirection, stretch, data.uniform, softness, mix);
apply((BoneApplied)bones[0], (BoneApplied)bones[1], target.worldX, target.worldY, bendDirection, stretch, data.uniform,
softness, mix);
}
}
/** The bones that will be modified by this IK constraint. */
public Array<Bone> getBones () {
/** The 1 or 2 bones that will be modified by this IK constraint. */
public Array<BoneApplied> getBones () {
return bones;
}
@ -101,7 +111,7 @@ public class IkConstraint implements Updatable {
return target;
}
public void setTarget (Bone target) {
public void setTarget (BoneApplied target) {
if (target == null) throw new IllegalArgumentException("target cannot be null.");
this.target = target;
}
@ -171,12 +181,12 @@ public class IkConstraint implements Updatable {
}
/** Applies 1 bone IK. The target is specified in the world coordinate system. */
static public void apply (Bone bone, float targetX, float targetY, boolean compress, boolean stretch, boolean uniform,
static public void apply (BoneApplied bone, float targetX, float targetY, boolean compress, boolean stretch, boolean uniform,
float alpha) {
if (bone == null) throw new IllegalArgumentException("bone cannot be null.");
Bone p = bone.parent;
BoneApplied p = bone.parentApplied;
float pa = p.a, pb = p.b, pc = p.c, pd = p.d;
float rotationIK = -bone.ashearX - bone.arotation, tx, ty;
float rotationIK = -bone.shearX - bone.rotation, tx, ty;
switch (bone.inherit) {
case onlyTranslation:
tx = (targetX - bone.worldX) * Math.signum(bone.skeleton.scaleX);
@ -197,17 +207,17 @@ public class IkConstraint implements Updatable {
tx = 0;
ty = 0;
} else {
tx = (x * pd - y * pb) / d - bone.ax;
ty = (y * pa - x * pc) / d - bone.ay;
tx = (x * pd - y * pb) / d - bone.x;
ty = (y * pa - x * pc) / d - bone.y;
}
}
rotationIK += atan2Deg(ty, tx);
if (bone.ascaleX < 0) rotationIK += 180;
if (bone.scaleX < 0) rotationIK += 180;
if (rotationIK > 180)
rotationIK -= 360;
else if (rotationIK < -180) //
rotationIK += 360;
float sx = bone.ascaleX, sy = bone.ascaleY;
bone.rotation += rotationIK * alpha;
if (compress || stretch) {
switch (bone.inherit) {
case noScale, noScaleOrReflection -> {
@ -215,27 +225,27 @@ public class IkConstraint implements Updatable {
ty = targetY - bone.worldY;
}
}
float b = bone.data.length * sx;
float b = bone.data.length * bone.scaleX;
if (b > 0.0001f) {
float dd = tx * tx + ty * ty;
if ((compress && dd < b * b) || (stretch && dd > b * b)) {
float s = ((float)Math.sqrt(dd) / b - 1) * alpha + 1;
sx *= s;
if (uniform) sy *= s;
bone.scaleX *= s;
if (uniform) bone.scaleY *= s;
}
}
}
bone.updateWorldTransform(bone.ax, bone.ay, bone.arotation + rotationIK * alpha, sx, sy, bone.ashearX, bone.ashearY);
bone.updateWorldTransform();
}
/** Applies 2 bone IK. The target is specified in the world coordinate system.
* @param child A direct descendant of the parent bone. */
static public void apply (Bone parent, Bone child, float targetX, float targetY, int bendDir, boolean stretch, boolean uniform,
float softness, float alpha) {
static public void apply (BoneApplied parent, BoneApplied child, float targetX, float targetY, int bendDir, boolean stretch,
boolean uniform, float softness, float alpha) {
if (parent == null) throw new IllegalArgumentException("parent cannot be null.");
if (child == null) throw new IllegalArgumentException("child cannot be null.");
if (parent.inherit != Inherit.normal || child.inherit != Inherit.normal) return;
float px = parent.ax, py = parent.ay, psx = parent.ascaleX, psy = parent.ascaleY, sx = psx, sy = psy, csx = child.ascaleX;
float px = parent.x, py = parent.y, psx = parent.scaleX, psy = parent.scaleY, csx = child.scaleX;
int os1, os2, s2;
if (psx < 0) {
psx = -psx;
@ -254,18 +264,17 @@ public class IkConstraint implements Updatable {
os2 = 180;
} else
os2 = 0;
float cx = child.ax, cy, cwx, cwy, a = parent.a, b = parent.b, c = parent.c, d = parent.d;
float cwx, cwy, a = parent.a, b = parent.b, c = parent.c, d = parent.d;
boolean u = Math.abs(psx - psy) <= 0.0001f;
if (!u || stretch) {
cy = 0;
cwx = a * cx + parent.worldX;
cwy = c * cx + parent.worldY;
child.y = 0;
cwx = a * child.x + parent.worldX;
cwy = c * child.x + parent.worldY;
} else {
cy = child.ay;
cwx = a * cx + b * cy + parent.worldX;
cwy = c * cx + d * cy + parent.worldY;
cwx = a * child.x + b * child.y + parent.worldX;
cwy = c * child.x + d * child.y + parent.worldY;
}
Bone pp = parent.parent;
BoneApplied pp = parent.parentApplied;
a = pp.a;
b = pp.b;
c = pp.c;
@ -276,7 +285,8 @@ public class IkConstraint implements Updatable {
float l1 = (float)Math.sqrt(dx * dx + dy * dy), l2 = child.data.length * csx, a1, a2;
if (l1 < 0.0001f) {
apply(parent, targetX, targetY, false, stretch, false, alpha);
child.updateWorldTransform(cx, cy, 0, child.ascaleX, child.ascaleY, child.ashearX, child.ashearY);
child.rotation = 0;
child.updateWorldTransform();
return;
}
x = targetX - pp.worldX;
@ -306,8 +316,8 @@ public class IkConstraint implements Updatable {
a2 = 0;
if (stretch) {
a = ((float)Math.sqrt(dd) / (l1 + l2) - 1) * alpha + 1;
sx *= a;
if (uniform) sy *= a;
parent.scaleX *= a;
if (uniform) parent.scaleY *= a;
}
} else
a2 = (float)Math.acos(cos) * bendDir;
@ -364,20 +374,20 @@ public class IkConstraint implements Updatable {
a2 = maxAngle * bendDir;
}
}
float os = atan2(cy, cx) * s2;
float rotation = parent.arotation;
a1 = (a1 - os) * radDeg + os1 - rotation;
float os = atan2(child.y, child.x) * s2;
a1 = (a1 - os) * radDeg + os1 - parent.rotation;
if (a1 > 180)
a1 -= 360;
else if (a1 < -180) //
a1 += 360;
parent.updateWorldTransform(px, py, rotation + a1 * alpha, sx, sy, 0, 0);
rotation = child.arotation;
a2 = ((a2 + os) * radDeg - child.ashearX) * s2 + os2 - rotation;
parent.rotation += a1 * alpha;
parent.updateWorldTransform();
a2 = ((a2 + os) * radDeg - child.shearX) * s2 + os2 - child.rotation;
if (a2 > 180)
a2 -= 360;
else if (a2 < -180) //
a2 += 360;
child.updateWorldTransform(cx, cy, rotation + a2 * alpha, child.ascaleX, child.ascaleY, child.ashearX, child.ashearY);
child.rotation += a2 * alpha;
child.updateWorldTransform();
}
}

View File

@ -35,6 +35,7 @@ import java.util.Arrays;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.FloatArray;
import com.badlogic.gdx.utils.Null;
import com.esotericsoftware.spine.PathConstraintData.PositionMode;
import com.esotericsoftware.spine.PathConstraintData.RotateMode;
@ -51,16 +52,23 @@ public class PathConstraint implements Updatable {
static final float epsilon = 0.00001f;
final PathConstraintData data;
final Array<Bone> bones;
final Array<BoneApplied> bones;
Slot slot;
float position, spacing, mixRotate, mixX, mixY;
PathConstraint applied;
boolean active;
float position, spacing, mixRotate, mixX, mixY;
private final FloatArray spaces = new FloatArray(), positions = new FloatArray();
private final FloatArray world = new FloatArray(), curves = new FloatArray(), lengths = new FloatArray();
private final float[] segments = new float[10];
public PathConstraint (PathConstraintData data, Array<BoneApplied> bones, Slot slot) {
this.data = data;
this.bones = bones;
this.slot = slot;
}
public PathConstraint (PathConstraintData data, Skeleton skeleton) {
if (data == null) throw new IllegalArgumentException("data cannot be null.");
if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
@ -68,10 +76,12 @@ public class PathConstraint implements Updatable {
bones = new Array(data.bones.size);
for (BoneData boneData : data.bones)
bones.add(skeleton.bones.get(boneData.index));
bones.add(skeleton.bones.get(boneData.index).applied);
slot = skeleton.slots.get(data.slot.index);
applied = new PathConstraint(data, bones, slot);
setToSetupPose();
}
@ -108,7 +118,7 @@ public class PathConstraint implements Updatable {
case percent -> {
if (scale) {
for (int i = 0, n = spacesCount - 1; i < n; i++) {
var bone = (Bone)bones[i];
var bone = (BoneApplied)bones[i];
float setupLength = bone.data.length;
float x = setupLength * bone.a, y = setupLength * bone.c;
lengths[i] = (float)Math.sqrt(x * x + y * y);
@ -119,7 +129,7 @@ public class PathConstraint implements Updatable {
case proportional -> {
float sum = 0;
for (int i = 0, n = spacesCount - 1; i < n;) {
var bone = (Bone)bones[i];
var bone = (BoneApplied)bones[i];
float setupLength = bone.data.length;
if (setupLength < epsilon) {
if (scale) lengths[i] = 0;
@ -141,7 +151,7 @@ public class PathConstraint implements Updatable {
default -> {
boolean lengthSpacing = data.spacingMode == SpacingMode.length;
for (int i = 0, n = spacesCount - 1; i < n;) {
var bone = (Bone)bones[i];
var bone = (BoneApplied)bones[i];
float setupLength = bone.data.length;
if (setupLength < epsilon) {
if (scale) lengths[i] = 0;
@ -163,11 +173,11 @@ public class PathConstraint implements Updatable {
tip = data.rotateMode == RotateMode.chain;
else {
tip = false;
Bone p = slot.bone;
BoneApplied p = slot.bone.applied;
offsetRotation *= p.a * p.d - p.b * p.c > 0 ? degRad : -degRad;
}
for (int i = 0, p = 3; i < boneCount; i++, p += 3) {
var bone = (Bone)bones[i];
var bone = (BoneApplied)bones[i];
bone.worldX += (boneX - bone.worldX) * mixX;
bone.worldY += (boneY - bone.worldY) * mixY;
float x = positions[p], y = positions[p + 1], dx = x - boneX, dy = y - boneY;
@ -518,7 +528,7 @@ public class PathConstraint implements Updatable {
}
/** The bones that will be modified by this path constraint. */
public Array<Bone> getBones () {
public Array<BoneApplied> getBones () {
return bones;
}

View File

@ -38,7 +38,11 @@ import com.esotericsoftware.spine.Skeleton.Physics;
* See <a href="https://esotericsoftware.com/spine-physics-constraints">Physics constraints</a> in the Spine User Guide. */
public class PhysicsConstraint implements Updatable {
final PhysicsConstraintData data;
Bone bone;
final Skeleton skeleton;
BoneApplied bone;
PhysicsConstraint applied;
boolean active;
float inertia, strength, damping, massInverse, wind, gravity, mix;
boolean reset = true;
@ -47,19 +51,23 @@ public class PhysicsConstraint implements Updatable {
float yOffset, yVelocity;
float rotateOffset, rotateVelocity;
float scaleOffset, scaleVelocity;
boolean active;
final Skeleton skeleton;
float remaining, lastTime;
private PhysicsConstraint (PhysicsConstraintData data, Skeleton skeleton, BoneApplied bone) {
this.data = data;
this.skeleton = skeleton;
this.bone = bone;
}
public PhysicsConstraint (PhysicsConstraintData data, Skeleton skeleton) {
if (data == null) throw new IllegalArgumentException("data cannot be null.");
if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
this.data = data;
this.skeleton = skeleton;
bone = skeleton.bones.get(data.bone.index);
bone = skeleton.bones.get(data.bone.index).applied;
applied = new PhysicsConstraint(data, skeleton, bone);
setToSetupPose();
}
@ -118,7 +126,7 @@ public class PhysicsConstraint implements Updatable {
if (mix == 0) return;
boolean x = data.x > 0, y = data.y > 0, rotateOrShearX = data.rotate > 0 || data.shearX > 0, scaleX = data.scaleX > 0;
Bone bone = this.bone;
BoneApplied bone = this.bone;
float l = bone.data.length;
switch (physics) {
@ -278,11 +286,11 @@ public class PhysicsConstraint implements Updatable {
}
/** The bone constrained by this physics constraint. */
public Bone getBone () {
public BoneApplied getBone () {
return bone;
}
public void setBone (Bone bone) {
public void setBone (BoneApplied bone) {
this.bone = bone;
}

View File

@ -37,6 +37,8 @@ import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.FloatArray;
import com.badlogic.gdx.utils.Null;
import com.esotericsoftware.spine.Animation.BoneTimeline;
import com.esotericsoftware.spine.Animation.Timeline;
import com.esotericsoftware.spine.Skin.SkinEntry;
import com.esotericsoftware.spine.attachments.Attachment;
import com.esotericsoftware.spine.attachments.ClippingAttachment;
@ -55,6 +57,7 @@ public class Skeleton {
final Array<Bone> bones;
final Array<Slot> slots;
Array<Slot> drawOrder;
final Array<Slider> sliders;
final Array<IkConstraint> ikConstraints;
final Array<TransformConstraint> transformConstraints;
final Array<PathConstraint> pathConstraints;
@ -85,27 +88,30 @@ public class Skeleton {
slots = new Array(data.slots.size);
drawOrder = new Array(data.slots.size);
for (SlotData slotData : data.slots) {
var bone = (Bone)bones[slotData.boneData.index];
var slot = new Slot(slotData, bone);
var slot = new Slot(slotData, this);
slots.add(slot);
drawOrder.add(slot);
}
sliders = new Array(data.sliders.size);
for (SliderData constraint : data.sliders)
sliders.add(new Slider(constraint, this));
ikConstraints = new Array(data.ikConstraints.size);
for (IkConstraintData ikConstraintData : data.ikConstraints)
ikConstraints.add(new IkConstraint(ikConstraintData, this));
for (IkConstraintData constraint : data.ikConstraints)
ikConstraints.add(new IkConstraint(constraint, this));
transformConstraints = new Array(data.transformConstraints.size);
for (TransformConstraintData transformConstraintData : data.transformConstraints)
transformConstraints.add(new TransformConstraint(transformConstraintData, this));
for (TransformConstraintData constraint : data.transformConstraints)
transformConstraints.add(new TransformConstraint(constraint, this));
pathConstraints = new Array(data.pathConstraints.size);
for (PathConstraintData pathConstraintData : data.pathConstraints)
pathConstraints.add(new PathConstraint(pathConstraintData, this));
for (PathConstraintData constraint : data.pathConstraints)
pathConstraints.add(new PathConstraint(constraint, this));
physicsConstraints = new Array(data.physicsConstraints.size);
for (PhysicsConstraintData physicsConstraintData : data.physicsConstraints)
physicsConstraints.add(new PhysicsConstraint(physicsConstraintData, this));
for (PhysicsConstraintData constraint : data.physicsConstraints)
physicsConstraints.add(new PhysicsConstraint(constraint, this));
color = new Color(1, 1, 1, 1);
@ -140,21 +146,25 @@ public class Skeleton {
for (Slot slot : skeleton.drawOrder)
drawOrder.add(slots.get(slot.data.index));
sliders = new Array(skeleton.sliders.size);
for (Slider constraint : skeleton.sliders)
sliders.add(new Slider(constraint, skeleton));
ikConstraints = new Array(skeleton.ikConstraints.size);
for (IkConstraint ikConstraint : skeleton.ikConstraints)
ikConstraints.add(new IkConstraint(ikConstraint, skeleton));
for (IkConstraint constraint : skeleton.ikConstraints)
ikConstraints.add(new IkConstraint(constraint, skeleton));
transformConstraints = new Array(skeleton.transformConstraints.size);
for (TransformConstraint transformConstraint : skeleton.transformConstraints)
transformConstraints.add(new TransformConstraint(transformConstraint, skeleton));
for (TransformConstraint constraint : skeleton.transformConstraints)
transformConstraints.add(new TransformConstraint(constraint, skeleton));
pathConstraints = new Array(skeleton.pathConstraints.size);
for (PathConstraint pathConstraint : skeleton.pathConstraints)
pathConstraints.add(new PathConstraint(pathConstraint, skeleton));
for (PathConstraint constraint : skeleton.pathConstraints)
pathConstraints.add(new PathConstraint(constraint, skeleton));
physicsConstraints = new Array(skeleton.physicsConstraints.size);
for (PhysicsConstraint physicsConstraint : skeleton.physicsConstraints)
physicsConstraints.add(new PhysicsConstraint(physicsConstraint, skeleton));
for (PhysicsConstraint constraint : skeleton.physicsConstraints)
physicsConstraints.add(new PhysicsConstraint(constraint, skeleton));
skin = skeleton.skin;
color = new Color(skeleton.color);
@ -192,13 +202,21 @@ public class Skeleton {
}
}
int ikCount = ikConstraints.size, transformCount = transformConstraints.size, pathCount = pathConstraints.size,
physicsCount = physicsConstraints.size;
Object[] ikConstraints = this.ikConstraints.items, transformConstraints = this.transformConstraints.items,
pathConstraints = this.pathConstraints.items, physicsConstraints = this.physicsConstraints.items;
int sliderCount = sliders.size, ikCount = ikConstraints.size, transformCount = transformConstraints.size,
pathCount = pathConstraints.size, physicsCount = physicsConstraints.size;
Object[] sliders = this.sliders.items, ikConstraints = this.ikConstraints.items,
transformConstraints = this.transformConstraints.items, pathConstraints = this.pathConstraints.items,
physicsConstraints = this.physicsConstraints.items;
int constraintCount = ikCount + transformCount + pathCount + physicsCount;
outer:
for (int i = 0; i < constraintCount; i++) {
for (int ii = 0; ii < sliderCount; ii++) {
var constraint = (Slider)sliders[ii];
if (constraint.data.order == i) {
sortSlider(constraint);
continue outer;
}
}
for (int ii = 0; ii < ikCount; ii++) {
var constraint = (IkConstraint)ikConstraints[ii];
if (constraint.data.order == i) {
@ -233,21 +251,42 @@ public class Skeleton {
sortBone((Bone)bones[i]);
}
private void sortSlider (Slider constraint) {
constraint.active = !constraint.data.skinRequired || (skin != null && skin.constraints.contains(constraint.data, true));
if (!constraint.active) return;
Object[] bones = this.bones.items;
for (Timeline timeline : constraint.animation.timelines)
if (timeline instanceof BoneTimeline boneTimeline) sortBone((Bone)bones[boneTimeline.getBoneIndex()]);
updateCache.add(constraint);
for (Timeline timeline : constraint.animation.timelines) {
if (timeline instanceof BoneTimeline boneTimeline) {
var bone = (Bone)bones[boneTimeline.getBoneIndex()];
sortReset(bone.children);
bone.sorted = false;
}
}
for (Timeline timeline : constraint.animation.timelines)
if (timeline instanceof BoneTimeline boneTimeline) sortBone((Bone)bones[boneTimeline.getBoneIndex()]);
}
private void sortIkConstraint (IkConstraint constraint) {
constraint.active = constraint.target.active
&& (!constraint.data.skinRequired || (skin != null && skin.constraints.contains(constraint.data, true)));
if (!constraint.active) return;
sortBone(constraint.target);
sortBone(constraint.target.pose);
Array<Bone> constrained = constraint.bones;
Bone parent = constrained.first();
Array<BoneApplied> constrained = constraint.bones;
Bone parent = constrained.first().pose;
sortBone(parent);
if (constrained.size == 1) {
updateCache.add(constraint);
sortReset(parent.children);
} else {
Bone child = constrained.peek();
Bone child = constrained.peek().pose;
sortBone(child);
updateCache.add(constraint);
@ -262,27 +301,27 @@ public class Skeleton {
&& (!constraint.data.skinRequired || (skin != null && skin.constraints.contains(constraint.data, true)));
if (!constraint.active) return;
sortBone(constraint.source);
sortBone(constraint.source.pose);
Object[] constrained = constraint.bones.items;
int boneCount = constraint.bones.size;
if (constraint.data.localSource) {
for (int i = 0; i < boneCount; i++) {
var child = (Bone)constrained[i];
Bone child = ((BoneApplied)constrained[i]).pose;
sortBone(child.parent);
sortBone(child);
}
} else {
for (int i = 0; i < boneCount; i++)
sortBone((Bone)constrained[i]);
sortBone(((BoneApplied)constrained[i]).pose);
}
updateCache.add(constraint);
for (int i = 0; i < boneCount; i++)
sortReset(((Bone)constrained[i]).children);
sortReset(((BoneApplied)constrained[i]).children);
for (int i = 0; i < boneCount; i++)
((Bone)constrained[i]).sorted = true;
((BoneApplied)constrained[i]).pose.sorted = true;
}
private void sortPathConstraint (PathConstraint constraint) {
@ -302,7 +341,7 @@ public class Skeleton {
Object[] constrained = constraint.bones.items;
int boneCount = constraint.bones.size;
for (int i = 0; i < boneCount; i++)
sortBone((Bone)constrained[i]);
sortBone(((BoneApplied)constrained[i]).pose);
updateCache.add(constraint);
@ -331,13 +370,13 @@ public class Skeleton {
int nn = pathBones[i++];
nn += i;
while (i < nn)
sortBone((Bone)bones[pathBones[i++]]);
sortBone(((BoneApplied)bones[pathBones[i++]]).pose);
}
}
}
private void sortPhysicsConstraint (PhysicsConstraint constraint) {
Bone bone = constraint.bone;
Bone bone = constraint.bone.pose;
constraint.active = bone.active
&& (!constraint.data.skinRequired || (skin != null && skin.constraints.contains(constraint.data, true)));
if (!constraint.active) return;
@ -355,7 +394,7 @@ public class Skeleton {
Bone parent = bone.parent;
if (parent != null) sortBone(parent);
bone.sorted = true;
updateCache.add(bone);
updateCache.add(bone.applied);
}
private void sortReset (Array<Bone> bones) {
@ -376,14 +415,29 @@ public class Skeleton {
Object[] bones = this.bones.items;
for (int i = 0, n = this.bones.size; i < n; i++) {
var bone = (Bone)bones[i];
bone.ax = bone.x;
bone.ay = bone.y;
bone.arotation = bone.rotation;
bone.ascaleX = bone.scaleX;
bone.ascaleY = bone.scaleY;
bone.ashearX = bone.shearX;
bone.ashearY = bone.shearY;
if (!bone.active) continue;
BoneApplied applied = bone.applied;
applied.x = bone.x;
applied.y = bone.y;
applied.rotation = bone.rotation;
applied.scaleX = bone.scaleX;
applied.scaleY = bone.scaleY;
applied.shearX = bone.shearX;
applied.shearY = bone.shearY;
applied.inherit = bone.inherit;
}
Object[] slots = this.slots.items;
for (int i = 0, n = this.slots.size; i < n; i++) {
var slot = (Slot)slots[i];
if (!slot.bone.active) continue;
Slot applied = slot.applied;
applied.color.set(slot.color);
if (applied.darkColor != null) applied.darkColor.set(slot.darkColor);
applied.attachment = slot.attachment;
applied.sequenceIndex = slot.sequenceIndex;
applied.deform = slot.deform;
}
// BOZO! - Reset the rest.
Object[] updateCache = this.updateCache.items;
for (int i = 0, n = this.updateCache.size; i < n; i++)
@ -395,23 +449,26 @@ public class Skeleton {
* <p>
* See <a href="https://esotericsoftware.com/spine-runtime-skeletons#World-transforms">World transforms</a> in the Spine
* Runtimes Guide. */
public void updateWorldTransform (Physics physics, Bone parent) {
public void updateWorldTransform (Physics physics, BoneApplied parent) {
if (parent == null) throw new IllegalArgumentException("parent cannot be null.");
Object[] bones = this.bones.items;
for (int i = 1, n = this.bones.size; i < n; i++) { // Skip root bone.
var bone = (Bone)bones[i];
bone.ax = bone.x;
bone.ay = bone.y;
bone.arotation = bone.rotation;
bone.ascaleX = bone.scaleX;
bone.ascaleY = bone.scaleY;
bone.ashearX = bone.shearX;
bone.ashearY = bone.shearY;
BoneApplied applied = bone.applied;
applied.x = bone.x;
applied.y = bone.y;
applied.rotation = bone.rotation;
applied.scaleX = bone.scaleX;
applied.scaleY = bone.scaleY;
applied.shearX = bone.shearX;
applied.shearY = bone.shearY;
applied.inherit = bone.inherit;
}
// BOZO! - Reset the rest.
// Apply the parent bone transform to the root bone. The root bone always inherits scale, rotation and reflection.
Bone rootBone = getRootBone();
BoneApplied rootBone = getRootBone().applied;
float pa = parent.a, pb = parent.b, pc = parent.c, pd = parent.d;
rootBone.worldX = pa * x + pb * y + parent.worldX;
rootBone.worldY = pc * x + pd * y + parent.worldY;
@ -447,21 +504,25 @@ public class Skeleton {
for (int i = 0, n = this.bones.size; i < n; i++)
((Bone)bones[i]).setToSetupPose();
Object[] ikConstraints = this.ikConstraints.items;
for (int i = 0, n = this.ikConstraints.size; i < n; i++)
((IkConstraint)ikConstraints[i]).setToSetupPose();
Object[] constraints = sliders.items;
for (int i = 0, n = sliders.size; i < n; i++)
((Slider)constraints[i]).setToSetupPose();
Object[] transformConstraints = this.transformConstraints.items;
for (int i = 0, n = this.transformConstraints.size; i < n; i++)
((TransformConstraint)transformConstraints[i]).setToSetupPose();
constraints = ikConstraints.items;
for (int i = 0, n = ikConstraints.size; i < n; i++)
((IkConstraint)constraints[i]).setToSetupPose();
Object[] pathConstraints = this.pathConstraints.items;
for (int i = 0, n = this.pathConstraints.size; i < n; i++)
((PathConstraint)pathConstraints[i]).setToSetupPose();
constraints = transformConstraints.items;
for (int i = 0, n = transformConstraints.size; i < n; i++)
((TransformConstraint)constraints[i]).setToSetupPose();
Object[] physicsConstraints = this.physicsConstraints.items;
for (int i = 0, n = this.physicsConstraints.size; i < n; i++)
((PhysicsConstraint)physicsConstraints[i]).setToSetupPose();
constraints = pathConstraints.items;
for (int i = 0, n = pathConstraints.size; i < n; i++)
((PathConstraint)constraints[i]).setToSetupPose();
constraints = physicsConstraints.items;
for (int i = 0, n = physicsConstraints.size; i < n; i++)
((PhysicsConstraint)constraints[i]).setToSetupPose();
}
/** Sets the slots and draw order to their setup pose values. */
@ -617,6 +678,23 @@ public class Skeleton {
slot.setAttachment(attachment);
}
/** The skeleton's sliders. */
public Array<Slider> getSliders () {
return sliders;
}
/** Finds a slider by comparing each slider's name. It is more efficient to cache the results of this method than to call it
* repeatedly. */
public @Null Slider findSlider (String constraintName) {
if (constraintName == null) throw new IllegalArgumentException("constraintName cannot be null.");
Object[] sliders = this.sliders.items;
for (int i = 0, n = this.sliders.size; i < n; i++) {
var slider = (Slider)sliders[i];
if (slider.data.name.equals(constraintName)) return slider;
}
return null;
}
/** The skeleton's IK constraints. */
public Array<IkConstraint> getIkConstraints () {
return ikConstraints;

View File

@ -44,6 +44,7 @@ public class SkeletonData {
@Null Skin defaultSkin;
final Array<EventData> events = new Array();
final Array<Animation> animations = new Array();
final Array<SliderData> sliders = new Array();
final Array<IkConstraintData> ikConstraints = new Array();
final Array<TransformConstraintData> transformConstraints = new Array();
final Array<PathConstraintData> pathConstraints = new Array();
@ -159,6 +160,25 @@ public class SkeletonData {
return null;
}
// --- Sliders
/** The skeleton's sliders. */
public Array<SliderData> getSliders () {
return sliders;
}
/** Finds a slider by comparing each IK constraint's name. It is more efficient to cache the results of this method than to
* call it multiple times. */
public @Null SliderData findSlider (String constraintName) {
if (constraintName == null) throw new IllegalArgumentException("constraintName cannot be null.");
Object[] sliders = this.sliders.items;
for (int i = 0, n = this.sliders.size; i < n; i++) {
var constraint = (SliderData)sliders[i];
if (constraint.name.equals(constraintName)) return constraint;
}
return null;
}
// --- IK constraints
/** The skeleton's IK constraints. */

View File

@ -80,8 +80,8 @@ public class SkeletonRendererDebug {
Gdx.gl.glBlendFunc(srcFunc, GL20.GL_ONE_MINUS_SRC_ALPHA);
ShapeRenderer shapes = this.shapes;
Array<Bone> bones = skeleton.getBones();
Array<Slot> slots = skeleton.getSlots();
Array<Bone> bones = skeleton.bones;
Array<Slot> slots = skeleton.slots;
shapes.begin(ShapeType.Filled);
@ -96,11 +96,12 @@ public class SkeletonRendererDebug {
shapes.setColor(boneOriginColor);
} else
shapes.setColor(boneLineColor);
float x = length * bone.a + bone.worldX;
float y = length * bone.c + bone.worldY;
shapes.rectLine(bone.worldX, bone.worldY, x, y, width * scale);
BoneApplied applied = bone.applied;
float x = length * applied.a + applied.worldX;
float y = length * applied.c + applied.worldY;
shapes.rectLine(applied.worldX, applied.worldY, x, y, width * scale);
}
shapes.x(skeleton.getX(), skeleton.getY(), 4 * scale);
shapes.x(skeleton.x, skeleton.y, 4 * scale);
}
if (drawPoints) {
@ -110,8 +111,8 @@ public class SkeletonRendererDebug {
if (!slot.bone.active) continue;
Attachment attachment = slot.attachment;
if (!(attachment instanceof PointAttachment point)) continue;
point.computeWorldPosition(slot.getBone(), temp1);
temp2.set(8, 0).rotate(point.computeWorldRotation(slot.getBone()));
point.computeWorldPosition(slot.bone.applied, temp1);
temp2.set(8, 0).rotate(point.computeWorldRotation(slot.bone.applied));
shapes.rectLine(temp1, temp2, boneWidth / 2 * scale);
}
}
@ -244,7 +245,7 @@ public class SkeletonRendererDebug {
for (int i = 0, n = bones.size; i < n; i++) {
Bone bone = bones.get(i);
if (!bone.active) continue;
shapes.circle(bone.worldX, bone.worldY, 3 * scale, 8);
shapes.circle(bone.applied.worldX, bone.applied.worldY, 3 * scale, 8);
}
}
@ -255,7 +256,7 @@ public class SkeletonRendererDebug {
if (!slot.bone.active) continue;
Attachment attachment = slot.attachment;
if (!(attachment instanceof PointAttachment point)) continue;
point.computeWorldPosition(slot.getBone(), temp1);
point.computeWorldPosition(slot.bone.applied, temp1);
shapes.circle(temp1.x, temp1.y, 3 * scale, 8);
}
}

View File

@ -0,0 +1,100 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, Esoteric Software LLC
*
* Integration of the Spine Runtimes into software or otherwise creating
* derivative works of the Spine Runtimes is permitted under the terms and
* conditions of Section 2 of the Spine Editor License Agreement:
* http://esotericsoftware.com/spine-editor-license
*
* Otherwise, it is permitted to integrate the Spine Runtimes into software
* or otherwise create derivative works of the Spine Runtimes (collectively,
* "Products"), provided that each user of the Products must obtain their own
* Spine Editor license and redistribution of the Products in any form must
* include this license and copyright notice.
*
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
package com.esotericsoftware.spine;
import com.esotericsoftware.spine.Animation.MixBlend;
import com.esotericsoftware.spine.Animation.MixDirection;
import com.esotericsoftware.spine.Skeleton.Physics;
/** Stores the setup pose for a {@link PhysicsConstraint}.
* <p>
* See <a href="https://esotericsoftware.com/spine-physics-constraints">Physics constraints</a> in the Spine User Guide. */
public class Slider implements Updatable {
final SliderData data;
final Skeleton skeleton;
Animation animation;
float time, mix;
boolean active;
public Slider (SliderData data, Skeleton skeleton) {
if (data == null) throw new IllegalArgumentException("data cannot be null.");
if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
this.data = data;
this.skeleton = skeleton;
setToSetupPose();
}
/** Copy constructor. */
public Slider (Slider slider, Skeleton skeleton) {
this(slider.data, skeleton);
setToSetupPose();
}
public void update (Physics physics) {
animation.apply(skeleton, time, time, false, null, mix, MixBlend.replace, MixDirection.in, true);
}
public void setToSetupPose () {
SliderData data = this.data;
animation = data.animation;
time = data.time;
mix = data.mix;
}
public boolean isActive () {
return true;
}
public Animation getAnimation () {
return animation;
}
public void setAnimation (Animation animation) {
this.animation = animation;
}
public float getTime () {
return time;
}
public void setTime (float time) {
this.time = time;
}
public float getMix () {
return mix;
}
public void setMix (float mix) {
this.mix = mix;
}
}

View File

@ -0,0 +1,66 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, Esoteric Software LLC
*
* Integration of the Spine Runtimes into software or otherwise creating
* derivative works of the Spine Runtimes is permitted under the terms and
* conditions of Section 2 of the Spine Editor License Agreement:
* http://esotericsoftware.com/spine-editor-license
*
* Otherwise, it is permitted to integrate the Spine Runtimes into software
* or otherwise create derivative works of the Spine Runtimes (collectively,
* "Products"), provided that each user of the Products must obtain their own
* Spine Editor license and redistribution of the Products in any form must
* include this license and copyright notice.
*
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
package com.esotericsoftware.spine;
/** Stores the setup pose for a {@link PhysicsConstraint}.
* <p>
* See <a href="https://esotericsoftware.com/spine-physics-constraints">Physics constraints</a> in the Spine User Guide. */
public class SliderData extends ConstraintData {
Animation animation;
float time, mix;
public SliderData (String name) {
super(name);
}
public Animation getAnimation () {
return animation;
}
public void setAnimation (Animation animation) {
this.animation = animation;
}
public float getTime () {
return time;
}
public void setTime (float time) {
this.time = time;
}
public float getMix () {
return mix;
}
public void setMix (float mix) {
this.mix = mix;
}
}

View File

@ -44,20 +44,34 @@ import com.esotericsoftware.spine.attachments.VertexAttachment;
public class Slot {
final SlotData data;
final Bone bone;
Color color = new Color();
Slot applied;
final Color color = new Color();
@Null final Color darkColor;
@Null Attachment attachment;
int sequenceIndex;
FloatArray deform = new FloatArray();
FloatArray deform;
int attachmentState;
public Slot (SlotData data, Bone bone) {
if (data == null) throw new IllegalArgumentException("data cannot be null.");
if (bone == null) throw new IllegalArgumentException("bone cannot be null.");
private Slot (SlotData data, Bone bone) {
this.data = data;
this.bone = bone;
darkColor = data.darkColor == null ? null : new Color();
}
public Slot (SlotData data, Skeleton skeleton) {
if (data == null) throw new IllegalArgumentException("data cannot be null.");
if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
this.data = data;
this.bone = skeleton.bones.get(data.boneData.index);
darkColor = data.darkColor == null ? null : new Color();
deform = new FloatArray();
applied = new Slot(data, bone);
setToSetupPose();
}
@ -71,7 +85,7 @@ public class Slot {
darkColor = slot.darkColor == null ? null : new Color(slot.darkColor);
attachment = slot.attachment;
sequenceIndex = slot.sequenceIndex;
deform.addAll(slot.deform);
deform = new FloatArray(slot.deform);
}
/** The slot's setup pose data. */
@ -95,10 +109,6 @@ public class Slot {
return color;
}
public void setColor (Color color) {
this.color = color;
}
/** The dark color used to tint the slot's attachment for two color tinting, or null if two color tinting is not used. The dark
* color's alpha is not used. */
public @Null Color getDarkColor () {
@ -158,6 +168,11 @@ public class Slot {
}
}
/** Returns the bone for applied pose. */
public Slot getApplied () {
return applied;
}
public String toString () {
return data.name;
}

View File

@ -31,8 +31,8 @@ package com.esotericsoftware.spine;
import static com.badlogic.gdx.math.MathUtils.*;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.Null;
import com.esotericsoftware.spine.Skeleton.Physics;
import com.esotericsoftware.spine.TransformConstraintData.FromProperty;
@ -44,12 +44,18 @@ import com.esotericsoftware.spine.TransformConstraintData.ToProperty;
* See <a href="https://esotericsoftware.com/spine-transform-constraints">Transform constraints</a> in the Spine User Guide. */
public class TransformConstraint implements Updatable {
final TransformConstraintData data;
final Array<Bone> bones;
Bone source;
final Array<BoneApplied> bones;
BoneApplied source;
TransformConstraint applied;
boolean active;
float mixRotate, mixX, mixY, mixScaleX, mixScaleY, mixShearY;
boolean active;
final Vector2 temp = new Vector2();
public TransformConstraint (TransformConstraintData data, Array<BoneApplied> bones, BoneApplied source) {
this.data = data;
this.bones = bones;
this.source = source;
}
public TransformConstraint (TransformConstraintData data, Skeleton skeleton) {
if (data == null) throw new IllegalArgumentException("data cannot be null.");
@ -58,9 +64,11 @@ public class TransformConstraint implements Updatable {
bones = new Array(data.bones.size);
for (BoneData boneData : data.bones)
bones.add(skeleton.bones.get(boneData.index));
bones.add(skeleton.bones.get(boneData.index).applied);
source = skeleton.bones.get(data.source.index);
source = skeleton.bones.get(data.source.index).applied;
applied = new TransformConstraint(data, bones, source);
setToSetupPose();
}
@ -87,12 +95,12 @@ public class TransformConstraint implements Updatable {
TransformConstraintData data = this.data;
boolean localFrom = data.localSource, localTarget = data.localTarget, additive = data.additive, clamp = data.clamp;
Bone source = this.source;
BoneApplied source = this.source;
Object[] fromItems = data.properties.items;
int fn = data.properties.size;
Object[] bones = this.bones.items;
for (int i = 0, n = this.bones.size; i < n; i++) {
var bone = (Bone)bones[i];
var bone = (BoneApplied)bones[i];
for (int f = 0; f < fn; f++) {
var from = (FromProperty)fromItems[f];
float value = from.value(data, source, localFrom) - from.offset;
@ -119,16 +127,16 @@ public class TransformConstraint implements Updatable {
}
/** The bones that will be modified by this transform constraint. */
public Array<Bone> getBones () {
public Array<BoneApplied> getBones () {
return bones;
}
/** The bone whose world transform will be copied to the constrained bones. */
public Bone getSource () {
public BoneApplied getSource () {
return source;
}
public void setSource (Bone source) {
public void setSource (BoneApplied source) {
if (source == null) throw new IllegalArgumentException("source cannot be null.");
this.source = source;
}

View File

@ -221,7 +221,7 @@ public class TransformConstraintData extends ConstraintData {
public final Array<ToProperty> to = new Array();
/** Reads this property from the specified bone. */
abstract public float value (TransformConstraintData data, Bone source, boolean local);
abstract public float value (TransformConstraintData data, BoneApplied source, boolean local);
}
/** Constrained property for a {@link TransformConstraint}. */
@ -239,12 +239,12 @@ public class TransformConstraintData extends ConstraintData {
abstract public float mix (TransformConstraint constraint);
/** Applies the value to this property. */
abstract public void apply (TransformConstraint constraint, Bone bone, float value, boolean local, boolean additive);
abstract public void apply (TransformConstraint constraint, BoneApplied bone, float value, boolean local, boolean additive);
}
static public class FromRotate extends FromProperty {
public float value (TransformConstraintData data, Bone source, boolean local) {
if (local) return source.arotation + data.offsetRotation;
public float value (TransformConstraintData data, BoneApplied source, boolean local) {
if (local) return source.rotation + data.offsetRotation;
float value = atan2(source.c, source.a) * radDeg
+ (source.a * source.d - source.b * source.c > 0 ? data.offsetRotation : -data.offsetRotation);
if (value < 0) value += 360;
@ -257,10 +257,10 @@ public class TransformConstraintData extends ConstraintData {
return constraint.mixRotate;
}
public void apply (TransformConstraint constraint, Bone bone, float value, boolean local, boolean additive) {
public void apply (TransformConstraint constraint, BoneApplied bone, float value, boolean local, boolean additive) {
if (local) {
if (!additive) value -= bone.arotation;
bone.arotation += value * constraint.mixRotate;
if (!additive) value -= bone.rotation;
bone.rotation += value * constraint.mixRotate;
} else {
float a = bone.a, b = bone.b, c = bone.c, d = bone.d;
value *= degRad;
@ -280,8 +280,8 @@ public class TransformConstraintData extends ConstraintData {
}
static public class FromX extends FromProperty {
public float value (TransformConstraintData data, Bone source, boolean local) {
return local ? source.ax + data.offsetX : data.offsetX * source.a + data.offsetY * source.b + source.worldX;
public float value (TransformConstraintData data, BoneApplied source, boolean local) {
return local ? source.x + data.offsetX : data.offsetX * source.a + data.offsetY * source.b + source.worldX;
}
}
@ -290,10 +290,10 @@ public class TransformConstraintData extends ConstraintData {
return constraint.mixX;
}
public void apply (TransformConstraint constraint, Bone bone, float value, boolean local, boolean additive) {
public void apply (TransformConstraint constraint, BoneApplied bone, float value, boolean local, boolean additive) {
if (local) {
if (!additive) value -= bone.ax;
bone.ax += value * constraint.mixX;
if (!additive) value -= bone.x;
bone.x += value * constraint.mixX;
} else {
if (!additive) value -= bone.worldX;
bone.worldX += value * constraint.mixX;
@ -302,8 +302,8 @@ public class TransformConstraintData extends ConstraintData {
}
static public class FromY extends FromProperty {
public float value (TransformConstraintData data, Bone source, boolean local) {
return local ? source.ay + data.offsetY : data.offsetX * source.c + data.offsetY * source.d + source.worldY;
public float value (TransformConstraintData data, BoneApplied source, boolean local) {
return local ? source.y + data.offsetY : data.offsetX * source.c + data.offsetY * source.d + source.worldY;
}
}
@ -312,10 +312,10 @@ public class TransformConstraintData extends ConstraintData {
return constraint.mixY;
}
public void apply (TransformConstraint constraint, Bone bone, float value, boolean local, boolean additive) {
public void apply (TransformConstraint constraint, BoneApplied bone, float value, boolean local, boolean additive) {
if (local) {
if (!additive) value -= bone.ay;
bone.ay += value * constraint.mixY;
if (!additive) value -= bone.y;
bone.y += value * constraint.mixY;
} else {
if (!additive) value -= bone.worldY;
bone.worldY += value * constraint.mixY;
@ -324,8 +324,8 @@ public class TransformConstraintData extends ConstraintData {
}
static public class FromScaleX extends FromProperty {
public float value (TransformConstraintData data, Bone source, boolean local) {
return (local ? source.ascaleX : (float)Math.sqrt(source.a * source.a + source.c * source.c)) + data.offsetScaleX;
public float value (TransformConstraintData data, BoneApplied source, boolean local) {
return (local ? source.scaleX : (float)Math.sqrt(source.a * source.a + source.c * source.c)) + data.offsetScaleX;
}
}
@ -334,12 +334,12 @@ public class TransformConstraintData extends ConstraintData {
return constraint.mixScaleX;
}
public void apply (TransformConstraint constraint, Bone bone, float value, boolean local, boolean additive) {
public void apply (TransformConstraint constraint, BoneApplied bone, float value, boolean local, boolean additive) {
if (local) {
if (additive)
bone.ascaleX *= 1 + ((value - 1) * constraint.mixScaleX);
else if (bone.ascaleX != 0) //
bone.ascaleX = 1 + (value / bone.ascaleX - 1) * constraint.mixScaleX;
bone.scaleX *= 1 + ((value - 1) * constraint.mixScaleX);
else if (bone.scaleX != 0) //
bone.scaleX = 1 + (value / bone.scaleX - 1) * constraint.mixScaleX;
} else {
float s;
if (additive)
@ -355,8 +355,8 @@ public class TransformConstraintData extends ConstraintData {
}
static public class FromScaleY extends FromProperty {
public float value (TransformConstraintData data, Bone source, boolean local) {
return (local ? source.ascaleY : (float)Math.sqrt(source.b * source.b + source.d * source.d)) + data.offsetScaleY;
public float value (TransformConstraintData data, BoneApplied source, boolean local) {
return (local ? source.scaleY : (float)Math.sqrt(source.b * source.b + source.d * source.d)) + data.offsetScaleY;
}
}
@ -365,12 +365,12 @@ public class TransformConstraintData extends ConstraintData {
return constraint.mixScaleY;
}
public void apply (TransformConstraint constraint, Bone bone, float value, boolean local, boolean additive) {
public void apply (TransformConstraint constraint, BoneApplied bone, float value, boolean local, boolean additive) {
if (local) {
if (additive)
bone.ascaleY *= 1 + ((value - 1) * constraint.mixScaleY);
else if (bone.ascaleY != 0) //
bone.ascaleY = 1 + (value / bone.ascaleY - 1) * constraint.mixScaleY;
bone.scaleY *= 1 + ((value - 1) * constraint.mixScaleY);
else if (bone.scaleY != 0) //
bone.scaleY = 1 + (value / bone.scaleY - 1) * constraint.mixScaleY;
} else {
float s;
if (additive)
@ -386,8 +386,8 @@ public class TransformConstraintData extends ConstraintData {
}
static public class FromShearY extends FromProperty {
public float value (TransformConstraintData data, Bone source, boolean local) {
return (local ? source.ashearY : (atan2(source.d, source.b) - atan2(source.c, source.a)) * radDeg - 90)
public float value (TransformConstraintData data, BoneApplied source, boolean local) {
return (local ? source.shearY : (atan2(source.d, source.b) - atan2(source.c, source.a)) * radDeg - 90)
+ data.offsetShearY;
}
}
@ -397,10 +397,10 @@ public class TransformConstraintData extends ConstraintData {
return constraint.mixShearY;
}
public void apply (TransformConstraint constraint, Bone bone, float value, boolean local, boolean additive) {
public void apply (TransformConstraint constraint, BoneApplied bone, float value, boolean local, boolean additive) {
if (local) {
if (!additive) value -= bone.ashearY;
bone.ashearY += value * constraint.mixShearY;
if (!additive) value -= bone.shearY;
bone.shearY += value * constraint.mixShearY;
} else {
float b = bone.b, d = bone.d, by = atan2(d, b);
value = (value + 90) * degRad;

View File

@ -34,7 +34,7 @@ import static com.esotericsoftware.spine.utils.SpineUtils.*;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.math.Vector2;
import com.esotericsoftware.spine.Bone;
import com.esotericsoftware.spine.BoneApplied;
/** An attachment which is a single point and a rotation. This can be used to spawn projectiles, particles, etc. A bone can be
* used in similar ways, but a PointAttachment is slightly less expensive to compute and can be hidden, shown, and placed in a
@ -90,13 +90,13 @@ public class PointAttachment extends Attachment {
return color;
}
public Vector2 computeWorldPosition (Bone bone, Vector2 point) {
public Vector2 computeWorldPosition (BoneApplied bone, Vector2 point) {
point.x = x * bone.getA() + y * bone.getB() + bone.getWorldX();
point.y = x * bone.getC() + y * bone.getD() + bone.getWorldY();
return point;
}
public float computeWorldRotation (Bone bone) {
public float computeWorldRotation (BoneApplied bone) {
float r = rotation * degRad, cos = cos(r), sin = sin(r);
float x = cos * bone.getA() + sin * bone.getB();
float y = cos * bone.getC() + sin * bone.getD();

View File

@ -37,6 +37,7 @@ import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.utils.Null;
import com.esotericsoftware.spine.Bone;
import com.esotericsoftware.spine.BoneApplied;
import com.esotericsoftware.spine.Slot;
/** An attachment that displays a textured quadrilateral.
@ -179,7 +180,7 @@ public class RegionAttachment extends Attachment implements HasTextureRegion {
if (sequence != null) sequence.apply(slot, this);
float[] vertexOffset = this.offset;
Bone bone = slot.getBone();
BoneApplied bone = slot.getBone().getApplied();
float x = bone.getWorldX(), y = bone.getWorldY();
float a = bone.getA(), b = bone.getB(), c = bone.getC(), d = bone.getD();
float offsetX, offsetY;

View File

@ -35,6 +35,7 @@ import com.badlogic.gdx.utils.FloatArray;
import com.badlogic.gdx.utils.Null;
import com.esotericsoftware.spine.Bone;
import com.esotericsoftware.spine.BoneApplied;
import com.esotericsoftware.spine.Skeleton;
import com.esotericsoftware.spine.Slot;
@ -91,7 +92,7 @@ abstract public class VertexAttachment extends Attachment {
int[] bones = this.bones;
if (bones == null) {
if (deformArray.size > 0) vertices = deformArray.items;
Bone bone = slot.getBone();
BoneApplied bone = slot.getBone().getApplied();
float x = bone.getWorldX(), y = bone.getWorldY();
float a = bone.getA(), b = bone.getB(), c = bone.getC(), d = bone.getD();
for (int v = start, w = offset; w < count; v += 2, w += stride) {
@ -114,7 +115,7 @@ abstract public class VertexAttachment extends Attachment {
int n = bones[v++];
n += v;
for (; v < n; v++, b += 3) {
var bone = (Bone)skeletonBones[bones[v]];
BoneApplied bone = ((Bone)skeletonBones[bones[v]]).getApplied();
float vx = vertices[b], vy = vertices[b + 1], weight = vertices[b + 2];
wx += (vx * bone.getA() + vy * bone.getB() + bone.getWorldX()) * weight;
wy += (vx * bone.getC() + vy * bone.getD() + bone.getWorldY()) * weight;
@ -129,7 +130,7 @@ abstract public class VertexAttachment extends Attachment {
int n = bones[v++];
n += v;
for (; v < n; v++, b += 3, f += 2) {
var bone = (Bone)skeletonBones[bones[v]];
BoneApplied bone = ((Bone)skeletonBones[bones[v]]).getApplied();
float vx = vertices[b] + deform[f], vy = vertices[b + 1] + deform[f + 1], weight = vertices[b + 2];
wx += (vx * bone.getA() + vy * bone.getB() + bone.getWorldX()) * weight;
wy += (vx * bone.getC() + vy * bone.getD() + bone.getWorldY()) * weight;

View File

@ -41,7 +41,7 @@ public class SkeletonClipping {
private final FloatArray clippingPolygon = new FloatArray();
private final FloatArray clipOutput = new FloatArray(128);
private final FloatArray clippedVertices = new FloatArray(128);
private final FloatArray clippedUvs = new FloatArray(128);
private final FloatArray clippedUvs = new FloatArray(0);
private final ShortArray clippedTriangles = new ShortArray(128);
private final FloatArray scratch = new FloatArray();
@ -459,7 +459,7 @@ public class SkeletonClipping {
return clippedVertices;
}
/** Only returns a non-empty array if clipTrianglesUnpacked() was used **/
/** Returns an empty array unless {@link #clipTrianglesUnpacked(float[], int, short[], int, float[])} was used. **/
public FloatArray getClippedUvs () {
return clippedUvs;
}