Learn OpenGL with Qt——完美的Qt攝像機

如果你是中途開始學習本教程的,即使你對OpenGL已經非常熟悉,請至少了解以下幾個章節,因爲Qt中提供了OpenGL的很多便捷操作,熟悉這些操作可以讓我們在Qt中高效的使用OpenGL進行繪圖。

創建OpenGL窗口

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

紋理的創建與加載

使用Qt內置矩陣進行變換

針對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);   //調用父類的事件分發函數
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章