• Bugs
  • Editor: Parent constraint order breaks child constraints

Ran into this bug in the editor when building a monstrously overengineered leg rig. Transform constraints between children of a bone will be ignored if the parent bone is moved afterwards by another constraint, be it an IK chain on bones above it, or a transform constraint tying it to something, even if the transform constraint doesn't alter the property the children alter. Flipping the local or relative switches on constraints doesn't appear to change things.

If the offending parent constraint is moved ahead of the child constraints in the constraint order, then everything behaves as expected, but in this case there's no reason I can see that the children should be dependent in any way on how the parent moves. They are set to copy each other's rotation in local space, and are even separated from the parent by another layer of bones.

In the rig I was working with in my own project, I had a convoluted rotation setup for a foot that could result in the ankle shifting around. I wanted that to be able to offset the IK target for the leg it was attached to, but this ordering bug means the IK MUST be resolved before any constraints in the foot, which precludes reading things from the foot to affect that IK chain... or offsetting the foot itself to resolve the ankle shift!

This bug occurs in 3.8.99 and 4.0.74-beta

Project with test cases attached!

Related Discussions
...

Thanks for the thorough explanation! It is a gotcha certainly, but unfortunately it's not actually a bug. It is described here:
Constraints: Bone transforms

As you know, when a constraint is applied it modifies the transforms of the constrained bones. Glossing over a few minor details: Afterward, the constrained bones need to recalculate their world transform by combining their parent bone world transform and their local transform (Bone updateWorldTransform in the Spine Runtimes). After that's been done, all the children of the constrained bones also need to recalculate their world transform. This is because, like all bones (except the root), their world transform is a combination of their parent bone world transform and their local transform.

For example, if a constraint moves a bone, that bone and its children need to recompute their world transform. If the children didn't, then they would remain in the same world position as before the parent bone moved.

The world transform stores rotation, scale, and shear combined in a 2x2 affine transformation matrix and translation X and Y are stored separately. We don't currently make a distinction between the translation and the rest needing to be recomputed, we just recomputed the whole world transform. It could be that we could make improvements in this area, but we'd need to be careful not to let it complicate things any further than they already are.

Huh. I guess my confusion stems from spending a lot of time with Unity's transforms, I expected that when the transform of the parents were changed that the calculation would use the altered local transform, and that because there was nothing that'd cause a loop or a dependency that it'd work fine.

...But if I'm understanding you correctly, Spine isn't actually altering the local transform, is it? That's all staying stable behind the scenes and the constraints are just applying a temporary shift that's "non canonical" as it were, just written into the outputted, visible skeleton but not stored in any way on the local transform of the real skeleton. Then, when something higher in the hierarchy is updated in the constraint, it's all wiped as the updater walks down the graph and recomputes from the base, unaltered local transforms.

Hm.

I guess for it to work like I expected it to work, one of two things would need to happen:

1) The constraints modify a local transform offset that's added to the local transform during the recomputation phase. Because the recomputation happens with the real local transform plus the changes earlier constraints made, their changes are preserved. You'd need to zero out that offset on every bone at the start of constraint processing. This is what I'd erroneously assumed it was doing.

2) When recalculating the children instead of walking the graph and chaining transform matrices, you instead apply a bulk transformation on the children using the matrix that would have taken the parent bone from its old transform to its new one. If I haven't screwed up my vector algebra, applying that matrix to their world transform should be the same as applying it to the parent and then chaining the local matrices... that is, it would be if there wasn't that wrinkle of the non-inherited transform properties.

I guess you need to walk the graph anyway to find all the things that need the transformation, so you could special case it at the bones with inheritance toggles and apply a different transformation once you're inside that branch, though getting that right seems like it might have some gotchas? If the standard procedure is to just ignore any changes to the properties that aren't inherited, then I think a new old->new transformation matrix calculated on the offending bone would work for all of its children, but not 100% sure off the top of my head.

At any rate, I'd missed that caveat in the manual and that had left me really baffled by some of the constraint behaviour I was seeing... it all makes quite a bit more sense now! I think that means there isn't a way to push information up a hierarchy and and do any processing on it if it'd affect the original, which seems unfortunate. Maybe you could work around it by copying and reapplying the constraints for a second go at it, but I've already got 2+ pages of constraints to scroll through... so I think I'll pass on duplicating them for now. ^_^;

Still, at least now I can spot that limitation coming and plan around it, so thank you for the clarification!

1 个月 后

Sorry for the delay, I meant to look into this deeper quite a while ago.

IanM :

Spine isn't actually altering the local transform, is it? That's all staying stable behind the scenes and the constraints are just applying a temporary shift that's "non canonical" as it were, just written into the outputted, visible skeleton but not stored in any way on the local transform of the real skeleton. Then, when something higher in the hierarchy is updated in the constraint, it's all wiped as the updater walks down the graph and recomputes from the base, unaltered local transforms.

You wouldn't want applying constraints to mutate the local transforms. The local transforms make up the skeleton's pose, so for example, if your skeleton was just standing there, each frame you apply constraints and the pose would change. To apply constraints more than once you'd have to change the local transforms back to how they were originally.

To solve that, bones have a second local transform called the "applied transform". A constraint either changes the applied transform or it changes the world transform and marks the applied transform as invalid. Before the applied transform can be used, for example by a constraint, first Bone updateAppliedTransform needs to be called if the applied transform is invalid. This work is avoided if a constraint changes the world transform and nothing else needs the applied transform values.

Note this is low level stuff. It is rare for most users to need to worry about the applied transform.

I reviewed your project again and created a simpler version:
http://n4te.com/x/1823-simpler.spine
I agree it seems odd the changes made by the transform constraint are lost, even when the IK constraint mix is 0. In that case the IK constraint code is bypassed, so it isn't changing any bones. The problem is with the skeleton "update cache", which is the order bones and constraints are updated. The update cache is sorted so parent bones are updated before children, constraint target and constrained bones are updated before constraints are applied, and the children of constrained bones are updated after a constraint is applied.

Here is the update cache for the skeleton in the project I linked:

root-bone
ik-parent-bone
ik-child-bone
transform-target-bone
transform-constrained-bone
transform-constraint
ik-target-bone
ik-constraint
transform-target-bone
transform-constrained-bone

First the root bone is updated, then the IK parent and child bones. After that is the transform constraint target and constrained bones, then the transform constraint is applied. Good so far. Next we have the IK target bone and then the IK constraint is applied. This still makes sense.

Lastly the transform constraint target and constrained bones are updated again. They are updated because their parent bone, ik-child-bone may have been modified by the IK constraint (the update cache doesn't know what the mix is, so they are updated even if the mix is zero). Bones in the update cache are updated by calling Bone updateWorldTransform with no arguments, which uses its local transform, not the applied transform. That is where the changes made by the transform constraint are lost.

We could improve this by first setting the applied transforms for all bones, then bones in the update cache would always use their applied transform, not their original local transform.

One problem with this is the way we mark the applied transform as invalid to defer recomputing it until just before it is needed. In the update cache above, the applied transform for transform-constrained-bone is invalidated when transform-constraint is applied and then recomputed the second time transform-constrained-bone is updated. At that point its parent bone has already been moved and/or rotated by the IK constraint. The recomputed applied transform will use the parent bone's current world transform, which results in the transform-constrained-bone not moving with its parent! To fix that the applied transform needs to be recomputed before the parent bone's world transform changes. For example, instead of deferral, we could just recompute the applied transform. This is a little unfortunate if it is not needed by anything else (the cost is a few trig calls more than updating a bone). I'm not sure there is a better way though.

TLDR; we could fix the issue by setting the applied transforms first, then updating bones. Also, constraints that modify the world transform must recompute the applied transform to keep it in sync, which requires slightly more effort than we currently spend.


To follow up, we've decided it is best to implement the changes as discussed above. The applied transform is now always kept in sync with changes made to the world transform. This improves the behavior of constraints and should fixed the problem you initially reported.

The commit is in the 4.0-beta branch, here:
https://github.com/EsotericSoftware/spine-runtimes/commit/4f73fbbb396f406954646af76f167d8238802171

That's awesome to hear, thank you! Looking forward to being able to play with that in the future.

And thank you for spending the time to lay out the underlying implementation, it's cool to have it broken down like this and have the benefit of the thought process behind it. I didn't realize that there was that much computation being deferred under the hood, though now that you mention it it makes sense. It's very easy to see how a large hierarchy could turn into massive repeated calculations if you're not careful.

"Applied transform" is much better terminology than the clumsy word salad I was tossing together in my last post. It's interesting to me how this sort of system lends itself to those kind of layers of separations. A long time ago I built a much more primitive 2D skeleton editor as a hobby project and between that and being a professional tech artist I was very curious about the under-the-hood implementation, so thank you for indulging me!