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

Advanced OpenGL in Python with PyGame and PyOpenGL

介绍

在上一篇文章"通过Python理解OpenGL"奠定了进一步学习的基础之后,我们可以使用PyGame和PyOpenGL进入OpenGL。

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

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

在本文中,我们将跳入您需要了解的几个基本主题:

  • 使用PyGame初始化项目

  • 绘图对象

  • 迭代动画

  • 利用转换矩阵

  • 多重转换执行

  • 实施实例

  • 使用PyGame初始化项目

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

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

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

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

    由于没有必要向您卸载三本有关图形理论的书籍,因此我们将使用PyGame库为我们提供一个良好的开端。 从本质上讲,它只会缩短从项目初始化到实际建模和动画制作的过程。

    首先,我们需要从OpenGL和PyGame导入所有必需的东西:

    1
    2
    3
    4
    5
    import pygame as pg
    from pygame.locals import *

    from OpenGL.GL import *
    from OpenGL.GLU import *

    接下来,我们进行初始化:

    1
    2
    3
    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(视野):角度(度)

  • 宽高比:定义为宽高比

  • 邻近裁剪平面的z坐标:最小绘制距离

  • 远裁剪平面的z坐标:最大绘制距离

  • 因此,让我们继续使用OpenGL C代码并考虑以下参数来实现相机:

    1
    2
    void gluPerspective(GLdouble fovy, GLdouble aspect, GLdouble zNear, GLdouble zFar);
    gluPerspective(60, (display[0]/display[1]), 0.1, 100.0)

    为了更好地了解平截头体的工作原理,请参考以下图片:

    frustum view

    使用近平面和远平面可以提高性能。 实际上,渲染我们视野之外的任何东西都是在浪费硬件性能,而这些硬件性能可用于渲染我们可以实际看到的东西。

    因此,玩家看不到的所有内容都隐式存储在内存中,即使它们在视觉上不存在。 这是一段很棒的视频,展示了仅在平截头体内部呈现的样子。

    绘图对象

    设置完成后,我想我们会问自己一个相同的问题:

    好吧,这一切都很好,但我该如何制作超级巨星毁灭者-

    好吧...带点。 OpenGL对象中的每个模型都存储为一组顶点及其关系(连接了哪些顶点)。 因此,从理论上讲,如果您知道绘制超级星毁灭者所用的每个点的位置,那么就可以绘制一个!

    我们可以通过几种方法在OpenGL中为对象建模:

  • 使用顶点进行绘制,并且取决于OpenGL如何解释这些顶点,我们可以使用以下方法进行绘制:
  • 点:就像没有以任何方式连接的文字点一样
  • 线:每对顶点构成一条连接的线
  • 三角形:每三个顶点组成一个三角形
  • 四边形:每四个顶点成一个四边形
  • 多边形:你明白了
  • 还有很多...
  • 点:与没有以任何方式连接的文字点一样

  • 线:每对顶点构成一条连接的线

  • 三角形:每三个顶点组成一个三角形

  • 四边形:每四个顶点成一个四边形

  • 多边形:你明白了

  • 还有很多...

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

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

    drawing a cube

    然后,我们需要定义它们之间的连接方式。 如果要制作线立方体,则需要定义立方体的边缘:

    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等。

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

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

    这也是直观的-为了在立方体的顶部制作四边形,我们希望为点0364之间的所有内容"着色"。

    请记住,实际上有一个原因,我们将顶点标记为定义数组的索引。这使得编写连接它们的代码非常容易。

    以下函数用于绘制有线立方体:

    1
    2
    3
    4
    5
    6
    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的坐标定义一个顶点的函数,该坐标放置在一个向量(元组)内(替代方案为glVertex3fl,它使用一个参数列表而不是向量)

  • 按照类似的逻辑,以下函数用于绘制实体立方体:

    1
    2
    3
    4
    5
    6
    def solidCube():
        glBegin(GL_QUADS)
        for cubeQuad in cubeQuads:
            for cubeVertex in cubeQuad:
                glVertex3fv(cubeVertices[cubeVertex])
        glEnd()

    迭代动画

    为了使我们的程序"可杀死",我们需要插入以下代码片段:

    1
    2
    3
    4
    for event in pg.event.get():
        if event.type == pg.QUIT:
            pg.quit()
            quit()

    基本上,它只是一个在PyGame的事件中滚动的侦听器,并且如果检测到我们单击了" kill window"按钮,则会退出该应用程序。

    在以后的文章中,我们将介绍PyGame的更多事件-立即介绍该事件,因为用户和您自己每次想要退出应用程序时都必须启动任务管理器会非常不舒服。

    在此示例中,我们将使用双缓冲,这意味着我们将使用两个缓冲区(可以将它们视为绘制画布),它们将以固定的间隔交换并给出运动的错觉。

    知道这一点,我们的代码必须具有以下模式:

    1
    2
    3
    4
    5
    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。

  • 该代码应如下所示:

    1
    2
    3
    4
    5
    6
    while True:
        handleEvents()
        glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT)
        doTransformationsAndDrawing()
        pg.display.flip()
        pg.time.wait(1)

    利用转换矩阵

    在上一篇文章中,我们解释了从理论上讲我们如何构造一个具有引用点的转换。

    OpenGL的工作方式相同,如以下代码所示:

    1
    2
    3
    glTranslatef(1,1,1)
    glRotatef(30,0,0,1)
    glTranslatef(-1,-1,-1)

    在此示例中,我们在xy平面中进行了z轴旋转,旋转中心为(1,1,1) 30度。

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

  • z轴旋转意味着我们围绕z轴旋转

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

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

    但是有一个陷阱-OpenGL通过不断地记住和修改一个全局转换矩阵来理解上面的代码。

    因此,当您在OpenGL中编写内容时,您的意思是:

    1
    2
    3
    4
    5
    # 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()

    1
    2
    3
    4
    5
    6
    # 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中,将转换添加到位于转换矩阵堆栈顶部的活动转换矩阵中。

    这意味着转换以相反的顺序执行。 例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    ######### 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度。 通过将cubeQuadscubeEdges换出,可以很容易地修改它以绘制线形立方体:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    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窗口,渲染立方体动画:

    pygame cube animation

    结论

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

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

    不用担心,在下一篇文章中,我们实际上将绘制一些像样的东西。