QtOpenGL之绘制三角形Qt5

4 篇文章 0 订阅

在本节中,我们将研究Qt5提供的一些OpenGL数据类型抽象。 这些数据类型抽象并不是创建OpenGL应用程序的100%必需的,但是它大大减少了代码量,使我们可以专注于渲染和UI,而不必担心我们的OpenGL实现。

我们可以创造一个OpenGL带有颜色的三角星在我们的屏幕上。

QOpenGLBuffer 

 OpenGL中的QOpenGLBuffer对象有点像GPU上动态内存的唯一ID。对于那些不熟悉的人,可以将它们近似为GPU动态内存。 您可以为GPU提供有关如何使用内存的提示,并且根据您的选择,它会更改信息的访问位置和访问效率。

Qt5提供了一个帮助器类,该类封装了称为OpenGL缓冲区的OpenGL缓冲区的所有功能。

这些缓冲区不会在CPU端动态分配(通过new / delete),它们在内部由OpenGL返回给我们的整数id表示。 因此,QOpenGLBuffers可以自由传递,而不会出现效率问题。

但是,当调用destroy()时,该缓冲区无效。

QOpenGLVertexArrayObject

 一遍又一遍地为对象绑定和取消绑定OpenGL缓冲区会变得很麻烦。因此,引入了QOpenGLVertexArrayObjects(VAO)。顶点数组对象只是存储在GPU上的对象,该对象跟踪所有缓冲区并绑定与绘图调用关联的信息。

这只是减少代码重复的另一种方法。视是否实现而定,它几乎是一种便利,它可能带来也可能不会带来速度上的好处。但是,我们希望以创建美观且易于消化的OpenGL代码为目标。我们将在初始化时绑定信息,然后允许“顶点数组对象”为我们记住我们的绑定信息。

典型的使用模式是:

  • 对于每个视觉对象
    • 绑定顶点数组对象
    • 设置顶点状态,绑定属性等。
    • 取消绑定顶点数组对象

然后,代替在渲染时为每个对象再次绑定所有属性信息,我们可以

  • 对于每个视觉对象
    • 绑定顶点数组对象
    • 使用glDraw *()函数绘制对象。
    • 取消绑定顶点数组对象

 QOpenGLShaderProgram

 这确实是本教程的核心。通常,对于首次使用OpenGL的用户而言,着色器编译有些棘手。但是,Qt5提供了功能的QOpenGLShaderProgram类抽象,使我们编写代码更加轻松。此类可以采用字符串形式,QOpenGLShader类型或文件路径。我们也可以将着色器代码作为资源传递,因为用它的资源编译Qt应用程序很容易。

着色器程序(如果您不熟悉它们的话)是在GPU上运行以处理数据的一些代码。通常,我们从CPU端获取数据,该数据将位置信息与一些属性信息配对,这就是着色器程序将处理的数据。保留位置和属性信息的结构通常称为“顶点”。

简单的着色器管道仅包含一条数据流和两种着色器类型,可以概括如下:

着色器有很多种,但是现在我们仅使用如上所述的“顶点着色器”和“片段着色器”。

  • 顶点着色器
    • 获取输入信息(通常以顶点信息的形式)并处理该信息以找到GPU必须在其间绘制和插值的最终位置。插值将由“顶点着色器”和“片段着色器”之间的流程自动处理。我们将使用的顶点信息是:位置和颜色。
  • 片段着色器
    • 从“顶点着色器”获取插值结果,并将计算结果输出到某处。现在,这听起来似乎很神秘,但是Fragment Shader的基本情况是输出到后台缓冲区,因此我们可以调用特定于系统的SwapBuffer()命令使其可见。
      • 注意:关于将其称为“片段”着色器还是“像素”着色器,有些困惑。我认为,这两个名称就足够了,任何图形程序员都可以使用一个名称或另一个名称来理解您的意思。我更喜欢OpenGL术语,因为片段着色器描述了由转换后的顶点输入表示的一组输出所发生的情况。 (例如,我们不输出到“像素”,而是评估许多“输出”)

基本渲染

如果你从固定管线的角度来看,通过着色器管线发送信息可能会感觉有些不自然。 因为实际上你并没有对绘画内容进行精确的控制,这完全与批处理有关,以便GPU可以随其一起运行。 一开始动态地控制如此多的信息可能会感到有些困惑。 但是最后, 一旦理解,它会变得容易得多,甚至很有趣!

首先,我们第一步是先发送数据

1,创建一个着色器类

由于顶点信息(组成一个几何图形的信息)是用户定义的,因此Qt不会提供现成的QVertex类。 为了简化问题,我们将创建一个类作为顶点信息,并且将尝试使其尽可能“Qt”(双关语)。

因此我创建一个叫 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。 如果您不知道这意味着什么,请不必担心太多。 此处是为了保持完整性,并且仅加快编译时已知的值。 (在这种情况下,我们的应用程序确实受益,但是当我们动态加载数据时不会。)

此处的Qt关键字,等同于c++11新特性中的constexpr,其作用就是在编译阶段对该函数直接计算结果。

constexpr的好处:

  1. 是一种很强的约束,更好地保证程序的正确语义不被破坏。
  2. 编译器可以在编译期对constexpr的代码进行非常大的优化,比如将用到的constexpr表达式都直接替换成最终结果等。
  3. 相比宏来说,没有额外的开销,但更安全可靠
  • Q_DECLARE_TYPEINFO(顶点,Q_MOVABLE_TYPE)
    • 为Qt创建详细的运行时类型信息。 在这种情况下,好处来自Q_MOVABLE_TYPE,这意味着可以通过memcpy()整个类型。 调用构造函数并不重要。

如您所见,我们还将QVector3D类用于位置和颜色。 通常,我这样做是为了减少创建自己的向量类。 稍后,这可能会很有用。

如果您不熟悉OpenGL或Vertex信息,可能会对* TupleSize,* Offset()和stride()符号感到困惑。

  • *TupleSize
    • 表示该数据的信息集合中存在多少个元素。 例如,PositionTupleSize为3:x,y,z。
  • *Offset()
    • 用于访问该数据的结构偏移量。 例如,颜色必须使自身偏移1个QVector3D类。 由于我们不想考虑填充问题,因此我们只需要使用offsetof(结构,成员)。
  • stride()
    • 如果数据保存在连续数组中,则步长就是任何给定属性必须移动以获取下一个数据位的信息量。 Stride通常是sizeof(Type),但在这里我们将其封装在静态的类函数中以使代码更整洁。

2.添加QOpenGL* 到window类

 在此应用程序中,我们将在屏幕上绘制一些内容。 我们将创建和分配一些信息。 但是,我们做的改动依然很少。

#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

接下里cpp的部分我们分两个部分来讲解。之后的内容都与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信息。 特定的顶点信息数组形成一个适合屏幕边界的三角形,每个位置具有一个属性(在这种情况下,该属性为颜色)。

如果存在一个坐标系,z轴由屏幕伸向我们,长为x,宽为y,屏幕中心为(0,0,0),按照下面点的顺序我们可以连成一个三角形。

(0.0f,0.75f,1.0f)->(-0.75f,-0.75f,1.0f)->(0.75f,-0.75f,1.0f)

每个三角形的顶点对应一个不同的属性,我们称其为颜色:

 红色->绿色->蓝色

紧接着,我们将要编辑 initializeGL()函数:

ndow.cppC++

// ...

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()函数来进行绘图。这将使我们的循环更新保持有效,让我们讨论下这部分。

  • 创建着色器
    • 这是唯一通过new动态分配的东西。我们甚至可以删除实例从GPU内存中释放着色器。
    • 正如我们所讨论的,顶点着色器将采用我们的顶点类型,并生成插值的片段数据。 Fragmet着色器将从Vertex Shader获取片段输出,并将最终结果数据输出到某个缓冲区。在我们的例子中,结果数据将被简单地绘制到屏幕上。
      • 注意:我们将在本教程的后面部分创建着色器。
    • 将所有已加载的着色器链接在一起(像CPU一样编译链接,GPU的着色器语言一样如此)。理想情况下,我们应该检查此调用的返回值,因为它可能会失败。我们可能会在另一篇教程中添加错误处理。尽管是在理想的情况下,我们也需要编程,并检查这些函数是否存在返回错误。
    • 绑定着色器,使其成为当前活动的着色器。
  • 创建缓冲区
    • 创建一个缓冲区,以便以后进行动态分配。
    • 绑定缓冲区,使其成为当前活动缓冲区。
    • 由于我们永远不会更改将要通过Buffer的数据,因此我们将使用模式称为StaticDraw。
    • * Draw共有三种类型:静态,动态和流。稍后,我们将使用这些使用模式进行更多探索。
    • 分配并初始化信息。
      • 注意:这里的sizeof()使用是一种特殊情况 ,因为数组的长度已知。在此处,我们并不能总是这样使用。
  • 创建顶点数组对象
    • 创建顶点数组对象(从此处开始为VAO)。
    • 绑定VAO,使其成为当前活动的VAO。
      • 注意:从这里到释放/解除绑定VAO的所有内容都与用于绘制缓冲区数据的缓冲区信息有关。
    • 启用属性0和1。
    • 将属性0设置为位置,并将属性1设置为颜色。
      • 这是我们* Offset(),* TupleSize和stride()的辅助函数发挥作用的地方。如您所见,这大大简化了我们的代码。
    • 释放(取消绑定)全部
      • 解除绑定的发生顺序是正式的。因此,我们将解除绑定VAO,然后绑定缓冲区和着色器。
      • Qt团队选择的奇怪的命名约定也可能会让您失望。release()。看来它可能会释放信息,但实际为取消绑定。我不确定为什么要发布这个名字,但我们也不必为这个名字执着。

在这里博主啰嗦两句,如果你对上面的操作内容不理解,可以看这里,让我粗略解释下,如果还是不够明白,你可以点击下面的链接来补充你的opengl的基本知识。https://learnopengl-cn.github.io/01%20Getting%20started/04%20Hello%20Triangle/

着色器,可以认为是一段程序,这段程序是提供给GPU的,它的基本实现与c差不多,后面我们会看到。

QOpenGLBuffer 如果有基础的话,我们一眼就可以看出来是VBO,不理解没关系,我们可以认为这是GPU一段显存,用来存放我们的数据。

QOpenGLVertexArrayObject 这个很明显对应VAO,VAO其实是方便我们使用的,上面提到的buffer里面存放的只有数据,而我们的顶点属性既包括了顶点位置,又包括了颜色。因此我们有了VAO后只需要配置一次,以后使用的话直接bind就好。而VBO就需要不断的配置(setAttributeBuffer(0, GL_FLOAT, Vertex::positionOffset(), Vertex::PositionTupleSize, Vertex::stride());) 。还是不懂的话,参考这里https://blog.csdn.net/lixiaoguang20/article/details/78667929

接下来,我们可以更新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。
    • 从第0个开始绘制一个带有3个索引的三角形。
    • 解除绑定VAO。
  • 取消绑定着色器。

为了解决顶点数组中的变化,我们正在动态计算索引数。 如果您回想起以前,我声称由于sg_vertexes在数据类型和数组长度上都是已知的,因此sizeof(sg_vertexes)返回以字节为单位的数组大小。 因此,不用说sizeof(sg_vertexes [0])将返回元素的大小(以字节为单位)。

 3.创建我们的着色器

接下来,我们需要一些文件才能实际加载到着色器程序中。 稍后,我们将动态加载文件,但现在,我们将它们作为Qt资源烘焙到可执行文件中。

因此,创建一个Qt Resource,我们将开始使用(我将资源文件命名为resources.qrc)。

 之后,我们将从“ GLSL”模板创建两个着色器。 一个将是一个名为simple.vert的顶点着色器,另一个将是一个名为simple.frag的片段着色器。 我们将它们保存在项目目录中创建的子目录中。

 然后我们将创建的两个着色器文件添加到Source目录下:

最后,我们需要编辑我们的着色器代码。

 

#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。

  • 注意:在上一教程中,当我谈到QOpenGLFunctions时,我声称我们将能够使用OpenGL ES 2.0 API。 这仍然是正确的,我们不需要API的任何其他功能。 但是,在着色器语言方面,我们将针对OpenGL 3.3(桌面版本)。 这意味着我们在此处编写的相同代码不会轻易移植到移动设备。 相反,它将需要较小的调整。
#version 330
in vec4 vColor;
out vec4 fColor;

void main()
{
   fColor = vColor;
}

片段着色器甚至更简单,它只接受输入颜色,并输出不变的颜色。 真的不需要多说什么。

我们参考上一节的例程,补充完我们其他的代码,就可以看到结果了。

不懂的我们可以参考源码。 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值