【qt openGL入门笔记】坐标系统和摄像机

前言

网上查opengl入门学习网站,介绍**LearnOpenGL**比较多,虽然不是基于qt项目,但原理是一致的,本篇基于样例把坐标系和相机的知识点合并到一个qt项目中,方便关联阅读。

一、坐标系统

对我们来说比较重要的总共有5个不同的坐标系统

空间 含义 谁负责
局部空间 模型自己的坐标 模型数据
世界空间 放到场景里的位置 Model 矩阵
观察空间 从相机视角看到的空间 View 矩阵
裁剪空间 透视/正交投影后的空间 Projection 矩阵
屏幕空间 最终窗口像素坐标 GPU 自动完成

这些坐标点,在顶点着色器中有相关的表达式,表达式如下:

表达式 得到的是什么
aPos 局部坐标
model * vec4(aPos, 1.0) 世界坐标
view * model * vec4(aPos, 1.0) 观察坐标
projection * view * model * vec4(aPos, 1.0) 裁剪坐标

比如上一篇分享的着色器、纹理、变换的顶点着色器中,就包含有以下两种坐标

局部坐标:aPos

世界坐标:uMatrix * vec4(aPos, 1.0) //uMatrix是时间空间对应的Model 矩阵的变量名称

本篇的样例中将会用到裁剪坐标来展示投影后的特性。

二、摄像机

简单的理解是改变V(View) + P(Projection) 的参数来实现的实例。本篇会提供通过键盘值改变V+P值的qt项目案例。

三、案例

3.1 核心代码

glwidget.h

复制代码
#ifndef GLWIDGET_H
#define GLWIDGET_H

#include <QOpenGLWidget>
#include <QOpenGLFunctions>
#include <QOpenGLShaderProgram>
#include <QOpenGLBuffer>
#include <QMatrix4x4>
#include <QVector3D>
#include <QTimer>
#include <QKeyEvent>
#define GLF_SIZE 6
class GLWidget : public QOpenGLWidget, protected QOpenGLFunctions
{
    Q_OBJECT
public:
    explicit GLWidget(QWidget *parent = nullptr);
    ~GLWidget();

protected:
    void initializeGL() override;
    void resizeGL(int w, int h) override;   // ★ 窗口尺寸变化时必须更新投影矩阵
    void paintGL() override;
    void keyPressEvent(QKeyEvent *event) override;  // 键盘控制相机位置

private:
    QOpenGLShaderProgram m_program;
    QOpenGLBuffer m_vbo{QOpenGLBuffer::VertexBuffer};
    QTimer *m_timer;

    // ★ 坐标系统的三大主角
    QMatrix4x4 m_model;        // 模型矩阵:物体 → 世界
    QMatrix4x4 m_view;         // 视图矩阵:世界 → 相机
    QMatrix4x4 m_projection;   // 投影矩阵:相机 → 裁剪

    float m_angle = 0.0f;      // 立方体自转角度

    // 相机参数(用于构建 view 矩阵)
    QVector3D m_cameraPos   = QVector3D(0.0f, 0.0f,  5.0f);
    QVector3D m_cameraTarget= QVector3D(0.0f, 0.0f,  0.0f);
    QVector3D m_cameraUp    = QVector3D(0.0f, 1.0f,  0.0f);

    void buildMatrices(int w, int h);  // 抽出来,resizeGL 和 paintGL 共用
};

#endif // GLWIDGET_H

glwidget.cpp

复制代码
#include "glwidget.h"

GLWidget::GLWidget(QWidget *parent)
    : QOpenGLWidget(parent)
{
    setFocusPolicy(Qt::StrongFocus);   // 获取焦距,捕获键盘事件
    m_timer = new QTimer(this);
    connect(m_timer, &QTimer::timeout, this, [=]{
        m_angle += 1.5f;
        if (m_angle >= 360.0f) m_angle -= 360.0f;
        update();   // 触发 paintGL
    });
}

GLWidget::~GLWidget() = default;

void GLWidget::initializeGL()
{
    initializeOpenGLFunctions();
    glEnable(GL_DEPTH_TEST);   // ★ 3D 必须开深度测试,否则立方体背面盖住正面

    // ---- 立方体顶点(36 个顶点 = 6 面 × 2 三角形 × 3 顶点)----
    // 只含位置和颜色,简化示例;纹理可自行扩展
    GLfloat vertices[] = {
        // ---- 位置 ----        ---- 颜色 ----
        // 前面 (z = +0.5)
        -0.5f,-0.5f, 0.5f,  1.0f,0.0f,0.0f,
         0.5f,-0.5f, 0.5f,  1.0f,0.0f,0.0f,
         0.5f, 0.5f, 0.5f,  1.0f,0.0f,0.0f,
        -0.5f,-0.5f, 0.5f,  1.0f,0.0f,0.0f,
         0.5f, 0.5f, 0.5f,  1.0f,0.0f,0.0f,
        -0.5f, 0.5f, 0.5f,  1.0f,0.0f,0.0f,
        // 后面 (z = -0.5)
        -0.5f,-0.5f,-0.5f,  0.0f,1.0f,0.0f,
         0.5f,-0.5f,-0.5f,  0.0f,1.0f,0.0f,
         0.5f, 0.5f,-0.5f,  0.0f,1.0f,0.0f,
        -0.5f,-0.5f,-0.5f,  0.0f,1.0f,0.0f,
         0.5f, 0.5f,-0.5f,  0.0f,1.0f,0.0f,
        -0.5f, 0.5f,-0.5f,  0.0f,1.0f,0.0f,
        // 上面 (y = +0.5)
        -0.5f, 0.5f,-0.5f,  0.0f,0.0f,1.0f,
         0.5f, 0.5f,-0.5f,  0.0f,0.0f,1.0f,
         0.5f, 0.5f, 0.5f,  0.0f,0.0f,1.0f,
        -0.5f, 0.5f,-0.5f,  0.0f,0.0f,1.0f,
         0.5f, 0.5f, 0.5f,  0.0f,0.0f,1.0f,
        -0.5f, 0.5f, 0.5f,  0.0f,0.0f,1.0f,
        // 下面 (y = -0.5)
        -0.5f,-0.5f,-0.5f,  1.0f,1.0f,0.0f,
         0.5f,-0.5f,-0.5f,  1.0f,1.0f,0.0f,
         0.5f,-0.5f, 0.5f,  1.0f,1.0f,0.0f,
        -0.5f,-0.5f,-0.5f,  1.0f,1.0f,0.0f,
         0.5f,-0.5f, 0.5f,  1.0f,1.0f,0.0f,
        -0.5f,-0.5f, 0.5f,  1.0f,1.0f,0.0f,
        // 右面 (x = +0.5)
         0.5f,-0.5f, 0.5f,  1.0f,0.0f,1.0f,
         0.5f,-0.5f,-0.5f,  1.0f,0.0f,1.0f,
         0.5f, 0.5f,-0.5f,  1.0f,0.0f,1.0f,
         0.5f,-0.5f, 0.5f,  1.0f,0.0f,1.0f,
         0.5f, 0.5f,-0.5f,  1.0f,0.0f,1.0f,
         0.5f, 0.5f, 0.5f,  1.0f,0.0f,1.0f,
        // 左面 (x = -0.5)
        -0.5f,-0.5f, 0.5f,  0.0f,1.0f,1.0f,
        -0.5f,-0.5f,-0.5f,  0.0f,1.0f,1.0f,
        -0.5f, 0.5f,-0.5f,  0.0f,1.0f,1.0f,
        -0.5f,-0.5f, 0.5f,  0.0f,1.0f,1.0f,
        -0.5f, 0.5f,-0.5f,  0.0f,1.0f,1.0f,
        -0.5f, 0.5f, 0.5f,  0.0f,1.0f,1.0f,
    };

    //VBO:把数据从内存搬进显存
    m_vbo.create();
    m_vbo.bind();
    m_vbo.allocate(vertices, sizeof(vertices));

    // ---- 顶点着色器:三个矩阵按顺序合并 ----
    m_program.addShaderFromSourceCode(QOpenGLShader::Vertex,
        "#version 150\n"
        "in vec3 aPos;\n"
        "in vec3 aColor;\n"
        "out vec3 vColor;\n"
        "uniform mat4 uModel;\n"        // 模型矩阵
        "uniform mat4 uView;\n"         // 视图矩阵
        "uniform mat4 uProjection;\n"   // 投影矩阵
        "void main() {\n"
        "    gl_Position = uProjection * uView * uModel * vec4(aPos, 1.0);\n"
        "    vColor = aColor;\n"
        "}"
    );

    // ---- 片段着色器:纯顶点色,聚焦矩阵本身 ----
    m_program.addShaderFromSourceCode(QOpenGLShader::Fragment,
        "#version 150\n"
        "in vec3 vColor;\n"
        "out vec4 FragColor;\n"
        "void main() {\n"
        "    FragColor = vec4(vColor, 1.0);\n"
        "}"
    );

    // 5. 编译并链接程序
    if (!m_program.link()) {
        qDebug() << "Shader Link Error:" << m_program.log();
        return;
    }

    m_timer->start(16);  // 约 60 FPS
}

// ★★★ 坐标系统最容易踩坑的地方:aspect ratio 必须在窗口变化时更新 ★★★
void GLWidget::resizeGL(int w, int h)
{
    glViewport(0, 0, w, h);
    buildMatrices(w, h);   // 重新构建投影矩阵
}

void GLWidget::buildMatrices(int w, int h)
{
    // ---- 1. 模型矩阵:把物体从"自身坐标"放到"世界坐标" ----
    m_model.setToIdentity();
    m_model.rotate(m_angle, 0.5f, 1.0f, 0.0f);  // 绕斜轴自转(好看)

    // ---- 2. 视图矩阵:把世界变换到"相机视角" ----
    m_view.setToIdentity();
    m_view.lookAt(
        m_cameraPos,      // 相机位置 (0,0,5) ------ 往后拉 5 个单位才能看到物体
        m_cameraTarget,   // 相机看向哪里(原点)
        m_cameraUp        // 上向量,定义"哪边是上"
    );

    // ---- 3. 投影矩阵:把 3D 场景投到 2D 屏幕 ----
    m_projection.setToIdentity();
    //m_projection.ortho(-2.0f, 2.0f, -1.5f, 1.5f, 0.1f, 100.0f);
    m_projection.perspective(
        45.0f,                            // fov:视野角度,越大越"广角"
        float(w) / float(h),              // ★ aspect:必须按真实宽高比,否则物体被拉伸
        0.1f,                             // near:近平面,比这更近的都裁掉
        100.0f                            // far :远平面
    );
}

void GLWidget::paintGL()
{
    // 1.清屏(深蓝色背景)
    glClearColor(0.05f, 0.05f, 0.12f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);  // ★ 深度缓冲也要清

    //2.使用着色器程序
    m_program.bind();
    // 3. 绑定顶点数据
    m_vbo.bind();

    // 使用 attributeLocation 获取位置
    int posAttr   = m_program.attributeLocation("aPos");
    int colorAttr = m_program.attributeLocation("aColor");

    // 步长 = 6 个 float(3 位置 + 3 颜色)
    m_program.setAttributeBuffer(posAttr,   GL_FLOAT, 0, 3, GLF_SIZE * sizeof(GLfloat));
    m_program.enableAttributeArray(posAttr);
    m_program.setAttributeBuffer(colorAttr, GL_FLOAT, 3 * sizeof(GLfloat), 3, GLF_SIZE * sizeof(GLfloat));
    m_program.enableAttributeArray(colorAttr);

    // ★ 每帧更新模型矩阵(自转角度在变),view/projection 只在 resizeGL/按键时更新
    m_model.setToIdentity();
    m_model.rotate(m_angle, 0.5f, 1.0f, 0.0f);

    m_program.setUniformValue("uModel",       m_model);
    m_program.setUniformValue("uView",        m_view);
    m_program.setUniformValue("uProjection",  m_projection);

    glDrawArrays(GL_TRIANGLES, 0, 36);

    m_program.disableAttributeArray(posAttr);
    m_program.disableAttributeArray(colorAttr);
    m_program.release();
}


// 键盘 WASD 控制相机位置,直观感受 view 矩阵的作用
void GLWidget::keyPressEvent(QKeyEvent *event)
{
    const float speed = 0.3f;
    switch (event->key()) {
    case Qt::Key_W: m_cameraPos.setZ(m_cameraPos.z() - speed); break;  // 前进
    case Qt::Key_S: m_cameraPos.setZ(m_cameraPos.z() + speed); break;  // 后退
    case Qt::Key_A: m_cameraPos.setX(m_cameraPos.x() - speed); break;  // 左移
    case Qt::Key_D: m_cameraPos.setX(m_cameraPos.x() + speed); break;  // 右移
    case Qt::Key_Q: m_cameraPos.setY(m_cameraPos.y() + speed); break;  // 升高
    case Qt::Key_E: m_cameraPos.setY(m_cameraPos.y() - speed); break;  // 下降
    }
    buildMatrices(width(), height());  // 相机动了,重算 view 矩阵
    update();
}

3.2 代码讲解

3.2.1 VBO

VBO中存在了立方体(6个面)的坐标和颜色属性

复制代码
GLfloat vertices[] = {
        // ---- 位置 ----        ---- 颜色 ----
        // 前面 (z = +0.5)
        -0.5f,-0.5f, 0.5f,  1.0f,0.0f,0.0f,
         0.5f,-0.5f, 0.5f,  1.0f,0.0f,0.0f,
         0.5f, 0.5f, 0.5f,  1.0f,0.0f,0.0f,
        -0.5f,-0.5f, 0.5f,  1.0f,0.0f,0.0f,
         0.5f, 0.5f, 0.5f,  1.0f,0.0f,0.0f,
        -0.5f, 0.5f, 0.5f,  1.0f,0.0f,0.0f,
        // 后面 (z = -0.5)
        -0.5f,-0.5f,-0.5f,  0.0f,1.0f,0.0f,
         0.5f,-0.5f,-0.5f,  0.0f,1.0f,0.0f,
         0.5f, 0.5f,-0.5f,  0.0f,1.0f,0.0f,
        -0.5f,-0.5f,-0.5f,  0.0f,1.0f,0.0f,
         0.5f, 0.5f,-0.5f,  0.0f,1.0f,0.0f,
        -0.5f, 0.5f,-0.5f,  0.0f,1.0f,0.0f,
        // 上面 (y = +0.5)
        -0.5f, 0.5f,-0.5f,  0.0f,0.0f,1.0f,
         0.5f, 0.5f,-0.5f,  0.0f,0.0f,1.0f,
         0.5f, 0.5f, 0.5f,  0.0f,0.0f,1.0f,
        -0.5f, 0.5f,-0.5f,  0.0f,0.0f,1.0f,
         0.5f, 0.5f, 0.5f,  0.0f,0.0f,1.0f,
        -0.5f, 0.5f, 0.5f,  0.0f,0.0f,1.0f,
        // 下面 (y = -0.5)
        -0.5f,-0.5f,-0.5f,  1.0f,1.0f,0.0f,
         0.5f,-0.5f,-0.5f,  1.0f,1.0f,0.0f,
         0.5f,-0.5f, 0.5f,  1.0f,1.0f,0.0f,
        -0.5f,-0.5f,-0.5f,  1.0f,1.0f,0.0f,
         0.5f,-0.5f, 0.5f,  1.0f,1.0f,0.0f,
        -0.5f,-0.5f, 0.5f,  1.0f,1.0f,0.0f,
        // 右面 (x = +0.5)
         0.5f,-0.5f, 0.5f,  1.0f,0.0f,1.0f,
         0.5f,-0.5f,-0.5f,  1.0f,0.0f,1.0f,
         0.5f, 0.5f,-0.5f,  1.0f,0.0f,1.0f,
         0.5f,-0.5f, 0.5f,  1.0f,0.0f,1.0f,
         0.5f, 0.5f,-0.5f,  1.0f,0.0f,1.0f,
         0.5f, 0.5f, 0.5f,  1.0f,0.0f,1.0f,
        // 左面 (x = -0.5)
        -0.5f,-0.5f, 0.5f,  0.0f,1.0f,1.0f,
        -0.5f,-0.5f,-0.5f,  0.0f,1.0f,1.0f,
        -0.5f, 0.5f,-0.5f,  0.0f,1.0f,1.0f,
        -0.5f,-0.5f, 0.5f,  0.0f,1.0f,1.0f,
        -0.5f, 0.5f,-0.5f,  0.0f,1.0f,1.0f,
        -0.5f, 0.5f, 0.5f,  0.0f,1.0f,1.0f,
    };

3.2.2 顶点着色器

顶点着色器增加了View 矩阵和Projection矩阵

复制代码
// ---- 顶点着色器:三个矩阵按顺序合并 ----
    m_program.addShaderFromSourceCode(QOpenGLShader::Vertex,
        "#version 150\n"
        "in vec3 aPos;\n"
        "in vec3 aColor;\n"
        "out vec3 vColor;\n"
        "uniform mat4 uModel;\n"        // 模型矩阵
        "uniform mat4 uView;\n"         // 视图矩阵
        "uniform mat4 uProjection;\n"   // 投影矩阵
        "void main() {\n"
        "    gl_Position = uProjection * uView * uModel * vec4(aPos, 1.0);\n"
        "    vColor = aColor;\n"
        "}"
    );

3.2.3 paintGL

在渲染函数paintGL中,也增加了View和Projection的取样

复制代码
    m_program.setUniformValue("uView",        m_view);
    m_program.setUniformValue("uProjection",  m_projection);

3.2.4 重构mvp模型

为了测试重构mvp的功能,增加了一个buildMatrices函数

复制代码
void GLWidget::buildMatrices(int w, int h)
{
    // ---- 1. 模型矩阵:把物体从"自身坐标"放到"世界坐标" ----
    m_model.setToIdentity();
    m_model.rotate(m_angle, 0.5f, 1.0f, 0.0f);  // 绕斜轴自转(好看)

    // ---- 2. 视图矩阵:把世界变换到"相机视角" ----
    m_view.setToIdentity();
    m_view.lookAt(
        m_cameraPos,      // 相机位置 (0,0,5) ------ 往后拉 5 个单位才能看到物体
        m_cameraTarget,   // 相机看向哪里(原点)
        m_cameraUp        // 上向量,定义"哪边是上"
    );

    // ---- 3. 投影矩阵:把 3D 场景投到 2D 屏幕 ----
    m_projection.setToIdentity();
    //m_projection.ortho(-2.0f, 2.0f, -1.5f, 1.5f, 0.1f, 100.0f);
    m_projection.perspective(
        45.0f,                            // fov:视野角度,越大越"广角"
        float(w) / float(h),              // ★ aspect:必须按真实宽高比,否则物体被拉伸
        0.1f,                             // near:近平面,比这更近的都裁掉
        100.0f                            // far :远平面
    );
}

当应用窗口变换,或者是捕获到指定的键盘值时触发

复制代码
void GLWidget::resizeGL(int w, int h)
{
    glViewport(0, 0, w, h);
    buildMatrices(w, h);   // 重新构建投影矩阵
}

// 键盘 WASD 控制相机位置,直观感受 view 矩阵的作用
void GLWidget::keyPressEvent(QKeyEvent *event)
{
    const float speed = 0.3f;
    switch (event->key()) {
    case Qt::Key_W: m_cameraPos.setZ(m_cameraPos.z() - speed); break;  // 前进
    case Qt::Key_S: m_cameraPos.setZ(m_cameraPos.z() + speed); break;  // 后退
    case Qt::Key_A: m_cameraPos.setX(m_cameraPos.x() - speed); break;  // 左移
    case Qt::Key_D: m_cameraPos.setX(m_cameraPos.x() + speed); break;  // 右移
    case Qt::Key_Q: m_cameraPos.setY(m_cameraPos.y() + speed); break;  // 升高
    case Qt::Key_E: m_cameraPos.setY(m_cameraPos.y() - speed); break;  // 下降
    }
    buildMatrices(width(), height());  // 相机动了,重算 view 矩阵
    update();
}

四、效果图

相关推荐
C++ 老炮儿的技术栈15 小时前
main函数之后的调用
c语言·c++·人工智能·qt·mfc·c
沫璃染墨20 小时前
《Qt从零入门系列(十二):Qt文件操作详解——从QFile读写到QFileInfo与记事本实战》
开发语言·网络·c++·qt·交互·信号处理·文件
时间的拾荒人20 小时前
Qt 界面布局与容器控件详解:从分组框到布局管理器
开发语言·qt·面试
王铭诗1 天前
QT界面安装
qt
程序员老陆1 天前
嵌入式 Qt:QWidgets 还是 QML / Qt Quick?
开发语言·qt
待什么青丝1 天前
【外设】之大彩串口显示屏
单片机·qt·显示器
Frank_refuel1 天前
深入理解RpcRouter(远端调用模块)
开发语言·qt
Quz2 天前
QML 分隔线:Rectangle、菜单与工具栏分隔线用法
qt
Quz2 天前
QML PathView 入门:直线路径与曲线路径
qt