[libgdx] Added IkConstraintPose and data classes use a setup pose field.

This commit is contained in:
Nathan Sweet 2025-04-15 14:39:03 -04:00
parent cab5944663
commit 08af7407b3
14 changed files with 288 additions and 289 deletions

View File

@ -563,7 +563,7 @@ public class Animation {
Bone bone = skeleton.bones.get(boneIndex);
if (bone.active) {
BonePose pose = appliedPose ? bone.applied : bone.pose;
pose.rotation = getRelativeValue(time, alpha, blend, pose.rotation, bone.data.rotation);
pose.rotation = getRelativeValue(time, alpha, blend, pose.rotation, bone.data.setup.rotation);
}
}
}
@ -588,18 +588,18 @@ public class Animation {
Bone bone = skeleton.bones.get(boneIndex);
if (!bone.active) return;
BonePose pose = appliedPose ? bone.applied : bone.pose;
BonePose pose = appliedPose ? bone.applied : bone.pose, setup = bone.data.setup;
float[] frames = this.frames;
if (time < frames[0]) {
switch (blend) {
case setup:
pose.x = bone.data.x;
pose.y = bone.data.y;
pose.x = setup.x;
pose.y = setup.y;
return;
case first:
pose.x += (bone.data.x - pose.x) * alpha;
pose.y += (bone.data.y - pose.y) * alpha;
pose.x += (setup.x - pose.x) * alpha;
pose.y += (setup.y - pose.y) * alpha;
}
return;
}
@ -626,13 +626,13 @@ public class Animation {
switch (blend) {
case setup:
pose.x = bone.data.x + x * alpha;
pose.y = bone.data.y + y * alpha;
pose.x = setup.x + x * alpha;
pose.y = setup.y + y * alpha;
break;
case first:
case replace:
pose.x += (bone.data.x + x - pose.x) * alpha;
pose.y += (bone.data.y + y - pose.y) * alpha;
pose.x += (setup.x + x - pose.x) * alpha;
pose.y += (setup.y + y - pose.y) * alpha;
break;
case add:
pose.x += x * alpha;
@ -660,7 +660,7 @@ public class Animation {
Bone bone = skeleton.bones.get(boneIndex);
if (bone.active) {
BonePose pose = appliedPose ? bone.applied : bone.pose;
pose.x = getRelativeValue(time, alpha, blend, pose.x, bone.data.x);
pose.x = getRelativeValue(time, alpha, blend, pose.x, bone.data.setup.x);
}
}
}
@ -684,7 +684,7 @@ public class Animation {
Bone bone = skeleton.bones.get(boneIndex);
if (bone.active) {
BonePose pose = appliedPose ? bone.applied : bone.pose;
pose.y = getRelativeValue(time, alpha, blend, pose.y, bone.data.y);
pose.y = getRelativeValue(time, alpha, blend, pose.y, bone.data.setup.y);
}
}
}
@ -709,18 +709,18 @@ public class Animation {
Bone bone = skeleton.bones.get(boneIndex);
if (!bone.active) return;
BonePose pose = appliedPose ? bone.applied : bone.pose;
BonePose pose = appliedPose ? bone.applied : bone.pose, setup = bone.data.setup;
float[] frames = this.frames;
if (time < frames[0]) {
switch (blend) {
case setup:
pose.scaleX = bone.data.scaleX;
pose.scaleY = bone.data.scaleY;
pose.scaleX = setup.scaleX;
pose.scaleY = setup.scaleY;
return;
case first:
pose.scaleX += (bone.data.scaleX - pose.scaleX) * alpha;
pose.scaleY += (bone.data.scaleY - pose.scaleY) * alpha;
pose.scaleX += (setup.scaleX - pose.scaleX) * alpha;
pose.scaleY += (setup.scaleY - pose.scaleY) * alpha;
}
return;
}
@ -744,13 +744,13 @@ public class Animation {
x = getBezierValue(time, i, VALUE1, curveType - BEZIER);
y = getBezierValue(time, i, VALUE2, curveType + BEZIER_SIZE - BEZIER);
}
x *= bone.data.scaleX;
y *= bone.data.scaleY;
x *= setup.scaleX;
y *= setup.scaleY;
if (alpha == 1) {
if (blend == add) {
pose.scaleX += x - bone.data.scaleX;
pose.scaleY += y - bone.data.scaleY;
pose.scaleX += x - setup.scaleX;
pose.scaleY += y - setup.scaleY;
} else {
pose.scaleX = x;
pose.scaleY = y;
@ -761,8 +761,8 @@ public class Animation {
if (direction == out) {
switch (blend) {
case setup:
bx = bone.data.scaleX;
by = bone.data.scaleY;
bx = setup.scaleX;
by = setup.scaleY;
pose.scaleX = bx + (Math.abs(x) * Math.signum(bx) - bx) * alpha;
pose.scaleY = by + (Math.abs(y) * Math.signum(by) - by) * alpha;
break;
@ -774,14 +774,14 @@ public class Animation {
pose.scaleY = by + (Math.abs(y) * Math.signum(by) - by) * alpha;
break;
case add:
pose.scaleX += (x - bone.data.scaleX) * alpha;
pose.scaleY += (y - bone.data.scaleY) * alpha;
pose.scaleX += (x - setup.scaleX) * alpha;
pose.scaleY += (y - setup.scaleY) * alpha;
}
} else {
switch (blend) {
case setup:
bx = Math.abs(bone.data.scaleX) * Math.signum(x);
by = Math.abs(bone.data.scaleY) * Math.signum(y);
bx = Math.abs(setup.scaleX) * Math.signum(x);
by = Math.abs(setup.scaleY) * Math.signum(y);
pose.scaleX = bx + (x - bx) * alpha;
pose.scaleY = by + (y - by) * alpha;
break;
@ -793,8 +793,8 @@ public class Animation {
pose.scaleY = by + (y - by) * alpha;
break;
case add:
pose.scaleX += (x - bone.data.scaleX) * alpha;
pose.scaleY += (y - bone.data.scaleY) * alpha;
pose.scaleX += (x - setup.scaleX) * alpha;
pose.scaleY += (y - setup.scaleY) * alpha;
}
}
}
@ -820,7 +820,7 @@ public class Animation {
Bone bone = skeleton.bones.get(boneIndex);
if (bone.active) {
BonePose pose = appliedPose ? bone.applied : bone.pose;
pose.scaleX = getScaleValue(time, alpha, blend, direction, pose.scaleX, bone.data.scaleX);
pose.scaleX = getScaleValue(time, alpha, blend, direction, pose.scaleX, bone.data.setup.scaleX);
}
}
}
@ -844,7 +844,7 @@ public class Animation {
Bone bone = skeleton.bones.get(boneIndex);
if (bone.active) {
BonePose pose = appliedPose ? bone.applied : bone.pose;
pose.scaleY = getScaleValue(time, alpha, blend, direction, pose.scaleY, bone.data.scaleY);
pose.scaleY = getScaleValue(time, alpha, blend, direction, pose.scaleY, bone.data.setup.scaleY);
}
}
}
@ -869,18 +869,18 @@ public class Animation {
Bone bone = skeleton.bones.get(boneIndex);
if (!bone.active) return;
BonePose pose = appliedPose ? bone.applied : bone.pose;
BonePose pose = appliedPose ? bone.applied : bone.pose, setup = bone.data.setup;
float[] frames = this.frames;
if (time < frames[0]) {
switch (blend) {
case setup:
pose.shearX = bone.data.shearX;
pose.shearY = bone.data.shearY;
pose.shearX = setup.shearX;
pose.shearY = setup.shearY;
return;
case first:
pose.shearX += (bone.data.shearX - pose.shearX) * alpha;
pose.shearY += (bone.data.shearY - pose.shearY) * alpha;
pose.shearX += (setup.shearX - pose.shearX) * alpha;
pose.shearY += (setup.shearY - pose.shearY) * alpha;
}
return;
}
@ -907,13 +907,13 @@ public class Animation {
switch (blend) {
case setup:
pose.shearX = bone.data.shearX + x * alpha;
pose.shearY = bone.data.shearY + y * alpha;
pose.shearX = setup.shearX + x * alpha;
pose.shearY = setup.shearY + y * alpha;
break;
case first:
case replace:
pose.shearX += (bone.data.shearX + x - pose.shearX) * alpha;
pose.shearY += (bone.data.shearY + y - pose.shearY) * alpha;
pose.shearX += (setup.shearX + x - pose.shearX) * alpha;
pose.shearY += (setup.shearY + y - pose.shearY) * alpha;
break;
case add:
pose.shearX += x * alpha;
@ -941,7 +941,7 @@ public class Animation {
Bone bone = skeleton.bones.get(boneIndex);
if (bone.active) {
BonePose pose = appliedPose ? bone.applied : bone.pose;
pose.shearX = getRelativeValue(time, alpha, blend, pose.shearX, bone.data.shearX);
pose.shearX = getRelativeValue(time, alpha, blend, pose.shearX, bone.data.setup.shearX);
}
}
}
@ -965,7 +965,7 @@ public class Animation {
Bone bone = skeleton.bones.get(boneIndex);
if (bone.active) {
BonePose pose = appliedPose ? bone.applied : bone.pose;
pose.shearY = getRelativeValue(time, alpha, blend, pose.shearY, bone.data.shearY);
pose.shearY = getRelativeValue(time, alpha, blend, pose.shearY, bone.data.setup.shearY);
}
}
}
@ -1007,13 +1007,13 @@ public class Animation {
BonePose pose = appliedPose ? bone.applied : bone.pose;
if (direction == out) {
if (blend == setup) pose.inherit = bone.data.inherit;
if (blend == setup) pose.inherit = bone.data.setup.inherit;
return;
}
float[] frames = this.frames;
if (time < frames[0]) {
if (blend == setup || blend == first) pose.inherit = bone.data.inherit;
if (blend == setup || blend == first) pose.inherit = bone.data.setup.inherit;
return;
}
pose.inherit = Inherit.values[(int)frames[search(frames, time, ENTRIES) + INHERIT]];
@ -1064,7 +1064,7 @@ public class Animation {
float[] frames = this.frames;
Color color = pose.color;
if (time < frames[0]) {
Color setup = slot.data.color;
Color setup = slot.data.setup.color;
switch (blend) {
case setup:
color.set(setup);
@ -1107,7 +1107,7 @@ public class Animation {
if (alpha == 1)
color.set(r, g, b, a);
else {
if (blend == setup) color.set(slot.data.color);
if (blend == setup) color.set(slot.data.setup.color);
color.add((r - color.r) * alpha, (g - color.g) * alpha, (b - color.b) * alpha, (a - color.a) * alpha);
}
}
@ -1154,7 +1154,7 @@ public class Animation {
float[] frames = this.frames;
Color color = pose.color;
if (time < frames[0]) {
Color setup = slot.data.color;
Color setup = slot.data.setup.color;
switch (blend) {
case setup:
color.r = setup.r;
@ -1199,7 +1199,7 @@ public class Animation {
color.b = b;
} else {
if (blend == setup) {
Color setup = slot.data.color;
Color setup = slot.data.setup.color;
color.r = setup.r;
color.g = setup.g;
color.b = setup.b;
@ -1234,7 +1234,7 @@ public class Animation {
float[] frames = this.frames;
Color color = pose.color;
if (time < frames[0]) {
Color setup = slot.data.color;
Color setup = slot.data.setup.color;
switch (blend) {
case setup:
color.a = setup.a;
@ -1249,7 +1249,7 @@ public class Animation {
if (alpha == 1)
color.a = a;
else {
if (blend == setup) color.a = slot.data.color.a;
if (blend == setup) color.a = slot.data.setup.color.a;
color.a += (a - color.a) * alpha;
}
}
@ -1305,7 +1305,8 @@ public class Animation {
float[] frames = this.frames;
Color light = pose.color, dark = pose.darkColor;
if (time < frames[0]) {
Color setupLight = slot.data.color, setupDark = slot.data.darkColor;
SlotPose setup = slot.data.setup;
Color setupLight = setup.color, setupDark = setup.darkColor;
switch (blend) {
case setup:
light.set(setupLight);
@ -1370,8 +1371,9 @@ public class Animation {
dark.b = b2;
} else {
if (blend == setup) {
light.set(slot.data.color);
Color setupDark = slot.data.darkColor;
SlotPose setup = slot.data.setup;
light.set(setup.color);
Color setupDark = setup.darkColor;
dark.r = setupDark.r;
dark.g = setupDark.g;
dark.b = setupDark.b;
@ -1432,7 +1434,8 @@ public class Animation {
float[] frames = this.frames;
Color light = pose.color, dark = pose.darkColor;
if (time < frames[0]) {
Color setupLight = slot.data.color, setupDark = slot.data.darkColor;
SlotPose setup = slot.data.setup;
Color setupLight = setup.color, setupDark = setup.darkColor;
switch (blend) {
case setup:
light.r = setupLight.r;
@ -1498,7 +1501,8 @@ public class Animation {
dark.b = b2;
} else {
if (blend == setup) {
Color setupLight = slot.data.color, setupDark = slot.data.darkColor;
SlotPose setup = slot.data.setup;
Color setupLight = setup.color, setupDark = setup.darkColor;
light.r = setupLight.r;
light.g = setupLight.g;
light.b = setupLight.b;
@ -1958,8 +1962,9 @@ public class Animation {
}
}
/** Changes an IK constraint's {@link IkConstraint#getMix()}, {@link IkConstraint#getSoftness()},
* {@link IkConstraint#getBendDirection()}, {@link IkConstraint#getStretch()}, and {@link IkConstraint#getCompress()}. */
/** Changes an IK constraint's {@link IkConstraintPose#getMix()}, {@link IkConstraintPose#getSoftness()},
* {@link IkConstraintPose#getBendDirection()}, {@link IkConstraintPose#getStretch()}, and
* {@link IkConstraintPose#getCompress()}. */
static public class IkConstraintTimeline extends CurveTimeline {
static public final int ENTRIES = 6;
static private final int MIX = 1, SOFTNESS = 2, BEND_DIRECTION = 3, COMPRESS = 4, STRETCH = 5;
@ -2001,24 +2006,24 @@ public class Animation {
IkConstraint constraint = skeleton.ikConstraints.get(constraintIndex);
if (!constraint.active) return;
if (appliedPose) constraint = constraint.applied;
IkConstraintPose pose = appliedPose ? constraint.applied : constraint.pose, setup = constraint.data.setup;
float[] frames = this.frames;
if (time < frames[0]) {
switch (blend) {
case setup:
constraint.mix = constraint.data.mix;
constraint.softness = constraint.data.softness;
constraint.bendDirection = constraint.data.bendDirection;
constraint.compress = constraint.data.compress;
constraint.stretch = constraint.data.stretch;
pose.mix = setup.mix;
pose.softness = setup.softness;
pose.bendDirection = setup.bendDirection;
pose.compress = setup.compress;
pose.stretch = setup.stretch;
return;
case first:
constraint.mix += (constraint.data.mix - constraint.mix) * alpha;
constraint.softness += (constraint.data.softness - constraint.softness) * alpha;
constraint.bendDirection = constraint.data.bendDirection;
constraint.compress = constraint.data.compress;
constraint.stretch = constraint.data.stretch;
pose.mix += (setup.mix - pose.mix) * alpha;
pose.softness += (setup.softness - pose.softness) * alpha;
pose.bendDirection = setup.bendDirection;
pose.compress = setup.compress;
pose.stretch = setup.stretch;
}
return;
}
@ -2043,25 +2048,25 @@ public class Animation {
softness = getBezierValue(time, i, SOFTNESS, curveType + BEZIER_SIZE - BEZIER);
}
if (blend == setup) {
constraint.mix = constraint.data.mix + (mix - constraint.data.mix) * alpha;
constraint.softness = constraint.data.softness + (softness - constraint.data.softness) * alpha;
if (blend == MixBlend.setup) {
pose.mix = setup.mix + (mix - setup.mix) * alpha;
pose.softness = setup.softness + (softness - setup.softness) * alpha;
if (direction == out) {
constraint.bendDirection = constraint.data.bendDirection;
constraint.compress = constraint.data.compress;
constraint.stretch = constraint.data.stretch;
pose.bendDirection = setup.bendDirection;
pose.compress = setup.compress;
pose.stretch = setup.stretch;
} else {
constraint.bendDirection = (int)frames[i + BEND_DIRECTION];
constraint.compress = frames[i + COMPRESS] != 0;
constraint.stretch = frames[i + STRETCH] != 0;
pose.bendDirection = (int)frames[i + BEND_DIRECTION];
pose.compress = frames[i + COMPRESS] != 0;
pose.stretch = frames[i + STRETCH] != 0;
}
} else {
constraint.mix += (mix - constraint.mix) * alpha;
constraint.softness += (softness - constraint.softness) * alpha;
pose.mix += (mix - pose.mix) * alpha;
pose.softness += (softness - pose.softness) * alpha;
if (direction == in) {
constraint.bendDirection = (int)frames[i + BEND_DIRECTION];
constraint.compress = frames[i + COMPRESS] != 0;
constraint.stretch = frames[i + STRETCH] != 0;
pose.bendDirection = (int)frames[i + BEND_DIRECTION];
pose.compress = frames[i + COMPRESS] != 0;
pose.stretch = frames[i + STRETCH] != 0;
}
}
}

View File

@ -417,23 +417,23 @@ public class AnimationState {
Bone bone = skeleton.bones.get(timeline.boneIndex);
if (!bone.active) return;
BonePose pose = bone.getPose();
BonePose pose = bone.getPose(), setup = bone.data.setup;
float[] frames = timeline.frames;
float r1, r2;
if (time < frames[0]) { // Time is before first frame.
switch (blend) {
case setup:
pose.rotation = bone.data.rotation;
pose.rotation = setup.rotation;
// Fall through.
default:
return;
case first:
r1 = pose.rotation;
r2 = bone.data.rotation;
r2 = setup.rotation;
}
} else {
r1 = blend == MixBlend.setup ? bone.data.rotation : pose.rotation;
r2 = bone.data.rotation + timeline.getCurveValue(time);
r1 = blend == MixBlend.setup ? setup.rotation : pose.rotation;
r2 = setup.rotation + timeline.getCurveValue(time);
}
// Mix between rotations using the direction of the shortest route on the first frame.

View File

@ -66,7 +66,7 @@ public class Bone {
/** Sets this bone's local transform to the setup pose. */
public void setupPose () {
pose.set(data);
pose.set(data.setup);
}
/** The bone's setup pose data. */

View File

@ -33,10 +33,11 @@ import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.utils.Null;
/** The setup pose for a bone. */
public class BoneData extends BonePose {
public class BoneData {
final int index;
final String name;
@Null final BoneData parent;
final BonePose setup = new BonePose();
float length;
boolean skinRequired;
@ -54,12 +55,12 @@ public class BoneData extends BonePose {
}
/** Copy constructor. */
public BoneData (BoneData bone, @Null BoneData parent) {
index = bone.index;
name = bone.name;
public BoneData (BoneData data, @Null BoneData parent) {
index = data.index;
name = data.name;
this.parent = parent;
length = bone.length;
set(bone);
length = data.length;
setup.set(data.setup);
}
/** The index of the bone in {@link Skeleton#getBones()}. */
@ -76,6 +77,10 @@ public class BoneData extends BonePose {
return parent;
}
public BonePose getSetupPose () {
return setup;
}
/** The bone's length. */
public float getLength () {
return length;

View File

@ -39,16 +39,16 @@ public class BonePose {
BonePose () {
}
public void set (BonePose bone) {
if (bone == null) throw new IllegalArgumentException("bone cannot be null.");
x = bone.x;
y = bone.y;
rotation = bone.rotation;
scaleX = bone.scaleX;
scaleY = bone.scaleY;
shearX = bone.shearX;
shearY = bone.shearY;
inherit = bone.inherit;
public void set (BonePose pose) {
if (pose == null) throw new IllegalArgumentException("pose cannot be null.");
x = pose.x;
y = pose.y;
rotation = pose.rotation;
scaleX = pose.scaleX;
scaleY = pose.scaleY;
shearX = pose.shearX;
shearY = pose.shearY;
inherit = pose.inherit;
}
/** The local x translation. */

View File

@ -43,19 +43,9 @@ public class IkConstraint implements Updatable {
final IkConstraintData data;
final Array<BoneApplied> bones;
BoneApplied target;
IkConstraint applied;
final IkConstraintPose pose = new IkConstraintPose(), applied = new IkConstraintPose();
boolean active;
int bendDirection;
boolean compress, stretch;
float mix = 1, softness;
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.");
if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
@ -67,8 +57,6 @@ public class IkConstraint implements Updatable {
target = skeleton.bones.get(data.target.index).applied;
applied = new IkConstraint(data, bones, target);
setupPose();
}
@ -79,24 +67,20 @@ public class IkConstraint implements Updatable {
}
public void setupPose () {
IkConstraintData data = this.data;
mix = data.mix;
softness = data.softness;
bendDirection = data.bendDirection;
compress = data.compress;
stretch = data.stretch;
pose.set(data.setup);
}
/** Applies the constraint to the constrained bones. */
public void update (Physics physics) {
if (mix == 0) return;
IkConstraintPose pose = applied;
if (pose.mix == 0) return;
BoneApplied target = this.target;
Object[] bones = this.bones.items;
switch (this.bones.size) {
case 1 -> apply((BoneApplied)bones[0], target.worldX, target.worldY, compress, stretch, data.uniform, mix);
case 1 -> apply((BoneApplied)bones[0], target.worldX, target.worldY, pose.compress, pose.stretch, data.uniform, pose.mix);
case 2 -> //
apply((BoneApplied)bones[0], (BoneApplied)bones[1], target.worldX, target.worldY, bendDirection, stretch, data.uniform,
softness, mix);
apply((BoneApplied)bones[0], (BoneApplied)bones[1], target.worldX, target.worldY, pose.bendDirection, pose.stretch,
data.uniform, pose.softness, pose.mix);
}
}
@ -115,55 +99,12 @@ public class IkConstraint implements Updatable {
this.target = target;
}
/** A percentage (0-1) that controls the mix between the constrained and unconstrained rotation.
* <p>
* For two bone IK: if the parent bone has local nonuniform scale, the child bone's local Y translation is set to 0. */
public float getMix () {
return mix;
public IkConstraintPose getPose () {
return pose;
}
public void setMix (float mix) {
this.mix = mix;
}
/** For two bone IK, the target bone's distance from the maximum reach of the bones where rotation begins to slow. The bones
* will not straighten completely until the target is this far out of range. */
public float getSoftness () {
return softness;
}
public void setSoftness (float softness) {
this.softness = softness;
}
/** For two bone IK, controls the bend direction of the IK bones, either 1 or -1. */
public int getBendDirection () {
return bendDirection;
}
public void setBendDirection (int bendDirection) {
this.bendDirection = bendDirection;
}
/** For one bone IK, when true and the target is too close, the bone is scaled to reach it. */
public boolean getCompress () {
return compress;
}
public void setCompress (boolean compress) {
this.compress = compress;
}
/** When true and the target is out of range, the parent bone is scaled to reach it.
* <p>
* For two bone IK: 1) the child bone's local Y translation is set to 0, 2) stretch is not applied if {@link #getSoftness()} is
* > 0, and 3) if the parent bone has local nonuniform scale, stretch is not applied. */
public boolean getStretch () {
return stretch;
}
public void setStretch (boolean stretch) {
this.stretch = stretch;
public IkConstraintPose getAppliedPose () {
return applied;
}
/** Returns false when this constraint won't be updated by

View File

@ -37,9 +37,8 @@ import com.badlogic.gdx.utils.Array;
public class IkConstraintData extends ConstraintData {
final Array<BoneData> bones = new Array();
BoneData target;
int bendDirection;
boolean compress, stretch, uniform;
float mix, softness;
final IkConstraintPose setup = new IkConstraintPose();
boolean uniform;
public IkConstraintData (String name) {
super(name);
@ -60,58 +59,12 @@ public class IkConstraintData extends ConstraintData {
this.target = target;
}
/** A percentage (0-1) that controls the mix between the constrained and unconstrained rotation.
* <p>
* For two bone IK: if the parent bone has local nonuniform scale, the child bone's local Y translation is set to 0. */
public float getMix () {
return mix;
public IkConstraintPose getSetupPose () {
return setup;
}
public void setMix (float mix) {
this.mix = mix;
}
/** For two bone IK, the target bone's distance from the maximum reach of the bones where rotation begins to slow. The bones
* will not straighten completely until the target is this far out of range. */
public float getSoftness () {
return softness;
}
public void setSoftness (float softness) {
this.softness = softness;
}
/** For two bone IK, controls the bend direction of the IK bones, either 1 or -1. */
public int getBendDirection () {
return bendDirection;
}
public void setBendDirection (int bendDirection) {
this.bendDirection = bendDirection;
}
/** For one bone IK, when true and the target is too close, the bone is scaled to reach it. */
public boolean getCompress () {
return compress;
}
public void setCompress (boolean compress) {
this.compress = compress;
}
/** When true and the target is out of range, the parent bone is scaled to reach it.
* <p>
* For two bone IK: 1) the child bone's local Y translation is set to 0, 2) stretch is not applied if {@link #getSoftness()} is
* > 0, and 3) if the parent bone has local nonuniform scale, stretch is not applied. */
public boolean getStretch () {
return stretch;
}
public void setStretch (boolean stretch) {
this.stretch = stretch;
}
/** When true and {@link #getCompress()} or {@link #getStretch()} is used, the bone is scaled on both the X and Y axes. */
/** When true and {@link IkConstraintPose#getCompress()} or {@link IkConstraintPose#getStretch()} is used, the bone is scaled
* on both the X and Y axes. */
public boolean getUniform () {
return uniform;
}

View File

@ -0,0 +1,96 @@
/******************************************************************************
* 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 current pose for an IK constraint. */
public class IkConstraintPose {
int bendDirection;
boolean compress, stretch;
float mix, softness;
public void set (IkConstraintPose pose) {
mix = pose.mix;
softness = pose.softness;
bendDirection = pose.bendDirection;
compress = pose.compress;
stretch = pose.stretch;
}
/** A percentage (0-1) that controls the mix between the constrained and unconstrained rotation.
* <p>
* For two bone IK: if the parent bone has local nonuniform scale, the child bone's local Y translation is set to 0. */
public float getMix () {
return mix;
}
public void setMix (float mix) {
this.mix = mix;
}
/** For two bone IK, the target bone's distance from the maximum reach of the bones where rotation begins to slow. The bones
* will not straighten completely until the target is this far out of range. */
public float getSoftness () {
return softness;
}
public void setSoftness (float softness) {
this.softness = softness;
}
/** For two bone IK, controls the bend direction of the IK bones, either 1 or -1. */
public int getBendDirection () {
return bendDirection;
}
public void setBendDirection (int bendDirection) {
this.bendDirection = bendDirection;
}
/** For one bone IK, when true and the target is too close, the bone is scaled to reach it. */
public boolean getCompress () {
return compress;
}
public void setCompress (boolean compress) {
this.compress = compress;
}
/** When true and the target is out of range, the parent bone is scaled to reach it.
* <p>
* For two bone IK: 1) the child bone's local Y translation is set to 0, 2) stretch is not applied if {@link #getSoftness()} is
* > 0, and 3) if the parent bone has local nonuniform scale, stretch is not applied. */
public boolean getStretch () {
return stretch;
}
public void setStretch (boolean stretch) {
this.stretch = stretch;
}
}

View File

@ -53,6 +53,7 @@ import com.esotericsoftware.spine.utils.SkeletonClipping;
* Runtimes Guide. */
public class Skeleton {
static private final short[] quadTriangles = {0, 1, 2, 2, 3, 0};
private static final Object IkConstraint = null;
final SkeletonData data;
final Array<Bone> bones;
final Array<Slot> slots;
@ -412,16 +413,22 @@ public class Skeleton {
* See <a href="https://esotericsoftware.com/spine-runtime-skeletons#World-transforms">World transforms</a> in the Spine
* Runtimes Guide. */
public void updateWorldTransform (Physics physics) {
Object[] bones = this.bones.items;
Object[] objects = this.bones.items;
for (int i = 0, n = this.bones.size; i < n; i++) {
var bone = (Bone)bones[i];
var bone = (Bone)objects[i];
if (bone.active) bone.applied.set(bone.pose);
}
Object[] slots = this.slots.items;
objects = this.slots.items;
for (int i = 0, n = this.slots.size; i < n; i++) {
var slot = (Slot)slots[i];
var slot = (Slot)objects[i];
if (slot.bone.active) slot.applied.set(slot.pose);
}
objects = ikConstraints.items;
for (int i = 0, n = ikConstraints.size; i < n; i++) {
var constraint = (IkConstraint)objects[i];
if (constraint.active) constraint.applied.set(constraint.pose);
}
// BOZO! - Reset the rest.
Object[] updateCache = this.updateCache.items;

View File

@ -217,15 +217,16 @@ public class SkeletonBinary extends SkeletonLoader {
String name = input.readString();
BoneData parent = i == 0 ? null : (BoneData)bones[input.readInt(true)];
var data = new BoneData(i, name, parent);
data.rotation = input.readFloat();
data.x = input.readFloat() * scale;
data.y = input.readFloat() * scale;
data.scaleX = input.readFloat();
data.scaleY = input.readFloat();
data.shearX = input.readFloat();
data.shearY = input.readFloat();
BonePose setup = data.setup;
setup.rotation = input.readFloat();
setup.x = input.readFloat() * scale;
setup.y = input.readFloat() * scale;
setup.scaleX = input.readFloat();
setup.scaleY = input.readFloat();
setup.shearX = input.readFloat();
setup.shearY = input.readFloat();
setup.inherit = Inherit.values[input.readByte()];
data.length = input.readFloat() * scale;
data.inherit = Inherit.values[input.readByte()];
data.skinRequired = input.readBoolean();
if (nonessential) {
Color.rgba8888ToColor(data.color, input.readInt());
@ -241,10 +242,10 @@ public class SkeletonBinary extends SkeletonLoader {
String slotName = input.readString();
var boneData = (BoneData)bones[input.readInt(true)];
var data = new SlotData(i, slotName, boneData);
Color.rgba8888ToColor(data.color, input.readInt());
Color.rgba8888ToColor(data.setup.color, input.readInt());
int darkColor = input.readInt();
if (darkColor != -1) Color.rgb888ToColor(data.darkColor = new Color(), darkColor);
if (darkColor != -1) Color.rgb888ToColor(data.setup.darkColor = new Color(), darkColor);
data.attachmentName = input.readStringRef();
data.blendMode = BlendMode.values[input.readInt(true)];
@ -263,12 +264,13 @@ public class SkeletonBinary extends SkeletonLoader {
data.target = (BoneData)bones[input.readInt(true)];
int flags = input.read();
data.skinRequired = (flags & 1) != 0;
data.bendDirection = (flags & 2) != 0 ? 1 : -1;
data.compress = (flags & 4) != 0;
data.stretch = (flags & 8) != 0;
data.uniform = (flags & 16) != 0;
if ((flags & 32) != 0) data.mix = (flags & 64) != 0 ? input.readFloat() : 1;
if ((flags & 128) != 0) data.softness = input.readFloat() * scale;
data.uniform = (flags & 2) != 0;
IkConstraintPose setup = data.setup;
setup.bendDirection = (flags & 4) != 0 ? 1 : -1;
setup.compress = (flags & 8) != 0;
setup.stretch = (flags & 16) != 0;
if ((flags & 32) != 0) setup.mix = (flags & 64) != 0 ? input.readFloat() : 1;
if ((flags & 128) != 0) setup.softness = input.readFloat() * scale;
o[i] = data;
}

View File

@ -174,14 +174,15 @@ public class SkeletonJson extends SkeletonLoader {
}
var data = new BoneData(skeletonData.bones.size, boneMap.getString("name"), parent);
data.length = boneMap.getFloat("length", 0) * scale;
data.x = boneMap.getFloat("x", 0) * scale;
data.y = boneMap.getFloat("y", 0) * scale;
data.rotation = boneMap.getFloat("rotation", 0);
data.scaleX = boneMap.getFloat("scaleX", 1);
data.scaleY = boneMap.getFloat("scaleY", 1);
data.shearX = boneMap.getFloat("shearX", 0);
data.shearY = boneMap.getFloat("shearY", 0);
data.inherit = Inherit.valueOf(boneMap.getString("inherit", Inherit.normal.name()));
BonePose setup = data.setup;
setup.x = boneMap.getFloat("x", 0) * scale;
setup.y = boneMap.getFloat("y", 0) * scale;
setup.rotation = boneMap.getFloat("rotation", 0);
setup.scaleX = boneMap.getFloat("scaleX", 1);
setup.scaleY = boneMap.getFloat("scaleY", 1);
setup.shearX = boneMap.getFloat("shearX", 0);
setup.shearY = boneMap.getFloat("shearY", 0);
setup.inherit = Inherit.valueOf(boneMap.getString("inherit", Inherit.normal.name()));
data.skinRequired = boneMap.getBoolean("skin", false);
String color = boneMap.getString("color", null);
@ -203,10 +204,10 @@ public class SkeletonJson extends SkeletonLoader {
var data = new SlotData(skeletonData.slots.size, slotName, boneData);
String color = slotMap.getString("color", null);
if (color != null) Color.valueOf(color, data.getColor());
if (color != null) Color.valueOf(color, data.getSetupPose().getColor());
String dark = slotMap.getString("dark", null);
if (dark != null) data.setDarkColor(Color.valueOf(dark));
if (dark != null) Color.valueOf(dark, data.getSetupPose().getDarkColor());
data.attachmentName = slotMap.getString("attachment", null);
data.blendMode = BlendMode.valueOf(slotMap.getString("blend", BlendMode.normal.name()));
@ -230,12 +231,13 @@ public class SkeletonJson extends SkeletonLoader {
data.target = skeletonData.findBone(targetName);
if (data.target == null) throw new SerializationException("IK target bone not found: " + targetName);
data.mix = constraintMap.getFloat("mix", 1);
data.softness = constraintMap.getFloat("softness", 0) * scale;
data.bendDirection = constraintMap.getBoolean("bendPositive", true) ? 1 : -1;
data.compress = constraintMap.getBoolean("compress", false);
data.stretch = constraintMap.getBoolean("stretch", false);
data.uniform = constraintMap.getBoolean("uniform", false);
IkConstraintPose setup = data.setup;
setup.mix = constraintMap.getFloat("mix", 1);
setup.softness = constraintMap.getFloat("softness", 0) * scale;
setup.bendDirection = constraintMap.getBoolean("bendPositive", true) ? 1 : -1;
setup.compress = constraintMap.getBoolean("compress", false);
setup.stretch = constraintMap.getBoolean("stretch", false);
skeletonData.ikConstraints.add(data);
}

View File

@ -45,7 +45,7 @@ public class Slot {
if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
this.data = data;
bone = skeleton.bones.get(data.boneData.index);
if (data.darkColor != null) {
if (data.setup.darkColor != null) {
pose.darkColor = new Color();
applied.darkColor = new Color();
}
@ -58,7 +58,7 @@ public class Slot {
if (bone == null) throw new IllegalArgumentException("bone cannot be null.");
data = slot.data;
this.bone = bone;
if (data.darkColor != null) {
if (data.setup.darkColor != null) {
pose.darkColor = new Color();
applied.darkColor = new Color();
}
@ -67,7 +67,7 @@ public class Slot {
/** Sets this slot to the setup pose. */
public void setupPose () {
pose.set(data);
pose.set(data.setup);
if (data.attachmentName != null) pose.setAttachment(bone.skeleton.getAttachment(data.index, data.attachmentName));
}

View File

@ -29,14 +29,14 @@
package com.esotericsoftware.spine;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.utils.Null;
/** Stores the setup pose for a {@link Slot}. */
public class SlotData extends SlotPose {
public class SlotData {
final int index;
final String name;
final BoneData boneData;
final SlotPose setup = new SlotPose();
@Null String attachmentName;
BlendMode blendMode;
@ -67,20 +67,8 @@ public class SlotData extends SlotPose {
return boneData;
}
/** The color used to tint the slot's attachment. If {@link #getDarkColor()} is set, this is used as the light color for two
* color tinting. */
public Color getColor () {
return 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 () {
return darkColor;
}
public void setDarkColor (@Null Color darkColor) {
this.darkColor = darkColor;
public SlotPose getSetupPose () {
return setup;
}
public void setAttachmentName (@Null String attachmentName) {

View File

@ -44,21 +44,21 @@ import com.esotericsoftware.spine.attachments.VertexAttachment;
public class SlotPose {
final Color color = new Color();
@Null Color darkColor;
@Null Attachment attachment;
@Null Attachment attachment; // Not used in setup pose.
int sequenceIndex;
final FloatArray deform = new FloatArray(0);
SlotPose () {
}
public void set (SlotPose slot) {
if (slot == null) throw new IllegalArgumentException("slot cannot be null.");
color.set(slot.color);
if (slot.darkColor != null) darkColor.set(slot.darkColor);
attachment = slot.attachment;
sequenceIndex = slot.sequenceIndex;
public void set (SlotPose pose) {
if (pose == null) throw new IllegalArgumentException("pose cannot be null.");
color.set(pose.color);
if (darkColor != null) darkColor.set(pose.darkColor);
attachment = pose.attachment;
sequenceIndex = pose.sequenceIndex;
deform.size = 0;
deform.addAll(slot.deform);
deform.addAll(pose.deform);
}
/** The color used to tint the slot's attachment. If {@link #getDarkColor()} is set, this is used as the light color for two