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

Clamping a Quaternion

Discussion in 'Scripting' started by Huknar, Sep 4, 2015.

  1. Huknar

    Huknar

    Joined:
    Mar 6, 2015
    Posts:
    40
    EDIT: Okay, I found out why it is doing this, at least. It's because a quaternion wraps its eulerAngles to 0-360, so as soon as the camera reaches below 0, it starts counting down from 360, or above 360, counting up from 0. This causes the opposite clamp to come into effect.

    I am not quite sure how to avoid this though, and have my clamps respect this wrapping.

    I've got a problem with my clamping of a quaternion for my camera. The code I have clamps the quaternion to the angle I want, but there is an issue where if I get the character into a certain position outside one of the clamping boundaries, the camera seems to snap into a mirror of its clamped axis, turning the other way rather than trying to look at the point it is focused on.



    Code (CSharp):
    1. void CameraFixed() {
    2.     if(LookAt) {
    3.         targetRot = Quaternion.LookRotation((cameraLookAt.transform.position + LookAtOffset) - Camera.main.transform.position);
    4.         ClampCameraAngle(ref targetRot);
    5.          
    6.         Camera.main.transform.rotation = Quaternion.Slerp(Camera.main.transform.rotation, targetRot, Time.deltaTime*LookAtSmoothness);
    7.     }
    8. }


    (This next code is applied before the quaternion is used in a Slerp.)

    Code (CSharp):
    1. void ClampCameraAngle(ref Quaternion targetRotation) {
    2.     float rotPosX = Mathf.Clamp(targetRotation.eulerAngles.x, initialRotation.x - MinClamp.x, initialRotation.x + MaxClamp.x);
    3.     float rotPosY = Mathf.Clamp(targetRotation.eulerAngles.y, initialRotation.y - MinClamp.y, initialRotation.y + MaxClamp.y);
    4.     float rotPosZ = Mathf.Clamp(targetRotation.eulerAngles.z, initialRotation.z - MinClamp.z, initialRotation.z + MaxClamp.z);
    5.  
    6.     targetRotation = Quaternion.Euler(new Vector3(rotPosX, rotPosY, rotPosZ));
    7. }


    I can fix this issue by clamping the euler angles after Slerping, but then I lose that elastic smoothness that doesn't cause the camera to feel it's hitting a brick wall.

    Rotations are not my strong point, so I'd appreciate some help in understanding why this is happening and how I can avoid it.
     
    Last edited: Sep 4, 2015
  2. Huknar

    Huknar

    Joined:
    Mar 6, 2015
    Posts:
    40
    Okay, I found out why it is doing this, at least. It's because a quaternion wraps its eulerAngles to 0-360, so as soon as the camera reaches below 0, it starts counting down from 360, or above 360, counting up from 0. This causes the opposite clamp to come into effect.

    I am not quite sure how to avoid this though, and have my clamps respect this wrapping.
     
  3. exvalid

    exvalid

    Joined:
    Oct 20, 2017
    Posts:
    9
    You need to offset your min/max angles to have +180 right befor assigning them if you have them as + and minus numbers.,

    1. float rotPosX = Mathf.Clamp(targetRotation.eulerAngles.x, initialRotation.x - MinClamp.x +180, initialRotation.x + MaxClamp.x+180);
    2. float rotPosY = Mathf.Clamp(targetRotation.eulerAngles.y, initialRotation.y - MinClamp.y +180, initialRotation.y + MaxClamp.y+180);
    3. float rotPosZ = Mathf.Clamp(targetRotation.eulerAngles.z, initialRotation.z - MinClamp.z+180, initialRotation.z + MaxClamp.z+180);

      i think this should work as it will correct the min value to align with the wrapped value.

      another way would be to create a set of if statements to dectect and then clamp but thats another mission.
     
  4. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,514
    It's not that a quaternion wraps its euler angles...

    Quaternions don't have euler angles.

    The 'eulerAngles' property is a conversion of a Quaternion to euler angles.

    ...

    One of your key things you're probably leaving out in your clamping is that a Quaternion is not a orientation. It's not some specific point in rotation.

    A Quaternion is an amount of rotation. It's a rotational vector, in that it has a rotational direction and magnitude. Sort of like how Vector3 is a cartesian vector with linear direction and magnitude.

    This is very different from euler angles. Euler angles represent an orientation, and it does so as a value of rotation around 3 static axes. The problem in recording it as such is that any given orientation can be gotten to through any number of rotations. There's not 1 path to a given orientation.

    For example the rotation <90,40,0> is the same as <90,0,-40>. This creates HUGE flaws in clamping. If you said "I only allow values from -10 to 10 around the z axis", well the first of these rotations satisfy this and are legal, but the second would fail and get clamped to <90,0,-10>.

    BUT THEY'RE THE SAME ORIENTATION!

    And this is why you're probably having problems.

    To define proper clamps of the 3 axes of an euler angle, you must consider the order in which those various angle axes are applied. It's sort of like a gimbal (actually it's exactly like a gimbal). A gimbal has arms for 3 axes, and if you were to put physical constraints on them, well the order of the armatures defines how you'd configure those physical clamps.

    ...

    Quaternion's don't work like this.

    The very nearest real-world visualizable concept you can really think of a Quaternion as is not as a set of 3 axes rotations. But instead as an arbitrary axis and an amount of degrees around it.

    Quaternions have true omni-directional rotation, unlike euler angles, since it can rotate around any arbitrary axis.

    Where as clamping is specifically based on some sort of defined rotational constraint.

    Lets take the 'head' for instance. It is constrained rotationally by your neck which has 2 pivots. You can turn your head around the +y-axis with about 180 degrees of freedom. The second pivot is more of a lever rather than a rotation as you can tilt your neck downward.

    These actions don't really have euler angle comparisons.

    And they especially don't have Quaternion comparisons.

    You have to convert them.

    If you want head like rotation a conversion from Quaternion to "Head Orientation", and back. And then you take your Quaternion, convert it to "Head Rotation", clamp, and then convert back to Quaternion.

    Because the conversion are key steps, the impacts of those conversion mean something.

    If you want to clamp in euler angles. You again have to convert to euler angles, clamp in euler, and convert back. Keeping in mind what those conversions have built into them as that impacts the overall idea.

    And finally, keep in mind that euler angles don't clamp well inherently since euler angles can have the same orientation recorded as multiple different values.
     
  5. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,514
    You might ask then how to clamp in code then.

    Lets take the head example. We need to define a conversion from Quaternion to "Head Rotation" and back for clamping.

    Now I defined how the head actually works and how really it's 2 pivots. One is a rotation around the +y axis, and the other is a lever tilting of a head. This can be described as a Vector3 in the up direction either spinning on itself, as well as the same vector tilting off of +y-axis. These combined is the full motion of the head.

    BUT, that's a very complicated rotational system.

    And really, in the end, what results from it?

    Really what results is that you have a cone of vision... sort of, not geometrically perfectly a cone, but almost nearly a cone. Close enough that we could just describe it as a cone for simulation sake.

    So instead I'll define a forward vector pointing out the front of the face (+z axis). I have the default rotation (+z) and then the orientation when I'm turned.

    Code (csharp):
    1.  
    2. var defaultForward = Vector3.forward;
    3. var forward = transform.localRotation = Vector3.forward;
    4.  
    (this is done in local space since you'd probably allow rotation of the avatar's body)

    Now a cone can be represented as just a specific direction (this forward) and an angle of range off of that forward vector. So lets say you allow 60 degrees turned in any direction, or 120 degrees complete rotational freedom, we can just measure the angle of forward off of defaultForward and see if it's in or out of the range.

    Code (csharp):
    1.  
    2. float maxAngle = 60f;
    3. float a = Vector3.Angle(defaultForward, forward);
    4. if(a > maxAngle)
    5. {
    6.     //clamp
    7. }
    8.  
    If it's less than maxAngle we do nothing, but if it's greater than, we have to clamp it.

    To clamp it we just rotate the vector back towards forward.

    Code (csharp):
    1.  
    2. if (a > maxAngle)
    3. {
    4.     forward = Vector3.RotateTowards(forward, defaultForward, (a - maxAngle) * Mathf.Deg2Rad, 0f);
    5.     this.transform.localRotation = Quaternion.LookRotation(forward, Vector3.up);
    6. }
    7.  
    This of course allows pivoting 360 degrees around forward, which isn't accurate. But there are work arounds for this.

    1) never pivot the head (there's no real reason to in most games)

    2) move this line outside of the if statement permanently clamping the head rotation to 0
    Code (csharp):
    1. this.transform.localRotation = Quaternion.LookRotation(forward, Vector3.up);
    3) Add in a second clamp where you get the up vector and measure the angle between Vector3.up and it and constrain that as well.
     
  6. Quatum1000

    Quatum1000

    Joined:
    Oct 5, 2014
    Posts:
    889
    Ouchie!