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

1 篇文章 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上下载

  • 1
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值