How to move the camera in three-dimensional space?

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

+6
source share
1 answer

I believe this is due to a bit of confusion between the “camera matrix” (the world’s spatial position of the camera) and the inverse matrix “presentation matrix” (the matrix that converts from world space to the viewing space).

Firstly, a little background.

You start with the position of space in the camera world and rotate X, Y and Z. If this camera was just a typical object that we placed on the stage, we would set it like this:

 glTranslate(camX, camY, camZ); glRotate(x); glRotate(y); glRotate(z); 

Together, these operations create a matrix, which I define as "CameraToWorldMatrix", or "a matrix that converts from camera space to world space."

However, when we are dealing with representation matrices, we do not want to transform from camera space to world space. For a view matrix, we want to convert coordinates from world space to camera space (reverse operation). Thus, our view matrix is ​​indeed "WorldToCameraMatrix".

The way you accept the inverse CameraToWorldMatrix should have performed all the operations in the reverse order (which you came close to doing but changed the order a bit).

The inverse to the above matrix will be:

 glRotate(-z); glRotate(-y); glRotate(-x); glTranslate(-camX, -camY, -camZ); 

This is almost what you had, but you had a mess.

In your code here:

  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); 

You defined "CameraToWorldMatrix" as "first rotate around X, then Y, then Z, and then translate."

However, when you invert this, you get something different from what you used as your "WorldToCameraMatrix", which was (translated, then rotates around z, then rotates around y, and then rotates around x).

Since your viewing matrix and camera matrix did not actually define the same thing, they do not synchronize and you get strange behavior.

+10
source

Source: https://habr.com/ru/post/923073/


All Articles