如何在 3D 世界中正确旋转相机?

How do I rotate my camera correctly in my 3D world?

我正在尝试学习 3d 编程,目前正在为一个爱好项目开发 FPS 相机风格。 我已经创建了我认为我应该使用的那些矩阵,但我无法理解如何将所有内容连接到相机旋转。 所以,我有一个相机 Class 有:

get_World_To_View矩阵

    mat4f rotMatrix = mat4f::rotation(-rotation.z, -rotation.x, -rotation.y);
    rotMatrix.transpose();
    return rotMatrix * mat4f::translation(-position);

get_ViewToWorld矩阵

return mat4f::translation(position) * mat4f::rotation(-rotation.z, -rotation.x, -rotation.y);

get_ProjectionMatrix:

return mat4f::projection(vfov, aspect, zNear, zFar);

vector3 用于 get_forward

mat4f ViewToWorld = mat4f::translation(position) * mat4f::rotation(-rotation.z, -rotation.x, - 
rotation.y);
vec4f forward = ViewToWorld * vec4f(0, 0, -1, 0);
return forward.xyz();

和get_rightwards:

mat4f ViewToWorld = mat4f::translation(position) * mat4f::rotation(-rotation.z, -rotation.x, - 
rotation.y);
vec4f rightways = ViewToWorld * vec4f(-1, 0, 0, 0);
return rightways.xyz();

从这里开始,我认为需要一个实际旋转我的相机的功能,但我已经尝试了几件事,但我真的不明白应该如何将它拼凑在一起。

我渲染了我的两个矩阵:get_WorldToView 和 get_ProjectionMatrix 并且我可以使用 WASD 键四处移动。 有没有人告诉我应该如何考虑我的 RotateCamera() 函数?我是否遗漏了一些非常重要的东西? 我对编程很陌生,我仍然很难 "seeing" 我面前的逻辑。

所以尽可能清楚: 我在 Main.cpp(更新)中有一个函数用于输入,它的工作方式类似于。

If(mousedeltaX != 0.0f || mousedeltaY != 0.0f)
{
   // Call a function that rotate the camera.
}

就是这个函数,我需要一些关于如何思考的帮助。

当我使用 WASD 键移动时,我只是调用一个函数 Move(),它将位置 += 设置为具有正确 x、y、z 方向乘以 camera_velocity 的 vector3与旋转本身无关。

我想演示如何通过对相机的 4×4 矩阵进行连续更改来简单地实现相机运动。

因此相机矩阵是视图矩阵的逆矩阵。相机矩阵表示相机相对于世界原点的坐标(位置、方向),而视图矩阵表示相反的情况——世界相对于相机原点的位置。当 3d 内容映射到屏幕时,后者是渲染所需的转换。然而,人类(没有以自我为中心的干扰)习惯于看待自己与世界的关系。因此,我认为相机矩阵的操作更直观。

左侧的 3d 视图显示的是第一人称视角,右侧是俯视图,其中第一人称视角的 position/orientation 由红色三角形标记。

相机矩阵最初设置为单位矩阵,在 y 方向上有一个小仰角,从地面 - x-z 平面上方出现。

  • x轴指向右边。
  • y 轴指向上方。
  • z 轴指向屏幕外。

因此,视线矢量是负 z 轴。

因此,可以在平移中添加负 z 值来实现前进。

相机向上矢量是 y 轴。

因此,绕y轴正转可以实现向左转,负转可以实现向右转

现在,如果相机已经转向了,向前移动如何考虑转向的视线?

诀窍是将平移应用到 z 轴,但在相机的局部坐标系中。

用矩阵做这个,你只需要正确的乘法顺序。

void moveObs(
  QMatrix4x4 &matCam, // camera matrix
  double v, // speed (forwards, backwards)
  double rot) // rotate (left, right)
{
  QMatrix4x4 matFwd; matFwd.translate(0, 0, -v); // moving forwards / backwards: -z is line-of-sight
  QMatrix4x4 matRot; matRot.rotate(rot, 0, 1, 0); // turning left / right: y is camera-up-vector
  matCam *= matRot * matFwd;
}

我用了 QMatrix4x4 因为这是我手边的东西。在其他 API 中应该没有什么不同,例如 glm or DirectXMath as all of them are based on the same mathematical basics.

(不过,您必须始终检查特定的 API 是否公开矩阵行优先或列优先:Matrix array order of OpenGL Vs DirectX。)

我必须承认我是 OpenGL 社区的成员,主要忽略了 Direct3D。因此,我觉得无法准备一个 MCVE in Direct3D but made one in OpenGL. I used the Qt framework,它提供了很多开箱即用的东西来使示例尽可能紧凑。 (这对于 3d 编程和 GUI 编程都不太容易,尤其是对于两者的组合。)

(完整)源代码testQOpenGLWidgetNav.cc

#include <QtWidgets>

/* This function is periodically called to move the observer
 * (aka player, aka first person camera).
 */
void moveObs(
  QMatrix4x4 &matCam, // camera matrix
  double v, // speed (forwards, backwards)
  double rot) // rotate (left, right)
{
  QMatrix4x4 matFwd; matFwd.translate(0, 0, -v); // moving forwards / backwards: -z is line-of-sight
  QMatrix4x4 matRot; matRot.rotate(rot, 0, 1, 0); // turning left / right: y is camera-up-vector
  matCam *= matRot * matFwd;
}

class OpenGLWidget: public QOpenGLWidget, public QOpenGLFunctions {

  private:
    QMatrix4x4 &_matCam, _matProj, _matView, *_pMatObs;
    QOpenGLShaderProgram *_pGLPrg;
    GLuint _coordAttr;

  public:
    OpenGLWidget(QMatrix4x4 &matCam, QMatrix4x4 *pMatObs = nullptr):
      QOpenGLWidget(),
      _matCam(matCam), _pMatObs(pMatObs), _pGLPrg(nullptr)
    { }

    QSize sizeHint() const override { return QSize(256, 256); }

  protected:
    virtual void initializeGL() override
    {
      initializeOpenGLFunctions();
      glClearColor(0.525f, 0.733f, 0.851f, 1.0f);
    }

    virtual void resizeGL(int w, int h) override
    {
      _matProj.setToIdentity();
      _matProj.perspective(45.0f, GLfloat(w) / h, 0.01f, 100.0f);
    }

    virtual void paintGL() override;

  private:
    void drawTriStrip(const GLfloat *coords, size_t nCoords, const QMatrix4x4 &mat, const QColor &color);
};

static const char *vertexShaderSource =
  "# version 330\n"
  "layout (location = 0) in vec3 coord;\n"
  "uniform mat4 mat;\n"
  "void main() {\n"
  "  gl_Position = mat * vec4(coord, 1.0);\n"
  "}\n";

static const char *fragmentShaderSource =
  "#version 330\n"
  "uniform vec4 color;\n"
  "out vec4 colorFrag;\n"
  "void main() {\n"
  "  colorFrag = color;\n"
  "}\n";

const GLfloat u = 0.5; // base unit
const GLfloat coordsGround[] = {
  -15 * u, 0, +15 * u,
  +15 * u, 0, +15 * u,
  -15 * u, 0, -15 * u,
  +15 * u, 0, -15 * u,
};
const size_t sizeCoordsGround = sizeof coordsGround / sizeof *coordsGround;
const GLfloat coordsCube[] = {
  -u, +u, +u,
  -u, -u, -u,
  -u, -u, +u,
  +u, -u, +u,
  -u, +u, +u,
  +u, +u, +u,
  +u, +u, -u,
  +u, -u, +u,
  +u, -u, -u,
  -u, -u, -u,
  +u, +u, -u,
  -u, +u, -u,
  -u, +u, +u,
  -u, -u, -u
};
const size_t sizeCoordsCube = sizeof coordsCube / sizeof *coordsCube;
const GLfloat coordsObs[] = {
  -u, 0, +u,
  +u, 0, +u,
   0, 0, -u
};
const size_t sizeCoordsObs = sizeof coordsObs / sizeof *coordsObs;

void OpenGLWidget::paintGL()
{
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
  glEnable(GL_DEPTH_TEST);
  glEnable(GL_CULL_FACE);
  _matView = _matCam.inverted();
  // create shader program if not yet done
  if (!_pGLPrg) {
    _pGLPrg = new QOpenGLShaderProgram(this);
    _pGLPrg->addShaderFromSourceCode(QOpenGLShader::Vertex,
      vertexShaderSource);
    _pGLPrg->addShaderFromSourceCode(QOpenGLShader::Fragment,
      fragmentShaderSource);
    _pGLPrg->link();
    _coordAttr = _pGLPrg->attributeLocation("coord");
  }
  _pGLPrg->bind();
  // render scene
  const QColor colors[] = {
    Qt::white, Qt::green, Qt::blue,
    Qt::black, Qt::darkRed, Qt::darkGreen, Qt::darkBlue,
    Qt::cyan, Qt::magenta, Qt::yellow, Qt::gray,
    Qt::darkCyan, Qt::darkMagenta, Qt::darkYellow, Qt::darkGray
  };
  QMatrix4x4 matModel;
  drawTriStrip(coordsGround, sizeCoordsGround, matModel, Qt::lightGray);
  const size_t nColors = sizeof colors / sizeof *colors;
  for (int x = -2, i = 0; x <= 2; ++x) {
    for (int z = -2; z <= 2; ++z, ++i) {
      if (!x && !z) continue;
      matModel.setToIdentity();
      matModel.translate(x * 5 * u, u, z * 5 * u);
      drawTriStrip(coordsCube, sizeCoordsCube, matModel, colors[i++ % nColors]);
    }
  }
  // draw cam
  if (_pMatObs) drawTriStrip(coordsObs, sizeCoordsObs, *_pMatObs, Qt::red);
  // done
  _pGLPrg->release();
}

void OpenGLWidget::drawTriStrip(const GLfloat *coords, size_t sizeCoords, const QMatrix4x4 &matModel, const QColor &color)
{
  _pGLPrg->setUniformValue("mat", _matProj * _matView * matModel);
  _pGLPrg->setUniformValue("color",
    QVector4D(color.redF(), color.greenF(), color.blueF(), 1.0));
  const size_t nVtcs = sizeCoords / 3;
  glVertexAttribPointer(_coordAttr, 3, GL_FLOAT, GL_FALSE, 0, coords);
  glEnableVertexAttribArray(0);
  glDrawArrays(GL_TRIANGLE_STRIP, 0, nVtcs);
  glDisableVertexAttribArray(0);
}

struct ToolButton: QToolButton {
  ToolButton(const char *text): QToolButton()
  {
    setText(QString::fromUtf8(text));
    setCheckable(true);
    QFont qFont = font();
    qFont.setPointSize(2 * qFont.pointSize());
    setFont(qFont);
  }
};

struct MatrixView: QGridLayout {
  QLabel qLbls[4][4];
  MatrixView();
  void setText(const QMatrix4x4 &mat);
};

MatrixView::MatrixView()
{
  QColor colors[4] = { Qt::red, Qt::darkGreen, Qt::blue, Qt::black };
  for (int j = 0; j < 4; ++j) {
    for (int i = 0; i < 4; ++i) {
      QLabel &qLbl = qLbls[i][j];
      qLbl.setAlignment(Qt::AlignCenter);
      if (i < 3) {
        QPalette qPalette = qLbl.palette();
        qPalette.setColor(QPalette::WindowText, colors[j]);
        qLbl.setPalette(qPalette);
      }
      addWidget(&qLbl, i, j, Qt::AlignCenter);
    }
  }
}

void MatrixView::setText(const QMatrix4x4 &mat)
{
  for (int j = 0; j < 4; ++j) {
    for (int i = 0; i < 4; ++i) {
      qLbls[i][j].setText(QString().number(mat.row(i)[j], 'f', 3));
    }
  }
}

const char *const Up = "261", *const Down = "263";
const char *const Left = "266", *const Right = "267";

int main(int argc, char **argv)
{
  qDebug() << "Qt Version:" << QT_VERSION_STR;
  QApplication app(argc, argv);
  // setup GUI
  QWidget qWinMain;
  QHBoxLayout qHBox;
  QMatrix4x4 matCamObs; // position/orientation of observer
  matCamObs.setToIdentity();
  matCamObs.translate(0, 0.7, 0);
  OpenGLWidget qGLViewObs(matCamObs); // observer view
  qHBox.addWidget(&qGLViewObs, 1);
  QVBoxLayout qVBox;
  QGridLayout qGrid;
  ToolButton qBtnUp(Up), qBtnLeft(Left), qBtnDown(Down), qBtnRight(Right);
  qGrid.addWidget(&qBtnUp, 0, 1);
  qGrid.addWidget(&qBtnLeft, 1, 0);
  qGrid.addWidget(&qBtnDown, 1, 1);
  qGrid.addWidget(&qBtnRight, 1, 2);
  qVBox.addLayout(&qGrid);
  qVBox.addWidget(new QLabel(), 1); // spacer
  qVBox.addWidget(new QLabel("<b>Camera Matrix:</b>"));
  MatrixView qMatView;
  qMatView.setText(matCamObs);
  qVBox.addLayout(&qMatView);
  QMatrix4x4 matCamMap; // position/orientation of "god" cam.
  matCamMap.setToIdentity();
  matCamMap.translate(0, 15, 0);
  matCamMap.rotate(-90, 1, 0, 0);
  OpenGLWidget qGLViewMap(matCamMap, &matCamObs); // overview
  qVBox.addWidget(&qGLViewMap);
  qHBox.addLayout(&qVBox);
  qWinMain.setLayout(&qHBox);
  qWinMain.show();
  qWinMain.resize(720, 400);
  // setup animation
  const double v = 0.5, rot = 15.0; // linear speed, rot. speed
  const double dt = 0.05; // target 20 fps
  QTimer qTimer;
  qTimer.setInterval(dt * 1000 /* ms */);
  QObject::connect(&qTimer, &QTimer::timeout,
    [&]() {
      // fwd and turn are "tristate" vars. with value 0, -1, or +1
      const int fwd = (int)qBtnUp.isChecked() - (int)qBtnDown.isChecked();
      const int turn = (int)qBtnLeft.isChecked() - (int)qBtnRight.isChecked();
      moveObs(matCamObs, v * dt * fwd, rot * dt * turn);
      qGLViewObs.update(); qGLViewMap.update(); qMatView.setText(matCamObs);
    });
  qTimer.start();
  // runtime loop
  return app.exec();
}

和我准备 VisualStudio 解决方案的 CMakeLists.txt

project(QOpenGLWidgetNav)

cmake_minimum_required(VERSION 3.10.0)

set_property(GLOBAL PROPERTY USE_FOLDERS ON)
#set(CMAKE_CXX_STANDARD 17)
#set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

find_package(Qt5Widgets CONFIG REQUIRED)

include_directories("${CMAKE_SOURCE_DIR}")

add_executable(testQOpenGLWidgetNav
  testQOpenGLWidgetNav.cc)

target_link_libraries(testQOpenGLWidgetNav
  Qt5::Widgets)

演示输出: