AvatarPoseRotationConstraint's math is wrong when the constraint is not aligned with the user root
Describe the bug?
MathX.LimitTwist and MathX.LimitSwing limit rotation to a set amount of degrees. There are vector inputs but they only define which parts of the rotation are twist and swing respectively.
The rotation is still limited to an absolute "magnitude" at the end.
Since the absolute magnitude is limited this operation cannot be preserved when rotating the coordinate system. *
The implementation of AvatarPoseRotationConstraint.ProcessPose is built on the assumption that the opposite is true and tries to convert the inputs of the constraint into the avatar root's coordinate system.
This will cause incorrect results when the constraint's slot and the avatar root don't align in rotation. (column "Original" in the attached picture)
I created a mod that correctly implements the constraint. The results are visible in the attached picture. (column "Fixed")
A correct computation could be this:
Slot slot = this.Slot;
rotation = slot.SpaceRotationToLocal(in rotation, space);
float3 axis = Axis;
float3 orthoAxis = TwistReferenceAxis;
rotation = MathX.LimitSwing(in rotation, in axis, MaxSwing);
rotation = MathX.LimitTwist(in rotation, in axis, in orthoAxis, MaxTwist);
rotation = slot.LocalRotationToSpace(in rotation, space);
*) I hid the math under "additional context".
To Reproduce
- Create an AvatarAnchor with an AvatarPoseRotationConstraint and a small amount of
MaxTwistand/orMaxSwing! - Enter it with different rotations of your user space!
- Rotate your hand/foot/whatever is constrained!
- Rotate the constraint!
- Observe how well the hand is aligned with the constraint!
Reproduction Item/World
An avatar anchor that puts a constraint on the hand: resrec:///U-TheAutopilot/R-ff060d20-233c-40a2-801e-c16ce85fc486 The constraint can then be moved around.
Expected behavior
The constrained limb should always stay within the bound of the constraint even if it is rotated compared to the user's root.
Screenshots
The bug is visible in the column "Original". Note how the forward direction of the hand deviates a lot from the forward direction of the constraint!
Resonite Version Number
2025.5.16.1282
What Platforms does this occur on?
Windows
What headset if any do you use?
No response
Log Files
I can provide them on request.
Additional Context
To simplify the check i will only look at limiting the rotation relative to a single axis:
Original = MathX.LimitSwing(Input, T(Constraint->UR) * Axis, MaxSwing)
Fixed = T(Constraint->UR) * MathX.LimitSwing(T(UR->Constraint) * Input, Axis, MaxSwing)
The current implementation assumes correct behavior. (i.e. Original = Fixed)
T(Constraint->UR) is the transform from the constraint's space to the user root's space.
If one assumes that constraint and user root only differ in a rotation around Axis it simplifies the computation to a single angle around that axis:
Original.Angle = LimitSwing2D(Input.Angle, MaxSwing)
Fixed.Angle = T(Constraint->UR).Angle + LimitSwing2D(T(UR->Constraint).Angle + Input.Angle, MaxSwing)
In the 2D case LimitSwing2D is actually just clamping the input angle around +-MaxSwing:
Original.Angle = MathX.Clamp(Input.Angle, -MaxSwing, +MaxSwing)
Fixed.Angle = T(Constraint->UR).Angle + MathX.Clamp(T(UR->Constraint).Angle + Input.Angle, -MaxSwing, +MaxSwing)
Let's make the transform between the coordinates a bit simpler to look at with T(Constraint->UR)=d and T(UR->Constraint)=-d where d is the rotation of the constraint relative to the user's root:
Original.Angle = MathX.Clamp(Input.Angle, -MaxSwing, +MaxSwing)
Fixed.Angle = d + MathX.Clamp(Input.Angle - d, -MaxSwing, +MaxSwing)
+d can be moved into the Clamp if it is applied to all inputs equally:
Original.Angle = MathX.Clamp(Input.Angle, -MaxSwing, +MaxSwing)
Fixed.Angle = MathX.Clamp(Input.Angle - d + d, d-MaxSwing, d+MaxSwing)
Simplifying that yields the result that the two implementations are only truly equal for d=0 (when constraint aligns with user root):
Original.Angle = MathX.Clamp(Input.Angle, -MaxSwing, +MaxSwing)
Fixed.Angle = MathX.Clamp(Input.Angle, d-MaxSwing, d+MaxSwing)
Furthermore it shows that for small d and Input.Angle compared to a relatively large MaxSwing it is possible to get the same result. This could explain why this bug hasn't been reported yet because it sometimes works.
Reporters
@JackTheFoxOtter actually found the bug and requested me to figure out why it is. :-)
Now that we know how exactly the current implementation is incorrect, we have the knowledge to compromise for the incorrectness using ProtoFlux! The following setup can be used to correct the issues with AvatarPoseRotationConstraint pose filters present in the current Resonite build.
Attention: This workaround will break once the underlying issue is fixed, so use this with care. When it does break, you'll have to rip the correction code out of your creation.
The example item demonstrating the fix can be found here:
resrec:///U-JackTheFoxOtter/R-74C9AD538FB405C7D1BA157997DD06F816325E87631B421E49DDF224331F06A3