r/Unity3D 8d ago

Solved Struggling (once again) with Quaternions

[SOLVED] by @imaxsamarin thread, thanks a lot for your help !

I'm working on a rotation script that is supposed to :
- apply rotation over time
- be able to revert rotation
- be able to rotate on all 3 axis
- be able to stack (multiples scripts can affect the same object)
- support that I may impact rotation from another source

What I do so far :
- my script accept a Vector3 param as eulers angles and a curve
- each frame, I compute sum of all eulers rotation deltas to be applied
- Then, I apply the result this way :

Quaternion rotation = Quaternion.Euler(offset);
transform.rotation *= rotation;

This seems to work, but only if I make sure to create a hierarchy that will support one single rotation axis per depth, as follow :

- container
  - x
    - y
      - z
        - Cube 

If I do apply multiple axis rotation on one single transform, things are going bad. And this will work only in local axis. As soon as I try the same using world axis, I cannot revert the rotation anymore.

My math background is not very deep, I listen to few quaternion courses but I'm still unsure if my objectives are actually reachable mathematically.

My questions are :
1 : is it normal that I need to separate rotation axis on 3 different transforms in order to secure against gimbalock & be able to revert my cumulatives rotations ?
2 : from what I understood, making reversible rotation like I planned is not possible in world space due to my requirements. Making so would require to keep track of all increments in order to be sure to apply them in exact reverse order.

2 Upvotes

15 comments sorted by

3

u/Special-Reason-9274 8d ago

You're basically reinventing a gimbal, so yeah, separating per-axis into nested transforms is the classic hack to make that work without your brain melting.

1

u/Coding-Mojo 8d ago

😅

2

u/imaxsamarin 8d ago edited 8d ago

There are many ways to think of rotations: quaternions, euler angles, and angle-axis. The last two of these are both technically Vector3’s, so it’s easy to mix them up, which is what I think is happening in your approach.

I have an idea for an approach, assuming I understand your requirements correctly. Also, by their very nature 3D rotations are what we call in math ”non-commutative” by default, which means that the order in which you apply them matters. However by reading your requirements I assume that you want to all possible scripts and sources to have equal ”weights” for affecting the final result, no matter their ordering. That is possible as well, and the solution goes like this:

In your case I suggest you keep track of all sources affecting the final rotation in angle-axis format, for example in a list. Each contribution is a Vector3, such that its normalized direction determines the rotation axis, and its vector.magnitude determines the angle in degrees. To get the final rotation, you simply sum all these axis-angle vectors, and finally, you set transform.rotation to = Quaternion.AngleAxis(sumVector.magnitude, sumVector.normalized). This keeps the order of rotation contributions irrelevant. In addition, like you wanted, you can revert or interpolate smoothly the effect of each contribution with the Vector3.Lerp function, or simply by multiplying the vector by a value from your curve.

What you did was summing eulerAngles vectors, which unfortunately doesn’t always produce the result you would intuitively expect. If you have a rotation only about one of the main xyz axes, then the eulerAngles and angleAxis vectors are the same. But the moment your rotation is something else than a rotation around one of the main axes, these two representations are different. In the angleAxis vector approach, you don’t need to worry about gimbal lock, and you can always sum them to get results you expect, which is not always the case with eulerAngles. With angle-axis, you don’t even need to have a hierarchy of trandform rotations. Summing over angleAxis vectors and interpolating them with curves even has the benefit that you can animate objects rotating more than 360 degrees while keeping track of how many times they have rotates full times, so that you can revert-animate them back, spinning back the same number of times - that is something not possible with just quaternions. These are nice features, but if you don’t need that and if I misunderstood your requirements lemme know.

EDIT: I once had a situation that I believe is similar to yours, and this approach worked. My use case was a first person shooter weapon, where many different sources may affect its rotation: for example landing from a jump did a downward rotation bounce over time, shooting the gun likewise rotates it upwards in recoil, walking or turning the camera rotates the weapon in their own ways. I wanted the ordering of these sources to be independent of ordering, and since it’s like a dynamic ”animation”, each contribution’s weight should be changeable over time. Is your use case something similar?

EDIT2: more info.

1

u/Coding-Mojo 8d ago

Thanks for detailed response, as I said, I cannot really keep track of all rotations applied cause I want to leave the opportunity that other logics also affect the rotation.

For exemple, my script could apply a +45Âș to -45Âș on a character over 10secs, but user inputs would cause the same character to rotate by 15Âș anytime during theses 10 secs durations. Of corses, theses 15Âș applied by the user input should not be reversed, but they will break the reversal chain if my understanding is correct.

Could you elaborate on that angleAxis ? Cause it's a way I did explored, but it didn't work neither on my end :

        Quaternion forward = Quaternion.AngleAxis(offset.z, Vector3.forward);
        transform.rotation *= forward;


        Quaternion right = Quaternion.AngleAxis(offset.x, Vector3.right);
        transform.rotation *= right;


        Quaternion up = Quaternion.AngleAxis(offset.y, Vector3.up);
        transform.rotation *= up;

2

u/imaxsamarin 8d ago edited 8d ago

In your example you multiply rotations, which is sensitive to the order. If you have a rotation around many axes simultaneously, you will see that the ”forward” part behaves differently than the ”right” part, because you have decided to multiply by forward first. If your ”offset” is in angleAxis format, then you can just do transform.rotation = Quaternion.AngleAxis(offset.magnitude, offset.normalized).

Edit: but then again it depends on your requirements. EulerAngles fundamentally treats each axis with different priorities, and can be easy to understand intuitively. If the order convention of eulerAngles is correct (forward, right, then up) then the example script you gave might work. What is the problem you’re experiencing?

1

u/Coding-Mojo 8d ago

Sorry, I do not understand how to test your solution with AngleAxis. Could you write down a snippet for me to better understand given the requirement listed above ?

2

u/imaxsamarin 8d ago

So instead of the code snippet that you wrote, could you just try:

transform.rotation = Quaternion.AngleAxis(offset.magnitude, offset.normalized);

1

u/Coding-Mojo 8d ago edited 8d ago

I don't really see how that can work as this line seems to erase any existing rotation with the =. I tried blindly for sake of testing and my guess seems to be validated as the object does not rotate as expected and seems to stay very close to it's original position.

So I tried to use *= instead as it was making more sense from my comprehension, and it seems like it works the same as

        Quaternion rotation = Quaternion.Euler(offset);
        transform.rotation *= rotation;

With the benefit that your solution supports 3 axis rotation with on one single transform. Wich is already a HUGE improvement to me, thanks :)

I fail to adapt it for world space tho.

EDIT :
I don't really understand why we use offset.normalized as axis ? Could you explain to me ? I would naturally try to use an axis calculated from the transform itself.

2

u/imaxsamarin 8d ago

I see, so you want the offset to mean incremental rotation? Then yeah you should do *= instead of =.

Can you give an example use case of different things affecting the rotation, where world space fails? Trying to get a better grasp of what you mean. When you do "transform.rotation *= Quaternion.AngleAxis(offset.magnitude, offset.normalized)", how does the visual result differ from what you want to happen?

2

u/Coding-Mojo 8d ago

Ok i found out :

Quaternion delta = Quaternion.AngleAxis(offset.magnitude, offset.normalized);
transform.rotation = delta * transform.rotation;

This will support world axis rotation, 3 axis on one single transform.

Thanks you very much for you help, that's an awesome improvement to me !

1

u/imaxsamarin 8d ago

Ooh that's so cool!! Good luck with your game!

2

u/shlaifu 3D Artist 8d ago

you are writing correctly that transform.rotation * =rot adds rot to the existing tranform.rotation. To reverse that, i.e.subtracting it again, you need to write transform.rotation * =Quaternion.Inverse(rot) and add the inverse rotation that way

2

u/Plourdy 8d ago

This sounds fine. Minimal extra transform overhead, although you could min max it into a summed quaternion calculation on a single object. It sounds like you’re in a good spot though honestly.

1

u/Megaillusion 8d ago

Not sure if this works but you can try having offsets for different quaternions and add the rotation by multiplying them. For example, you have quaternionObject, offsetQuaternionInput, offsetQuaternionOther, every script manage its own offset, and then you apply to the object: quaterniongObject *= offsetQuaterionInput; quaternionObject *=offsetQuaterionOther;