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

一、理论基础

1、渲染管线

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

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

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

在这里插入图片描述

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

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

  1. 顶点处理(Vertex Processing)。CPU传来的数据都是顶点数据,其中最重要的是顶点的坐标,而这些坐标是在模型坐标系(每个模型自身拥有的独立的三维坐标系)中的。顶点处理阶段的一个重要任务就是将模型坐标转换为NDC坐标。具体来说,模型坐标首先通过模型变换(Model Transformation)转换为世界坐标,之后通过视图变换(View Transformation)转换为观察坐标,最后通过投影变换(Projection Transformation)和透视除法转换为NDC坐标。模型变换、视图变换、投影变换合在一起称为MVP变换。顶点处理是通过一个叫做顶点着色器(Vertex Shader)的运行在GPU中的程序实现的,顶点着色器程序由用户自行编写。

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

  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变换。

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目标的操作,实际上都是对此对象的操作。

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

vsProgram = shaders.compileShader(vs, GL_VERTEX_SHADER)
fsProgram = shaders.compileShader(fs, GL_FRAGMENT_SHADER)
program = shaders.compileProgram(vsProgram, fsProgram)

经过上述代码,最终的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理解数据所表示的含义:

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

前面给出的顶点着色器代码中,有两个变量需要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)用于绘制三角形。其第一个参数表示需要绘制的图形的类型;第二个参数是顶点数组的起始索引;第三个参数是要绘制的顶点个数。

完整代码:

from OpenGL.GL import *
from OpenGL.GL import shaders
from OpenGL.arrays.vbo import VBO
from window import Window	# 第一章中封装的Window类
import numpy as np
# 创建窗口
w = Window(1920, 1080, "Test")
# 定义数据
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 = VBO(triangle, GL_STATIC_DRAW)
vbo.bind()
# 着色器(具体内容在前面)
vs = """ ... """
fs = """ ... """
# 编译着色器
vsProgram = shaders.compileShader(vs, GL_VERTEX_SHADER)
fsProgram = shaders.compileShader(fs, GL_FRAGMENT_SHADER)
program = shaders.compileProgram(vsProgram, fsProgram)
# 解释数据含义
aPosLoc = glGetAttribLocation(program, 'aPos')
glVertexAttribPointer(aPosLoc, 3, GL_FLOAT, False, 24, ctypes.c_void_p(0))
glEnableVertexAttribArray(aPosLoc)
aColorLoc = glGetAttribLocation(program, 'aColor')
glVertexAttribPointer(aColorLoc, 3, GL_FLOAT, False, 24, ctypes.c_void_p(12))
glEnableVertexAttribArray(aColorLoc)
# 渲染循环
def render():
    glUseProgram(program)
    glDrawArrays(GL_TRIANGLES, 0, 3)

w.loop(render)

结果:

在这里插入图片描述

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后的完整代码:

from OpenGL.GL import *
from OpenGL.GL import shaders
from OpenGL.arrays.vbo import VBO
from window import Window	# 第一章中封装的Window类
import numpy as np
# 创建窗口
w = Window(1920, 1080, "Test")
# 定义数据
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
vao = glGenVertexArrays(1)
glBindVertexArray(vao)
# 创建、绑定VBO
vbo = VBO(triangle, GL_STATIC_DRAW)
vbo.bind()
# 着色器(具体内容在前面)
vs = """ ... """
fs = """ ... """
# 编译着色器
vsProgram = shaders.compileShader(vs, GL_VERTEX_SHADER)
fsProgram = shaders.compileShader(fs, GL_FRAGMENT_SHADER)
program = shaders.compileProgram(vsProgram, fsProgram)
# 解释数据含义
aPosLoc = glGetAttribLocation(program, 'aPos')
glVertexAttribPointer(aPosLoc, 3, GL_FLOAT, False, 24, ctypes.c_void_p(0))
glEnableVertexAttribArray(aPosLoc)
aColorLoc = glGetAttribLocation(program, 'aColor')
glVertexAttribPointer(aColorLoc, 3, GL_FLOAT, False, 24, ctypes.c_void_p(12))
glEnableVertexAttribArray(aColorLoc)
# 渲染循环
def render():
    glUseProgram(program)
	glBindVertexArray(vao)
    glDrawArrays(GL_TRIANGLES, 0, 3)

w.loop(render)

结果与之前完全相同。

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

三、结语

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

  • 3
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值