OpenGL(3)之Qt窗口(QOpenGLWidget详解)
在上一篇中窗口类渲染OpenGL部件是基于QWindow,但在实际应用开发中比较常用的窗口是基于QWidget(当然还有Qt Quick这里并不展开讲)。至于QWindow
和QWidget
的联系,可以简略看一下这边博文从QWindow到QWidget(Qt5)
QOpenGLWidget 类详解
QOpenGLWidget
是一个渲染OpenGL图形的窗口部件。 QOpenGLWidget
提供的功能是把OpenGL
图形显示功能整合到Qt 应用程序当中。它非常容易使用:把我们的子类继承QOpenGLWidget
,然后就可以像使用其它QWidget
那样使用,除此之外,你也可以使用 QPainter
和标准的OpenGL
渲染命令。
QOpenGLWidget
提供3个便利的虚函数,我们可以再我们的子类中重新实现内容,用来执行OpenGL
的典型任务:
paintGL()
: 渲染OpenGL的场景。当窗口需要被更新时,调用这个函数。resizeGL()
: 设置OpenGL的视口,投影矩阵等;当窗口改变大小时被调用,以及在第一次显示时会被调用(这是因为新建一个窗口时
会自动发送重置窗口大小事件[resize event
])initializeGL()
: 设置OpenGL的资源和状态。这个函数需要在paintGL()
,resizeGL()
之前调用。
如果想在其它地方触发paintGL()
进行重绘,不直接调用paintGL()
函数。可以使用update()
函数。QWidget::update()
;
当调用 initializeGL()
,paintGL()
,paintGL()
,窗口中OpenGL渲染的上下文使用的是当前的。如果需要在其它地方调用标注的OpenGL API的函数(例如:在我们自己窗口的构造函数或者在我们自己绘制函数中),我们需要先调用makeCurrent()
;
所有的渲染都发生在一个OpenGL framebuffer对象中,即幕后渲染。makeCurrent()
确保它在上下文中绑定。在paintGL()
的呈现代码中,创建和绑定额外的framebuffer对象时,请记住这一点,永远不要重新绑定ID为0的framebuffer。相反,调用defaultFramebufferObject()
来获取应该绑定的ID。
在平台支持时,QOpenGLWidget
允许使用不同OpenGL的版本以及配置文件。只要通过setFormat()
函数来设置需要的格式。注意:在同一窗口中有多个QOpenGLWidget
实例需要使用相同的格式,或者至少不能使它们的上下文不能共享。为了克服这个问题,最好使用QSurfaceFormat::setDefaultFormat()
来替代setFormat()
函数。
【注意】:在一些平台(例如macOS)上,当请求OpenGL核心配置(即上下文)时,在构造QApplication实例之前必须的调用QSurfaceFormat::setDefaultFormat(),。这是为了确保在使用正确的版本和配置文件创建所有内部上下文时,上下文之间的资源共享保持正常工作。
OpenGL Function Calls, Headers and QOpenGLFunctions
在进行OpenGL函数调用时,强烈建议避免直接调用函数。相反,更喜欢使用QOpenGLFunctions(在制作可移植应用程序时)或版本化变体(例如,QOpenGLFunctions_3_2_Core等,当针对现代的,仅限桌面的OpenGL时)。这样,应用程序将在所有Qt构建配置中正常工作,包括执行动态OpenGL实现加载的应用程序,这意味着应用程序不直接链接到GL实现,因此直接函数调用是不可行的。
在paintGL()中,当前场景(context)始终可以通过调用QOpenGLContext :: currentContext()来访问。从这个context中,可以通过调用QOpenGLContext :: functions()来检索已经初始化的,准备好使用的QOpenGLFunctions实例。为每个GL调用添加前缀的替代方法是从QOpenGLFunctions继承并在initializeGL()中调用QOpenGLFunctions :: initializeOpenGLFunctions()。
至于OpenGL头文件,请注意,在大多数情况下,不需要直接包含任何头文件,如GL.h.与OpenGL相关的Qt头文件将包含qopengl.h,后者将包含适用于系统的标头。这可能是OpenGL ES 3.x或2.0标头,可用的最高版本,或系统提供的gl.h.此外,作为OpenGL和OpenGL ES的Qt的一部分,提供了扩展头的副本(在某些系统上称为glext.h)。这些将在可行的情况下自动包含在平台上。这意味着来自ARB,EXT,OES扩展的常量和函数指针typedef自动可用。
实例一
直接继承 QOpenGLWidget
,这里面就要例举上述三个函数具体需要实现什么功能。
QOpenGLContext::currentContext()->functions();
从当前上下文中获取关联的OpenGL函数,只有获取这个函数的指针,我们调用OpenGL函数进行渲染才是有效的。
class MyGLWidget : public QOpenGLWidget
{
public:
MyGLWidget(QWidget *parent) : QOpenGLWidget(parent) { }
protected:
void initializeGL()
{
//设置渲染的上下文,加载着色器和其他资源,等等...
QOpenGLFunctions *f = QOpenGLContext::currentContext()->functions();
f->glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
...
}
void resizeGL(int w, int h)
{
//更新投影矩阵和,设置视口等其它相关设置
m_projection.setToIdentity();
m_projection.perspective(45.0f, w / float(h), 0.01f, 100.0f);
...
}
void paintGL()
{
// 绘制场景
QOpenGLFunctions *f = QOpenGLContext::currentContext()->functions();
f->glClear(GL_COLOR_BUFFER_BIT);
...
}
};
实例二
直接继承 QOpenGLWidget
,同时继承QOpenGLFunctions
。这么做的目的只是为了简化代码,可以不用每次调用QOpenGLContext::currentContext()->functions();
获取当前上下文的QOpenGLFunctions
对象,需要任何函数直接调用就可以。不过在初始化函数需要先调用initializeOpenGLFunctions();
可以使OpenGL的函数只关联到当前上下文,这样我们才能在当前上下文进行渲染等操作。
class MyGLWidget : public QOpenGLWidget, protected QOpenGLFunctions
{
public:
MyGLWidget(QWidget *parent) : QOpenGLWidget(parent) { }
protected:
void initializeGL()
{
initializeOpenGLFunctions();
glClearColor(...);
...
}
void resizeGL(int w, int h)
{
//更新投影矩阵和,设置视口等其它相关设置
m_projection.setToIdentity();
m_projection.perspective(45.0f, w / float(h), 0.01f, 100.0f);
...
}
void paintGL()
{
// 绘制场景
glClear(GL_COLOR_BUFFER_BIT);
...
}
};
实例三:上下文配置格式
- 配置格式:要获取与给定OpenGL版本或配置文件兼容的上下文,或请求深度和模具缓冲区,调用setFormat():
QOpenGLWidget *widget = new QOpenGLWidget(parent);
QSurfaceFormat format;
format.setDepthBufferSize(24);
format.setStencilBufferSize(8);
format.setVersion(3, 2);
format.setProfile(QSurfaceFormat::CoreProfile);
//必须在显示之前调用
widget->setFormat(format);
- 对于OpenGL 3.0+上下文,当可移植性不重要时,我们可以使用下面这种方式来访问具体版本的的函数功能。
...
void paintGL()
{
QOpenGLFunctions_3_2_Core *f = QOpenGLContext::currentContext()- >versionFunctions<QOpenGLFunctions_3_2_Core>();
...
f->glDrawArraysInstanced(...);
...
}
...
- 如上所述,全局地设置请求的格式,以便在应用程序的生命周期中应用于所有窗口和上下文,这样更简单、更健壮。
int main(int argc, char **argv)
{
QApplication app(argc, argv);
QSurfaceFormat format;
format.setDepthBufferSize(24);
format.setStencilBufferSize(8);
format.setVersion(3, 2);
format.setProfile(QSurfaceFormat::CoreProfile);
QSurfaceFormat::setDefaultFormat(format);
MyWidget widget;
widget.show();
return app.exec();
}
与QGLWidget 的联系
传统的QtOpenGL模块(以QGL为前缀的类)提供了一个名为QGLWidget的widget。QOpenGLWidget旨在成为它的替代品。因此,特别是在新的应用程序中,一般建议使用QOpenGLWidget。
虽然API非常相似,但两者之间存在重要差异:QOpenGLWidget始终使用帧缓冲对象在屏幕外渲染。另一方面,QGLWidget使用原生窗口和曲面。后者在复杂的用户界面中使用它时会引起问题,因为根据平台,这种本机子窗口小部件可能具有各种限制,例如关于堆叠命令。QOpenGLWidget通过不创建单独的本机窗口来避免这种情况。
由于帧缓冲对象的支持,QOpenGLWidget的行为与QOpenGLWindow非常相似,更新行为设置为PartialUpdateBlit或PartialUpdateBlend。这意味着在paintGL()调用之间保留内容,以便可以进行增量渲染。使用QGLWidget(当然QOpenGLWindow具有默认的更新行为)通常不是这种情况,因为交换缓冲区会使后台缓冲区中的内容不确定。
注意:大多数应用程序不需要增量渲染,因为它们将在每次调用绘制时呈现视图中的所有内容。在这种情况下,在paintGL()中尽早调用glClear()非常重要。这有助于使用基于tile的架构的移动GPU认识到tile缓冲区不需要用framebuffer以前的内容重新加载。忽略clear调用可能会导致此类系统的性能显著下降
注意:避免在QOpenGLWidget上调用winId()。此功能触发创建本机窗口,从而降低性能并可能出现毛刺。
与QGLWidget的差异
除了framebuffer对象支持的主要概念差异之外,QOpenGLWidget和旧的QGLWidget之间存在许多较小的内部差异:
调用paintGL()时的OpenGL状态。 QOpenGLWidget通过glViewport()设置视口。 它不执行任何清算。
当开始绘画时通过QPainter清除。 与常规widget不同,QGLWidget默认为autoFillBackground的值为true。 然后,每次使用QPainter :: begin()时,它都会清除调色板的背景颜色。 QOpenGLWidget不遵循:autoFillBackground默认为false,就像任何其他widget一样。 唯一的例外是当用作QGraphicsView等其他小部件的视口时。 在这种情况下,autoFillBackground将自动设置为true,以确保与基于QGLWidget的视口兼容。
多重采样
要启用多采样,请在传递给setFormat()的QSurfaceFormat上设置请求样本的数量。在不支持它的系统上,请求可能会被忽略。
Context Sharing
当多个QOpenGLWidgets作为子组件添加到同一个顶级小部件时,它们的上下文将相互共享。这不适用于属于不同窗口的QOpenGLWidget实例。
这意味着同一个窗口中的所有QOpenGLWidgets都可以访问彼此共享的资源,比如纹理,而不需要额外的“全局共享”上下文,就像QGLWidget一样。
要在属于不同窗口的QOpenGLWidget实例之间建立共享,请在实例化QApplication之前设置Qt::AA_ShareOpenGLContexts应用程序属性。这将触发所有QOpenGLWidget实例之间的共享,而无需进行任何步骤。