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

3 Answers 3

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\$
5
  • \$\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\$ Commented Mar 11, 2015 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\$ Commented Mar 15, 2015 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\$ Commented Mar 16, 2015 at 22:54
  • \$\begingroup\$ I didn't accept it, stack exchanges algorithm auto assigned the points to you. :/ \$\endgroup\$ Commented Mar 20, 2015 at 23:40
  • \$\begingroup\$ Im sorry for this injustice. \$\endgroup\$ Commented Mar 21, 2015 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\$
4
  • \$\begingroup\$ I'm getting the exact same behavior doing this. \$\endgroup\$ Commented Mar 13, 2015 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\$ Commented Mar 14, 2015 at 6:16
  • \$\begingroup\$ That doesn't work either, I get the same behavior. \$\endgroup\$ Commented Mar 16, 2015 at 2:18
  • \$\begingroup\$ I put ALL of my code here \$\endgroup\$ Commented Mar 16, 2015 at 22:54

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .