What I want to do:
I am trying to figure out how to make the camera work like this:
- Mouse movement: the camera rotates.
- Up / Down key: the camera moves forward / backward; forward means the direction the camera is in.
- Left / right key: the camera moves sideways
- Q / E key: camera moves up and down
Since I have a lot of code, I will do my best to try to explain how I did it, without much code. The project I'm working on is very large and has a fairly large library with many classes and types that would make it difficult to understand.
Problem
I was able to almost get this work, but after a slight movement at some angles, everything starts to fail: when you press Up, the camera moves sideways, etc.
The following describes the algorithm in more detail.
The question is, am I doing something wrong? What could make him fail? I tried to debug this camera all day and didn’t understand what makes it fail.
Explanation
This is how I understood rotation: a three-dimensional vector (possibly an incorrectly named vector), where each component means the axis around which the object rotates. For example, the X value will be how much the object rotates around the X axis. Since I work in OpenGL, the rotation values ​​will be in degrees (not radians).
When rendering a camera, I simply translate the position of the camera, but with the opposite sign.
The same applies to rotation:
glRotatef(-currentCamera->Rotation().X, 1.0f, 0, 0); glRotatef(-currentCamera->Rotation().Y, 0, 1.0f, 0); glRotatef(-currentCamera->Rotation().Z, 0, 0, 1.0f); glTranslatef(-currentCamera->Position().X, -currentCamera->Position().Y, -currentCamera->Position().Z);
What I tried (and did not work):
I tried using simple geometry and mathematics using the Pythagorean theorem and simple trigonometry, but it failed, so I stopped trying to get this work done. (for example, the result of NaN if any of the rotation coordinates is 0).
What I tried (and worked almost):
Using transformation matrices.
When the user presses any of these keys, a 3D vector is generated:
+X = right; -X = left +Y = top; -Y = bottom +Z = backward (towards camera); -Z = forward (away from camera)
Then I will generate the transformation matrix: the identity (4x4 matrix) is multiplied by the rotation matrix 3 times, for each of the three coordinates (X, then Y, then Z). Then I apply the matrix to the vector I created, and add the result to the old camera position.
However, there seems to be a problem with this approach. At first it works just fine, but after a while, when I press Up, it goes sideways, and not as it should be.
Actual code
As I said above, I tried to use as little code as possible. However, if this is not useful enough, here is some actual code. I did my best to select only the most suitable code.
// ... Many headers // 'Camera' is a class, which, among other things, it has (things relevant here): // * Position() getter, SetPosition() setter // * Rotation() getter, SetRotation() setter // The position and rotation are stored in another class (template), 'Vector3D <typename T>', // which has X, Y and Z values. It also implements a '+' operator. float angle; // this is for animating our little cubes Camera* currentCamera; // 'Matrix' is a template, which contains a 4x4 array of a generic type, which is public and // called M. It also implements addition/subtraction operators, and multiplication. The // constructor memset the array to 0. // Generates a matrix with 1.0 on the main diagonal Matrix<float> IdentityMatrix() { Matrix<float> res; for (int i = 0; i < 4; i++) res.M[i][i] = 1.0f; return res; } // I used the OpenGL documentation about glRotate() to write this Matrix<float> RotationMatrix (float angle, float x, float y, float z) { Matrix<float> res; // Normalize; x, y and z must be smaller than 1 if (abs(x) > 1 || abs(y) > 1 || abs(z) > 1) { // My own implementation of max which allows 3 parameters float M = Math::Max(abs(x), abs(y), abs(z)); x /= M; y /= M; z /= M; } // Vars float s = Math::SinD(angle); // SinD and CosD convert the angle to degrees float c = Math::CosD(angle); // before calling the standard library sin and cos // Vector res.M[0][0] = x * x * (1 - c) + c; res.M[0][1] = x * y * (1 - c) - z * s; res.M[0][2] = x * z * (1 - c) + y * s; res.M[1][0] = y * x * (1 - c) + z * s; res.M[1][1] = y * y * (1 - c) + c; res.M[1][2] = y * z * (1 - c) - x * s; res.M[2][0] = x * z * (1 - c) - y * s; res.M[2][1] = y * z * (1 - c) + x * s; res.M[2][2] = z * z * (1 - c) + c; res.M[3][3] = 1.0f; return res; } // Used wikipedia for this one :) Matrix<float> TranslationMatrix (float x, float y, float z) { Matrix<float> res = IdentityMatrix(); res.M[0][3] = x; res.M[1][3] = y; res.M[2][3] = z; return res; } Vector3D<float> ApplyMatrix (Vector3D<float> v, const Matrix<float>& m) { Vector3D<float> res; res.X = mM[0][0] * vX + mM[0][1] * vY + mM[0][2] * vZ + mM[0][3]; res.Y = mM[1][0] * vX + mM[1][1] * vY + mM[1][2] * vZ + mM[1][3]; res.Z = mM[2][0] * vX + mM[2][1] * vY + mM[2][2] * vZ + mM[2][3]; return res; } // Vector3D instead of x, y and z inline Matrix<float> RotationMatrix (float angle, Vector3D<float> v) { return RotationMatrix (angle, vX, vY, vZ); } inline Matrix<float> TranslationMatrix (Vector3D<float> v) { return TranslationMatrix (vX, vY, vZ); } inline Matrix<float> ScaleMatrix (Vector3D<float> v) { return ScaleMatrix (vX, vY, vZ); } // This gets called after everything is initialized (SDL, OpenGL etc) void OnStart() { currentCamera = new Camera("camera0"); angle = 0; SDL_ShowCursor(0); // Hide cursor } // This gets called periodically void OnLogicUpdate() { float delta = .02; // How much we move Vector3D<float> rot = currentCamera->Rotation(); Vector3D<float> tr (0, 0, 0); Uint8* keys = SDL_GetKeyState(0); // Cube animation angle += 0.05; // Handle keyboard stuff if (keys[SDLK_LSHIFT] || keys[SDLK_RSHIFT]) delta = 0.1; if (keys[SDLK_LCTRL] || keys[SDLK_RCTRL]) delta = 0.008; if (keys[SDLK_UP] || keys[SDLK_w]) tr.Z += -delta; if (keys[SDLK_DOWN] || keys[SDLK_s]) tr.Z += delta; if (keys[SDLK_LEFT] || keys[SDLK_a]) tr.X += -delta; if (keys[SDLK_RIGHT] || keys[SDLK_d]) tr.X += delta; if (keys[SDLK_e]) tr.Y += -delta; if (keys[SDLK_q]) tr.Y += delta; if (tr != Vector3D<float>(0.0f, 0.0f, 0.0f)) { Math::Matrix<float> r = Math::IdentityMatrix(); r *= Math::RotationMatrix(rot.X, 1.0f, 0, 0); r *= Math::RotationMatrix(rot.Y, 0, 1.0f, 0); r *= Math::RotationMatrix(rot.Z, 0, 0, 1.0f); Vector3D<float> new_pos = Math::ApplyMatrix(tr, r); currentCamera->SetPosition(currentCamera->Position() + new_pos); } } // Event handler, handles mouse movement and ESCAPE exit void OnEvent(SDL_Event* e) { const float factor = -.1f; if (e->type == SDL_MOUSEMOTION) { // Is mouse in the center? If it is, we just moved it there, ignore if (e->motion.x == surface->w / 2 && e->motion.y == surface->h / 2) return; // Get delta float dx = e->motion.xrel; float dy = e->motion.yrel; // Make change currentCamera->SetRotation(currentCamera->Rotation() + World::Vector3D<float>(dy * factor, dx * factor, 0)); // Move back to center SDL_WarpMouse(surface->w / 2, surface->h / 2); } else if (e->type == SDL_KEYUP) switch (e->key.keysym.sym) { case SDLK_ESCAPE: Debug::Log("Escape key pressed, will exit."); StopMainLoop(); // This tells the main loop to stop break; default: break; } } // Draws a cube in 'origin', and rotated at angle 'angl' void DrawCube (World::Vector3D<float> origin, float angl) { glPushMatrix(); glTranslatef(origin.X, origin.Y, origin.Z); glRotatef(angl, 0.5f, 0.2f, 0.1f); glBegin(GL_QUADS); glColor3f(0.0f,1.0f,0.0f); // green glVertex3f( 1.0f, 1.0f,-1.0f); // Top Right Of The Quad (Top) glVertex3f(-1.0f, 1.0f,-1.0f); // Top Left Of The Quad (Top) glVertex3f(-1.0f, 1.0f, 1.0f); // Bottom Left Of The Quad (Top) glVertex3f( 1.0f, 1.0f, 1.0f); // Bottom Right Of The Quad (Top) glColor3f(1.0f,0.5f,0.0f); // orange glVertex3f( 1.0f,-1.0f, 1.0f); // Top Right Of The Quad (Bottom) glVertex3f(-1.0f,-1.0f, 1.0f); // Top Left Of The Quad (Bottom) glVertex3f(-1.0f,-1.0f,-1.0f); // Bottom Left Of The Quad (Bottom) glVertex3f( 1.0f,-1.0f,-1.0f); // Bottom Right Of The Quad (Bottom) glColor3f(1.0f,0.0f,0.0f); // red glVertex3f( 1.0f, 1.0f, 1.0f); // Top Right Of The Quad (Front) glVertex3f(-1.0f, 1.0f, 1.0f); // Top Left Of The Quad (Front) glVertex3f(-1.0f,-1.0f, 1.0f); // Bottom Left Of The Quad (Front) glVertex3f( 1.0f,-1.0f, 1.0f); // Bottom Right Of The Quad (Front) glColor3f(1.0f,1.0f,0.0f); // yellow glVertex3f( 1.0f,-1.0f,-1.0f); // Bottom Left Of The Quad (Back) glVertex3f(-1.0f,-1.0f,-1.0f); // Bottom Right Of The Quad (Back) glVertex3f(-1.0f, 1.0f,-1.0f); // Top Right Of The Quad (Back) glVertex3f( 1.0f, 1.0f,-1.0f); // Top Left Of The Quad (Back) glColor3f(0.0f,0.0f,1.0f); // blue glVertex3f(-1.0f, 1.0f, 1.0f); // Top Right Of The Quad (Left) glVertex3f(-1.0f, 1.0f,-1.0f); // Top Left Of The Quad (Left) glVertex3f(-1.0f,-1.0f,-1.0f); // Bottom Left Of The Quad (Left) glVertex3f(-1.0f,-1.0f, 1.0f); // Bottom Right Of The Quad (Left) glColor3f(1.0f,0.0f,1.0f); // violet glVertex3f( 1.0f, 1.0f,-1.0f); // Top Right Of The Quad (Right) glVertex3f( 1.0f, 1.0f, 1.0f); // Top Left Of The Quad (Right) glVertex3f( 1.0f,-1.0f, 1.0f); // Bottom Left Of The Quad (Right) glVertex3f( 1.0f,-1.0f,-1.0f); // Bottom Right Of The Quad (Right) glEnd(); glPopMatrix(); } // Gets called periodically void OnRender() { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glLoadIdentity(); // Camera movement glRotatef(-currentCamera->Rotation().X, 1.0f, 0, 0); glRotatef(-currentCamera->Rotation().Y, 0, 1.0f, 0); glRotatef(-currentCamera->Rotation().Z, 0, 0, 1.0f); glTranslatef(-currentCamera->Position().X, -currentCamera->Position().Y, -currentCamera->Position().Z); // Draw some cubes for (float i = -5; i <= 5; i++) for (float j = -5; j <= 5; j++) { DrawCube(World::Vector3D<float>(i*3, j * 3, -5), angle + 5 * i + 5 * j); } SDL_GL_SwapBuffers(); }
As you probably see, it is very difficult for me to create a simple example, because so many things are happening, and so many classes and data types.
Other bonuses
I also downloaded the executable (I hope it works) so you can see what I'm talking about:
https://dl.dropbox.com/u/24832466/Downloads/debug.zip