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 updatedresizeGL(): 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 timeresizeGL()orpaintGL()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()函數:
QImage QOpenGLWidgetPrivate::grabFramebuffer()void QOpenGLWidget::resizeEvent(QResizeEvent *e)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 ®ion, 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 ®ion, 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 |
Viewer::Viewer() { |
也就是說,當前OpenGL的版本是2.0的。早期版本的渲染VAO和VBO是可選的。那么我們手動設置format為OpenGL-3.3, Core profile:
1 |
Viewer::Viewer() { |
現在可以再跑一次代碼,就會發現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(); }
這里我們使用QOpenGLVertexArray,QOpenGLBuffer和QOpenGLShaderProgram的時候,最好看一下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
- QOpenGLWidget Simple Usage
- QOpenGLContext Class
- QSurface Class
- QSurfaceFormat Class
- QOpenGLFunctions Class
- QOpenGLFramebufferObject Class
- QBackingStore Class
- QPlatformBackingStore source
- QOpenGLWidget source
轉自:
https://realjishi.github.io/2018/04/06/QOpenGLWidgetFurther/


