Friday, August 28, 2020

How to Fix Gimbal Lock in N-Dimensions

How to Fix Gimbal Lock in N-Dimensions

Gimbal lock is a common issue that arises in 3D rotation systems. Conventional wisdom says that you should represent your rotations as quaternions to avoid this problem (as stated in many popular game engine docs and countless YouTube videos). But I couldn’t find any good explanations of exactly how quaternions solve it.

This was important for me because I was implementing a 4D geometry viewer and spent a lot of time trying to figure out how to generalize quaternions to higher dimensions, only to discover that you can still get gimbal lock with quaternions!

The answer is it’s not about what you use to represent your rotations (quaternions, matrices, or rotors), it’s about how you apply those rotations.

What causes gimbal lock?

Gimbal lock is the loss of a degree of freedom in a rotation system. It will always happen in any system that uses Euler angles, where you rotate your object by applying a fixed set of successive rotations.

In 3D, this happens when one of the 3 axes of rotation becomes parallel to any other one (or in other words, when the “gimbals” line up). This is shown in the animation below. It starts with the +Z side facing the camera. The first 90 degree rotation places the top cube in a gimbal locked state: rotating by the blue and red gimbals now produce the same rotation. It is not possible in this state to rotate around the cube’s local X axis (pointing towards the camera). This is the degree of freedom we’ve lost and is represented by the red gimbal in the bottom view.

It’s easier to see this by trying it yourself in this demo (requires a keyboard):

  • Rotate the cubes with W,A,S,D and Q, E
  • R to reset

https://gimbal-lock-quaternions.glitch.me/

The top two cubes use Euler angles, so there will always be a way to get into this gimbal locked state. All you have to do is line up the blue and red gimbals. Then compare the behavior with the bottom two cubes, which show what the correct rotation should be.

Here is pseudocode for how the rotation for the top left cube is implemented:

rotationAroundX = Matrix3.fromAxisAngle(angle1, Xaxis);
rotationAroundY = Matrix3.fromAxisAngle(angle2, Yaxis);
rotationAroundZ = Matrix3.fromAxisAngle(angle3, Zaxis);
cubeRotation = rotationAroundX * rotationAroundY * rotationAroundZ;

This is an Euler angle system that uses rotation matrices to represent and apply the rotations. We will get exactly the same behavior if we swap out the matrices for quaternions.

Here is how the top right cube is implemented:

rotationAroundX = Quaternion.fromAxisAngle(angle1, Xaxis);
rotationAroundY = Quaternion.fromAxisAngle(angle2, Yaxis);
rotationAroundZ = Quaternion.fromAxisAngle(angle3, Zaxis);
cubeRotation = rotationAroundX * rotationAroundY * rotationAroundZ;

This is an Euler angle system that uses quaternions to represent and apply the rotations, and thus also suffers from gimbal lock.

How do you fix gimbal lock?

To fix gimbal lock, we must avoid modelling this physical gimbal system.

In 3D, instead of using 3 fixed angles that we multiply together to get the final rotation, we will:

  1. Construct a quaternion that describes a rotation around whatever axis we want, and the angle to rotate by.
  2. Multiply that by the object’s current rotation as represented by a quaternion.
  3. Take the result and overwrite it back into the object’s current stored rotation.

This is how the bottom right cube in the demo above is implemented. The pseudocode looks like this:

// We want to rotate by 0.05 radians in this frame.
rotationThisFrame = Quaternion.fromAxisAngle(0.05, arbitraryAxis);
cubeRotation = cubeRotation * rotationThisFrame;

Nothing about this technique is unique to quaternions. Here is how the bottom left cube implements this with matrices:

rotationThisFrame = Matrix3.fromAxisAngle(0.05, arbitraryAxis);cubeRotation = cubeRotation * rotationThisFrame;

The “arbitrary axis” is the cube’s local X/Y/Z axes, depending on what key is pressed. It can instead be the global X/Y/Z axes if we wanted to rotate relative to a fixed global frame.

You can see the full JavaScript code for all 4 rotating cubes here: https://glitch.com/edit/#!/gimbal-lock-quaternions?path=index.html%3A51%3A37

Generalizing to N-dimensions

To generalize this solution to higher dimensions we need two things:

  • A way to represent rotations. So far in 3D we’ve used 3x3 matrices and quaternions.
  • A way to describe a rotation around an arbitrary axis.

Side note: it’s more generalizable to talk about “rotating within a plane” instead of “rotating around an axis”. In 2D there is only one plane of rotation, the XY plane. In 3D there are 3 planes of rotation. In 4D there are 6 planes. In N-D there are (N choose 2) planes.

I ended up choosing 4x4 matrices to represent my rotations, because 4x4 matrix operations are supported on the GPU and so I could rotate the geometry in my vertex shader, in the same way I would in 3D.

So the proposed solution is:

rotationThisFrame = Matrix4.fromAxisAngle(0.05, arbitraryAxis);cubeRotation = cubeRotation * rotationThisFrame;

Now the tricky part is implementing a Matrix4.fromAxisAngle function. It’s much easier to create a 4x4 rotation matrix for each of the 6 planes of rotations and compose them. So this can be broken down into:

// Check which key(s) were pressed. In this frame, only the key for rotating in the XZ plane was pressed, so the rXZ 4x4 matrix gets a non-zero angle. rXY = Matrix4.fromXYRotation(0);
rXZ = Matrix4.fromXZRotation(0.05);
rYZ = Matrix4.fromYZRotation(0);
rXW = Matrix4.fromXWRotation(0);
rYW = Matrix4.fromYWRotation(0);
rZW = Matrix4.fromZWRotation(0);
rotationThisFrame = rXY * rXZ * rYZ * XW * rYW * rZW;cubeRotation = cubeRotation * rotationThisFrame;

This looks suspiciously like Euler angles, but it’s not. This will never gimbal lock. No matter what orientation the cube ends up in, we can always rotate it by 0.05 radians in any of the 6 planes.

To illustrate the difference, here’s a version that will gimbal lock:

// Check which key(s) were pressed. In this frame, only the key for the rotating in the XZ plane was pressed, so we add 0.05 to angle2.angle2 += 0.05;rXY = Matrix4.fromXYRotation(angle1);
rXZ = Matrix4.fromXZRotation(angle2);
rYZ = Matrix4.fromYZRotation(angle3);
rXW = Matrix4.fromXWRotation(angle4);
rYW = Matrix4.fromYWRotation(angle5);
rZW = Matrix4.fromZWRotation(angle6);
cubeRotation = rXY * rXZ * rYZ * XW * rYW * rZW;

Beyond 4D, you could continue using NxN rotation matrices, but since GPUs don’t support that, I’d use rotors from Geometric Algebra to simplify implementation, which is how you generalize quaternions to higher dimensions.



from Hacker News https://ift.tt/3jgzn79

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.