image.pngPyOpenGL 是 OpenGL 和 Python 之间的标准化桥梁。PyGame 是一个用 Python 制作游戏的标准库。在本文中,我们将充分利用这两者,并介绍 OpenGL 中使用 Python 的一些重要主题。
我们可以使用 PyGame 和 PyOpenGL 来进入 OpenGL 。
PyOpenGL 是标准化库,用作 Python 和 OpenGL API 之间的桥梁,PyGame 是用于在 Python 中制作游戏的标准库。它提供了内置的图形和音频库,我们将使用它来在文章末尾更轻松地渲染结果。
如前一篇文章所述, OpenGL 非常古老,因此您在网上找不到许多有关如何正确使用它和理解它的教程,因为所有顶尖人物都已经深入到新技术中。
使用 PyGame
初始化项目
首先,如果您尚未这样做,请安装 PyGame 和 PyOpenGL:
python3 -m pip install -U pygame --user
python3 -m pip install PyOpenGL PyOpenGL_accelerate
如果您在安装方面遇到问题,PyGame
的 入门指南
可能是一个好地方。
https://www.pygame.org/wiki/GettingStarted
我们将使用 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 个关键参数定义:
- 视野(Field of View,
FOV
):角度制 - 宽高比:定义为宽度和高度的比率
- 近裁剪面的 z 坐标:最小绘制距离
- 远裁剪面的 z 坐标:最大绘制距离
因此,让我们根据这些参数使用 OpenGL C 代码实现摄像机:
void gluPerspective(GLdouble fovy, GLdouble aspect, GLdouble zNear, GLdouble zFar);
gluPerspective(60, (display[0]/display[1]), 0.1, 100.0)
为了更好地理解视锥体的工作原理,以下是一个参考图片:
image.png
近和远平面用于提高性能。实际上,在我们的视野范围之外渲染任何东西都是浪费硬件性能,这些性能可以用来渲染我们实际可以看到的东西。
绘制对象
在进行此设置后,我想我们会问自己同样的问题:这一切都好极了,但我如何制作一个超级星际毁灭者?
在 OpenGL 中,每个对象的模型都存储为一组顶点及其关系的集合(哪些顶点相连)。因此,理论上,如果您知道用于绘制超级星际毁灭者的每个点的位置,您完全可以绘制一个!
我们可以通过以下几种方式来在 OpenGL 中建模对象:
- 使用顶点进行绘制,并根据 OpenGL 解释这些顶点的方式,我们可以绘制:
- 点:字面上的点,它们没有以任何方式连接
- 线:每对顶点构成一个连接线
- 三角形:每三个顶点构成一个三角形
- 四边形:每四个顶点构成一个四边形
- 多边形:你懂的
- 更多...
- 使用 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))
image.png
接下来,我们需要定义它们之间的关系。如果我们要制作一个线框立方体,我们需要定义立方体的边:
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
:使用类型为GLfloat
的 3 个坐标定义顶点的函数 -
glVertex3fv
:使用类型为GLfloat
的 3 个坐标定义顶点的函数,这些坐标被放置在向量(元组)中(另一种选择是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 事件 - 立即引入此事件是因为对于用户和您自己,每次想要退出应用程序时都必须启动任务管理器可能会非常不舒适。
在本示例中,我们将使用双缓冲技术,这意味着我们将使用两个缓冲区(您可以将它们视为绘图画布),这些缓冲区将在固定时间间隔内交换,从而产生运动的幻觉。
因此,我们的代码必须遵循以下模式:
# 处理事件()
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
# 进行变换和绘制()
pg.display.flip()
pg.time.wait(1)
-
glClear
:清除指定的缓冲区(画布)的函数,在本例中为颜色缓冲区(包含绘制生成对象所需的颜色信息)和深度缓冲区(存储所有生成对象的前后关系)。 -
pg.display.flip()
:更新具有活动缓冲区内容的窗口的函数。 -
pg.time.wait(1)
:暂停程序一段时间的函数。
我们必须使用 glClear
,因为如果我们不使用它,我们将只是在已经绘制的画布上涂画,而在本例中,我们的屏幕将变得混乱不堪。
接下来,如果我们想要连续更新我们的屏幕,就像动画一样,我们必须将所有代码放在一个 while 循环中,其中我们:
- 1 处理事件(在本例中仅退出)。
- 2 清除颜色和深度缓冲区,以便可以再次绘制它们。
- 3 变换和绘制对象。
- 4 更新屏幕。
- 5 GOTO 1:。
代码应该类似于:
-
glClear
:清除指定的缓冲区(画布)的函数,在本例中为颜色缓冲区(包含绘制生成对象所需的颜色信息)和深度缓冲区(存储所有生成对象的前后关系)。 -
pg.display.flip()
:更新具有活动缓冲区内容的窗口的函数。 -
pg.time.wait(1)
:暂停程序一段时间的函数。
我们必须使用 glClear
,因为如果我们不使用它,我们将只是在已经绘制的画布上涂画,而在本例中,我们的屏幕将变得混乱不堪。
接下来,如果我们想要连续更新我们的屏幕,就像动画一样,我们必须将所有代码放在一个 while 循环中,其中我们:
- 1 处理事件(在本例中仅退出)。
- 2 清除颜色和深度缓冲区,以便可以再次绘制它们。
- 3 变换和绘制对象。
- 4 更新屏幕。
- 5 GOTO 1。
代码应该类似于:
while True:
# 处理事件()
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
# 进行变换和绘制()
pg.display.flip()
pg.time.wait(1)
利用变换矩阵
理论上需要构建一个具有引用点的变换。
OpenGL 的工作方式相同,如下面的代码所示:
glTranslatef(1, 1, 1)
glRotatef(30, 0, 0, 1)
glTranslatef(-1, -1, -1)
在此示例中,我们在 xy 平面上的z轴上以 30 度对绕 z 轴旋转进行了旋转,以 (1,1,1)
作为旋转中心。
如果这些术语听起来有点混乱,请让我们回顾一下:
- z 轴旋转意味着我们围绕 z 轴旋转。
这只是意味着我们用3D空间近似 2D 平面,这整个变换基本上类似于在 2D 空间中围绕引用点进行普通旋转。
- 通过将整个 3D 空间压缩成一个
z=0
的平面(在所有方式中消除 z 参数),我们得到xy
平面。
3.旋转中心是我们将围绕其旋转给定对象的顶点(默认旋转中心是原点顶点 (0,0,0)
)。
但是有一个问题 : OpenGL 通过不断记忆和修改一个全局变换矩阵来理解上述代码。
因此,当您在 OpenGL 中编写某些内容时,您要表达的是:
# 变换矩阵= E(中立)
glTranslatef(1, 1, 1)
# 变换矩阵= TxE
# 从现在开始的所有对象都将平移 `(1,1,1)`
正如您可以想象的那样,这会带来巨大的问题,因为有时我们想要在单个对象上使用变换,而不是在整个源代码上使用变换。这是低级 OpenGL 中出现错误的一个非常常见的原因。
为了解决 OpenGL 的这个问题,我们提供了推送和弹出变换矩阵 glPushMatrix()
和 glPopMatrix()
:
# 此代码块之前,变换矩阵为 T1
glPushMatrix()
glTranslatef(1, 0, 0)
generateObject() # 此对象已被平移
glPopMatrix()
generateSecondObject() # 此对象未被平移
这些按照简单的 后进先出( LIFO
)原则工作。当我们希望对矩阵执行平移时,我们首先要复制它,然后将其推送到变换矩阵堆栈的顶部。
换句话说,它通过创建一个可以在完成后删除的本地矩阵来隔离我们在此块中执行的所有变换。
一旦对象被平移,我们就从堆栈中弹出变换矩阵,使其余矩阵不受影响。
多变换执行
在 OpenGL 中,如前所述,变换被添加到变换矩阵堆栈的顶部的活动变换矩阵中。
这意味着变换是按相反的顺序执行的。例如:
第一个例子
######### 第一个例子 ##########
glTranslatef(-1, 0, 0)
glRotatef(30, 0, 0, 1)
drawObject1()
#############################
第二个例子
########## 第二个例子 #########
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 窗口,渲染立方体动画:
24b9f4fbb9795086fcea71564e97042c.gif结论
关于 OpenGL 还有很多要学习的内容 - 灯光,纹理,高级表面建模,复合模块化动画等等。
网友评论