Qt的Graphics-View框架和OpenGL結合詳解
演示程序下載地址:這里
程序源代碼下載地址:這里
這是一篇純技術文,介紹了這一個月來我抽時間研究的成果。
Qt中有一個非常炫的例子:Boxes,它展示了Qt能夠讓其Graphics–View框架和Qt的OpenGL模塊結合起來,渲染出非常出色的效果。其實我私自認為憑這個程序,已經有很多游戲開發者關注Qt了,因為游戲開發一個非常常見的模塊就是UI,一般情況下游戲引擎提供的UI模塊比較弱,基本上都是游戲引擎+第三方GUI庫進行結合的。但是Qt以其Graphics–View框架能夠非常輕松地將UI控件嵌入場景中,而且能夠和OpenGL底層共存,更重要的是,憑借着Qt的qss,Qt可以定制許多GUI元素,這是非常具有吸引力的。所以說,如果大家對游戲開發感興趣,那么不妨看一下Qt。
好了,下面介紹一下前幾天我制作並發布的一個demo。這個demo是對Boxes這個例子進行模仿,結合學習《OpenGL超級寶典》,制作而成的,由於最近比較忙,所以總共花了將近一個月才完成,不得不說效率有點兒低。
首先從MainWindow.cpp這個文件說起吧,一開始是要初始化MainWindow類的,這個類是繼承QMainWindow的,這里重點說說它的構造函數:
MainWindow::MainWindow(QWidget*pParent): QMainWindow(pParent) { QGLWidget*pWidget=newQGLWidget(QGLFormat(QGL::SampleBuffers),this); pWidget->makeCurrent(); //scene的內容 GraphicsScene*pScene=newGraphicsScene(this); OpenGLView*pView=newOpenGLView(this); pView->setViewport(pWidget); pView->setViewportUpdateMode(QGraphicsView::FullViewportUpdate); pView->setScene(pScene); //選擇不同的着色器的時候進行着色器連接 connect(pScene,SIGNAL(SwitchShader(constQString&)), pView,SLOT(SwitchShader(constQString&))); connect(pScene,SIGNAL(SetLightPos(constQVector3D&)), pView,SLOT(SetLightPos(constQVector3D&))); setCentralWidget(pView); setWindowTitle(tr("Lightforshader")); resize(640,360); }
首先在我們創建了一個QWidget,然后調用makeCurrent()成員函數,其實意思是讓它的rendercontext設為當前的rendercontext。隨后建立的是OpenGLView,這個OpenGLView是來自於QGraphicsView的,它的初始化和其祖先的並無二致,隨后一句非常重要:setViewport(),它的作用是將QGLWidget設置為OpenGLView的viewport,這樣的話背景的rendercontext不再是rastercontext而是OpenGLcontext了,否則場景的背景還是需要用CPU渲染的,效率低下。接着是兩段建立連接的代碼。最后設置的是窗口大小和標題什么的,一開始還是非常簡單的。
接下來我們看看OpenGLView是怎么定義的:
classOpenGLView:publicQGraphicsView, protectedQOpenGLFunctions { Q_OBJECT public: OpenGLView(QWidget*pParent=0); virtual~OpenGLView(void); voidsetScene(GraphicsScene*pScene); publicslots: boolSwitchShader(constQString&shaderFileName); voidSetLightPos(constQVector3D&lightPos=QVector3D()); protected: voidresizeEvent(QResizeEvent*pEvent); voidmousePressEvent(QMouseEvent*pEvent); voidmouseReleaseEvent(QMouseEvent*pEvent); voidmouseMoveEvent(QMouseEvent*pEvent); voidwheelEvent(QWheelEvent*pEvent); voiddrawBackground(QPainter*pPainter,constQRectF&rect); private: voidInitGL(void); voidResizeGL(intwidth,intheight); voidPaintGL(void); voidDrawAxis(void); boolSetupShaders(void); Cameram_Camera; Format3DSm_3DS; //着色器 QOpenGLShader*m_pVertexShader; QOpenGLShaderProgram*m_pShaderProgram; };
這里我們在它的成員中添加了一個攝像機,一個3ds模型實例,還有一個頂點着色器和着色器程序類。在上次的博客中講到了在這種情況最好使用類指針而不是類成員作為數據成員,這里我索性把着色器程序類也做成了指針成員了。
由於OpenGLView類實現比較長,這里我着重說一下其中的幾個函數。下面是drawBackground()函數的實現:
voidOpenGLView::drawBackground(QPainter*pPainter, constQRectF&) { pPainter->beginNativePainting(); glPushAttrib(GL_ALL_ATTRIB_BITS); InitGL(); ResizeGL(pPainter->device()->width(), pPainter->device()->height()); PaintGL(); glPopAttrib(); pPainter->endNativePainting(); }
為什么選擇drawBackground()?因為我們想要得到一種效果,OpenGL在底層繪制,上面繪制控件,其實自從QPainter有了beginNativePainting()和endNativePainting()這兩個函數,我們就可以進行純OpenGL的繪制了。這里還要注意的是,因為繪制控件也是使用OpenGL的context,這樣簡單調用會讓OpenGL的狀態混亂,所以需要將各種狀態通過glPushAttrib(GL_ALL_ATTRIB_BITS);保存起來,然后初始化我們的OpenGL狀態,以及繪圖,最后記得glPopAttrib();還原所有狀態供2D繪制。這里就不像以往的GLWidget套路了,因為GLWidget里面的initializeGL()函數只調用一次,paintGL()函數調用多次,但是在這里,只要有刷新的消息(通過update()或repaint()觸發),就必須調用InitGL()函數來進行OpenGL狀態的設置,否則先前設置的所有狀態都消失了。
接下來看看ResizeEvent()函數:
voidOpenGLView::resizeEvent(QResizeEvent*pEvent) { scene()->setSceneRect(0.0, 0.0, pEvent->size().width(), pEvent->size().height()); pEvent->accept(); }
這里由於我們已經設置了scene為GraphicsScene類的實例指針,因此scene()是非空的,我們將場景限制為view的大小,這樣可以避免一些刷新的問題。
而paintGL()函數也是相當的簡單。
voidOpenGLView::PaintGL(void) { glClear(GL_COLOR_BUFFER_BIT| GL_DEPTH_BUFFER_BIT); glPushMatrix(); m_Camera.Apply(); m_3DS.RenderGL(); if(g_ShowAxis)DrawAxis(); glPopMatrix(); }
接着我向大家介紹一下GraphicsScene類:
classGraphicsScene:publicQGraphicsScene { Q_OBJECT public: GraphicsScene(QObject*pParent=0); voidSetCamera(Camera*pCamera); signals: voidSwitchShader(constQString&shaderFileName); voidSetLightPos(constQVector3D&pos); protected: voidmousePressEvent(QGraphicsSceneMouseEvent*pEvent); voidmouseMoveEvent(QGraphicsSceneMouseEvent*pEvent); voidmouseReleaseEvent(QGraphicsSceneMouseEvent*pEvent); voidwheelEvent(QGraphicsSceneWheelEvent*pEvent); privateslots: voidFeedback(void); private: //鼠標事件需要 QPointFm_LastPos; Camera*m_pCamera; };
這里的GraphicsScene類保存的是來自view的Camera和一些信號以及事件的處理。在實現上也說一下它的構造函數吧。
GraphicsScene::GraphicsScene(QObject*pParent): QGraphicsScene(pParent),m_pCamera(Q_NULLPTR) { ClickableTextItem*pTextItem=newClickableTextItem(Q_NULLPTR); pTextItem->setPos(10.0,10.0); pTextItem->setHtml(tr("<fontcolor=white>" "MadeByJiangcaiyang<br>" "CreatedinSeptember<br>" "Clickforfeedback." "</font>")); connect(pTextItem,SIGNAL(Clicked()), this,SLOT(Feedback())); addItem(pTextItem); ShaderOptionDialog*pDialog=newShaderOptionDialog; connect(pDialog,SIGNAL(SwitchShader(constQString&)), this,SIGNAL(SwitchShader(constQString&))); connect(pDialog,SIGNAL(SetLightPos(constQVector3D&)), this,SIGNAL(SetLightPos(constQVector3D&))); QGraphicsProxyWidget*pProxy=addWidget(pDialog,Qt::Window|Qt::WindowTitleHint); pProxy->setPos(100,200); }
我們創建了一個ClickableTextItem類,它繼承於QGraphicsTextItem,它被擺在左上角,顯示三排可以點擊的文字。隨后添加了一個對話框,設置信號和槽完畢后就用代理放入場景中了。整個過程也是非常簡單的。
我測試了下,整個程序在我的Ubuntu13.04下能夠正常運行。只是由於顯卡不同(Ubuntu下較難支持nvidia顯卡,使用的是intel集顯),specularOpt效果出不來。