OpenGL Programming/Modern OpenGL Tutorial Virtual Trackball
The virtual trackball is a tool to rotate objects with the mouse naturally.
The concept
editImagine a virtual ball that is just behind the screen. By clicking it with your mouse, you would pinch it, and by moving your mouse you would make the ball spin around its center. And the same rotation would be applied to an object in the OpenGL scene!
The diagram on the right shows the virtual ball in top view. The black line at the bottom is the screen, and x1 and x2 are two successive mouse positions during the drag. This is shown in 2D but the same principle applies in 3D.
The goal is to compute the α angle and the rotation axis. This is the point where you understand that maths are really necessary as soon as you get serious with OpenGL ;) In particular we'll need the Pythagorean theorem, the vector dot product and cross product.
We'll:
- Convert the screen coordinates (in pixels) to camera coordinates (in [-1, 1])
- Compute the vectors OP1 and OP2, the points at the surface of the ball that match our mouse click
- x and y coordinates are directly taken from the click in camera coordinates
- z coordinate is computed using the classical Pythagorean theorem
- If P1 or P2 is too far away from the sphere ( ), we normalize it to get the nearest point on the surface of the ball
- We have , and the ball's size is 1 ( ), so we get the angle using .
- Get the rotation axis in 3D, we compute , which will give a unit perpendicular vector
The code
editCapturing mouse events
editGLUT offers a way to get the mouse clicks and drag events:
glutMouseFunc(onMouse)
will call onMouse(int button, int state, int x, int y)
for each mouse click, where:
- button is GLUT_LEFT_BUTTON, GLUT_MIDDLE_BUTTON or GLUT_RIGHT_BUTTON
- state is GLUT_DOWN or GLUT_UP
- x and y are the screen coordinates, starting from the top-left corner (y is reversed compared to OpenGL coordinates!)
glutMotionFunc(onMotion)
will call onMotion(int x, int y)
for each mouse move when any button is pressed down, where x and y are the screen coordinates.
You also have glutPassiveMotionFunc(...)
which works similarly for mouse moves when no button is pressed at all.
So we add two functions to keep track of the mouse moves when the left button is pressed:
/* Global */
int last_mx = 0, last_my = 0, cur_mx = 0, cur_my = 0;
int trackball_on = false;
/* main() */
glutMouseFunc(onMouse);
glutMotionFunc(onMotion);
void onMouse(int button, int state, int x, int y) {
if (button == GLUT_LEFT_BUTTON && state == GLUT_DOWN) {
trackball_on = true;
last_mx = cur_mx = x;
last_my = cur_my = y;
} else {
trackball_on = false;
}
}
void onMotion(int x, int y) {
if (trackball_on) { // if left button is pressed
cur_mx = x;
cur_my = y;
}
}
Compute OP1 and OP2
editWe add a new function to compute the trackball surface point:
/**
* Get a normalized vector from the center of the virtual ball O to a
* point P on the virtual ball surface, such that P is aligned on
* screen's (X,Y) coordinates. If (X,Y) is too far away from the
* sphere, return the nearest point on the virtual ball surface.
*/
glm::vec3 get_trackball_vector(int x, int y) {
glm::vec3 P = glm::vec3(1.0*x/screen_width*2 - 1.0,
1.0*y/screen_height*2 - 1.0,
0);
P.y = -P.y;
float OP_squared = P.x * P.x + P.y * P.y;
if (OP_squared <= 1*1)
P.z = sqrt(1*1 - OP_squared); // Pythagoras
else
P = glm::normalize(P); // nearest point
return P;
}
We first convert the x,y screen coordinates to [-1,1] coordinates (and reverse y coordinates). Then we use the Pythagorean theorem to check the length of the OP vector and compute the z coordinate, as explained above.
Compute the angle and axis
edit /* onIdle() */
if (cur_mx != last_mx || cur_my != last_my) {
glm::vec3 va = get_trackball_vector(last_mx, last_my);
glm::vec3 vb = get_trackball_vector( cur_mx, cur_my);
float angle = acos(min(1.0f, glm::dot(va, vb)));
glm::vec3 axis_in_camera_coord = glm::cross(va, vb);
glm::mat3 camera2object = glm::inverse(glm::mat3(transforms[MODE_CAMERA]) * glm::mat3(mesh.object2world));
glm::vec3 axis_in_object_coord = camera2object * axis_in_camera_coord;
mesh.object2world = glm::rotate(mesh.object2world, glm::degrees(angle), axis_in_object_coord);
last_mx = cur_mx;
last_my = cur_my;
}
Once we have OP1 and OP2 (here named va
and vb
), we can compute the angle with acos(dot(va,vb))
.
Since we're using float
variables, there may be precision issues: dot
may return a value slightly greater than 1, and acos
will return nan
, which means an invalid float. The consequence is that our rotation matrix will be all messed, and usually our object will just disappear from the screen! To remedy this, we cap the value with a maximum of 1.0
.
An extra trick is converting the rotation axis from camera coordinates to object coordinates. It's useful when the camera and object are placed differently. For instace, if you rotate the object by 90° on the Y axis ("turn its head" to the right), then perform a vertical move with your mouse, you make a rotation on the camera X axis, but it should become a rotation on the Z axis (plane barrel roll) for the object. By converting the axis in object coordinates, the rotation will respect that the user work in camera coordinates (WYSIWYG). To transform from camera to object coordinates, we take the inverse of the MV matrix (from the MVP matrix triplet).
And last we can apply our transformation using glm::rotate
as usual :)
Exercises
edit- Is the rotation angle proportional to the mouse move? Try to make a move near the border of the virtual ball.
- The virtual ball will stop rolling when the mouse is too far away. Other mouse controls are possible. For instance, study how dragging with the middle button works in the Blender 3D modeler.
- Try different roll speeds, by multiplying the rotation angle.