Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. We have updated the language to the Editor Terms based on feedback from our employees and community. Learn more.
    Dismiss Notice

Resolved Quaternion.LookRotation() in combination with Quaternion.Euler creates weird rotations

Discussion in 'Scripting' started by TheMatrixAgent22, Aug 11, 2023.

  1. TheMatrixAgent22

    TheMatrixAgent22

    Joined:
    Jul 9, 2017
    Posts:
    41
    Hello there!
    I've been working a few on a Foot-IK system. Only a few months ago did I realize that there is now something called TwoBoneIKConstraint that WORKS WITH THE ANIMATOR, and don't have to use humanoid rigs (since it's a simple game, I 100% prefer to animate everything in Unity, which is not possible with humanoid animations).
    In the end it all came down to computing the angle of the foot. It has been a REAL challenge, especially since I'm really bad with Quaternions and rotations in general.
    Here is the code that works great:
    Code (CSharp):
    1. Physics.Raycast(rightLegTarget.position + (Vector3.up * 3), Vector3.down, out RaycastHit hit, 30, LayerMask.GetMask("Terrain"));
    2.  
    3.             rightLegTarget.position = hit.point + positionOffset;
    4.             Vector3 crossVector = Vector3.Cross(rightFoot.right, hit.normal);
    5.             rightLegTarget.rotation = Quaternion.LookRotation(crossVector, hit.normal) * Quaternion.Euler(rotationOffset) * Quaternion.Euler(0, transform.rotation.y, 0);
    Aside from one thing! The rotation offset is there, because the foot has the rotation of (95, 0, 0) when flat. If it's (0,0,0) it just points up. It's how my rig is for some reason, and can't fix that. The offset works. However, when I enter 'non-nice' values like 95/105/etc. instead of 90/180/etc. the foot slowly rotates around the 'global Y axis' (to its right/left). I've tried lots of things, none of which have worked, let alone fixed the problem.
    Maybe you guys can enlighten me?
    Thanks a lot in advance,
    Alvin.
     
  2. wideeyenow_unity

    wideeyenow_unity

    Joined:
    Oct 7, 2020
    Posts:
    728
    Maybe something I wrote in this post will help?
    https://forum.unity.com/threads/problem-with-x-axis-rotation.1472409/#post-9210924

    But when you say:
    This makes me think you're referring to Euler angles, which is the simplified Vector3(X,Y,Z) like what you see in the Inspector. Now messing with any rotations of a transform, those are Quaternions(X,Y,Z,W). So to convert a quat to euler you would need
    transform.rotation = Quaternion.Euler(X, Y, Z);
    . Which I see you're already playing around with.

    But as far as your mention of "non-nice" values, I don't think I've had that issue. Maybe if going over the euler of 360 or under -360? But then again it might just be your rotation call. It would be best to just get a rotation(if euler) set first, before telling the transform to rotate to it.

    Sorry if I'm off point, it's kinda hard to compile code in my head. :)
     
  3. TheMatrixAgent22

    TheMatrixAgent22

    Joined:
    Jul 9, 2017
    Posts:
    41
    I am only working with eulerAngles for the offset, since I am setting it in the inspector (I am ABSOLUTELY NOT setting Quaternions in the inspector).
    The
    crossProduct
    of
    foot.right
    and
    hit.normal
    gives me a Vector3 going along the floor beneath me. The last line calculates the rotation taking into account the offset that I am trying to set to (95,0,0) and the direction the player is facing (though I am not really sure WHY
    Quaternion.Euler(0, transform.rotation.y, 0)
    makes it follow the facing direction of the player as opposed to
    Quaternion.Euler(0, transform.eulerAngles.y, 0)
    , but anyway). If I set (90,0,0) for offset, everything works correctly... that's where I'm lost.
    I did try to store the rotation in a variable first, and then set it, which changed nothing. Thank you for your quick reply though!
     
  4. wideeyenow_unity

    wideeyenow_unity

    Joined:
    Oct 7, 2020
    Posts:
    728
    gotcha, yeah I heard issues about using
    eulerAngles
    as @Kurt-Dekker will probably well inform you on. But I think they cause a gimbal lock issue. It's recommended to modify rotations more like I showed in that other post.
     
  5. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    6,003
    The problem with euler angles is that they can represent multiple different rotations, and that the euler angles you see in the inspector often are not the same as the actual underlying values.

    While they're there for an understandable convenience, they generate create more troubles than they're worth.

    It looks like you want rotations around particular axis, so you should opt to use Quaternion.AngleAxis instead.
     
    wideeyenow_unity likes this.
  6. TheMatrixAgent22

    TheMatrixAgent22

    Joined:
    Jul 9, 2017
    Posts:
    41
    I see. I should basically avoid eulerAngles altogether...
    I changed the last line to this:
    Quaternion finalRotation = Quaternion.LookRotation(crossVector, hit.normal) * Quaternion.AngleAxis(rotationOffset, leftFoot.right);
    . I am leaving out the player's rotation for now. This seemed to work... when I increased rotationOffset the foot was starting to move downward, but when I get to around 60/70 it starts spinning randomly. What am I doing wrong here?
    Thanks a lot for your help!
     
  7. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    6,003
    You're not supplying any 'up' direction to your look rotation, which by default just uses
    Vector3.up
    . The closer you get to directly up or directly down, the more you'll find you get unreliable results.

    So you will need to work out an appropriate up direction in this situation, preferably something close to perpendicular to it.
     
  8. TheMatrixAgent22

    TheMatrixAgent22

    Joined:
    Jul 9, 2017
    Posts:
    41
    Is there another argument I have to use? I thought
    hit.normal
    is the upward argument in
    Quaternion.LookRotation(crossVector, hit.normal)
    ... I'm really confused now.
     
  9. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    6,003
    Right, sorry I missed that you were already using the normal.

    Worth nothing that the order in which you multiple rotations has an effect on the result.

    Mind you, shouldn't the look rotation on its own be sufficient to orient a foot to a surface?
     
  10. TheMatrixAgent22

    TheMatrixAgent22

    Joined:
    Jul 9, 2017
    Posts:
    41
    That's the thing! It is! However, due to the way my rig is setup, the foot is pointing up. I need to ALWAYS add an extra rotation of (95,0,0) for the foot to be on the ground. That is what the offset is. That is also mostly what I'm struggling with. Here's what
    Quaternion finalRotation = Quaternion.LookRotation(crossVector, hit.normal);
    does.
     
  11. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    6,003
    Well it might just be a case of multiplying the rotations in the reverse order.

    Though are you using the foot bone's
    transform.right
    as the direction vector? That's probably going to be unreliable at it will constantly be changing. You might want a static reference transform (basically make another empty a child of the lower leg bone) that you use as your directional reference.

    You also probably want to be using Debug.Draw ray and similar to actually visualise the directions you're producing. Highly likely that you're producing directions that vary wildly from what you expect. Just a case of debugging.
     
  12. TheMatrixAgent22

    TheMatrixAgent22

    Joined:
    Jul 9, 2017
    Posts:
    41
    In the end, the problem was the direction vector, you were right! I changed it simply to Vector3.right, which was close enough to what I needed, and the random movement stopped. This is the code now:
    Quaternion finalRotation = Quaternion.AngleAxis(rotationOffset, Vector3.right) * Quaternion.LookRotation(crossVector, hit.normal);

    Now the only thing missing, is aligning this to the rotation of the character. I tried
    Quaternion.Euler(0, transform.rotation.y, 0)
    (which seems to only work with the code in the thread subject... I was confused as to why it worked) as well as
    Quaternion.AngleAxis(transform.eulerAngles.y, transform.up)
    (transform of course being the transform of the actual player GameObject). None of which worked. Do you maybe have another idea/know why that doesn't work?
     
  13. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    6,003
    I think you just want to do what I suggested already, which is to have another reference transform in the object heirarchy to have a consistant direction source.

    Mind you, the knee's
    transform.right
    will probably get you the same result.
     
  14. PraetorBlue

    PraetorBlue

    Joined:
    Dec 13, 2012
    Posts:
    7,735
    You wrote:
    Code (CSharp):
    1. Quaternion.Euler(0, transform.rotation.y, 0);
    Which makes absolutely no sense.

    transform.rotation.y
    is the y part of a Quaternion. It is NOT an euler angle and it makes NO sense to use it as an Euler angle. That will easily explain why you might be getting nonsense results. In fact transform.rotation.y is largely guaranteed to be somewhere between -1 and 1 and therefore may explain the whole "slow rotation around y axis" thing.
     
    Bunny83 and spiney199 like this.
  15. TheMatrixAgent22

    TheMatrixAgent22

    Joined:
    Jul 9, 2017
    Posts:
    41
    Yes, that's what I thought. Though if I try
    Quaternion finalRotation = Quaternion.LookRotation(crossVector, hit.normal) * Quaternion.AngleAxis(rotationOffset, Vector3.right) * Quaternion.AngleAxis(rotationOffset1, hit.normal)
    with
    rotationOffset1
    being the 'player.eulerAngles.y' (just made a float that I can control for testing). I finally figured out, that 'hit.normal' will always point to 'foot.up'. And now adjusting 'rotationOffset1' does spin the foot around. However... constantly. Like it doesn't stop. The rotationOffset1 only dictates the speed. I've also tried all combinations for the quaternions. None of them work as expected. This combiation at least actually rotates it around the axis I tell it to. Just not the way I want it. What do you recon I could do to fix it?
    Once again, thanks a lot to all of you for helping!!!
     
  16. wideeyenow_unity

    wideeyenow_unity

    Joined:
    Oct 7, 2020
    Posts:
    728
    In my particular case for rotating bones in a blender model, I find what is normal(stand straight tall) and only modify from that. But I normally work with flat surfaces, so I haven't needed the normal yet for things such as an incline. So I only use height references with raycast, or common floor heightY.

    But I would assume to find what is the foots normal rotation, and the offset needed to give a vector3.up, and compare that direction with what your surface normal is, to know foot would only need (31,2,78) to match that floors normal.

    But I did have a problem with shoulder bones being weird, and messing with what I wanted normal arm rotations to be, so I had to go into blender and make the shoulder bones straight, then reapply their transforms to make the "after" math more easy. Which sucks because if you need to re-use that script for a new model, you have to keep making those exact changes.

    So you're probably right to just get the bones as they normally are, and find the offset to what normally is "up", regardless of bone roll. And just have the offsets to be the modifications.. I'll have to play around with that, because it'll definitely help my future projects. :)
     
  17. TheMatrixAgent22

    TheMatrixAgent22

    Joined:
    Jul 9, 2017
    Posts:
    41
    In the end, after vigorous testing, I got it to work... this is the way:
    Code (CSharp):
    1. Physics.Raycast(leftLegTarget.position + (Vector3.up * 3), Vector3.down, out RaycastHit hit, 30, LayerMask.GetMask("Terrain"));
    2.  
    3. leftLegTarget.position = hit.point + positionOffset;
    4. Quaternion xOffset = Quaternion.AngleAxis(rotationOffsetXleft, Vector3.right);
    5. Quaternion angle = Quaternion.LookRotation(Vector3.Cross(leftFoot.transform.right, hit.normal), hit.normal);
    6. leftLegTarget.rotation = angle * xOffset;
    xOffset
    is just a quaternion that adds an extra rotation on the 'x' axis, you might wanna change the 'Vector3.right' to a better reference point like wideeyenow_unity mentioned above though. For me it works just fine. You can also change it to another axis if you need to. Also make sure to apply
    angle
    and
    xOffset
    in this order! Apparently, I don't even need an extra Quaternion for the rotation of the player, since the
    transform.right
    is plugged into the
    Vector3.Cross()
    method.
    positionOffset
    is exactly what it says it is. It adjusts the position from the IK position. Mostly for if the foot lands too far down or up.
    Thanks a lot to everyone for the help!!!
     
  18. wideeyenow_unity

    wideeyenow_unity

    Joined:
    Oct 7, 2020
    Posts:
    728
    Correct me if I'm wrong, but:
    The first position is where the ray starts, and the second position is where the ray goes to(shoots). So I think you have this backwards, as the ray is shooting from target position to the models (0,1,0)(or possibly world position(0,1,0)). Unless of course that's what you meant to do?
     
  19. TheMatrixAgent22

    TheMatrixAgent22

    Joined:
    Jul 9, 2017
    Posts:
    41
    I want to shoot a ray from the foot, downward.
    leftLegTarget.position + (Vector3.up * 3)
    starts a ray from 3 units above the foot: foot position + 3 units in direction (0,1,0) (in case we are moving up) and shoots it sraight down (0, -1, 0).
    Edit: I APOLOGIZE. I didn't clarify what the variables were... this is completely my fault. I just noticed.
    leftLegTarget
    is the Transform of the TARGET of the TwoBoneIKConstraint, which constantly changes the position and rotation of the foot. I use this so I can apply rotations and position WHILE the animation is running, which I was so happy about in my first post. The target is simply the object it tries to immitate. There's a dozen of reasons as to why this makes it so much easier!
     
  20. wideeyenow_unity

    wideeyenow_unity

    Joined:
    Oct 7, 2020
    Posts:
    728
    That's what I thought, it's just
    looks like target means where it wants to go, and you're adding another (0,3,0) units of measurement on top of that. How big is this model? lol.

    Best thing for debugging lines/rays is either
    Debug.DrawLine
    or
    Debug.DrawRay
    , one works with position-position, the other with position-direction. So I would definitely play around with those, and make sure your positions and possibly directions are all exactly as you want them. :)
     
  21. TheMatrixAgent22

    TheMatrixAgent22

    Joined:
    Jul 9, 2017
    Posts:
    41
    Yea,
    Debug.DrawRay
    has been a lifesaver, especially for this script! Everything works great now, and I'm really grateful for your help. Thanks a lot!!!

    For anyone in need of the script, here it is!
    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEngine.Animations.Rigging;
    3.  
    4. public class FootAnimationEngine : MonoBehaviour {
    5.  
    6.     //use this script with animations that have events that set the weight of the IKConstraints correctly. Use the SetIKWeightRight and SetIKWeightLeft methods for this! This script should be used with TwoBoneIKConstraints for the IK, though you can feel free to change it to your heart's desire!
    7.  
    8.     #region
    9.     [Header("Foot Animation with Comuputational Kinematics (F***) variables")]
    10.     [Space]
    11.     [Header("Object References")]
    12.     [SerializeField] TwoBoneIKConstraint leftLegIK; //IK constraint component
    13.     [SerializeField] TwoBoneIKConstraint rightLegIK;
    14.     [SerializeField] Transform leftLegTarget; //the target of the IK constraint (sets the position and rotation of the foot)
    15.     [SerializeField] Transform rightLegTarget;
    16.     [SerializeField] Transform leftFoot; //the transform of the foot (bone)
    17.     [SerializeField] Transform rightFoot;
    18.     [SerializeField] Transform pelvis; //the pelvis for offsetting the height when the slope gets steeper
    19.     Animator animator;
    20.  
    21.     [Header("IK Variables")]
    22.     [SerializeField] LayerMask terrainMask; //terrain mask for raycasts
    23.     [SerializeField] Vector3 positionOffset; //applied on top of IK position
    24.     [SerializeField] float rotationOffsetX; //the float for the Quaternion.AngleAxis offset
    25.     [SerializeField] float pelvisOffsetPerDeg = 0.2f; //how much to change the pelvis height per degree of steepness on a slope
    26.     float pelvisHeightYOrigin; //saves the intital height of the pelvis when the script loads
    27.  
    28.     [Header("Switches")]
    29.     [SerializeField] bool enableFootIK = true;
    30.     #endregion
    31.  
    32.     void Start() {
    33.         animator = GetComponent<Animator>();
    34.         pelvisHeightYOrigin = pelvis.localPosition.z; //FOR ME IT IS localPosition.z. FOR YOU IT'S MOST LIKELY localPosition.y!!! USE THE AXIS THAT ALWAYS POINTS UP!!!
    35.     }
    36.  
    37.     private void FixedUpdate() {
    38.  
    39.         if (enableFootIK) RunFootIK();
    40.     }
    41.  
    42.     private void RunFootIK() {
    43.  
    44.         //don't run unneccessary calculations, if the IK is not being applied
    45.         if (leftLegIK.weight > 0) {
    46.             if (Physics.Raycast(leftFoot.position + (Vector3.up * 3), Vector3.down, out RaycastHit hit, 10, terrainMask)) {
    47.  
    48.                 leftLegTarget.position = hit.point + positionOffset; //set the position
    49.                 Quaternion xOffset = Quaternion.AngleAxis(rotationOffsetX, Vector3.right); //add an offset. You may change Vector3.right with anything to add an offset to any axis!
    50.                 Quaternion angle = Quaternion.LookRotation(Vector3.Cross(leftFoot.transform.right, hit.normal), hit.normal); //Calculate foot rotation
    51.                 leftLegTarget.rotation = angle * xOffset; //add rotation. MAKE SURE IT IS IN THIS ORDER!
    52.                 animator.SetBool("inAir", false);
    53.  
    54.             } else {
    55.                 animator.SetBool("inAir", true); //this is optional
    56.             }
    57.  
    58.         }
    59.         if (rightLegIK.weight > 0) {
    60.             if (Physics.Raycast(rightFoot.position + (Vector3.up * 3), Vector3.down, out RaycastHit hit, 10, terrainMask)) {
    61.  
    62.                 rightLegTarget.position = hit.point + positionOffset;
    63.                 Quaternion xOffset = Quaternion.AngleAxis(rotationOffsetX, Vector3.right);
    64.                 Quaternion angle = Quaternion.LookRotation(Vector3.Cross(rightFoot.transform.right, hit.normal), hit.normal);
    65.                 rightLegTarget.rotation = angle * xOffset;
    66.                 animator.SetBool("inAir", false);
    67.                 MovePelvisHeight(Vector3.Angle(Vector3.up, hit.normal)); //move pelvis height. Vector3.Angle(Vector3.up, hit.normal) simply returns the angle between up and the Vector pointing AWAY from the slope in degrees
    68.             } else {
    69.                 animator.SetBool("inAir", true);
    70.             }
    71.         }
    72.  
    73.     }
    74.  
    75.     private void MovePelvisHeight(float slopeAngle) {
    76.         float offset = slopeAngle * pelvisOffsetPerDeg;
    77.         //positionOffset = 7 * offset * Vector3.up; USE THIS IF THE FOOT TARGETS DON'T MOVE WITH THE PELVIS. For me it doesn't and this line fixes this as best as possible. You might want to change 7 to something that suits your needs. Also make sure to not change positionOffset in the editor while the game is running if you're using this
    78.         pelvis.localPosition = (pelvisHeightYOrigin - offset) * Vector3.forward; //offset pelvis. LIKE PREVIOUSLY STATED, CHANGE Vector3.forward WITH THE AXIS THAT POINTS UP!!!
    79.     }
    80.  
    81.     public void SetIKWeightRight(float amount) => rightLegIK.weight = amount;
    82.     public void SetIKWeightLeft(float amount) => leftLegIK.weight = amount;
    83.  
    84. }
    85.