studio programacion para móviles libro edición desarrollo desarrollar curso aprende aplicaciones c++ math opengl 3d sdl
https://dl.dropbox.com/u/24832466/Downloads/debug.zip

c++ - programacion - ¿Cómo puedo mover la cámara correctamente en el espacio 3D?



manual de programacion android pdf (1)

Lo que quiero hacer:

Estoy intentando descubrir cómo hacer que la cámara funcione así:

  • Movimiento del mouse: la cámara gira
  • Tecla Arriba / Abajo: la cámara se mueve hacia adelante / hacia atrás; hacia adelante significa la dirección hacia la que se enfrenta la cámara
  • Tecla izquierda / derecha: la cámara se mueve hacia los lados
  • Tecla Q / E: la cámara se mueve hacia arriba y hacia abajo

Como tengo un montón de código, haré todo lo posible para tratar de explicar cómo lo hice, sin demasiado código. El proyecto en el que estoy trabajando es muy grande y tiene una biblioteca bastante grande con muchas clases y tipos que dificultarían su comprensión.

El problema

Casi he logrado que funcione, pero después de moverme un poco, en algunos ángulos, las cosas empiezan a fallar: al presionar Arriba, la cámara se mueve hacia los lados, y así sucesivamente.

El algoritmo en el que pensé se explica en detalle a continuación.

La pregunta es, ¿estoy haciendo las cosas mal? ¿Qué podría hacer que falle? Intenté depurar esta cámara durante todo el día y no he descubierto qué es lo que hace que falle.

Aclaraciones

  • Así es como entendí la rotación : un vector 3D (quizás llamado incorrectamente vector), donde cada componente significa el eje alrededor del cual gira el objeto. Por ejemplo, el valor X sería cuánto gira el objeto alrededor del eje X. Como estoy trabajando en OpenGL, los valores de rotación estarán en grados (no radianes).

  • Cuando renderizo la cámara, simplemente traduzco la posición de la cámara, pero con el signo opuesto.

Lo mismo aplica para la rotación:

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

Lo que probé (y no funcionó):

Intenté utilizar la geometría simple y las matemáticas, usando el teorema de Pitágoras y la trigonometría simple, pero falló miserablemente, así que dejé de intentar que esto funcionara. (Por ejemplo, NaN resultado si cualquiera de las coordenadas de rotación fue 0).

Lo que probé (y funcionó ... casi):

Usando matrices de transformación.

Cuando el usuario presiona cualquiera de esas teclas, se genera un vector 3d:

+X = right; -X = left +Y = top; -Y = bottom +Z = backward (towards camera); -Z = forward (away from camera)

Luego, genero una matriz de transformación: la identidad (matriz 4x4) se multiplica por la matriz de rotación 3 veces, para cada una de las 3 coordenadas (X luego Y y luego Z). Luego, aplico la matriz al vector que creé, y agrego el resultado a la posición anterior de la cámara.

Sin embargo, parece haber un problema con este enfoque. Al principio funciona bien, pero después de un rato, cuando presiono Arriba, va hacia los lados en lugar de hacerlo como debería.

Código real

Como dije antes, traté de usar el menor código posible. Sin embargo, si esto no es lo suficientemente útil, aquí hay un código real. Hice todo lo posible para seleccionar solo el código más relevante.

// ... 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''s 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 = m.M[0][0] * v.X + m.M[0][1] * v.Y + m.M[0][2] * v.Z + m.M[0][3]; res.Y = m.M[1][0] * v.X + m.M[1][1] * v.Y + m.M[1][2] * v.Z + m.M[1][3]; res.Z = m.M[2][0] * v.X + m.M[2][1] * v.Y + m.M[2][2] * v.Z + m.M[2][3]; return res; } // Vector3D instead of x, y and z inline Matrix<float> RotationMatrix (float angle, Vector3D<float> v) { return RotationMatrix (angle, v.X, v.Y, v.Z); } inline Matrix<float> TranslationMatrix (Vector3D<float> v) { return TranslationMatrix (v.X, v.Y, v.Z); } inline Matrix<float> ScaleMatrix (Vector3D<float> v) { return ScaleMatrix (v.X, v.Y, v.Z); } // 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(); }

Como probablemente pueda ver, es muy difícil para mí crear un ejemplo fácil, porque hay tantas cosas sucediendo detrás, y tantas clases y tipos de datos.

Otras cosas de bonificación

También subí un ejecutable (con suerte funciona), para que pueda ver de qué problema estoy hablando:

https://dl.dropbox.com/u/24832466/Downloads/debug.zip


Creo que esto tiene que ver con un poco de confusión entre la "matriz de la cámara" (posición espacial mundial de la cámara), y su matriz inversa es la "matriz de visión" (matriz que se convierte del espacio mundial para ver el espacio).

Primero, un poco de fondo.

Estás comenzando con una posición espacial mundial de la cámara, y es rotación X, Y y Z. Si esta cámara fuera solo un objeto típico que estábamos colocando en la escena, lo configuraríamos así:

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

En conjunto, estas operaciones crean la matriz que definiré como "CameraToWorldMatrix", o "la matriz que se transforma del espacio de la cámara al espacio mundial".

Sin embargo, cuando se trata de matrices de visualización, no queremos transformarnos de espacio de cámara a espacio mundial. Para la matriz de vista, queremos transformar las coordenadas del espacio mundial al espacio de la cámara (la operación inversa). Entonces nuestra matriz de vistas es realmente una "Matriz de WorldToCamera".

La forma en que tome el "inverso" de "CameraToWorldMatrix" sería realizar todas las operaciones en el orden inverso (que estuvo cerca de hacer, pero hizo que el orden se mezcle un poco).

La inversa de la matriz anterior sería:

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

Que es casi lo que tenía, pero tenía el orden mezclado.

En tu código aquí:

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

Estabas definiendo la "CameraToWorldMatrix" como "primero rota alrededor de X, luego Y, luego Z, luego traduce".

Sin embargo, cuando inviertes esto, obtienes algo diferente de lo que estabas usando como tu "WorldToCameraMatrix", que era (traducir, luego girar alrededor de z, luego girar alrededor de y, luego girar alrededor de x).

Debido a que la matriz de vistas y la matriz de la cámara no definían realmente lo mismo, se desincronizan y obtienes un comportamiento extraño.