一、介绍
继上一篇文章《通过 Python 了解 OpenGL》中我们为进一步学习奠定了基础之后,我们可以使用PyGame和PyOpenGL进入OpenGL。
PyOpenGL 是用作 Python 和 OpenGL API 之间桥梁的标准化库,PyGame 是用于用 Python 制作游戏的标准化库。它提供了内置的方便的图形和音频库,我们将在本文末尾使用它来更轻松地呈现结果。
正如上一篇文章中提到的,OpenGL 非常古老,因此您不会在网上找到很多关于如何正确使用它和理解它的教程,因为所有顶尖技术都已经深入了解新技术。
在本文中,我们将讨论您需要了解的几个基本主题:
- 使用 PyGame 初始化项目
- 绘制对象
- 迭代动画
- 利用变换矩阵
- 多重转换执行
- 实施例
二、使用 PyGame 初始化项目
首先,如果您还没有安装 PyGame 和 PyOpenGL,我们需要安装:
$ python3 -m pip install -U pygame --user
$ python3 -m pip install PyOpenGL PyOpenGL_accelerate
注意:您可以在之前的 OpenGL 文章中找到更详细的安装信息。
如果您遇到安装问题,PyGame 的“入门”部分可能是一个不错的选择。
由于向您卸载 3 本书的图形理论是没有意义的,因此我们将使用 PyGame 库来为我们提供一个良好的开端。它本质上只会缩短从项目初始化到实际建模和动画的过程。
首先,我们需要从 OpenGL 和 PyGame 导入所有必需的内容:
import pygame as pg
from pygame.locals import *
from OpenGL.GL import *
from OpenGL.GLU import *
接下来,我们进行初始化:
pg.init()
windowSize = (1920,1080)
pg.display.set_mode(display, DOUBLEBUF|OPENGL)
虽然初始化只有三行代码,但每行代码至少都值得一个简单的解释:
pg.init():所有 PyGame 模块的初始化 - 这个函数是天赐之物
windowSize = (1920, 1080):定义固定窗口大小
pg.display.set_mode(display, DOUBLEBUF|OPENGL):在这里,我们指定我们将使用带有双缓冲的OpenGL
双缓冲意味着在任何给定时间都有两幅图像 - 一幅我们可以看到,另一幅我们可以根据需要进行转换。当两个缓冲区交换时,我们可以看到由转换引起的实际变化。
既然我们已经设置了视口,接下来我们需要指定我们将看到的内容,或者更确切地说,“相机”将放置在哪里,以及它可以看到的距离和宽度。
这被称为截锥体- 它只是一个截断的金字塔,在视觉上代表相机的视线(它能看到和不能看到的东西)。
平截头体由 4 个关键参数定义:
- FOV(Field of View视野):角度(以度为单位)
- The Aspect Ratio(纵横比):定义为宽度和高度的比率
- The z coordinate of the near Clipping Plane(近剪裁平面的z坐标):最小绘制距离
- The z coordinate of the far Clipping Plane(远剪裁平面的z坐标):最大绘制距离
因此,让我们继续使用 OpenGL C 代码来实现考虑这些参数的相机:
void gluPerspective(GLdouble fovy, GLdouble aspect, GLdouble zNear, GLdouble zFar);
gluPerspective(60, (display[0]/display[1]), 0.1, 100.0)
为了更好地理解平截头体的工作原理,这里有一张参考图:
使用近平面和远平面以获得更好的性能。实际上,渲染我们视野之外的任何东西都是对硬件性能的浪费,而硬件性能本来可以用来渲染我们实际看到的东西。
因此,玩家看不到的所有内容都会隐式存储在内存中,即使它在视觉上并不存在。这是一个精彩的视频,展示了仅在视锥体内渲染的样子。
三、绘制对象图
完成此设置后,我想我们会问自己同样的问题:
好吧,这一切都很好,但我如何制作一个立体图呢?
嗯…有点难。OpenGL 对象中的每个模型都存储为一组顶点及其关系(哪些顶点相互连接)。因此,从理论上讲,如果您知道用于绘制超级歼星舰的每个点的位置,您很可能会画出一艘!
我们可以通过几种方法在 OpenGL 中对对象进行建模:
- 使用顶点进行绘制,根据 OpenGL 如何解释这些顶点,我们可以使用以下命令进行绘制:
- point:就像不以任何方式连接的字面上的点一样
- lines:每对顶点构造一条连接线
- 三角形:每三个顶点构成一个三角形
- 四边形:每四个顶点构成一个四边形
- 多边形:你明白了
- 还有很多…
- 使用 OpenGL 贡献者精心建模的内置形状和对象进行绘图
- 导入已经完全建模的对象
因此,例如要绘制一个立方体,我们首先需要定义它的顶点:
cubeVertices = ((1,1,1),(1,1,-1),(1,-1,-1),(1,-1,1),(-1,1,1),(-1,-1,-1),(-1,-1,1),(-1, 1,-1))
画出立方体:
然后,我们需要定义它们是如何连接的。如果我们想制作一个线立方体,我们需要定义立方体的边缘:
cubeEdges = ((0,1),(0,3),(0,4),(1,2),(1,7),(2,5),(2,3),(3,6),(4,6),(4,7),(5,6),(5,7))
这非常直观 - 该点0有一条边1, , 3, 和4。该点1有一条边,其中有点3、5、 和7,依此类推。
如果我们想要制作一个实心立方体,那么我们需要定义立方体的四边形:
cubeQuads = ((0,3,6,4),(2,5,6,3),(1,2,5,7),(1,0,4,7),(7,4,6,5),(2,3,0,1))
这也很直观 - 要在立方体的顶部制作一个四边形,我们需要为点0、3、6和之间的所有内容“着色” 4。
请记住,我们将顶点标记为定义它们的数组的索引是有实际原因的。这使得编写连接它们的代码变得非常容易。
以下函数用于绘制有线立方体:
def wireCube():
glBegin(GL_LINES)
for cubeEdge in cubeEdges:
for cubeVertex in cubeEdge:
glVertex3fv(cubeVertices[cubeVertex])
glEnd()
glBegin()是一个函数,指示我们将在下面的代码中定义图元的顶点。当我们定义完原语后,我们使用该函数glEnd()。
GL_LINES是一个宏,指示我们将绘制线条。
glVertex3fv()是一个定义空间顶点的函数,这个函数有几个版本,所以为了清楚起见,让我们看看名称是如何构造的:
glVertex:定义顶点的函数
glVertex3:使用 3 个坐标定义顶点的函数
glVertex3f:使用 3 个坐标类型定义顶点的函数GLfloat
glVertex3fv:使用 3 个类型坐标定义顶点的函数,GLfloat这些坐标放入向量
(元组)内(另一种方法是glVertex3fl使用参数列表而不是向量)
按照类似的逻辑,以下函数用于绘制实心立方体:
def solidCube():
glBegin(GL_QUADS)
for cubeQuad in cubeQuads:
for cubeVertex in cubeQuad:
glVertex3fv(cubeVertices[cubeVertex])
glEnd()
四、迭代动画
为了使我们的程序“可杀死”,我们需要插入以下代码片段:
for event in pg.event.get():
if event.type == pg.QUIT:
pg.quit()
quit()
它基本上只是一个滚动浏览 PyGame 事件的监听器,如果它检测到我们单击了“终止窗口”按钮,它就会退出应用程序。
我们将在以后的文章中介绍更多 PyGame 的事件 - 立即介绍这个事件是因为用户和您自己每次想要退出应用程序时都必须启动任务管理器,这会让他们感到非常不舒服。
在此示例中,我们将使用双缓冲,这意味着我们将使用两个缓冲区(您可以将它们视为绘图的画布),它们将以固定间隔交换并给出运动的错觉。
知道了这一点,我们的代码必须具有以下模式:
handleEvents()
glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT)
doTransformationsAndDrawing()
pg.display.flip()
pg.time.wait(1)
- glClear:清除指定缓冲区(画布)的函数,在本例中为颜色缓冲区(包含用于绘制生成对象的颜色信息)和深度缓冲区(存储对象的前后关系的缓冲区)所有生成的对象)。
- pg.display.flip():使用活动缓冲区内容更新窗口的函数
- pg.time.wait(1):暂停程序一段时间的功能
- glClear必须使用,因为如果我们不使用它,我们将只是在已经绘制的画布上绘画,在这种情况下,这是我们的屏幕,我们最终会变得一团糟。
接下来,如果我们想像动画一样不断更新屏幕,我们必须将所有代码放入一个while循环中,在该循环中:
- 处理事件(在本例中,只是退出)
- 清除颜色和深度缓冲区,以便可以再次绘制它们
- 变换和绘制对象
- 更新屏幕
- 转到 1。
代码应该看起来像这样:
while True:
handleEvents()
glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT)
doTransformationsAndDrawing()
pg.display.flip()
pg.time.wait(1)
五、利用变换矩阵
在上一篇文章中,我们解释了理论上我们需要如何构建具有引用点的转换。
OpenGL 的工作方式相同,如以下代码所示:
glTranslatef(1,1,1)
glRotatef(30,0,0,1)
glTranslatef(-1,-1,-1)
在此示例中,我们在xy 平面上进行了z 轴旋转,旋转中心为30 度。(1,1,1)
如果这些术语听起来有点令人困惑,让我们回顾一下:
-
z 轴旋转意味着我们绕 z 轴旋转
这只是意味着我们正在用 3D 空间逼近 2D 平面,整个变换基本上就像围绕 2D 空间中的参考点进行正常旋转。 -
我们通过将整个 3D 空间压缩到一个具有xy 平面的z=0平面(我们以各种方式消除 z 参数)
-
旋转中心是我们将围绕其旋转给定对象的顶点(默认旋转中心是原点顶点(0,0,0))
但有一个问题 - OpenGL 通过不断记住和修改一个全局变换矩阵来理解上面的代码。
因此,当您用 OpenGL 编写内容时,您所说的是:
# This part of the code is not translated
# transformation matrix = E (neutral)
glTranslatef(1,1,1)
# transformation matrix = TxE
# ALL OBJECTS FROM NOW ON ARE TRANSLATED BY (1,1,1)
正如您可能想象的那样,这提出了一个巨大的问题,因为有时我们希望对单个对象而不是整个源代码进行转换。这是低级 OpenGL 中出现错误的一个非常常见的原因。
为了解决 OpenGL 的这个有问题的功能,我们提出了推入和弹出变换矩阵 -glPushMatrix()并且glPopMatrix():
# Transformation matrix is T1 before this block of code
glPushMatrix()
glTranslatef(1,0,0)
generateObject() # This object is translated
glPopMatrix()
generateSecondObject() # This object isn't translated
它们遵循简单的后进先出(LIFO) 原则。当我们希望对矩阵进行转换时,我们首先复制它,然后将其推入变换矩阵堆栈的顶部。
换句话说,它通过创建一个本地矩阵来隔离我们在该块中执行的所有转换,我们可以在完成后废弃该矩阵。
一旦对象被平移,我们就会从堆栈中弹出变换矩阵,而其余矩阵保持不变。
六、多重转换执行
在 OpenGL 中,如前所述,变换被添加到变换矩阵堆栈顶部的活动变换矩阵中。
这意味着转换以相反的顺序执行。例如:
######### First example ##########
glTranslatef(-1,0,0)
glRotatef(30,0,0,1)
drawObject1()
##################################
######## Second Example #########
glRotatef(30,0,0,1)
glTranslatef(-1,0,0)
drawObject2()
#################################
在此示例中,Object1 首先旋转,然后平移,Object2 首先平移,然后旋转。最后两个概念不会在实现示例中使用,但将在本系列的下一篇文章中实际使用。
七、实施例
下面的代码在屏幕上绘制一个实心立方体,并围绕(1,1,1)矢量连续旋转 1 度。并且可以很容易地对其进行修改,通过将 替换为 来绘制线cubeQuads立方体cubeEdges:
import pygame as pg
from pygame.locals import *
from OpenGL.GL import *
from OpenGL.GLU import *
cubeVertices = ((1,1,1),(1,1,-1),(1,-1,-1),(1,-1,1),(-1,1,1),(-1,-1,-1),(-1,-1,1),(-1,1,-1))
cubeEdges = ((0,1),(0,3),(0,4),(1,2),(1,7),(2,5),(2,3),(3,6),(4,6),(4,7),(5,6),(5,7))
cubeQuads = ((0,3,6,4),(2,5,6,3),(1,2,5,7),(1,0,4,7),(7,4,6,5),(2,3,0,1))
def wireCube():
glBegin(GL_LINES)
for cubeEdge in cubeEdges:
for cubeVertex in cubeEdge:
glVertex3fv(cubeVertices[cubeVertex])
glEnd()
def solidCube():
glBegin(GL_QUADS)
for cubeQuad in cubeQuads:
for cubeVertex in cubeQuad:
glVertex3fv(cubeVertices[cubeVertex])
glEnd()
def main():
pg.init()
display = (1680, 1050)
pg.display.set_mode(display, DOUBLEBUF|OPENGL)
gluPerspective(45, (display[0]/display[1]), 0.1, 50.0)
glTranslatef(0.0, 0.0, -5)
while True:
for event in pg.event.get():
if event.type == pg.QUIT:
pg.quit()
quit()
glRotatef(1, 1, 1, 1)
glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT)
solidCube()
#wireCube()
pg.display.flip()
pg.time.wait(10)
if __name__ == "__main__":
main()
运行这段代码,会弹出一个 PyGame 窗口,渲染立方体动画:
文章来源:https://www.toymoban.com/news/detail-855333.html
八、结论
关于 OpenGL还有很多东西需要学习 - 光照、纹理、高级曲面建模、复合模块化动画等等。但不用担心,所有这些都将在以下文章中进行解释。文章来源地址https://www.toymoban.com/news/detail-855333.html
到了这里,关于【OpenGL教程4】高级 OpenGL实现:使用 PyGame 和 PyOpenGL混合编程的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!