如何在3D空间中正确移动相机?

时间:2012-08-16 20:15:53

标签: c++ math opengl 3d sdl

我想做什么:

我正在试图弄清楚如何让相机像这样工作:

  • 鼠标移动:相机旋转
  • 上/下键:相机向前/向后移动;向前表示相机朝向的方向
  • 左/右键:相机侧向移动
  • Q / E键:相机上下移动

由于我有很多代码,我会尽力解释我是如何做到的,没有太多代码。我正在处理的项目非常庞大,并且有一个非常大的库,其中包含许多类和类型,这使得它很难理解。

问题

我已经成功地完成了这项工作,但是在稍微移动一点之后,事情开始失败:当按下Up时,相机会侧向移动等等。

我想到的算法将在下面详细解释。

问题是,我做错了吗?什么可能使它失败?我整天都试着调试这台相机,但还没弄清楚是什么让它失败了。

澄清

  • 这就是我理解 rotation 的方式:一个3D矢量(可能是不正确地称为矢量),其中每个组件表示对象围绕其旋转的轴。例如,X值将是对象围绕X轴旋转的程度。因为我在OpenGL中工作,所以旋转值将是(不是弧度)。

  • 渲染相机时,我只是翻译相机位置,但符号相反。

同样适用于轮换:

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

我尝试过(并且没有用):

我尝试使用简单的几何和数学,使用毕达哥拉斯定理和简单的三角函数,但它失败了,所以我不再试图让它工作。 (例如,如果任何旋转坐标为0,则为NaN结果。)

我尝试了什么(并且几乎可以工作):

使用转换矩阵。

当用户按下任意键时,会生成一个3d矢量:

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

接下来,我生成一个变换矩阵:对于3个坐标中的每个坐标(X然后是Y,然后是Z),身份(4x4矩阵)乘以旋转矩阵3次。接下来,我将矩阵应用于我创建的矢量,然后将结果添加到摄像机的旧位置。

然而,这种方法似乎存在问题。起初它的效果很好,但过了一段时间,当我按下它时,它会侧向移动而不是应该的方式。

实际代码

正如我上面所说,我尝试使用尽可能少的代码。但是,如果这不够有用,这里有一些实际的代码。我尽力选择最相关的代码。

// ... 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();
}

正如您可能看到的,我很难创建一个简单的示例,因为背后发生了很多事情,以及如此多的类和数据类型。

其他奖金

我还上传了一个可执行文件(希望它有效),这样你就可以看到我在说什么问题了:

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

1 个答案:

答案 0 :(得分:10)

我认为这与“相机矩阵”(相机的世界空间位置)之间有点混淆,而它的逆矩阵是“视图矩阵”(从世界空间转换为视图空间的矩阵) )。

首先,一点背景。

你从相机的世界空间位置开始,它是X,Y和Z旋转。如果这台相机只是我们放置在场景中的典型物体,我们会将其设置为:

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

这些操作一起创建了我将定义为“CameraToWorldMatrix”的矩阵,或“从相机空间转换到世界空间的矩阵”。

但是,当我们处理视图矩阵时,我们不希望从相机空间转换到世界空间。对于视图矩阵,我们希望将坐标从世界空间转换为相机空间(逆操作)。所以我们的视图矩阵实际上是一个“WorldToCameraMatrix”。

你采用“CameraToWorldMatrix”的“逆”的方式是以相反的顺序执行所有操作(你接近这样做,但是命令略微混淆了)。

上述矩阵的反函数为:

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

这几乎就是你所拥有的,但你把订单搞砸了。

在您的代码中:

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

您将“CameraToWorldMatrix”定义为“首先绕X旋转,然后旋转Y,然后旋转Z,然后翻译”。

然而,当你反过来时,你得到的东西与你使用的“WorldToCameraMatrix”不同,它是(平移,然后围绕z旋转,然后围绕y旋转,然后围绕x旋转)。

因为你的视图矩阵和相机矩阵实际上没有定义相同的东西,所以它们会失去同步而你会得到奇怪的行为。