Resonite-Issues icon indicating copy to clipboard operation
Resonite-Issues copied to clipboard

AvatarPoseRotationConstraint's math is wrong when the constraint is not aligned with the user root

Open mpmxyz opened this issue 7 months ago • 1 comments

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 MaxTwist and/or MaxSwing!
  • 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! Image

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. :-)

mpmxyz avatar May 21 '25 21:05 mpmxyz

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.

Image

The example item demonstrating the fix can be found here: resrec:///U-JackTheFoxOtter/R-74C9AD538FB405C7D1BA157997DD06F816325E87631B421E49DDF224331F06A3

JackTheFoxOtter avatar May 22 '25 19:05 JackTheFoxOtter