[cpp] Port additive timeline and color flicker fixes from spine-libgdx

- Revised which timelines support additive blending: IK and path
  constraint mix timelines no longer support add blend (treat as
  replace). Physics constraint timelines only support add for wind
  and gravity. Ports spine-libgdx commit b92058975.

- Fixed alpha and RGB timeline flicker when blend is setup and alpha
  is not 1: interpolate from setup value instead of set-then-add.
  Clamp color output to 0-1. Ports spine-libgdx commit 8943c84d8.
This commit is contained in:
Mario Zechner 2026-03-10 16:43:19 +01:00
parent f21b38904f
commit 29e2eac56e
6 changed files with 88 additions and 103 deletions

View File

@ -67,6 +67,7 @@ namespace spine {
virtual bool global(PhysicsConstraintData &constraintData) = 0;
int _constraintIndex;
bool _additive;
};
/// Changes a physics constraint's PhysicsConstraintPose::getInertia().
@ -184,7 +185,7 @@ namespace spine {
public:
explicit PhysicsConstraintWindTimeline(size_t frameCount, size_t bezierCount, int physicsConstraintIndex)
: PhysicsConstraintTimeline(frameCount, bezierCount, physicsConstraintIndex, Property_PhysicsConstraintWind) {};
: PhysicsConstraintTimeline(frameCount, bezierCount, physicsConstraintIndex, Property_PhysicsConstraintWind) { _additive = true; };
protected:
float get(PhysicsConstraintPose &pose) override {
@ -210,7 +211,7 @@ namespace spine {
public:
explicit PhysicsConstraintGravityTimeline(size_t frameCount, size_t bezierCount, int physicsConstraintIndex)
: PhysicsConstraintTimeline(frameCount, bezierCount, physicsConstraintIndex, Property_PhysicsConstraintGravity) {};
: PhysicsConstraintTimeline(frameCount, bezierCount, physicsConstraintIndex, Property_PhysicsConstraintGravity) { _additive = true; };
protected:
float get(PhysicsConstraintPose &pose) override {

View File

@ -136,6 +136,7 @@ void RGBTimeline::setFrame(int frame, float time, float r, float g, float b) {
void RGBTimeline::_apply(Slot &slot, SlotPose &pose, float time, float alpha, MixBlend blend) {
Color &color = pose._color;
float r, g, b;
if (time < _frames[0]) {
Color &setup = slot._data._setup._color;
switch (blend) {
@ -145,59 +146,58 @@ void RGBTimeline::_apply(Slot &slot, SlotPose &pose, float time, float alpha, Mi
color.b = setup.b;
return;
case MixBlend_First:
color.r += (setup.r - color.r) * alpha;
color.g += (setup.g - color.g) * alpha;
color.b += (setup.b - color.b) * alpha;
return;
r = color.r + (setup.r - color.r) * alpha;
g = color.g + (setup.g - color.g) * alpha;
b = color.b + (setup.b - color.b) * alpha;
break;
default:
return;
}
}
float r, g, b;
int i = Animation::search(_frames, time, ENTRIES);
int curveType = (int) _curves[i >> 2];
switch (curveType) {
case LINEAR: {
float before = _frames[i];
r = _frames[i + R];
g = _frames[i + G];
b = _frames[i + B];
float t = (time - before) / (_frames[i + ENTRIES] - before);
r += (_frames[i + ENTRIES + R] - r) * t;
g += (_frames[i + ENTRIES + G] - g) * t;
b += (_frames[i + ENTRIES + B] - b) * t;
break;
}
case STEPPED: {
r = _frames[i + R];
g = _frames[i + G];
b = _frames[i + B];
break;
}
default: {
r = getBezierValue(time, i, R, curveType - BEZIER);
g = getBezierValue(time, i, G, curveType + BEZIER_SIZE - BEZIER);
b = getBezierValue(time, i, B, curveType + BEZIER_SIZE * 2 - BEZIER);
break;
}
}
if (alpha == 1) {
color.r = r;
color.g = g;
color.b = b;
} else {
if (blend == MixBlend_Setup) {
Color &setup = slot._data._setup._color;
color.r = setup.r;
color.g = setup.g;
color.b = setup.b;
int i = Animation::search(_frames, time, ENTRIES);
int curveType = (int) _curves[i >> 2];
switch (curveType) {
case LINEAR: {
float before = _frames[i];
r = _frames[i + R];
g = _frames[i + G];
b = _frames[i + B];
float t = (time - before) / (_frames[i + ENTRIES] - before);
r += (_frames[i + ENTRIES + R] - r) * t;
g += (_frames[i + ENTRIES + G] - g) * t;
b += (_frames[i + ENTRIES + B] - b) * t;
break;
}
case STEPPED: {
r = _frames[i + R];
g = _frames[i + G];
b = _frames[i + B];
break;
}
default: {
r = getBezierValue(time, i, R, curveType - BEZIER);
g = getBezierValue(time, i, G, curveType + BEZIER_SIZE - BEZIER);
b = getBezierValue(time, i, B, curveType + BEZIER_SIZE * 2 - BEZIER);
break;
}
}
if (alpha != 1) {
if (blend == MixBlend_Setup) {
Color &setup = slot._data._setup._color;
r = setup.r + (r - setup.r) * alpha;
g = setup.g + (g - setup.g) * alpha;
b = setup.b + (b - setup.b) * alpha;
} else {
r = color.r + (r - color.r) * alpha;
g = color.g + (g - color.g) * alpha;
b = color.b + (b - color.b) * alpha;
}
}
color.r += (r - color.r) * alpha;
color.g += (g - color.g) * alpha;
color.b += (b - color.b) * alpha;
}
color.r = r < 0 ? 0 : (r > 1 ? 1 : r);
color.g = g < 0 ? 0 : (g > 1 ? 1 : g);
color.b = b < 0 ? 0 : (b > 1 ? 1 : b);
}
RTTI_IMPL(AlphaTimeline, SlotCurveTimeline)
@ -229,6 +229,7 @@ void AlphaTimeline::apply(Skeleton &skeleton, float lastTime, float time, Array<
if (!slot->_bone._active) return;
Color &color = (appliedPose ? *slot->_applied : slot->_pose)._color;
float a;
if (time < _frames[0]) {
Color &setup = slot->_data._setup._color;
switch (blend) {
@ -236,20 +237,22 @@ void AlphaTimeline::apply(Skeleton &skeleton, float lastTime, float time, Array<
color.a = setup.a;
return;
case MixBlend_First:
color.a += (setup.a - color.a) * alpha;
return;
a = color.a + (setup.a - color.a) * alpha;
break;
default:
return;
}
} else {
a = getCurveValue(time);
if (alpha != 1) {
if (blend == MixBlend_Setup) {
Color &setup = slot->_data._setup._color;
a = setup.a + (a - setup.a) * alpha;
} else
a = color.a + (a - color.a) * alpha;
}
}
float a = getCurveValue(time);
if (alpha == 1)
color.a = a;
else {
if (blend == MixBlend_Setup) color.a = slot->_data._setup._color.a;
color.a += (a - color.a) * alpha;
}
color.a = a < 0 ? 0 : (a > 1 ? 1 : a);
}
RTTI_IMPL(RGBA2Timeline, SlotCurveTimeline)

View File

@ -108,30 +108,20 @@ void IkConstraintTimeline::apply(Skeleton &skeleton, float lastTime, float time,
}
}
switch (blend) {
case MixBlend_Setup: {
IkConstraintPose &setup = constraint->_data._setup;
pose._mix = setup._mix + (mix - setup._mix) * alpha;
pose._softness = setup._softness + (softness - setup._softness) * alpha;
if (direction == MixDirection_Out) {
pose._bendDirection = setup._bendDirection;
pose._compress = setup._compress;
pose._stretch = setup._stretch;
return;
}
break;
if (blend == MixBlend_Setup) {
IkConstraintPose &setup = constraint->_data._setup;
pose._mix = setup._mix + (mix - setup._mix) * alpha;
pose._softness = setup._softness + (softness - setup._softness) * alpha;
if (direction == MixDirection_Out) {
pose._bendDirection = setup._bendDirection;
pose._compress = setup._compress;
pose._stretch = setup._stretch;
return;
}
case MixBlend_First:
case MixBlend_Replace:
pose._mix += (mix - pose._mix) * alpha;
pose._softness += (softness - pose._softness) * alpha;
if (direction == MixDirection_Out) return;
break;
case MixBlend_Add:
pose._mix += mix * alpha;
pose._softness += softness * alpha;
if (direction == MixDirection_Out) return;
break;
} else {
pose._mix += (mix - pose._mix) * alpha;
pose._softness += (softness - pose._softness) * alpha;
if (direction == MixDirection_Out) return;
}
pose._bendDirection = (int) _frames[i + BEND_DIRECTION];
pose._compress = _frames[i + COMPRESS] != 0;

View File

@ -109,25 +109,15 @@ void PathConstraintMixTimeline::apply(Skeleton &skeleton, float lastTime, float
}
}
switch (blend) {
case MixBlend_Setup: {
PathConstraintPose &setup = constraint->_data._setup;
pose._mixRotate = setup._mixRotate + (rotate - setup._mixRotate) * alpha;
pose._mixX = setup._mixX + (x - setup._mixX) * alpha;
pose._mixY = setup._mixY + (y - setup._mixY) * alpha;
break;
}
case MixBlend_First:
case MixBlend_Replace:
pose._mixRotate += (rotate - pose._mixRotate) * alpha;
pose._mixX += (x - pose._mixX) * alpha;
pose._mixY += (y - pose._mixY) * alpha;
break;
case MixBlend_Add:
pose._mixRotate += rotate * alpha;
pose._mixX += x * alpha;
pose._mixY += y * alpha;
break;
if (blend == MixBlend_Setup) {
PathConstraintPose &setup = constraint->_data._setup;
pose._mixRotate = setup._mixRotate + (rotate - setup._mixRotate) * alpha;
pose._mixX = setup._mixX + (x - setup._mixX) * alpha;
pose._mixY = setup._mixY + (y - setup._mixY) * alpha;
} else {
pose._mixRotate += (rotate - pose._mixRotate) * alpha;
pose._mixX += (x - pose._mixX) * alpha;
pose._mixY += (y - pose._mixY) * alpha;
}
}

View File

@ -60,6 +60,6 @@ void PathConstraintSpacingTimeline::apply(Skeleton &skeleton, float lastTime, fl
if (constraint->isActive()) {
PathConstraintPose &pose = appliedPose ? *constraint->_applied : constraint->_pose;
PathConstraintData &data = constraint->_data;
pose._spacing = getAbsoluteValue(time, alpha, blend, pose._spacing, data._setup._spacing);
pose._spacing = getAbsoluteValue(time, alpha, blend == MixBlend_Add ? MixBlend_Replace : blend, pose._spacing, data._setup._spacing);
}
}

View File

@ -52,13 +52,14 @@ RTTI_IMPL(PhysicsConstraintMixTimeline, PhysicsConstraintTimeline)
RTTI_IMPL_MULTI(PhysicsConstraintResetTimeline, Timeline, ConstraintTimeline)
PhysicsConstraintTimeline::PhysicsConstraintTimeline(size_t frameCount, size_t bezierCount, int constraintIndex, Property property)
: CurveTimeline1(frameCount, bezierCount), ConstraintTimeline(), _constraintIndex(constraintIndex) {
: CurveTimeline1(frameCount, bezierCount), ConstraintTimeline(), _constraintIndex(constraintIndex), _additive(false) {
PropertyId ids[] = {((PropertyId) property << 32) | constraintIndex};
setPropertyIds(ids, 1);
}
void PhysicsConstraintTimeline::apply(Skeleton &skeleton, float, float time, Array<Event *> *, float alpha, MixBlend blend, MixDirection direction,
bool appliedPose) {
if (blend == MixBlend_Add && !_additive) blend = MixBlend_Replace;
if (_constraintIndex == -1) {
float value = time >= _frames[0] ? getCurveValue(time) : 0;