QOpenGLWidget 揭秘

QOpenGLWidget揭秘

From QWidget to QOpenGLWidget

QOpenGLWidget是用于显示OpenGL所产生的图形,其使用很简单:

  • 创建自己的widget class, 继承QOpenGLWidget
  • 实现paintGL(), resizeGL(), initializeGL()

那么问题来了,paintGL(), resizeGL(), initializeGL()是什么?这三个函数如何被调用?如何实现这三个函数?

xxxGL() Functions

首先,这里要对上面的三个函数做一个简要的介绍。Qt document一上来就让我们实现三个xxxGL()函数,这三个函数的意思如下:

  • paintGL(): Renders the OpenGL scene. Gets called whenever the widget needs to be updated
  • resizeGL(): Sets up the OpenGL viewport, projection, etc. Gets called whenever the widget has been resized (and also when it is shown for the first time because all newly created widgets get a resize event automatically).
  • intializeGL(): Sets up the OpenGL resources and state. Gets called once before the first time resizeGL() or paintGL() is called.

那么我们再来看看它们对应的代码:

void QOpenGLWidget::initializeGL() {}
void QOpenGLWidget::resizeGL(int w, int h) {
    Q_UNUSED(w);
    Q_UNUSED(h);
}
void QOpenGLWidget::paintGL() {}

可以发现,这三个函数都是空的,所以我们必须继承自QOpenGLWidget,并且实现paintGL(), resizeGL(), initializeGL()来渲染图形。

How to render

如何调用paintGL(), resizeGL(), initializeGL()

现在我们再一次分析下QOpenGLWidget的源代码。

QOpenGLWidget::resizeEvent

resizeGL()的调用最为直接,我们可以在QOpenGLWidget::resizeEvent看到,代码如下:

void QOpenGLWidget::resizeEvent(QResizeEvent *e)
{
    Q_D(QOpenGLWidget);
    if (e->size().isEmpty()) {
        d->fakeHidden = true;
        return;
    }
    d->fakeHidden = false;
    d->initialize();
    if (!d->initialized)
        return;
    d->recreateFbo();
    resizeGL(width(), height());
    d->sendPaintEvent(QRect(QPoint(0, 0), size()));
}

QOpenGLWidgetPrivate::initialize()

我们先看QOpenGLWidget::initializeGL,其会在QOpenGLWidgetPrivate::initialize()中被调用:

void QOpenGLWidgetPrivate::initialize() {
    Q_Q(QOpenGLWidget);
    if (initialized)
        return;
    // ...
    q->initializeGL();
}

那么我们再来看看有哪些地方会调用initialize()函数:

  1. QImage QOpenGLWidgetPrivate::grabFramebuffer()
  2. void QOpenGLWidget::resizeEvent(QResizeEvent *e)
  3. bool QOpenGLWidget::event(QEvent *e)

不难理解,这些地方都是要确定OpenGL资源被创建才能执行后续操作。

void QOpenGLWidgetPrivate::invokeUserPaint()

QOpenGLWidget::paintGL()QOpenGLWidgetPrivate::invokeUserPaint()中被调用:

void QOpenGLWidgetPrivate::invokeUserPaint() {
    Q_Q(QOpenGLWidget);
    // ...
    q->paintGL();
    // ...
}

invokeUserPaint()void QOpenGLWidgetPrivate::render()被调用,可以发现render()会在void QOpenGLWidget::paintEvent(QPaintEvent *e)被调用:

void QOpenGLWidget::paintEvent(QPaintEvent *e)
{
    Q_UNUSED(e);
    Q_D(QOpenGLWidget);
    if (!d->initialized)
        return;
    if (updatesEnabled())
        d->render();
}

还有QImage QOpenGLWidgetPrivate::grabFramebuffer()也会调用render(),不再累述。


  • QOpenGLWidget::paintEvent()->…->paintGL()
  • void QOpenGLWidget::resizeEvent->…->resizeGL()
  • initializeGL()保证渲染之前资源被初始化

QWidget Review

这里我们先简单回顾基类QWidget的事件过程,时序图如下:

我们可以发现,所有的事件都会在eventloop被执行。那么结合我们这里的QOpenGLWidget,我们就可以知道,paintGL(), resizeGL(), initializeGL()都会在事件中被调用。

QOpenGLWidget Application

Simple Viewer

一般OpenGL教程都会画个三角形作为入门,那么这里也是一样。您可能会觉得简单,但是不要着急,后面会对这样一个简单的程序做一个深入的探究。

void Viewer::initializeGL() {
    QOpenGLFunctions *f = QOpenGLContext::currentContext()->functions();

    f->glClearColor( 0.1f, 0.1f, 0.1f, 1.0f );
    f->glEnable(GL_DEPTH_TEST);

    m_program = new QOpenGLShaderProgram( QOpenGLContext::currentContext() );
    m_program->addShaderFromSourceCode( QOpenGLShader::Vertex, VERTEX_SHADER );
    m_program->addShaderFromSourceCode( QOpenGLShader::Fragment, FRAGMENT_SHADER );
    m_program->link();
    m_program->bind();
}

void Viewer::paintGL() {
    // draw scene
    QOpenGLFunctions *f = QOpenGLContext::currentContext()->functions();
    f->glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    static GLfloat const Vertices[] = {
        -1.0f, -1.0f, -1.0f,
        1.0f, -1.0f, -1.0f,
        0.0f, 1.0f, -1.0f
    };

    QMatrix4x4 pmvMatrix;
    pmvMatrix.ortho(-1.0f, 1.0f, -1.0f, 1.0f, 0.0f, 10.0f);
    int vertexLocation = m_program->attributeLocation("inPos");
    int matrixLocation = m_program->uniformLocation("matrix");
    m_program->enableAttributeArray(vertexLocation);
    m_program->setAttributeArray(vertexLocation, Vertices, 3);
    m_program->setUniformValue(matrixLocation, pmvMatrix);
    f->glDrawArrays(GL_TRIANGLES, 0, 3);
    m_program->disableAttributeArray(vertexLocation);
}

代码确实很简单,为了节省空间省去效果图,大家可以自己动手实践。

QOpenGLWidget Exploration

本章假定读者对OpenGL的渲染有深入的了解
本章可能存在一些错误,可能会随时更新

上面我们仅仅给出了一个很简单的例子,但是在渲染的过程中,Qt究竟做了哪些工作?我们从以下几点出发,来深入讨论Qt OpenGL rendering的详细过程:

  • 如何将Default Framebuffer渲染到屏幕上?
  • 为何没有显式的调用swap buffer,哪里进行了调用?
  • QOpenGLContext用来做什么?
  • QOpenGLShaderProgram渲染之前为什么没有绑定VAO,VBO?它们在哪里被调用?

QOpenGLWidget Rendering Insight

QOpenGLWidget Rendering Flow

首先,我们来看下QOpenGLWidget是如何渲染到屏幕的。我们先回顾一下void QOpenGLWidget::paintEvent(QPaintEvent *e)事件的整个流程:

// paintEvent
void QOpenGLWidget::paintEvent(QPaintEvent *e) {
    d->render();
}

// render() insight
void QOpenGLWidgetPrivate::render() {
    q->makeCurrent();
    invokeUserPaint();
}

// makeCurrent() insight
void QOpenGLWidget::makeCurrent()
{
    d->context->makeCurrent(d->surface);
    if (d->fbo) // there may not be one if we are in reset()
        d->fbo->bind();
}

// invokeUsePaint()
void QOpenGLWidgetPrivate::invokeUserPaint() {
    QOpenGLContextPrivate::get(ctx)->defaultFboRedirect = fbo->handle();
    q->paintGL();
    QOpenGLContextPrivate::get(ctx)->defaultFboRedirect = 0;
}

有一行代码很重要:d->fbo->bind();,其意义就是我们绑定默认帧缓冲,和我们绑定Frame Buffer是一样的。在绑定之后,paintGL()就会将结果渲染到fbo中。那么问题来了,如何将fbo的数据渲染到屏幕?

Rendering to Screen

我们知道QOpenGLWidget继承自QWidget,那么我们来看看QWidget中的事件函数:

// Event:
bool QWidget::event(QEvent *event) {
    // ...
        case QEvent::UpdateRequest:
        d->syncBackingStore();
        break;
}

// d->syncBackingStore() insight
void QWidgetPrivate::syncBackingStore()
{
    if (paintOnScreen()) {
        repaint_sys(dirty);
        dirty = QRegion();
    } else if (QWidgetBackingStore *bs = maybeBackingStore()) {
        bs->sync();
    }
}

QWidgetBackingStore是做什么的?

先看下QBackingStore的描述:

The QBackingStore class provides a drawing area for QWindow.

QBackingStore enables the use of QPainter to paint on a QWindow with type RasterSurface. The other way of rendering to a QWindow is through the use of OpenGL with QOpenGLContext.
A QBackingStore contains a buffered representation of the window contents, and thus supports partial updates by using QPainter to only update a sub region of the window contents.

大体意思就是QBackingStore提供了绘制窗口QWindow(不是Widget)的渲染区域,亦渲染的逻辑层实现。在Qt中,提供了QPlatformBackingStore专门用于基于不同平台的backing store实现,对于不同的平台还有各种继承QPlatformBackingStore的实现,比如QDirectFbBackingStore, QOpenGLCompositorBackingStore等等。他们都对应了不同的createPlatformBackingStore用于创建对应的backing store。那么我们接着看代码:

// bs->sync() insight
void QWidgetBackingStore::sync()
{
    // ...
    if (syncAllowed())
        doSync();
}

// doSync() insight
void QWidgetBackingStore::doSync()
{
    {   // ...
        // We might have newly exposed areas on the screen if this function was
        // called from sync(QWidget *, QRegion)), so we have to make sure those
        // are flushed. We also need to composite the renderToTexture widgets.
        flush();
        // ...
    }
}

这里出现了很常见的flush()函数,我们看下它的实现:

void QWidgetBackingStore::flush(QWidget *widget)
{
    // ...
    if (!dirtyOnScreen.isEmpty()) {
        // ...
        qt_flush(target, dirtyOnScreen, store, tlw, widgetTexturesFor(tlw, tlw), this);
        // ...
    }

    // Render-to-texture widgets are not in dirtyOnScreen so flush if we have not done it above.
    if (!flushed && !hasDirtyOnScreenWidgets) {
            // ...
            qt_flush(target, QRegion(), store, tlw, tl, this);
            // ...
    }

    for (int i = 0; i < dirtyOnScreenWidgets->size(); ++i) {
        // ...
        qt_flush(w, *wd->needsFlush, store, tlw, widgetTexturesForNative, this);
    }
}

到现在出现了widgetTexture这样的关键字,那是不是将fbo转化为纹理,然后渲染到widget上呢? 为了证明这个假设,我们再来看下qt_flush:

// qt_flush insight
oid QWidgetBackingStore::qt_flush(QWidget *widget, const QRegion &region, QBackingStore *backingStore,
                                   QWidget *tlw, QPlatformTextureList *widgetTextures,
                                   QWidgetBackingStore *widgetBackingStore)
{
    const bool compositionWasActive = widget->d_func()->renderToTextureComposeActive;
    if (!widgetTextures) {
        widget->d_func()->renderToTextureComposeActive = false;
        // Detect the case of falling back to the normal flush path when no
        // render-to-texture widgets are visible anymore. We will force one
        // last flush to go through the OpenGL-based composition to prevent
        // artifacts. The next flush after this one will use the normal path.
        if (compositionWasActive)
            widgetTextures = qt_dummy_platformTextureList;
    } else {
        widget->d_func()->renderToTextureComposeActive = true;
    }
    // When changing the composition status, make sure the dirty region covers
    // the entire widget.  Just having e.g. the shown/hidden render-to-texture
    // widget's area marked as dirty is incorrect when changing flush paths.
    if (compositionWasActive != widget->d_func()->renderToTextureComposeActive)
        effectiveRegion = widget->rect();
    // re-test since we may have been forced to this path via the dummy texture list above
    if (widgetTextures) {
        qt_window_private(tlw->windowHandle())->compositing = true;
        widget->window()->d_func()->sendComposeStatus(widget->window(), false);
        // A window may have alpha even when the app did not request
        // WA_TranslucentBackground. Therefore the compositor needs to know whether the app intends
        // to rely on translucency, in order to decide if it should clear to transparent or opaque.
        const bool translucentBackground = widget->testAttribute(Qt::WA_TranslucentBackground);
        backingStore->handle()->composeAndFlush(widget->windowHandle(), effectiveRegion, offset,
                                                widgetTextures, translucentBackground);
        widget->window()->d_func()->sendComposeStatus(widget->window(), true);
    } else
#endif
        backingStore->flush(effectiveRegion, widget->windowHandle(), offset);
}

现在出现了很多关键的代码:

  • backingStore->handle()->composeAndFlush();
  • backingStore->flush()

让我们逐一分析:

void QPlatformBackingStore::composeAndFlush(QWindow *window, const QRegion &region,
                                            const QPoint &offset,
                                            QPlatformTextureList *textures,
                                            bool translucentBackground)
{
    // ...
    funcs->glGenTextures(1, &d_ptr->textureId);
    funcs->glBindTexture(GL_TEXTURE_2D, d_ptr->textureId);

    // ...        
    textureId = toTexture(deviceRegion(region, window, offset), &d_ptr->textureSize, &flags);

    // ...
    d_ptr->context->swapBuffers(window);
}

这里就很清楚了,在composeAndFlush中,我们绑定纹理,将指定区域转化为纹理,swapBuffers(window)。再看一眼toTexture:

// toTexture() insight
GLuint QPlatformBackingStore::toTexture(const QRegion &dirtyRegion, QSize *textureSize, TextureFlags *flags) const
{
    // ...
    QImage image = toImage();

    // ...
    funcs->glGenTextures(1, &d_ptr->textureId);
    funcs->glBindTexture(GL_TEXTURE_2D, d_ptr->textureId);
    return d_ptr->textureId;
}

// toImage() insight
/*!
  Implemented in subclasses to return the content of the backingstore as a QImage.
  If QPlatformIntegration::RasterGLSurface is supported, either this function or
  toTexture() must be implemented.
 */
QImage QPlatformBackingStore::toImage() const
{
    return QImage();
}

突然发现,QPlatformBackingStore::toImage()是个空函数,什么都不干。让我们在回想一下QPlatformBackingStore,上面说过很多的backing store根据自己的平台实现了自己的backing store,那么这样就清楚了,toImage()视平台而定!
同样的,backingStore->flush()也是平台相关的实现,这里就不再累述平台相关的代码了。


QOpenGLWidget首先将用户定制的paintGL()渲染到fbo中;然后通过QWidget::event调用sync(),同步fbo和屏幕。其中首先将fbo转化为纹理,然后根据平台渲染到屏幕上去,swapBuffer也会在这中间完成。这个过程就是所谓的Qt Native OpenGL


QOpenGLContext

QOpenGLContext是对OpenGL中context的一个封装。OpenGL自身是一个巨大的状态机(State Machine):一系列的变量描述OpenGL此刻应当如何运行。OpenGL的状态通常被称为OpenGL上下文(Context)。我们通常使用如下途径去更改OpenGL状态:设置选项,操作缓冲。最后,我们使用当前OpenGL上下文来渲染。渲染时,一定要先创建context,才能进行之后的操作!

OOpenGLContext还包含了对QOpenGLFunctions的访问,主要目的是对context进行设置,并且增加了兼容性。

QOpenGLFunctions *f = QOpenGLContext::currentContext()->functions();

Where is VAO & VBO?

在Simple Viewer的代码中,并没有和常见的Modern OpenGL一样必须有VAO,VBO等的配置。这时候突然想到OpenGL的渲染区域是由QSurface提供的,而QSurface是由QSurfaceFormat配置。那么我们先看看当前Viewer的版本是什么:

1
2
3
4
5
Viewer::Viewer() {
qDebug() << this->format().version();
}
// output:
// QPair(2,0)

也就是说,当前OpenGL的版本是2.0的。早期版本的渲染VAO和VBO是可选的。那么我们手动设置format为OpenGL-3.3, Core profile

1
2
3
4
5
6
7
Viewer::Viewer() {
// set version
QSurfaceFormat format;
format.setProfile( QSurfaceFormat::CoreProfile );
format.setVersion( 3, 3 );
this->setFormat( format );
}

现在可以再跑一次代码,就会发现nothing but background!

可以看下initializeGL()中的代码,会发现没有绑定任何的VBO;在paintGL()中,会发现,每次的渲染都是直接从内存中取顶点数据,这和我们平常的渲染流程有着很大的不同。为了和平时渲染代码保持一致,这里我们自己实现VAO,VBO来完成渲染,关键代码如下:

void Viewer::initializeGL() {
    QOpenGLFunctions *f = QOpenGLContext::currentContext()->functions();
    f->glClearColor( 0.1f, 0.1f, 0.1f, 1.0f );
    f->glEnable( GL_DEPTH_TEST );

    m_program = new QOpenGLShaderProgram( QOpenGLContext::currentContext() );
    m_program->addShaderFromSourceCode( QOpenGLShader::Vertex, VERTEX_SHADER );
    m_program->addShaderFromSourceCode( QOpenGLShader::Fragment, FRAGMENT_SHADER );
    m_program->link();
    m_program->bind();

    m_vbo = new QOpenGLBuffer;
    m_vbo->create();
    m_vbo->bind();
    m_vbo->setUsagePattern( QOpenGLBuffer::StaticDraw );
    m_vbo->allocate( Vertices, sizeof(Vertices) );

    m_vao = new QOpenGLVertexArrayObject;
    m_vao->create();
    m_vao->bind();
//    f->glEnableVertexAttribArray(0);
    m_program->enableAttributeArray(0);
//    f->glVertexAttribPointer( 0, 3, GL_FLOAT, GL_FALSE, 0, 0 );
    m_program->setAttributeArray( 0, GL_FLOAT, NULL, 3, 0 );
    m_vbo->release();
    m_vao->release();
    m_program->release();
}

void Viewer::paintGL() {
    // draw scene
    QOpenGLFunctions *f = QOpenGLContext::currentContext()->functions();
    f->glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
    m_program->bind();
    m_vao->bind();

    int matrixLocation = m_program->uniformLocation( "matrix" );
    QMatrix4x4 pmvMatrix;
    pmvMatrix.ortho( -1.0f, 1.0f, -1.0f, 1.0f, 0.0f, 10.0f);
    m_program->setUniformValue( matrixLocation, pmvMatrix );

    f->glDrawArrays( GL_TRIANGLES, 0, 3 );
    m_vao->release();
    m_program->release();
}

这里我们使用QOpenGLVertexArrayQOpenGLBufferQOpenGLShaderProgram的时候,最好看一下wrapper函数对应的OpenGL代码,比如:

void QOpenGLShaderProgram::setAttributeArray
    (int location, GLenum type, const void *values, int tupleSize, int stride)
{
    Q_D(QOpenGLShaderProgram);
    Q_UNUSED(d);
    if (location != -1) {
        d->glfuncs->glVertexAttribPointer(location, tupleSize, type, GL_TRUE,
                              stride, values);
    }
}

我个人一点心得是,可以直接调用对应的OpenGL函数,少走一些弯路。

Reference

0%