From d5354cb6bf52e3edf0ee8fe4ff0895cf869dfa8c Mon Sep 17 00:00:00 2001 From: pharan Date: Tue, 14 Aug 2018 23:45:38 +0800 Subject: [PATCH] [csharp] Ported stretchy IK. See https://github.com/EsotericSoftware/spine-runtimes/issues/1142 --- spine-csharp/src/Animation.cs | 25 +++++++++++----- spine-csharp/src/IkConstraint.cs | 45 ++++++++++++++++++++-------- spine-csharp/src/IkConstraintData.cs | 9 ++++++ spine-csharp/src/Skeleton.cs | 1 + spine-csharp/src/SkeletonBinary.cs | 3 +- spine-csharp/src/SkeletonJson.cs | 4 ++- 6 files changed, 66 insertions(+), 21 deletions(-) diff --git a/spine-csharp/src/Animation.cs b/spine-csharp/src/Animation.cs index 271e6dcbd..b994172c0 100644 --- a/spine-csharp/src/Animation.cs +++ b/spine-csharp/src/Animation.cs @@ -1210,9 +1210,9 @@ namespace Spine { } public class IkConstraintTimeline : CurveTimeline { - public const int ENTRIES = 3; - private const int PREV_TIME = -3, PREV_MIX = -2, PREV_BEND_DIRECTION = -1; - private const int MIX = 1, BEND_DIRECTION = 2; + public const int ENTRIES = 4; + private const int PREV_TIME = -4, PREV_MIX = -3, PREV_BEND_DIRECTION = -2, PREV_STRETCH = -1; + private const int MIX = 1, BEND_DIRECTION = 2, STRETCH = 3; internal int ikConstraintIndex; internal float[] frames; @@ -1230,11 +1230,12 @@ namespace Spine { } /// Sets the time, mix and bend direction of the specified keyframe. - public void SetFrame (int frameIndex, float time, float mix, int bendDirection) { + public void SetFrame (int frameIndex, float time, float mix, int bendDirection, bool stretch) { frameIndex *= ENTRIES; frames[frameIndex] = time; frames[frameIndex + MIX] = mix; frames[frameIndex + BEND_DIRECTION] = bendDirection; + frames[frameIndex + STRETCH] = stretch ? 1 : 0; } override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, MixDirection direction) { @@ -1245,10 +1246,12 @@ namespace Spine { case MixBlend.Setup: constraint.mix = constraint.data.mix; constraint.bendDirection = constraint.data.bendDirection; + constraint.stretch = constraint.data.stretch; return; case MixBlend.First: constraint.mix += (constraint.data.mix - constraint.mix) * alpha; constraint.bendDirection = constraint.data.bendDirection; + constraint.stretch = constraint.data.stretch; return; } return; @@ -1257,11 +1260,19 @@ namespace Spine { if (time >= frames[frames.Length - ENTRIES]) { // Time is after last frame. if (blend == MixBlend.Setup) { constraint.mix = constraint.data.mix + (frames[frames.Length + PREV_MIX] - constraint.data.mix) * alpha; - constraint.bendDirection = direction == MixDirection.Out ? constraint.data.bendDirection - : (int)frames[frames.Length + PREV_BEND_DIRECTION]; + if (direction == MixDirection.Out) { + constraint.bendDirection = constraint.data.bendDirection; + constraint.stretch = constraint.data.stretch; + } else { + constraint.bendDirection = (int)frames[frames.Length + PREV_BEND_DIRECTION]; + constraint.stretch = frames[frames.Length + PREV_STRETCH] != 0; + } } else { constraint.mix += (frames[frames.Length + PREV_MIX] - constraint.mix) * alpha; - if (direction == MixDirection.In) constraint.bendDirection = (int)frames[frames.Length + PREV_BEND_DIRECTION]; + if (direction == MixDirection.In) { + constraint.bendDirection = (int)frames[frames.Length + PREV_BEND_DIRECTION]; + constraint.stretch = frames[frames.Length + PREV_STRETCH] != 0; + } } return; } diff --git a/spine-csharp/src/IkConstraint.cs b/spine-csharp/src/IkConstraint.cs index 9d188a69e..721d5e617 100644 --- a/spine-csharp/src/IkConstraint.cs +++ b/spine-csharp/src/IkConstraint.cs @@ -35,14 +35,24 @@ namespace Spine { internal IkConstraintData data; internal ExposedList bones = new ExposedList(); internal Bone target; - internal float mix; internal int bendDirection; + internal bool stretch; + internal float mix; public IkConstraintData Data { get { return data; } } public int Order { get { return data.order; } } public ExposedList Bones { get { return bones; } } public Bone Target { get { return target; } set { target = value; } } public int BendDirection { get { return bendDirection; } set { bendDirection = value; } } + + /// + /// When true, if the target is out of range, the parent bone is scaled on the X axis to reach it. + /// IF the parent bone has nonuniform scale, stretching is not applied. + public bool Stretch { + get { return stretch; } + set { stretch = value; } + } + public float Mix { get { return mix; } set { mix = value; } } public IkConstraint (IkConstraintData data, Skeleton skeleton) { @@ -51,6 +61,7 @@ namespace Spine { this.data = data; mix = data.mix; bendDirection = data.bendDirection; + stretch = data.stretch; bones = new ExposedList(data.bones.Count); foreach (BoneData boneData in data.bones) @@ -68,10 +79,10 @@ namespace Spine { ExposedList bones = this.bones; switch (bones.Count) { case 1: - Apply(bones.Items[0], target.worldX, target.worldY, mix); + Apply(bones.Items[0], target.worldX, target.worldY, stretch, mix); break; case 2: - Apply(bones.Items[0], bones.Items[1], target.worldX, target.worldY, bendDirection, mix); + Apply(bones.Items[0], bones.Items[1], target.worldX, target.worldY, bendDirection, stretch, mix); break; } } @@ -82,7 +93,7 @@ namespace Spine { /// Adjusts the bone rotation so the tip is as close to the target position as possible. The target is specified /// in the world coordinate system. - static public void Apply (Bone bone, float targetX, float targetY, float alpha) { + static public void Apply (Bone bone, float targetX, float targetY, bool stretch, float alpha) { if (!bone.appliedValid) bone.UpdateAppliedTransform(); Bone p = bone.parent; float id = 1 / (p.a * p.d - p.b * p.c); @@ -93,14 +104,21 @@ namespace Spine { if (rotationIK > 180) rotationIK -= 360; else if (rotationIK < -180) rotationIK += 360; - bone.UpdateWorldTransform(bone.ax, bone.ay, bone.arotation + rotationIK * alpha, bone.ascaleX, bone.ascaleY, bone.ashearX, + + float sx = bone.ascaleX; + if (stretch) { + float b = bone.data.length * sx, dd = (float)Math.Sqrt(tx * tx + ty * ty); + if (dd > b && b > 0.0001f) sx *= (dd / b - 1) * alpha + 1; + } + + bone.UpdateWorldTransform(bone.ax, bone.ay, bone.arotation + rotationIK * alpha, sx, bone.ascaleY, bone.ashearX, bone.ashearY); } /// Adjusts the parent and child bone rotations so the tip of the child is as close to the target position as /// possible. The target is specified in the world coordinate system. /// A direct descendant of the parent bone. - static public void Apply (Bone parent, Bone child, float targetX, float targetY, int bendDir, float alpha) { + static public void Apply (Bone parent, Bone child, float targetX, float targetY, int bendDir, bool stretch, float alpha) { if (alpha == 0) { child.UpdateWorldTransform (); return; @@ -108,7 +126,7 @@ namespace Spine { //float px = parent.x, py = parent.y, psx = parent.scaleX, psy = parent.scaleY, csx = child.scaleX; if (!parent.appliedValid) parent.UpdateAppliedTransform(); if (!child.appliedValid) child.UpdateAppliedTransform(); - float px = parent.ax, py = parent.ay, psx = parent.ascaleX, psy = parent.ascaleY, csx = child.ascaleX; + float px = parent.ax, py = parent.ay, psx = parent.ascaleX, sx = psx, psy = parent.ascaleY, csx = child.ascaleX; int os1, os2, s2; if (psx < 0) { psx = -psx; @@ -144,17 +162,20 @@ namespace Spine { c = pp.c; d = pp.d; float id = 1 / (a * d - b * c), x = targetX - pp.worldX, y = targetY - pp.worldY; - float tx = (x * d - y * b) * id - px, ty = (y * a - x * c) * id - py; + float tx = (x * d - y * b) * id - px, ty = (y * a - x * c) * id - py, dd = tx * tx + ty * ty; x = cwx - pp.worldX; y = cwy - pp.worldY; float dx = (x * d - y * b) * id - px, dy = (y * a - x * c) * id - py; float l1 = (float)Math.Sqrt(dx * dx + dy * dy), l2 = child.data.length * csx, a1, a2; if (u) { l2 *= psx; - float cos = (tx * tx + ty * ty - l1 * l1 - l2 * l2) / (2 * l1 * l2); + float cos = (dd - l1 * l1 - l2 * l2) / (2 * l1 * l2); if (cos < -1) cos = -1; - else if (cos > 1) cos = 1; + else if (cos > 1) { + cos = 1; + if (stretch && l1 + l2 > 0.0001f) sx *= ((float)Math.Sqrt(dd) / (l1 + l2) - 1) * alpha + 1; + } a2 = (float)Math.Acos(cos) * bendDir; a = l1 + l2 * cos; b = l2 * (float)Math.Sin(a2); @@ -162,7 +183,7 @@ namespace Spine { } else { a = psx * l2; b = psy * l2; - float aa = a * a, bb = b * b, dd = tx * tx + ty * ty, ta = (float)Math.Atan2(ty, tx); + float aa = a * a, bb = b * b, ta = (float)Math.Atan2(ty, tx); c = bb * l1 * l1 + aa * dd - aa * bb; float c1 = -2 * bb * l1, c2 = bb - aa; d = c1 * c1 - 4 * c2 * c; @@ -215,7 +236,7 @@ namespace Spine { if (a1 > 180) a1 -= 360; else if (a1 < -180) a1 += 360; - parent.UpdateWorldTransform(px, py, rotation + a1 * alpha, parent.scaleX, parent.ascaleY, 0, 0); + parent.UpdateWorldTransform(px, py, rotation + a1 * alpha, sx, parent.ascaleY, 0, 0); rotation = child.arotation; a2 = ((a2 + os) * MathUtils.RadDeg - child.ashearX) * s2 + os2 - rotation; if (a2 > 180) diff --git a/spine-csharp/src/IkConstraintData.cs b/spine-csharp/src/IkConstraintData.cs index b0b0cf0ce..e1b25a3d2 100644 --- a/spine-csharp/src/IkConstraintData.cs +++ b/spine-csharp/src/IkConstraintData.cs @@ -39,6 +39,7 @@ namespace Spine { internal List bones = new List(); internal BoneData target; internal int bendDirection = 1; + internal bool stretch; internal float mix = 1; /// The IK constraint's name, which is unique within the skeleton. @@ -68,6 +69,14 @@ namespace Spine { set { bendDirection = value; } } + /// + /// When true, if the target is out of range, the parent bone is scaled on the X axis to reach it. + /// If the bone has local nonuniform scale, stretching is not applied. + public bool Stretch { + get { return stretch; } + set { stretch = value; } + } + public float Mix { get { return mix; } set { mix = value; } } public IkConstraintData (string name) { diff --git a/spine-csharp/src/Skeleton.cs b/spine-csharp/src/Skeleton.cs index f774e0490..2ce1b0df4 100644 --- a/spine-csharp/src/Skeleton.cs +++ b/spine-csharp/src/Skeleton.cs @@ -309,6 +309,7 @@ namespace Spine { for (int i = 0, n = ikConstraints.Count; i < n; i++) { IkConstraint constraint = ikConstraintsItems[i]; constraint.bendDirection = constraint.data.bendDirection; + constraint.stretch = constraint.data.stretch; constraint.mix = constraint.data.mix; } diff --git a/spine-csharp/src/SkeletonBinary.cs b/spine-csharp/src/SkeletonBinary.cs index 5402df094..fc46572e2 100644 --- a/spine-csharp/src/SkeletonBinary.cs +++ b/spine-csharp/src/SkeletonBinary.cs @@ -210,6 +210,7 @@ namespace Spine { data.target = skeletonData.bones.Items[ReadVarint(input, true)]; data.mix = ReadFloat(input); data.bendDirection = ReadSByte(input); + data.stretch = ReadBoolean(input); skeletonData.ikConstraints.Add(data); } @@ -648,7 +649,7 @@ namespace Spine { IkConstraintTimeline timeline = new IkConstraintTimeline(frameCount); timeline.ikConstraintIndex = index; for (int frameIndex = 0; frameIndex < frameCount; frameIndex++) { - timeline.SetFrame(frameIndex, ReadFloat(input), ReadFloat(input), ReadSByte(input)); + timeline.SetFrame(frameIndex, ReadFloat(input), ReadFloat(input), ReadSByte(input), ReadBoolean(input)); if (frameIndex < frameCount - 1) ReadCurve(input, frameIndex, timeline); } timelines.Add(timeline); diff --git a/spine-csharp/src/SkeletonJson.cs b/spine-csharp/src/SkeletonJson.cs index 37047feb1..5ba575a80 100644 --- a/spine-csharp/src/SkeletonJson.cs +++ b/spine-csharp/src/SkeletonJson.cs @@ -182,6 +182,7 @@ namespace Spine { if (data.target == null) throw new Exception("Target bone not found: " + targetName); data.bendDirection = GetBoolean(constraintMap, "bendPositive", true) ? 1 : -1; + data.stretch = GetBoolean(constraintMap, "stretch", false); data.mix = GetFloat(constraintMap, "mix", 1); skeletonData.ikConstraints.Add(data); @@ -597,7 +598,8 @@ namespace Spine { float time = (float)valueMap["time"]; float mix = GetFloat(valueMap, "mix", 1); bool bendPositive = GetBoolean(valueMap, "bendPositive", true); - timeline.SetFrame(frameIndex, time, mix, bendPositive ? 1 : -1); + bool stretch = GetBoolean(valueMap, "stretch", false); + timeline.SetFrame(frameIndex, time, mix, bendPositive ? 1 : -1, stretch); ReadCurve(valueMap, timeline, frameIndex); frameIndex++; }