Do not use Euler angles for this as they have many issues like the one you got. Instead use cumulative transform matrices. It looks like this question is asked again and again... for some time now. So I decided to make an example how to do it with pure OpenGL 1.0 no GLM or funny stuff.
Definitions
Lets have Player control-able object called obj
and camera eye
. Each of them should be represented by separate 4x4
transform matrix. The OpenGL stores them as 1D arrays. For more info see
We want to control obj
in its local coordinate system independent on camera view. If you are used to have both camera and object matrices multiplied together in GL_MODELVIEW
to avoid GL_PROJECTION
matrix abuse then you quickly realize that this is not solvable by simply glRotate/glTranslate
calls in usual manner.
Because of that many people switch to Euler angles which can handle this easily but brings up a whole bunch of other problems (many nowadays games are still using them where they should not and there are a tons of bugs and issues because of it).
So add this to your project:
GLfloat mobj[16],meye[16];
Using GL for our matrices
This is simple just do this:
glMatrixMode(GL_MODELVIEW);
glPushMatrix();
glLoadMatrixf(mobj);
glGetFloatv(GL_MODELVIEW_MATRIX,mobj);
glPopMatrix();
With this we can use GL calls for our matrices outside the rendering loop. (for example in keyboard handler or in some timer).
Rendering loop matrices
Now if we want to render our object with our matrices then we need to set the GL matrices properly. Let assume Projection matrix is set then just Modelview is in question. The modelview matrix should be:
GL_MODELVIEW = Inverse(meye) * mobj
But OpenGL does not have any matrix inverse function. So this is the only thing we need to code. As the matrix is always 4x4
then it is not that hard.
I put all this together into this simple GL/C++/VCL example:
#include <vcl.h> // you can ignore this
#include <gl/gl.h>
#include <gl/glu.h>
#pragma hdrstop
#include "Unit1.h"
#pragma package(smart_init)
#pragma resource "*.dfm"
TForm1 *Form1;
int xs,ys;
HDC hdc;
HGLRC hrc;
GLfloat mobj[16];
GLfloat meye[16];
WORD key_left =37;
WORD key_right=39;
WORD key_up =38;
WORD key_down =40;
WORD key_forw =32;
bool _left =false;
bool _right=false;
bool _up =false;
bool _down =false;
bool _forw =false;
bool _shift=false;
bool _redraw=true;
GLfloat matrix_subdet ( GLfloat *a,int r,int s)
{
GLfloat c,q[9];
int i,j,k;
k=0;
for (j=0;j<4;j++)
if (j!=s)
for (i=0;i<4;i++)
if (i!=r)
{
q[k]=a[i+(j<<2)];
k++;
}
c=0;
c+=q[0]*q[4]*q[8];
c+=q[1]*q[5]*q[6];
c+=q[2]*q[3]*q[7];
c-=q[0]*q[5]*q[7];
c-=q[1]*q[3]*q[8];
c-=q[2]*q[4]*q[6];
if (int((r+s)&1)) c=-c;
return c;
}
void matrix_subdet (GLfloat *c,GLfloat *a)
{
GLfloat q[16];
int i,j;
for (i=0;i<4;i++)
for (j=0;j<4;j++)
q[j+(i<<2)]=matrix_subdet(a,i,j);
for (i=0;i<16;i++) c[i]=q[i];
}
GLfloat matrix_det ( GLfloat *a)
{
GLfloat c=0;
c+=a[ 0]*matrix_subdet(a,0,0);
c+=a[ 4]*matrix_subdet(a,0,1);
c+=a[ 8]*matrix_subdet(a,0,2);
c+=a[12]*matrix_subdet(a,0,3);
return c;
}
GLfloat matrix_det ( GLfloat *a,GLfloat *b)
{
GLfloat c=0;
c+=a[ 0]*b[ 0];
c+=a[ 4]*b[ 1];
c+=a[ 8]*b[ 2];
c+=a[12]*b[ 3];
return c;
}
void matrix_inv (GLfloat *c,GLfloat *a)
{
GLfloat d[16],D;
matrix_subdet(d,a);
D=matrix_det(a,d);
if (D) D=1.0/D;
for (int i=0;i<16;i++) c[i]=d[i]*D;
}
int TForm1::ogl_init()
{
if (ogl_inicialized) return 1;
hdc = GetDC(Form1->Handle);
PIXELFORMATDESCRIPTOR pfd;
ZeroMemory( &pfd, sizeof( pfd ) );
pfd.nSize = sizeof( pfd );
pfd.nVersion = 1;
pfd.dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER;
pfd.iPixelType = PFD_TYPE_RGBA;
pfd.cColorBits = 24;
pfd.cDepthBits = 24;
pfd.iLayerType = PFD_MAIN_PLANE;
SetPixelFormat(hdc,ChoosePixelFormat(hdc, &pfd),&pfd);
hrc = wglCreateContext(hdc);
if(hrc == NULL)
{
ShowMessage("Could not initialize OpenGL Rendering context !!!");
ogl_inicialized=0;
return 0;
}
if(wglMakeCurrent(hdc, hrc) == false)
{
ShowMessage("Could not make current OpenGL Rendering context !!!");
wglDeleteContext(hrc);
ogl_inicialized=0;
return 0;
}
ogl_resize();
glEnable(GL_DEPTH_TEST);
glDisable(GL_CULL_FACE);
glDisable(GL_TEXTURE_2D);
glDisable(GL_BLEND);
glShadeModel(GL_SMOOTH);
ogl_inicialized=1;
return 1;
}
void TForm1::ogl_exit()
{
if (!ogl_inicialized) return;
wglMakeCurrent(NULL, NULL);
wglDeleteContext(hrc);
ogl_inicialized=0;
}
void TForm1::ogl_draw()
{
_redraw=false;
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
GLfloat ieye[16];
matrix_inv(ieye,meye);
glMatrixMode(GL_MODELVIEW);
glLoadMatrixf(ieye);
glMultMatrixf(mobj);
float x=0.5,y=0.1,z=0.7;
glColor3f(0.7,0.7,0.7);
glBegin(GL_TRIANGLE_FAN);
glVertex3f(0.0,0.0,+z);
glVertex3f( -x,-y,-z);
glVertex3f( +x,-y,-z);
glVertex3f(0.0,+y,-z);
glVertex3f( -x,-y,-z);
glEnd();
glColor3f(0.5,0.5,0.5);
glBegin(GL_TRIANGLES);
glVertex3f( -x,-y,-z);
glVertex3f( +x,-y,-z);
glVertex3f(0.0,+y,-z);
glEnd();
glBegin(GL_LINES);
glColor3f(1.0,0.0,0.0); glVertex3f(0.0,0.0,0.0); glVertex3f(1.0,0.0,0.0);
glColor3f(0.0,1.0,0.0); glVertex3f(0.0,0.0,0.0); glVertex3f(0.0,1.0,0.0);
glColor3f(0.0,0.0,1.0); glVertex3f(0.0,0.0,0.0); glVertex3f(0.0,0.0,1.0);
glEnd();
glFlush();
SwapBuffers(hdc);
}
void TForm1::ogl_resize()
{
xs=ClientWidth;
ys=ClientHeight;
if (xs<=0) xs = 1;
if (ys<=0) ys = 1;
if (!ogl_inicialized) return;
glViewport(0,0,xs,ys);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective(30,float(xs)/float(ys),0.1,100.0);
glMatrixMode(GL_TEXTURE);
glLoadIdentity();
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
}
__fastcall TForm1::TForm1(TComponent* Owner):TForm(Owner)
{
ogl_inicialized=0;
hdc=NULL;
hrc=NULL;
ogl_init();
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
glRotatef(180.0,0.0,1.0,0.0);
glGetFloatv(GL_MODELVIEW_MATRIX,mobj);
glLoadIdentity();
glTranslatef(0.0,0.0,+20.0);
glGetFloatv(GL_MODELVIEW_MATRIX,meye);
}
void __fastcall TForm1::FormDestroy(TObject *Sender)
{
ogl_exit();
}
void __fastcall TForm1::FormResize(TObject *Sender)
{
ogl_resize();
_redraw=true;
}
void __fastcall TForm1::FormPaint(TObject *Sender)
{
_redraw=true;
}
void __fastcall TForm1::tim_updateTimer(TObject *Sender)
{
GLfloat da=5.0;
GLfloat dp=0.1;
glMatrixMode(GL_MODELVIEW);
glPushMatrix();
if (_shift)
{
glLoadMatrixf(meye);
if (_left ) { _redraw=true; glRotatef(+da,0.0,1.0,0.0); }
if (_right) { _redraw=true; glRotatef(-da,0.0,1.0,0.0); }
if (_up ) { _redraw=true; glRotatef(-da,1.0,0.0,0.0); }
if (_down ) { _redraw=true; glRotatef(+da,1.0,0.0,0.0); }
glGetFloatv(GL_MODELVIEW_MATRIX,meye);
}
else{
glLoadMatrixf(mobj);
if (_left ) { _redraw=true; glRotatef(+da,0.0,1.0,0.0); }
if (_right) { _redraw=true; glRotatef(-da,0.0,1.0,0.0); }
if (_up ) { _redraw=true; glRotatef(-da,1.0,0.0,0.0); }
if (_down ) { _redraw=true; glRotatef(+da,1.0,0.0,0.0); }
glGetFloatv(GL_MODELVIEW_MATRIX,mobj);
}
glPopMatrix();
if (_forw )
{
_redraw=true;
mobj[12]+=dp*mobj[8];
mobj[13]+=dp*mobj[9];
mobj[14]+=dp*mobj[10];
}
if (_redraw) ogl_draw();
}
void __fastcall TForm1::FormMouseWheelDown(TObject *Sender, TShiftState Shift, TPoint &MousePos, bool &Handled)
{
glMatrixMode(GL_MODELVIEW);
glPushMatrix();
glLoadMatrixf(meye);
glTranslatef(0,0,+2.0);
glGetFloatv(GL_MODELVIEW_MATRIX,meye);
glPopMatrix();
Handled=true;
_redraw=true;
}
void __fastcall TForm1::FormMouseWheelUp(TObject *Sender, TShiftState Shift, TPoint &MousePos, bool &Handled)
{
glMatrixMode(GL_MODELVIEW);
glPushMatrix();
glLoadMatrixf(meye);
glTranslatef(0,0,-2.0);
glGetFloatv(GL_MODELVIEW_MATRIX,meye);
glPopMatrix();
Handled=true;
_redraw=true;
}
void __fastcall TForm1::FormKeyDown(TObject *Sender, WORD &Key,TShiftState Shift)
{
_shift=Shift.Contains(ssShift);
if (Key==key_left ) _left =true;
if (Key==key_right) _right=true;
if (Key==key_up ) _up =true;
if (Key==key_down ) _down =true;
if (Key==key_forw ) _forw =true;
Key=0;
}
void __fastcall TForm1::FormKeyUp(TObject *Sender, WORD &Key, TShiftState Shift)
{
_shift=Shift.Contains(ssShift);
if (Key==key_left ) _left =false;
if (Key==key_right) _right=false;
if (Key==key_up ) _up =false;
if (Key==key_down ) _down =false;
if (Key==key_forw ) _forw =false;
}
void __fastcall TForm1::FormActivate(TObject *Sender)
{
_left =false;
_right=false;
_up =false;
_down =false;
_forw =false;
}
It is simple single form VCL application with single 20ms timer in it. So port the events to your environment style code. You can ignore the VCL pragmas and includes. This example is driven by arrows. If shift is pressed then the arrows are turning camera otherwise the object. Space is moving object forward.
Here compiled Win32 standalone demo:
This approach has one drawback
With cumulating the transforms you are loosing precision. To remedy that you can exploit vector multiplication (cross product). Simply count the number of operations performed on such matrix and if threshold is reached normalize the matrix and reset the counter.
By normalization I mean ensuring all the axises are unit and perpendicular to each other Leaving the direction of main axis (usually forward of view or object) as is. Cross product of 2 vectors returns perpendicular vector to each. So for example if you extract the X,Y,Z
axises (locations are described in the link in #1) and Z
is the main axis then:
X = Y x Z
Y = Z x X
Z = Z / |Z|
X = X / |X|
Y = Y / |Y|
Where:
W.x=(U.y*V.z)-(U.z*V.y)
W.y=(U.z*V.x)-(U.x*V.z)
W.z=(U.x*V.y)-(U.y*V.x)
a=U.x*V.x+U.y*V.y+U.z*V.z
a=sqrt((U.x*U.x)+(U.y*U.y)+(U.z*U.z))
In case your coordinate system is not derived from unit matrix then you need to negate some axis or change the order of cross product operands to ensure the direction of your axises stays as should.
For more info take a look at:
glOrtho
doing there? - derhass