文章目錄
OpenGLBuffer
OpenGL中的QOpenGLBuffer對象有點像GPU上動態內存的唯一ID。這樣理解有點困難,但是對於那些不熟悉的人,可以將它們近似爲GPU動態內存。我們可以給GPU提供有關如何使用內存的提示,並且根據我們的選擇,它會更改信息的訪問位置和訪問效率。
Qt5提供了一個幫助器類,該類封裝了提供OpenGL緩衝區的所有功能,稱爲QOpenGLBuffer。
這些緩衝區不會在CPU端動態分配(通過new / delete),它們在內部由OpenGL返回給我們的整數id表示。因此,QOpenGLBuffers可以自由傳遞,而不會出現效率問題。
但是,當調用destroy()時,該緩衝區會變得無效,並且之後不應在該點之後使用。
QOpenGLVertexArrayObject
一遍又一遍地爲對象綁定和取消綁定OpenGL緩衝區會變得很麻煩。因此,引入了QOpenGLVertexArrayObjects(VAO)。頂點數組對象只是存儲在GPU上的對象,該對象跟蹤所有緩衝區並綁定與繪圖調用關聯的信息。
這只是減少代碼重複的另一種方法。它幾乎是一種便利,它可能帶來也可能不會帶來速度上的好處。但是,我們希望以創建美觀且易於理解的OpenGL代碼爲目標。我們將在初始化時綁定信息,然後允許“頂點數組對象”(VAO)爲我們記住我們的綁定信息。
典型的使用模式是:
- 對於每個視覺對象
- 綁定頂點數組對象(VAO)
- 設置頂點狀態,綁定屬性等。
- 取消綁定頂點數組對象(VAO)
然後,我們不再渲染時爲每個對象再次綁定所有屬性信息,我們可以
- 對於每個視覺對象
- 綁定頂點數組對象(VAO)
- 使用glDraw *()函數繪製對象。
- 取消綁定頂點數組對象(VAO)
QOpenGLShaderProgram
這確實是本教程的核心。 通常,對於首次使用OpenGL的用戶而言,着色器編譯有些棘手。 但是,Qt5提供了功能的QOpenGLShaderProgram類抽象,使我們的編程更加輕鬆。 此類可以採用源字符串,QOpenGLShader類型或文件路徑。
着色器程序(shader program,如果您不熟悉它們的話)是在GPU上運行以處理數據的一些代碼。 通常,我們從CPU端獲取數據,該數據將位置信息與一些屬性信息配對,這就是着色器程序將處理的數據。 保留位置和屬性信息的結構通常稱爲“頂點”(Vertex)。
簡單的着色器管道僅涉及單遍和兩種着色器類型,可以概括如下:
着色器有很多種,但是現在我們僅使用如上所述的“頂點着色器”和“片段着色器”。
- 頂點着色器(Vertex Shader)
- 獲取輸入信息(通常以頂點信息的形式)並處理該信息以找到GPU必須在其間繪製和插值的最終位置。插值將由“頂點着色器”和“片段着色器”之間的設備自動處理。我們將使用的頂點信息是:位置和顏色。
- 片段着色器(Fragment Shader)
- 從“頂點着色器”獲取插值結果,並將計算結果輸出到某處。現在,這聽起來似乎很神祕,但是Fragment Shader的基本情況是輸出到後臺緩衝區,因此我們可以調用特定於系統的SwapBuffer()命令使其可見。
基本渲染
1.創建一個Vertex類(以簡化操作)。
由於頂點信息(組成一個幾何圖形的信息)是用戶定義的,因此Qt不會提供現成的QVertex類。爲了簡化問題,我們將創建一個類作爲頂點信息
我們將創建一個名爲vertex.h的新頭文件,並將以下代碼放入。
#ifndef VERTEX_H
#define VERTEX_H
#include <QVector3D>
class Vertex
{
public:
// Constructors
Q_DECL_CONSTEXPR Vertex();
Q_DECL_CONSTEXPR explicit Vertex(const QVector3D &position);
Q_DECL_CONSTEXPR Vertex(const QVector3D &position, const QVector3D &color);
// Accessors / Mutators
Q_DECL_CONSTEXPR const QVector3D& position() const;
Q_DECL_CONSTEXPR const QVector3D& color() const;
void setPosition(const QVector3D& position);
void setColor(const QVector3D& color);
// OpenGL Helpers
static const int PositionTupleSize = 3;
static const int ColorTupleSize = 3;
static Q_DECL_CONSTEXPR int positionOffset();
static Q_DECL_CONSTEXPR int colorOffset();
static Q_DECL_CONSTEXPR int stride();
private:
QVector3D m_position;
QVector3D m_color;
};
/*******************************************************************************
* Inline Implementation
******************************************************************************/
// Note: Q_MOVABLE_TYPE means it can be memcpy'd.
Q_DECLARE_TYPEINFO(Vertex, Q_MOVABLE_TYPE);
// Constructors
Q_DECL_CONSTEXPR inline Vertex::Vertex() {}
Q_DECL_CONSTEXPR inline Vertex::Vertex(const QVector3D &position) : m_position(position) {}
Q_DECL_CONSTEXPR inline Vertex::Vertex(const QVector3D &position, const QVector3D &color) : m_position(position), m_color(color) {}
// Accessors / Mutators
Q_DECL_CONSTEXPR inline const QVector3D& Vertex::position() const { return m_position; }
Q_DECL_CONSTEXPR inline const QVector3D& Vertex::color() const { return m_color; }
void inline Vertex::setPosition(const QVector3D& position) { m_position = position; }
void inline Vertex::setColor(const QVector3D& color) { m_color = color; }
// OpenGL Helpers
Q_DECL_CONSTEXPR inline int Vertex::positionOffset() { return offsetof(Vertex, m_position); }
Q_DECL_CONSTEXPR inline int Vertex::colorOffset() { return offsetof(Vertex, m_color); }
Q_DECL_CONSTEXPR inline int Vertex::stride() { return sizeof(Vertex); }
#endif // VERTEX_H
在進行下一步之前,讓我們討論一下這個Vertex類。實際上,這裏只有一些Qt特定的東西。
- Q_DECL_CONSTEXPR
- Qt提供的宏,如果編譯器支持,它將設置爲constexpr。如果您不知道這意味着什麼,請不必擔心太多。此處是爲了保持完整性,並且僅加快編譯時已知的值。 (在這種情況下,我們的應用程序確實受益,但是當我們動態加載數據時不會。)
- Q_DECLARE_TYPEINFO(Vertex,,Q_MOVABLE_TYPE)
- 爲Qt創建詳細的運行時類型信息。在這種情況下,好處來自Q_MOVABLE_TYPE,這意味着可以通過memcpy()移動整個類型。
如您所見,我們還將QVector3D類用於位置和顏色。通常,我這樣做是爲了減少創建自己的向量類型的需要。稍後,這可能會很有用,儘管您可以告訴您僅定義啞的Vertex類型就可以。
如果您不熟悉OpenGL或Vertex信息,可能會對* TupleSize,* Offset()和stride()符號感到困惑。
- *TupleSize
- 表示該數據的信息集合中存在多少個元素。例如,PositionTupleSize爲3:x,y,z。
- *Offset()
- 用於訪問該數據的結構偏移量。例如,顏色必須使自身偏移1個QVector3D類。因爲我們不想考慮填充,所以我們只使用offsetof(structure,member)。
- stride()
- 跨度(stride)就是如果數據保存在連續數組中,任何給定屬性將必須移動以獲取下一個數據位的信息量。步幅通常是sizeof(Type),但是在這裏我們將其封裝在靜態的類函數中,以使代碼更簡潔。
2.將QOpenGL *類添加到Window類
在此應用程序中,我們將在屏幕上繪製一些內容。 因此,我們將不得不創建和分配一些信息。 但是,我們仍然做得很少,因此更改將很小。
您需要對window.h進行的更改在下面突出顯示:
#ifndef WINDOW_H
#define WINDOW_H
#include <QOpenGLWindow>
#include <QOpenGLFunctions>
#include <QOpenGLBuffer>
#include <QOpenGLVertexArrayObject>
class QOpenGLShaderProgram;
class Window : public QOpenGLWindow,
protected QOpenGLFunctions
{
Q_OBJECT
// OpenGL Events
public:
~Window();
void initializeGL();
void resizeGL(int width, int height);
void paintGL();
void teardownGL();
private:
// OpenGL State Information
QOpenGLBuffer m_vertex;
QOpenGLVertexArrayObject m_object;
QOpenGLShaderProgram *m_program;
// Private Helpers
void printVersionInformation();
};
#endif // WINDOW_H
實現部分需要將其分解爲幾個部分,讓我來討論一下。
因此,第2節中此點之後的所有內容都將與window.cpp有關。
第一個更改是在window.cpp的頂部:
#include "window.h"
#include <QDebug>
#include <QString>
#include <QOpenGLShaderProgram>
#include "vertex.h"
// Create a colored triangle
static const Vertex sg_vertexes[] = {
Vertex( QVector3D( 0.00f, 0.75f, 1.0f), QVector3D(1.0f, 0.0f, 0.0f) ),
Vertex( QVector3D( 0.75f, -0.75f, 1.0f), QVector3D(0.0f, 1.0f, 0.0f) ),
Vertex( QVector3D(-0.75f, -0.75f, 1.0f), QVector3D(0.0f, 0.0f, 1.0f) )
};
// ...
乍一看可能有點怪異,但請記住我們所討論的QOpenGLShaderProgram和Vertex信息。 特定的頂點信息數組形成一個適合屏幕邊界的三角形,每個位置具有一個屬性(在這種情況下,該屬性將被認爲是顏色,儘管可以代表任何東西)。
因此,我們有一些點可以逆時針移動以形成三角形:
(0.0f,0.75f,1.0f)->(-0.75f,-0.75f,1.0f)->(0.75f,-0.75f,1.0f)
每個點對應一個不同的屬性,我們稱其爲顏色:
紅色->綠色->藍色
接下來,我們將編輯initializeGL()函數:
// ...
void Window::initializeGL()
{
// Initialize OpenGL Backend
initializeOpenGLFunctions();
printVersionInformation();
// Set global information
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
// Application-specific initialization
{
// Create Shader (Do not release until VAO is created)
m_program = new QOpenGLShaderProgram();
m_program->addShaderFromSourceFile(QOpenGLShader::Vertex, ":/shaders/simple.vert");
m_program->addShaderFromSourceFile(QOpenGLShader::Fragment, ":/shaders/simple.frag");
m_program->link();
m_program->bind();
// Create Buffer (Do not release until VAO is created)
m_vertex.create();
m_vertex.bind();
m_vertex.setUsagePattern(QOpenGLBuffer::StaticDraw);
m_vertex.allocate(sg_vertexes, sizeof(sg_vertexes));
// Create Vertex Array Object
m_object.create();
m_object.bind();
m_program->enableAttributeArray(0);
m_program->enableAttributeArray(1);
m_program->setAttributeBuffer(0, GL_FLOAT, Vertex::positionOffset(), Vertex::PositionTupleSize, Vertex::stride());
m_program->setAttributeBuffer(1, GL_FLOAT, Vertex::colorOffset(), Vertex::ColorTupleSize, Vertex::stride());
// Release (unbind) all
m_object.release();
m_vertex.release();
m_program->release();
}
}
// ...
以上就是我們的大部分變化。通常,我們將準備大量數據,並準備使用glDraw *()進行繪製。讓我們來談談這些部分,
-
創建着色器(Shader)
-
這是唯一通過new動態分配的東西,因爲這個對象可能會被當作參數傳遞.刪除實例即可從GPU內存中釋放着色器。
-
正如我們所討論的,頂點着色器將採用我們的頂點(Vertex )類型,並生成插值的片段數據。 Fragmet着色器將從Vertex Shader獲取片段輸出,並將最終結果數據輸出到某個緩衝區。在我們的例子中,結果數據將被簡單地繪製到屏幕上。
- 注意:我們將在本教程的後面部分創建着色器。
-
將所有已加載的着色器鏈接在一起。理想情況下,我們應該檢查此調用的返回值,因爲它可能會失敗。
-
綁定着色器,使其成爲當前活動的着色器。
-
-
創建緩衝區(Buffer)
- 創建一個緩衝區,以便以後進行動態分配。
- 綁定緩衝區,使其成爲當前活動緩衝區。
- 由於我們不會更改將要存入Buffer的數據,因此我們將使用StaticDraw模式。
- *Draw共有三種類型:靜態,動態和流(tatic, Dynamic, and Stream)。稍後,我們將使用這些使用模式進行更多探索。
- 分配並初始化信息。
-
創建頂點數組對象(Vertex Array Object)
- 創建頂點數組對象(從此處開始爲VAO)。
- 綁定VAO,使其成爲當前活動的VAO。
- 注意:從這裏到釋放/解除綁定VAO的所有內容都與用於繪製緩衝區數據的緩衝區信息有關。
- 啓用屬性(Attribute)0和1。
- 將屬性0設置爲位置,並將屬性1設置爲顏色。
- 這是我們*Offset(),*TupleSize和stride()的輔助函數發揮作用的地方。如您所見,這大大簡化了我們的代碼。
-
釋放(取消綁定)全部
- 解除綁定的發生順序是正式的。因此,我們將解除綁定VAO,然後緩衝區和着色器。
接下來,我們可以更新paintGL()以實際繪製三角形:
// ...
void Window::paintGL()
{
// Clear
glClear(GL_COLOR_BUFFER_BIT);
// Render using our shader
m_program->bind();
{
m_object.bind();
glDrawArrays(GL_TRIANGLES, 0, sizeof(sg_vertexes) / sizeof(sg_vertexes[0]));
m_object.release();
}
m_program->release();
}
// ...
如您所見,這很簡單,因爲我們使用的是VAO。
- 綁定我們要繪製的着色器。
- 綁定我們要繪製的VAO。
- 從第0個索引開始繪製一個帶有3個索引的三角形。
- 解除綁定VAO。
- 取消綁定着色器。
最後,我們需要通過teardownGL()函數發佈信息:
// ...
void Window::teardownGL()
{
// Actually destroy our OpenGL information
m_object.destroy();
m_vertex.destroy();
delete m_program;
}
// ...
3.創建我們的着色器資源
接下來,我們需要一些文件才能實際加載到着色器程序中。 稍後,我們將動態加載文件,但現在,我們將它們作爲Qt資源烘焙到可執行文件中。
因此,創建一個Qt資源,我們將開始使用(我將資源文件命名爲resources.qrc)。
之後,我們將從“ GLSL”“文件和類”模板創建兩個着色器。 一個將是一個名爲simple.vert的頂點着色器,另一個將是一個名爲simple.frag的片段着色器。 我們將它們保存在項目目錄中創建的子目錄中。
注意:這兩個文件都應保存在:
/ shaders / simple.{vert | frag}
:= .pro文件所在的位置。
因此,如果我的項目名爲“ 1_BasicRendering”,並且保存在“ C:\ QtProjects \ 1_BasicRendering \”中,那麼我將創建一個名爲“ shaders”的文件夾,並保存以下兩個文件:
“ C:\ QtProjects \ 1_BasicRendering \ shaders \ simple.vert”
“ C:\ QtProjects \ 1_BasicRendering \ shaders \ simple.frag”
然後,我們將通過首先創建一個空的前綴,然後添加這兩個文件,將這兩個文件添加到resources.qrc中。
如果正確完成,您的項目佈局應如下所示:
最後我們所需要做的是編輯我們的着色器代碼
shaders/simple.vert:
#version 330
layout(location = 0) in vec3 position;
layout(location = 1) in vec3 color;
out vec4 vColor;
void main()
{
gl_Position = vec4(position, 1.0);
vColor = vec4(color, 1.0);
}
在文件頂部,通常會加一個註釋,指出您希望該文件在哪個版本的着色器語言上運行。 這說明着色器應在OpenGL 3.3上運行。 着色器接受兩個輸入(位置和顏色),並具有一個輸出:vColor。 在着色器的源代碼中,我們要做的就是通過強制轉換將兩種vec3類型升級爲vec4。
shaders/simple.frag:
#version 330
in vec4 vColor;
out vec4 fColor;
void main()
{
fColor = vColor;
}
片段着色器更簡單,僅僅是將入參vColor傳給fColor,
在這完成之後執行這個程序,我們會看到如下
總結
在本教程中,我們學習了……
- 爲了方便起見,該Qt5封裝了一些OpenGL數據類型。
- 如何創建用於將信息傳遞到GPU的Vertex類。
- QOpenGL *類的怪癖和怪異之處,以及如何代替QOpenGLFunctions *類使用它們。
- 着色器管道的更新,以及如何向我們的Qt應用程序添加資源。
- 最後,我們學習瞭如何在屏幕上渲染三角形!
代碼可以在Gitee上下載