PyOpenGL代码实战(三):基本图形绘制——glDrawArrays

一、理论基础

1、渲染管线

3D建模设计师在完成模型设计后,会产生一个模型文件。这个文件中储存了模型中每个顶点的数据(一般包括坐标、法线、uv等),以及哪些顶点构成一个面。渲染管线即是对这些数据进行处理,并在计算机中绘制图形的过程

接下来,我将为大家简要介绍一下渲染管线的全过程。

此处只是对渲染管线的简要介绍,省略了其中繁杂的细节。更加详细的内容,请读者自行查阅相关资料。

在这里插入图片描述

首先,我们需要确定要绘制哪些内容,并将这些内容的数据打包交给GPU。这一阶段完全由CPU负责,被称为应用阶段

GPU接收到CPU传来的数据后,就要根据数据绘制图形了,这一阶段被称为几何阶段。几何阶段的具体流程如下:

  1. 顶点处理(Vertex Processing)。CPU传来的数据都是顶点数据,其中最重要的是顶点的坐标,而这些坐标是在模型坐标系(每个模型自身拥有的独立的三维坐标系)中的。顶点处理阶段的一个重要任务就是将模型坐标转换为NDC坐标。具体来说有三个步骤:
    1.1 模型坐标首先通过模型变换(Model Transformation)转换为世界坐标。模型坐标只记录了模型内的各个顶点之间的相对位置关系,而并没有确定这个模型在世界内是如何摆放的,游戏开发者或场景建模师可以将这些模型摆在世界的任何位置,并且对模型进行缩放和旋转,以满足他们自己的要求。
    1.2 之后,世界坐标通过视图变换(View Transformation)转换为观察坐标。这一步的目的是将场景中的摄像机移动到世界坐标原点的位置,并使其朝向-z方向(场景中的所有物体都会跟随摄像机移动,以保持物体相对位置不变),便于后续将场景投影到2D屏幕上。
    1.3 最后,通过投影变换(Projection Transformation)和透视除法转换为NDC坐标。投影变换能够将摄像机能看到的场景投影到2D空间内,它的实现原理比较复杂,这里不展开讲解。
    模型变换、视图变换、投影变换合在一起称为MVP变换。顶点处理是通过一个叫做顶点着色器(Vertex Shader)的运行在GPU中的程序实现的,顶点着色器程序由用户自行编写。

NDC坐标在上一章有详细介绍。

投影变换的作用是将3D转2D,但是NDC空间仍然是3D空间,那这个空间的z轴的含义是什么呢?
实际上,经过投影变换后,顶点的z坐标已经不能具体表示该顶点在z轴上的位置,但是经过变换后,不同顶点的z坐标大小关系仍不变,因此z坐标仍然可以作为判断物体前后遮挡关系的依据,这正是投影变换后保留z坐标的原因。后续章节会介绍的深度测试正是用到了这一原理。

  1. 图元装配。这一阶段的主要任务是将NDC坐标通过视口变换(Viewport Transformation)转换为屏幕坐标。同时也将进行裁剪、背面剔除等提高效率的工作。这一阶段由硬件自动完成。
  2. 光栅化(Rasterization)。经过上述步骤,原本用模型坐标表示的顶点数据已经被转换为屏幕坐标数据。这一步的任务就是将三角形绘制在屏幕上。三角形中的每一个像素都有自己的颜色,而颜色值是由片元着色器(Fragment Shader)确定的。片元着色器程序由用户编写,GPU运行。

经过上述步骤,就能够在屏幕中绘制三角形了。

2、MVP变换

上面已经说过,MVP变换时将模型坐标转换为NDC坐标的过程。具体说来,我们可以通过模型在世界中的摆放位置和方向、摄像机在世界中的位置和朝向等信息,可以计算出一个矩阵(MVP矩阵)。将原来的模型坐标与MVP矩阵相乘,即可将模型坐标转换为NDC坐标。MVP矩阵的具体计算方法这里不做介绍,在OpenGL中,可以通过调用API自动完成计算。

3、着色器

上面说过,在几何阶段,有两个程序需要用户自行编写:顶点着色器和片元着色器。每个顶点都会运行一次顶点着色器程序,而片元着色器则是针对每个像素而言的。

着色器的具体内容会在以后介绍,在本章,会直接为读者提供最基础的着色器程序。

二、绘制三角形

1、顶点数据

首先,提供三角形的顶点数据。

import numpy as np
# 顶点坐标数组,一定要是numpy数组!数据类型为np.float32(4字节float数据)
triangle = np.array([
    # 每一行为一个顶点,前三个元素为顶点坐标,后三个元素为顶点颜色
    -0.5, -0.5, 0, 1, 0, 0,
    0.5, -0.5, 0, 0, 1, 0,
    0, 0.5, 0, 0, 0, 1
], dtype=np.float32)

注意,上述的数组中,每一行为一个顶点数据,前三个元素构成一个顶点坐标,此处的顶点坐标为NDC坐标。事实上模型文件中的顶点坐标都是模型坐标,通过MVP变换后才会转换成NDC坐标。我们以后会讲到如何进行MVP变换。暂时我们直接定义顶点的NDC坐标,跳过MVP变换的步骤。

2、VBO

刚才定义的顶点数据是储存在内存中的,要将这些数据传入GPU,就需要VBO(Vertex Buffer Object,顶点缓冲对象)的帮助。

from OpenGL.arrays.vbo import VBO	# 引入VBO类
vbo = VBO(triangle, usage=GL_STATIC_DRAW, target=GL_ARRAY_BUFFER)	# 创建VBO
vbo.bind()	# 绑定VBO

VBO(triangle, usage=GL_STATIC_DRAW, target=GL_ARRAY_BUFFER)中的最后一个参数就是默认值,所以可以省略为VBO(triangle, GL_STATIC_DRAW)

VBO(triangle, usage=GL_STATIC_DRAW, target=GL_ARRAY_BUFFER):这句代码创建了一个VBO对象,其中第一个参数是顶点数据。

第二个参数usage指定了我们希望显卡如何管理给定的数据。它有三种形式:

usage说明
GL_STATIC_DRAW数据不会或几乎不会改变
GL_DYNAMIC_DRAW数据常常会发生改变
GL_STREAM_DRAW数据每次绘制时都会改变

第三个参数target是什么意思呢?这实际上与OpenGL的状态机机制有关。在OpenGL中,很多对象需要被绑定到被称为“目标”的位置才能使用,而GL_ARRAY_BUFFER正是这些目标位置之一。所以这里的target参数,表示了这个VBO对象应该占据的目标位置。而下面那句vbo.bind()则是真正占据这个目标位置。此后,对于所有针对GL_ARRAY_BUFFER目标的操作,实际上都是对此对象的操作。

上面这段话可能难以理解,这里打个比方。
你可以把OpenGL看成一个大公司,在这个公司里有很多职位,每种职位都有它对应的工位。VBO(triangle, usage=GL_STATIC_DRAW, target=GL_ARRAY_BUFFER)中的target,就是为当前正在创建的vbo分配一个名为GL_ARRAY_BUFFER的职位,而vbo.bind()则是让这个vbo在它的工位上落座,开始干活。
但是,OpenGL公司有些奇葩,职位为GL_ARRAY_BUFFER的员工可能有很多个,但是GL_ARRAY_BUFFER所对应的工位却只有一个,如果其他的vbo想要工作怎么办呢?只能由这个vbo调用bind()方法,抢占掉原来vbo的位置。因此,在每次使用vbo之前,都应该调用它的bind方法,让它能够正常工作。

3、着色器

下面直接提供顶点着色器和片元着色器的代码:

# 顶点着色器
vs = """
#version 330 core
in vec3 aPos;
in vec3 aColor;

out vec3 VertexColor;

void main()
{
    gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
    VertexColor = aColor;
}
"""

# 片元着色器
fs = """
#version 330 core
in vec3 VertexColor;

out vec4 FragColor;

void main()
{
    FragColor = vec4(VertexColor.rgb, 1.0f);
} 
"""

注意上面的着色器代码是直接嵌入python程序中的,在正式程序中,这些代码常常会从其它文件读取,python读取文件的操作很容易,这里不再讲解。

以后我们会详细介绍着色器的编写方式。现在读者只需要关注顶点着色器中的这两行代码:

in vec3 aPos;
in vec3 aColor;

这两行代码都是定义变量的代码。其中,in表示这个变量由CPU传入,vec3定义数据类型为3维向量。

上述代码只是定义了着色器的程序代码,接下来我们还要对这段程序代码进行编译:

from OpenGL.GL import shaders

vs_program = shaders.compileShader(vs, GL_VERTEX_SHADER)
fs_program = shaders.compileShader(fs, GL_FRAGMENT_SHADER)
program = shaders.compileProgram(vs_program, fs_program)

经过上述代码,最终的program变量即为编译后的shader程序。

4、解释数据含义

之前我们已经定义了表示顶点数据的数组:

triangle = np.array([
    # 每一行为一个顶点,前三个元素为顶点坐标,后三个元素为顶点颜色
    -0.5, -0.5, 0, 1, 0, 0,
    0.5, -0.5, 0, 0, 1, 0,
    0, 0.5, 0, 0, 0, 1
], dtype=np.float32)

我们也通过VBO将这些数据交给了GPU,但GPU并不知道这些数据的含义。通过以下代码,可以让GPU理解数据所表示的含义:

# 坐标
a_pos_loc = glGetAttribLocation(program, 'aPos')
glVertexAttribPointer(a_pos_loc, 3, GL_FLOAT, GL_FALSE, 24, ctypes.c_void_p(0))
glEnableVertexAttribArray(a_pos_loc)
# 颜色
a_color_loc = glGetAttribLocation(program, 'aColor')
glVertexAttribPointer(a_color_loc, 3, GL_FLOAT, GL_FALSE, 24, ctypes.c_void_p(12))
glEnableVertexAttribArray(a_color_loc)

前面给出的顶点着色器代码中,有两个变量需要CPU传入:aPosaColor。在着色器编译时,编译器会为它们指定一个索引,通过glGetAttribLocation(program, name)即可获取这个索引。

glVertexAttribPointer(aPosLoc, 3, GL_FLOAT, GL_FALSE, 24, ctypes.c_void_p(0)):这句代码是解释了顶点的某一个属性的信息。它的参数较多,我们一一介绍:

第一个参数:该属性的索引。

第二个参数:该属性的元素数量。在本文中,属性aPos的类型为vec3,需要三个浮点型的元素。

第三个参数:该属性的元素的类型。注意不能用floatnp.float32代替GL_FLOAT

第四个参数:是否标准化。如果为GL_TRUE,则数据会自动标准化到 [ 0 , 1 ] [0, 1] [0,1]之间。可以用TrueFalse代替。

第五个参数:步长。这个参数表示两个顶点的同一属性之间的距离(字节)。对于本文而言,每两个顶点之间相差了6个np.float32类型的元素,因此步长为 4 × 6 = 24 4\times6=24 4×6=24个字节。

第六个参数:数组中第一个表示当前属性的元素相对于数组起始位置的偏移量(位)。对于aPos而言,其偏移量为0;对于aColor而言,其偏移量为12位。

在这里插入图片描述

最后,glEnableVertexAttribArray(loc),启用顶点属性。

5、绘制图形

def render():
    glUseProgram(program)	# 使用着色器
    glDrawArrays(GL_TRIANGLES, 0, 3)	# 绘制三角形

glDrawArrays(GL_TRIANGLES, 0, 3)用于绘制三角形。其第一个参数表示需要绘制的图形的类型;第二个参数是顶点数组的起始索引;第三个参数是要绘制的顶点个数。

如果你运行时出现类似于numpy.dtype size changed, may indicate binary incompatibility. Expected 96 from C header, got 88 from PyObject的错误,尝试重新安装版本为1.26.4的numpy。

完整代码:

import numpy as np
from OpenGL.GL import *
from OpenGL.GL import shaders
from Window import Window
from OpenGL.arrays.vbo import VBO

program = None


def render():
    glUseProgram(program)  # 使用着色器
    glDrawArrays(GL_TRIANGLES, 0, 3)  # 绘制三角形


if __name__ == '__main__':
    width, height, title = 1920, 1080, "Test"
    w = Window(width, height, title)

    # 顶点坐标数组,一定要是numpy数组!数据类型为np.float32(4字节float数据)
    triangle = np.array([
        # 每一行为一个顶点,前三个元素为顶点坐标,后三个元素为顶点颜色
        -0.5, -0.5, 0, 1, 0, 0,
        0.5, -0.5, 0, 0, 1, 0,
        0, 0.5, 0, 0, 0, 1
    ], dtype=np.float32)

    vbo = VBO(triangle, usage=GL_STATIC_DRAW, target=GL_ARRAY_BUFFER)
    vbo.bind()

    # 顶点着色器
    vs = """
        #version 330 core
        in vec3 aPos;
        in vec3 aColor;

        out vec3 VertexColor;

        void main()
        {
            gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
            VertexColor = aColor;
        }
        """

    # 片元着色器
    fs = """
        #version 330 core
        in vec3 VertexColor;

        out vec4 FragColor;

        void main()
        {
            FragColor = vec4(VertexColor.rgb, 1.0f);
        } 
        """

    vs_program = shaders.compileShader(vs, GL_VERTEX_SHADER)
    fs_program = shaders.compileShader(fs, GL_FRAGMENT_SHADER)
    program = shaders.compileProgram(vs_program, fs_program)

    # 坐标
    a_pos_loc = glGetAttribLocation(program, 'aPos')
    glVertexAttribPointer(a_pos_loc, 3, GL_FLOAT, GL_FALSE, 24, ctypes.c_void_p(0))
    glEnableVertexAttribArray(a_pos_loc)
    # 颜色
    a_color_loc = glGetAttribLocation(program, 'aColor')
    glVertexAttribPointer(a_color_loc, 3, GL_FLOAT, GL_FALSE, 24, ctypes.c_void_p(12))
    glEnableVertexAttribArray(a_color_loc)

    w.add_render_event(render)
    w.loop()

结果:

在这里插入图片描述

6、VAO

至此,我们已经成功绘制出了三角形。但是依然存在一个问题:现在我们只绘制了一个物体,如果我们的场景中有上百个不同的物体呢?我们对每一个物体,都要重复一次上述过程,配置顶点的状态信息。有没有一些方法可以使我们把所有这些状态配置储存在一个对象中,并且可以通过绑定这个对象来恢复状态呢?

VAO(Vertex Array Object, 顶点数组对象)可以像顶点缓冲对象那样被绑定,任何随后的顶点属性调用都会储存在这个VAO中。这样的好处就是,当配置顶点属性指针时,你只需要将那些调用执行一次,之后再绘制物体的时候只需要绑定相应的VAO就行了。这使在不同顶点数据和属性配置之间切换变得非常简单,只需要绑定不同的VAO就行了。刚刚设置的所有状态都将存储在VAO中。

一个VAO会储存以下这些内容:

  • glEnableVertexAttribArrayglDisableVertexAttribArray的调用。
  • 通过glVertexAttribPointer设置的顶点属性配置。
  • 通过glVertexAttribPointer调用与顶点属性关联的顶点缓冲对象。

在这里插入图片描述

以上对于VAO的介绍来自于LearnOpenGL CN

创建VAO:

vao = glGenVertexArrays(1)
glBindVertexArray(vao)

glGenVertexArrays用于创建VAO对象,其参数表示一次创建多少个对象。glBindVertexArray用于绑定VAO。

绑定VAO后再配置顶点数据信息,VAO会存储这些内容。

使用VAO后的完整代码:

import numpy as np
from OpenGL.GL import *
from OpenGL.GL import shaders
from Window import Window
from OpenGL.arrays.vbo import VBO

program = None


def render():
    glUseProgram(program)  # 使用着色器
    glDrawArrays(GL_TRIANGLES, 0, 3)  # 绘制三角形


if __name__ == '__main__':
    width, height, title = 1920, 1080, "Test"
    w = Window(width, height, title)

    # 顶点坐标数组,一定要是numpy数组!数据类型为np.float32(4字节float数据)
    triangle = np.array([
        # 每一行为一个顶点,前三个元素为顶点坐标,后三个元素为顶点颜色
        -0.5, -0.5, 0, 1, 0, 0,
        0.5, -0.5, 0, 0, 1, 0,
        0, 0.5, 0, 0, 0, 1
    ], dtype=np.float32)

    vao = glGenVertexArrays(1)
    glBindVertexArray(vao)

    vbo = VBO(triangle, usage=GL_STATIC_DRAW, target=GL_ARRAY_BUFFER)
    vbo.bind()

    # 顶点着色器
    vs = """
        #version 330 core
        in vec3 aPos;
        in vec3 aColor;

        out vec3 VertexColor;

        void main()
        {
            gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
            VertexColor = aColor;
        }
        """

    # 片元着色器
    fs = """
        #version 330 core
        in vec3 VertexColor;

        out vec4 FragColor;

        void main()
        {
            FragColor = vec4(VertexColor.rgb, 1.0f);
        } 
        """

    vs_program = shaders.compileShader(vs, GL_VERTEX_SHADER)
    fs_program = shaders.compileShader(fs, GL_FRAGMENT_SHADER)
    program = shaders.compileProgram(vs_program, fs_program)

    # 坐标
    a_pos_loc = glGetAttribLocation(program, 'aPos')
    glVertexAttribPointer(a_pos_loc, 3, GL_FLOAT, GL_FALSE, 24, ctypes.c_void_p(0))
    glEnableVertexAttribArray(a_pos_loc)
    # 颜色
    a_color_loc = glGetAttribLocation(program, 'aColor')
    glVertexAttribPointer(a_color_loc, 3, GL_FLOAT, GL_FALSE, 24, ctypes.c_void_p(12))
    glEnableVertexAttribArray(a_color_loc)

    w.add_render_event(render)
    w.loop()

结果与之前完全相同。

除了VAO、VBO之外,OpenGL中还有一种常用的用于绘制物体的对象——EBO。关于EBO的内容,限于篇幅不再介绍,感兴趣的读者可以自行搜索相关资料了解。

三、结语

本章介绍了渲染管线,并讲解了一种常用的绘制三角形的方法——glDrawArrays。在下一章中,将详细讲解着色器的编写过程。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值