目录

一、说明

二、QOpenGLWidget 类对象

2.1 概要 

2.1.1 函数功能 

 2.1. 2 三个虚函数

 2.1.3 信号

 2.2 详细说明

2.2.1  三个虚函数

2.2.2 绘画技巧

2.2.3 OpenGL 函数调用、标头和 QOpenGLFunctions 

三、实现代码示例

3.1 最简模式

3.2 与 QGLWidget 的关系

3.3 与 QGLWidget 的差异

 3.4 多重采样

3.5 线程化

3.6 上下文共享

四、资源初始化和清理

五、局限性 


一、说明

        据说QOpenGLWidget是用来取代QGLWidget的继承者,我们通过多次尝试,发现QGLWidget的鲁棒性很差,用来开发游戏,其步履和亚马逊泥沼中行走有类同,谈不上体验,只能叫半成品的测试。从本文之后,我们将尝试QOpenGLWidget,看看是不是人类的曙光再次诞临,若不是也不必太勉强了。用用GLUT或pygame也能度过。

二、QOpenGLWidget 类对象

        该类 QOpenGLWidget是用于渲染 OpenGL 图形的小部件。更多的

【QOpenGL实践】QOpenGLWidget-LMLPHP

2.1 概要 

2.1.1 函数功能 

 2.1. 2 三个虚函数

 2.1.3 信号

 2.2 详细说明

2.2.1  三个虚函数

2.2.2 绘画技巧

2.2.3 OpenGL 函数调用、标头和 QOpenGLFunctions 

        在进行 OpenGL 函数调用时,强烈建议避免直接调用函数。相反,更喜欢使用QOpenGLFunctions(在制作便携式应用程序时)或版本化变体(例如,QOpenGLFunctions_3_2_Core当针对现代的、仅限桌面的 OpenGL 时,类似的)。这样,应用程序将在所有 Qt 构建配置中正常工作,包括执行动态 OpenGL 实现加载的配置,这意味着应用程序不直接链接到 GL 实现,因此直接函数调用不可行。

        在paintGL()当前上下文中始终可以通过 caling 访问currentContext()。在此上下文中QOpenGLFunctions,可以通过调用 来检索已初始化的、可供使用的实例functions()。为每个 GL 调用添加前缀的替代方法是继承QOpenGLFunctionsinitializeOpenGLFunctions()调用initializeGL().

        至于 OpenGL 标头,请注意,在大多数情况下,不需要直接包含任何标头(例如 GL.h)。与 OpenGL 相关的 Qt 头文件将包括 qopengl.h,而 qopengl.h 又将包括适合系统的头文件。这可能是 OpenGL ES 3.x 或 2.0 标头、可用的最高版本或系统提供的 gl.h。此外,扩展头文件的副本(在某些系统上称为 glext.h)作为 Qt 的一部分提供,适用于 OpenGL 和 OpenGL ES。在可行的情况下,这些将自动包含在平台上。这意味着来自 ARB、EXT、OES 扩展的常量和函数指针类型定义自动可用。

三、实现代码示例

3.1 最简模式

首先,最简单的QOpenGLWidget子类可能如下所示:

class MyGLWidget : public QOpenGLWidget
{
public:
    MyGLWidget(QWidget *parent) : QOpenGLWidget(parent) { }

protected:
    void initializeGL() override
    {
        // Set up the rendering context, load shaders and other resources, etc.:
        QOpenGLFunctions *f = QOpenGLContext::currentContext()->functions();
        f->glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
        ...
    }

    void resizeGL(int w, int h) override
    {
        // Update projection matrix and other size related settings:
        m_projection.setToIdentity();
        m_projection.perspective(45.0f, w / float(h), 0.01f, 100.0f);
        ...
    }

    void paintGL() override
    {
        // Draw the scene:
        QOpenGLFunctions *f = QOpenGLContext::currentContext()->functions();
        f->glClear(GL_COLOR_BUFFER_BIT);
        ...
    }

};

        或者,可以通过派生来避免每个 OpenGL 调用的前缀QOpenGLFunctions

class MyGLWidget : public QOpenGLWidget, protected QOpenGLFunctions
{
    ...
    void initializeGL() override
    {
        initializeOpenGLFunctions();
        glClearColor(...);
        ...
    }
    ...
};

        要获得与给定 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); // must be called before the widget or its parent window gets shown

        在 OpenGL 3.0+ 上下文中,当可移植性并不重要时,版本化QOpenGLFunctions变体可以轻松访问给定版本中可用的所有现代 OpenGL 函数:

 
void paintGL() override
{
    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();
}

3.2 与 QGLWidget 的关系

遗留的QtOpenGL模块(以 QGL 为前缀的类)提供了一个名为 的小部件QGLWidgetQOpenGLWidget旨在成为它的现代替代品。因此,特别是在新的应用中,一般建议使用QOpenGLWidget.

虽然 API 非常相似,但两者之间有一个重要的区别:QOpenGLWidget始终使用帧缓冲区对象在屏幕外渲染。QGLWidget另一方面使用本机窗口和表面。后者在复杂的用户界面中使用它时会引起问题,因为根据平台的不同,此类本机子小部件可能具有各种限制,例如关于堆叠顺序的限制。QOpenGLWidget通过不创建单独的本机窗口来避免这种情况。

由于由帧缓冲区对象支持, 的行为与设置为或 的更新行为QOpenGLWidget非常相似。这意味着内容在调用之间被保留,以便增量渲染成为可能。使用(当然也使用默认的更新行为)通常情况并非如此,因为交换缓冲区会使后台缓冲区留下未定义的内容。QOpenGLWindowPartialUpdateBlitPartialUpdateBlendpaintGL()QGLWidgetQOpenGLWindow

笔记

大多数应用程序不需要增量渲染,因为它们将在每次绘制调用时渲染视图中的所有内容。在这种情况下,尽早在paintGL().这有助于使用基于图块的架构的移动 GPU 认识到图块缓冲区不需要重新加载帧缓冲区的先前内容。省略clear调用可能会导致此类系统的性能显着下降。

笔记

避免winId()调用QOpenGLWidget.此函数会触发本机窗口的创建,从而导致性能下降并可能出现渲染故障。

3.3 与 QGLWidget 的差异

QOpenGLWidget除了由帧缓冲区对象支持的主要概念差异之外,旧版本之间还存在许多较小的内部差异QGLWidget

  • 调用时的 OpenGL 状态paintGL()QOpenGLWidget通过 glViewport() 设置视口。它不执行任何清除操作。

  • 开始通过 绘制时清除QPainter。与常规小部件不同,QGLWidget默认值为truefor autoFillBackground。然后,每次begin()使用时都会对调色板的背景颜色执行清除操作。QOpenGLWidget不遵循这个:autoFillBackground默认为 false,就像任何其他小部件一样。唯一的例外是用作其他小部件(例如QGraphicsView.在这种情况下autoFillBackground将自动设置为 true 以确保与QGLWidget基于视口的兼容性。

 3.4 多重采样

        QSurfaceFormat要启用多重采样,请在传递给 的上设置请求的样本数setFormat()。在不支持它的系统上,该请求可能会被忽略。

多重采样支持需要支持多重采样渲染缓冲区和帧缓冲区位块传送。在 OpenGL ES 2.0 实现中,这些可能不会出现。这意味着多重采样将不可用。对于现代 OpenGL 版本和 OpenGL ES 3.0 及更高版本,这通常不再是问题。

3.5 线程化

paintGL()通过公开小部件来支持在工作线程上执行离屏渲染,例如生成随后在 GUI/主线程中使用的纹理,QOpenGLContext以便可以在每个线程上创建与其共享的其他上下文。

通过重新实现不执行任何操作,QOpenGLWidget可以直接绘制到GUI/主线程外部的帧缓冲区。paintEvent()上下文的线程关联性必须通过更改moveToThread()。之后,makeCurrent()doneCurrent()可在工作线程上使用。之后请小心将上下文移回 GUI/主线程。

与 不同的是QGLWidget,仅针对 触发缓冲区交换QOpenGLWidget是不可能的,因为它没有真正的屏幕本机表面。相反,由小部件堆栈来管理 GUI 线程上的组​​合和缓冲区交换。当线程完成帧缓冲区更新后,调用GUI/主线程来安排合成。update()

当 GUI/主线程执行合成时,必须格外小心,避免使用帧缓冲区。当乐曲开始和结束时,将发出aboutToCompose()和信号。frameSwapped()它们在 GUI/主线程上发出。这意味着通过使用直接连接aboutToCompose()可以阻塞 GUI/主线程,直到工作线程完成渲染。之后,工作线程必须不再执行渲染,直到frameSwapped()发出信号为止。如果这是不可接受的,工作线程必须实现双缓冲机制。这涉及使用完全由线程控制的替代渲染目标(例如附加的帧缓冲区对象)进行绘制,并QOpenGLWidget在适当的时间位块传输到 的帧缓冲区。

3.6 上下文共享

当多个 QOpenGLWidget 作为子级添加到同一个顶级 widget 时,它们的上下文将相互共享。这不适用于QOpenGLWidget属于不同窗口的实例。

这意味着同一窗口中的所有 QOpenGLWidget 都可以访问彼此的可共享资源,例如纹理,并且不需要额外的“全局共享”上下文,就像QGLWidget.

QOpenGLWidget要在属于不同窗口的实例之间设置共享,请AA_ShareOpenGLContexts在实例化之前设置应用程序属性QApplication。这将触发所有QOpenGLWidget实例之间的共享,无需任何进一步的步骤。

QOpenGLContext创建与 的上下文共享纹理等资源的额外实例QOpenGLWidget也是可能的。只需在调用之前传递从context()to返回的指针即可。生成的上下文还可以在不同的线程上使用,从而允许线程化纹理生成和异步纹理上传。setShareContext()create()

请注意,QOpenGLWidget当涉及底层图形驱动程序时,期望资源共享的标准一致实现。例如,某些驱动程序(特别是针对移动和嵌入式硬件的驱动程序)在现有上下文和以后创建的其他上下文之间设置共享时存在问题。当尝试利用不同线程之间的共享资源时,其他一些驱动程序可能会以意想不到的方式运行。

四、资源初始化和清理

        无论何时调用和 ,的关联 OpenGL上下文QOpenGLWidget都保证是最新的。在调用之前不要尝试创建 OpenGL 资源。例如,在子类的构造函数中尝试编译着色器、初始化顶点缓冲区对象或上传纹理数据将失败。这些操作必须推迟到。一些 Qt 的 OpenGL 帮助器类(例如或)具有匹配的延迟行为:它们可以在没有上下文的情况下实例化,但所有初始化都会延迟到或类似的调用。这意味着它们可以用作子类中的普通(非指针)成员变量,但只能从.但请注意,并非所有类都是这样设计的。如有疑问,请将成员变量设置为指针,并分别在析构函数中动态创建和销毁实例。initializeGL()paintGL()initializeGL()initializeGL()QOpenGLBufferQOpenGLVertexArrayObjectcreate()QOpenGLWidgetcreate()initializeGL()initializeGL()

        释放资源还需要当前上下文。因此,执行此类清理的析构函数预计会makeCurrent()在继续销毁任何 OpenGL 资源或包装器之前调用。避免通过deleteLater()或 的育儿机制进行延迟删除QObject。无法保证在相关实例真正被销毁时正确的上下文将是当前的。

因此,在资源初始化和销毁​​方面,典型的子类通常如下所示:

class MyGLWidget : public QOpenGLWidget
{
    ...

private:
    QOpenGLVertexArrayObject m_vao;
    QOpenGLBuffer m_vbo;
    QOpenGLShaderProgram *m_program;
    QOpenGLShader *m_shader;
    QOpenGLTexture *m_texture;
};

MyGLWidget::MyGLWidget()
    : m_program(0), m_shader(0), m_texture(0)
{
    // No OpenGL resource initialization is done here.
}

MyGLWidget::~MyGLWidget()
{
    // Make sure the context is current and then explicitly
    // destroy all underlying OpenGL resources.
    makeCurrent();

    delete m_texture;
    delete m_shader;
    delete m_program;

    m_vbo.destroy();
    m_vao.destroy();

    doneCurrent();
}

void MyGLWidget::initializeGL()
{
    m_vao.create();
    if (m_vao.isCreated())
        m_vao.bind();

    m_vbo.create();
    m_vbo.bind();
    m_vbo.allocate(...);

    m_texture = new QOpenGLTexture(QImage(...));

    m_shader = new QOpenGLShader(...);
    m_program = new QOpenGLShaderProgram(...);

    ...
}

        这自然不是唯一可能的解决方案。一种替代方法是使用aboutToBeDestroyed()的信号QOpenGLContext。通过使用直接连接将插槽连接到此信号,每当QOpenGLContext要释放底层本机上下文句柄或整个实例时,就可以执行清理。以下代码片段原则上与上一个代码片段等效:

void MyGLWidget::initializeGL()
{
    // context() and QOpenGLContext::currentContext() are equivalent when called from initializeGL or paintGL.
    connect(context(), &QOpenGLContext::aboutToBeDestroyed, this, &MyGLWidget::cleanup);
}

void MyGLWidget::cleanup()
{
    makeCurrent();
    delete m_texture;
    m_texture = 0;
    ...
    doneCurrent();
}
        对于在其生命周期内多次更改其关联的顶级窗口的小部件,组合方法至关重要。每当小部件或其父级重新设置父级以使顶级窗口变得不同时,小部件的关联上下文就会被销毁并创建一个新的上下文。然后调用initializeGL()所有 OpenGL 资源必须重新初始化的位置。因此,执行正确清理的唯一选择是连接到上下文的 aboutToBeDestroyed() 信号。请注意,当信号发出时,所讨论的上下文可能不是当前的上下文。因此,最好的做法是调用makeCurrent()已连接的槽。此外,必须从派生类的析构函数执行相同的清理步骤,因为当小部件被销毁时,连接到信号的槽将不会被调用。

        注意:

        设置后AA_ShareOpenGLContexts,小部件的上下文永远不会更改,即使在重新设置父级时也不会更改,因为保证也可以从新的顶级上下文访问小部件的关联纹理。

由于上下文共享,正确的清理尤为重要。即使每个QOpenGLWidget的关联上下文与 一起被销毁QOpenGLWidget,该上下文中的可共享资源(如纹理)将保持有效,直到其中存在的顶级窗口QOpenGLWidget被销毁。此外,诸如某些 Qt 模块之类的设置AA_ShareOpenGLContexts可能会触发更广泛的共享上下文,从而可能导致相关资源在应用程序的整个生命周期内保持活动状态。因此,最安全、最稳健的方法始终是对QOpenGLWidget.

五、局限性 

        将其他小部件放在下面并使其QOpenGLWidget透明不会导致预期的结果:下面的小部件将不可见。这是因为实际上QOpenGLWidget是在所有其他常规非 OpenGL 小部件之前绘制的,因此透视类型的解决方案是不可行的。其他类型的布局,例如在 之上放置小部件QOpenGLWidget,将按预期运行。

        当绝对必要时,可以通过WA_AlwaysStackOnTopQOpenGLWidget.但请注意,这会破坏堆叠顺序,例如,不可能在 的顶部放置其他小部件,因此它只能在需要QOpenGLWidget半透明且其他小部件在下面可见的情况下使用。QOpenGLWidget

请注意,当下面没有其他小部件并且目的是拥有半透明窗口时,这并不适用。在这种情况下WA_TranslucentBackground,在顶层窗口上进行设置的传统方法就足够了。请注意,如果仅需要透明区域QOpenGLWidget,则启用后WA_NoSystemBackground需要将其转回。此外,根据系统的不同,可能还需要为的上下文请求 alpha 通道。falseWA_TranslucentBackgroundQOpenGLWidgetsetFormat()

QOpenGLWidget支持多种更新行为,就像QOpenGLWindow.在保留模式下,上一次调用的渲染内容paintGL()可在下一次调用中使用,从而允许增量渲染。在非保留模式下,内容会丢失,并且paintGL()预计实现会重绘视图中的所有内容。

在 Qt 5.5 之前,默认行为是在调用QOpenGLWidget之间保留渲染的内容paintGL()。自 Qt 5.5 起,默认行为不再保留,因为这提供了更好的性能,并且大多数应用程序不需要以前的内容。这也类似于基于 OpenGL 的语义,并且与每个帧的颜色和辅助缓冲区无效的QWindow默认行为相匹配。QOpenGLWindow要恢复保留的行为,请setUpdateBehavior()使用 进行调用PartialUpdate

        注意

        由于与其他基于内容的组合的工作方式,显示QOpenGLWidget需要在关联的顶级窗口的后备存储中使用 Alpha 通道QWidget。如果没有alpha通道,渲染的内容QOpenGLWidget将不可见。当使用低于 24 的颜色深度时,这在远程显示设置(例如使用 Xvnc)中的 Linux/X11 上尤其重要。例如,16 的颜色深度通常会映射到使用具有以下格式的后备存储图像Format_RGB16(RGB565),没有为 Alpha 通道留下空间。因此,如果在与窗口中的其他小部件正确获取合成内容时遇到问题QOpenGLWidget,请确保服务器(例如 vncserver)配置为 24 或 32 位深度而不是 16 位深度。

04-18 19:37