12
\$\begingroup\$

I am trying to let the user of my app rotate a 3D object drawn in the center of the screen by dragging their finger on screen. A horizontal movement on screen means rotation around a fixed Y axis, and a vertical movement means rotation around the X axis. The problem I am having is that if I just allow rotation around one axis the object rotates fine, but as soon as I introduce a second rotation the object doesn't rotate as expected.

Here is a picture of what is happening:

enter image description here

The blue axis represents my fixed axis. Picture the screen having this fixed blue axis. This is what I want the object to rotate in relation to. What is happening is in red.

Here's what I know:

  1. The first rotation around Y (0, 1, 0) causes the model to move from the blue space (call this space A) into another space (call this space B)
  2. Trying to rotate again using the vector (1, 0, 0) rotates around the x axis in space B NOT in space A which is not what I mean to do.

Here's what I tried, given what I (think) I know (leaving out the W coord for brevity):

  1. First rotate around Y (0, 1, 0) using a Quaternion.
  2. Convert the rotation Y Quaternion to a Matrix.
  3. Multiply the Y rotation matrix by my fixed axis x Vector (1, 0, 0) to get the X axis in relation to the new space.
  4. Rotate around this new X Vector using a Quaternion.

Here's the code:

private float[] rotationMatrix() {

    final float[] xAxis = {1f, 0f, 0f, 1f};
    final float[] yAxis = {0f, 1f, 0f, 1f};
    float[] rotationY = Quaternion.fromAxisAngle(yAxis, -angleX).toMatrix();

    // multiply x axis by rotationY to put it in object space
    float[] xAxisObjectSpace = new float[4];
    multiplyMV(xAxisObjectSpace, 0, rotationY, 0, xAxis, 0);

    float[] rotationX = Quaternion.fromAxisAngle(xAxisObjectSpace, -angleY).toMatrix();

    float[] rotationMatrix = new float[16];
    multiplyMM(rotationMatrix, 0, rotationY, 0, rotationX, 0);
    return rotationMatrix;
  }

This isn't working how I expect. The rotation seems to work, but at some point horizontal movement doesn't rotate about the Y axis, it appears to rotate about the Z axis.

I'm not sure if my understanding is wrong, or if something else is causing a problem. I have some other transformations I'm doing to the object besides rotation. I move the object to the center before applying rotation. I rotate it using the matrix returned from my function above, then I translate it -2 in the Z direction so I can see the object. I don't think this is messing up my rotations, but here's the code for that anyways:

private float[] getMvpMatrix() {
    // translates the object to where we can see it
    final float[] translationMatrix = new float[16];
    setIdentityM(translationMatrix, 0);
    translateM(translationMatrix, 0, translationMatrix, 0, 0f, 0f, -2);

    float[] rotationMatrix = rotationMatrix();

    // centers the object
    final float[] centeringMatrix = new float[16];
    setIdentityM(centeringMatrix, 0);
    float moveX = (extents.max[0] + extents.min[0]) / 2f;
    float moveY = (extents.max[1] + extents.min[1]) / 2f;
    float moveZ = (extents.max[2] + extents.min[2]) / 2f;
    translateM(centeringMatrix, 0, //
      -moveX, //
      -moveY, //
      -moveZ //
    );

    // apply the translations/rotations
    final float[] modelMatrix = new float[16];
    multiplyMM(modelMatrix, 0, translationMatrix, 0, rotationMatrix, 0);
    multiplyMM(modelMatrix, 0, modelMatrix, 0, centeringMatrix, 0);

    final float[] mvpMatrix = new float[16];
    multiplyMM(mvpMatrix, 0, projectionMatrix, 0, modelMatrix, 0);
    return mvpMatrix;
  }

I've been stuck on this for a few days. Help is much appreciated.

==================================================================

UPDATE:

Getting this to work in Unity is straightforward. Here's some code that rotates a cube centered at the origin:

public class CubeController : MonoBehaviour {

    Vector3 xAxis = new Vector3 (1f, 0f, 0f);
    Vector3 yAxis = new Vector3 (0f, 1f, 0f);

    // Update is called once per frame
    void FixedUpdate () {
        float horizontal = Input.GetAxis ("Horizontal");
        float vertical = Input.GetAxis ("Vertical");

        transform.Rotate (xAxis, vertical, Space.World);
        transform.Rotate (yAxis, -horizontal, Space.World);
    }
}

The part that makes the rotations behave as I'm expecting is the Space.World parameter to the Rotate function on the transform.

If I could use Unity I would, unfortunately I have to code this behavior myself.

\$\endgroup\$
  • 1
    \$\begingroup\$ My answer here gamedev.stackexchange.com/questions/67199/… might help you.. \$\endgroup\$ – concept3d Mar 12 '15 at 9:53
  • \$\begingroup\$ I understand the concept behind your answer, but how to implement is escapes me. \$\endgroup\$ – Christopher Perry Mar 16 '15 at 22:56
  • \$\begingroup\$ If you checked the other answers syntac answer implements the idea that I explained. \$\endgroup\$ – concept3d Mar 16 '15 at 23:49
  • \$\begingroup\$ No it doesn't, it's doing multiple rotations about different axes. You suggest doing a single rotation. \$\endgroup\$ – Christopher Perry Mar 17 '15 at 0:24
3
+25
\$\begingroup\$

The problem your having is called gimble lock. I think what your looking to do is called arcball rotation. The math for arcball can be abit complicated.

A simpler way of doing it is finding a 2d vector perpendicular to the 2d swipe on screen.

Take the vector and project it onto the camera near plane to get a 3d vector in world space. Screen space to World space.

Then create a quaternion with this vector and multiply it to gameobject. Probably with some slurp or lerp transition.

Edit:

Unity Example: In the unity example, the internal state of the gameobjects rotation is a quaternion not a matrix. The transform.rotation method generates a quaternion based on the vector and angle provided and multiplys that quaternion with the gameobjects rotation quaternion. It only generates the rotation matrix for rendering or physics at a later point. Quaternions are additive and avoid gimble lock.

Your Code Should look something like this:

private float[] rotationMatrix() {

    final float[] xAxis = {1f, 0f, 0f, 1f};
    final float[] yAxis = {0f, 1f, 0f, 1f};

    Quaternion qY = Quaternion.fromAxisAngle(yAxis, angleX);
    Quaternion qX = Quaternion.fromAxisAngle(xAxis, -angleY);

    return (qX * qY).getMatrix(); // should probably represent the gameobjects rotation as a quaternion(not a matrix) and multiply all 3 quaternions before generating the matrix. 
  }

ArcBall Rotation Opengl Tutorial

\$\endgroup\$
  • \$\begingroup\$ I'm not getting gimbal lock, the first rotation moves the axis so a second rotation is based on the moved axis. Please have a second look at the picture I provided. \$\endgroup\$ – Christopher Perry Mar 11 '15 at 23:48
  • \$\begingroup\$ I hope you figured it out. In short. Quaternions can be multiplied together to apply rotation. You should only generate the matrix at the end of all rotation calculations. Also xQ * yQ is not equal to yQ * xQ. Quaternions are non commutative like Christopher Perry said. \$\endgroup\$ – Anthony Raimondo Mar 15 '15 at 13:34
  • \$\begingroup\$ I put ALL of my code here. I feel like I've tried everything. Maybe somebody else's eyes on this will catch my mistake. \$\endgroup\$ – Christopher Perry Mar 16 '15 at 22:54
  • \$\begingroup\$ I didn't accept it, stack exchanges algorithm auto assigned the points to you. :/ \$\endgroup\$ – Christopher Perry Mar 20 '15 at 23:40
  • \$\begingroup\$ Im sorry for this injustice. \$\endgroup\$ – Anthony Raimondo Mar 21 '15 at 16:20
3
\$\begingroup\$

I was able to get the rotations expected by rotating an accumulated rotation matrix.

setIdentityM(currentRotation, 0);
rotateM(currentRotation, 0, angleY, 0, 1, 0);
rotateM(currentRotation, 0, angleX, 1, 0, 0);

// Multiply the current rotation by the accumulated rotation,
// and then set the accumulated rotation to the result.
multiplyMM(temporaryMatrix, 0, currentRotation, 0, accumulatedRotation, 0);
arraycopy(temporaryMatrix, 0, accumulatedRotation, 0, 16);
\$\endgroup\$
1
\$\begingroup\$

Your image corresponds with your rotationMatrix code, by rotating the x axis with your previous y rotation you get the local x axis, when you then rotate the object around that you would get the result that you show in your image. To have the rotation be logical from a users point of view you would want to rotate the object using the world coordinate axis instead.

If you want your user to be able to spin your object many times it would make sense to store your rotation in a quaternion instead of a matrix, over time multiple spins (and floating point inaccuracies) will cause the matrix to look less and less like a rotation matrix, the same happens in a quaternion of course, but just normalizing the quaternion brings it back to a good rotation.

Simply use the identity quaternion as the initial value, and each time the user swipes the screen you rotate your quaternion with the Quaternion.fromAxisAngle(yAxis, -angleX) code. Always using (1,0,0,1) for x rotations and (0,1,0,1) for y rotations.

static final float[] xAxis = {1f, 0f, 0f, 1f};
static final float[] yAxis = {0f, 1f, 0f, 1f};

private void rotateObject(float angleX, float angleY) {
  Quaternion rotationY = Quaternion.fromAxisAngle(yAxis, -angleX);
  Quaternion rotationX = Quaternion.fromAxisAngle(xAxis, -angleY);

  myRotation = myRotation.rotate(rotationY).rotate(rotationX).normalize();
}
private float[] rotationMatrix() {
  return myRotation.toMatrix();
}

Since you didn't mention the language or any specific framework the methods on Quaternion may be called something differently of course, and normalize isn't necessary to call that often, but since the rotation comes from a user swiping the screen they won't slow things down much and that way there is no chance of the Quaternion slipping away from a unit quaternion.

\$\endgroup\$
  • \$\begingroup\$ I'm getting the exact same behavior doing this. \$\endgroup\$ – Christopher Perry Mar 13 '15 at 17:56
  • 1
    \$\begingroup\$ @ChristopherPerry Try reversing the rotation order: myRotation = rotationX.rotate(rotationY).rotate(myRotation).normalize() They are not commutative so the order you do them counts. Add another comment with your framework/language if that didn't work and Ill dig into it a bit more. \$\endgroup\$ – Daniel Carlsson Mar 14 '15 at 6:16
  • \$\begingroup\$ That doesn't work either, I get the same behavior. \$\endgroup\$ – Christopher Perry Mar 16 '15 at 2:18
  • \$\begingroup\$ I put ALL of my code here \$\endgroup\$ – Christopher Perry Mar 16 '15 at 22:54

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.