Qt 5的图形架构非常依赖OpenGL作为底层3D图形API,但近年来,随着Metal和Vulkan的推出,Qt 6完全改变了局面。Qt Quick中的所有3D图形现在都建立在新的3D图形抽象层之上,该抽象层称为 渲染硬件接口(RHI) 。这使Qt可以使用目标OS /平台上原生的3D图形API。所以Qt Quick现在默认会在Windows上使用Direct3D,在macOS上使用Metal。有关RHI的学习资料可参照QT官网。

本文主要使用QT5.14来学习QT封装的OpenGL的渲染。

一、QT中实现OpenGL渲染

1.1 QWindow实现渲染

1.1.1 框架介绍

首先必须继承QWindow类,之后重载两个虚函数event、exposeEvent。

  • event(): 重写此方法以处理发送到窗口的任何事件。如果事件已被识别并处理,则返回true.
  • exposeEvent(): 每当窗口的某个区域无效时(例如,由于窗口系统中的曝光发生更改),窗口系统都会发送暴露事件。
    该应用程序一旦获得isExposed()为true的值,便可以使用QBackingStore和QOpenGLContext开始渲染到窗口中。如果将窗口移出屏幕,使其完全被另一个窗口(图标化或类似窗口)遮盖,则可能会调用此函数,并且isExposed()的值可能更改为false。发生这种情况时,应用程序应停止其呈现,因为它不再对用户可见。
  • isExposed(): 返回此窗口是否在窗口系统中公开。
class OpenGLWindow : public QWindow {Q_OBJECT
public:explicit OpenGLWindow(QWindow *parent = 0);// ...protected:bool event(QEvent *event) override;void exposeEvent(QExposeEvent *event) override;private:// ...
};

1.1.2 设置窗口的surfaceType

您可以使用基于网格的QPainter或OpenGL进行绘制。最好在类构造函数中进行设置,例如:

OpenGLWindow::OpenGLWindow(QWindow *parent) :QWindow(parent)
{setSurfaceType(QWindow::OpenGLSurface);
}

通过调用函数setSurfaceType(QWindow :: OpenGLSurface),您可以指定要创建本机OpenGL窗口。

此时,QT框架会发送以下两个事件:

  • QEvent::UpdateRequest: 部件应该被重绘;
  • QEvent::Expose: 当其屏幕上的内容无效,发送到窗口,并需要从后台存储刷新。

在实现重载的函数void ExposureEvent(QExposeEvent * event)中:

void OpenGLWindow::exposeEvent(QExposeEvent * /*event*/) {renderNow(); //渲染操作
}

此处我们仅需要进行渲染即可(下一小节介绍)。

在通用事件处理函数的实现中,event()我们仅UpdateRequest选择事件:

bool OpenGLWindow::event(QEvent *event) {switch (event->type()) {case QEvent::UpdateRequest:renderNow(); //渲染操作return true;default:return QWindow::event(event);}
}

然后我们的任务将很清楚 renderNow()实现使用OpenGL绘制的函数

1.1.3 OpenGL绘制实现

在Qt中QOpenGLFunctions封装了对本机OpenGL函数的访问。它既可以保留为数据成员,也可以作为实现继承。

1.1.3.1 框架调整

class OpenGLWindow : public QWindow, protected QOpenGLFunctions {Q_OBJECT
public:explicit OpenGLWindow(QWindow *parent = 0);virtual void initialize() = 0;virtual void render() = 0;public slots:void renderLater();void renderNow();protected:bool event(QEvent *event) override;void exposeEvent(QExposeEvent *event) override;QOpenGLContext *m_context; // wraps the OpenGL context
};

有两个纯粹的虚函数initialize()和render(),没有它们,OpenGL程序将无法执行。因此,该基类的用户提供这些功能(内容将在后面说明)。

除了renderNow()上面已经调用过的函数(其任务是即时OpenGL绘图)之外,还有另一个函数renderLater()。他们的任务最终是请求一个与垂直同步匹配的新字符调用,这最终对应UpdateRequest于在应用程序事件循环中发送事件。

void OpenGLWindow::renderLater() {requestUpdate();
}

严格来说,您还可以保存函数并直接requestUpdate()调用插槽,但是该名称最终指示在下一个VSync之前不会进行绘制。

在这一点上,可以预期与帧速率同步的两件事:

  • 用双缓冲区绘制
  • 默认情况下,Qt配置为QEvent::UpdateRequest始终将其发送到VSync。对于60Hz的刷新率,当然可以假设直到切换字符缓冲区为止的时间不超过16 ms

发送UpdateRequest到事件循环中的变量的优点是:在同步周期(即16ms内)内多次调用此函数(例如通过信号插槽连接)最终被组合为一个事件,因此每个VSync仅绘制一次。否则会浪费计算时间。

最后,m_context应该指出新的私有成员变量。该上下文最终封装了本机OpenGL上下文,即OpenGL中使用的状态机。尽管这是动态生成的,但我们不需要析构函数,因为我们还会自动m_context清理QObject-parent关系。

在构造函数中,我们使用nullptr初始化指针变量。

OpenGLWindow::OpenGLWindow(QWindow *parent) :QWindow(parent),m_context(nullptr)
{setSurfaceType(QWindow::OpenGLSurface);
}

1.1.3.2 初始化OpenGL窗口

现在有几种初始化OpenGL绘图窗口的方法。您可以立即在构造函数中执行此操作,但是所有必需的资源(包括mesh/纹理等)都应该已经初始化。

现在,您可以实现自己的初始化函数,该函数最初由类的用户调用。或者,您可以在第一次显示窗口时执行此操作。这里有很多回旋余地,根据初始化对错误的复杂性和敏感性,具有显式初始化功能的变量当然是好的。

此处使用首次使用初始化的变体(这是在Qt中使用对话框时的常见模式)。这意味着该函数renderNow()需要初始化:

void OpenGLWindow::renderNow() {// only render if exposedif (!isExposed())return;bool needsInitialize = false;// initialize on first callif (m_context == nullptr) {m_context = new QOpenGLContext(this);m_context->setFormat(requestedFormat());m_context->create();needsInitialize = true;}m_context->makeCurrent(this);if (needsInitialize) {initializeOpenGLFunctions();initialize(); // call user code}render(); // call user codem_context->swapBuffers(this);
}

该函数由exposeEvent()和调用一次event()。在这两种情况下,仅应在窗口实际可见的情况下进行绘制。因此,isExposed()首先检查该功能以查看窗口是否完全可见。如果不可见退出。

1.1.3.3 QOpenGLContext对象

现在是上面提到的首次使用初始化。

首先QOpenGLContext创建对象。接下来,设置各种特定于OpenGL的要求,从而将在QWindow中设置的格式传输到QOpenGLContext。

requestFormat()函数返回QWindow为表面(设置的格式QSurfaceFormat。其中包含颜色和深度缓冲区以及OpenGL渲染的抗锯齿设置。

  • requestFormat(): 返回此窗口的请求表面格式。如果平台实现不支持所请求的格式,则requestedFormat将与实际的窗口格式不同。

在初始化OpenGL上下文时,必须已经为QWindow定义了这种格式,即在第一次show()调用OpenGLWindow之前。

如果要避免这种错误源,则QSurfaceFormat实际上必须在请求所需函数时将初始化移至特殊函数。

通过调用m_context->create() OpenGL上下文(即状态)来创建,从而使用先前设置的格式参数。

如果要稍后更改格式参数(例如,抗锯齿),则必须首先在上下文对象中重置格式,然后create()再次调用。这将清除并替换之前的上下文。

上下文创建后,最重要的功能makeCurrent()和swapBuffers()服务。

  1. 调用m_context->makeCurrent(this)将上下文对象的内容传输到OpenGL状态。
  2. 初始化的第二步在于调用函数 QOpenGLFunctions :: initializeOpenGLFunctions()。最后,平台特定的OpenGL库是动态集成的,并且将函数指针glXXX…提取到本地OpenGL函数()。
  3. 最后,调用initialize()具有用户特定初始化的函数。
  4. 然后,用户必须在功能中进行3D场景render()的实际渲染。
  5. 最后,我们m_context->swapBuffers(this)将窗口缓冲区与渲染缓冲区交换。
  • QOpenGLFunctions :: initializeOpenGLFunctions(): 为当前上下文初始化OpenGL函数解析。调用此函数后,QOpenGLFunctions对象只能与当前上下文以及与其共享的其他上下文一起使用。再次调用initializeOpenGLFunctions()以更改对象的上下文关联。
  • QOpenGLContext :: makeCurrent(): 在给定的surface上使当前线程中的上下文成为当前上下文。成功返回true,否则返回false。如果表面未暴露,或者由于例如应用程序被挂起而导致图形硬件不可用,则后者可能发生。
  • QOpenGLContext :: swapBuffers(): 交换渲染表面的前后缓冲区。调用此命令以完成OpenGL渲染的框架,并确保在发出任何其他OpenGL命令(例如,作为新框架的一部分)之前再次调用makeCurrent()。

更新窗口缓冲区后,无需重新渲染即可将窗口移动到屏幕上的任何位置,甚至最小化。至少在我们开始处理场景中的动画之前,这是正确的。对于没有动画的应用程序,不要像Unity / Unreal / Irrlicht等游戏引擎那样自动重新渲染每一帧是有意义的。

如果我们仍然要设置动画(如果只是平滑的跟踪镜头),则应在函数结尾处调用renderNow()该函数renderLater(),以便在下一个VSync处接收新的调用。哦,是的:如果窗口是隐藏的(未暴露),则该函数当然会快速退出,并且renderLater()不会调用该函数。那将停止动画。为了使其再次开始运行,有一个已实现的事件函数exposeEvent()可以再次触发渲染。

这样,将完成OpenGL渲染窗口的中央基类。现在,我们便可以使用此基类实现渲染。

1.1.3.4 渲染窗口的实现

用户可自行创建类对象继承OpenGLWindow ,使用头文件调用特定的呈现窗口。比如:

class TriangleWindow : public OpenGLWindow {public:TriangleWindow();~TriangleWindow() Q_DECL_OVERRIDE;void initialize() Q_DECL_OVERRIDE;void render() Q_DECL_OVERRIDE;private:// VAOQOpenGLVertexArrayObject    m_vao;// Vertex bufferQOpenGLBuffer             m_vertexBufferObject;// shader programsQOpenGLShaderProgram     *m_program;
};

上述案例中的私有成员变量则是QT中对OpenGL的封装,具体使用下文介绍。

1.2 QOpenGLWindow实现渲染

QOpenGLWindow是增强的QWindow,它允许使用兼容QOpenGLWidget且类似于旧版QGLWidget的API轻松创建执行OpenGL渲染的窗口。与QOpenGLWidget不同,QOpenGLWindow不依赖于widgets模块,并提供更好的性能。

一个典型的应用程序将继承QOpenGLWindow并重新实现以下虚函数:

  • initializeGL(): 执行OpenGL资源初始化
  • resizeGL(): 设置转换矩阵和其他与窗口大小有关的资源
  • paintGL(): 发出OpenGL命令或使用QPainter绘制

要计划重绘,请调用update()函数。请注意,这不会立即导致对paintGL()的调用。连续多次调用update()不会以任何方式改变行为。

这是一个插槽,因此可以将其连接到QTimer::timeout()信号以执行动画。但是请注意,在现代OpenGL世界中,依靠同步到显示器的垂直刷新率是一个更好的选择。有关交换间隔的说明,请参见setSwapInterval()。交换间隔为1,这在大多数系统上都是默认情况下的情况,每次重新粉刷后QOpenGLWindow在内部执行的swapBuffers()调用将阻塞并等待vsync。这意味着只要交换完成,就可以通过调用update()再次调度更新,而无需依赖计时器。

要请求上下文的特定配置,请像其他任何QWindow一样使用setFormat()。除其他外,这允许请求给定的OpenGL版本和配置文件,或启用深度和模板缓冲区。

与QWindow不同,QOpenGLWindow允许自己打开一个画家并执行基于QPainter的绘制。

QOpenGLWindow支持多种更新行为。默认值NoPartialUpdate等效于基于OpenGL的常规QWindow或旧版QGLWidget。相比之下,PartialUpdateBlit以及PartialUpdateBlend更符合QOpenGLWidget工作的方式,其中总有一个额外的,专用的帧缓冲区对象存在。通过牺牲一些性能,这些模式可以在每个绘画上仅重画一个较小的区域,并保留前一帧的其余内容。这对于使用QPainter进行增量渲染的应用程序很有用,因为这样一来,它们不必在每个paintGL()调用上重新绘制整个窗口内容。

与QOpenGLWidget相似,QOpenGLWindow支持Qt :: AA_ShareOpenGLContexts属性。启用后,所有QOpenGLWindow实例的OpenGL上下文将彼此共享。这允许访问彼此的共享OpenGL资源。

1.2.1 QOpenGLWindow类的实现

1.2.1.1 QWindow与OpenGLWindow区别

要了解教程QWindow与OpenGLWindow类的异同,得具体看一下该类的实现,其中最重要的区别是继承层次结构。QOpenGLWindow从中得出QOpenGLPaintDevice基于光栅的硬件加速工程图QPainter。

但是有一个小问题。引用手册:

  • OpenGL绘制引擎中的抗锯齿是使用多重采样完成的。大多数硬件需要大量内存来进行多重采样,因此产生的质量与软件绘画引擎的质量不相称。OpenGL绘画引擎的优势在于其性能,而不是视觉渲染质量。(适用于QOpenGLPaintDevice的Qt文档5.9)

如果在OpenGL窗口中绘制了褪色的小部件或控件,则还会影响应用程序的整体外观,而且还会影响具有尖锐边缘的经典小部件。当Windows 10中的应用程序最终绘制一个像素缓冲区时,您可能会从Windows的模糊窗口中熟悉该问题,然后将该像素缓冲区作为纹理插入到3D窗口表面中。

如果要在OpenGL小部件中使用现有的绘图功能(基于QPainter),这仍然很有帮助。如果不需要该功能,PaintDevice及其所需的功能会带来一些不必要的开销(尤其是内存消耗)。

1.2.1.2 QWindow与OpenGLWindow相似之处

  • a.构造函数

构造函数看起来几乎与OpenGLWindow最初的类完全一样。除了将参数传递到私有Pimpl类中。

QOpenGLWindow::QOpenGLWindow(QOpenGLWindow::UpdateBehavior updateBehavior, QWindow *parent): QPaintDeviceWindow(*(new QOpenGLWindowPrivate(nullptr, updateBehavior)), parent)
{setSurfaceType(QSurface::OpenGLSurface);
}
  • b.事件处理函数
void QOpenGLWindow::paintEvent(QPaintEvent * /*event*/ ) {paintGL();
}void QOpenGLWindow::resizeEvent(QResizeEvent * /*event*/ ) {Q_D(QOpenGLWindow);d->initialize();resizeGL(width(), height());
}

这paintEvent()简单地传递给用户要实现的功能paintGL()。在这方面,类似于QEvent::UpdateRequest等待的OpenGLWidget中的事件处理。但是,在调用paintEvent()函数直至创建QPaintEvent对象的过程中,将执行许多中间步骤,而这完全不需要。当您查看呼叫链时,思路将变得很清楚:

QPaintDeviceWindow::event(QEvent *event)  // waits for QEvent::UpdateRequest
QPaintDeviceWindowPrivate::handleUpdateEvent()
QPaintDeviceWindowPrivate::doFlush()  // calls QPaintDeviceWindowPrivate::paint()bool paint(const QRegion &region){Q_Q(QPaintDeviceWindow);QRegion toPaint = region & dirtyRegion;if (toPaint.isEmpty())return false;// Clear the region now. The overridden functions may call update().dirtyRegion -= toPaint;beginPaint(toPaint); // here we call QOpenGLWindowPrivate::beginPaint()QPaintEvent paintEvent(toPaint);q->paintEvent(&paintEvent); // here we call QOpenGLWindowPrivate::paintEvent()endPaint(); // here we call QOpenGLWindowPrivate::endPaint()return true;}

或者,paintGL()可以从事件QPaintDeviceWindow::exposeEvent()处理例程进行调用,在该例程中直接进行调用QPaintDeviceWindowPrivate::doFlush()。这些功能beginPaint()并 endPaint()照顾临时帧缓冲区,在其中进行UpdateBehaviorQOpenGLWindow::PartialUpdateBlit和QOpenGLWindow::PartialUpdateBlend渲染。没有这些模式,该功能几乎不会发生。

  • c.初始化

resizeEvent()事件处理例程中的初始化调用也很有意思

void QOpenGLWindowPrivate::initialize()
{Q_Q(QOpenGLWindow);if (context)return;if (!q->handle())qWarning("Attempted to initialize QOpenGLWindow without a platform window");context.reset(new QOpenGLContext);context->setShareContext(shareContext);context->setFormat(q->requestedFormat());if (!context->create())qWarning("QOpenGLWindow::beginPaint: Failed to create context");if (!context->makeCurrent(q))qWarning("QOpenGLWindow::beginPaint: Failed to make context current");paintDevice.reset(new QOpenGLWindowPaintDevice(q));if (updateBehavior == QOpenGLWindow::PartialUpdateBlit)hasFboBlit = QOpenGLFramebufferObject::hasOpenGLFramebufferBlit();q->initializeGL();
}

实际上,该函数几乎OpenGLWindow::renderNow()与QWindow中的函数初始化部分完全一样。当然,除了QOpenGLWindowPaintDevice创建另一个实例。

1.3 QOpenGLWidget实现渲染

1.3.1 QOpenGLWidget介绍

在所有Qt OpenGL类中,这QOpenGLWidget是迄今为止记录最好的类。
具体列出以下几点:

  • 所有渲染都发生在OpenGL帧缓冲区对象中。
  • 由于由帧缓冲区对象支持,因此QOpenGLWidget的行为与QOpenGLWindow非常相似,其更新行为设置为PartialUpdateBlit或PartialUpdateBlend。这意味着在两次paintGL()调用之间保留了内容,从而可以进行增量渲染。
    注意: 大多数应用程序不需要增量渲染,因为它们将在每次绘画调用时渲染视图中的所有内容。
  • 将QOpenGLWidget添加到窗口中会为整个窗口打开基于OpenGL的合成。在某些特殊情况下,这可能并不理想,因此需要具有单独的本机子窗口的旧QGLWidget样式行为。了解此方法局限性的桌面应用程序(例如,涉及重叠,透明,滚动视图和MDI区域),可以将QOpenGLWindow与QWidget::createWindowContainer()一起使用。这是QGLWidget的现代替代方案,由于缺少附加的合成步骤,因此它比QOpenGLWidget更快。强烈建议将这种方法的使用限制在没有其他选择的情况下。请注意,此选项不适用于大多数嵌入式和移动平台,并且已知在某些台式机平台(例如macOS)上也存在问题。

基本上:OpenGL图像QOpenGLWidget 始终总是首先在缓冲区中渲染,然后根据构图规则(合成)在屏幕上绘制。当然,这比直接绘制要花费更长的时间。

缓冲绘图的主要优点是可以进行增量渲染。是否需要它在很大程度上取决于实际应用。实际上,仅当要渲染的窗口由几个单独的部分组成时,这才有意义。在这种情况下,应用程序也可以由几个OpenGL窗口组成,并且每个窗口都可以单独绘制。

关于可移植性和稳定性的最后一点也许并不完全无关紧要。因此,您可以从两个方面看整个事情:

  • 使用QOpenGLWidget,但如果出现性能问题可以考虑进行切换其他;
  • QWindow与OpenGLWindow是一个自写的轻量级类,如果在发生兼容性问题时可以切换到QOpenGLWidget

1.3.1.1 继承关系

如下类:

class Window : public QOpenGLWidget, protected QOpenGLFunctions {public:Window(QWidget * parent = nullptr);....protected:void initializeGL() override;void paintGL() override;....
};

该类QOpenGLWidget本身并不继承自QOpenGLFunctions,这就是为什么必须将此类指定为附加基类的原因(还有另一种方法,但是不必在源代码中进行太多调整)。与其他小部件一样,构造函数也将父指针作为参数。

功能initializeGL()和 paintGL()是在QOpenGLWidget受保护的。

1.3.1.2 初始化

必须相应地扩展构造函数,以便将parent指针传递给基类:

Window::Window(QWidget * parent) :QOpenGLWidget(parent),m_vertexColors{      QColor("#f6a509"),QColor("#cb2dde"),QColor("#0eeed1"),QColor("#068918") },m_program(nullptr),m_frameCount(5000)
{setMinimumSize(600,400);
}

如果该类是一个小部件,您也可以在此处设置最小大小。必须在第一次显示之前设置大小,否则窗口小部件将不可见(并且无法放大)。

使用继承的QOpenGLFunctions函数还需要初始化,但这必须通过调用ininitializeOpenGLFunctions()中的函数在initializeGL()完成。

void Window::initializeGL() {initializeOpenGLFunctions();....
}

这UpdateBehavior被设置为QOpenGLWidget默认QOpenGLWidget::NoPartialUpdate,所以不必另行调整。

1.3.1.3 嵌入到QWidget

可以省略窗口小部件容器,并且像其他任何窗口小部件一样嵌入窗口小部件。
具体调用如下:

....m_window= new Window(this);
m_window->setFormat(format);// *** create the layout and insert widget containerQVBoxLayout * vlay = new QVBoxLayout;
vlay->setMargin(0);
vlay->setSpacing(0);
vlay->addWidget(m_window);....

1.3.2 性能比较

QOpenGLWidget与直接通过QWindow使用或QOpenGLWindow使用您自己的OpenGLWindow类进行绘制相比,它要慢多少?

  • 调整窗口大小行为是不同的。调整窗口小部件的大小(在Windows和其他平台上)以及在发布模式下编译程序时都存在明显的延迟。

有区别,但貌似对整体来说影响很小。在动画期间放大/缩小窗口时,优化的延迟效果可能是个问题。

二、QT封装的OpenGL与原生OpenGL API对比

2.1 着色器程序

该类QOpenGLShaderProgram封装了着色器程序,并提供了在本机OpenGL调用中实现的各种便利功能。

m_program = new QOpenGLShaderProgram();

这大致对应于以下OpenGL命令:

unsigned int shaderProgram;
shaderProgram = glCreateProgram();

2.2 着色器程序的编译和链接

QOpenGLShaderProgram::addShaderFromSourceFile()类中有几个重载函数,在这里使用带有文件名传输的变量。这些文件在资源文件中引用,因此是通过资源路径指定的。在此处和指定着色器程序的类型很重要。

通过返回代码指示成功或失败。错误处理的功能以后有时间再整理。

最后一步是着色器程序的链接,即,自定义变量的链接(着色器程序之间的通信)。

该类的函数QOpenGLShaderProgram最终封装了以下类型的OpenGL命令:

if (!m_program->addShaderFromSourceFile(QOpenGLShader::Vertex, ":/Vertex.vert")){qDebug() << "Vertex shader errors :\n" << m_program->log();}if (!m_program->addShaderFromSourceFile(QOpenGLShader::Fragment, ":/Fragment.frag")){qDebug() << "Fragment shader errors :\n" << m_program->log();}if (!m_program->link())qDebug() << "Shader linker errors :\n" << m_program->log();

原生OpenGL着色器程序初始化

// create the shader
unsigned int vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER);// pass shader program in C string
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);// compile the shader
glCompileShader(vertexShader);// check success of compilation
int  success;
char infoLog[512];
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);// print out an error if any
if (!success) {glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);std::cout << "Vertex shader error:\n" << infoLog << std::endl;
}// ... same for fragment shader// attach shaders to shader program
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);// and link
glLinkProgram(shaderProgram);

2.3 顶点缓冲对象(VBO)和顶点数组对象(VBA)

着色器程序完成后,我们首先创建具有三角形坐标的顶点缓冲区对象。然后,将顶点数据分配给属性。因此,您不必一次又一次地进行这些分配,可以在VertexArrayObject(VBA)中进行标注。

  • 顶点缓冲对象(ENGL。VertexBuffer Objects(VBO))最终包含发送到顶点着色器的数据。从OpenGL的角度来看,必须首先创建这些对象,然后将它们绑定(即,后续的OpenGL命令引用缓冲区),然后再次释放。

例如:

float vertices[] = {-0.5f, -0.5f, 0.0f,0.5f, -0.5f, 0.0f,0.0f,  0.5f, 0.0f};// create a new buffer for the verticesm_vertexBufferObject = QOpenGLBuffer(QOpenGLBuffer::VertexBuffer); // VBOm_vertexBufferObject.create(); // create underlying OpenGL objectm_vertexBufferObject.setUsagePattern(QOpenGLBuffer::StaticDraw); // must be called before allocatem_vertexBufferObject.bind(); // set it active in the context, so that we can write to it// int bufSize = sizeof(vertices) = 9 * sizeof(float) = 9*4 = 36 bytesm_vertexBufferObject.allocate(vertices, sizeof(vertices) ); // copy data into buffer

在上面的源代码中,首先定义了具有9个浮点数(3 x 3矢量)的静态数组。Z坐标为0。现在我们创建一个类型为的新VertexBufferObject QOpenGLBuffer::VertexBuffer。该调用create()创建对象本身。这对应于原生OpenGL调用如下:

unsigned int VBO;
glGenBuffers(1, &VBO);

然后,通过setUsagePattern()通知QOpenGLBuffer缓冲区对象计划的访问类型。这不会执行OpenGL调用,但是会保存此属性以供以后使用。

通过bind()此VBO的调用,在OpenGL上下文中将其设置为活动状态,即,随后引用VBO的函数调用将引用我们创建的VBO。这对应于原生OpenGL调用如下:

glBindBuffer(GL_ARRAY_BUFFER, VBO);

最后,将数据allocate()复制到对的调用中的缓冲区中。这大致相当于一个memcpy命令,即,缓冲区的源地址已传送,字节长度是第二个参数。在这种情况下,有9个浮点数,即9 * 4 = 36个字节。这对应于原生OpenGL调用如下:

glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

这里使用预先设置的使用类型(usagePattern)。因此,setUsagePattern()始终保持领先 很重要allocate()。

现在绑定了缓冲区,您现在可以将顶点数据与着色器程序中的输入参数链接起来。由于我们不想每次都在绘制之前执行此操作,因此我们使用VertexArrayObject(VBA),它最终表示类似此类链接的容器。您可以想象一个VBA,就像下面的链接命令的记录一样,其中,当前活动的顶点缓冲区和链接的变量被一起保存。稍后,在实际绘制过程中,您仅需集成VBA,然后VBA将在引擎盖下播放所有记录的链接,从而相应地恢复OpenGL状态。

具体来说,如下所示:

 // Initialize the Vertex Array Object (VAO) to record and remember subsequent attribute assocations with// generated vertex buffer(s)m_vao.create(); // create underlying OpenGL objectm_vao.bind(); // sets the Vertex Array Object current to the OpenGL context so it monitors attribute assignments// now all following enableAttributeArray(), disableAttributeArray() and setAttributeBuffer() calls are// "recorded" in the currently bound VBA.// Enable attribute array at layout location 0m_program->enableAttributeArray(0);m_program->setAttributeBuffer(0, GL_FLOAT, 0, 3);// This maps the data we have set in the VBO to the "position" attribute.// 0 - offset - means the "position" data starts at the begin of the memory array// 3 - size of each vertex (=vec3) - means that each position-tuple has the size of 3 floats (those are the 3 coordinates,//     mind: this is the size of GL_FLOAT, not the size in bytes!

首先,我们创建并集成顶点数组对象。enableAttributeArray()并随后setAttributeBuffer()记录对和的所有后续调用。

该命令enableAttributeArray(0)激活顶点缓冲区中的属性(或变量),然后可以在着色器程序中使用布局索引0对其进行寻址。在此示例的顶点着色器中(请参见上文),这是位置矢量。

setAttributeBuffer()现在定义with ,可以在顶点缓冲区中找到数据的位置,即数据类型,编号(此处为3个浮点数,对应于3个坐标)和起始偏移量(此处为0)。

这两个调用对应于OpenGL调用:

glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);

现在,所有数据都已初始化,并且可以释放缓冲区对象:

// Release (unbind) allm_vertexBufferObject.release();m_vao.release(); // not really necessary, but done for completeness

这对应于OpenGL调用:

glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);

您可以看到Qt类最终封装了本地OpenGL函数调用(有时非常直接)。

Qt API在这里感觉不太好选择。像这样的调用m_programm->enableAttributeArray(0)表明实际上在这里更改了对象属性;实际上正在使用OpenGL状态机。相应地,对于许多命令来说,调用的顺序很重要,尽管首先设置哪个属性与对象的单独设置属性无关紧要。这就是为什么我在上面的教程中明确说明了其背后的OpenGL命令。

因此,建议您再次在自己的类中打包Qt API,然后设计相应的蛇形且无错的API。

2.4 QT_OpenGL渲染

实际的渲染发生在render()被基类称为纯虚函数的函数中OpenGLWindow。基类还检查是否需要渲染并设置当前OpenGL上下文。这使您可以直接在此功能中开始渲染。

void TriangleWindow::render() {// this function is called for every frame to be rendered on screenconst qreal retinaScale = devicePixelRatio(); // needed for Macs with retina displayglViewport(0, 0, width() * retinaScale, height() * retinaScale);// set the background color = clear colorglClearColor(0.1f, 0.1f, 0.2f, 1.0f);glClear(GL_COLOR_BUFFER_BIT);// use our shader programm_program->bind();// bind the vertex array object, which in turn binds the vertex buffer object and// sets the attribute buffer in the OpenGL contextm_vao.bind();// now draw the triangles:// - GL_TRIANGLES - draw individual triangles// - 0 index of first triangle to draw// - 3 number of vertices to processglDrawArrays(GL_TRIANGLES, 0, 3);// finally release VAO again (not really necessary, just for completeness)m_vao.release();
}

前三个glXXX命令是本机OpenGL调用,实际上应该总是以这种方式出现。调整ViewPort(glViewport(…))对于调整大小操作以及删除颜色缓冲区()是必不可少的glClear(…)(其他缓冲区将在此调用中稍后删除)。devicePixelRatio()函数适用于缩放比例合适的屏幕(主要用于配备Retina显示屏的Mac)。

只要背景颜色(纯色)不变,此调用也可以移至初始化。

然后是有趣的部分。绑定着色器程序(m_programm->bind()),然后绑定顶点数组对象(VAO)(m_vao.bind())。后者确保在OpenGL上下文中也设置了顶点缓冲区对象和属性映射。然后可以将其用于简单绘制,为此,glDrawArrays(…)将再次使用本机OpenGL命令。

程序的这一部分在原生OpenGL代码中如下所示:

glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
glBindVertexArray(0);

2.5 资源释放

剩下的就是清理析构函数中的保留资源。

TriangleWindow::~TriangleWindow() {// resource cleanup// since we release resources related to an OpenGL context,// we make this context current before cleaning up our resourcesm_context->makeCurrent(this);m_vao.destroy();m_vertexBufferObject.destroy();delete m_program;
}

由于某些资源属于当前窗口的OpenGL上下文,因此您应该首先将OpenGL上下文设置为“当前”(m_context->makeCurrent(this);),以便可以安全地释放这些资源。

这样就可以TriangleWindow完成实施。

2.6 初始化纹理

如果使用原生OpenGL代码创建纹理,则其外观如下所示:

// erstelle Texturobjekt
unsigned int texture;
glGenTextures(1, &texture);
// binde Textur
glBindTexture(GL_TEXTURE_2D, texture);
// setze Attribute:// Wrap style
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);// Border color in case of GL_CLAMP_TO_BORDER
float borderColor[] = { 1.0f, 1.0f, 0.0f, 1.0f };
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);// Texture Filtering
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);// Lade Texturdaten mittels 'stb_image.h'
unsigned char *data = stbi_load("container.jpg", &width, &height, &nrChannels, 0);// Kopiere Daten in Texture und Erstelle Mipmap
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);

如果使用QOpenGLTexture的话就会变成如下所示:

// erstelle Texturobjekt
QOpenGLTexture * texture = new QOpenGLTexture(QOpenGLTexture::Target2D);
texture->create();
// setze Attribute// Wrap style
texture->setWrapMode(QOpenGLTexture::ClampToBorder);
texture->setBorderColor(Qt::red);// Texture Filtering
texture->setMinificationFilter(QOpenGLTexture::NearestMipMapLinear);
texture->setMagnificationFilter(QOpenGLTexture::Linear);// Lade Bild
QImage img(":/textures/brickwall.jpg");
// Kopiere Daten in Texture und Erstelle Mipmap
texture->setData(img); // allocate() will be called internally

调用mipmap数据时,默认情况下将setData()生成这些数据,而无需其他参数。

调用时setData(),它将自动被调用,allocate()并且图像数据被复制到OpenGL纹理中。如果之后再次调用它allocate(),则会收到错误消息:GL_INVALID_OPERATION error generated. Texture is immutable.

  • 至少在嵌入图像(和mipmap)的属性方面,纹理对象是不可变的。调用后setData(),实际上只能更改影响绑定数据解释的属性(即过滤器和换行样式)。如果要自己更改纹理,则必须销毁并重新创建对象。

2.6.1 着色器纹理链接

如果在一个着色器中使用多个纹理,则必须告诉着色器程序可以在哪个ID下找到纹理。

信息链如下所示:

  • 在着色器程序(片段着色器)中指定纹理(sampler2D),例如brickTexture或roofTiles
  • 您需要提供其参数/统一索引,就好像它们是普通的统一变量→
    brickTextureUniformID,roofTilesUniformID一样。使用这些变量ID,可以指定着色器参数。
  • 这些变量中的每一个都被赋予一个纹理ID,例如BrickTextureUniformID变量获得Texture#0,roofTilesUniformID获得Texture#1。为自己的纹理编号完全独立于统一的ID。
  • 渲染之前,您要集成纹理并指定纹理编号。

在初始化中,它看起来像这样:

SHADER(0)->setUniformValue(m_shaderPrograms[0].m_uniformIDs[1+i],i);

通常,您最多可以使用16个纹理。因此,在具有大量纹理的大型场景中,drawXXX不可避免地要拆分成多个调用。

2.7 帧缓冲区的初始化

使用OpenGL,您必须创建帧缓冲区,深度和模板附件,并为颜色值附加纹理:

// framebuffer configuration
// -------------------------
glGenFramebuffers(1, &framebuffer);
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
// create a color attachment texture
glGenTextures(1, &textureColorbuffer);
glBindTexture(GL_TEXTURE_2D, textureColorbuffer);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, scr_width, scr_height, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, textureColorbuffer, 0);
// create a renderbuffer object for depth and stencil attachment (we won't be sampling these)
glGenRenderbuffers(1, &rbo);
glBindRenderbuffer(GL_RENDERBUFFER, rbo);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, scr_width, scr_height); // use a single renderbuffer object for both a depth AND stencil buffer.
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo); // now actually attach it
// now that we actually created the framebuffer and added all attachments we want to check if it is actually complete now
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)qDebug() << "ERROR::FRAMEBUFFER:: Framebuffer is not complete!" << endl;
glBindFramebuffer(GL_FRAMEBUFFER, 0);

而使用QT封装的函数可以直接简单如下(基础版):

m_frameBufferObject = new QOpenGLFramebufferObject(QSize(scr_width, scr_height), QOpenGLFramebufferObject::CombinedDepthStencil);

2.8 窗口尺寸调整

如果更改窗口大小,则还必须调整缓冲区的大小。在resizeGL()中使用原始OpenGL看起来像这样:

// also resize the texture buffer
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
glBindTexture(GL_TEXTURE_2D, textureColorbuffer);
// actual resize operation
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, scr_width, scr_height, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL);
glBindRenderbuffer(GL_RENDERBUFFER, rbo);
// actual resize operation
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, scr_width, scr_height);

使用QOpenGLFrameBufferObject类时,您只需要重新创建类对象即可:

delete m_frameBufferObject;
m_frameBufferObject = new QOpenGLFramebufferObject(QSize(scr_width, scr_height), QOpenGLFramebufferObject::CombinedDepthStencil);

2.9 使用帧缓冲区

2.9.1 在帧缓冲区中渲染

首先,将帧缓冲区与原始OpenGL集成在一起:

glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);

与QOpenGLFrameBufferObject:

m_frameBufferObject->bind();

2.9.2 重置帧缓冲区

渲染场景后,使用本机OpenGL重置正常的渲染缓冲区:

glBindFramebuffer(GL_FRAMEBUFFER, 0);

与QOpenGLFrameBufferObject:

m_frameBufferObject->bindDefault();

2.9.3 帧缓冲纹理的整合

并嵌入纹理以用于ScreenFill着色器和矩形。再次使用OpenGL:

glBindTexture(GL_TEXTURE_2D, textureColorbuffer);

并带有QOpenGLFrameBufferObject:

glBindTexture(GL_TEXTURE_2D, m_frameBufferObject->texture());

三、渲染示例

QT_OpenGL渲染总结相关推荐

  1. keyshot怎么批量渲染_提高Keyshot逼真渲染的小技巧

    Keyshot是一个特别神奇的应用软件,但是,就像Photoshop一样,如果你不知道怎么使用它,那么再优秀的工具在你手中也什么都是了.这里我就告诉你一些制作优秀效果图的技巧以及如何使用这个神奇软件. ...

  2. Gin 框架学习笔记(03)— 输出响应与渲染

    在 Gin 框架中,对 HTTP 请求可以很方便有多种不同形式的响应.比如响应为 JSON . XML 或者是 HTML 等. ​ Context 的以下方法在 Gin 框架中把内容序列化为不同类型写 ...

  3. LeetCode简单题之图像渲染

    题目 有一幅以二维整数数组表示的图画,每一个整数表示该图画的像素值大小,数值在 0 到 65535 之间. 给你一个坐标 (sr, sc) 表示图像渲染开始的像素值(行 ,列)和一个新的颜色值 new ...

  4. Turing渲染着色器网格技术分析

    Turing渲染着色器网格技术分析 图灵体系结构通过使用 网格着色器 引入了一种新的可编程几何着色管道.新的着色器将计算编程模型引入到图形管道中,因为协同使用线程在芯片上直接生成紧凑网格( meshl ...

  5. 优化汽车仪表板以实现高效渲染

    优化汽车仪表板以实现高效渲染 Optimizing automotive dashboards for efficient rendering 数字显示屏似乎已经占领了全世界,最早出现在20世纪80年 ...

  6. 用Microsoft DirectX光线跟踪改善渲染质量

    用Microsoft DirectX光线跟踪改善渲染质量 Implementing Stochastic Levels of Detail with Microsoft DirectX Raytrac ...

  7. echarts数据变了不重新渲染,以及重新渲染了前后数据会重叠渲染的问题

    1.echarts数据变了但是视图不重新渲染 新建Chart.vue文件 <template>  <p :id="id" :style="style&q ...

  8. computed set 自定义参数_完全理解Vue的渲染watcher、computed和user watcher

    作者:Naice https://segmentfault.com/a/1190000023196603 这篇文章将带大家全面理解vue的watcher.computed和user watcher,其 ...

  9. php渲染nodejs api,如何使用nodejs 服务器读取HTML文件渲染至前端

    这次给大家带来如何使用nodejs 服务器读取HTML文件渲染至前端,使用nodejs 服务器读取HTML文件渲染至前端的注意事项有哪些,下面就是实战案例,一起来看一下. 1.分别简单实现三个备用页面 ...

最新文章

  1. ESXi6.5环境搭建(三:vSphere Client6.0安装)
  2. 【字符串】字符串查找 ( Rabin-Karp 算法 )
  3. mysql触发器 生僻字_MySQL生僻字插入失败的处理方法(Incorrect string value)
  4. python读取文件夹下所有图像 预处理_Tensorflow之tif图像文件预处理
  5. mysql字段简索引_3万字总结,Mysql优化之精髓
  6. 奇安信代码安全实验室帮助微软修复多个高危漏洞,获官方致谢
  7. Mac DBeaver Client home is not specified for connection解决办法
  8. jQuery UI dialog实现dialog弹框显示
  9. PS换照片底色(三种方式)
  10. 重要的xcel文件e报表丢了如何恢复呢
  11. 天池竞赛-金融风控-task1
  12. centos7安装python3.7.4_基于centos7 安装python3.6.4出错的解决方法
  13. 《指定一个用户只能在特定的时间里不能登陆》『罗斌原创』
  14. Sql Server 2008 R2 清理内存的三种方法
  15. Qt 在mac上使用证书签名并生成pkg安装包
  16. The REBOL Scripting Language 读后感
  17. 电磁波以及常见电磁波波谱
  18. 因为Istio,谷歌不惜公开与CNCF、合作伙伴撕破脸
  19. 电子招标系统源码之了解电子招标投标全流程
  20. 稻盛和夫(INAMORI KAZUO)

热门文章

  1. (郭霖)Android图片加载框架最全解析(一),Glide的基本用法
  2. Java程序员拼多多3轮面试,这些面试题你能掌握多少?
  3. 【网络Ping不通如何解决?】
  4. android内核函数,Android display架构分析三-Kernel Space Display架构介绍
  5. hdu6069 Counting Divisors
  6. android exoplayer 直播流,使用Exo-Media Player播放RTMP直播
  7. 【转载】开源项目推荐:Qt有关的GitHub/Gitee开源项目(★精品收藏★)
  8. 《算法设计与分析》第十三周作业
  9. BLE 怎样添加 Characteristic
  10. 问题事件名称: APPCRASH(解决方法)