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')).

Arcball tutorial

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 <math.h>
#include <iostream>
#include <Eigen/Core>
#include <Eigen/Geometry>
#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 > (11E-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

#ifndef ARCBALL_H
#define ARCBALL_H
#ifdef __APPLE__
#include <OpenGL/OpenGL.h>
#endif
#ifdef __linux__
#include <GL/gl.h>
#include <GL/glu.h>
#endif
#ifdef _WIN32
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#include <gl\gl.h> // Header File For The OpenGL32 Library
#include <gl\glu.h> // Header File For The GLu32 Library
#endif
#include <Eigen/Core>
#include <Eigen/Geometry>
using namespace Eigen;
/*
* \class Arcball
* \ingroup GLVisualization
* \brief Arcball is a method to manipulate and rotate objects in 3D intuitively.
*
* The motivation behind the trackball (aka 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.
*
* 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.
* The algorithm for accomplishing this, needs to perform the following steps (not neseccarily in order).
*
*
*
*
* \b ArcBall Algorithm made easy
* – Detect the left-button of the mouse being depressed.
* – Keep track of the last known mouse position.
* – Treat the mouse position as the projection of a point on the hemi-sphere down to the image plane (along the z-axis), and determine that point on the hemi-sphere.
* – Detect the mouse movement
* – Determine the great circle connecting the old mouse-hemi-sphere point to the current mouse-hemi-sphere point.
* – Calculate the normal to this plane. This will be the axis about which to rotate.
* – Set the OpenGL state to modify the MODELVIEW matrix.
* – Read off the current matrix, since we want this operation to be the last transformation, not the first, and OpenGL does things LIFO.
* – Reset the model-view matrix to the identity
* – Rotate about the axis
* – Multiply the resulting matrix by the saved matrix.
* – Force a redraw of the scene.
*
* An example of the code for using successfully this class is the following:
* First set the radius of the arcball by using the appropriate method when you call the handle to the GLUT resize method (this is usually accomplished with a
* \code
* void handleResize(int w, int h)
* \endcode
* function in GLUT (if you are using Qt or other, please refer to the relative API ).
* An example of a \code handleResize(int w, int h) \endcode function is the following:
* \code
* void handleResize(int w, int h)
* {
* arcball.setWidthHeight(w, h);
* glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
* glViewport(0, 0, w, h);
*
* glMatrixMode(GL_PROJECTION);
* glLoadIdentity();
* gluPerspective(viewAngle, (float)w / (float)w, zNear, zFar );
*
* glMatrixMode(GL_MODELVIEW);
* }
* \endcode
*
*
These are the two functions (GLUT) that control the mouse events:
* \code
* void mouseFunc(int state, int button, int _x , int _y)
* {
* if ( button == GLUT_LEFT_BUTTON )
* arcball.startRotation(_x,_y);
* else
* arcball.stopRotation();
*
*
* glutPostRedisplay();
* }
*
* void mouseDrag(int _x, int _y)
* {
*
* arcball.updateRotation(_x,_y);
* glutPostRedisplay();
* }
*
* \endcode
*
*/
typedef float GLMatrix[16];
class Arcball
{
private:
float fov;
int fovStartY;
int fovCurrentY;
float transX, transY;
float currentTransX, currentTransY;
float startTransX, startTransY;
GLMatrix startMatrix;
GLMatrix currentMatrix;
Vector3d startRotationVector;
Vector3d currentRotationVector;
bool isRotating;
float ballRadius;
double residualSpin;
static const float INITIAL_FOV;
static const float MINIMAL_FOV;
static const float TRANSLATION_FACTOR;
Vector3d convertXY(int x, int y);
int width, height;
public:
Arcball();
void setWidthHeight(int w, int h);
void startRotation(int x, int y);
void updateRotation(int x, int y);
void stopRotation();
void applyTranslationMatrix(bool reverse = false);
void applyRotationMatrix();
void setRadius(float newRadius);
void reset();
};
#endif

view raw
Arcball.hpp
hosted with ❤ by GitHub

P.S. If you want to compile the two class with g++ compiler, don’t forget to include your Eigen version

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

I don’t provide an executable or Makefile, so don’t ask me.

Un pensiero su “Tutorial of Arcball without quaternions

Rispondi

Inserisci i tuoi dati qui sotto o clicca su un'icona per effettuare l'accesso:

Logo di WordPress.com

Stai commentando usando il tuo account WordPress.com. Chiudi sessione /  Modifica )

Google photo

Stai commentando usando il tuo account Google. Chiudi sessione /  Modifica )

Foto Twitter

Stai commentando usando il tuo account Twitter. Chiudi sessione /  Modifica )

Foto di Facebook

Stai commentando usando il tuo account Facebook. Chiudi sessione /  Modifica )

Connessione a %s...