如果你是中途開始學習本教程的,即使你對OpenGL已經非常熟悉,請至少了解以下幾個章節,因爲Qt中提供了OpenGL的很多便捷操作,熟悉這些操作可以讓我們在Qt中高效的使用OpenGL進行繪圖。
Qt-OpenGL的幾個優勢:
- Qt內嵌了opengl的相關環境,不需要我們自己來搭建,這對小白來說是很友好的。
- Qt和opengl都具有優良的跨平臺特性,使用Qt做opengl開發可謂是強強聯合。
- Qt可以輕鬆的控制窗口的各種處理事件以及窗口屬性。
- Qt提供了opengl函數的C++封裝,使得opengl原來的C風格API可以通過C++的面向對象技術來實現。
- Qt提供了十分完善的官方文檔,有助於我們掌握QtOpenGL的各種細節。
這個教程將完全使用Qt對openglAPI的C++封裝,內容板塊儘量與learnopengl保持一致。筆者會逐步的實現教程裏的demo,儘可能的說明每一個操作細節。你可以在文章的右上角找到本節的索引目錄,如果什麼地方操作失敗,你可以直接複製代碼節點的代碼,嘗試運行一下,再對比一下自己的代碼,看自己是否什麼地方出問題了,如果還不能解決問題,可以在下方評論區留言,筆者看到一定第一時間解答。
筆者對openGL瞭解不是很深,如果什麼地方存在問題,希望朋友們能夠詳細指出。
顏色
在前面的教程中我們已經簡要提到過該如何在OpenGL中使用顏色(Color),但是我們至今所接觸到的都是很淺層的知識。本節我們將會更深入地討論什麼是顏色,並且還會爲接下來的光照(Lighting)教程創建一個場景。
現實世界中有無數種顏色,每一個物體都有它們自己的顏色。我們需要使用(有限的)數值來模擬真實世界中(無限)的顏色,所以並不是所有現實世界中的顏色都可以用數值來表示的。然而我們仍能通過數值來表現出非常多的顏色,甚至你可能都不會注意到與現實的顏色有任何的差異。顏色可以數字化的由紅色(Red)、綠色(Green)和藍色(Blue)三個分量組成,它們通常被縮寫爲RGB。僅僅用這三個值就可以組合出任意一種顏色。例如,要獲取一個珊瑚紅(Coral)色的話,我們可以定義這樣的一個顏色向量:
QVector3D coral(1.0f,0.5f,0.31f);
我們在現實生活中看到某一物體的顏色並不是這個物體真正擁有的顏色,而是它所反射的(Reflected)顏色。換句話說,那些不能被物體所吸收(Absorb)的顏色(被拒絕的顏色)就是我們能夠感知到的物體的顏色。例如,太陽光能被看見的白光其實是由許多不同的顏色組合而成的(如下圖所示)。如果我們將白光照在一個藍色的玩具上,這個藍色的玩具會吸收白光中除了藍色以外的所有子顏色,不被吸收的藍色光被反射到我們的眼中,讓這個玩具看起來是藍色的。下圖顯示的是一個珊瑚紅的玩具,它以不同強度反射了多個顏色。
你可以看到,白色的陽光實際上是所有可見顏色的集合,物體吸收了其中的大部分顏色。它僅反射了代表物體顏色的部分,被反射顏色的組合就是我們所感知到的顏色(此例中爲珊瑚紅)。
這些顏色反射的定律被直接地運用在圖形領域。當我們在OpenGL中創建一個光源時,我們希望給光源一個顏色。在上一段中我們有一個白色的太陽,所以我們也將光源設置爲白色。當我們把光源的顏色與物體的顏色值相乘,所得到的就是這個物體所反射的顏色(也就是我們所感知到的顏色)。讓我們再次審視我們的玩具(這一次它還是珊瑚紅),看看如何在圖形學中計算出它的反射顏色。我們將這兩個顏色向量作分量相乘,結果就是最終的顏色向量了:
QVector3D lightColor(1.0f,1.0f,1.0f);
QVector3D toyColor(1.0f,0.5f,0.31f);
QVector3D result = lightColor * toyColor; // = (1.0f, 0.5f, 0.31f);
我們可以看到玩具的顏色吸收了白色光源中很大一部分的顏色,但它根據自身的顏色值對紅、綠、藍三個分量都做出了一定的反射。這也表現了現實中顏色的工作原理。由此,我們可以定義物體的顏色爲物體從一個光源反射各個顏色分量的大小。現在,如果我們使用綠色的光源又會發生什麼呢?
QVector3D lightColor(0.0f,1.0f,0.0f);
QVector3D toyColor(1.0f,0.5f,0.31f);
QVector3D result = lightColor * toyColor; // = (0.0f, 0.5f, 0.0f);
可以看到,並沒有紅色和藍色的光讓我們的玩具來吸收或反射。這個玩具吸收了光線中一半的綠色值,但仍然也反射了一半的綠色值。玩具現在看上去是深綠色(Dark-greenish)的。我們可以看到,如果我們用綠色光源來照射玩具,那麼只有綠色分量能被反射和感知到,紅色和藍色都不能被我們所感知到。這樣做的結果是,一個珊瑚紅的玩具突然變成了深綠色物體。現在我們來看另一個例子,使用深橄欖綠色(Dark olive-green)的光源:
QVector3D lightColor(0.33f,0.42f,0.18f);
QVector3D toyColor(1.0f,0.5f,0.31f);
QVector3D result = lightColor * toyColor; // = (0.33f, 0.21f, 0.06f);
可以看到,我們可以使用不同的光源顏色來讓物體顯現出意想不到的顏色。有創意地利用顏色其實並不難。
這些顏色的理論已經足夠了,下面我們來構造一個實驗用的場景吧。
創建一個光照場景
在接下來的教程中,我們將會廣泛地使用顏色來模擬現實世界中的光照效果,創造出一些有趣的視覺效果。由於我們現在將會使用光源了,我們希望將它們顯示爲可見的物體,並在場景中至少加入一個物體來測試模擬光照的效果。
首先我們需要一個物體來作爲被投光(Cast the light)的對象,我們將使用前面教程中的那個著名的立方體箱子。我們還需要一個物體來代表光源在3D場景中的位置。簡單起見,我們依然使用一個立方體來代表光源(我們已擁有立方體的頂點數據是吧?)。
填一個頂點緩衝對象(VBO),設定一下頂點屬性指針和其它一些亂七八糟的東西現在對你來說應該很容易了,所以我們就不再贅述那些步驟了。如果你仍然覺得這很困難,我建議你複習之前的教程,並且在繼續學習之前先把練習過一遍。我們首先需要一個頂點着色器來繪製箱子。與之前的頂點着色器相比,容器的頂點位置是保持不變的(雖然這一次我們不需要紋理座標了),因此頂點着色器中沒有新的代碼。我們將會使用之前教程頂點着色器的精簡版:
#version 330 core
layout (location = 0) in vec3 aPos;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main()
{
gl_Position = projection * view * model * vec4(aPos, 1.0);
}
記得更新你的頂點數據和屬性指針使其與新的頂點着色器保持一致(當然你可以繼續留着紋理數據和屬性指針。在這一節中我們將不會用到它們,但有一個全新的開始也不是什麼壞主意)。
因爲我們還要創建一個表示燈(光源)的立方體,所以我們還要爲這個燈創建一個專門的VAO與着色器程序。當然我們也可以讓這個燈和其它物體使用同一個VAO,簡單地對它的model(模型)矩陣做一些變換就好了,然而接下來的教程中我們會頻繁地對頂點數據和屬性指針做出修改,我們並不想讓這些修改影響到燈(我們只關心燈的頂點位置和顏色),爲了實現這個目標,我們需要爲燈的繪製創建另外的一套着色器,從而能保證它能夠在其它光照着色器發生改變的時候不受影響。頂點着色器與我們當前的頂點着色器是一樣的,所以你可以直接把現在的頂點着色器用在燈上。燈的片段着色器給燈定義了一個不變的常量白色,保證了燈的顏色一直是亮的。
首先在頭文件中添加着色器程序:
QOpenGLShaderProgram lampShader;
頂點着色器我們直接使用原來的,我們只需要在qrc中創建一個新的片段着色器(lamp.frag):
#version 330 core
out vec4 FragColor;
void main()
{
FragColor = vec4(1.0); // 將向量的四個分量全部設置爲1.0
}
因爲我們還要創建一個表示燈(光源)的立方體,所以我們還要爲這個燈創建一個專門的VAO。當然我們也可以讓這個燈和其它物體使用同一個VAO,簡單地對它的model(模型)矩陣做一些變換就好了,然而接下來的教程中我們會頻繁地對頂點數據和屬性指針做出修改,我們並不想讓這些修改影響到燈(我們只關心燈的頂點位置),因此我們有必要爲燈創建一個新的VAO(lightVAO)。
QOpenGLVertexArrayObject lightVAO;
並在initializeGL中爲這個着色器程序裝載着色器,並設置頂點屬性:
if(!lampShader.addShaderFromSourceFile(QOpenGLShader::Vertex,":/triangle.vert")){ //添加並編譯頂點着色器
qDebug()<<"ERROR:"<<shaderProgram.log(); //如果編譯出錯,打印報錯信息
}
if(!lampShader.addShaderFromSourceFile(QOpenGLShader::Fragment,":/lamp.frag")){ //添加並編譯片段着色器
qDebug()<<"ERROR:"<<shaderProgram.log(); //如果編譯出錯,打印報錯信息
}
if(!lampShader.link()){ //鏈接着色器
qDebug()<<"ERROR:"<<shaderProgram.log(); //如果鏈接出錯,打印報錯信息
}
QOpenGLVertexArrayObject::Binder{&lightVAO};
VBO.bind(); //只需要綁定VBO不用再次設置VBO的數據,因爲箱子的VBO數據中已經包含了正確的立方體頂點數據
lampShader.setAttributeBuffer("aPos", GL_FLOAT, 0, 3, sizeof(GLfloat) * 3);
lampShader.enableAttributeArray("aPos");
然後在paintGL中繪製燈立方體:
lampShader.bind();
model.translate(0.0f,1.5f,1.5f);
model.scale(0.2);
lampShader.setUniformValue("model",model);
lampShader.setUniformValue("view",camera.getView());
lampShader.setUniformValue("projection",projection);
QOpenGLVertexArrayObject::Binder{&lightVAO};
this->glDrawArrays(GL_TRIANGLES, 0, 36);
請把上述的所有代碼片段放在你程序中合適的位置,這樣我們就能有一個乾淨的光照實驗場地了。如果一切順利,運行效果將會如下圖所示:
沒什麼好看的是嗎?但我保證在接下來的教程中它會變得更加有趣。
如果你覺得將上面的代碼片段整合到一起非常困難,可以來看一下源代碼
我們現在已經對顏色有一定的瞭解了,並且已經創建了一個簡單的場景供我們之後繪製動感的光照,我們可以進入下一節進行學習,真正的魔法即將開始!
代碼節點:
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;
QOpenGLShaderProgram lampShader;
QOpenGLBuffer VBO;
QOpenGLVertexArrayObject VAO;
QOpenGLVertexArrayObject lightVAO;
QTimer timer;
Camera camera;
};
#endif // WIDGET_H
widget.cpp
#include "widget.h"
#include <QtMath>
Widget::Widget(QWidget *parent)
: QOpenGLWidget(parent)
, VBO(QOpenGLBuffer::VertexBuffer)
, camera(this)
{
vertices = {
-0.5f, -0.5f, -0.5f,
0.5f, -0.5f, -0.5f,
0.5f, 0.5f, -0.5f,
0.5f, 0.5f, -0.5f,
-0.5f, 0.5f, -0.5f,
-0.5f, -0.5f, -0.5f,
-0.5f, -0.5f, 0.5f,
0.5f, -0.5f, 0.5f,
0.5f, 0.5f, 0.5f,
0.5f, 0.5f, 0.5f,
-0.5f, 0.5f, 0.5f,
-0.5f, -0.5f, 0.5f,
-0.5f, 0.5f, 0.5f,
-0.5f, 0.5f, -0.5f,
-0.5f, -0.5f, -0.5f,
-0.5f, -0.5f, -0.5f,
-0.5f, -0.5f, 0.5f,
-0.5f, 0.5f, 0.5f,
0.5f, 0.5f, 0.5f,
0.5f, 0.5f, -0.5f,
0.5f, -0.5f, -0.5f,
0.5f, -0.5f, -0.5f,
0.5f, -0.5f, 0.5f,
0.5f, 0.5f, 0.5f,
-0.5f, -0.5f, -0.5f,
0.5f, -0.5f, -0.5f,
0.5f, -0.5f, 0.5f,
0.5f, -0.5f, 0.5f,
-0.5f, -0.5f, 0.5f,
-0.5f, -0.5f, -0.5f,
-0.5f, 0.5f, -0.5f,
0.5f, 0.5f, -0.5f,
0.5f, 0.5f, 0.5f,
0.5f, 0.5f, 0.5f,
-0.5f, 0.5f, 0.5f,
-0.5f, 0.5f, -0.5f,
};
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());
//設置頂點解析格式,並啓用頂點
shaderProgram.setAttributeBuffer("aPos", GL_FLOAT, 0, 3, sizeof(GLfloat) * 3);
shaderProgram.enableAttributeArray("aPos");
if(!lampShader.addShaderFromSourceFile(QOpenGLShader::Vertex,":/triangle.vert")){ //添加並編譯頂點着色器
qDebug()<<"ERROR:"<<shaderProgram.log(); //如果編譯出錯,打印報錯信息
}
if(!lampShader.addShaderFromSourceFile(QOpenGLShader::Fragment,":/lamp.frag")){ //添加並編譯片段着色器
qDebug()<<"ERROR:"<<shaderProgram.log(); //如果編譯出錯,打印報錯信息
}
if(!lampShader.link()){ //鏈接着色器
qDebug()<<"ERROR:"<<shaderProgram.log(); //如果鏈接出錯,打印報錯信息
}
QOpenGLVertexArrayObject::Binder{&lightVAO};
VBO.bind(); //只需要綁定VBO不用再次設置VBO的數據,因爲箱子的VBO數據中已經包含了正確的立方體頂點數據
lampShader.setAttributeBuffer("aPos", GL_FLOAT, 0, 3, sizeof(GLfloat) * 3);
lampShader.enableAttributeArray("aPos");
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();
QVector3D lightColor(1.0f,1.0f,1.0f);
QVector3D objectColor(1.0f,0.5f,0.31f);
shaderProgram.setUniformValue("objectColor",objectColor);
shaderProgram.setUniformValue("lightColor",lightColor);
QMatrix4x4 model;
shaderProgram.setUniformValue("model",model);
shaderProgram.setUniformValue("view",camera.getView());
QMatrix4x4 projection;
projection.perspective(45.0f,width()/(float)height(),0.1f,100.0f);
shaderProgram.setUniformValue("projection",projection);
QOpenGLVertexArrayObject::Binder{&VAO};
this->glDrawArrays(GL_TRIANGLES, 0, 36);
lampShader.bind();
model.translate(0.0f,1.5f,1.5f);
model.scale(0.2);
lampShader.setUniformValue("model",model);
lampShader.setUniformValue("view",camera.getView());
lampShader.setUniformValue("projection",projection);
QOpenGLVertexArrayObject::Binder{&lightVAO};
this->glDrawArrays(GL_TRIANGLES, 0, 36);
}
bool Widget::event(QEvent *e)
{
camera.handle(e);
return QWidget::event(e); //調用父類的事件分發函數
}
triangle.vert
#version 330 core
layout (location = 0) in vec3 aPos;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main()
{
gl_Position = projection * view * model * vec4(aPos, 1.0);
}
triangle.frag
#version 330 core
out vec4 FragColor;
uniform vec3 objectColor;
uniform vec3 lightColor;
void main()
{
FragColor = vec4(lightColor * objectColor, 1.0);
}
lamp.frag
#version 330 core
out vec4 FragColor;
void main()
{
FragColor = vec4(1.0); // 將向量的四個分量全部設置爲1.0
}