Qt5 OpenGL教程系列1:基礎渲染

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上下載

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