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.
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
"
"layout (location = 0) in vec3 coord;
"
"uniform mat4 mat;
"
"void main() {
"
" gl_Position = mat * vec4(coord, 1.0);
"
"}
";
static const char *fragmentShaderSource =
"#version 330
"
"uniform vec4 color;
"
"out vec4 colorFrag;
"
"void main() {
"
" colorFrag = color;
"
"}
";
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 = "342206221", *const Down = "342206223";
const char *const Left = "342206266", *const Right = "342206267";
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 R