Я хочу продемонстрировать, как можно просто добиться движения камеры, применяя непрерывные изменения матрицы 4 × 4 для камеры.
Таким образом, матрица камеры является обратной к матрице вида. В то время как матрица камеры представляет координаты (положение, ориентацию) камеры относительно источника мира, матрица представления представляет противоположность - положение мира относительно источника камеры. Последнее является необходимым преобразованием для рендеринга, когда 3d-содержимое отображается на экране. Однако люди (без эгоцентрических нарушений) привыкли видеть себя по отношению к миру. Поэтому я считаю, что манипулирование матрицей камеры более интуитивно понятно.
Левый трехмерный вид показывает камеру от первого лица, правый - вид сверху где положение / ориентация камеры от первого лица отмечены красным треугольником.
Матрица камеры изначально настроена на единичную матрицу с небольшим возвышением в направлении y, чтобы отображаться над землей - плоскостью xz.
- Ось X направлена вправо.
- Ось Y направлена вверх.
- Ось Z направлена за пределы экрана.
Таким образом, вектор линии прямой видимости является отрицательной осью Z.
Следовательно, можно двигаться вперед, добавляя отрицательные значения z к переводу.
Камера- Вектор вверх - это ось Y.
Следовательно, поворот влево можно осуществить с помощью положительного поворота вокруг оси Y, а поворот вправо с отрицательным.
Теперь, если камера имеет был повернут, как двигаться вперед, считая, что повернул прямой видимости?
T Хитрость заключается в том, чтобы применить перевод к оси 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 или DirectXMath , они не должны быть такими разными, поскольку все они основаны на одних и тех же математических основах .
(Тем не менее, вы всегда должны проверять, предоставляет ли указанный c API матричный мажор строки или мажорный столбец: Порядок матричного массива OpenGL против DirectX .)
Я должен признать, что Я являюсь членом сообщества OpenGL, в основном игнорируя Direct3D. Следовательно, я не чувствовал себя в состоянии подготовить MCVE в Direct3D, но сделал его в OpenGL. Я использовал 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 = "\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();
}
и CMakeLists.txt
, из которого я приготовил свое решение VisualStudio:
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)
Демонстрационный вывод: