OpenGL(三)之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實例之間的共享,而無需進行任何步驟。