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); // set matrix target we want to work with does not matter really which)
glPushMatrix(); // store original matrix so we do not mess something up
glLoadMatrixf(mobj); // load our matrix into GL
//here do your stuff like glRotatef(10.0,0.0,1.0,0.0);
glGetFloatv(GL_MODELVIEW_MATRIX,mobj); // get our updated matrix from GL
glPopMatrix(); // restore original state
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 // you can ignore this
#include "Unit1.h" // you can ignore this
//---------------------------------------------------------------------------
// you can ignore this
#pragma package(smart_init)
#pragma resource "*.dfm"
TForm1 *Form1;
//---------------------------------------------------------------------------
// here some global variables
int xs,ys; // window resolution
HDC hdc; // device context
HGLRC hrc; // rendering context
// 4x4 transform matrices
GLfloat mobj[16]; // object transform matrix
GLfloat meye[16]; // camera transform matrix
// key codes for controling (Arrows + Space)
WORD key_left =37;
WORD key_right=39;
WORD key_up =38;
WORD key_down =40;
WORD key_forw =32;
// key pressed state
bool _left =false;
bool _right=false;
bool _up =false;
bool _down =false;
bool _forw =false;
bool _shift=false;
// sceene need repaint?
bool _redraw=true;
//---------------------------------------------------------------------------
// here inverse matrix computation
GLfloat matrix_subdet ( GLfloat *a,int r,int s)
{
GLfloat c,q[9];
int i,j,k;
k=0; // q = sub matrix
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; // add signum
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;
}
//---------------------------------------------------------------------------
// here OpenGL stuff
//---------------------------------------------------------------------------
int TForm1::ogl_init()
{
// just init OpenGL
if (ogl_inicialized) return 1;
hdc = GetDC(Form1->Handle); // get device context
PIXELFORMATDESCRIPTOR pfd;
ZeroMemory( &pfd, sizeof( pfd ) ); // set the pixel format for the DC
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); // create current rendering context
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); // destroy rendering context
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()
{
// just exit from OpneGL
if (!ogl_inicialized) return;
wglMakeCurrent(NULL, NULL); // release current rendering context
wglDeleteContext(hrc); // destroy rendering context
ogl_inicialized=0;
}
//---------------------------------------------------------------------------
void TForm1::ogl_draw()
{
// rendering routine
_redraw=false;
// here the whole rendering
glClearColor(0.0f, 0.0f, 0.0f, 1.0f); // background color
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
GLfloat ieye[16]; // inverse camera transform matrix
matrix_inv(ieye,meye);
glMatrixMode(GL_MODELVIEW);
glLoadMatrixf(ieye);
glMultMatrixf(mobj);
// render player controlable object
// centered by (0,0,0)
// +z forward, +x right, +y up
float x=0.5,y=0.1,z=0.7; // half sizes of object
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();
// render x,y,z axises as r,g,b lines
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; // Prevent a divide by zero
if (ys<=0) ys = 1;
if (!ogl_inicialized) return;
glViewport(0,0,xs,ys); // Set Viewport to window dimensions
glMatrixMode(GL_PROJECTION); // use projection matrix
glLoadIdentity(); // set it to unit matrix
gluPerspective(30,float(xs)/float(ys),0.1,100.0); // perspective projection 30 degrees FOV and 0.1 focal length view depth 100-0.1
glMatrixMode(GL_TEXTURE); // use texture matrix
glLoadIdentity(); // set it to unit matrix
glMatrixMode(GL_MODELVIEW); // use modelview marix
glLoadIdentity(); // set it to unit matrix
}
//---------------------------------------------------------------------------
// here window stuff
//---------------------------------------------------------------------------
// window constructor
__fastcall TForm1::TForm1(TComponent* Owner):TForm(Owner)
{
ogl_inicialized=0;
hdc=NULL;
hrc=NULL;
ogl_init();
// init matrices
glMatrixMode(GL_MODELVIEW);
// object is at (0,0,0) rotatet so Z+ is pointing to screen
glLoadIdentity();
glRotatef(180.0,0.0,1.0,0.0);
glGetFloatv(GL_MODELVIEW_MATRIX,mobj);
// camera is behind object looking at object
glLoadIdentity();
glTranslatef(0.0,0.0,+20.0);
glGetFloatv(GL_MODELVIEW_MATRIX,meye);
}
//---------------------------------------------------------------------------
// window destructor
void __fastcall TForm1::FormDestroy(TObject *Sender)
{
ogl_exit();
}
//---------------------------------------------------------------------------
// common window events
//---------------------------------------------------------------------------
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)
{
// here movement and repaint timer handler (I have 20ms interval)
GLfloat da=5.0; // angular turn speed in [deg/timer_iteration]
GLfloat dp=0.1; // movement speed in [world_units/timer_iteration]
glMatrixMode(GL_MODELVIEW);
glPushMatrix();
if (_shift) // if Shift pressed control camera
{
// copy meye to GL
glLoadMatrixf(meye);
// handle keyboard with GL functions
if (_left ) { _redraw=true; glRotatef(+da,0.0,1.0,0.0); }
if (_right) { _redraw