Rotating an object in 3D is a neat way of letting your users interact with the scene, but the math can be tricky to get right. In this article, I’ll take a look at a simple way to rotate an object based on the touch events, and how to work around the main drawback of this method.
Simple rotations.
This is the easiest way to rotate an object based on touch movement. Here is example pseudocode:
Matrix.setIdentity(modelMatrix); ... do other translations here ... Matrix.rotateX(totalYMovement); Matrix.rotateY(totalXMovement);
This is done every frame.
To rotate an object up or down, we rotate it around the X-axis, and to rotate an object left or right, we rotate it around the Y axis. We could also rotate an object around the Z axis if we wanted it to spin around.
How to make the rotation appear relative to the user’s point of view.
The main problem with the simple way of rotating is that the object is being rotated in relation to itself, instead of in relation to the user’s point of view. If you rotate left and right from a point of zero rotation, the cube will rotate as you expect, but what if you then rotate it up or down 180 degrees? Trying to rotate the cube left or right will now rotate it in the opposite direction!
One easy way to work around this problem is to keep a second matrix around that will store all of the accumulated rotations.
Here’s what we need to do:
- Every frame, calculate the delta between the last position of the pointer, and the current position of the pointer. This delta will be used to rotate our accumulated rotation matrix.
- Use this matrix to rotate the cube.
What this means is that drags left, right, up, and down will always move the cube in the direction that we expect.
Android Code
The code examples here are written for Android, but can easily be adapted to any platform running OpenGL ES. The code is based on Android Lesson Six: An Introduction to Texture Filtering.
In LessonSixGLSurfaceView.java, we declare a few member variables:
private float mPreviousX; private float mPreviousY; private float mDensity;
We will store the previous pointer position each frame, so that we can calculate the relative movement left, right, up, or down. We also store the screen density so that drags across the screen can move the object a consistent amount across devices, regardless of the pixel density.
Here’s how to get the pixel density:
final DisplayMetrics displayMetrics = new DisplayMetrics(); getWindowManager().getDefaultDisplay().getMetrics(displayMetrics); density = displayMetrics.density
Then we add our touch event handler to our custom GLSurfaceView:
public boolean onTouchEvent(MotionEvent event) { if (event != null) { float x = event.getX(); float y = event.getY(); if (event.getAction() == MotionEvent.ACTION_MOVE) { if (mRenderer != null) { float deltaX = (x - mPreviousX) / mDensity / 2f; float deltaY = (y - mPreviousY) / mDensity / 2f; mRenderer.mDeltaX += deltaX; mRenderer.mDeltaY += deltaY; } } mPreviousX = x; mPreviousY = y; return true; } else { return super.onTouchEvent(event); } }
Every frame, we compare the current pointer position with the previous, and use that to calculate the delta offset. We then divide that delta offset by the pixel density and a slowing factor of 2.0f to get our final delta values. We apply those directly to the renderer to a couple of public variables that we have also declared as volatile, so that they can be updated between threads.
Remember, on Android, the OpenGL renderer runs in a different thread than the UI event handler thread, and there is a slim chance that the other thread fires in-between the X and Y variable assignments (there are also additional points of contention with the += syntax). I have left the code like this to bring up this point; as an exercise for the reader I leave it to you to add synchronized statements around the public variable read and write pairs instead of using volatile variables.
First, let’s add a couple of matrices and initialize them:
/** Store the accumulated rotation. */ private final float[] mAccumulatedRotation = new float[16]; /** Store the current rotation. */ private final float[] mCurrentRotation = new float[16];
@Override public void onSurfaceCreated(GL10 glUnused, EGLConfig config) { ... // Initialize the accumulated rotation matrix Matrix.setIdentityM(mAccumulatedRotation, 0); }
Here’s what our matrix code looks like in the onDrawFrame method:
// Draw a cube. // Translate the cube into the screen. Matrix.setIdentityM(mModelMatrix, 0); Matrix.translateM(mModelMatrix, 0, 0.0f, 0.8f, -3.5f); // Set a matrix that contains the current rotation. Matrix.setIdentityM(mCurrentRotation, 0); Matrix.rotateM(mCurrentRotation, 0, mDeltaX, 0.0f, 1.0f, 0.0f); Matrix.rotateM(mCurrentRotation, 0, mDeltaY, 1.0f, 0.0f, 0.0f); mDeltaX = 0.0f; mDeltaY = 0.0f; // Multiply the current rotation by the accumulated rotation, and then set the accumulated // rotation to the result. Matrix.multiplyMM(mTemporaryMatrix, 0, mCurrentRotation, 0, mAccumulatedRotation, 0); System.arraycopy(mTemporaryMatrix, 0, mAccumulatedRotation, 0, 16); // Rotate the cube taking the overall rotation into account. Matrix.multiplyMM(mTemporaryMatrix, 0, mModelMatrix, 0, mAccumulatedRotation, 0); System.arraycopy(mTemporaryMatrix, 0, mModelMatrix, 0, 16);
- First we translate the cube.
- Then we build a matrix that will contain the current amount of rotation, between this frame and the preceding frame.
- We then multiply this matrix with the accumulated rotation, and assign the accumulated rotation to the result. The accumulated rotation contains the result of all of our rotations since the beginning.
- Now that we’ve updated the accumulated rotation matrix with the most recent rotation, we finally rotate the cube by multiplying the model matrix with our rotation matrix, and then we set the model matrix to the result.
The above code might look a bit confusion due to the placement of the variables, so remember the definitions:
public static void multiplyMM (float[] result, int resultOffset, float[] lhs, int lhsOffset, float[] rhs, int rhsOffset)
public static void arraycopy (Object src, int srcPos, Object dst, int dstPos, int length)
Note the position of source and destination for each method call.
Trouble spots and pitfalls
- The accumulated matrix should be set to identity once when initialized, and should not be reset to identity each frame.
- Previous pointer positions must also be set on pointer down events, not only on pointer move events.
- Watch the order of parameters, and also watch out for corrupting your matrices. Android’s Matrix.multiplyMM states that “the result element values are undefined if the result elements overlap either the lhs or rhs elements.” Use temporary matrices to avoid this problem.
WebGL examples
The example on the left uses the simplest method of rotating, while the example on the right uses the accumulated rotations matrix.
Further exercises
What are the drawbacks of using a matrix to hold accumulated rotations and updating it every frame based on the movement delta for that frame? What other ways of rotation are there? Try experimenting, and see what else you can come up with!