# Tutorial of Arcball without quaternions

Today I’m showing you a little mathematical tutorial about the techniques which are behind the implementation of the ubiquitous and very clever Arcball method. Arcball is a method to manipulate and rotate objects in 3D intuitively.

The motivation behind the Arcball is to provide an intuitive user interface for complex 3D object rotation via a simple, virtual sphere – the screen space analogy to the familiar input device bearing the same name.

If you are here because were searching about quaternions, I’m telling you that Arcball really doesn’t need quaternions to accomplish its simple task!

Arcball algorithm works by defining a hemisphere inscribed in screen area, where $x$ axis is along the screen width and $y$ axis is along the screen height. Also remember that in OpenGL the origin of pixel coordinates $(0,0)$ is in the upper left corner of the window.

The $(x,y)$ mouse coordinates are used to sample from a $z(x,y)$ function:



This means that points inside the circle in figure are mapped on a sphere, while the outer points are mapped in the $z=0$ plane.

Arcball 2d setup

The choice of this function is very smart. It helps rotating 3D objects when the selection is done inside the circle, while in the borders of the screen the rotation is only around $z$ axis, being $z=0$.

The mapping function $z$ allows us to define a start vector $\mathbf{p}_1 = (x,y,z) \in \mathbb{R}^3$ from which we start the rotation.

We proceed with the description of the arcball algorithm. After the first click the start vector is well defined and suddenly after the user is dragging the mouse in the screen area.

Let’s suppose that the user reaches a new position given by $(x',y')$ pixel coordinates. We then remap this pixel coordinates again using the function described before, obtaining a new vector

$\mathbf{p}_2 = (x',y',z(x',y'))$.

User clicks on point p1 on the (x,y) screen area and the point is sampled on the sphere. Then user drags the cursor to reach point (x’,y’) that is mapped to p2. The angle-axis transformation between p1 and p2 is exploited to create the appropriate rotation matrix to apply to our scene.

Now in order to obtain a rotation in terms of a $\mathbb{R}^{3\times 3}$ orthogonal rotation matrix we have to compute the angle between start and end vectors.

It’s simple to show  that the angle $\theta$ formed by the two vectors $\mathbf{p}_1$ and $\mathbf{p}_2$ is

$\theta = \arccos \left( \dfrac{ \mathbf{p}_1 \cdot \mathbf{p}_2 }{ ||\mathbf{p}_2|| \cdot || \mathbf{p}_1 ||} \right)$

and the axis of rotation $\mathbf{u}$ is given by the normalized cross-product of the two vector

$\mathbf{u} = \dfrac{\mathbf{p}_1 \times \mathbf{p}_2 }{||\mathbf{p}_2|| \cdot || \mathbf{p}_1 ||}$

Vector to rotate ModelView matrix around

In OpenGL is then very simple to apply such a rotation, using a glRotate{f,d} function.

There is a standard method to obtain a rotation matrix for this kind of angle-axis transformation, but please forget quaternions, they are difficult and in this very simple case, rather an overkill in my opinion. Some informations can be found at wikipedia page for rotation matrices http://en.wikipedia.org/wiki/Rotation_matrix#Axis_and_angle

Just for clarity the $3 \times 3$ rotation matrix for a 3D axis-angle rotation of $\theta$ radians around a $u$ is given by:



The sphere is a good choice for a virtual trackball because it makes a good enclosure for most any object: and its surface is smooth and continuous, which is important in the generation of smooth rotations in response to smooth mouse movements.
Any smooth, continuous shape, however, could be used, so long as points on its surface can be generated in a consistent way.

I provide a C++ class for Arcball manipulation which only dependency is the matrix library Eigen, in the following snippet:

 #define _USE_MATH_DEFINES #include #include #include #include #include "Arcball.hpp" using namespace std; using namespace Eigen; /** * \ingroup GLVisualization * Default constructor, it sets the ballRadius to 600 **/ Arcball::Arcball() { this->ballRadius=600; isRotating=false; width=height=0; reset(); } /** * \ingroup GLVisualization * Set width and height of the current windows, it's needed every time you resize the window * \param w Width of the rendering window * \param h Height of the rendering window **/ void Arcball::setWidthHeight(int w, int h) { width=w; height=h; ballRadius = min((int)(w/2), (int)(h/2)); } /** * \ingroup GLVisualization * Set the radius of the ball (a typical radius for a 1024×768 window is 600 * \param newRadius The radius of the spherical dragging area **/ void Arcball::setRadius(float newRadius) { ballRadius = newRadius; } /** * \ingroup GLVisualization * Start the rotation. Use this method in association with the left click. * Here you must give directly the coordinates of the mouse as the glut functions extract. This method supposes that the 0,0 is in the upper-left part of the screen * \param _x Horizontal position of the mouse (0,0) = upperleft corner (w,h) = lower right * \param _y Vertical position of the mouse (0,0) = upperleft corner (w,h) = lower right * **/ void Arcball::startRotation(int _x, int _y) { int x = ( (_x)-(width/2) ); int y = ((height/2)-_y); startRotationVector = convertXY(x,y); startRotationVector.normalize(); currentRotationVector= startRotationVector; isRotating = true; } /** * \ingroup GLVisualization * Update the rotation. Use this method in association with the drag event. * Here you must give directly the coordinates of the mouse as the glut functions extract. This method supposes that the 0,0 is in the upper-left part of the screen * \param _x Horizontal position of the mouse (0,0) = upperleft corner (w,h) = lower right * \param _y Vertical position of the mouse (0,0) = upperleft corner (w,h) = lower right **/ void Arcball::updateRotation(int _x, int _y) { int x = ( (_x)-(width/2) ); int y = ((height/2)-_y); currentRotationVector = convertXY(x,y); currentRotationVector.normalize(); } /** * \ingroup GLVisualization * Apply the computed rotation matrix * This method must be invoked inside the \code glutDisplayFunc() \endcode * **/ void Arcball::applyRotationMatrix() { if (isRotating) { // Do some rotation according to start and current rotation vectors //cerr << currentRotationVector.transpose() << " " << startRotationVector.transpose() << endl; if ( ( currentRotationVector – startRotationVector).norm() > 1E-6 ) { Vector3d rotationAxis = currentRotationVector.cross(startRotationVector); rotationAxis.normalize(); double val = currentRotationVector.dot(startRotationVector); val > (1–1E-10) ? val=1.0 : val=val ; double rotationAngle = acos(val) * 180.0f/(float)M_PI; // rotate around the current position applyTranslationMatrix(true); glRotatef(rotationAngle * 2, -rotationAxis.x(), -rotationAxis.y(),-rotationAxis.z()); applyTranslationMatrix(false); } } glMultMatrixf(startMatrix); } /** * \ingroup GLVisualization * Stop the current rotation and prepare for a new click-then-drag event * **/ void Arcball::stopRotation() { glMatrixMode(GL_MODELVIEW); glLoadIdentity(); applyRotationMatrix(); // set the current matrix as the permanent one glGetFloatv(GL_MODELVIEW_MATRIX, startMatrix); isRotating = false; } /** * \ingroup GLVisualization * Apply the translation matrix to the current transformation (zoom factor) **/ void Arcball::applyTranslationMatrix(bool reverse) { float factor = (reverse?-1.0f:1.0f); float tx = transX + (currentTransX – startTransX)*TRANSLATION_FACTOR; float ty = transY + (currentTransY – startTransY)*TRANSLATION_FACTOR; glTranslatef(factor*tx, factor*(-ty), 0); } /** * \ingroup GLVisualization * Maps the mouse coordinates to points on a sphere, if the points lie outside the sphere, the z is 0, otherwise is \f$\sqrt(r^2 – (x^2+y^2) ) \f$ where \f$x,y \f$ * are the window centric coordinates of the mouse * \param x Mouse x coordinate * \param y Mouse y coordinate **/ Vector3d Arcball::convertXY(int x, int y) { int d = x*x+y*y; float radiusSquared = ballRadius*ballRadius; if (d > radiusSquared) { return Vector3d((float)x,(float)y, 0 ); } else { return Vector3d((float)x,(float)y, sqrt(radiusSquared – d)); } } /** * \ingroup GLVisualization * Reset the current transformation to the identity **/ void Arcball::reset() { fov = INITIAL_FOV; // reset matrix memset(startMatrix, 0, sizeof(startMatrix)); startMatrix[0] = 1; startMatrix[1] =0; startMatrix[2] = 0; startMatrix[3] = 0; startMatrix[4] = 0; startMatrix[5] =1; startMatrix[6] = 0; startMatrix[7] = 0; startMatrix[8] = 0; startMatrix[9] =0; startMatrix[10] = 1; startMatrix[11] = 0; startMatrix[12] = 0; startMatrix[13] =0; startMatrix[14] = 0; startMatrix[15] = 1; transX = transY = 0; startTransX = startTransY = currentTransX = currentTransY = 0; } const float Arcball::INITIAL_FOV = 30; const float Arcball::TRANSLATION_FACTOR = 0.01f;

view raw
Arcball.cpp
hosted with ❤ by GitHub

g++ -I/home/user/Desktop/Eigen -c Arcball.cpp Arcball.h -o Arcball.o