【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坐标轴、生成立方体

复制代码
// ============================================================
// 几何构建:网格 / 坐标轴 / 立方体(pos+color+normal 交错存储)
// ============================================================
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::vector<float>& 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_cast<int>(data.size() * sizeof(float)));

        // 3. 告诉显卡如何解析这 36 字节的数据
        // layout(location = 0) 对应顶点位置
        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, kStride, reinterpret_cast<void*>(0));
        glEnableVertexAttribArray(0);

        // layout(location = 1) 对应顶点颜色
        glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, kStride, reinterpret_cast<void*>(3 * sizeof(float)));
        glEnableVertexAttribArray(1);

        // layout(location = 2) 对应法线
        glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, kStride, reinterpret_cast<void*>(6 * sizeof(float)));
        glEnableVertexAttribArray(2);

        // 4. 解绑 VBO 和 VAO (防止后续代码误修改状态)
        vbo.release();
        vao.release();
    };

    // ==========================================
    // 1. 生成地面网格 (Ground Grid)
    // ==========================================
    {
        std::vector<float> 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_cast<int>(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::vector<float> 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::vector<float> 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_pitch);
  • Projection(投影矩阵):负责镜头的"广角/焦距",把3D压成2D(m_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;
    QSet<int> 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(); }
相关推荐
扶尔魔ocy3 小时前
qt openGL入门笔记】FPS相机和轨道相机
qt·opengl
尘客-追梦4 小时前
qmake / jom / 影子构建:把编译管起来
开发语言·c++·qt
qeen874 小时前
【QT】 信号与槽初始
开发语言·笔记·qt·学习
Quz2 天前
QML PathView:吸附对齐与环形轮播
qt
时间的拾荒人2 天前
Qt 多线程详解:从 QThread 到实战
开发语言·qt·面试
时间的拾荒人2 天前
Qt 事件机制全解析:从事件处理到事件过滤器
开发语言·qt·面试
时间的拾荒人2 天前
Qt 窗口系统详解:从菜单栏到对话框的完整指南
开发语言·qt·面试
qq_401700412 天前
Qt 炫酷曲线,图表,2D/3D开源库
开发语言·qt
扶尔魔ocy2 天前
【qt openGL入门笔记】基础光照
qt·opengl