Qt+OpenGL(9)摄像机

发布时间 2023-08-30 17:47:29作者: Clemens

一、准备工作

基础知识详见:摄像机learnopengl(9)摄像机

当我们讨论摄像机/观察空间(Camera/View Space)的时候,是在讨论以摄像机的视角作为场景原点时场景中所有的顶点坐标:观察矩阵把所有的世界坐标变换为相对于摄像机位置与方向的观察坐标。要定义一个摄像机,我们需要它在世界空间中的位置、观察的方向、一个指向它右侧的向量以及一个指向它上方的向量。

1. 摄像机位置

跟上一节一样,定义摄像机位置,摄像机向后移动,z轴是从屏幕指向我们的,摄像机向后移动,即向z轴正方向移动。

 

2. 摄像机方向

摄像机方向指的是摄像机指向哪个方向,用摄像机的位置减去指向的位置便可得到摄像机的指向向量。由于我们知道摄像机指向Z轴负方向,但我们希望方向向量指向摄像机的z轴正方向,交换一下相减顺序便可得到指向摄像机z轴正方向的方向向量。

 

3. 右轴

右向量代表摄像机空间的x轴的正方向。为了获取右向量,我们通常先定义一个上向量,通常取up=(0, 1, 0),不论该up向量是否与摄像机方向向量垂直,但是该上向量up和摄像机方向向量张成的平面一定和摄像机空间的x轴垂直,因此,我们通过上向量up叉乘摄像机方向向量,便可得到摄像机空间的右向量,即x轴正向的方向向量。

 

4. 上轴

我们通过上面已经求得了摄像机方向向量Z轴向量,和右向量X向量,通过Z向量叉乘X向量,便可得到上向量,即摄像机空间的Y轴正向向量。

由此,我们便得到了摄像机空间坐标系的3个方向向量。

 

二、Look At矩阵

我们使用lookat矩阵将世界坐标变换到观察空间。我们从上面得到了3个相互垂直的抽和一个定义摄像机空间的位置坐标,我们可以用他们创建的矩阵乘以任何向量来将其变换到那个坐标空间。

其中R是右向量,U是上向量,D是方向向量,它们都是单位向量,P是摄像机在世界空间中的位置。

如何理解这个矩阵?我们暂且称乘号左边的矩阵为矩阵A,乘号右边的矩阵为矩阵B。

我们用C=A*B*V。我们反过来计算,V是A矩阵中三个相互垂直的向量中的一个。

 矩阵B是一个平移矩阵,即将摄像机位置平移至世界坐标系原点。

矩阵A是一个旋转矩阵,由于三个向量的正交性,经过矩阵B平移后并不改变方向,且R,U,D为单位向量,用矩阵A作用于R,U,D,会得到(1, 0, 0), (0, 1, 0), (0, 0, 1),它们正是世界空间坐标系的三个方向向量。

在Qt中QMatrix4x4类提供了该方法,可以方便的计算。

 

我们接下来在上一节的基础上,使摄像机在场景中绕原点(0,0,0)以10为半径不断旋转。

我们要做的就是把观察矩阵的计算放到渲染循环中去。因为之前是静止的,我们没有更新画面,所以现在我们还需要添加一个定时器,使不断update(),然后才能不断的刷新调用paintGL()方法。

    QTimer *timer = new QTimer(this);
    timer->setInterval(50);
    connect(timer, &QTimer::timeout, this, &GLWidget::timerSlot);
    timer->start();

// 
void GLWidget::timerSlot()
{
    this->update();
}

渲染循环中:

void GLWidget::paintGL()
{
    ...
    // 绑定着色器程序至当前上下文,相当于调用glUseProgram()
    m_program->bind();
    // 使用lookat方法创建观察矩阵
    int curtime = QTime::currentTime().msecsSinceStartOfDay();
    QMatrix4x4 view;
    float radius = 10.0;    //摄像机绕原点旋转的半径为10
    float camX = static_cast<float>(qSin(curtime) * radius);
    float camZ = static_cast<float>(qCos(curtime) * radius);
    // 假设摄像机在xoz平面上,绕原点(0,0,0)不断旋转
    view.lookAt(QVector3D(camX, 0, camZ), QVector3D(0.0, 0.0, 0.0), QVector3D(0.0, 1.0, 0.0));
    m_program->setUniformValue("view", view);
    ...
}

运行程序。我们便能得到一个摄像机视角不断变化的场景。

完整代码如下:

查看代码
 //glwidge.h
#ifndef GLWIDGET_H
#define GLWIDGET_H

#include <QOpenGLWidget>
#include <QOpenGLExtraFunctions>
#include <QOpenGLVertexArrayObject>
#include <QOpenGLBuffer>
#include <QOpenGLShader>
#include <QOpenGLShaderProgram>
#include <QOpenGLTexture>

class GLWidget : public QOpenGLWidget, protected QOpenGLFunctions
{
public:
    explicit GLWidget(QWidget *parent = nullptr);
    ~GLWidget();

protected:
    void resizeGL(int w, int h) override;
    void initializeGL() override;
    void paintGL() override;

private slots:
    void timerSlot();

private:
    QOpenGLVertexArrayObject *m_vao;    //顶点数组对象
    QOpenGLBuffer *m_vbo;               //顶点缓冲对象
    QOpenGLBuffer *m_ebo;               //元素缓冲对象
    QOpenGLShader *m_vshader;           //顶点着色器
    QOpenGLShader *m_fshader;           //片段着色器
    QOpenGLShaderProgram *m_program;    //着色器程序对象
    QOpenGLTexture *m_texture1 ;          //纹理对象
    QOpenGLTexture *m_texture2 ;          //纹理对象
};
#endif // GLWIDGET_H

//glwidget.cpp
#include "glwidget.h"
#include <QDir>
#include <QFile>
#include <QDebug>
#include <QImage>
#include <QMatrix4x4>
#include <QTime>
#include <QtMath>
#include <QTimer>

float vertices[] = {
    // 位置                 // 纹理坐标
    -0.5f, -0.5f, -0.5f,  0.0f, 0.0f,
        0.5f, -0.5f, -0.5f,  1.0f, 0.0f,
        0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
        0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
        -0.5f,  0.5f, -0.5f,  0.0f, 1.0f,
        -0.5f, -0.5f, -0.5f,  0.0f, 0.0f,

        -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
        0.5f, -0.5f,  0.5f,  1.0f, 0.0f,
        0.5f,  0.5f,  0.5f,  1.0f, 1.0f,
        0.5f,  0.5f,  0.5f,  1.0f, 1.0f,
        -0.5f,  0.5f,  0.5f,  0.0f, 1.0f,
        -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,

        -0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
        -0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
        -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
        -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
        -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
        -0.5f,  0.5f,  0.5f,  1.0f, 0.0f,

        0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
        0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
        0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
        0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
        0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
        0.5f,  0.5f,  0.5f,  1.0f, 0.0f,

        -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
        0.5f, -0.5f, -0.5f,  1.0f, 1.0f,
        0.5f, -0.5f,  0.5f,  1.0f, 0.0f,
        0.5f, -0.5f,  0.5f,  1.0f, 0.0f,
        -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
        -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,

        -0.5f,  0.5f, -0.5f,  0.0f, 1.0f,
        0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
        0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
        0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
        -0.5f,  0.5f,  0.5f,  0.0f, 0.0f,
        -0.5f,  0.5f, -0.5f,  0.0f, 1.0f
    };
QVector3D cubePositions[] = {
    QVector3D(0.0f,  0.0f,  0.0f),
    QVector3D(2.0f,  5.0f, -15.0f),
    QVector3D(-1.5f, -2.2f, -2.5f),
    QVector3D(-3.8f, -2.0f, -12.3f),
    QVector3D(2.4f, -0.4f, -3.5f),
    QVector3D(-1.7f,  3.0f, -7.5f),
    QVector3D(1.3f, -2.0f, -2.5f),
    QVector3D(1.5f,  2.0f, -2.5f),
    QVector3D(1.5f,  0.2f, -1.5f),
    QVector3D(-1.3f,  1.0f, -1.5f)
};

unsigned int indices[] = {
    // 注意索引从0开始!
    // 此例的索引(0,1,2,3)就是顶点数组vertices的下标,
    // 这样可以由下标代表顶点组合成矩形
    0, 1, 3, // 第一个三角形
    1, 2, 3  // 第二个三角形
};

GLWidget::GLWidget(QWidget *parent) : QOpenGLWidget(parent)
{
    makeCurrent();
    QTimer *timer = new QTimer(this);
    timer->setInterval(50);
    connect(timer, &QTimer::timeout, this, &GLWidget::timerSlot);
    timer->start();
}

GLWidget::~GLWidget()
{
    if (m_program == nullptr) { return; }
    makeCurrent();
    delete m_program;
    delete m_vshader;
    delete m_fshader;
    m_vbo->destroy();
    m_vao->destroy();
    doneCurrent();
}

//设置OpenGL视口、投影等。每当窗口部件被调整大小时调用(也在第一次显示时调用,因为所有新创建的窗口部件都自动获得调整大小事件)。
void GLWidget::resizeGL(int w, int h)
{

}

//设置OpenGL资源和状态。在第一次调用resizeGL()或paintGL()之前调用一次。
void GLWidget::initializeGL()
{
    initializeOpenGLFunctions();
    glClearColor(0.2f, 0.3f, 0.3f, 1.0f);// 设置清屏颜色
    glEnable(GL_DEPTH_TEST);    // 启用深度测试
    glEnable(GL_COLOR_BUFFER_BIT);  // 启用颜色缓冲区

    // 创建顶点着色器对象,并编译
    m_vshader = new QOpenGLShader(QOpenGLShader::Vertex);
    m_vshader->compileSourceFile(QString(":/shader/shader.vs"));
    // 创建片段着色器对象,并编译
    m_fshader = new QOpenGLShader(QOpenGLShader::Fragment);
    m_fshader->compileSourceFile(QString(":/shader/shader.fs"));
    // 创建着色器程序对象,添加顶点着色器和片段着色器,并链接它们
    m_program = new QOpenGLShaderProgram;
    m_program->addShader(m_vshader);
    m_program->addShader(m_fshader);
    m_program->link();
    // 创建顶点数组对象,并绑定到当前上下文
    m_vao = new QOpenGLVertexArrayObject;
    m_vao->create();
    QOpenGLVertexArrayObject::Binder vaoBinder(m_vao);
    // 创建顶点缓冲对象,
    m_vbo = new QOpenGLBuffer(QOpenGLBuffer::VertexBuffer);
    m_vbo->create();
    m_vbo->bind();
    // 分配显存大小,并搬运至显存
    m_vbo->allocate(vertices, sizeof(vertices));
    // 链接顶点属性
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), 0);
    // 顶点属性默认是禁用的,启用顶点属性0(location=0)
    m_program->enableAttributeArray(0);
    // 纹理坐标属性
    glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void *)(3 * sizeof(float)));
    m_program->enableAttributeArray(1);


    // 加载纹理
    m_texture1 = new QOpenGLTexture(QImage(":/image/container.jpg").mirrored());
    // 设置纹理环绕方式
    m_texture1->setWrapMode(QOpenGLTexture::DirectionS, QOpenGLTexture::Repeat);
    m_texture1->setWrapMode(QOpenGLTexture::DirectionT, QOpenGLTexture::Repeat);
    // 设置多级渐远纹理过滤方式
    m_texture1->setMinMagFilters(QOpenGLTexture::LinearMipMapLinear, QOpenGLTexture::Linear);

    // 加载纹理
    m_texture2 = new QOpenGLTexture(QImage(":/image/awesomeface.png").mirrored());
    // 设置纹理环绕方式
    m_texture2->setWrapMode(QOpenGLTexture::DirectionS, QOpenGLTexture::Repeat);
    m_texture2->setWrapMode(QOpenGLTexture::DirectionT, QOpenGLTexture::Repeat);
    // m_texture2
    m_texture1->setMinMagFilters(QOpenGLTexture::LinearMipMapLinear, QOpenGLTexture::Linear);


    // 我们需要手动设置哪个采样器对应哪个纹理单元
    m_program->bind();  //在修改uniform值之前,一定要绑定着色器程序到当前激活的opengl上下文
    m_program->setUniformValue("texture1", 0);
    m_program->setUniformValue("texture2", 1);
    // 创建投影矩阵,视野为45,这会显得更真实;近距离设置为0.1,远距离设置为100
    QMatrix4x4 projection;
    projection.perspective(45, width() / height(), 0.1, 100.0);
//    // 赋值给顶点着色器中的uniform变量
    m_program->setUniformValue("projection", projection);


    m_vao->release();
    m_vbo->release();
    // VAO还在使用时,不能释放EBO
}

//渲染OpenGL场景。每当窗口部件需要更新时调用。
void GLWidget::paintGL()
{
    // 清空颜色缓冲区和深度缓冲区
    glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    // 将纹理绑定到当前激活的纹理单元
    m_texture1->bind(0);
    m_texture2->bind(1);
    // 绑定着色器程序至当前上下文,相当于调用glUseProgram()
    m_program->bind();
    // 使用lookat方法创建观察矩阵
    int curtime = QTime::currentTime().msecsSinceStartOfDay();
    QMatrix4x4 view;
    float radius = 10.0;    //摄像机绕原点旋转的半径为10
    float camX = static_cast<float>(qSin(curtime) * radius);
    float camZ = static_cast<float>(qCos(curtime) * radius);
    // 假设摄像机在xoz平面上,绕原点(0,0,0)不断旋转
    view.lookAt(QVector3D(camX, 0, camZ), QVector3D(0.0, 0.0, 0.0), QVector3D(0.0, 1.0, 0.0));
    m_program->setUniformValue("view", view);

    // 绑定VAO,调用设置的一组状态
    QOpenGLVertexArrayObject::Binder vaoBinder(m_vao);
    // 绘制立方体,并平移到设定好的不同位置。
    for (int i = 0; i < 10 ; i++) {
        QMatrix4x4 model;
        model.translate(cubePositions[i]);
        m_program->setUniformValue("model", model);
        glDrawArrays(GL_TRIANGLES, 0, 36);
    }
    // 交换缓存
    auto *context = QOpenGLContext::currentContext();
    context->swapBuffers(context->surface());
    //使用swapBuffers后,必须调用makeCurrent后才能使用其他OpenGL函数。
    makeCurrent();
}

void GLWidget::timerSlot()
{
    this->update();
}

 

三、自由移动

接下来,我们加入键盘事件,使我们可以自由移动摄像机。

我们首先定义三个关于摄像机的变量:

    QVector3D cameraPos{0, 0, 3.0};         //摄像机位置
    QVector3D cameraFront{0, 0, -1.0};      //方向向量
    QVector3D cameraUp{0.0, 1.0, 0.0};      //上向量

然后将lookat函数的参数改成:

QMatrix4x4 view;
view.lookAt(this->cameraPos, this->cameraPos + this->cameraFront, this->cameraUp);

方向是当前的位置加上方向向量,这样能保证无论我们怎么移动,摄像机都会注释这目标方向。

接下来我们定义键盘事件,我们使用WASD 4个按键控制摄像机的位置:

void GLWidget::keyPressEvent(QKeyEvent *ev)
{
    float cameraSpeed = 0.05f;
    switch (Qt::Key(ev->key())) {
        case Qt::Key_W: {
            this->cameraPos += cameraSpeed * this->cameraFront;
            break;
        }
        case Qt::Key_A: {
            this->cameraPos -= cameraSpeed * QVector3D::crossProduct(this->cameraFront, this->cameraUp).normalized();
            break;
        }
        case Qt::Key_S: {
            this->cameraPos -= cameraSpeed *  this->cameraFront;
            break;
        }
        case Qt::Key_D: {
            this->cameraPos += cameraSpeed * QVector3D::crossProduct(this->cameraFront, this->cameraUp).normalized();
            break;
        }
        default:
            break;
    }
    update();
}

注意,我们对右向量进行了标准化。如果我们没对这个向量进行标准化,最后的叉乘结果会根据cameraFront变量返回大小不同的向量。如果我们不对向量进行标准化,我们就得根据摄像机的朝向不同加速或减速移动了,但如果进行了标准化移动就是匀速的。

完整代码如下:

查看代码
 //glwidget.h
#ifndef GLWIDGET_H
#define GLWIDGET_H

#include <QOpenGLWidget>
#include <QOpenGLExtraFunctions>
#include <QOpenGLVertexArrayObject>
#include <QOpenGLBuffer>
#include <QOpenGLShader>
#include <QOpenGLShaderProgram>
#include <QOpenGLTexture>
#include <QKeyEvent>
#include <QMouseEvent>

class GLWidget : public QOpenGLWidget, protected QOpenGLFunctions
{
public:
    explicit GLWidget(QWidget *parent = nullptr);
    ~GLWidget();

protected:
    void resizeGL(int w, int h) override;
    void initializeGL() override;
    void paintGL() override;
    void keyPressEvent(QKeyEvent *ev) override;
    void mousePressEvent(QMouseEvent *e) override;
private slots:
    void timerSlot();

private:
    QOpenGLVertexArrayObject *m_vao;    //顶点数组对象
    QOpenGLBuffer *m_vbo;               //顶点缓冲对象
    QOpenGLBuffer *m_ebo;               //元素缓冲对象
    QOpenGLShader *m_vshader;           //顶点着色器
    QOpenGLShader *m_fshader;           //片段着色器
    QOpenGLShaderProgram *m_program;    //着色器程序对象
    QOpenGLTexture *m_texture1 ;          //纹理对象
    QOpenGLTexture *m_texture2 ;          //纹理对象
    QVector3D cameraPos{0, 0, 3.0};         //摄像机位置
    QVector3D cameraFront{0, 0, -1.0};      //方向向量
    QVector3D cameraUp{0.0, 1.0, 0.0};      //上向量

};
#endif // GLWIDGET_H


//glwidget.cpp
#include "glwidget.h"
#include <QDir>
#include <QFile>
#include <QDebug>
#include <QImage>
#include <QMatrix4x4>
#include <QTime>
#include <QtMath>
#include <QTimer>

float vertices[] = {
    // 位置                 // 纹理坐标
    -0.5f, -0.5f, -0.5f,  0.0f, 0.0f,
        0.5f, -0.5f, -0.5f,  1.0f, 0.0f,
        0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
        0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
        -0.5f,  0.5f, -0.5f,  0.0f, 1.0f,
        -0.5f, -0.5f, -0.5f,  0.0f, 0.0f,

        -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
        0.5f, -0.5f,  0.5f,  1.0f, 0.0f,
        0.5f,  0.5f,  0.5f,  1.0f, 1.0f,
        0.5f,  0.5f,  0.5f,  1.0f, 1.0f,
        -0.5f,  0.5f,  0.5f,  0.0f, 1.0f,
        -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,

        -0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
        -0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
        -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
        -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
        -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
        -0.5f,  0.5f,  0.5f,  1.0f, 0.0f,

        0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
        0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
        0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
        0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
        0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
        0.5f,  0.5f,  0.5f,  1.0f, 0.0f,

        -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
        0.5f, -0.5f, -0.5f,  1.0f, 1.0f,
        0.5f, -0.5f,  0.5f,  1.0f, 0.0f,
        0.5f, -0.5f,  0.5f,  1.0f, 0.0f,
        -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
        -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,

        -0.5f,  0.5f, -0.5f,  0.0f, 1.0f,
        0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
        0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
        0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
        -0.5f,  0.5f,  0.5f,  0.0f, 0.0f,
        -0.5f,  0.5f, -0.5f,  0.0f, 1.0f
    };
QVector3D cubePositions[] = {
    QVector3D(0.0f,  0.0f,  0.0f),
    QVector3D(2.0f,  5.0f, -15.0f),
    QVector3D(-1.5f, -2.2f, -2.5f),
    QVector3D(-3.8f, -2.0f, -12.3f),
    QVector3D(2.4f, -0.4f, -3.5f),
    QVector3D(-1.7f,  3.0f, -7.5f),
    QVector3D(1.3f, -2.0f, -2.5f),
    QVector3D(1.5f,  2.0f, -2.5f),
    QVector3D(1.5f,  0.2f, -1.5f),
    QVector3D(-1.3f,  1.0f, -1.5f)
};

unsigned int indices[] = {
    // 注意索引从0开始!
    // 此例的索引(0,1,2,3)就是顶点数组vertices的下标,
    // 这样可以由下标代表顶点组合成矩形
    0, 1, 3, // 第一个三角形
    1, 2, 3  // 第二个三角形
};

GLWidget::GLWidget(QWidget *parent) : QOpenGLWidget(parent)
{
    makeCurrent();
    QTimer *timer = new QTimer(this);
    timer->setInterval(1000);
    connect(timer, &QTimer::timeout, this, &GLWidget::timerSlot);
    timer->start();
    this->setFocusPolicy(Qt::StrongFocus);

}

GLWidget::~GLWidget()
{
    if (m_program == nullptr) { return; }
    makeCurrent();
    delete m_program;
    delete m_vshader;
    delete m_fshader;
    m_vbo->destroy();
    m_vao->destroy();
    doneCurrent();
}

//设置OpenGL视口、投影等。每当窗口部件被调整大小时调用(也在第一次显示时调用,因为所有新创建的窗口部件都自动获得调整大小事件)。
void GLWidget::resizeGL(int w, int h)
{

}

//设置OpenGL资源和状态。在第一次调用resizeGL()或paintGL()之前调用一次。
void GLWidget::initializeGL()
{
    initializeOpenGLFunctions();
    glClearColor(0.2f, 0.3f, 0.3f, 1.0f);// 设置清屏颜色
    glEnable(GL_DEPTH_TEST);    // 启用深度测试
    glEnable(GL_COLOR_BUFFER_BIT);  // 启用颜色缓冲区

    // 创建顶点着色器对象,并编译
    m_vshader = new QOpenGLShader(QOpenGLShader::Vertex);
    m_vshader->compileSourceFile(QString(":/shader/shader.vs"));
    // 创建片段着色器对象,并编译
    m_fshader = new QOpenGLShader(QOpenGLShader::Fragment);
    m_fshader->compileSourceFile(QString(":/shader/shader.fs"));
    // 创建着色器程序对象,添加顶点着色器和片段着色器,并链接它们
    m_program = new QOpenGLShaderProgram;
    m_program->addShader(m_vshader);
    m_program->addShader(m_fshader);
    m_program->link();
    // 创建顶点数组对象,并绑定到当前上下文
    m_vao = new QOpenGLVertexArrayObject;
    m_vao->create();
    QOpenGLVertexArrayObject::Binder vaoBinder(m_vao);
    // 创建顶点缓冲对象,
    m_vbo = new QOpenGLBuffer(QOpenGLBuffer::VertexBuffer);
    m_vbo->create();
    m_vbo->bind();
    // 分配显存大小,并搬运至显存
    m_vbo->allocate(vertices, sizeof(vertices));
    // 链接顶点属性
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), 0);
    // 顶点属性默认是禁用的,启用顶点属性0(location=0)
    m_program->enableAttributeArray(0);
    // 纹理坐标属性
    glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void *)(3 * sizeof(float)));
    m_program->enableAttributeArray(1);


    // 加载纹理
    m_texture1 = new QOpenGLTexture(QImage(":/image/container.jpg").mirrored());
    // 设置纹理环绕方式
    m_texture1->setWrapMode(QOpenGLTexture::DirectionS, QOpenGLTexture::Repeat);
    m_texture1->setWrapMode(QOpenGLTexture::DirectionT, QOpenGLTexture::Repeat);
    // 设置多级渐远纹理过滤方式
    m_texture1->setMinMagFilters(QOpenGLTexture::LinearMipMapLinear, QOpenGLTexture::Linear);

    // 加载纹理
    m_texture2 = new QOpenGLTexture(QImage(":/image/awesomeface.png").mirrored());
    // 设置纹理环绕方式
    m_texture2->setWrapMode(QOpenGLTexture::DirectionS, QOpenGLTexture::Repeat);
    m_texture2->setWrapMode(QOpenGLTexture::DirectionT, QOpenGLTexture::Repeat);
    // m_texture2
    m_texture1->setMinMagFilters(QOpenGLTexture::LinearMipMapLinear, QOpenGLTexture::Linear);


    // 我们需要手动设置哪个采样器对应哪个纹理单元
    m_program->bind();  //在修改uniform值之前,一定要绑定着色器程序到当前激活的opengl上下文
    m_program->setUniformValue("texture1", 0);
    m_program->setUniformValue("texture2", 1);
    // 创建投影矩阵,视野为45,这会显得更真实;近距离设置为0.1,远距离设置为100
    QMatrix4x4 projection;
    projection.perspective(45, width() / height(), 0.1, 100.0);
//    // 赋值给顶点着色器中的uniform变量
    m_program->setUniformValue("projection", projection);


    m_vao->release();
    m_vbo->release();
    // VAO还在使用时,不能释放EBO
}

//渲染OpenGL场景。每当窗口部件需要更新时调用。
void GLWidget::paintGL()
{
    // 清空颜色缓冲区和深度缓冲区
    glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    // 将纹理绑定到当前激活的纹理单元
    m_texture1->bind(0);
    m_texture2->bind(1);
    // 绑定着色器程序至当前上下文,相当于调用glUseProgram()
    m_program->bind();
    // 使用lookat方法创建观察矩阵
    QMatrix4x4 view;
    view.lookAt(this->cameraPos, this->cameraPos + this->cameraFront, this->cameraUp);
    m_program->setUniformValue("view", view);

    // 绑定VAO,调用设置的一组状态
    QOpenGLVertexArrayObject::Binder vaoBinder(m_vao);
    // 绘制立方体,并平移到设定好的不同位置。
    for (int i = 0; i < 10 ; i++) {
        QMatrix4x4 model;
        model.translate(cubePositions[i]);
        m_program->setUniformValue("model", model);
        glDrawArrays(GL_TRIANGLES, 0, 36);
    }
    // 交换缓存
    auto *context = QOpenGLContext::currentContext();
    context->swapBuffers(context->surface());
    //使用swapBuffers后,必须调用makeCurrent后才能使用其他OpenGL函数。
    makeCurrent();
}

void GLWidget::timerSlot()
{
    this->update();
}

void GLWidget::keyPressEvent(QKeyEvent *ev)
{
    float cameraSpeed = 0.05f;
    switch (Qt::Key(ev->key())) {
        case Qt::Key_W: {
            this->cameraPos += cameraSpeed * this->cameraFront;
            break;
        }
        case Qt::Key_A: {
            this->cameraPos -= cameraSpeed * QVector3D::crossProduct(this->cameraFront, this->cameraUp).normalized();
            break;
        }
        case Qt::Key_S: {
            this->cameraPos -= cameraSpeed *  this->cameraFront;
            break;
        }
        case Qt::Key_D: {
            this->cameraPos += cameraSpeed * QVector3D::crossProduct(this->cameraFront, this->cameraUp).normalized();
            break;
        }
        default:
            break;
    }
    update();
}

void GLWidget::mousePressEvent(QMouseEvent *e)
{

}

 

移动速度

现在我们设置的移动速度是个常量,但是因为处理器的不同,有些处理器可能每秒会绘制的帧数更多。结果就是,根据配置不同,有些电脑会移动的很快,有些会移动的很慢。当我们发布程序的时候,要确保它在所有硬件上移动速度都一样。

图形程序和游戏通常会跟踪一个时间差(Deltatime)变量,它储存了渲染上一帧所用的时间。我们把速度乘以deltatime值,这样如果上一帧渲染的时间很长,那么这一帧移动的距离就会变大,从而去平衡渲染所花去的时间。使用这种方法时,不论电脑快慢,摄像机的速度都会相应的平衡。这样每个用户的体验就一样了。

我们计算整个渲染循环所用的时间差的方法是:在渲染开始计算当前时间,在渲染结束计算当前时间,然后相减得到时间差。

void GLWidget::paintGL()
{
    int currentFrame = QTime::currentTime().msecsSinceStartOfDay();// 渲染起始时间
    ...
    int currentFrame1 = QTime::currentTime().msecsSinceStartOfDay();    // 渲染结束时间
    this->deltaTime = currentFrame1 - currentFrame;
    (this->deltaTime > 0) ? (this->deltaTime) : (this->deltaTime = 1);  // 有可能出现0就会卡一下
}

void GLWidget::keyPressEvent(QKeyEvent *ev)
{
    float cameraSpeed = 0.05 * this->deltaTime;
    ...

}

 这样就能得到一个比较平滑的体验(好吧,其实我的电脑根本体现不出来,但是方法是这么个方法)。

完整代码如下:

查看代码
 #ifndef GLWIDGET_H
#define GLWIDGET_H

#include <QOpenGLWidget>
#include <QOpenGLExtraFunctions>
#include <QOpenGLVertexArrayObject>
#include <QOpenGLBuffer>
#include <QOpenGLShader>
#include <QOpenGLShaderProgram>
#include <QOpenGLTexture>
#include <QKeyEvent>
#include <QMouseEvent>

class GLWidget : public QOpenGLWidget, protected QOpenGLFunctions
{
public:
    explicit GLWidget(QWidget *parent = nullptr);
    ~GLWidget();

protected:
    void resizeGL(int w, int h) override;
    void initializeGL() override;
    void paintGL() override;
    void keyPressEvent(QKeyEvent *ev) override;
    void mousePressEvent(QMouseEvent *e) override;
private slots:
    void timerSlot();

private:
    QOpenGLVertexArrayObject *m_vao;    //顶点数组对象
    QOpenGLBuffer *m_vbo;               //顶点缓冲对象
    QOpenGLBuffer *m_ebo;               //元素缓冲对象
    QOpenGLShader *m_vshader;           //顶点着色器
    QOpenGLShader *m_fshader;           //片段着色器
    QOpenGLShaderProgram *m_program;    //着色器程序对象
    QOpenGLTexture *m_texture1 ;          //纹理对象
    QOpenGLTexture *m_texture2 ;          //纹理对象
    QVector3D cameraPos{0, 0, 3.0};         //摄像机位置
    QVector3D cameraFront{0, 0, -1.0};      //方向向量
    QVector3D cameraUp{0.0, 1.0, 0.0};      //上向量
    float deltaTime{0.0};               //当前帧与上一帧的时间差
};
#endif // GLWIDGET_H

// glwidget.cpp
#include "glwidget.h"
#include <QDir>
#include <QFile>
#include <QDebug>
#include <QImage>
#include <QMatrix4x4>
#include <QTime>
#include <QtMath>
#include <QTimer>

float vertices[] = {
    // 位置                 // 纹理坐标
    -0.5f, -0.5f, -0.5f,  0.0f, 0.0f,
        0.5f, -0.5f, -0.5f,  1.0f, 0.0f,
        0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
        0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
        -0.5f,  0.5f, -0.5f,  0.0f, 1.0f,
        -0.5f, -0.5f, -0.5f,  0.0f, 0.0f,

        -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
        0.5f, -0.5f,  0.5f,  1.0f, 0.0f,
        0.5f,  0.5f,  0.5f,  1.0f, 1.0f,
        0.5f,  0.5f,  0.5f,  1.0f, 1.0f,
        -0.5f,  0.5f,  0.5f,  0.0f, 1.0f,
        -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,

        -0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
        -0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
        -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
        -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
        -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
        -0.5f,  0.5f,  0.5f,  1.0f, 0.0f,

        0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
        0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
        0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
        0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
        0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
        0.5f,  0.5f,  0.5f,  1.0f, 0.0f,

        -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
        0.5f, -0.5f, -0.5f,  1.0f, 1.0f,
        0.5f, -0.5f,  0.5f,  1.0f, 0.0f,
        0.5f, -0.5f,  0.5f,  1.0f, 0.0f,
        -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
        -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,

        -0.5f,  0.5f, -0.5f,  0.0f, 1.0f,
        0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
        0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
        0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
        -0.5f,  0.5f,  0.5f,  0.0f, 0.0f,
        -0.5f,  0.5f, -0.5f,  0.0f, 1.0f
    };
QVector3D cubePositions[] = {
    QVector3D(0.0f,  0.0f,  0.0f),
    QVector3D(2.0f,  5.0f, -15.0f),
    QVector3D(-1.5f, -2.2f, -2.5f),
    QVector3D(-3.8f, -2.0f, -12.3f),
    QVector3D(2.4f, -0.4f, -3.5f),
    QVector3D(-1.7f,  3.0f, -7.5f),
    QVector3D(1.3f, -2.0f, -2.5f),
    QVector3D(1.5f,  2.0f, -2.5f),
    QVector3D(1.5f,  0.2f, -1.5f),
    QVector3D(-1.3f,  1.0f, -1.5f)
};

unsigned int indices[] = {
    // 注意索引从0开始!
    // 此例的索引(0,1,2,3)就是顶点数组vertices的下标,
    // 这样可以由下标代表顶点组合成矩形
    0, 1, 3, // 第一个三角形
    1, 2, 3  // 第二个三角形
};

GLWidget::GLWidget(QWidget *parent) : QOpenGLWidget(parent)
{
    makeCurrent();
    QTimer *timer = new QTimer(this);
    timer->setInterval(1000);
    connect(timer, &QTimer::timeout, this, &GLWidget::timerSlot);
//    timer->start();
    this->setFocusPolicy(Qt::StrongFocus);

}

GLWidget::~GLWidget()
{
    if (m_program == nullptr) { return; }
    makeCurrent();
    delete m_program;
    delete m_vshader;
    delete m_fshader;
    m_vbo->destroy();
    m_vao->destroy();
    doneCurrent();
}

//设置OpenGL视口、投影等。每当窗口部件被调整大小时调用(也在第一次显示时调用,因为所有新创建的窗口部件都自动获得调整大小事件)。
void GLWidget::resizeGL(int w, int h)
{

}

//设置OpenGL资源和状态。在第一次调用resizeGL()或paintGL()之前调用一次。
void GLWidget::initializeGL()
{
    initializeOpenGLFunctions();
    glClearColor(0.2f, 0.3f, 0.3f, 1.0f);// 设置清屏颜色
    glEnable(GL_DEPTH_TEST);    // 启用深度测试
    glEnable(GL_COLOR_BUFFER_BIT);  // 启用颜色缓冲区

    // 创建顶点着色器对象,并编译
    m_vshader = new QOpenGLShader(QOpenGLShader::Vertex);
    m_vshader->compileSourceFile(QString(":/shader/shader.vs"));
    // 创建片段着色器对象,并编译
    m_fshader = new QOpenGLShader(QOpenGLShader::Fragment);
    m_fshader->compileSourceFile(QString(":/shader/shader.fs"));
    // 创建着色器程序对象,添加顶点着色器和片段着色器,并链接它们
    m_program = new QOpenGLShaderProgram;
    m_program->addShader(m_vshader);
    m_program->addShader(m_fshader);
    m_program->link();
    // 创建顶点数组对象,并绑定到当前上下文
    m_vao = new QOpenGLVertexArrayObject;
    m_vao->create();
    QOpenGLVertexArrayObject::Binder vaoBinder(m_vao);
    // 创建顶点缓冲对象,
    m_vbo = new QOpenGLBuffer(QOpenGLBuffer::VertexBuffer);
    m_vbo->create();
    m_vbo->bind();
    // 分配显存大小,并搬运至显存
    m_vbo->allocate(vertices, sizeof(vertices));
    // 链接顶点属性
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), 0);
    // 顶点属性默认是禁用的,启用顶点属性0(location=0)
    m_program->enableAttributeArray(0);
    // 纹理坐标属性
    glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void *)(3 * sizeof(float)));
    m_program->enableAttributeArray(1);


    // 加载纹理
    m_texture1 = new QOpenGLTexture(QImage(":/image/container.jpg").mirrored());
    // 设置纹理环绕方式
    m_texture1->setWrapMode(QOpenGLTexture::DirectionS, QOpenGLTexture::Repeat);
    m_texture1->setWrapMode(QOpenGLTexture::DirectionT, QOpenGLTexture::Repeat);
    // 设置多级渐远纹理过滤方式
    m_texture1->setMinMagFilters(QOpenGLTexture::LinearMipMapLinear, QOpenGLTexture::Linear);

    // 加载纹理
    m_texture2 = new QOpenGLTexture(QImage(":/image/awesomeface.png").mirrored());
    // 设置纹理环绕方式
    m_texture2->setWrapMode(QOpenGLTexture::DirectionS, QOpenGLTexture::Repeat);
    m_texture2->setWrapMode(QOpenGLTexture::DirectionT, QOpenGLTexture::Repeat);
    // m_texture2
    m_texture1->setMinMagFilters(QOpenGLTexture::LinearMipMapLinear, QOpenGLTexture::Linear);


    // 我们需要手动设置哪个采样器对应哪个纹理单元
    m_program->bind();  //在修改uniform值之前,一定要绑定着色器程序到当前激活的opengl上下文
    m_program->setUniformValue("texture1", 0);
    m_program->setUniformValue("texture2", 1);
    // 创建投影矩阵,视野为45,这会显得更真实;近距离设置为0.1,远距离设置为100
    QMatrix4x4 projection;
    projection.perspective(45, width() / height(), 0.1, 100.0);
//    // 赋值给顶点着色器中的uniform变量
    m_program->setUniformValue("projection", projection);


    m_vao->release();
    m_vbo->release();
    // VAO还在使用时,不能释放EBO
}

//渲染OpenGL场景。每当窗口部件需要更新时调用。
void GLWidget::paintGL()
{
    int currentFrame = QTime::currentTime().msecsSinceStartOfDay();// 渲染起始时间
    // 清空颜色缓冲区和深度缓冲区
    glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    // 将纹理绑定到当前激活的纹理单元
    m_texture1->bind(0);
    m_texture2->bind(1);
    // 绑定着色器程序至当前上下文,相当于调用glUseProgram()
    m_program->bind();
    // 使用lookat方法创建观察矩阵
    QMatrix4x4 view;
    view.lookAt(this->cameraPos, this->cameraPos + this->cameraFront, this->cameraUp);
    m_program->setUniformValue("view", view);

    // 绑定VAO,调用设置的一组状态
    QOpenGLVertexArrayObject::Binder vaoBinder(m_vao);
    // 绘制立方体,并平移到设定好的不同位置。
    for (int i = 0; i < 10 ; i++) {
        QMatrix4x4 model;
        model.translate(cubePositions[i]);
        m_program->setUniformValue("model", model);
        glDrawArrays(GL_TRIANGLES, 0, 36);
    }
    // 交换缓存
    auto *context = QOpenGLContext::currentContext();
    context->swapBuffers(context->surface());
    //使用swapBuffers后,必须调用makeCurrent后才能使用其他OpenGL函数。
    makeCurrent();

    int currentFrame1 = QTime::currentTime().msecsSinceStartOfDay();    // 渲染结束时间
    this->deltaTime = currentFrame1 - currentFrame;
    (this->deltaTime > 0) ? (this->deltaTime) : (this->deltaTime = 1);  // 有可能出现0就会卡一下
}

void GLWidget::timerSlot()
{
    this->update();
}

void GLWidget::keyPressEvent(QKeyEvent *ev)
{
    float cameraSpeed = 0.05 * this->deltaTime;
    switch (Qt::Key(ev->key())) {
        case Qt::Key_W: {
            this->cameraPos += cameraSpeed * this->cameraFront;
            break;
        }
        case Qt::Key_A: {
            this->cameraPos -= cameraSpeed * QVector3D::crossProduct(this->cameraFront, this->cameraUp).normalized();
            break;
        }
        case Qt::Key_S: {
            this->cameraPos -= cameraSpeed *  this->cameraFront;
            break;
        }
        case Qt::Key_D: {
            this->cameraPos += cameraSpeed * QVector3D::crossProduct(this->cameraFront, this->cameraUp).normalized();
            break;
        }
        default:
            break;
    }
    update();
}

void GLWidget::mousePressEvent(QMouseEvent *e)
{

}

 

 四、视角移动

上下左右旋转

欧拉角(Euler Angle)是可以表示3D空间中任何旋转的3个值,由莱昂哈德·欧拉(Leonhard Euler)在18世纪提出。一共有3种欧拉角:俯仰角(Pitch)、偏航角(Yaw)和滚转角(Roll),下面的图片展示了它们的含义:

 

 

俯仰角是描述我们如何往上或往下看的角,可以在第一张图中看到。第二张图展示了偏航角,偏航角表示我们往左和往右看的程度。滚转角代表我们如何翻滚摄像机,通常在太空飞船的摄像机中使用。每个欧拉角都有一个值来表示,把三个角结合起来我们就能够计算3D空间中任何的旋转向量了。

 我们现在只关心俯仰角和偏航角。

 假设摄像机方向向量的长度为1.根据下图,我们可以计算出在进行上下和左右移动方向后,每个轴分量的长度,从而得到基于俯仰角和偏航角的方向向量。

 

direction.x = cos(glm::radians(pitch)) * cos(glm::radians(yaw)); // 译注:direction代表摄像机的前轴(Front),这个前轴是和本文第一幅图片的第二个摄像机的方向向量是相反的
direction.y = sin(glm::radians(pitch));
direction.z = cos(glm::radians(pitch)) * sin(glm::radians(yaw));

 这样我们就有了一个可以把俯仰角和偏航角转化为用来自由旋转视角的摄像机的3维方向向量了。

我们使用鼠标输入来控制俯仰角和偏航角。其实就是通过计算偏航角和俯仰角来计算摄像机的方向向量。

我们在鼠标左键按住界面的时候,进行上下旋转和左右旋转,主要是通过计算鼠标x坐标和y坐标的这一帧和上一帧的偏移量来叠加到俯仰角和偏航角上去,然后计算出方向向量。

主要代码如下:

//glwidget.h
// 添加私有成员变量
    float pitch{0};                     //俯仰角
    float yaw{-90.0};                   //偏航角
    QPoint lastP;                      //上一次鼠标的位置
    Qt::MouseButton curMouseBtn;        // 记录当前点击的哪个鼠标
    bool firstMouse;                    // 鼠标释放之后是否又重新点击了

//glwidget.cpp
void GLWidget::mousePressEvent(QMouseEvent *e)
{
    // 记录当前点击的是鼠标的哪个键
    this->curMouseBtn = e->button();
    // 每次重新点击都要重新置为真,不然会出现大的跳动。
    this->firstMouse = true;
}
void GLWidget::mouseMoveEvent(QMouseEvent *e)
{
    if (this->curMouseBtn == Qt::LeftButton) {
        // 获取当前的鼠标位置
        int xpos = e->localPos().x();
        int ypos = e->localPos().y();
        // 第一次鼠标移动的时候,防止大的跳动
        if (this->firstMouse) {
            this->lastP.setX(xpos);
            this->lastP.setY(ypos);
            this->firstMouse = false;
        }
        // 计算鼠标的偏移量
        float xoffset = xpos - this->lastP.x();
        float yoffset = ypos - this->lastP.y();
        this->lastP.setX(xpos);
        this->lastP.setY(ypos);
        float sensitivity = 0.1;  //灵敏度自己调节,调整到自己合适的大小,以至于移动速度不会太快。
        xoffset *= sensitivity;
        yoffset *= sensitivity;
        // 将鼠标偏移量添加到俯仰角和偏航角
        this->yaw += xoffset;
        this->pitch += yoffset;
        // 确保屏幕俯仰角不会反转,不然会360度旋转,继续旋转也能回来,但体验不好。
        (this->pitch > 89.0) ? (this->pitch = 89.0) : (this->pitch);
        (this->pitch < -89.0) ? (this->pitch = -89.0) : (this->pitch);
        // 根据公式计算出方向向量
        QVector3D front;
        float yawRadian = qDegreesToRadians(this->yaw);
        float pitchRadian = qDegreesToRadians(this->pitch);
        front.setX(qCos(pitchRadian) * qCos(yawRadian));
        front.setY(qSin(pitchRadian));
        front.setZ(qCos(pitchRadian) * qSin(yawRadian));
        this->cameraFront = front.normalized();
        update();
    }
}

这样我们就能在上下和左右旋转了。

 

缩放

我们前面使用键盘控制摄像机的方向向量来达到实现缩放的目的,因为我们使用的是透视投影,所以如果摄像机的位置越远那么物体看上去就会越小,反之越大。

我们现在通过改变在之前说过的视野(Field of View)或fov来实现缩放。fov定义了我们可以看到场景中多大的范围。当视野变小时,场景投影出来的空间就会变小,产生放大的感觉,反之则产生缩小的感觉。

我们使用鼠标的滚轮来放大缩小。即来改变perspective中的fov值,现在每帧都需要改变,我们要把投影矩阵的计算也放到渲染循环中去。

主要代码如下:

void GLWidget::paintGL()
{
    // 投影矩阵
    QMatrix4x4 projection;
    projection.perspective(this->fov, width() / height(), 0.1, 100.0);
    m_program->setUniformValue("projection", projection);
}


void GLWidget::wheelEvent(QWheelEvent *e)
{
    this->fov -= e->angleDelta().y() / 100.0;
    (this->fov > 45.0) ? (this->fov = 45) : (this->fov);
    (this->fov < 1.0) ? (this->fov = 1.0) : (this->fov);
    update();
}

这样我们就能使用鼠标滚轮来完成缩放了。

 

完整代码如下;

查看代码
 //glwidget.h
#ifndef GLWIDGET_H
#define GLWIDGET_H

#include <QOpenGLWidget>
#include <QOpenGLExtraFunctions>
#include <QOpenGLVertexArrayObject>
#include <QOpenGLBuffer>
#include <QOpenGLShader>
#include <QOpenGLShaderProgram>
#include <QOpenGLTexture>
#include <QKeyEvent>
#include <QMouseEvent>
#include <QWheelEvent>

class GLWidget : public QOpenGLWidget, protected QOpenGLFunctions
{
public:
    explicit GLWidget(QWidget *parent = nullptr);
    ~GLWidget();

protected:
    void resizeGL(int w, int h) override;
    void initializeGL() override;
    void paintGL() override;
    void keyPressEvent(QKeyEvent *ev) override;
    void mousePressEvent(QMouseEvent *e) override;
    void mouseMoveEvent(QMouseEvent *e) override;
    void wheelEvent(QWheelEvent *e) override;
private slots:
    void timerSlot();

private:
    QOpenGLVertexArrayObject *m_vao;    //顶点数组对象
    QOpenGLBuffer *m_vbo;               //顶点缓冲对象
    QOpenGLBuffer *m_ebo;               //元素缓冲对象
    QOpenGLShader *m_vshader;           //顶点着色器
    QOpenGLShader *m_fshader;           //片段着色器
    QOpenGLShaderProgram *m_program;    //着色器程序对象
    QOpenGLTexture *m_texture1 ;          //纹理对象
    QOpenGLTexture *m_texture2 ;          //纹理对象
    QVector3D cameraPos{0, 0, 3.0};         //摄像机位置
    QVector3D cameraFront{0, 0, -1.0};      //方向向量
    QVector3D cameraUp{0.0, 1.0, 0.0};      //上向量
    float deltaTime{0.0};               //当前帧与上一帧的时间差
    float pitch{0};                     //俯仰角
    float yaw{-90.0};                   //偏航角
    QPoint lastP;                      //上一次鼠标的位置
    Qt::MouseButton curMouseBtn;        // 记录当前点击的哪个鼠标
    bool firstMouse;                    // 鼠标释放之后是否又重新点击了
    float fov{45.0};                          // 视野
};
#endif // GLWIDGET_H


//glwidget.cpp
#include "glwidget.h"
#include <QDir>
#include <QFile>
#include <QDebug>
#include <QImage>
#include <QMatrix4x4>
#include <QTime>
#include <QtMath>
#include <QTimer>

float vertices[] = {
    // 位置                 // 纹理坐标
    -0.5f, -0.5f, -0.5f,  0.0f, 0.0f,
        0.5f, -0.5f, -0.5f,  1.0f, 0.0f,
        0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
        0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
        -0.5f,  0.5f, -0.5f,  0.0f, 1.0f,
        -0.5f, -0.5f, -0.5f,  0.0f, 0.0f,

        -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
        0.5f, -0.5f,  0.5f,  1.0f, 0.0f,
        0.5f,  0.5f,  0.5f,  1.0f, 1.0f,
        0.5f,  0.5f,  0.5f,  1.0f, 1.0f,
        -0.5f,  0.5f,  0.5f,  0.0f, 1.0f,
        -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,

        -0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
        -0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
        -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
        -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
        -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
        -0.5f,  0.5f,  0.5f,  1.0f, 0.0f,

        0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
        0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
        0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
        0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
        0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
        0.5f,  0.5f,  0.5f,  1.0f, 0.0f,

        -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
        0.5f, -0.5f, -0.5f,  1.0f, 1.0f,
        0.5f, -0.5f,  0.5f,  1.0f, 0.0f,
        0.5f, -0.5f,  0.5f,  1.0f, 0.0f,
        -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
        -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,

        -0.5f,  0.5f, -0.5f,  0.0f, 1.0f,
        0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
        0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
        0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
        -0.5f,  0.5f,  0.5f,  0.0f, 0.0f,
        -0.5f,  0.5f, -0.5f,  0.0f, 1.0f
    };
QVector3D cubePositions[] = {
    QVector3D(0.0f,  0.0f,  0.0f),
    QVector3D(2.0f,  5.0f, -15.0f),
    QVector3D(-1.5f, -2.2f, -2.5f),
    QVector3D(-3.8f, -2.0f, -12.3f),
    QVector3D(2.4f, -0.4f, -3.5f),
    QVector3D(-1.7f,  3.0f, -7.5f),
    QVector3D(1.3f, -2.0f, -2.5f),
    QVector3D(1.5f,  2.0f, -2.5f),
    QVector3D(1.5f,  0.2f, -1.5f),
    QVector3D(-1.3f,  1.0f, -1.5f)
};

unsigned int indices[] = {
    // 注意索引从0开始!
    // 此例的索引(0,1,2,3)就是顶点数组vertices的下标,
    // 这样可以由下标代表顶点组合成矩形
    0, 1, 3, // 第一个三角形
    1, 2, 3  // 第二个三角形
};

GLWidget::GLWidget(QWidget *parent) : QOpenGLWidget(parent)
{
    makeCurrent();
    QTimer *timer = new QTimer(this);
    timer->setInterval(1000);
    connect(timer, &QTimer::timeout, this, &GLWidget::timerSlot);
//    timer->start();
    this->setFocusPolicy(Qt::StrongFocus);
    this->lastP.setX(width() / 2);
    this->lastP.setY(height() / 2);
}

GLWidget::~GLWidget()
{
    if (m_program == nullptr) { return; }
    makeCurrent();
    delete m_program;
    delete m_vshader;
    delete m_fshader;
    m_vbo->destroy();
    m_vao->destroy();
    doneCurrent();
}

//设置OpenGL视口、投影等。每当窗口部件被调整大小时调用(也在第一次显示时调用,因为所有新创建的窗口部件都自动获得调整大小事件)。
void GLWidget::resizeGL(int w, int h)
{

}

//设置OpenGL资源和状态。在第一次调用resizeGL()或paintGL()之前调用一次。
void GLWidget::initializeGL()
{
    initializeOpenGLFunctions();
    glClearColor(0.2f, 0.3f, 0.3f, 1.0f);// 设置清屏颜色
    glEnable(GL_DEPTH_TEST);    // 启用深度测试
    glEnable(GL_COLOR_BUFFER_BIT);  // 启用颜色缓冲区

    // 创建顶点着色器对象,并编译
    m_vshader = new QOpenGLShader(QOpenGLShader::Vertex);
    m_vshader->compileSourceFile(QString(":/shader/shader.vs"));
    // 创建片段着色器对象,并编译
    m_fshader = new QOpenGLShader(QOpenGLShader::Fragment);
    m_fshader->compileSourceFile(QString(":/shader/shader.fs"));
    // 创建着色器程序对象,添加顶点着色器和片段着色器,并链接它们
    m_program = new QOpenGLShaderProgram;
    m_program->addShader(m_vshader);
    m_program->addShader(m_fshader);
    m_program->link();
    // 创建顶点数组对象,并绑定到当前上下文
    m_vao = new QOpenGLVertexArrayObject;
    m_vao->create();
    QOpenGLVertexArrayObject::Binder vaoBinder(m_vao);
    // 创建顶点缓冲对象,
    m_vbo = new QOpenGLBuffer(QOpenGLBuffer::VertexBuffer);
    m_vbo->create();
    m_vbo->bind();
    // 分配显存大小,并搬运至显存
    m_vbo->allocate(vertices, sizeof(vertices));
    // 链接顶点属性
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), 0);
    // 顶点属性默认是禁用的,启用顶点属性0(location=0)
    m_program->enableAttributeArray(0);
    // 纹理坐标属性
    glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void *)(3 * sizeof(float)));
    m_program->enableAttributeArray(1);


    // 加载纹理
    m_texture1 = new QOpenGLTexture(QImage(":/image/container.jpg").mirrored());
    // 设置纹理环绕方式
    m_texture1->setWrapMode(QOpenGLTexture::DirectionS, QOpenGLTexture::Repeat);
    m_texture1->setWrapMode(QOpenGLTexture::DirectionT, QOpenGLTexture::Repeat);
    // 设置多级渐远纹理过滤方式
    m_texture1->setMinMagFilters(QOpenGLTexture::LinearMipMapLinear, QOpenGLTexture::Linear);

    // 加载纹理
    m_texture2 = new QOpenGLTexture(QImage(":/image/awesomeface.png").mirrored());
    // 设置纹理环绕方式
    m_texture2->setWrapMode(QOpenGLTexture::DirectionS, QOpenGLTexture::Repeat);
    m_texture2->setWrapMode(QOpenGLTexture::DirectionT, QOpenGLTexture::Repeat);
    // m_texture2
    m_texture1->setMinMagFilters(QOpenGLTexture::LinearMipMapLinear, QOpenGLTexture::Linear);


    // 我们需要手动设置哪个采样器对应哪个纹理单元
    m_program->bind();  //在修改uniform值之前,一定要绑定着色器程序到当前激活的opengl上下文
    m_program->setUniformValue("texture1", 0);
    m_program->setUniformValue("texture2", 1);
//    // 创建投影矩阵,视野为45,这会显得更真实;近距离设置为0.1,远距离设置为100
//    QMatrix4x4 projection;
//    projection.perspective(45, width() / height(), 0.1, 100.0);
////    // 赋值给顶点着色器中的uniform变量
//    m_program->setUniformValue("projection", projection);


    m_vao->release();
    m_vbo->release();
    // VAO还在使用时,不能释放EBO
}

//渲染OpenGL场景。每当窗口部件需要更新时调用。
void GLWidget::paintGL()
{
    int currentFrame = QTime::currentTime().msecsSinceStartOfDay();// 渲染起始时间
    // 清空颜色缓冲区和深度缓冲区
    glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    // 将纹理绑定到当前激活的纹理单元
    m_texture1->bind(0);
    m_texture2->bind(1);
    // 绑定着色器程序至当前上下文,相当于调用glUseProgram()
    m_program->bind();
    // 使用lookat方法创建观察矩阵
    QMatrix4x4 view;
    view.lookAt(this->cameraPos, this->cameraPos + this->cameraFront, this->cameraUp);
    m_program->setUniformValue("view", view);
    // 投影矩阵
    QMatrix4x4 projection;
    projection.perspective(this->fov, width() / height(), 0.1, 100.0);
    m_program->setUniformValue("projection", projection);

    // 绑定VAO,调用设置的一组状态
    QOpenGLVertexArrayObject::Binder vaoBinder(m_vao);
    // 绘制立方体,并平移到设定好的不同位置。
    for (int i = 0; i < 10 ; i++) {
        QMatrix4x4 model;
        model.translate(cubePositions[i]);
        m_program->setUniformValue("model", model);
        glDrawArrays(GL_TRIANGLES, 0, 36);
    }
    // 交换缓存
    auto *context = QOpenGLContext::currentContext();
    context->swapBuffers(context->surface());
    //使用swapBuffers后,必须调用makeCurrent后才能使用其他OpenGL函数。
    makeCurrent();

    int currentFrame1 = QTime::currentTime().msecsSinceStartOfDay();    // 渲染结束时间
    this->deltaTime = currentFrame1 - currentFrame;
    (this->deltaTime > 0) ? (this->deltaTime) : (this->deltaTime = 1);  // 有可能出现0就会卡一下
}

void GLWidget::timerSlot()
{
    this->update();
}

void GLWidget::keyPressEvent(QKeyEvent *ev)
{
    float cameraSpeed = 0.05 * this->deltaTime;
    switch (Qt::Key(ev->key())) {
        case Qt::Key_W: {
            this->cameraPos += cameraSpeed * this->cameraFront;
            break;
        }
        case Qt::Key_A: {
            this->cameraPos -= cameraSpeed * QVector3D::crossProduct(this->cameraFront, this->cameraUp).normalized();
            break;
        }
        case Qt::Key_S: {
            this->cameraPos -= cameraSpeed *  this->cameraFront;
            break;
        }
        case Qt::Key_D: {
            this->cameraPos += cameraSpeed * QVector3D::crossProduct(this->cameraFront, this->cameraUp).normalized();
            break;
        }
        default:
            break;
    }
    update();
}

void GLWidget::mousePressEvent(QMouseEvent *e)
{
    // 记录当前点击的是鼠标的哪个键
    this->curMouseBtn = e->button();
    // 每次重新点击都要重新置为真,不然会出现大的跳动。
    this->firstMouse = true;
}

void GLWidget::mouseMoveEvent(QMouseEvent *e)
{
    if (this->curMouseBtn == Qt::LeftButton) {
        // 获取当前的鼠标位置
        int xpos = e->localPos().x();
        int ypos = e->localPos().y();
        // 第一次鼠标移动的时候,防止大的跳动
        if (this->firstMouse) {
            this->lastP.setX(xpos);
            this->lastP.setY(ypos);
            this->firstMouse = false;
        }
        // 计算鼠标的偏移量
        float xoffset = xpos - this->lastP.x();
        float yoffset = ypos - this->lastP.y();
        this->lastP.setX(xpos);
        this->lastP.setY(ypos);
        float sensitivity = 0.1;  //灵敏度自己调节,调整到自己合适的大小,以至于移动速度不会太快。
        xoffset *= sensitivity;
        yoffset *= sensitivity;
        // 将鼠标偏移量添加到俯仰角和偏航角
        this->yaw += xoffset;
        this->pitch += yoffset;
        // 确保屏幕俯仰角不会反转,不然会360度旋转,继续旋转也能回来,但体验不好。
        (this->pitch > 89.0) ? (this->pitch = 89.0) : (this->pitch);
        (this->pitch < -89.0) ? (this->pitch = -89.0) : (this->pitch);
        // 根据公式计算出方向向量
        QVector3D front;
        float yawRadian = qDegreesToRadians(this->yaw);
        float pitchRadian = qDegreesToRadians(this->pitch);
        front.setX(qCos(pitchRadian) * qCos(yawRadian));
        front.setY(qSin(pitchRadian));
        front.setZ(qCos(pitchRadian) * qSin(yawRadian));
        this->cameraFront = front.normalized();
        update();
    }
}

void GLWidget::wheelEvent(QWheelEvent *e)
{
    this->fov -= e->angleDelta().y() / 100.0;
    (this->fov > 45.0) ? (this->fov = 45) : (this->fov);
    (this->fov < 1.0) ? (this->fov = 1.0) : (this->fov);
    update();
}