Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

Question Multi-Aim Constraint Rollover Issue

Discussion in 'Animation Rigging' started by Yandalf, Apr 29, 2021.

  1. Yandalf

    Yandalf

    Joined:
    Feb 11, 2014
    Posts:
    491
    I'm currently working on making a human character follow a LookAt target with both the head and (upper) spine, to achieve a more natural look. The goal for this is to work in a 2.5D game and allow the character a 360° viewing angle by rotating the head and upper body (think twin-stick shooter style seen from the side, like old Contra games).
    After some experimenting around I came up with a solution that has a "target transform" within the spine rig execute a LookAt to the target on itself with a Multi-Aim Constraint, which then becomes the Tip Target of a Chain Twist Constraint (published by Unity on Siggraph 2019, shared at the end of this talk) which distributes the desired rotation over the spine chain.
    The LookAt Constraint is set up to rotate only over the up-axis of the spine (Y) with the Z-axis as the LookAt axis.
    My problem is that if the target is behind the character and the character turns its body and head to see the target, at a certain distance suddenly the LookAt constraint flips so its X-axis points towards its opposite direction. This of course causes a twist that completely breaks the spine, and is an immediate jerk motion to boot.
    I have tried to fix this by constraining the X-axis as well which did nothing but cause an extra jitter, and messing around with the limits didn't seem to affect this any way whatsoever.
    Screenshot to illustrate included.
    On the left you see the LookAt Constraint object selected, pointing with the z-axis towards the red cross which is the LookAt target. On the right, the target has been moved a bit further, and the selected constraint object has flipped.

    Any help in getting this fixed is highly appreciated.
     

    Attached Files:

  2. Yandalf

    Yandalf

    Joined:
    Feb 11, 2014
    Posts:
    491
    Ok, after some more debugging I've concluded the problem is not with the Multi-Aim Constraint, but instead with the Twist Chain Constraint.
    I am assuming some Quaternion math is going wrong here due to each transform in the chain being a child of the previous one, but I'm unsure how to solve this.
    The source code for the Twist Chain is pretty simple and just lerps between two Quaternions:
    Code (CSharp):
    1.             // Retrieve root and tip rotation.
    2.             Quaternion rootRotation = rootTarget.GetRotation(stream);
    3.             Quaternion tipRotation = tipTarget.GetRotation(stream);
    4.  
    5.             float mainWeight = jobWeight.Get(stream);
    6.  
    7.             // Interpolate rotation on chain.
    8.             for (int i = 0; i < chain.Length; ++i)
    9.             {
    10.                 chain[i].SetRotation(stream, Quaternion.Lerp(chain[i].GetRotation(stream), Quaternion.Lerp(rootRotation, tipRotation, weights[i]), mainWeight));
    11.             }
     
  3. Yandalf

    Yandalf

    Joined:
    Feb 11, 2014
    Posts:
    491
    I've been further debugging this and been trying to write my own constraint to solve this issue.
    The root cause of the problem is that Quaternions are incapable of describing rotations over 180° on an axis, so after some looking around I found a suggestion here to use an AngleAxis approach instead.

    So my approach now:

    Code (CSharp):
    1.         Quaternion rootRotation = rootTarget.GetRotation(stream);
    2.         Quaternion tipRotation = tipTarget.GetRotation(stream);
    3.         rootRotation.ToAngleAxis(out float angleRoot, out Vector3 axisRoot);
    4.         tipRotation.ToAngleAxis(out float angleEnd, out Vector3 axisEnd);
    5.         for(int i = 0; i < chain.Length; i++)
    6.         {
    7.             float selfWeight = weights[i];
    8.             var tarAngle = Mathf.LerpAngle(angleRoot, angleEnd, selfWeight);
    9.             var tarAxis = Vector3.Slerp(axisRoot, axisEnd, selfWeight);
    10.             var targetRot = Quaternion.AngleAxis(tarAngle, tarAxis);
    11.             chain[i].SetRotation(stream, targetRot);
    12.         }
    However, despite this I still encounter the same issue. I further debugged this and found an interesting occurence.

    The start rotation for my object is at (0, 90, 0) in worldSpace so rootRotation.ToAngleAxis returns axisRoot = (0,1,0) and angleRoot = 90.
    With the tipRotation going from 90° to 270°, everything behaves normally.
    However, as soon as we hit the 270° mark, Mathf.LerpAngle returns values that are under angleRoot's value (in the case of 270°, the returned value is 17°, then linearly grows from there again). How is it possible that LerpAngle(90, 270, 0.4) returns 17? According to the documentation LerpAngle only has to account for over 360° wraparound, but it doesn't give any details on how exactly this works.

    Interestingly, with a regular Lerp the angles are correct, but converting them back to a Quaternion still results in the flipping behaviour.
     
    Last edited: May 5, 2021