If there is no sliding then:
rotation axis
will be parallel to your floor and perpendicular to your movement. So you can exploit cross product to get it. Let:
n
- floor normal vector
t
- movement direction parallel with floor (tangent)
b
- our rotation axis (binormal)
so we can compute it as:
b = cross(t,n) // cross product create perpendicular vector to both operands
t /= |t| // normalize to unit vector
b /= |b| // normalize to unit vector
n /= |n| // normalize to unit vector
rotation speed
this can be derived from arclength and speed vel [unit/s]
. So if our sphere is of radius r
then:
ang*r = vel*t
ang = vel*t/r // t=1.0 sec
omg = vel/r // [rad/sec]
so we need to rotate our sphere by omg
each second.
rotation math
Euler angles (your sequenced rotations X,Y,Z) are the worst thing for this I can think of as they will lead to singularities and weird stuff making this simple example horrible nightmare to implement. have you seen in a game or any 3D engine that suddenly you can not look as you expect, or randomly spin until you move/rotate differently or suddenly rotate by 180deg ... ? That are Euler angles singularities at work without proper handling...
Quaternions are somewhat alien to most people (me included) as they do not work like we think. IIRC You can look at them as efficient way of computing 3x3 3D rotation matrix with less goniometric functions needed. As we now have much different computational power than 20 years ago theres not much point choosing them if you do not know them at all. Anyway they have also another advantages which are still relevant like you can interpolate between rotations etc.
4x4 homogenuous transform matrices are your best choice. As their geometric representation is compatible with human abstract thinking (you can imagine what and how it is done hence you can construct your own matrices instead of having them as bunch of meaningless numbers).
I strongly recommend to start with 3D 4x4 homogenuous transform matrices. So all the rest of this answer will be aimed to them.
rotating
Now There are 2 ways I know of how to achieve your rotation. Either use Rodrigues_rotation_formula and encode it as transform matrix or simply construct your own rotation matrix that will represent your sphere aligned to floor. direction of movement and rotation axis.
The latter is much much simpler and we can do it directly as we already know the 3 basis vectors needed (t,b,n
). What is left is only the sphere position which should be also known.
So at start create a transform matrix (assuming OpenGL notation):
| tx bx nx x0 |
| ty by ny y0 |
| tz bz nz z0 |
| 0 0 0 1 |
Where x0,y0,z0
is start position of your sphere aligned with your mesh. So if center point of your mesh is (0,0,0)
then place your sphere r
above the floor...
Now just each elapsed time dt [sec]
(like timer) multiply this matrix by incremental rotation matrix around y
axis (as b
is our rotation axis) and angle omg*dt [rad]
.
We also need to translate our sphere by t*vel*dt
so simply add this vector to matrix position or multiply our matrix with:
| 1 0 0 tx*vel*dt |
| 0 1 0 ty*vel*dt |
| 0 0 1 tz*vel*dt |
| 0 0 0 1 |
And also render the scene again using our resulting matrix...
This approach is nice as you can anytime change the direction of movement (you just remember the position and change the inner 3x3 rotation part of the matrix with new t,b,n
vectors.
However there is one disadvantage that such cumulative matrix will degrade the accuracy over time (as we are performing multiplication by floating numbers over and over on it without reset) so the matrix can deform over time. To avoid this is enough to recompute and set the t,b,n
part of the matrix from time to time. I am used to do it each 128 rotations on 64bit double
variables precision. It can be done also automatically (when you have no prior info about the axises) I am doing like this:
Also using matrices have different notations (row/column major order, multiplication order) which can affect the equations a bit (either reverse order of multiplication and/or using inverse matrices instead).
Now in case your 3D engine does not support matrices (which is highly unlikely) you would need to convert our resulting matrix back into Euler angles. That is doable by goniometrics but for that you would need to know the order of the angles.
In case of Sliding you need to go in reverse order. So first compute the rotations and then compute the direction of translation from the grip forces with floor and inertia. Which is a bit more complex and pure physics ...
[Edit1] rotundus style simple OpenGL/C++/VCL example

Here simple control example using cumulative matrix (without the accuracy preservation):
//---------------------------------------------------------------------------
#include <vcl.h> // VCL stuff (ignore)
#include <math.h> // sin,cos,M_PI
#pragma hdrstop // VCL stuff (ignore)
#include "Unit1.h" // VCL stuff (header of this window)
#include "gl_simple.h" // my GL init (source included)
//---------------------------------------------------------------------------
#pragma package(smart_init) // VCL stuff (ignore)
#pragma resource "*.dfm" // VCL stuff (ignore)
TForm1 *Form1; // VCL stuff (this window)
//---------------------------------------------------------------------------
// vector/matrix math
//---------------------------------------------------------------------------
void vector_mul(double *c,double *a,double *b) // c[3] = a[3] x b[3] (cross product)
{
double q[3];
q[0]=(a[1]*b[2])-(a[2]*b[1]);
q[1]=(a[2]*b[0])-(a[0]*b[2]);
q[2]=(a[0]*b[1])-(a[1]*b[0]);
for(int i=0;i<3;i++) c[i]=q[i];
}
//---------------------------------------------------------------------------
void matrix_mul_vector(double *c,double *a,double *b) // c[3] = a[16]*b[3] (w=1)
{
double q[3];
q[0]=(a[ 0]*b[0])+(a[ 4]*b[1])+(a[ 8]*b[2])+(a[12]);
q[1]=(a[ 1]*b[0])+(a[ 5]*b[1])+(a[ 9]*b[2])+(a[13]);
q[2]=(a[ 2]*b[0])+(a[ 6]*b[1])+(a[10]*b[2])+(a[14]);
for(int i=0;i<3;i++) c[i]=q[i];
}
//---------------------------------------------------------------------------
void matrix_inv(double *a,double *b) // a[16] = (Pseudo)Inverse(b[16])
{
double x,y,z;
// transpose of rotation matrix
a[ 0]=b[ 0];
a[ 5]=b[ 5];
a[10]=b[10];
x=b[1]; a[1]=b[4]; a[4]=x;
x=b[2]; a[2]=b[8]; a[8]=x;
x=b[6]; a[6]=b[9]; a[9]=x;
// copy projection part
a[ 3]=b[ 3];
a[ 7]=b[ 7];
a[11]=b[11];
a[15]=b[15];
// convert origin: new_pos = - new_rotation_matrix * old_pos
x=(a[ 0]*b[12])+(a[ 4]*b[13])+(a[ 8]*b[14]);
y=(a[ 1]*b[12])+(a[ 5]*b[13])+(a[ 9]*b[14]);
z=(a[ 2]*b[12])+(a[ 6]*b[13])+(a[10]*b[14]);
a[12]=-x;
a[13]=-y;
a[14]=-z;
}
//---------------------------------------------------------------------------
double* matrix_ld (double *p,double a0,double a1,double a2,double a3,double a4,double a5,double a6,double a7,double a8,double a9,double a10,double a11,double a12,double a13,double a14,double a15) { p[0]=a0; p[1]=a1; p[2]=a2; p[3]=a3; p[4]=a4; p[5]=a5; p[6]=a6; p[7]=a7; p[8]=a8; p[9]=a9; p[10]=a10; p[11]=a11; p[12]=a12; p[13]=a13; p[14]=a14; p[15]=a15; return p; }
//---------------------------------------------------------------------------
void matrix_mul (double *c,double *a,double *b) // c[16] = a[16] * b[16]
{
double q[16];
q[ 0]=(a[ 0]*b[ 0])+(a[ 1]*b[ 4])+(a[ 2]*b[ 8])+(a[ 3]*b[12]);
q[ 1]=(a[ 0]*b[ 1])+(a[ 1]*b[ 5])+(a[ 2]*b[ 9])+(a[ 3]*b[13]);
q[ 2]=(a[ 0]*b[ 2])+(a[ 1]*b[ 6])+(a[ 2]*b[10])+(a[ 3]*b[14]);
q[ 3]=(a[ 0]*b[ 3])+(a[ 1]*b[ 7])+(a[ 2]*b[11])+(a[ 3]*b[15]);
q[ 4]=(a[ 4]*b[ 0])+(a[ 5]*b[ 4])+(a[ 6]*b[ 8])+(a[ 7]*b[12]);
q[ 5]=(a[ 4]*b[ 1])+(a[ 5]*b[ 5])+(a[ 6]*b[ 9])+(a[ 7]*b[13]);
q[ 6]=(a[ 4]*b[ 2])+(a[ 5]*b[ 6])+(a[ 6]*b[10])+(a[ 7]*b[14]);
q[ 7]=(a[ 4]*b[ 3])+(a[ 5]*b[ 7])+(a[ 6]*b[11])+(a[ 7]*b[15]);
q[ 8]=(a[ 8]*b[ 0])+(a[ 9]*b[ 4])+(a[10]*b[ 8])+(a[11]*b[12]);
q[ 9]=(a[ 8]*b[ 1])+(a[ 9]*b[ 5])+(a[10]*b[ 9])+(a[11]*b[13]);
q[10]=(a[ 8]*b[ 2])+(a[ 9]*b[ 6])+(a[10]*b[10])+(a[11]*b[14]);
q[11]=(a[ 8]*b[ 3])+(a[ 9]*b[ 7])+(a[10]*b[11])+(a[11]*b[15]);
q[12]=(a[12]*b[ 0])+(a[13]*b[ 4])+(a[14]*b[ 8])+(a[15]*b[12]);
q[13]=(a[12]*b[ 1])+(a[13]*b[ 5])+(a[14]*b[ 9])+(a[15]*b[13]);
q[14]=(a[12]*b[ 2])+(a[13]*b[ 6])+(a[14]*b[10])+(a[15]*b[14]);
q[15]=(a[12]*b[ 3])+(a[13]*b[ 7])+(a[14]*b[11])+(a[15]*b[15]);
for(int i=0;i<16;i++) c[i]=q[i];
}
//---------------------------------------------------------------------------
// old style GL sphere mesh
//---------------------------------------------------------------------------
const int nb=15; // slices
const int na=nb<<1; // points per equator
class sphere
{
public:
// movement
double r; // sphere radius [units]
double m[16]; // sphere direct matrix
double vel; // actual velocity [unit/sec] in forward direction
void turn(double da) // turn left/right by angle [deg]
{
// rotate m around global Z axis
da*=M_PI/180.0; // [deg] -> [rad]
double c=cos(da),s=sin(da),xyz[16];
matrix_ld(xyz, c,-s, 0, 0, // incremental rotation around Z
s, c, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1);
matrix_mul_vector(m+0,xyz,m+0); // transform all basis vectors of m from xyz [LCS] into world [GCS]
matrix_mul_vector(m+4,xyz,m+4);
matrix_mul_vector(m+8,xyz,m+8);
}
void update(double dt) // simulate dt [sec] time is elapsed
{
if (fabs(vel)<1e-6) return; // ignore stopped case
// compute unit tangent (both vectors are unit so no normalization needed)
double t[3]={ 0.0,0.0,1.0 }; // tangent is perpendiculr to global Z (turning axis)
vector_mul(t,t,m+0); // and perpendicular to local X (movement rotation axis)
// update position
for (int i=0;i<3;i++) m[12+i]+=vel*dt*t[i];
// update rotation
double da=vel*dt/r,c=cos(da),s=sin(da);
double xyz[16];
matrix_ld(xyz, 1, 0, 0, 0,
0, c,-s, 0,
0, s, c, 0,
0, 0, 0, 1);
matrix_mul(m,xyz,m);
}
// mesh and rendering
bool _init; // has been initiated ?
GLfloat pos[na][nb][3]; // vertex
GLfloat nor[na][nb][3]; // normal
GLfloat txr[na][nb][2]; // texcoord
GLuint txrid; // texture id
sphere() { _init=false; txrid=0; }
~sphere() { if (_init) glDeleteTextures(1,&txrid); }
void init(GLfloat r,AnsiString texture); // call after OpenGL is already working !!!
void draw();
};
void sphere::init(GLfloat _r,AnsiString texture)
{
GLfloat x,y,z,a,b,da,db;
GLfloat tx0,tdx,ty0,tdy;// just correction if CLAMP_TO_EDGE is not available
int ia,ib;
// varables
r=_r; vel=0.0;
for (ia=0;ia<16;ia++ ) m[ia]=0.0;
for (ia=0;ia<16;ia+=5) m[ia]=1.0;
// mesh
if (!_init) { _init=true; glGenTextures(1,&txrid); }
// a,b to texture coordinate system
tx0=0.0;
ty0=0.5;
tdx=0.5/M_PI;
tdy=1.0/M_PI;
// load texture to GPU memory
if (texture!="")
{
Byte q;
unsigned int *pp;
int xs,ys,x,y,adr,*txr;
union { unsigned int c32; Byte db[4]; } c;
Graphics::TBitmap *bmp=new Graphics::TBitmap; // new bmp
bmp->LoadFromFile(texture); // load from file
bmp->HandleType=bmDIB; // allow direct access to pixels
bmp->PixelFormat=pf32bit; // set pixel to 32bit so int is the same size as pixel
xs=bmp->Width; // resolution should be power of 2
ys=bmp->Height;
txr=new int[xs*ys];
for(adr=0,y=0;y<ys;y++)
{
pp=(unsigned int*)bmp->ScanLine[y];
for(x=0;x<xs;x++,adr++)
{
// rgb2bgr and copy bmp -> txr[]
c.c32=pp[x];
q =c.db[2];
c.db[2]=c.db[0];
c.db[0]=q;
txr[adr]=c.c32;
}
}
glEnable(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D,txrid);
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S,GL_CLAMP);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T,GL_CLAMP);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER,GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,GL_LINEAR);
glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE,GL_MODULATE);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, xs, ys, 0, GL_RGBA, GL_UNSIGNED_BYTE, txr);
glDisable(GL_TEXTURE_2D);
delete bmp;
delete[] txr;
// texture coordinates by 1 pixel from each edge (GL_CLAMP_TO_EDGE)
tx0+=1.0/GLfloat(xs);
ty0+=1.0/GLfloat(ys);
tdx*=GLfloat(xs-2)/GLfloat(xs);
tdy*=GLfloat(ys-2)/GLfloat(ys);
}
// correct texture coordinate system (invert x)
tx0=1.0-tx0; tdx=-tdx;
da=(2.0*M_PI)/GLfloat(na-1);
db= M_PI /GLfloat(nb-1);
for (ib=0,b=-0.5*M_PI;ib<nb;ib++,b+=db)
for (ia=0,a= 0.0 ;ia<na;ia++,a+=da)
{
x=cos(b)*cos(a);
y=cos(b)*sin(a);
z=sin(b);
nor[ia][ib][0]=x;
nor[ia][ib][1]=y;
nor[ia][ib][2]=z;
pos[ia][ib][0]=r*x;
pos[ia][ib][1]=r*y;
pos[ia][ib][2]=r*z;
txr[ia][ib][0]=tx0+(a*tdx);
txr[ia][ib][1]=ty0+(b*tdy);
}
}
void sphere::draw()
{
if (!_init) return;
int ia,ib0,ib1;
glMatrixMode(GL_MODELVIEW);
glPushMatrix();
glMultMatrixd(m);
glEnable(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D,txrid);
glEnable(GL_CULL_FACE);
glFrontFace(GL_CW);
glEnable(GL_LIGHTING);
glEnable(GL_LIGHT0);
glColor3f(1.0,1.0,1.0);
for (ib0=0,ib1=1;ib1<nb;ib0=ib1,ib1++)
{
glBegin(GL_QUAD_STRIP);
for (ia=0;ia<na;ia++)
{
glNormal3fv (nor[ia][ib0]);
glTexCoord2fv(txr[ia][ib0]);
glVertex3fv (pos[ia][ib0]);
glNormal3fv (nor[ia][ib1]);
glTexCoord2fv(txr[ia][ib1]);
glVertex3fv (pos[ia][ib1]);
}
glEnd();
}
glDisable(GL_TEXTURE_2D);
glDisable(GL_CULL_FACE);
glDisable(GL_LIGHTING);
glDisable(GL_LIGHT0);
/*
// local axises
double q=1.5*r;
glBegin(GL_LINES);
glColor3f(1.0,0.0,0.0); glVertex3d(0.0,0.0,0.0); glVertex3d(q,0.0,0.0);
glColor3f(0.0,1.0,0.0); glVertex3d(0.0,0.0,0.0); glVertex3d(0.0,q,0.0);
glColor3f(0.0,0.0,1.0); glVertex3d(0.0,0.0,0.0); glVertex3d(0.0,0.0,q);
glEnd();
*/
glMatrixMode(GL_MODELVIEW);
glPopMatrix();
}
//---------------------------------------------------------------------------
// rendring
bool _redraw=false;
double ieye[16]; // camera inverse matrix
sphere obj;
// key codes for controling (Arrows + Space)
WORD key_left =37;
WORD key_right=39;
WORD key_up =38;
WORD key_down =40;
// key pressed state
bool _left =false;
bool _right=false;
bool _up =false;
bool _down =false;
//---------------------------------------------------------------------------
void draw_map()
{
int i,j;
double u,v,p[3],dp[3];
// here 3D view must be already set (modelview,projection)
glDisable(GL_CULL_FACE);
// [draw 3D map]
const int n=30; // map size
double p0[3]={0.0,0.0,0.0}; // map start point
double du[3]={1.0,0.0,0.0}; // map u step (size of grid = 1.0 )
double dv[3]={0.0,1.0,0.0}; // map v step (size of grid = 1.0 )
glColor3f(0.5,0.7,1.0);
glBegin(GL_LINES);
for (j=0;j<=n;j++)
{
for (i=0;i<3;i++) p[i]=p0[i]+(double(j)*du[i])+(double(0)*dv[i]); glVertex3dv(p);
for (i=0;i<3;i++) p[i]=p0[i]+(double(j)*du[i])+(double(n)*dv[i]); glVertex3dv(p);
for (i=0;i<3;i++) p[i]=p0[i]+(double(0)*du[i])+(double(j)*dv[i]); glVertex3dv(p);
for (i=0;i<3;i++) p[i]=p0[i]+(double(n)*du[i])+(double(j)*dv[i]); glVertex3dv(p);
}
glEnd();
}
//---------------------------------------------------------------------------
void gl_draw()
{
_redraw=false;
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glMatrixMode(GL_MODELVIEW);
glLoadMatrixd(ieye); // inverse camera matrix
obj.draw();
draw_map();
glFlush();
SwapBuffers(hdc);
}
//---------------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner):TForm(Owner)
{
// this is called on window startup
gl_init(Handle); // init OpenGL 1.0
glMatrixMode(GL_MODELVIEW); // set camera to vew our map
glLoadIdentity;
glTranslatef(-15.0,-5.0,-10.5); // "centered" position above the map
glRotatef(-60.0,1.0,0.0,0.0); // rotate view to be more parallel to plane
glGetDoublev(GL_MODELVIEW_MATRIX,ieye); // store result
// ini obj
obj.init(1.0,"ball.bmp"); // radius texture and mesh
obj.m[12]=10.0; // position (x,y,z)
obj.m[13]=10.0;
obj.m[14]=+obj.r;
}
//---------------------------------------------------------------------------
void __fastcall TForm1::FormDestroy(TObject *Sender)
{
// this is called before window exits
gl_exit(); // exit OpenGL
}
//---------------------------------------------------------------------------
void __fastcall TForm1::FormResize(TObject *Sender)
{
// this is called on each window resize (and also after startup)
gl_resize(ClientWidth,ClientHeight);
}
//---------------------------------------------------------------------------
void __fastcall TForm1::FormPaint(TObject *Sender)
{
// this is called whnewer app needs repaint
gl_draw();
}
//---------------------------------------------------------------------------
void __fastcall TForm1::FormKeyDown(TObject *Sender, WORD &Key, TShiftState Shift)
{
// on key down event
if (Key==key_left ) _left =true;
if (Key==key_right) _right=true;
if (Key==key_up ) _up =true;
if (Key==key_down ) _down =true;
Key=0; // key is handled
}
//---------------------------------------------------------------------------
void __fastcall TForm1::FormKeyUp(TObject *Sender, WORD &Key, TShiftState Shift)
{
// on key release event
if (Key==key_left ) _left =false;
if (Key==key_right) _right=false;
if (Key==key_up ) _up =false;
if (Key==key_down ) _down =false;
}
//---------------------------------------------------------------------------
void __fastcall TForm1::FormMouseActivate(TObject *Sender, TMouseButton Button, TShiftState Shift, int X, int Y, int HitTest, TMouseActivate &MouseActivate)
{
_left =false; // clear key flags after focus change
_right=false; // just to avoid constantly "pressed" keys
_up =false; // after window focus swaping during key press
_down =false; // many games are ignoring this and you need to
}
//---------------------------------------------------------------------------
void __fastcall TForm1::Timer1Timer(TObject *Sender)
{
// here movement and repaint timer handler (I have 20ms interval)
double dt=0.001*double(Timer1->Interval); // timer period [sec]
double da=90.0*dt; // angular turn speed in [deg/sec]
double dp=10.0*dt; // camera movement speed in [units/sec]
double dv=10.0*dt; // sphere acceleration [units/sec^2]
// control object
if (_left ) { _redraw=true; obj.turn(-da); }
if (_right) { _redraw=true; obj.turn(+da); }
if (_up ) { _redraw=true; obj.vel+=dv; }
if (_down ) { _redraw=true; obj.vel-=dv; }
// simulate the ball movement
obj.update(dt); _redraw=true;
// render if needed
if (_redraw) gl_draw();
}
//---------------------------------------------------------------------------
Its an empty single form VCL app with single 20ms timer on it. In order to port to your environment just ignore the VCL stuff, mimic the relevant events of the app and port rendering to your components/style/api. The only important stuff is just the sphere
class marked as // movement
and the timer event Timer1Timer(TObject *Sender)
. All the rest is just rendering and keyboard handling ... Which I susspect you already got handled on your own ...
The preview shows movement while I control the ball with arrows:
up/down - accelerate/decelerate
left/right - turn left/right in respect to forward direction around normal to surface
Here texture I used (drawed in mspaint by hand so it might not be pixel perfect symmetrical...)

The gl_simple.h
of mine can be found in here: