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 = new QGLWidget( QGLFormat( QGL::SampleBuffers ), this );
pWidget->makeCurrent( );
// scene的内容
GraphicsScene* pScene = new GraphicsScene( this );
OpenGLView* pView = new OpenGLView( this );
pView->setViewport( pWidget );
pView->setViewportUpdateMode( QGraphicsView::FullViewportUpdate );
pView->setScene( pScene );
// 选择不同的着色器的时候进行着色器连接
connect( pScene, SIGNAL( SwitchShader( const QString& ) ),
pView, SLOT( SwitchShader( const QString& ) ) );
connect( pScene, SIGNAL( SetLightPos( const QVector3D& ) ),
pView, SLOT( SetLightPos( const QVector3D& ) ) );
setCentralWidget( pView );
setWindowTitle( tr( "Light for shader" ) );
resize( 640, 360 );
}
首先在我们创建了一个QWidget,然后调用makeCurrent()成员函数,其实意思是让它的rendercontext设为当前的rendercontext。随后建立的是OpenGLView,这个OpenGLView是来自于QGraphicsView的,它的初始化和其祖先的并无二致,随后一句非常重要:setViewport(),它的作用是将QGLWidget设置为OpenGLView的viewport,这样的话背景的rendercontext不再是rastercontext而是OpenGLcontext了,否则场景的背景还是需要用CPU渲染的,效率低下。接着是两段建立连接的代码。最后设置的是窗口大小和标题什么的,一开始还是非常简单的。
接下来我们看看OpenGLView是怎么定义的:
class OpenGLView: public QGraphicsView,
protected QOpenGLFunctions
{
Q_OBJECT
public:
OpenGLView( QWidget* pParent = 0 );
virtual ~OpenGLView( void );
void setScene( GraphicsScene* pScene );
public slots:
bool SwitchShader( const QString& shaderFileName );
void SetLightPos( const QVector3D& lightPos = QVector3D( ) );
protected:
void resizeEvent( QResizeEvent* pEvent );
void mousePressEvent( QMouseEvent* pEvent );
void mouseReleaseEvent( QMouseEvent* pEvent );
void mouseMoveEvent( QMouseEvent* pEvent );
void wheelEvent( QWheelEvent* pEvent );
void drawBackground( QPainter* pPainter, const QRectF& rect );
private:
void InitGL( void );
void ResizeGL( int width, int height );
void PaintGL( void );
void DrawAxis( void );
bool SetupShaders( void );
Camera m_Camera;
Format3DS m_3DS;
// 着色器
QOpenGLShader* m_pVertexShader;
QOpenGLShaderProgram* m_pShaderProgram;
};
这里我们在它的成员中添加了一个摄像机,一个3ds模型实例,还有一个顶点着色器和着色器程序类。在上次的博客中讲到了在这种情况最好使用类指针而不是类成员作为数据成员,这里我索性把着色器程序类也做成了指针成员了。
由于OpenGLView类实现比较长,这里我着重说一下其中的几个函数。下面是drawBackground()函数的实现:
void OpenGLView::drawBackground( QPainter* pPainter,
const QRectF& )
{
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()函数:
void OpenGLView::resizeEvent( QResizeEvent* pEvent )
{
scene( )->setSceneRect( 0.0,
0.0,
pEvent->size( ).width( ),
pEvent->size( ).height( ) );
pEvent->accept( );
}
这里由于我们已经设置了scene为GraphicsScene类的实例指针,因此scene()是非空的,我们将场景限制为view的大小,这样可以避免一些刷新的问题。
而paintGL()函数也是相当的简单。
void OpenGLView::PaintGL( void )
{
glClear( GL_COLOR_BUFFER_BIT |
GL_DEPTH_BUFFER_BIT );
glPushMatrix( );
m_Camera.Apply( );
m_3DS.RenderGL( );
if ( g_ShowAxis ) DrawAxis( );
glPopMatrix( );
}
接着我向大家介绍一下GraphicsScene类:
class GraphicsScene: public QGraphicsScene
{
Q_OBJECT
public:
GraphicsScene( QObject* pParent = 0 );
void SetCamera( Camera* pCamera );
signals:
void SwitchShader( const QString& shaderFileName );
void SetLightPos( const QVector3D& pos );
protected:
void mousePressEvent( QGraphicsSceneMouseEvent* pEvent );
void mouseMoveEvent( QGraphicsSceneMouseEvent* pEvent );
void mouseReleaseEvent( QGraphicsSceneMouseEvent* pEvent );
void wheelEvent( QGraphicsSceneWheelEvent* pEvent );
private slots:
void Feedback( void );
private:
// 鼠标事件需要
QPointF m_LastPos;
Camera* m_pCamera;
};
这里的GraphicsScene类保存的是来自view的Camera和一些信号以及事件的处理。在实现上也说一下它的构造函数吧。
GraphicsScene::GraphicsScene( QObject* pParent ):
QGraphicsScene( pParent ), m_pCamera( Q_NULLPTR )
{
ClickableTextItem* pTextItem = new ClickableTextItem( Q_NULLPTR );
pTextItem->setPos( 10.0, 10.0 );
pTextItem->setHtml( tr( "<font color=white>"
"Made By Jiangcaiyang<br>"
"Created in September<br>"
"Click for feedback."
"</font>" ) );
connect( pTextItem, SIGNAL( Clicked( ) ),
this, SLOT( Feedback( ) ) );
addItem( pTextItem );
ShaderOptionDialog* pDialog = new ShaderOptionDialog;
connect( pDialog, SIGNAL( SwitchShader( const QString& ) ),
this, SIGNAL( SwitchShader( const QString& ) ) );
connect( pDialog, SIGNAL( SetLightPos( const QVector3D& ) ),
this, SIGNAL( SetLightPos( const QVector3D& ) ) );
QGraphicsProxyWidget* pProxy = addWidget( pDialog, Qt::Window | Qt::WindowTitleHint );
pProxy->setPos( 100, 200 );
}
我们创建了一个ClickableTextItem类,它继承于QGraphicsTextItem,它被摆在左上角,显示三排可以点击的文字。随后添加了一个对话框,设置信号和槽完毕后就用代理放入场景中了。整个过程也是非常简单的。
我测试了下,整个程序在我的Ubuntu13.04下能够正常运行。只是由于显卡不同(Ubuntu下较难支持nvidia显卡,使用的是intel集显),specularOpt效果出不来。