ArcBall RotationArcBall Rotation Control, Revisited Wouldnt it be great to rotate your model at will, just by using the mouse? With an ArcBall you can do just that. In this document, Ill touch on my implementation, and considerations for adding it to your own projects. My implementation of the ArcBall class is based on Bretton Wades, which is based on Ken Shoemakes from the Graphic Gems series of books. However, I did a little bug fixing and optimization for our purposes. The ArcBall works by mapping click coordinates in a window directly into the ArcBalls sphere coordinates, as if it were directly in front of you. To accomplish this, first we simply scale down the mouse coordinates from the range of [0...width), [0...height) to [-1...1], [1...-1] (Keep in mind that we flip the sign of Y so that we get the correct results in OpenGL.) And this essentially looks like: MousePt.X = ((MousePt.X / ((Width 1) / 2)) 1); MousePt.Y = -((MousePt.Y / ((Height 1) / 2)) 1); The only reason we scale the coordinates down to the range of [-1...1] is to make the math simpler; by happy coincidence this also lets the compiler do a little optimization. Next we calculate the length of the vector and determine whether or not its inside or outside of the sphere bounds. If it is within the bounds of the sphere, we return a vector from within the inside the sphere, else we normalize the point and return the closest point to outside of the sphere. Once we have both vectors, we can then calculate a vector perpendicular to the start and end vectors with an angle, which turns out to be a quaternion. With this in hand we have enough information to generate a rotation matrix from, and were home free. The ArcBall is instantiated using the following constructor. NewWidth and NewHeight are essentially the width and height of the window. ArcBall_t::ArcBall_t(GLfloat NewWidth, GLfloat NewHeight) When the user clicks the mouse, the start vector is calculated based on where the click occurred. void ArcBall_t::click(const Point2fT* NewPt) When the user drags the mouse, the end vector is updated via the drag method, and if a quaternion output parameter is provided, this is updated with the resultant rotation. void ArcBall_t::drag(const Point2fT* NewPt, Quat4fT* NewRot) If the window size changes, we simply update the ArcBall with that information: void ArcBall_t::setBounds(GLfloat NewWidth, GLfloat NewHeight) When using this in your own project, youll want some member variables of your own. // Final Transform Matrix4fT Transform = { 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f }; Matrix3fT LastRot = { 1.0f, 0.0f, 0.0f, // Last Rotation 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f }; Matrix3fT ThisRot = { 1.0f, 0.0f, 0.0f, // This Rotation 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f }; ArcBallT ArcBall(640.0f, 480.0f); // ArcBall Instance Point2fT MousePt; // Current Mouse Point bool isClicked = false; // Clicking The Mouse? bool isRClicked = false; // Clicking The Right Mouse Button? bool isDragging = false; // Dragging The Mouse? Transform is our final transform- our rotation and any optional translation you may want to provide. LastRot is the last rotation we experienced at the end of a drag. ThisRot is the rotation we experience while dragging. All are initialized to identity. When we click, we start from an identity rotation state. When we drag, we are then calculating the rotation from the initial click point to the drag point. Even though we use this information to rotate the objects on screen, it is important to note that we are not actually rotating the ArcBall itself. Therefore to have cumulative rotations, we must handle this ourselves. This is where LastRot and ThisRot come into play. LastRot can be defined as all rotations up till now, whereas ThisRot can be defined by the current rotation. Every time a drag is started, ThisRot is modified by the original rotation. It is then updated to the product of itself * LastRot. (Then the final transformation is also updated.) Once a drag is stopped, LastRot is then assigned the value of ThisRot. If we didnt accumulate the rotations ourselves, the model would appear to snap to origin each time that we clicked. For instance if we rotate around the X-axis 90 degrees, then 45 degrees, we would want to see 135 degrees of rotation, not just the last 45. For the rest of the variables (except for isDragged), all you need to do is update them at the proper times based on your system. ArcBall needs its bounds reset whenever your window size changes. MousePt gets updated whenever your mouse moves, or just when the mouse button is down. isClicked / isRClicked whenever the left/right mouse button is clicked, respectively. isClicked is used to determine clicks and drags. Well use isRClicked to reset all rotations to identity. The additional system update code under NeHeGL/Windows looks something like this: void ReshapeGL (int width, int height) { . . . ArcBall.setBounds((GLfloat)width, (GLfloat)height); // Update Mouse Bounds For ArcBall } // Process Window Message Callbacks LRESULT CALLBACK WindowProc (HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { . . . // Mouse Based Messages For ArcBall case WM_MOUSEMOVE: MousePt.s.X = (GLfloat)LOWORD(lParam); MousePt.s.Y = (GLfloat)HIWORD(lParam); isClicked = (LOWORD(wParam) & MK_LBUTTON) ? true : false; isRClicked = (LOWORD(wParam) & MK_RBUTTON) ? true : false; break; case WM_LBUTTONUP: isClicked = false; break; case WM_RBUTTONUP: isRClicked = false; break; case WM_LBUTTONDOWN: isClicked = true; break; case WM_RBUTTONDOWN: isRClicked = true; break; . . . } Once we have the system update code in place, its time to put the click logic in place. This is very self-explanatory once you know everything above. if (isRClicked) // If Right Mouse Clicked, Reset All Rotations { // Reset Rotation Matrix3fSetIdentity(&LastRot); // Reset Rotation Matrix3fSetIdentity(&ThisRot); // Reset Rotation Matrix4fSetRotationFromMatrix3f(&Transform, &ThisRot); } if (!isDragging) // Not Dragging { if (isClicked) // First Click { isDragging = true; // Prepare For Dragging LastRot = ThisRot; // Set Last Static Rotation To Last Dynamic One ArcBall.click(&MousePt); // Update Start Vector And Prepare For Dragging } } else { if (isClicked) //Still clicked, so still dragging { Quat4fT ThisQuat; ArcBall.drag(&MousePt, &ThisQuat); // Update End Vector And Get Rotation As Quaternion Matrix3fSetRotationFromQuat4f(&ThisRot, &ThisQuat); // Convert Quaternion Into Matrix3fT Matrix3fMulMatrix3f(&ThisRot, &LastRot); // Accumulate Last Rotation Into This One Matrix4fSetRotationFromMatrix3f(&Transform, &ThisRot); // Set Our Final Transform's Rotation From This One } else // No Longer Dragging isDragging = false; } This takes care of everything for us. Now all we need to do is apply the transformation to our models and were done. Its really simple: glPushMatrix(); // Prepare Dynamic Transform glMultMatrixf(Transform.M); // Apply Dynamic Transform glBegin(GL_TRIANGLES); // Start Drawing Model . . . glEnd(); // Done Drawing Model glPopMatrix(); // Unapply Dynamic Transform I have included a sample, which demonstrates everything above. Youre not locked in to using my math types or functions; in fact I would suggest fitting this in to your own math system if youre confident enough. However, everything is self-contained otherwise and should work on its own. Now after seeing how simple this is, you should be well on your way to adding ArcBall to your own projects. Enjoy! * DOWNLOAD Visual C++ Code For This Lesson. * DOWNLOAD Borland C++ Builder 6 Code For This Lesson. ( Conversion by Le Thanh Cong ) |