Learn OpenGL with Qt——完美的Qt摄像机

如果你是中途开始学习本教程的,即使你对OpenGL已经非常熟悉,请至少了解以下几个章节,因为Qt中提供了OpenGL的很多便捷操作,熟悉这些操作可以让我们在Qt中高效的使用OpenGL进行绘图。

创建OpenGL窗口

着色器程序以及着色器的创建

纹理的创建与加载

使用Qt内置矩阵进行变换

Qt-OpenGL的几个优势:

  1. Qt内嵌了opengl的相关环境,不需要我们自己来搭建,这对小白来说是很友好的。
  2. Qt和opengl都具有优良的跨平台特性,使用Qt做opengl开发可谓是强强联合。
  3. Qt可以轻松的控制窗口的各种处理事件以及窗口属性。
  4. Qt提供了opengl函数的C++封装,使得opengl原来的C风格API可以通过C++的面向对象技术来实现。
  5. Qt提供了十分完善的官方文档,有助于我们掌握QtOpenGL的各种细节。

这个教程将完全使用Qt对openglAPI的C++封装,内容板块尽量与learnopengl保持一致。笔者会逐步的实现教程里的demo,尽可能的说明每一个操作细节。你可以在文章的右上角找到本节的索引目录,如果什么地方操作失败,你可以直接复制代码节点的代码,尝试运行一下,再对比一下自己的代码,看自己是否什么地方出问题了,如果还不能解决问题,可以在下方评论区留言,笔者看到一定第一时间解答。

笔者对openGL了解不是很深,如果什么地方存在问题,希望朋友们能够详细指出。

本节相对于GLFW窗口,对Qt窗口需要做两个优化。

  • 消除Qt窗口实现监听键盘事件的"卡顿"
  • 实现鼠标锁定到窗口并隐藏鼠标

摄像机

前面的教程中我们讨论了观察矩阵以及如何使用观察矩阵移动场景(我们向后移动了一点)。OpenGL本身没有摄像机(Camera)的概念,但我们可以通过把场景中的所有物体往相反方向移动的方式来模拟出摄像机,产生一种我们在移动的感觉,而不是场景在移动。

本节我们将会讨论如何在Qt中配置一个摄像机,并且将会讨论FPS风格的摄像机,让你能够在3D场景中自由移动。我们也会讨论键盘和鼠标输入,最终完成一个带有摄像机的窗口。

摄像机/观察空间

当我们讨论摄像机/观察空间(Camera/View Space)的时候,是在讨论以摄像机的视角作为场景原点时场景中所有的顶点坐标:观察矩阵把所有的世界坐标变换为相对于摄像机位置与方向的观察坐标。要定义一个摄像机,我们需要它在世界空间中的位置、观察的方向、一个指向它右测的向量以及一个指向它上方的向量。细心的读者可能已经注意到我们实际上创建了一个三个单位轴相互垂直的、以摄像机的位置为原点的坐标系。

1. 摄像机位置

注意:下面我们将会创建一系列成员变量,并在构造函数中对变量进行初始化。

获取摄像机位置很简单。摄像机位置简单来说就是世界空间中一个指向摄像机位置的向量。我们把摄像机位置设置为上一节中的那个相同的位置,添加私有成员变量cameraPos:

QVector3D cameraPos;

且在构造函数中初始化:

cameraPos(0.0f,0.0f,3.0f)

2. 摄像机方向

下一个需要的向量是摄像机的方向,这里指的是摄像机指向哪个方向。现在我们让摄像机指向场景原点:(0, 0, 0)。还记得如果将两个矢量相减,我们就能得到这两个矢量的差吗?用场景原点向量减去摄像机位置向量的结果就是摄像机的指向向量。由于我们知道摄像机指向z轴负方向,但我们希望方向向量(Direction Vector)指向摄像机的z轴正方向。如果我们交换相减的顺序,我们就会获得一个指向摄像机正z轴方向的向量:

按同样的方法,添加变量并完成初始化:

    QVector3D cameraTarget;
    QVector3D cameraDirection;
    , cameraTarget(0.0f,0.0f,0.0f)
    , cameraDirection(cameraPos-cameraTarget)

3. 右轴

我们需要的另一个向量是一个右向量(Right Vector),它代表摄像机空间的x轴的正方向。为获取右向量我们需要先使用一个小技巧:通过一个上向量(Up Vector),把上向量和第二步得到的方向向量进行叉乘。两个向量叉乘的结果会同时垂直于两向量,因此我们会得到指向x轴正方向的那个向量(如果我们交换两个向量叉乘的顺序就会得到相反的指向x轴负方向的向量):

    QVector3D cameraRight;
    , cameraRight(QVector3D::crossProduct({0.0f,1.0f,0.0f},cameraDirection))

4. 上轴

现在我们已经有了x轴向量和z轴向量,获取一个指向摄像机的正y轴向量就相对简单了:我们把右向量和方向向量进行叉乘:

    QVector3D cameraUp;
    , cameraUp(QVector3D::crossProduct(cameraDirection,cameraRight))

在叉乘和一些小技巧的帮助下,我们创建了所有构成观察/摄像机空间的向量。对于想学到更多数学原理的读者,提示一下,在线性代数中这个处理叫做格拉姆—施密特正交化(Gram-Schmidt Process)。使用这些摄像机向量我们就可以创建一个LookAt矩阵了,它在创建摄像机的时候非常有用。

使用矩阵的好处之一是如果你使用3个相互垂直(或非线性)的轴定义了一个坐标空间,你可以用这3个轴外加一个平移向量来创建一个矩阵,并且你可以用这个矩阵乘以任何向量来将其变换到那个坐标空间。这正是LookAt矩阵所做的,现在我们有了3个相互垂直的轴和一个定义摄像机空间的位置坐标,我们可以创建我们自己的LookAt矩阵了:

其中R是右向量,U是上向量,D是方向向量P是摄像机位置向量。注意,位置向量是相反的,因为我们最终希望把世界平移到与我们自身移动的相反方向。把这个LookAt矩阵作为观察矩阵可以很高效地把所有世界坐标变换到刚刚定义的观察空间。LookAt矩阵就像它的名字表达的那样:它会创建一个看着(Look at)给定目标的观察矩阵。

幸运的是,Qt已经提供了这些支持。我们要做的只是定义一个摄像机位置,一个目标位置和一个表示世界空间中的上向量的向量(我们计算右向量使用的那个上向量)。接着我们可以用Qt创建一个LookAt矩阵,我们可以把它当作我们的观察矩阵:

        QMatrix4x4 view;
        view.lookAt(cameraPos,cameraTarget,cameraUp);
        shaderProgram.setUniformValue("view",view);

lookAt函数需要一个位置、目标和上向量。它会创建一个和在上一节使用的一样的观察矩阵。

在讨论用户输入之前,我们先来做些有意思的事,把我们的摄像机在场景中旋转。我们会将摄像机的注视点保持在(0, 0, 0)。

我们需要用到一点三角学的知识来在每一帧创建一个x和z坐标,它会代表圆上的一点,我们将会使用它作为摄像机的位置。通过重新计算x和y坐标,我们会遍历圆上的所有点,这样摄像机就会绕着场景旋转了。我们预先定义这个圆的半径radius,在每次渲染迭代中使用time重新创建观察矩阵,来扩大这个圆。

        float time=QTime::currentTime().msecsSinceStartOfDay()/1000.0;
        QMatrix4x4 view;
        float radius=10.0f;
        cameraPos.setX(qSin(time)*radius);
        cameraPos.setZ(qCos(time)*radius);
        view.lookAt(cameraPos,cameraTarget,cameraUp);
        shaderProgram.setUniformValue("view",view);

如果你运行代码,应该会得到下面的结果:

代码节点

widget.h

#ifndef WIDGET_H
#define WIDGET_H

#include <QOpenGLWidget>
#include <QOpenGLExtraFunctions>
#include <QOpenGLBuffer>
#include <QOpenGLShaderProgram>
#include <QOpenGLVertexArrayObject>
#include <QOpenGLTexture>
#include <QTimer>
#include <QTime>
#include <QtMath>
class Widget : public QOpenGLWidget,public QOpenGLExtraFunctions
{
    Q_OBJECT

public:
    Widget(QWidget *parent = 0);
    ~Widget();
protected:
    virtual void initializeGL() override;
    virtual void resizeGL(int w,int h) override;
    virtual void paintGL() override;
private:
    QVector<float> vertices;
    QVector<QVector3D> cubePositions;
    QOpenGLShaderProgram shaderProgram;
    QOpenGLBuffer VBO;
    QOpenGLVertexArrayObject VAO;
    QOpenGLTexture texture;
    QOpenGLTexture texture1;
    QTimer timer;

    QVector3D cameraPos;
    QVector3D cameraTarget;
    QVector3D cameraDirection;
    QVector3D cameraRight;
    QVector3D cameraUp;
};

#endif // WIDGET_H

widget.cpp 

#include "widget.h"
#include <QtMath>

Widget::Widget(QWidget *parent)
    : QOpenGLWidget(parent)
    , VBO(QOpenGLBuffer::VertexBuffer)
    , texture(QOpenGLTexture::Target2D)
    , texture1(QOpenGLTexture::Target2D)
    , cameraPos(0.0f,0.0f,3.0f)
    , cameraTarget(0.0f,0.0f,0.0f)
    , cameraDirection(cameraPos-cameraTarget)
    , cameraRight(QVector3D::crossProduct({0.0f,1.0f,0.0f},cameraDirection))
    , cameraUp(QVector3D::crossProduct(cameraDirection,cameraRight))
{
    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, 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, 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
    };

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

    timer.setInterval(18);
    connect(&timer,&QTimer::timeout,this,static_cast<void (Widget::*)()>(&Widget::update));
    timer.start();
}

Widget::~Widget()
{
}

void Widget::initializeGL()
{
    this->initializeOpenGLFunctions();        //初始化opengl函数
    if(!shaderProgram.addShaderFromSourceFile(QOpenGLShader::Vertex,":/triangle.vert")){     //添加并编译顶点着色器
        qDebug()<<"ERROR:"<<shaderProgram.log();    //如果编译出错,打印报错信息
    }
    if(!shaderProgram.addShaderFromSourceFile(QOpenGLShader::Fragment,":/triangle.frag")){   //添加并编译片段着色器
        qDebug()<<"ERROR:"<<shaderProgram.log();    //如果编译出错,打印报错信息
    }
    if(!shaderProgram.link()){                      //链接着色器
        qDebug()<<"ERROR:"<<shaderProgram.log();    //如果链接出错,打印报错信息
    }

    QOpenGLVertexArrayObject::Binder{&VAO};

    VBO.create();       //生成VBO对象
    VBO.bind();         //将VBO绑定到当前的顶点缓冲对象(QOpenGLBuffer::VertexBuffer)中

    //将顶点数据分配到VBO中,第一个参数为数据指针,第二个参数为数据的字节长度
    VBO.allocate(vertices.data(),sizeof(float)*vertices.size());

    texture.create();
    texture.setData(QImage(":/opengl.jpg").mirrored());
    texture.setMinMagFilters(QOpenGLTexture::LinearMipMapLinear,QOpenGLTexture::Linear);
    texture.setWrapMode(QOpenGLTexture::DirectionS,QOpenGLTexture::Repeat);
    texture.setWrapMode(QOpenGLTexture::DirectionT,QOpenGLTexture::Repeat);

    texture1.create();
    texture1.setData(QImage(":/sea.jpg").mirrored());
    texture1.setMinMagFilters(QOpenGLTexture::LinearMipMapLinear,QOpenGLTexture::Linear);
    texture1.setWrapMode(QOpenGLTexture::DirectionS,QOpenGLTexture::Repeat);
    texture1.setWrapMode(QOpenGLTexture::DirectionT,QOpenGLTexture::Repeat);

    //设置顶点解析格式,并启用顶点
    shaderProgram.setAttributeBuffer("aPos", GL_FLOAT, 0, 3, sizeof(GLfloat) * 5);
    shaderProgram.enableAttributeArray("aPos");
    shaderProgram.setAttributeBuffer("aTexCoord", GL_FLOAT,sizeof(GLfloat) * 3, 2, sizeof(GLfloat) * 5);
    shaderProgram.enableAttributeArray("aTexCoord");

     this->glEnable(GL_DEPTH_TEST);
}

void Widget::resizeGL(int w, int h)
{
    this->glViewport(0,0,w,h);                //定义视口区域
}

void Widget::paintGL()
{
    this->glClearColor(0.1f,0.5f,0.7f,1.0f);  //设置清屏颜色
    this->glClear(GL_COLOR_BUFFER_BIT| GL_DEPTH_BUFFER_BIT);       //清除颜色缓存

    shaderProgram.bind();                     //使用shaderProgram着色程序
    {


        float time=QTime::currentTime().msecsSinceStartOfDay()/1000.0;
        QMatrix4x4 view;
        float radius=10.0f;
        cameraPos.setX(qSin(time)*radius);
        cameraPos.setZ(qCos(time)*radius);
        view.lookAt(cameraPos,cameraTarget,cameraUp);
        shaderProgram.setUniformValue("view",view);

        QMatrix4x4 projection;
        projection.perspective(45.0f,width()/(float)height(),0.1f,100.0f);
        shaderProgram.setUniformValue("projection",projection);

        texture.bind(0);                                    //将texture绑定到纹理单元0
        shaderProgram.setUniformValue("ourTexture",0);      //让ourTexture从纹理单元0中获取纹理数据

        texture1.bind(1);                                    //将texture绑定到纹理单元1
        shaderProgram.setUniformValue("ourTexture1",1);      //让ourTexture从纹理单元1中获取纹理数据

        QOpenGLVertexArrayObject::Binder{&VAO};


        for(unsigned int i = 0; i < 10; i++){
            QMatrix4x4 model;
            model.translate(cubePositions[i]);
            model.rotate(180*time+i*20.0f,QVector3D(1.0f,0.5f,0.3f));
            shaderProgram.setUniformValue("model",model);
            this->glDrawArrays(GL_TRIANGLES, 0, 36);
        }

    }
}

着色器无变动

视角移动

上面我们通过定时器实现了摄像机围绕原点在水平面进行公转,接下来我们实现用鼠标来控制摄像机的视角移动

为了能够改变视角,我们需要根据鼠标的输入改变cameraDirection向量。然而,根据鼠标移动改变方向向量有点复杂,需要一些三角学知识。如果你对三角学知之甚少,别担心,你可以跳过这一部分,直接复制粘贴我们的代码;当你想了解更多的时候再回来看。

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

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

对于我们的摄像机系统来说,我们只关心俯仰角和偏航角,所以我们不会讨论滚转角。给定一个俯仰角和偏航角,我们可以把它们转换为一个代表新的方向向量的3D向量。

我们首先在头文件中创建这两个变量(我们使用弧度制),另外,为了能够控制鼠标的灵敏度,我们再增加一个变量sensitivity(把它们放到cameraPos之前):

    double yaw;             //偏航角
    double pitch;           //俯视角
    double sensitivity;     //鼠标灵敏度

再在构造函数中进行初始化(同样需要注意顺序):

    , yaw(0.0)
    , pitch(0.0)
    , sensitivity(0.01)

俯仰角和偏航角转换为方向向量的处理需要一些三角学知识,我们先从最基本的情况开始:

如果我们把斜边边长定义为1,我们就能知道邻边的长度是,它的对边是。这样我们获得了能够得到x和y方向长度的通用公式,它们取决于所给的角度。我们使用它来计算方向向量的分量:

这个三角形看起来和前面的三角形很像,所以如果我们想象自己在xz平面上,看向y轴,我们可以基于第一个三角形计算来计算它的长度/y方向的强度(Strength)(我们往上或往下看多少)。从图中我们可以看到对于一个给定俯仰角的y值等于 sin (pitch),而x、z的值等于 cos(pitch)

再来看偏航角:

就像俯仰角的三角形一样,我们可以看到x分量取决于cos(yaw)的值,z值同样取决于偏航角的正弦值。把这个加到前面的值中,会得到基于俯仰角和偏航角方向向量的计算公式:

    cameraDirection.setX(cos(yaw)*cos(pitch));
    cameraDirection.setY(sin(pitch));
    cameraDirection.setZ(sin(yaw)*cos(pitch));

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

鼠标输入

偏航角和俯仰角是通过鼠标(或手柄)移动获得的,水平的移动影响偏航角,竖直的移动影响俯仰角。它的原理就是,储存上一帧鼠标的位置,在当前帧中我们当前计算鼠标位置与上一帧的位置相差多少。如果水平/竖直差别越大那么俯仰角或偏航角就改变越大,也就是摄像机需要移动更多的距离。

为了让待会我们的摄像机能够正确的看到图形,我们在构造函数中设置一下摄像机的初始位置和方向:

 , cameraPos(-5.0f,0.0f,0.0f)
 , cameraDirection(cos(yaw)*cos(pitch), sin(pitch), 

并且去掉之前定时器与cameraPos的绑定,修改view矩阵为:

QMatrix4x4 view;
view.lookAt(cameraPos,cameraPos+cameraDirection,cameraUp);
shaderProgram.setUniformValue("view",view);

 接下来我们要告诉Qt,它应该实时的捕获光标的移动位置。捕捉光标表示的是,如果焦点在你的程序上(译注:即表示你正在操作这个程序,Windows中拥有焦点的程序标题栏通常是有颜色的那个,而失去焦点的程序标题栏则是灰色的),光标应该停留在窗口中(除非程序失去焦点或者退出)。我们可以在构造函数中用一个简单地配置调用来完成:

setMouseTracking(true);         //开启鼠标追踪:Qt默认不会实时监控鼠标移动

我们要处理Qt窗口的鼠标移动事件,只需要实现虚函数void mouseMoveEvent(QMouseEvent *event)即可。

首先添加protected声明:

    virtual void mouseMoveEvent(QMouseEvent *event) override;

 在处理FPS风格摄像机的鼠标输入的时候,我们必须在最终获取方向向量之前做下面这几步:

  1. 计算鼠标的偏移量。
  2. 把偏移量添加到摄像机的俯仰角和偏航角中。
  3. 对偏航角和俯仰角进行最大和最小值的限制。
  4. 计算方向向量。

我们在cpp中进行实现:

void Widget::mouseMoveEvent(QMouseEvent *event)
{
    float xoffset = event->x() - rect().center().x();
    float yoffset = rect().center().y() - event->y(); // 注意这里是相反的,因为y坐标是从底部往顶部依次增大的
    xoffset *= sensitivity;
    yoffset *= sensitivity;
    yaw   += xoffset;
    pitch += yoffset;
    if(pitch >= M_PI/2)                          //将俯视角限制到[-90°,90°]
        pitch =  (M_PI)/2-0.1;
    if(pitch <= -M_PI/2)
        pitch = -(M_PI)/2+0.1;
    cameraDirection.setX(cos(yaw)*cos(pitch));
    cameraDirection.setY(sin(pitch));
    cameraDirection.setZ(sin(yaw)*cos(pitch));
    QCursor::setPos(geometry().center());       //将鼠标复原到窗口中央
}

首先我们计算鼠标位置与窗口中心的偏移量。接下来我们把偏移量加到全局变量pitch和yaw上

另外我们需要给摄像机添加一些限制,这样摄像机就不会发生奇怪的移动了(这样也会避免一些奇怪的问题)。对于俯仰角,(在90度时视角会发生逆转,所以我们把(PI/2-0.1)弧度作为极限。这样能够保证用户只能看到天空或脚下,但是不能超越这个限制。我们可以在值超过限制的时候将其改为极限值来实现,注意我们没有给偏航角设置限制,这是因为我们不希望限制用户的水平旋转。当然,给偏航角设置限制也很容易,如果你愿意可以自己实现。

接下来就是通过俯仰角和偏航角来计算以得到真正的方向向量。

最后一步,我们将鼠标的位置设置为窗口中心,这样鼠标就永远不会移到屏幕之外了。

然后我们运行代码,你会发现几个问题:

  • 鼠标的起始位置不在窗口中心,初次进入窗口会有较大的偏移。
  • 鼠标的光标没有进行隐藏

我们可以在initializeGL()函数中修复这两个问题(之所以不在构造函数中,是因为窗口需要在显示的时候才能确定位置,initializeGL()的调用刚好是窗口显示之后,调用绘图函数之前),我们只需要添加两行代码:

this->setCursor(Qt::BlankCursor);       //隐藏鼠标光标
QCursor::setPos(geometry().center());   //设置鼠标位置为窗口矩形区域的中心

再次运行,移动鼠标,你会看到这样的效果:

代码节点:

widget.h

#ifndef WIDGET_H
#define WIDGET_H

#include <QOpenGLWidget>
#include <QOpenGLExtraFunctions>
#include <QOpenGLBuffer>
#include <QOpenGLShaderProgram>
#include <QOpenGLVertexArrayObject>
#include <QOpenGLTexture>
#include <QTimer>
#include <QTime>
#include <QtMath>
#include <QMouseEvent>
class Widget : public QOpenGLWidget,public QOpenGLExtraFunctions
{
    Q_OBJECT

public:
    Widget(QWidget *parent = 0);
    ~Widget();
protected:
    virtual void initializeGL() override;
    virtual void resizeGL(int w,int h) override;
    virtual void paintGL() override;

    virtual void mouseMoveEvent(QMouseEvent *event) override;

private:
    QVector<float> vertices;
    QVector<QVector3D> cubePositions;
    QOpenGLShaderProgram shaderProgram;
    QOpenGLBuffer VBO;
    QOpenGLVertexArrayObject VAO;
    QOpenGLTexture texture;
    QOpenGLTexture texture1;
    QTimer timer;

    double yaw;             //偏航角
    double pitch;           //俯视角
    double sensitivity;     //鼠标灵敏度

    QVector3D cameraPos;
    QVector3D cameraTarget;
    QVector3D cameraDirection;
    QVector3D cameraRight;
    QVector3D cameraUp;
};

#endif // WIDGET_H

widget.cpp

#include "widget.h"
#include <QtMath>

Widget::Widget(QWidget *parent)
    : QOpenGLWidget(parent)
    , VBO(QOpenGLBuffer::VertexBuffer)
    , texture(QOpenGLTexture::Target2D)
    , texture1(QOpenGLTexture::Target2D)
    , yaw(0.0)
    , pitch(0.0)
    , sensitivity(0.01)
    , cameraPos(-5.0f,0.0f,0.0f)
    , cameraTarget(0.0f,0.0f,0.0f)
    , cameraDirection(cos(yaw)*cos(pitch), sin(pitch), sin(yaw)*cos(pitch))
    , cameraRight(QVector3D::crossProduct({0.0f,1.0f,0.0f},cameraDirection))
    , cameraUp(QVector3D::crossProduct(cameraDirection,cameraRight))
{
    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, 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, 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
    };

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

    timer.setInterval(18);
    connect(&timer,&QTimer::timeout,this,static_cast<void (Widget::*)()>(&Widget::update));
    timer.start();

    setMouseTracking(true);         //开启鼠标追踪:Qt默认不会实时监控鼠标移动
}

Widget::~Widget()
{
}

void Widget::initializeGL()
{
    this->initializeOpenGLFunctions();        //初始化opengl函数
    if(!shaderProgram.addShaderFromSourceFile(QOpenGLShader::Vertex,":/triangle.vert")){     //添加并编译顶点着色器
        qDebug()<<"ERROR:"<<shaderProgram.log();    //如果编译出错,打印报错信息
    }
    if(!shaderProgram.addShaderFromSourceFile(QOpenGLShader::Fragment,":/triangle.frag")){   //添加并编译片段着色器
        qDebug()<<"ERROR:"<<shaderProgram.log();    //如果编译出错,打印报错信息
    }
    if(!shaderProgram.link()){                      //链接着色器
        qDebug()<<"ERROR:"<<shaderProgram.log();    //如果链接出错,打印报错信息
    }

    QOpenGLVertexArrayObject::Binder{&VAO};

    VBO.create();       //生成VBO对象
    VBO.bind();         //将VBO绑定到当前的顶点缓冲对象(QOpenGLBuffer::VertexBuffer)中

    //将顶点数据分配到VBO中,第一个参数为数据指针,第二个参数为数据的字节长度
    VBO.allocate(vertices.data(),sizeof(float)*vertices.size());

    texture.create();
    texture.setData(QImage(":/opengl.jpg").mirrored());
    texture.setMinMagFilters(QOpenGLTexture::LinearMipMapLinear,QOpenGLTexture::Linear);
    texture.setWrapMode(QOpenGLTexture::DirectionS,QOpenGLTexture::Repeat);
    texture.setWrapMode(QOpenGLTexture::DirectionT,QOpenGLTexture::Repeat);

    texture1.create();
    texture1.setData(QImage(":/sea.jpg").mirrored());
    texture1.setMinMagFilters(QOpenGLTexture::LinearMipMapLinear,QOpenGLTexture::Linear);
    texture1.setWrapMode(QOpenGLTexture::DirectionS,QOpenGLTexture::Repeat);
    texture1.setWrapMode(QOpenGLTexture::DirectionT,QOpenGLTexture::Repeat);

    //设置顶点解析格式,并启用顶点
    shaderProgram.setAttributeBuffer("aPos", GL_FLOAT, 0, 3, sizeof(GLfloat) * 5);
    shaderProgram.enableAttributeArray("aPos");
    shaderProgram.setAttributeBuffer("aTexCoord", GL_FLOAT,sizeof(GLfloat) * 3, 2, sizeof(GLfloat) * 5);
    shaderProgram.enableAttributeArray("aTexCoord");

    this->glEnable(GL_DEPTH_TEST);          //开启深度测试
    this->setCursor(Qt::BlankCursor);       //隐藏鼠标光标
    QCursor::setPos(geometry().center());   //设置鼠标位置为窗口矩形区域的中心
}

void Widget::resizeGL(int w, int h)
{
    this->glViewport(0,0,w,h);                //定义视口区域
}

void Widget::paintGL()
{
    this->glClearColor(0.1f,0.5f,0.7f,1.0f);  //设置清屏颜色
    this->glClear(GL_COLOR_BUFFER_BIT| GL_DEPTH_BUFFER_BIT);       //清除颜色缓存

    shaderProgram.bind();                     //使用shaderProgram着色程序
    {

        float time=QTime::currentTime().msecsSinceStartOfDay()/1000.0;
        QMatrix4x4 view;
        view.lookAt(cameraPos,cameraPos+cameraDirection,cameraUp);
        shaderProgram.setUniformValue("view",view);

        QMatrix4x4 projection;
        projection.perspective(45.0f,width()/(float)height(),0.1f,100.0f);
        shaderProgram.setUniformValue("projection",projection);

        texture.bind(0);                                    //将texture绑定到纹理单元0
        shaderProgram.setUniformValue("ourTexture",0);      //让ourTexture从纹理单元0中获取纹理数据

        texture1.bind(1);                                    //将texture绑定到纹理单元1
        shaderProgram.setUniformValue("ourTexture1",1);      //让ourTexture从纹理单元1中获取纹理数据

        QOpenGLVertexArrayObject::Binder{&VAO};


        for(unsigned int i = 0; i < 10; i++){
            QMatrix4x4 model;
            model.translate(cubePositions[i]);
            model.rotate(180*time+i*20.0f,QVector3D(1.0f,0.5f,0.3f));
            shaderProgram.setUniformValue("model",model);
            this->glDrawArrays(GL_TRIANGLES, 0, 36);
        }

    }
}

void Widget::mouseMoveEvent(QMouseEvent *event)
{
    float xoffset = event->x() - rect().center().x();
    float yoffset = rect().center().y() - event->y(); // 注意这里是相反的,因为y坐标是从底部往顶部依次增大的
    xoffset *= sensitivity;
    yoffset *= sensitivity;
    yaw   += xoffset;
    pitch += yoffset;
    if(pitch >= M_PI/2)                          //将俯视角限制到[-90°,90°]
        pitch =  (M_PI)/2-0.1;
    if(pitch <= -M_PI/2)
        pitch = -(M_PI)/2+0.1;
    cameraDirection.setX(cos(yaw)*cos(pitch));
    cameraDirection.setY(sin(pitch));
    cameraDirection.setZ(sin(yaw)*cos(pitch));
    QCursor::setPos(geometry().center());       //将鼠标复原到窗口中央
}

着色器无变动

自由移动

我们目前已经能够用鼠标来控制视角了,但是让我们自己移动摄像机会更有趣!

由于Qt的键盘处理事件响应速度不是很快,如果直接使用Qt的keyEvent会有非常严重的卡顿感,这不是因为QT的性能低,而是Qt在封装键盘事件的时候,并没有想到有人需要高频率的键盘响应事件,因此限制了键盘的响应频率,且没有留有响应间隔的设置选项。

坑我已经踩了,下面就不再演示是怎样的坑,直接从修改后的代码说起。

而我们要来实现这个一个高频率的键盘响应事件,很简单,只需要通过一个定时器就好,不过我们这里不使用QTimer,而是直接使用QObject的timerEvent,这也是一个定时器,与QTimer相比,更加轻量高效。

这里我们需要利用三个事件来完成摄像机的自由移动:

    virtual void keyPressEvent(QKeyEvent *event) override;      //记录按键信息,开启定时器
    virtual void keyReleaseEvent(QKeyEvent *event) override;    //消除按键信息,关闭定时器
    virtual void timerEvent(QTimerEvent *event) override;       //处理按键操作

另外我们还需要借助三个变量:

    float moveSpeed;    //控制移动速度
    QSet<int> keys;     //记录当前被按下按键的集合
    int timeId;         //定时器id:此定时器用于完成键盘移动事件

之所以用集合是为了让多个按键同时按下时能正确的移动,比如左上。

并在构造函数中初始化:

    , moveSpeed(0.1f)
    , timeId(0)

然后实现三个事件处理函数:

void Widget::keyPressEvent(QKeyEvent *event)
{
    //isAutoRepeat用于判断此按键的来源是否是长按
    keys.insert(event->key());                              //添加按键
    if(!event->isAutoRepeat()&&timeId==0){                  //如果定时器未启动,则启动定时器
        timeId=startTimer(1);
    }
}

void Widget::keyReleaseEvent(QKeyEvent *event)
{
    keys.remove(event->key());
    if(!event->isAutoRepeat()&&timeId!=0&&keys.empty()){    //当没有按键按下且定时器正在运行,才关闭定时器
         killTimer(timeId);
         timeId=0;                                          //重置定时器id
    }
}


void Widget::timerEvent(QTimerEvent *event)                //键盘操作
{
    float cameraSpeed = moveSpeed;
    if (keys.contains(Qt::Key_W))                           //前
        cameraPos+=cameraSpeed * cameraDirection;
    if (keys.contains(Qt::Key_S))                           //后
        cameraPos -= cameraSpeed * cameraDirection;
    if (keys.contains(Qt::Key_A))                           //左
        cameraPos-=QVector3D::crossProduct(cameraDirection,cameraUp)*cameraSpeed;
    if (keys.contains(Qt::Key_D))                           //右
        cameraPos+=QVector3D::crossProduct(cameraDirection,cameraUp)*cameraSpeed;
    if (keys.contains(Qt::Key_Space))                       //上浮
        cameraPos.setY(cameraPos.y()+cameraSpeed);
    if (keys.contains(Qt::Key_Shift))                       //下沉
        cameraPos.setY(cameraPos.y()-cameraSpeed);
}

当我们按下WASD、空格、shift键的任意一个,摄像机的位置都会相应更新。如果我们希望向前或向后移动,我们就把位置向量加上或减去方向向量。如果我们希望向左右移动,我们使用叉乘来创建一个右向量(Right Vector),并沿着它相应移动就可以了。这样就创建了使用摄像机时熟悉的横移(Strafe)效果。

现在你就应该能够移动摄像机了,虽然移动速度和系统有关,你可能会需要调整一下cameraSpeed。

移动速度

目前我们的移动速度是个常量。理论上没什么问题,但是实际情况下根据处理器的能力不同,有些人可能会比其他人每秒绘制更多帧,也就是以更高的频率调用定时器处理函数。结果就是,根据配置的不同,有些人可能移动很快,而有些人会移动很慢。当你发布你的程序的时候,你必须确保它在所有硬件上移动速度都一样。

图形程序和游戏通常会跟踪一个时间差(Deltatime)变量,它储存了渲染上一帧所用的时间。我们把所有速度都去乘以deltaTime值。结果就是,如果我们的deltaTime很大,就意味着上一帧的渲染花费了更多时间,所以这一帧的速度需要变得更高来平衡渲染所花去的时间。使用这种方法时,无论你的电脑快还是慢,摄像机的速度都会相应平衡,这样每个用户的体验就都一样了。

我们跟踪两个成员变量来计算出deltaTime值:

    float deltaTime;    // 当前帧与上一帧的时间差
    float lastFrame;    // 上一帧的时间

在每一帧中我们计算出新的deltaTime以备后用(paintGL函数中)。

    deltaTime = time - lastFrame;                           //在此处更新时间差
    lastFrame = time;

 现在我们有了deltaTime,在计算速度的时候可以将其考虑进去了:

    float cameraSpeed = moveSpeed * deltaTime;

然后运行一下,可能移动速度比较慢,我们更改一下移动速度的初始值:

    , moveSpeed(2.5f)

然后我们再运行一下程序,你将会得到一个完美的Qt摄像机!

由于GIF和录屏软件的录制效果差强人意,所以这里就不再进行演示

代码节点

widget.h

#ifndef WIDGET_H
#define WIDGET_H

#include <QOpenGLWidget>
#include <QOpenGLExtraFunctions>
#include <QOpenGLBuffer>
#include <QOpenGLShaderProgram>
#include <QOpenGLVertexArrayObject>
#include <QOpenGLTexture>
#include <QTimer>
#include <QTime>
#include <QtMath>
#include <QKeyEvent>
class Widget : public QOpenGLWidget,public QOpenGLExtraFunctions
{
    Q_OBJECT

public:
    Widget(QWidget *parent = 0);
    ~Widget();
protected:
    virtual void initializeGL() override;
    virtual void resizeGL(int w,int h) override;
    virtual void paintGL() override;



    virtual void mouseMoveEvent(QMouseEvent *event) override;
    virtual void keyPressEvent(QKeyEvent *event) override;      //记录按键信息,开启定时器
    virtual void keyReleaseEvent(QKeyEvent *event) override;    //消除按键信息,关闭定时器
    virtual void timerEvent(QTimerEvent *event) override;       //处理按键操作


private:
    QVector<float> vertices;
    QVector<QVector3D> cubePositions;
    QOpenGLShaderProgram shaderProgram;
    QOpenGLBuffer VBO;
    QOpenGLVertexArrayObject VAO;
    QOpenGLTexture texture;
    QOpenGLTexture texture1;
    QTimer timer;

    float yaw;                  //偏航角
    float pitch;                //俯视角
    float sensitivity;          //鼠标灵敏度

    QVector3D cameraPos;        //摄像机初始位置
    QVector3D cameraTarget;     //观测点(不使用)
    QVector3D cameraDirection;  //摄像机方向
    QVector3D cameraRight;      //摄像机右向量
    QVector3D cameraUp;         //摄像机上向量

    float moveSpeed;    //控制移动速度
    QSet<int> keys;     //记录当前被按下按键的集合
    int timeId;         //定时器id:此定时器用于完成键盘移动事件
    float deltaTime;    // 当前帧与上一帧的时间差
    float lastFrame;    // 上一帧的时间
};

#endif // WIDGET_H

widget.cpp

#include "widget.h"
#include <QtMath>

Widget::Widget(QWidget *parent)
    : QOpenGLWidget(parent)
    , VBO(QOpenGLBuffer::VertexBuffer)
    , texture(QOpenGLTexture::Target2D)
    , texture1(QOpenGLTexture::Target2D)
    , yaw(0)
    , pitch(0)
    , sensitivity(0.005f)
    , cameraPos(-5.0f,0.0f,0.0f)
    , cameraTarget(0.0f,0.0f,0.0f)
    , cameraDirection(cos(yaw)*cos(pitch), sin(pitch), sin(yaw)*cos(pitch))
    , cameraRight(QVector3D::crossProduct({0.0f,1.0f,0.0f},cameraDirection))
    , cameraUp(QVector3D::crossProduct(cameraDirection,cameraRight))
    , moveSpeed(0.5f)
    , timeId(0)
{
    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, 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, 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
    };
    cubePositions = {
      { 0.0f,  0.0f,  0.0f  },
      { 2.0f,  5.0f, -15.0f },
      {-1.5f, -2.2f, -2.5f  },
      {-3.8f, -2.0f, -12.3f },
      { 2.4f, -0.4f, -3.5f  },
      {-1.7f,  3.0f, -7.5f  },
      { 1.3f, -2.0f, -2.5f  },
      { 1.5f,  2.0f, -2.5f  },
      { 1.5f,  0.2f, -1.5f  },
      {-1.3f,  1.0f, -1.5f  },
    };

    timer.setInterval(18);
    connect(&timer,&QTimer::timeout,this,static_cast<void (Widget::*)()>(&Widget::update));
    timer.start();

    setMouseTracking(true);         //开启鼠标追踪:Qt默认不会实时监控鼠标移动
}

Widget::~Widget()
{
    makeCurrent();
    texture.destroy();
    texture1.destroy();
    doneCurrent();
}

void Widget::initializeGL()
{
    this->initializeOpenGLFunctions();        //初始化opengl函数
    if(!shaderProgram.addShaderFromSourceFile(QOpenGLShader::Vertex,":/triangle.vert")){     //添加并编译顶点着色器
        qDebug()<<"ERROR:"<<shaderProgram.log();    //如果编译出错,打印报错信息
    }
    if(!shaderProgram.addShaderFromSourceFile(QOpenGLShader::Fragment,":/triangle.frag")){   //添加并编译片段着色器
        qDebug()<<"ERROR:"<<shaderProgram.log();    //如果编译出错,打印报错信息
    }
    if(!shaderProgram.link()){                      //链接着色器
        qDebug()<<"ERROR:"<<shaderProgram.log();    //如果链接出错,打印报错信息
    }

    QOpenGLVertexArrayObject::Binder{&VAO};

    VBO.create();       //生成VBO对象
    VBO.bind();         //将VBO绑定到当前的顶点缓冲对象(QOpenGLBuffer::VertexBuffer)中

    //将顶点数据分配到VBO中,第一个参数为数据指针,第二个参数为数据的字节长度
    VBO.allocate(vertices.data(),sizeof(float)*vertices.size());

    texture.create();
    texture.setData(QImage(":/opengl.jpg").mirrored());
    texture.setMinMagFilters(QOpenGLTexture::LinearMipMapLinear,QOpenGLTexture::Linear);
    texture.setWrapMode(QOpenGLTexture::DirectionS,QOpenGLTexture::Repeat);
    texture.setWrapMode(QOpenGLTexture::DirectionT,QOpenGLTexture::Repeat);

    texture1.create();
    texture1.setData(QImage(":/sea.jpg").mirrored());
    texture1.setMinMagFilters(QOpenGLTexture::LinearMipMapLinear,QOpenGLTexture::Linear);
    texture1.setWrapMode(QOpenGLTexture::DirectionS,QOpenGLTexture::Repeat);
    texture1.setWrapMode(QOpenGLTexture::DirectionT,QOpenGLTexture::Repeat);

    //设置顶点解析格式,并启用顶点
    shaderProgram.setAttributeBuffer("aPos", GL_FLOAT, 0, 3, sizeof(GLfloat) * 5);
    shaderProgram.enableAttributeArray("aPos");
    shaderProgram.setAttributeBuffer("aTexCoord", GL_FLOAT,sizeof(GLfloat) * 3, 2, sizeof(GLfloat) * 5);
    shaderProgram.enableAttributeArray("aTexCoord");

    this->glEnable(GL_DEPTH_TEST);

    QCursor::setPos(geometry().center());       //将鼠标移动窗口中央
    setCursor(Qt::BlankCursor);                 //隐藏鼠标光标
}

void Widget::resizeGL(int w, int h)
{
    this->glViewport(0,0,w,h);                  //定义视口区域
}

void Widget::paintGL()
{
    this->glClearColor(0.1f,0.5f,0.7f,1.0f);                       //设置清屏颜色
    this->glClear(GL_COLOR_BUFFER_BIT| GL_DEPTH_BUFFER_BIT);       //清除颜色缓存和深度缓存
    float time=QTime::currentTime().msecsSinceStartOfDay()/1000.0;
    shaderProgram.bind();                                          //使用shaderProgram着色程序
    {
        QMatrix4x4 view;
        view.lookAt(cameraPos,cameraPos+cameraDirection,cameraUp);
        shaderProgram.setUniformValue("view",view);

        QMatrix4x4 projection;
        projection.perspective(45.0f,width()/(float)height(),0.1f,100.0f);
        shaderProgram.setUniformValue("projection",projection);

        texture.bind(0);                                    //将texture绑定到纹理单元0
        shaderProgram.setUniformValue("ourTexture",0);      //让ourTexture从纹理单元0中获取纹理数据

        texture1.bind(1);                                    //将texture绑定到纹理单元1
        shaderProgram.setUniformValue("ourTexture1",1);      //让ourTexture从纹理单元1中获取纹理数据
        QOpenGLVertexArrayObject::Binder{&VAO};

        for(unsigned int i = 0; i < 10; i++){
            QMatrix4x4 model;
            model.translate(cubePositions[i]);
            model.rotate(180*time+i*20.0f,QVector3D(1.0f,0.5f,0.3f));
            shaderProgram.setUniformValue("model",model);
            this->glDrawArrays(GL_TRIANGLES, 0, 36);
        }
    }


    deltaTime = time - lastFrame;                           //在此处更新时间差
    lastFrame = time;
}

void Widget::keyPressEvent(QKeyEvent *event)
{
    //isAutoRepeat用于判断此按键的来源是否是长按
    keys.insert(event->key());                              //添加按键
    if(!event->isAutoRepeat()&&timeId==0){                  //如果定时器未启动,则启动定时器
        timeId=startTimer(1);
    }
}

void Widget::keyReleaseEvent(QKeyEvent *event)
{
    keys.remove(event->key());
    if(!event->isAutoRepeat()&&timeId!=0&&keys.empty()){    //当没有按键按下且定时器正在运行,才关闭定时器
         killTimer(timeId);
         timeId=0;                                          //重置定时器id
    }
}

void Widget::timerEvent(QTimerEvent *event)                //键盘操作
{
    float cameraSpeed = moveSpeed * deltaTime;
    if (keys.contains(Qt::Key_W))                           //前
        cameraPos+=cameraSpeed * cameraDirection;
    if (keys.contains(Qt::Key_S))                           //后
        cameraPos -= cameraSpeed * cameraDirection;
    if (keys.contains(Qt::Key_A))                           //左
        cameraPos-=QVector3D::crossProduct(cameraDirection,cameraUp)*cameraSpeed;
    if (keys.contains(Qt::Key_D))                           //右
        cameraPos+=QVector3D::crossProduct(cameraDirection,cameraUp)*cameraSpeed;
    if (keys.contains(Qt::Key_Space))                       //上浮
        cameraPos.setY(cameraPos.y()+cameraSpeed);
    if (keys.contains(Qt::Key_Shift))                       //下沉
        cameraPos.setY(cameraPos.y()-cameraSpeed);
}

void Widget::mouseMoveEvent(QMouseEvent *event)
{

    float xoffset = event->x() - rect().center().x();
    float yoffset = rect().center().y() - event->y(); // 注意这里是相反的,因为y坐标是从底部往顶部依次增大的
    xoffset *= sensitivity;
    yoffset *= sensitivity;
    yaw   += xoffset;
    pitch += yoffset;
    if(pitch > 1.55)         //将俯视角限制到[-89°,89°],89°约等于1.55
        pitch =  1.55;
    if(pitch < -1.55)
        pitch = -1.55;
    cameraDirection.setX(cos(yaw)*cos(pitch));
    cameraDirection.setY(sin(pitch));
    cameraDirection.setZ(sin(yaw)*cos(pitch));
    QCursor::setPos(geometry().center());       //将鼠标移动窗口中央
}

摄像机类

虽然我们已经制作出了一个完美的摄像机,但是你会发现,因为摄像机的缘故,导致我们的widget(代码)变得非常庞大,因此我们可以把camera的操作封装起来,单独作为一个类,封装与Qt机制有关。

另外上面的摄像机还存在一些问题,下面封装会解决这些问题,封装与Qt机制有关,因此不细说。

camera.h

#ifndef CAMERA_H
#define CAMERA_H

#include <QSet>
#include <QVector3D>
#include <QEvent>
#include <QWidget>
#include <QtMath>
#include <QMatrix4x4>
#include <QKeyEvent>
#include <QTime>


class Camera
{
public:
    Camera(QWidget *widget);

    float getMoveSpeed() const;
    void setMoveSpeed(float value);

    float getSensitivity() const;
    void setSensitivity(float value);

    float getYaw() const;
    void setYaw(float value);

    float getPitch() const;
    void setPitch(float value);

    QVector3D getCameraPos() const;
    void setCameraPos(const QVector3D &value);

    void init();                    //初始化摄像机

    void handle(QEvent *event);     //处理窗口事件

    QMatrix4x4 getView() const;     //获取观察矩阵

private:
    QWidget *widget;

    float yaw;                  //偏航角
    float pitch;                //俯视角
    float sensitivity;          //鼠标灵敏度

    QVector3D cameraPos;        //摄像机初始位置

    QVector3D cameraDirection;  //摄像机方向
    QVector3D cameraRight;      //摄像机右向量
    QVector3D cameraUp;         //摄像机上向量

    float moveSpeed;    //控制移动速度
    QSet<int> keys;     //记录当前被按下按键的集合

    int timeId;         //定时器id:此定时器用于完成键盘移动事件
    float deltaTime;    // 当前帧与上一帧的时间差
    float lastFrame;    // 上一帧的时间
    
    QMatrix4x4 view;    //观察矩阵

};

#endif // CAMERA_H

camera.cpp


#include "camera.h"

Camera::Camera(QWidget *widget)
    : widget(widget)
    , yaw(0)
    , pitch(0)
    , sensitivity(0.005f)
    , cameraPos(-5.0f,0.0f,0.0f)
    , cameraDirection(cos(yaw)*cos(pitch), sin(pitch), sin(yaw)*cos(pitch))
    , cameraRight(QVector3D::crossProduct({0.0f,1.0f,0.0f},cameraDirection))
    , cameraUp(QVector3D::crossProduct(cameraDirection,cameraRight))
    , moveSpeed(0.5f)
    , timeId(0)
{
}

float Camera::getMoveSpeed() const
{
    return moveSpeed;
}

void Camera::setMoveSpeed(float value)
{
    moveSpeed = value;
}

float Camera::getSensitivity() const
{
    return sensitivity;
}

void Camera::setSensitivity(float value)
{
    sensitivity = value;
}

float Camera::getYaw() const
{
    return yaw;
}

void Camera::setYaw(float value)
{
    yaw = value;
}

float Camera::getPitch() const
{
    return pitch;
}

void Camera::setPitch(float value)
{
    pitch = value;
}

QVector3D Camera::getCameraPos() const
{
    return cameraPos;
}

void Camera::setCameraPos(const QVector3D &value)
{
    cameraPos = value;
}

void Camera::handle(QEvent *e)
{
    if(e->type()==QEvent::MouseMove){
        QMouseEvent *event=static_cast<QMouseEvent*>(e);
        float xoffset = event->x() - widget->rect().center().x();
        float yoffset = widget->rect().center().y() - event->y(); // 注意这里是相反的,因为y坐标是从底部往顶部依次增大的
        xoffset *= sensitivity;
        yoffset *= sensitivity;
        yaw   += xoffset;
        pitch += yoffset;
        if(pitch > 1.55)         //将俯视角限制到[-89°,89°],89°约等于1.55
            pitch =  1.55;
        if(pitch < -1.55)
            pitch = -1.55;
        cameraDirection.setX(cos(yaw)*cos(pitch));
        cameraDirection.setY(sin(pitch));
        cameraDirection.setZ(sin(yaw)*cos(pitch));
        view.setToIdentity();
        view.lookAt(cameraPos,cameraPos+cameraDirection,cameraUp);
        QCursor::setPos(widget->geometry().center());       //将鼠标移动窗口中央
    }
    else if(e->type()==QEvent::Timer){
        float cameraSpeed = moveSpeed * deltaTime;
        if (keys.contains(Qt::Key_W))                           //前
            cameraPos+=cameraSpeed * cameraDirection;
        if (keys.contains(Qt::Key_S))                           //后
            cameraPos -= cameraSpeed * cameraDirection;
        if (keys.contains(Qt::Key_A))                           //左
            cameraPos-=QVector3D::crossProduct(cameraDirection,cameraUp)*cameraSpeed;
        if (keys.contains(Qt::Key_D))                           //右
            cameraPos+=QVector3D::crossProduct(cameraDirection,cameraUp)*cameraSpeed;
        if (keys.contains(Qt::Key_Space))                       //上浮
            cameraPos.setY(cameraPos.y()+cameraSpeed);
        if (keys.contains(Qt::Key_Shift))                       //下沉
            cameraPos.setY(cameraPos.y()-cameraSpeed);

        view.setToIdentity();
        view.lookAt(cameraPos,cameraPos+cameraDirection,cameraUp);
    }
    else if(e->type()==QEvent::KeyPress){
        //isAutoRepeat用于判断此按键的来源是否是长按
        QKeyEvent *event=static_cast<QKeyEvent*>(e);
        keys.insert(event->key());                              //添加按键
        if(!event->isAutoRepeat()&&timeId==0){                  //如果定时器未启动,则启动定时器
            timeId=widget->startTimer(1);
        }
    }
    else if(e->type()==QEvent::KeyRelease){
        QKeyEvent *event=static_cast<QKeyEvent*>(e);
        keys.remove(event->key());
        if(!event->isAutoRepeat()&&timeId!=0&&keys.empty()){    //当没有按键按下且定时器正在运行,才关闭定时器
             widget->killTimer(timeId);
             timeId=0;                                          //重置定时器id
        }
    }
    else if(e->type()==QEvent::UpdateRequest){
        float time=QTime::currentTime().msecsSinceStartOfDay()/1000.0;
        deltaTime = time - lastFrame;                           //在此处更新时间差
        lastFrame = time;
    }
    else if(e->type()==QEvent::FocusIn){
        widget->setCursor(Qt::BlankCursor);             //隐藏鼠标光标
        QCursor::setPos(widget->geometry().center());   //将鼠标移动窗口中央
        widget->setMouseTracking(true);                 //开启鼠标追踪
    }
    else if(e->type()==QEvent::FocusOut){
        widget->setCursor(Qt::ArrowCursor);   //恢复鼠标光标
        widget->setMouseTracking(false);      //关闭鼠标追踪
    }
}

void Camera::init()
{
    view.lookAt(cameraPos,cameraPos+cameraDirection,cameraUp);
    widget->activateWindow();                 //激活窗口
    widget->setFocus();

}

QMatrix4x4 Camera::getView() const
{
    return view;
}

这个类需要通过一个QWidget*作为参数来进行构造。之后我们可以在initializeGL函数中调用camera的init函数进行初始化。另外我们处理QWidget的事件处理,我们可以直接在事件分发函数(event)中对窗口事件做一个统一的处理。

同样我们只需实现虚函数即可:

    virtual bool event(QEvent *e) override;
bool Widget::event(QEvent *e)
{
    camera.handle(e);
    return QWidget::event(e);   //调用父类的事件分发函数
}

代码节点

widget.h

#ifndef WIDGET_H
#define WIDGET_H

#include "camera.h"

#include <QOpenGLWidget>
#include <QOpenGLExtraFunctions>
#include <QOpenGLBuffer>
#include <QOpenGLShaderProgram>
#include <QOpenGLVertexArrayObject>
#include <QOpenGLTexture>
#include <QTimer>
#include <QTime>
#include <QtMath>
#include <QKeyEvent>
class Widget : public QOpenGLWidget,public QOpenGLExtraFunctions
{
    Q_OBJECT

public:
    Widget(QWidget *parent = 0);
    ~Widget();
protected:
    virtual void initializeGL() override;
    virtual void resizeGL(int w,int h) override;
    virtual void paintGL() override;

    virtual bool event(QEvent *e) override;

private:
    QVector<float> vertices;
    QVector<QVector3D> cubePositions;
    QOpenGLShaderProgram shaderProgram;
    QOpenGLBuffer VBO;
    QOpenGLVertexArrayObject VAO;
    QOpenGLTexture texture;
    QOpenGLTexture texture1;
    QTimer timer;
    
    Camera camera;
};

#endif // WIDGET_H

widget.cpp

#include "widget.h"
#include <QtMath>

Widget::Widget(QWidget *parent)
    : QOpenGLWidget(parent)
    , VBO(QOpenGLBuffer::VertexBuffer)
    , texture(QOpenGLTexture::Target2D)
    , texture1(QOpenGLTexture::Target2D)
    , camera(this)
{
    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, 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, 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
    };
    cubePositions = {
      { 0.0f,  0.0f,  0.0f  },
      { 2.0f,  5.0f, -15.0f },
      {-1.5f, -2.2f, -2.5f  },
      {-3.8f, -2.0f, -12.3f },
      { 2.4f, -0.4f, -3.5f  },
      {-1.7f,  3.0f, -7.5f  },
      { 1.3f, -2.0f, -2.5f  },
      { 1.5f,  2.0f, -2.5f  },
      { 1.5f,  0.2f, -1.5f  },
      {-1.3f,  1.0f, -1.5f  },
    };

    timer.setInterval(18);
    connect(&timer,&QTimer::timeout,this,static_cast<void (Widget::*)()>(&Widget::update));
    timer.start();

}

Widget::~Widget()
{
    makeCurrent();
    texture.destroy();
    texture1.destroy();
    doneCurrent();
}

void Widget::initializeGL()
{
    this->initializeOpenGLFunctions();        //初始化opengl函数
    if(!shaderProgram.addShaderFromSourceFile(QOpenGLShader::Vertex,":/triangle.vert")){     //添加并编译顶点着色器
        qDebug()<<"ERROR:"<<shaderProgram.log();    //如果编译出错,打印报错信息
    }
    if(!shaderProgram.addShaderFromSourceFile(QOpenGLShader::Fragment,":/triangle.frag")){   //添加并编译片段着色器
        qDebug()<<"ERROR:"<<shaderProgram.log();    //如果编译出错,打印报错信息
    }
    if(!shaderProgram.link()){                      //链接着色器
        qDebug()<<"ERROR:"<<shaderProgram.log();    //如果链接出错,打印报错信息
    }

    QOpenGLVertexArrayObject::Binder{&VAO};

    VBO.create();       //生成VBO对象
    VBO.bind();         //将VBO绑定到当前的顶点缓冲对象(QOpenGLBuffer::VertexBuffer)中

    //将顶点数据分配到VBO中,第一个参数为数据指针,第二个参数为数据的字节长度
    VBO.allocate(vertices.data(),sizeof(float)*vertices.size());

    texture.create();
    texture.setData(QImage(":/opengl.jpg").mirrored());
    texture.setMinMagFilters(QOpenGLTexture::LinearMipMapLinear,QOpenGLTexture::Linear);
    texture.setWrapMode(QOpenGLTexture::DirectionS,QOpenGLTexture::Repeat);
    texture.setWrapMode(QOpenGLTexture::DirectionT,QOpenGLTexture::Repeat);

    texture1.create();
    texture1.setData(QImage(":/sea.jpg").mirrored());
    texture1.setMinMagFilters(QOpenGLTexture::LinearMipMapLinear,QOpenGLTexture::Linear);
    texture1.setWrapMode(QOpenGLTexture::DirectionS,QOpenGLTexture::Repeat);
    texture1.setWrapMode(QOpenGLTexture::DirectionT,QOpenGLTexture::Repeat);

    //设置顶点解析格式,并启用顶点
    shaderProgram.setAttributeBuffer("aPos", GL_FLOAT, 0, 3, sizeof(GLfloat) * 5);
    shaderProgram.enableAttributeArray("aPos");
    shaderProgram.setAttributeBuffer("aTexCoord", GL_FLOAT,sizeof(GLfloat) * 3, 2, sizeof(GLfloat) * 5);
    shaderProgram.enableAttributeArray("aTexCoord");

    this->glEnable(GL_DEPTH_TEST);

    camera.init();

}

void Widget::resizeGL(int w, int h)
{
    this->glViewport(0,0,w,h);                  //定义视口区域
}

void Widget::paintGL()
{
    this->glClearColor(0.1f,0.5f,0.7f,1.0f);                       //设置清屏颜色
    this->glClear(GL_COLOR_BUFFER_BIT| GL_DEPTH_BUFFER_BIT);       //清除颜色缓存和深度缓存
    float time=QTime::currentTime().msecsSinceStartOfDay()/1000.0;
    shaderProgram.bind();                                          //使用shaderProgram着色程序
    shaderProgram.setUniformValue("view",camera.getView());
    QMatrix4x4 projection;
    projection.perspective(45.0f,width()/(float)height(),0.1f,100.0f);
    shaderProgram.setUniformValue("projection",projection);

    texture.bind(0);                                    //将texture绑定到纹理单元0
    shaderProgram.setUniformValue("ourTexture",0);      //让ourTexture从纹理单元0中获取纹理数据

    texture1.bind(1);                                    //将texture绑定到纹理单元1
    shaderProgram.setUniformValue("ourTexture1",1);      //让ourTexture从纹理单元1中获取纹理数据
    QOpenGLVertexArrayObject::Binder{&VAO};

    for(unsigned int i = 0; i < 10; i++){
        QMatrix4x4 model;
        model.translate(cubePositions[i]);
        model.rotate(180*time+i*20.0f,QVector3D(1.0f,0.5f,0.3f));
        shaderProgram.setUniformValue("model",model);
        this->glDrawArrays(GL_TRIANGLES, 0, 36);
    }
}

bool Widget::event(QEvent *e)
{
    camera.handle(e);
    return QWidget::event(e);   //调用父类的事件分发函数
}

猜你喜欢

转载自blog.csdn.net/qq_40946921/article/details/106014771