1

Im triyng to learn 3d programming and Im currently working on a FPS camera-style for a hobby project. I've created those matrices I believe I should use but Im having trouble seeing how to connect everything to the camera rotation. So, I have a camera Class with:

get_World_To_View matrix

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

get_ViewToWorld matrix

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

get_ProjectionMatrix:

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

vector3 for 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();

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

From here on Im thinking that a need a function that actually rotate my camera, but I've tried several things but I cant really understand how it should be puzzeled together.

I render my two matrices: get_WorldToView and get_ProjectionMatrix and Im able to move around with the WASD keys. Does anyone have a tip for how I should think for my RotateCamera()-function? Am I missing something very important? Im quite new to programming and Im still having a hard time "seeing" the logic before me.

So to be as clear as I can: I have a function in Main.cpp (Update) for input that works like.

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

Its that function I want some help on how to think.

When I move with the WASD keys I just call a function Move() that sets the position += to the vector3 with the correct x,y,z direction multiplied by camera_velocity, so that have ofcause nothing to do with the rotation itself.

Emsko
  • 85
  • 12
  • So, you want to incorporate a rotation into your view-to-world matrix? You already have some rotation considered. Why you don't inc. or dec. that rotation depending of the mouse deltas? What's your actual problem with this? – Scheff's Cat Mar 06 '20 at 10:23
  • Yes, but I dont understand how I should use the Matrix. (Im quite new to this) To I've tried with a function like this: `{get_ViewToWorld(); rotation = position + vec3f(0, v.y, 0) + (get_rightwards() * get_Forward());}` And then called it like this: `if (g_InputHandler->GetMouseDeltaX() > 0.0f) { camera->RotateCamera({ -camera_vel * dt, 0.0f, 0.0f }); } else if (g_InputHandler->GetMouseDeltaY() > 0.0f) { camera->RotateCamera({ 0.0f, camera_vel * dt, 0.0f }); }` – Emsko Mar 06 '20 at 10:35
  • Concerning observer navigation: I usually distinguish observer movement vs. head movement. So, for observer movement, there is a main direction e.g. a vector. Observer rotation must rotate this vector as well. Head movement is an "offset" to observer rotation. It must rotate the camera as well but will not effect the motion vector. – Scheff's Cat Mar 06 '20 at 10:36
  • So, if I read your snippets right, you store observer/camera position and rotation in two triplets and compute the view matrix out of them. Right? – Scheff's Cat Mar 06 '20 at 10:38
  • And, you don't have separate head movement (as sketched above)? – Scheff's Cat Mar 06 '20 at 10:39
  • yes exactly. I can only move the camera up/down, left/right with mousemovement, but I cant get the rotation to work – Emsko Mar 06 '20 at 11:05
  • Tried this. it doesnt work but its getting there I hope: `void RotateCamera(vec3f r) { rotation = r; position = position + vec3f(r.x, r.y, r.z) * (get_rightwards() *get_Forward()); }` – Emsko Mar 06 '20 at 11:14
  • Become a little bit more familiar with the 4x4 matrix. A matrix which is built from translation and rotation only: the upper left 3x3 sub-matrix provides the rotated unit vectors. The right column is translation. So, if (say) the -z axis is the line-of-sight, this is just your vector for forward motion according to current rotation. The translation may be updated to multiply velocity with the -z axis and add this to the translation vector (the 4th column of matrix). And, some libs work with transposed matrices - so the 4th column may be the 4th row. – Scheff's Cat Mar 06 '20 at 11:21
  • Maybe, this helps: [Learn OpenGL - Camera](https://learnopengl.com/Getting-started/Camera). Please, ignore that's about OpenGL. The underlying mathematics are not that different from what's used in DirectX. ;-) – Scheff's Cat Mar 06 '20 at 11:31
  • 1
    Or this one: [First Person Camera](https://www.braynzarsoft.net/viewtutorial/q16390-19-first-person-camera) (added DirectX in my google search). ;-) – Scheff's Cat Mar 06 '20 at 11:33
  • Thank you very much, Ill look into the article right away! :-) appreciate it! – Emsko Mar 06 '20 at 11:37
  • Now my rotation is working a lot better! It works perfectly fine UNTIL I rotate it 180degrees and then all of my movement with WASD is acting as the opposite. i.e left is then right and forward is then backwards. Any idea? – Emsko Mar 06 '20 at 12:39
  • Please, don't answer your question in question. You may add a [self-answer](https://stackoverflow.com/help/self-answer) instead and even accept it (after a certain time). – Scheff's Cat Mar 07 '20 at 11:12

1 Answers1

5

I want to demonstrate how camera motion can be simply achieved applying continuous changes to the 4×4 matrix for the camera.

Thereby the camera matrix is the inverse of the view matrix. While the camera matrix represents coordinates (position, orientation) of the camera relative to world origin, the view matrix represents the opposite – the position of world relative to camera origin. The latter is a needed transformation for rendering when 3d contents is mapped to the screen. However, humans (without egocentrical disturbance) are used to see themselves in relation to world. Hence, I consider manipulation of camera matrix more intuitive.

Snapshot of testQOpenGLWidgetNav

The left 3d view shows the first-person-camera, the right a view from top where the position/orientation of first-person-camera is remarked by the red triangle.

The camera matrix is initially set to identity matrix with a small elevation into y direction to appear above from ground – the x-z plane.

  • The x-axis points to right.
  • The y-axis points up.
  • The z-axis points out of screen.

So, the line-of-sight vector is the negative z-axis.

Hence, moving forward can be achieved adding negative z-values to translation.

The camera-up vector is the y-axis.

Hence, turning to left can be achieved with a positive rotation about y-axis, turning to right with a negative.

Now, if the camera has been turned how can moving forward consider that turned line-of-sight?

The trick is to apply the translation to the z-axis but in the local coordinate system of the camera.

Doing this with matrices, you just need the correct order for multiplications.

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

I used QMatrix4x4 as this was what I had at hand. It shouldn't be that different in other APIs like glm or DirectXMath as all of them are based on the same mathematical basics.

(Though, you have always to check whether the specific API exposes the matrix row-major or column major: Matrix array order of OpenGL Vs DirectX.)

I must admit that I'm fellow of the OpenGL community, ignoring Direct3D mostly. Hence, I didn't feel able to prepare an MCVE in Direct3D but made one in OpenGL. I used the Qt framework which provides a lot of things out of the box to keep the sample as compact as possible. (That's not quite easy for 3d programming as well as for GUI programming and especially not for the combination of both.)

The (complete) source code 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 = "\342\206\221", *const Down = "\342\206\223";
const char *const Left = "\342\206\266", *const Right = "\342\206\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();
}

and the CMakeLists.txt from which I prepared my VisualStudio solution:

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)

Demo Output:

Snapshot of testQOpenGLWidgetNav (Recorded Demo)

Scheff's Cat
  • 19,528
  • 6
  • 28
  • 56
  • That was very helpful, thank you! Its always nice to get a visual example of the logic. Nice job! It was the trick with the z-axis yes. So now i used my Get_Forward() with the translation on the z-axis, in my Move()-function: Like this: `position += position * vec3f(0, v.y, 0) + vec3f(v.x, 0,0) * -get_rightwards() + vec3f(0,0, v.z) * -get_Forward();` Appreciate your help! – Emsko Mar 08 '20 at 13:37