【OpenGL】使用 python + Qt + OpenGL 的现代渲染

伴随资源

目录

  • 一、说明
  • 二、 关于PyQt6.x
    • 2.1 QOpenGLWidget详细说明
    • 2.2 绘画技巧
  • 三、PyOpenGL
  • 四、OpenGL 管线
  • 五、Python集成开发环境
    • 5.1 Emacs配置
    • 5.2 pycharm环境
  • 六、你好,OpenGL!
  • 七、QGL控件
  • 八、平截头体.svg
  • 九、定义几何
    • 9.1 立即模式与保留模式
    • 9.2 使用 VBO 定义 Cube
  • 十、渲染立方体
  • 十一、渲染循环
  • 十二、添加旋转滑块

一、说明

在本教程中,我们将编写一个小型 Python 脚本,该脚本在 GUI 中呈现一个立方体,并使用滑块来控制其旋转。这将基于其他教程,即本教程,但会更详细地解释该过程和一般 OpenGL 概念。您可以在此处下载完整的脚本。

二、 关于PyQt6.x

我i们先介绍QT6.6关于OpenGL的部件。参考官方手册。
有许多不同的框架用于在 python 中创建 GUI - 内置选项是TkInter,它提供了跨平台Tk GUI 工具包的包装器,该工具包具有简单的学习曲线并且适用于小型应用程序,但是流行的现代跨平台-平台解决方案是Qt。

2.1 QOpenGLWidget详细说明

QOpenGLWidget 提供了显示集成到 Qt 应用程序中的 OpenGL 图形的功能。使用起来非常简单:让你的类继承它并像使用其他类一样使用子类QWidget,除了您可以选择使用QPainter和标准 OpenGL 渲染命令。

QOpenGLWidget 提供了三个方便的虚拟函数,您可以在子类中重新实现它们来执行典型的 OpenGL 任务:

  • paintGL() - 渲染 OpenGL 场景。每当小部件需要更新时就会被调用。
  • resizeGL() - 设置 OpenGL 视口、投影等。每当调整窗口小部件的大小时(以及第一次显示窗口小部件时,因为所有新创建的窗口小部件都会自动获取调整大小事件),都会调用它。
  • initializeGL() - 设置 OpenGL 资源和状态。第一次之前被叫过一次resizeGL() 或者paintGL() 叫做。
    如果您需要从其他地方触发重绘paintGL()(一个典型的例子是使用timers动画场景),你应该调用小部件的update() 函数来安排更新。

您的小部件的 OpenGL 渲染上下文在以下情况下变为当前上下文:paintGL(),resizeGL(), 或者initializeGL() 叫做。如果您需要从其他地方调用标准 OpenGL API 函数(例如在您的小部件的构造函数中或在您自己的绘制函数中),您必须调用makeCurrent() 第一的。

所有渲染都发生在 OpenGL 帧缓冲区对象中。makeCurrent() 确保它在上下文中绑定。在渲染代码中创建和绑定附加帧缓冲区对象时请记住这一点paintGL()。切勿重新绑定 ID 为 0 的帧缓冲区。而是调用defaultFramebufferObject() 获取应该绑定的ID。

当平台支持时,QOpenGLWidget 允许使用不同的 OpenGL 版本和配置文件。只需通过设置请求的格式setFormat()。但请记住,在同一窗口中拥有多个 QOpenGLWidget 实例要求它们都使用相同的格式,或者至少使用不会使上下文不可共享的格式。为了解决这个问题,最好使用QSurfaceFormat::setDefaultFormat() 代替setFormat()。

注:打电话QSurfaceFormat::setDefaultFormat() 构建之前QApplication当请求 OpenGL 核心配置文件上下文时,实例在某些平台(例如 macOS)上是强制性的。这是为了确保上下文之间的资源共享保持功能,因为所有内部上下文都是使用正确的版本和配置文件创建的。

2.2 绘画技巧

如上所述,子类化 QOpenGLWidget 以通过以下方式渲染纯 3D 内容:

重新实施initializeGL() 和resizeGL() 函数设置 OpenGL 状态并提供透视变换。
重新实现paintGL() 绘制 3D 场景,仅调用 OpenGL 函数。
还可以使用以下命令将 2D 图形绘制到 QOpenGLWidget 子类上QPainter:

在paintGL(),不发出 OpenGL 命令,而是构造一个QPainter用于小部件的对象。
使用绘制基元QPainter的成员函数。
仍然可以发出直接 OpenGL 命令。但是,您必须确保它们包含在对画家的 beginNativePainting() 和 endNativePainting() 的调用中。
执行绘图时使用QPainter只是,也可以像普通小部件一样执行绘画:通过重新实现paintEvent()。

重新实施paintEvent() 功能。
构造一个QPainter针对小部件的对象。将小部件传递给构造函数或QPainter::begin() 功能。
使用绘制基元QPainter的成员函数。
绘画完成然后QPainter实例被销毁。或者,致电QPainter::end() 明确。
OpenGL 函数调用、标头和 QOpenGLFunction
在进行 OpenGL 函数调用时,强烈建议避免直接调用函数。相反,更喜欢使用QOpenGLFunctions(当制作便携式应用程序时)或版本化变体(例如,QOpenGLFunctions_3_2_Core类似地,当针对现代的、仅限桌面的 OpenGL 时)。这样,应用程序将在所有 Qt 构建配置中正常工作,包括执行动态 OpenGL 实现加载的配置,这意味着应用程序不直接链接到 GL 实现,因此直接函数调用不可行。

在paintGL() 当前上下文始终可以通过调用来访问QOpenGLContext::currentContext()。

三、PyOpenGL

还有许多不同的库可用于在 python 中创建 3D 图形,但最常见的跨平台解决方案是 OpenGL - 特别是使用PyOpenGL包装器。您应该能够pip install pyopengl在 Linux 上轻松安装它,或者再次使用此处pip install下载的轮子进行安装。在 Ubuntu 16.04 上,我发现我必须使用而不是 ‘’‘pip’‘’

apt-get install python-qt4-gl
PyQt 在其QtOpenGL模块中使用与 PyOpenGL 相同的系统安装,以提供特殊的 OpenGL QWidget,从而可以轻松进行交互。下面详细介绍这一点。

四、OpenGL 管线

请注意,我们将在此处使用的 OpenGL 功能主要是固定功能管道的一部分,实际上在 OpenGL 3.0 之后已弃用,转而使用可编程(基于着色器)管道,该管道利用现代 GPU 并行性来高效渲染。不幸的是,网络上的大多数教程都使用已弃用的管道,而且它也恰好是我使用旧模拟器所习惯的管道。稍后我可能会尝试将所有内容切换到所谓的现代 OpenGL,但现在这里有一篇很好的读物,它用简单的术语解释了这些差异。

五、Python集成开发环境

在我们深入研究使用 Qt + OpenGL 创建简单应用程序的示例之前,您可能想知道使用哪些工具在 Python 中进行开发。有很多、很多、很多文章专门讨论这一点,其细节比我所能提供的要详细得多,但我想说的是,通过一些工作,好的 ol’ Emacs 可以变成一个很棒的轻量级 IDE(因为所有涉及 Emacs 的东西都需要),好处是,如果您已经使用 Emacs 处理其他事情,它可以很好地集成到您的工作流程中。下面我将详细介绍我的 Emacs 设置。

如果您要从 Matlab 过渡到 Python 并寻找类似的工具,Spyder是一个不错的选择,并且与一些 Python 发行版中的其他工具配合使用。在使用 JetBrains 的 CLion 进行 C/C++ 开发后,我很想尝试PyCharm,我听说过它很棒(不过,除非您是学生,否则它不是免费的)。

5.1 Emacs配置

Emacs 生态系统中有很多用于 Python 开发的优秀软件包,但我最近对​​Elpy很感兴趣;您可以在这里了解更多信息。要让 Elpy 使用 python3 而不是 python2,请查看此线程以获取说明。

Elpy 内置了一些很棒的功能,例如自动完成,但您可能希望使用其他一些软件包来扩展它。要查看当前集成的内容,请打开 Emacs 并运行M-x elpy-config,这将为您提供一个小界面来检查已安装的内容。如果您想进一步进行设置,您可以按照这个很棒的指南来启用 Flyspell 以获得更好的语法检查等。

非常重要的是,Elpy 使用Flake8进行语法检查,并且可以配置为编辑文件~/.flake8(可能尚不存在,如果不存在则创建它)。例如,Flake8 突出显示了许多令人烦恼的小语法错误,因此您可以添加到此配置文件中:

[flake8]
max-line-length = 99
max-doc-length = 79
ignore = E2,E302,E41,E303

我们还将行长度设置为 99,文档字符串长度设置为 79,正如python.org 所建议的那样,尽管标准 python 库使用更严格的长度。

最后,作为旁注,如果您的 Emacs 设置中的任何内容要求您在“临时”缓冲区中计算 lisp 表达式,请不要惊慌 - 请查看此页面以获取有关如何执行此操作的帮助。可以在 Emacs 中CTRL-X LEFTARROW从主缓冲区轻松访问“临时”缓冲区。

5.2 pycharm环境

如果您在windows下采用pycharm开发环境,一切变得异常简单,无需什么特殊配置,只要安装好QT、OpenGL就一切就绪。

六、你好,OpenGL!

现在我们已经接触了 IDE 的设置和选择,让我们开始导入必要的模块并了解它们各自提供的内容。

from PyQt4 import QtCore      # core Qt functionality
from PyQt4 import QtGui       # extends QtCore with GUI functionality
from PyQt4 import QtOpenGL    # provides QGLWidget, a special OpenGL QWidget

import OpenGL.GL as gl        # python wrapping of OpenGL
from OpenGL import GLU        # OpenGL Utility Library, extends OpenGL functionality

import sys                    # we'll need this later to run our Qt application

无需将其转变为 Qt 教程的介绍(因为我们现在确实想专注于 OpenGL 集成),任何应用程序的主窗口都是由派生自QtGui.QMainWindow.让我们创建主窗口类,然后给它一个名称并在初始化程序中调整它的大小:

class MainWindow(QtGui.QMainWindow):

    def __init__(self):
	QtGui.QMainWindow.__init__(self)    # call the init for the parent class

	self.resize(300, 300)
	self.setWindowTitle('Hello OpenGL App')


if __name__ == '__main__':

    app = QtGui.QApplication(sys.argv)

    win = MainWindow()
    win.show()

    sys.exit(app.exec_())

如果您运行到目前为止的所有内容,则应该打开一个指定大小、名称为“Hello OpenGL App”的 Qt 窗口并且可以退出。多么激动人心!但是我们如何使用 OpenGL 在该窗口中渲染某些内容呢?这就是QGLWidget进来的地方。

QGL控件
定义几何
立即模式与保留模式
使用 VBO 定义 Cube
渲染立方体
渲染循环
添加旋转滑块
设置

七、QGL控件

QGLWidget是一个 Qt 小部件,旨在使用 OpenGL 轻松渲染图形。我们通过子类化 QGLWidget 并实现三个提供的虚拟函数来实现这一点,这些函数在必要时由 Qt 自动调用:

initializeGL:在调用调整大小或绘制之前调用一次以设置 OpenGL 渲染上下文
resizeGL:在创建窗口以及调整窗口大小以设置 OpenGL 视口和投影时调用一次
paintGL:在更新小部件以渲染场景时调用
让我们继承 QGLWidget 并开始填充这些函数。

class GLWidget(QtOpenGL.QGLWidget):
    def __init__(self, parent=None):
		self.parent = parent
		QtOpenGL.QGLWidget.__init__(self, parent)

在派生类的初始化程序中,我们调用父GLWidget类的初始化程序。

def initializeGL(self):
	self.qglClearColor(QtGui.QColor(0, 0, 255))    # initialize the screen to blue
	gl.glEnable(gl.GL_DEPTH_TEST)                  # enable depth testing

在初始化函数中启用深度测试,使 OpenGL 自动确保“片段”根据其在深度缓冲区中的值正确渲染。

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)

该resizeGL函数有更多的设置发生。首先,glViewport(x,y,width,height)指定窗口的哪一部分用于绘图,通常设置为使用窗口的完整宽度和高度(传递给resizeGL)。

接下来,glMatrixMode(mode)将活动矩阵堆栈设置为投影堆栈,其中包含用于定义查看体积的投影变换。为了定义这个变换,我们首先使用 加载单位矩阵到投影堆栈glLoadIdentity(),然后使用 定义视锥体gluPerspective(field_of_view, aspect_ratio, z_near, z_far):
在这里插入图片描述

八、平截头体.svg

透视投影创建的观察体积称为截锥体,如上所示;垂直视场 (FOV) 直接在第一个参数中指定,而水平视场则根据垂直视场和纵横比定义,近端和远端剪裁平面也是输入。

有一个更通用的函数glFrustum可用于创建离轴透视投影(实际上,在幕后gluPerspective调用)。我们也可以使用正交投影来glFrustrum定义观看体积。glOrtho这会创建一个矩形棱柱的观看体积,并且不切实际地导致位于不同深度(z 值)的相同高度的对象被绘制为相同的尺寸。我们将在这里坚持使用透视投影。
在这里插入图片描述

正交.svg

最后,我们将矩阵模式设置回GL_MODELVIEW用于所有后续相机和模型转换的矩阵模式。一般来说,我们不应该使用GL_PROJECTIONexcept 来定义观看量。

def paintGL(self):
gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT)

# Add rendering code here!

最后GLWidget要实现的虚拟函数是paintGL,我们将在其中进行所有渲染。目前,我们所做的只是使用glClear(bitmask)掩码的按位或来进行一些预渲染内务处理,告诉 OpenGL 要清除哪些缓冲区。我们清除颜色和深度缓冲区,以便每次都从干净的状态开始渲染步骤。

让我们GLWidget通过创建一个对象并将其设置为我们的中心小部件来使用我们新完成的类,MainWindow如下所示。

class MainWindow(QtGui.QMainWindow):

    def __init__(self):
	QtGui.QMainWindow.__init__(self)    # call the init for the parent class

	self.resize(300, 300)
	self.setWindowTitle('Hello OpenGL App')

	glWidget = GLWidget(self)
	self.setCentralWidget(glWidget)

所有QMainWindow对象都必须有一个中央小部件,其他小部件可以停靠在该中央小部件上。运行我们到目前为止编写的所有内容,您应该看到一个空白的蓝色窗口,而不是一个空白的黑色窗口,因为我们使用 OpenGL 在initializeGL.
在这里插入图片描述

hello_opengl_blue.svg

万岁!现在我们已经完成了GLWidget设置,让我们做一些更有趣的事情。

九、定义几何

9.1 立即模式与保留模式

就渲染简单几何体而言,有两种模式。立即模式包括将绘图命令夹在glBegin和glEnd命令之间,导致性能较差,因为 GPU 必须等待glEnd然后立即渲染。保留模式是现代方法;几何体不是在每个周期立即调用渲染,而是使用所谓的顶点缓冲区对象 (VBO)定义,这些对象被发送到并存储在 GPU 上进行渲染。除非需要更新VBO,否则后续渲染不需要CPU和GPU之间的通信,因为VBO数据存储在图形硬件上。

9.2 使用 VBO 定义 Cube

首先,我们将导入 python OpenGL 包提供的 VBO 类以及numpy通用数组:

from OpenGL.arrays import vbo
import numpy as np

现在我们准备定义我们正在绘制的立方体的几何形状。让我们向我们的GLWidget类添加一个新函数initGeometry,我们将在该类中定义立方体。

def initGeometry(self):
	self.cubeVtxArray = np.array(
    [[0.0, 0.0, 0.0],
     [1.0, 0.0, 0.0],
     [1.0, 1.0, 0.0],
     [0.0, 1.0, 0.0],
     [0.0, 0.0, 1.0],
     [1.0, 0.0, 1.0],
     [1.0, 1.0, 1.0],
     [0.0, 1.0, 1.0]])
	self.vertVBO = vbo.VBO(np.reshape(self.cubeVtxArray,
				  (1, -1)).astype(np.float32))
	self.vertVBO.bind()

首先,将以原点为中心的立方体顶点位置定义为 2D numpy 数组。然后,创建顶点位置 VBO vertVBO,注意将numpy 数组重塑为 1D 并将数组元素转换为,np.float32因为 OpenGL 需要浮点数(不是双精度数,这是 python 的默认值)。最后,绑定VBO以供GPU使用。

self.cubeClrArray = np.array(
    [[0.0, 0.0, 0.0],
     [1.0, 0.0, 0.0],
     [1.0, 1.0, 0.0],
     [0.0, 1.0, 0.0],
     [0.0, 0.0, 1.0],
     [1.0, 0.0, 1.0],
     [1.0, 1.0, 1.0],
     [0.0, 1.0, 1.0 ]])
self.colorVBO = vbo.VBO(np.reshape(self.cubeClrArray,
				   (1, -1)).astype(np.float32))
self.colorVBO.bind()

我们创建第二个 VBO 来存储每个顶点的颜色,该颜色被任意设置为与顶点位置相同。 OpenGL 将使用这些顶点颜色为立方体面着色,并具有良好的渐变效果。请注意,我们实际上并不需要一个全新的 VBO 来处理颜色;而是需要一个新的 VBO。相反,我们可以通过在渲染时交错行并指定步长(连续顶点或颜色数据值之间的字节数)来将顶点位置和颜色数据存储在同一个 VBO 中。

self.cubeIdxArray = np.array(
    [0, 1, 2, 3,
     3, 2, 6, 7,
     1, 0, 4, 5,
     2, 1, 5, 6,
     0, 3, 7, 4,
     7, 6, 5, 4 ])

最后,我们指定组成一维数组中六个立方体面中每一个的顶点。所有这些几何设置都应该在 中调用initializeGL,我们将其修改为

def initializeGL(self):
	self.qglClearColor(QtGui.QColor(0, 0, 255))    # initialize the screen to blue
	gl.glEnable(gl.GL_DEPTH_TEST)                  # enable depth testing

	self.initGeometry()

十、渲染立方体

要渲染立方体,我们需要向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.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.glDisableClientState(gl.GL_VERTEX_ARRAY)
	gl.glDisableClientState(gl.GL_COLOR_ARRAY)

	gl.glPopMatrix()    # restore the previous modelview matrix

让我们逐步看一下新的调用:

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.glTranslate(-0.5, -0.5, -0.5)   # first, translate cube center to origin

首先,我们用来glPushMatrix()复制当前的变换矩阵(因为我们没有做任何其他事情,所以它是恒等式)并将其推送到当前的矩阵堆栈(回想一下,这是在GL_MODELVIEW我们设置场景投影之后设置的)。然后,我们应用一系列变换,以相反的顺序构建它们 - 因此我们将立方体中心平移到原点(这在缩放或旋转之前是必要的),然后缩放立方体,然后将其平移到较大的深度,以便我们实际上可以看到它。

但是等等……立方体在哪里渲染?好吧,我们实际上还没有渲染任何东西 - 我们只是设置将应用于立方体的转换,它是通过以下方式渲染的:

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.glDisableClientState(gl.GL_VERTEX_ARRAY)
gl.glDisableClientState(gl.GL_COLOR_ARRAY)

gl.glPopMatrix()    # restore the previous modelview matrix

为了渲染立方体,我们首先启用顶点和颜色数组,然后将指针设置为我们之前创建的顶点位置和颜色数组glVertexPointer(size, type, stride, pointer),glColorPointer(size, type, stride, pointer)以便 GPU 可以访问数据。

然后,我们调用使用 中指定的面索引glDrawElements(mode, count, type, indices)来渲染立方体面。或者,我们可以直接在顶点 VBO 中定义面并使用,但这需要将重复的顶点发送到 GPU。在这种情况下,使用索引列表会更有效。GL_QUADScubeIdxArrayglDrawArraysglDrawElements

最后,我们禁用启用的客户端状态(为了安全起见),然后从模型视图堆栈中弹出当前矩阵,将堆栈顶部重置为恒等变换。

十一、渲染循环

把它们放在一起会产生……什么也没有!我们已经准备好所有代码,但paintGL从未实际调用过。由我们决定渲染的时间/频率;最简单的方法是设置一个计时器作为updateGL回调(paintGL自动调用),以便定期进行渲染。更新MainWindow类如下:

class MainWindow(QtGui.QMainWindow):

    def __init__(self):
	QtGui.QMainWindow.__init__(self)

	self.resize(300, 300)
	self.setWindowTitle('Hello OpenGL App')

	glWidget = GLWidget(self)
	self.setCentralWidget(glWidget)

	timer = QtCore.QTimer(self)
	timer.setInterval(20)   # period, in milliseconds
	timer.timeout.connect(glWidget.updateGL)
	timer.start()

我们创建了一个QTimer对象,并将其设置为每 20 毫秒 (50 Hz) 发出一个信号,并将该信号连接到派生基类提供的槽(函数)。当我们开始添加滑块和其他 GUI 元素时,我们将更多地利用 Qt 的信号和插槽。updateGLGLWidget

十二、添加旋转滑块

在这里插入图片描述
hello_opengl_cube.svg

现在我们的应用程序中渲染了一个立方体!让我们添加一个滑块来更改其方向,以便我们实际上可以看到它是三维的。这涉及到修改 MainWindow类,我们在其中设置 GUI 结构。请注意,创建更复杂的 GUI 可能需要 QtCreator(用于 C++ 应用程序)等设计工具,但对于简单的 GUI,我们可以手动添加元素。

class MainWindow(QtGui.QMainWindow):

def __init__(self):
    QtGui.QMainWindow.__init__(self)    # call the init for the parent class
    
    self.resize(300, 300)
    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()
    
def initGUI(self):
    central_widget = QtGui.QWidget()
    gui_layout = QtGui.QVBoxLayout()
    central_widget.setLayout(gui_layout)

    self.setCentralWidget(central_widget)

    gui_layout.addWidget(self.glWidget)

    sliderX = QtGui.QSlider(QtCore.Qt.Horizontal)
    sliderX.valueChanged.connect(lambda val: self.glWidget.setRotX(val))

    sliderY = QtGui.QSlider(QtCore.Qt.Horizontal)
    sliderY.valueChanged.connect(lambda val: self.glWidget.setRotY(val))

    sliderZ = QtGui.QSlider(QtCore.Qt.Horizontal)
    sliderZ.valueChanged.connect(lambda val: self.glWidget.setRotZ(val))
    
    gui_layout.addWidget(sliderX)
    gui_layout.addWidget(sliderY)
    gui_layout.addWidget(sliderZ)

我们添加了一个新initGUI函数,该函数被调用来代替原始调用以设置glWidget为应用程序的中央小部件。该函数封装了所有GUI布局设置;让我们一步一步地看一下。

def initGUI(self):
    central_widget = QtGui.QWidget()
    gui_layout = QtGui.QVBoxLayout()
    central_widget.setLayout(gui_layout)

self.setCentralWidget(central_widget)

我们可以通过以下方式构建 Qt GUI:首先定义一个新的中央小部件,为该小部件创建一个布局,该布局定义添加到其中的小部件将如何组织(垂直堆叠,因为我们使用QVBoxLayout),然后设置中央小部件的布局。最后,我们将新的小部件设置为该类的中心小部件MainWindow。

    gui_layout.addWidget(self.glWidget)

    sliderX = QtGui.QSlider(QtCore.Qt.Horizontal)
    sliderX.valueChanged.connect(lambda val: self.glWidget.setRotX(val))

    sliderY = QtGui.QSlider(QtCore.Qt.Horizontal)
    sliderY.valueChanged.connect(lambda val: self.glWidget.setRotY(val))

    sliderZ = QtGui.QSlider(QtCore.Qt.Horizontal)
    sliderZ.valueChanged.connect(lambda val: self.glWidget.setRotZ(val))
    
    gui_layout.addWidget(sliderX)
    gui_layout.addWidget(sliderY)
    gui_layout.addWidget(sliderZ)

接下来,我们将小部件添加到中央小部件的布局中,从glWidget顶部开始,然后QSlider在其下面添加三个小部件。我们将valueChanged每个滑块的信号连接到相应的槽函数,该函数捕获滑块值val并设置立方体绕轴的旋转。

我们将旋转角度作为glWidget属性添加到initializeGL函数中:

def initializeGL(self):
    self.qglClearColor(QtGui.QColor(0, 0, 255))    # initialize the screen to blue
    gl.glEnable(gl.GL_DEPTH_TEST)                  # enable depth testing

    self.initGeometry()

    self.rotX = 0.0
    self.rotY = 0.0
    self.rotZ = 0.0

使用上述设置器从滑块更新

def setRotX(self, val):
    self.rotX = val

def setRotY(self, val):
    self.rotY = val

def setRotZ(self, val):
    self.rotZ = val

然后更新paintGL渲染代码以使用这些角度来执行立方体围绕局部轴的连续旋转(我们将在以后的文章中详细介绍旋转,但格式是轴角度):

    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

结果是相同的渲染立方体,但其下方有三个滑块,允许我们旋转它。您应该看到一些与下面的立方体几乎但不完全不同的东西。

在这里插入图片描述

立方体.gif

想知道为什么我的立方体看起来这么糟糕?我使用这个漂亮的工具将屏幕直接录制为 GIF,而 GIF 格式的调色板极其有限(8 位,即 256 种颜色),这在尝试显示具有一堆颜色渐变的立方体时非常明显。我保证你的会看起来更好!
在这里插入图片描述

hello_opengl_cube_rotated.svg

您可以在此处下载完整的脚本。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/501462.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

消息队列的七种经典应用场景

在笔者心中,消息队列,缓存,分库分表是高并发解决方案三剑客。 在职业生涯中,笔者曾经使用过 ActiveMQ 、RabbitMQ 、Kafka 、RocketMQ 这些知名的消息队列 。 这篇文章,笔者结合自己的真实经历,和大家分享…

SEH异常之编译器原理探究(1)

_try_except原理 调用_except_handle3这个异常处理函数,这里并不是每个编译器的异常处理函数都是相同的,然后存入结构体,将esp的值赋给fs:[0],再就是提升堆栈的操作 每个使用 _try _except的函数,不管其内部嵌套或反复…

大模型预测,下一个token何必是文字?

太快了太快了… 大模型的生成技能,已经到了普通人看不懂的境界! 它可以根据用户过去5年的体检报告,生成未来第1年、第2年、第3年的体检报告。 你看,这个生成的过程,是不是像极了ChatGPT,根据历史单词预测…

vue-v-for遍历index与id

一.遍历列表key的作用(index作为key) 虚拟DOM上有key,是虚拟的,但是真实DOM上没有,key是Vue内部的 当使用index作为key的时候,Vue会根据初识数据生成一个初始的虚DOM, 然后在页面上映射出真实DOM 如果向数据…

Webpack生成企业站静态页面 - 项目搭建

现在Web前端流行的三大框架有Angular、React、Vue,很多项目经过这几年的洗礼,已经都 转型使用这三大框架进行开发,那为什么还要写纯静态页面呢?比如Vue中除了SPA单页面开发,也可以使用nuxt.js实现SSR服务端渲染&#x…

CrossOver2024最新免费版虚拟机软件 Mac和Linux系统上运行Windows 应用/游戏 CrossOver是什么软件

CrossOver是一款由CodeWeavers公司开发的,运行在Mac和Linux操作系统下,能够模拟Windows系统应用运行环境的软件。它不需要用户单独安装Windows操作系统,就能让Windows平台上的应用程序在Mac和Linux上顺畅运行。CrossOver在技术上使用了Wine&a…

module ‘numpy‘ has no attribute ‘int‘

在 NumPy 中,如果遇到了错误提示 "module numpy has no attribute int",这通常意味着正在尝试以错误的方式使用 NumPy 的整数类型。从 NumPy 1.20 版本开始,numpy.int 已经不再是一个有效的属性,因为 NumPy 不再推荐使用…

五、基于KubeAdm搭建多节点K8S集群

如需查阅上一步骤,请点击下面链接:四、戴尔R630本地服务器Linux Centos7.9系统安装docker-ce-20.10.10-3.el7版本-CSDN博客文章浏览阅读727次,点赞12次,收藏13次。1、准备工作3、Linux Centos7.9系统的iDRAC远程管理、网络设置、SecureCRT远程登录终端、企业级静态ip地址配…

20240329-科技咨询:比亚迪第五代DMi;央视AI《周处除三害》;带屏幕苹果耳机爆火

一、比亚迪5月份即将推出第五代DMi技术 近日,比亚迪举行了2023年财报投资人沟通会。会议纪要显示,比亚迪董事长王传福在会上透露,今年5月将推出第五代DMI混动技术,预计馈电油耗将降至2.9升/百公里,而满油满电续航将达…

第十四届蓝桥杯省赛C++ C组所有题目以及题解(C++)【编程题均通过100%测试数据】

第一题《求和》【简单模拟】 【问题描述】 求1(含)至20230408(含)中每个数的和。 【答案提交】 这是一道结果填空的题,你只需要算出结果后提交即可。本题的结果为一个整数,在提交答案时只填写这个整数&…

“地干天知”干旱监测与预警技术研讨及系统产品发布

3月28日,由国家气候中心气象灾害风险管理室、北京慧天卓特科技有限公司主办的“地干天知”干旱监测与预警技术研讨及系统产品发布活动在北京市海淀区中关村壹号隆重举办。活动旨在面向公众讲解干旱监测与预警技术原理,展示监测范围和预警能力。来自国家气…

protobuf学习笔记(二):结合grpc生成客户端和服务端

上一篇文章大概讲了如何将自定义的protobuf类型的message转换成相应的go文件,这次就结合grpc写一个比较认真的客户端和服务器端例子 一、项目结构 client存放rpc服务的客户端文件 server存放rpc服务的服务端文件 protobuf存放自定义的proto文件 grpc存放生成的g…

【LeetCode: 面试题 16.05. 阶乘尾数 + 阶乘】

🚀 算法题 🚀 🌲 算法刷题专栏 | 面试必备算法 | 面试高频算法 🍀 🌲 越难的东西,越要努力坚持,因为它具有很高的价值,算法就是这样✨ 🌲 作者简介:硕风和炜,…

ThreadPool-线程池使用及原理

1. 线程池使用方式 示例代码: // 一池N线程 Executors.newFixedThreadPool(int) // 一个任务一个任务执行,一池一线程 Executors.newSingleThreadExecutorO // 线程池根据需求创建线程,可扩容,遇强则强 Executors.newCachedThre…

谷歌seo站内优化需要做什么?

说一个比较重要的点,那就是页面的标签优化,网站的title标签跟Descriptions标签的重要性不言而喻,但其他页面的标签也是同样重要,毕竟客户看见在谷歌搜索引擎里看见的就是你的页面标题以及描述 所以页面的标题以及描述是很重要的&a…

电机试验平台的结构

电机试验平台的结构通常包括以下部分: 1.电机:试验平台主要是为了对电机进行各种试验,因此电机是平台的核心组成部分。电机通常由定子和转子组成,根据试验需要可以是直流电机、交流电机或者是特殊类型的电机。 2.控制系统&#…

一次性了解C语言中文件和文件操作

P. S.:以下代码均在VS2019环境下测试,不代表所有编译器均可通过。 P. S.:测试代码均未展示头文件stdio.h的声明,使用时请自行添加。 文件及文件操作 前言1. 文件分类1.1 文本文件1.2 二进制文件1.3 文本文件和二进制文件的区别 2…

Linux:程序地址空间详解

目录 一、堆、栈、环境参数所在位置 二、进程地址空间底层实现原理 ​编辑 三、什么是地址空间 四、为什么要有进程地址空间 五、细谈写实拷贝的实现及意义 在C/C学习中,都学习过如上图所示的一套存储结构,我们大致知道一般存储空间分为堆区&#…

数据结构:归并排序

归并排序 时间复杂度O(N*logN) 如果两个序列有序,通过归并,可以让两个序列合并后也有序,变成一个有序的新数组 对于一个数组,如果他的左右区间都有序,就可以进行归并了 归并的方法 将数组的左右两个有序区间比较,每次都取出一个最小的,然后放入临时数组(不能在原数组上修改…

纯小白蓝桥杯备赛笔记--DAY8(必备排序算法)

冒泡排序 算法思想 每次将最大的一下一下地运到最右边&#xff0c;然后确定这个最大的&#xff0c;接着可以发现该问题变成一个更小的子问题。具体操作&#xff1a;从左向右扫描&#xff0c;如a[i]>a[i1]&#xff0c;执行swap操作。代码格式 #include<bits/stdc.h> …