实验效果图:
代码
目录
- 一、说明
- 二、关于QGLWidget
- 2.1 三个方便的虚函数
- 2.2 析构函数
- 2.3 QGLWidget析构函数
- 三、关于QGLWidget的三个虚函数分工
- 3.1 initializeGL:数据准备、数据绑定分离
- 3.2 resizeGL:视角改变函数
- 3.3 paintGL:绘画函数
- 四、主窗口的配合:刷新周期的设定
- 4.1 分工和管理
- 4.2 主窗口的配合性代码
- 五、总结
一、说明
我们采用Qt5做OpenGL的界面,是因为在QGLWidget窗口上,既可以渲染三维动画,也能有按键,方便人机交互。学习渲染艺术的顺序是:首先要能对一些基本几何图形进行渲染。然后是二维物品渲染,最后是三维物体渲染。以下我们对这个学习过程进行系列解读。
二、关于QGLWidget
2.1 三个方便的虚函数
QGLWidget提供了三个方便的虚拟函数,可以在子类中重写这些函数来执行典型的OpenGL任务:
- paintGL() :渲染OpenGL场景。每当需要更新小部件时调用。
- resizeGL () :设置OpenGL视区、投影等。每当小部件调整了大小时都会调用该视区(并且当它第一次显示时也会调用,因为所有新创建的小部件都会自动获得一个调整大小的事件)。
- initializeGL() :设置OpenGL呈现上下文,定义显示列表等。在第一次调用resizeGL ()或paintGL()之前调用一次。此虚拟函数实时性很强。
注意:如果是数据需要准备,在initializeGL()中是不好的,因为数据准备程序量较大,不可以在实时性很强的程序中出现。因该让__init__分担数据读写、初始化等耗时的工作。总之,nitializeGL()越是轻便越好。
2.2 析构函数
PyQt5 存在一些问题,就是有时析构函数未必被调用,因此,准备的资源释放代码不能执行。
有时在关闭应用程序时,PyQt不会自动调用类的析构函数,可能导致一些资源无法正常释放。这可能是因为应用程序的主事件循环在关闭窗口时仍在运行,从而导致类的实例无法被销毁。
为了解决这个问题,我们可以通过重写QMainWindow的closeEvent方法来手动触发析构函数的调用。
from PyQt5.QtWidgets import QApplication, QMainWindow
class MyWindow(QMainWindow):
def __init__(self):
super().__init__()
def __del__(self):
print("析构函数被调用")
def closeEvent(self, event):
self.__del__() # 手动调用析构函数
event.accept()
app = QApplication([])
window = MyWindow()
window.show()
app.exec_()
在上面的示例代码中,我们重写了closeEvent方法,并在其中手动调用了析构函数。通过这种方式,我们可以确保在关闭窗口时,析构函数会被正确地调用。
在PyQt中,当一个类的实例被销毁时,Python会自动调用它的析构函数(即__del__方法)。析构函数主要用于释放资源(例如关闭文件、断开网络连接等),以确保在对象被销毁之前进行必要的清理工作。
2.3 QGLWidget析构函数
我们在实验程序中使用析构函数释放CPU缓存。设计模型如下:
母窗口:
class MainWindow(QtWidgets.QMainWindow):
... ... # 忽略其他程序
def __del__(self):
self.glWidget.__del__()
def closeEvent(self, event):
self.__del__() # 手动调用析构函数
event.accept()
子窗口:
class GLWidget(QtOpenGL.QGLWidget):
... ... # 忽略其他程序
def __del__(self):
print("finished")
其中self.vertVBO是被生成的数据biffer块。注意,非要在主窗启动closeEvent才能启动主窗的关闭程序,否则,主窗的__del__不能保证次次都被调用。
注意:这里的析构函数不要把buffer清除放入,因为QGLWidget已经给自动执行了,我们加入下列语句
gl.glDeleteBuffers(1, (self.vertVBO,))
gl.glDeleteBuffers(1, (self.colorVBO,))
反而会干扰系统执行。
三、关于QGLWidget的三个虚函数分工
3.1 initializeGL:数据准备、数据绑定分离
数据生成是一个长时间的任务,最好是“一次生成,整个程序生命周期有效”,因此,数据生成和使用前绑定需要分布在两个程序段内才能发挥效率。
3.2 resizeGL:视角改变函数
视角改变是指用鼠标转换观察角度,或者是在窗口拉大拉小后,需要改变观察点数据,该数据包括位置和角度矩阵。
def resizeGL(self, width, height):
gl.glViewport(0, 0, width, height)
gl.glMatrixMode(gl.GL_PROJECTION)
gl.glLoadIdentity()
aspect = width / float(height)
GLU.gluPerspective(45.0, aspect, 1.0, 100.0)
gl.glMatrixMode(gl.GL_MODELVIEW)
3.3 paintGL:绘画函数
在每次刷新窗口的时候,需要正式调用一次paintGL;注意,该函数是与时间密切相关的。刷新周期可以交给用户执行。
def paintGL(self):
gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT)
gl.glPushMatrix() # push the current matrix to the current stack
gl.glTranslate(0.0, 0.0, -50.0) # third, translate cube to specified depth
gl.glScale(20.0, 20.0, 20.0) # second, scale cube
gl.glRotate(self.rotX, 1.0, 0.0, 0.0)
gl.glRotate(self.rotY, 0.0, 1.0, 0.0)
gl.glRotate(self.rotZ, 0.0, 0.0, 1.0)
gl.glTranslate(-0.5, -0.5, -0.5) # first, translate cube center to origin
gl.glEnableClientState(gl.GL_VERTEX_ARRAY)
gl.glEnableClientState(gl.GL_COLOR_ARRAY)
gl.glVertexPointer(3, gl.GL_FLOAT, 0, self.vertVBO)
gl.glColorPointer(3, gl.GL_FLOAT, 0, self.colorVBO)
# gl.glDrawElements(gl.GL_QUADS, len(self.cubeIdxArray), gl.GL_UNSIGNED_INT, self.cubeIdxArray)
gl.glDrawArrays(gl.GL_POINTS, 0, 100)
gl.glDisableClientState(gl.GL_VERTEX_ARRAY)
gl.glDisableClientState(gl.GL_COLOR_ARRAY)
gl.glPopMatrix() # restore the previous modelview matrix
四、主窗口的配合:刷新周期的设定
4.1 分工和管理
主窗口的特点是:主窗口是时间、事件的触发地,通常,QGLWidget附着在主窗口上,主窗口负责整体协调,如图:
4.2 主窗口的配合性代码
该主窗口的构造函数,首先
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
QtWidgets.QMainWindow.__init__(self) # call the init for the parent class
self.resize(600, 600)
self.setWindowTitle('Hello OpenGL App')
self.glWidget = GLWidget(self)
self.initGUI()
# 设置循环刷屏
timer = QtCore.QTimer(self)
timer.setInterval(20) # period, in milliseconds
timer.timeout.connect(self.glWidget.updateGL)
timer.start()
五、总结
GLUT、pygame、QT5都能实现渲染界面,虽然它们的区别是微小的,但是,足以在一个小问题上浪费几周的时光,因此,本系列文章将逐步暴露它们的方式分别,以确保在正式开发中流畅自如,而不是现学现卖。