ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

【qt openGL入门笔记】FPS相机

【qt openGL入门笔记】FPS相机 前言前几篇比较随意本篇知识点接续坐标系统和摄像机按照比较规范的格式来提供FPS相机功能。效果图功能详解1.项目结构QtCameraDemo/ ├── CMakeLists.txt ├── main.cpp ├── res.qrc #资源文件 ├── mainwindow.h / mainwindow.cpp ├── GLWidget.h / GLWidget.cpp # OpenGL 渲染窗口 输入分发 └── Camera.h / Camera.cpp # 相机核心重点2.CMakeLists.txt代码中有涉及C 17的特性改下配置信息set(CMAKE_CXX_STANDARD 17)3.MainWindow主窗口负责显示相机、【交互提示框】及底部状态栏。// mainwindow.cpp #include mainwindow.h #include GLWidget.h #include QLabel #include QStatusBar MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) { //1.相机 m_glWidget new GLWidget(this); setCentralWidget(m_glWidget); //2. 左上角操作说明半透明悬浮 —— m_helpLabel new QLabel(m_glWidget); m_helpLabel-setText(QStringLiteral( 交互说明\n ──────────────────\n 【FPS 模式】\n W/A/S/D 前后左右\n Q / E 下降 / 上升\n Shift 加速 ×3\n 左键拖拽 转视角\n 滚轮 调节速度\n ──────────────────\n F 复位相机)); m_helpLabel-setStyleSheet( background: rgba(15,18,25,170); color:#e8e8e8; padding:10px; border-radius:8px; font-size:12px; font-family: Consolas, Microsoft YaHei, monospace;); m_helpLabel-adjustSize(); m_helpLabel-move(12, 12); m_helpLabel-raise(); //3.底部状态栏实时相机状态 —— m_infoLabel new QLabel(this); statusBar()-addWidget(m_infoLabel); connect(m_glWidget, GLWidget::cameraInfoChanged, m_infoLabel, QLabel::setText); resize(1280, 800); setWindowTitle(QStringLiteral(OpenGL FPS相机)); }4.3D渲染主体增加着色器前几篇是通过函数addShaderFromSourceCode加载代码字符串方式的GLSL实现对着色器的编译并把它加入程序后续要跟随.link()一起用addShaderFromSourceCode(QOpenGLShader::Vertex, string)本篇则是通过函数addShaderFromSourceFie加载代码文件方式的GLSL实现对着色器的编译并把它加入程序后续要跟随.link()一起用m_program.create(); m_program.addShaderFromSourceFile(QOpenGLShader::Vertex, :/shaders/cube.vert);//增加顶点着色器 m_program.addShaderFromSourceFile(QOpenGLShader::Fragment, :/shaders/cube.frag);//增加片段着色器 m_program.link();文件方式适合移植管理但是如果项目介意qrc资源方式加载时最好恢复成字符串的方式。这里额外说明addShader的意义是读取 GLSL 源码并提交给 OpenGL 驱动编译并非通常理解的gcc/g编译。后续跟随的link()目的是让显卡驱动维护好这个编译实例方便显卡GPU来调用它调用的契机是.bind()。设置初始光源方向m_program.bind();//激活着色器程序 //给着色器的uLightDir赋值---必须先激活着色器 m_program.setUniformValue(uLightDir, QVector3D(0.4f, 0.9f, 0.35f));//设置光源方向 m_program.release();//释放几何构建包括生成地面网格、生成中心的RGB坐标轴、生成立方体// // 几何构建网格 / 坐标轴 / 立方体poscolornormal 交错存储 // void GLWidget::setupSceneGeometry() { // // 顶点布局约定 (Vertex Layout) // // 我们自定义的顶点格式包含 9 个 float共 36 字节 // 索引 0-2: vec3 Position (空间坐标 x, y, z) // 索引 3-5: vec3 Color (顶点颜色 r, g, b) // 索引 6-8: vec3 Normal (法线向量用于光照计算) constexpr int kStride 9 * sizeof(float); // // 辅助函数创建并上传 VAO/VBO本身不写坐标它只负责把别人算好的坐标data送进 GPU // auto createVaoVbo [this](QOpenGLVertexArrayObject vao, QOpenGLBuffer vbo, const std::vectorfloat data) { // 1. 创建并绑定 VAO 记录了下面所有 vertexAttribPointer 的配置状态 vao.create(); vao.bind(); // 2. 创建、绑定 VBO 并上传顶点数据到显卡显存 vbo.create(); vbo.bind(); // data.data() 是 CPU 端的数组指针size() * sizeof(float) 是数据总大小 vbo.allocate(data.data(), static_castint(data.size() * sizeof(float))); // 3. 告诉显卡如何解析这 36 字节的数据 // layout(location 0) 对应顶点位置 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, kStride, reinterpret_castvoid*(0)); glEnableVertexAttribArray(0); // layout(location 1) 对应顶点颜色 glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, kStride, reinterpret_castvoid*(3 * sizeof(float))); glEnableVertexAttribArray(1); // layout(location 2) 对应法线 glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, kStride, reinterpret_castvoid*(6 * sizeof(float))); glEnableVertexAttribArray(2); // 4. 解绑 VBO 和 VAO (防止后续代码误修改状态) vbo.release(); vao.release(); }; // // 1. 生成地面网格 (Ground Grid) // { std::vectorfloat gridData; // 存储所有网格线的顶点数据 // 网格范围-100 到 100步长为 2 const int N 100; const int step 2; const int majorEvery 10; // 每 10 格画一条更明显的主线 // Lambda向数组中推入一条线段包含 2 个顶点共 18 个 float auto pushLine [gridData](const QVector3D p1, const QVector3D p2, const QVector3D color) { float v[] { p1.x(), p1.y(), p1.z(), color.x(), color.y(), color.z(), 0, 0, 1, p2.x(), p2.y(), p2.z(), color.x(), color.y(), color.z(), 0, 0, 1 }; gridData.insert(gridData.end(), v, v 18); }; const QVector3D minorCol(0.2f, 0.2f, 0.2f); // 细线颜色 (暗灰) const QVector3D majorCol(0.5f, 0.5f, 0.5f); // 主线颜色 (亮灰) // 循环生成平行于 X 轴 和 平行于 Z 轴的线条 for (int i -N; i N; i step) { const QVector3D col (i % majorEvery 0) ? majorCol : minorCol; pushLine(QVector3D(-N, 0, i), QVector3D(N, 0, i), col); // 画横线 pushLine(QVector3D(i, 0, -N), QVector3D(i, 0, N), col); // 画纵线 } // 计算网格线的顶点总数总 float 数 / 9 m_gridVertexCount static_castint(gridData.size()) / 9; createVaoVbo(m_gridVao, m_gridVbo, gridData); } // // 2. 生成 RGB 坐标轴 (X红, Y绿, Z蓝) // { // 手动定义 6 条线段 (每条线段 2 个顶点) // 数据格式依然是: X,Y,Z, R,G,B, Nx,Ny,Nz std::vectorfloat axesData { // X 轴 (红色) 0, 0, 0, 0.9, 0.2, 0.2, 0, 0, 1, // 起点 25, 0, 0, 0.9, 0.2, 0.2, 0, 0, 1, // 终点 // Y 轴 (绿色) 0, 0, 0, 0.2, 0.9, 0.2, 0, 0, 1, // 起点 0, 25, 0, 0.2, 0.9, 0.2, 0, 0, 1, // 终点 // Z 轴 (蓝色) 0, 0, 0, 0.2, 0.2, 0.9, 0, 0, 1, // 起点 0, 0, 25, 0.2, 0.2, 0.9, 0, 0, 1 // 终点 }; createVaoVbo(m_axesVao, m_axesVbo, axesData); } // // 3. 生成单位立方体 (Cube) // { std::vectorfloat cubeData; // 存储立方体的 36 个顶点 (6个面 * 2个三角形) // Lambda根据法线和中心点生成一个包含 2 个三角形的正方形面 auto addFace [cubeData](const QVector3D normal, const QVector3D up, const QVector3D col) { // 通过叉积计算向右的方向向量 const QVector3D right QVector3D::crossProduct(up, normal).normalized(); // 面的中心位置 (沿法线向外偏移 0.5) const QVector3D center normal * 0.5f; // 计算面的四个角点 const QVector3D corners[4] { center (-right - up) * 0.5f, center ( right - up) * 0.5f, center ( right up) * 0.5f, center (-right up) * 0.5f }; // 三角形绘制索引顺序 (逆时针 CCW) const int indices[6] { 0, 1, 2, 0, 2, 3 }; // 将两个三角形共 6 个顶点的数据压入数组 for (int idx : indices) { float v[] { corners[idx].x(), corners[idx].y(), corners[idx].z(), col.x(), col.y(), col.z(), normal.x(), normal.y(), normal.z() }; cubeData.insert(cubeData.end(), v, v 9); } }; // 定义立方体的统一颜色 const QVector3D col(0.85f, 0.85f, 0.88f); // 依次生成立方体的 6 个面 addFace(QVector3D( 0, 0, 1), QVector3D(0, 1, 0), col); // 正面 addFace(QVector3D( 0, 0, -1), QVector3D(0, 1, 0), col); // 背面 addFace(QVector3D( 1, 0, 0), QVector3D(0, 1, 0), col); // 右面 addFace(QVector3D(-1, 0, 0), QVector3D(0, 1, 0), col); // 左面 addFace(QVector3D( 0, 1, 0), QVector3D(0, 0, 1), col); // 顶面 addFace(QVector3D( 0, -1, 0), QVector3D(0, 0, 1), col); // 底面 createVaoVbo(m_cubeVao, m_cubeVbo, cubeData); } // // 4. 定义场景中的立方体实例 (Instancing Data) // // 我们只生成了一个单位立方体的网格数据通过 Model 矩阵把它缩放、移动到不同位置 m_cubes { // { 位置(x,y,z), 缩放大小, 旋转轴(X,Y,Z), 转速, 初始旋转角度 } { { 0.0f, 0.60f, 0.0f}, 1.2f, {0, 1, 0}, 20.0f, 0.0f }, // 中心 { { 6.0f, 1.00f, -4.0f}, 2.0f, {0, 1, 0}, 12.0f, 30.0f }, // 右侧 { {-7.0f, 0.50f, 5.0f}, 1.0f, {1, 1, 0}, 18.0f, 60.0f }, // 左侧 { { 4.0f, 0.35f, 8.0f}, 0.7f, {0, 0, 1}, 28.0f, 0.0f }, // 远处 { {-4.0f, 3.50f, -8.0f}, 1.5f, {1, 0, 1}, 9.0f, 45.0f } // 悬浮高台 }; }渲染包含相机变化渲染、地面表格按原有配置渲染、RGB坐标轴按原有配置渲染5个立方体渲染等。// // 渲染 // void GLWidget::paintGL() { // 1.清屏 glClearColor(0.10f, 0.12f, 0.16f, 1.0f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // 帧间隔 dt切后台回来防跳变 const qint64 now m_timer.elapsed(); float dt float(now - m_lastTime) / 1000.0f; m_lastTime now; dt qMin(dt, 0.1f); m_camera.setViewportSize(width(), height()); m_camera.update(dt); // FPS 键盘移动 //根据camera的实际变化进行渲染 m_program.bind(); m_program.setUniformValue(uView, m_camera.viewMatrix()); m_program.setUniformValue(uProjection, m_camera.projectionMatrix(float(width()) / qMax(1, height()))); m_program.setUniformValue(uViewPos, m_camera.position()); // —— 地面网格线不受光照—— QMatrix4x4 identity; m_program.setUniformValue(uIsLine, true); m_program.setUniformValue(uModel, identity); //网格线比较简单用vao记录的配置信息直接画即可 m_gridVao.bind(); glDrawArrays(GL_LINES, 0, m_gridVertexCount); m_gridVao.release(); // —— RGB坐标轴比较简单用vao记录的配置信息直接画即可 m_axesVao.bind(); glLineWidth(2.0f); glDrawArrays(GL_LINES, 0, 6); glLineWidth(1.0f); m_axesVao.release(); // —— 5个立方体光照—— m_program.setUniformValue(uIsLine, false); m_cubeVao.bind(); const float t float(m_timer.elapsed()) / 1000.0f; for (const auto cb : m_cubes) { QMatrix4x4 m; m.translate(cb.pos); m.rotate(cb.rotAngle0 cb.rotSpeed * t, cb.rotAxis); m.scale(cb.scale); //根据立方体的Model矩阵进行移动 m_program.setUniformValue(uModel, m); glDrawArrays(GL_TRIANGLES, 0, 36); } m_cubeVao.release(); m_program.release(); // 节流刷新状态栏10Hz 足够--发信号给主框架 if (now - m_lastInfoEmit 100) { m_lastInfoEmit now; emit cameraInfoChanged(infoString()); } }键值响应首先需要在构造函数GLWidget中设置键盘焦点和鼠标追踪然后重载相关事件setFocusPolicy(Qt::StrongFocus); // 键盘焦点 setMouseTracking(true); // 鼠标追踪protected: void mousePressEvent (QMouseEvent* e) override; void mouseMoveEvent (QMouseEvent* e) override; void mouseReleaseEvent(QMouseEvent* e) override; void wheelEvent (QWheelEvent* e) override; void keyPressEvent (QKeyEvent* e) override; void keyReleaseEvent (QKeyEvent* e) override; void focusOutEvent (QFocusEvent* e) override; // // 输入事件 → 相机纯 FPS // void GLWidget::mousePressEvent(QMouseEvent* e) { #if QT_VERSION QT_VERSION_CHECK(6, 0, 0) m_camera.onMousePressed(e-position(), e-button()); #else m_camera.onMousePressed(QPointF(e-pos()), e-button()); #endif setCursor(Qt::BlankCursor); // FPS 转视角时隐藏光标接近游戏手感 e-accept(); } void GLWidget::mouseMoveEvent(QMouseEvent* e) { #if QT_VERSION QT_VERSION_CHECK(6, 0, 0) m_camera.onMouseMoved(e-position()); #else m_camera.onMouseMoved(QPointF(e-pos())); #endif e-accept(); } void GLWidget::mouseReleaseEvent(QMouseEvent* e) { m_camera.onMouseReleased(); setCursor(Qt::ArrowCursor); e-accept(); } void GLWidget::wheelEvent(QWheelEvent* e) { m_camera.onWheelScrolled(float(e-angleDelta().y())); e-accept(); } void GLWidget::keyPressEvent(QKeyEvent* e) { if (e-isAutoRepeat()) { e-accept(); return; } switch (e-key()) { case Qt::Key_F: // 复位到默认出生机位 m_camera.reset(); break; default: // 其余按键WASD / QE / Shift交给相机做移动 m_camera.onKeyPressed(e-key()); break; } e-accept(); } void GLWidget::keyReleaseEvent(QKeyEvent* e) { if (!e-isAutoRepeat()) m_camera.onKeyReleased(e-key()); e-accept(); } void GLWidget::focusOutEvent(QFocusEvent* e) { m_camera.clearKeys(); // 防止失焦后按键卡死 QOpenGLWidget::focusOutEvent(e); }5.摄像机主体在【qt openGL入门笔记】坐标系统和摄像机-CSDN博客中已经提到过摄像机简单的理解是改变V(View) P(Projection) 的参数来实现的实例。原理上也简单Model模型矩阵负责摆在世界哪里、多大、怎么转在3D渲染主体类GLWidget中的几何构建和渲染中都含了地面网格、RGB坐标轴、立方体具体的变动效果View视图矩阵负责让眼睛相机站在哪m_position、朝哪看m_yaw / m_pitchProjection投影矩阵负责镜头的“广角/焦距”把3D压成2Dm_fov / m_nearZ / m_farZ。可以理解为VP侧重于“观察者的属性”属于VP具体的实现在Camera类中实现以下是完整代码Camera.h#pragma once #include QMatrix4x4 #include QVector3D #include QPointF #include QSet /** * 第一人称FPS相机 * - 左键拖动转视角Yaw / Pitch * - WASD 前后左右平移Q/E 升降Shift 加速 * - 滚轮调节移动速度 * - F 复位到默认出生机位 */ class Camera { public: Camera(); QMatrix4x4 viewMatrix() const; QMatrix4x4 projectionMatrix(float aspect) const; void onMousePressed(const QPointF pos, Qt::MouseButton button); void onMouseMoved(const QPointF pos); void onMouseReleased(); void onWheelScrolled(float angleDeltaY); void onKeyPressed(int key); void onKeyReleased(int key); void clearKeys(); void update(float dt); void setViewportSize(int w, int h) { m_viewportW w; m_viewportH h; } /** 配置默认出生机位相机位于 eye看向 center */ void setSpawnPose(const QVector3D eye, const QVector3D center); /** 立即摆位到默认出生机位 */ void applySpawnPose(); /** F 复位恢复默认出生机位与速度FPS 瞬间重置 */ void reset(); QVector3D position() const { return m_position; } float yaw() const { return m_yaw; } float pitch() const { return m_pitch; } float moveSpeed() const { return m_moveSpeed; } private: void updateCameraVectors(); QVector3D m_position{0, 0, 0}; QVector3D m_front{0, 0, -1}; QVector3D m_up{0, 1, 0}; QVector3D m_right{1, 0, 0}; QVector3D m_worldUp{0, 1, 0}; float m_yaw -90.0f; float m_pitch -15.0f; float m_fov 45.0f; float m_nearZ 0.1f; float m_farZ 10000.0f; float m_lookSensitivity 0.15f; float m_moveSpeed 12.0f; QSetint m_keysHeld; QPointF m_lastPos; bool m_rotating false; int m_viewportW 1280; int m_viewportH 800; // —— 默认出生机位启动 reset 的唯一数据源—— QVector3D m_spawnEye; QVector3D m_spawnCenter; static constexpr float kDefaultMoveSpeed 12.0f; };Camera.cpp#include Camera.h #include QtMath // 1. 眼睛在空间中的位置 m_position你站在哪里 // 2. 眼睛朝向哪里用两个角度描述m_yaw(偏航角左右转) 和 m_pitch(俯仰角抬头低头) // 有了位置和朝向就能算出 viewMatrix(视图矩阵)告诉 GPU 从哪个角度看世界。 Camera::Camera() { // —— 默认出生机位场景后上方悬空(0,6,18)看向场景中心(0,1,0)—— setSpawnPose(QVector3D(0.0f, 6.0f, 18.0f), QVector3D(0.0f, 1.0f, 0.0f)); // 把出生机位的坐标/角度真正写入运行状态 applySpawnPose(); // 根据刚算好的 yaw/pitch推导出 m_front/m_right/m_up 三个朝向向量 updateCameraVectors(); } // // 默认出生机位设置 / 应用 // void Camera::setSpawnPose(const QVector3D eye, const QVector3D center) { m_spawnEye eye; m_spawnCenter center; } void Camera::applySpawnPose() { m_position m_spawnEye; // 由出生视线方向反解 yaw/pitch与 updateCameraVectors 互为逆运算 const QVector3D dir (m_spawnCenter - m_position).normalized(); m_pitch qRadiansToDegrees(qAsin(qBound(-1.0f, dir.y(), 1.0f))); m_yaw qRadiansToDegrees(qAtan2(dir.z(), dir.x())); // 运行时状态一并还原 m_moveSpeed kDefaultMoveSpeed; m_rotating false; m_keysHeld.clear(); } // // F 复位恢复默认出生机位FPS 瞬间重置 // void Camera::reset() { applySpawnPose(); updateCameraVectors(); } // // 核心由 yaw/pitch 推导朝向向量pitch 正 抬头 // void Camera::updateCameraVectors() { // 把度转成弧度因为三角函数(sin/cos)只接受弧度 const float radYaw qDegreesToRadians(m_yaw); const float radPitch qDegreesToRadians(m_pitch); m_front QVector3D( qCos(radYaw) * qCos(radPitch), qSin(radPitch), qSin(radYaw) * qCos(radPitch) ).normalized(); // 右手方向 正前方 与 世界上方向(Y轴) 的叉积。 m_right QVector3D::crossProduct(m_front, m_worldUp).normalized(); // 头顶方向 右手方向 与 正前方 的叉积再次利用叉积得到第三个垂直轴 m_up QVector3D::crossProduct(m_right, m_front).normalized(); } // // 视图矩阵(View)把世界变换成从相机角度看的样子 // QMatrix4x4 Camera::viewMatrix() const { QMatrix4x4 view; view.lookAt(m_position, m_position m_front, m_worldUp); return view; } // // 投影矩阵(Projection)定义相机的镜头(透视效果) // QMatrix4x4 Camera::projectionMatrix(float aspect) const { QMatrix4x4 proj; proj.perspective(m_fov, aspect, m_nearZ, m_farZ); return proj; } // // 鼠标交互FPS左键按住拖动 转视角 // void Camera::onMousePressed(const QPointF pos, Qt::MouseButton button) { m_lastPos pos; if (button Qt::LeftButton) m_rotating true; } void Camera::onMouseMoved(const QPointF pos) { const float dx float(pos.x() - m_lastPos.x()); const float dyUp float(m_lastPos.y() - pos.y()); // 屏幕 y 向下为正取反成抬头为正 m_lastPos pos; if (m_rotating) { m_yaw dx * m_lookSensitivity; m_pitch qBound(-89.0f, m_pitch dyUp * m_lookSensitivity, 89.0f); updateCameraVectors(); } } void Camera::onMouseReleased() { m_rotating false; } // FPS滚轮调节移动速度不是缩放场景 void Camera::onWheelScrolled(float angleDeltaY) { const float steps angleDeltaY / 120.0f; m_moveSpeed * qPow(1.15f, steps); m_moveSpeed qBound(1.0f, m_moveSpeed, 300.0f); } // // 每帧更新FPS 键盘移动 // void Camera::update(float dt) { QVector3D dir; if (m_keysHeld.contains(Qt::Key_W)) dir m_front; if (m_keysHeld.contains(Qt::Key_S)) dir - m_front; if (m_keysHeld.contains(Qt::Key_D)) dir m_right; if (m_keysHeld.contains(Qt::Key_A)) dir - m_right; if (m_keysHeld.contains(Qt::Key_E)) dir m_worldUp; if (m_keysHeld.contains(Qt::Key_Q)) dir - m_worldUp; if (dir.lengthSquared() 1e-6f) { dir.normalize(); const float boost m_keysHeld.contains(Qt::Key_Shift) ? 3.0f : 1.0f; m_position dir * (m_moveSpeed * boost * dt); m_position.setY(qMax(m_position.y(), 0.5f)); // 防钻地 } } void Camera::onKeyPressed(int key) { m_keysHeld.insert(key); } void Camera::onKeyReleased(int key) { m_keysHeld.remove(key); } void Camera::clearKeys() { m_keysHeld.clear(); }
返回列表