📜  使用PyGame和PyOpenGL的Python中的高级OpenGL

📅  最后修改于: 2020-09-05 11:11:34             🧑  作者: Mango

介绍

PyOpenGL是用于在Python和OpenGL API之间建立桥梁的标准化库,而PyGame是用于在Python中制作游戏的标准化库。它提供了内置的方便的图形和音频库,在本文结尾处,我们将使用它来更轻松地呈现结果。

如前一篇文章所述,OpenGL很老,因此您不会在网上找到许多有关如何正确使用和理解它的教程,因为所有顶级公司都已经深陷于新技术之中。

 

使用PyGame初始化项目

首先,如果您尚未安装PyGame和PyOpenGL,则需要: 

$ python3 -m pip install -U pygame --user
$ python3 -m pip install PyOpenGL PyOpenGL_accelerate

注意:您可以在上一篇OpenGL文章中找到更详细的安装信息。

如果您在安装方面遇到问题,PyGame的“入门”部分可能是个不错的地方。

由于没有必要向您卸载三本有关图形理论的书籍,因此我们将使用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个关键参数定义:

  1. FOV(视野):角度(度)
  2. 宽高比:定义为宽高比
  3. 接近裁剪平面的z坐标最小绘制距离
  4. 远裁剪平面的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中为对象建模:

  1. 使用顶点进行绘制,并且取决于OpenGL如何解释这些顶点,我们可以使用以下方法进行绘制:
    • points:与没有任何形式连接的文字点一样
    • 线:每对顶点构成一条连接的线
    • 三角形:每三个顶点组成一个三角形
    • 四边形:每四个顶点组成一个四边形
    • 多边形:你明白了
    • 还有很多…
  2. 使用由OpenGL贡献者精心建模的内置形状和对象进行绘图
  3. 导入完全建模的对象

因此,以绘制一个立方体为例,我们首先需要定义其顶点: 

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与边缘134。点1与点的边缘357,等等。

而且,如果要制作一个实心立方体,则需要定义立方体的四边形: 

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))

这也是直观-使在立方体的顶侧的四边形,我们会想“色”一切点之间036,和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:一个函数,该函数使用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的事件中滚动的侦听器,并且如果检测到我们单击了“ kill window”按钮,则会退出该应用程序。

在以后的文章中,我们将介绍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. 处理事件(在这种情况下,只是退出)
  2. 清除颜色和深度缓冲区,以便可以再次绘制它们
  3. 变换和绘制对象
  4. 更新画面
  5. 转到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)

在这个例子中,我们做了z轴在旋转xy平面与所述旋转的中心存在(1,1,1)30度。

如果这些术语听起来有点令人困惑,那么让我们再回顾一下:

  1. z轴旋转意味着我们围绕z轴旋转

    这只是意味着我们用3D空间逼近2D平面,整个转换基本上就像围绕2D空间中的参照点进行法向旋转。

  2. 我们通过将整个3D空间挤压到具有xy平面的平面中z=0(通过各种方式消除z参数)
  3. 旋转中心是我们将围绕其旋转给定对象的顶点(默认旋转中心是原点(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窗口,渲染立方体动画:

结论

关于OpenGL,还有很多要学习的知识-照明,纹理,高级曲面建模,复合模块化动画等。

但是,不用担心,所有这些将在下面的文章中从头开始向公众教授OpenGL的正确方式进行解释。