着色器和渲染
在最底层,pyglet使用OpenGL在程序窗口中绘制图形。OpenGL接口通过 pyglet.gl
模块(请参见 OpenGL接口 )。
然而,对于新用户来说,直接使用OpenGL界面可能会让人望而生畏。这个 pyglet.graphics
模块提供高级抽象,这些抽象在内部使用顶点数组和顶点缓冲区对象来提供高性能。对于高级用户来说,这些抽象仍然有助于避免大量的OpenGL样板代码,否则这些代码将是您自己编写所必需的。
Piglet的呈现抽象由三个主要组件组成:“Vertex Domains”、“Vertex List”和“:py:class:~pyglet.graphics.shader.ShaderProgram”.以下各节将对这些内容进行更详细的说明,但大致概述如下:
ShaderProgram
位于最高级别,是标准OpenGL Shader程序的简单抽象。Pyglet执行完整的属性和统一的自省,并提供自动生成与属性格式匹配的缓冲区的方法。Vertex Domains位于最低级别,用户通常不需要直接与它们交互。它们维护与特定顶点属性集合匹配的原始OpenGL顶点数组缓冲区的所有权。缓冲区会根据需要自动调整大小。对这些缓冲区的访问通常不是直接完成的,而是通过使用顶点列表来完成的。
顶点列表位于顶点属性域和
ShaderProgram
。它们提供了一个简单的“视图”,可以看到顶点区域的缓冲区的一部分。一个ShaderProgram
能够直接生成顶点列表。
总而言之,这一过程如下:
用户创建一个
ShaderProgram
。自动自省顶点属性元数据。用户创建一个新的带有
vertex_list()
方法。用户不需要担心自己创建内部缓冲区。在步骤2中创建Vertex List时,pyglet会自动匹配
ShaderProgram
的属性。(如果没有可用的现有域,则创建一个新的域)。从匹配的顶点域生成顶点列表,并返回。
使用着色器
在现代OpenGL中绘制任何内容都需要Shader程序。直接使用着色器资源可能会很繁琐,因此 pyglet.graphics.shader
模块提供简化的(但健壮的)抽象。
请参阅 OpenGL Programming SDK 有关着色器和OpenGL着色器语言(GLSL)的详细信息。
创建着色器程序
若要创建着色器程序,请首先将GLSL源代码准备为Python字符串。这可以从磁盘加载,也可以简单地在项目内部定义。以下是简化的顶点和片段源::
vertex_source = """#version 150 core
in vec2 position;
in vec4 colors;
out vec4 vertex_colors;
uniform WindowBlock
{
mat4 projection;
mat4 view;
} window;
void main()
{
gl_Position = window.projection * window.view * vec4(position, 0.0, 1.0);
vertex_colors = colors;
}
"""
fragment_source = """#version 150 core
in vec4 vertex_colors;
out vec4 final_color;
void main()
{
final_color = vertex_colors;
}
"""
备注
默认情况下,pyglet包括并设置 WindowBlock
创建窗口时保持一致。如果不使用 window.projection
或 window.view
在您的顶点着色器中,您必须自己管理投影,否则您的图形可能无法正确显示。
然后使用源字符串创建 Shader
对象,然后将它们链接在 ShaderProgram
。将着色器对象链接到 ShaderProgram
,这样它们就可以在以后丢弃(或在其他 ShaderProgram
):
from pyglet.graphics.shader import Shader, ShaderProgram
vert_shader = Shader(vertex_source, 'vertex')
frag_shader = Shader(fragment_source, 'fragment')
program = ShaderProgram(vert_shader, frag_shader)
ShaderProgram
对创作的内在反思。可以查询几个属性来检查各种可用的顶点属性、制服和统一块。例如, uniforms 和 attributes 属性将返回显示以下对象的元数据的字典:
>>> for attribute in program.attributes.items():
... print(attribute)
...
('position', {'type': 35664, 'size': 1, 'location': 0, 'count': 2, 'format': 'f'})
('colors', {'type': 35666, 'size': 1, 'location': 1, 'count': 4, 'format': 'f'})
>>> for uniform in program.uniforms.items():
... print(uniform)
...
('time', {'location': 2, 'length': 1, 'size': 1})
备注
大多数OpenGL驱动程序将在编译期间优化着色器。如果没有使用某个属性或制服,则通常会将其优化。
制服
制服是一种变量,可以在 ShaderProgram
已编译为在运行时更改功能。
警告
在设置制服时,必须在设置时绑定程序。OpenGL 4.1+中不存在此限制,但如果您计划支持较旧的上下文(如3.3),则必须考虑到这一点。
制服可以作为钥匙在 ShaderProgram
它本身。例如,如果着色器中的制服是::
uniform float time;
然后,您可以使用统一名称作为键来设置(或获取)值::
program['time'] = delta_time
统一块和统一缓冲区对象
Pyglet还引入并提供统一块(或接口块),包括管理和使用统一缓冲区对象的方法。
默认情况下,Pyglet的 projection
和 view
矩阵都包含在 WindowBlock
统一挡块。在顶点着色器中如下所示::
uniform WindowBlock
{
mat4 projection;
mat4 view;
} window;
您可以查看存在于 ShaderProgram
使用 uniform_blocks 财产。这是一个包含统一块名键的字典 UniformBlock
对象值。在上面的示例中,名称将为 WindowBlock
当. window
实例标识符用于GLSL着色器本身。
通常,对于OpenGL,您必须在创建每个着色器程序时为每个均匀块手动分配全局绑定点值。使用Pyglet,全局绑定值和分配都在内部处理。
统一块是一次更新多个Shader程序统一的便捷方法,因为它们的数据是共享的。这允许您从多个Shader程序访问相同的信息,而不必绑定使用它的每个程序,只需修改统一值。这可以通过 UniformBufferObject
。
要修改制服,请在 UniformBlock
,您必须首先创建一个 UniformBufferObject
使用 create_ubo()
方法。::
ubo = program.uniform_blocks['WindowBlock'].create_ubo()
这个 UniformBufferObject
然后可以用作上下文管理器,以便轻松访问更新其制服::
with ubo as window_block:
window_block.projection[:] = new_matrix
您还可以创建多个 UniformBufferObject
如果您需要在不同的数据集之间交换,则可以使用实例。调用 bind()
将将缓冲区数据绑定到关联的绑定点。
可能有一天您不想要具体的 ShaderProgram
,或其中一组,以使用与其余着色器相同的统一数据集。此时,您必须将这些均匀块的绑定点修改为未使用的绑定点。这可以通过 set_binding()
.设置绑定后,您将必须创建新的 UniformBufferObject
使用 create_ubo()
方法并向它提供您的新数据集。
警告
通过py:meth:'~pyglet.graphics.shader.UniformBlock.set_binding '指定自定义绑定点时,建议使用未指定的绑定点,因为可能会发生意外行为。如果发生此类碰撞,将输出警告。
最大绑定点值由硬件确定。这可以通过调用来检索 get_maximum_binding_count()
.建议使用高于应用程序中均匀块数量的数字以防止冲突。
备注
无法设置绑定点0,因为它在内部用于 WindowBlock
。
创建顶点列表
一旦有了ShaderProgram,就需要顶点数据来渲染。作为手动创建和管理折点缓冲区的一种更容易的替代方案,pyglet提供了一个高级别 VertexList
对象。顶点列表是OpenGL缓冲区上的抽象,具有用于轻松访问属性数据数组的属性。
ShaderProgram提供以下两种方法: vertex_list()
和 vertex_list_indexed()
至少,您必须提供 count 和 mode 创建顶点列表时。这个 count 只是要创建的顶点数。这个 mode 是OpenGL基元类型。一个 group
和 batch
参数也被接受(如下所述)。
模式应使用以下常量之一进行传递:
pyglet.gl.GL_POINTS
pyglet.gl.GL_LINES
pyglet.gl.GL_LINE_STRIP
pyglet.gl.GL_TRIANGLES
pyglet.gl.GL_TRIANGLE_STRIP
使用时 GL_LINE_STRIP
和 GL_TRIANGLE_STRIP
,必须注意在每个顶点列表的开始和结束处插入退化顶点。例如,给定顶点列表::
A, B, C, D
提供顶点列表的正确顶点列表为::
A, A, B, C, D, D
备注
由于高级API呈现具有共享状态的多个基元的方式, GL_POLYGON
, GL_LINE_LOOP
和 GL_TRIANGLE_FAN
无法使用-结果未定义。
创建具有三个顶点的顶点列表,不包含初始数据::
vlist = program.vertex_list(3, pyglet.gl.GL_TRIANGLES)
通过检查上面的ShaderProgram.Attributes,我们知道 position 和 colors 属性可用。可以直接访问底层数组::
>>> vlist.position
<pyglet.graphics.shader.c_float_Array_6 object at 0x7f6d3a30b1c0>
>>> vlist.colors
<pyglet.graphics.shader.c_float_Array_12 object at 0x7f6d3a30b0c0>
>>>
>>> vlist.position[:]
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
>>>
>>> vlist.colors[:]
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
这个 position 数据是一个具有6个元素的浮点数组。此属性是一个 vec2 在着色器中。查看上面的属性元数据,我们可以确认 count=2 。由于Vertex List是使用3个顶点创建的,因此数组的长度仅为3 * 2 = 6. Likewise, the colors attribute is defined as a vec4 in the shader, so it's simply 3 * 4=12。
此Vertex List是在没有任何初始数据的情况下创建的,但可以通过传递正确长度的列表或元组在属性上设置(或更新)它。例如::
vlist.position = (100, 300, 200, 250, 200, 350)
# or slightly faster to update in-place:
vlist.position[:] = (100, 300, 200, 250, 200, 350)
默认数据格式为单精度浮点数,但也可以使用“格式字符串”指定格式。它在创建时作为一个Python关键字参数传递。以下格式可用:
格式 |
类型 |
Python 类型 |
---|---|---|
|
有符号字节 |
集成 |
|
无符号字节 |
集成 |
|
签名短 |
集成 |
|
无符号短码 |
集成 |
|
带符号整型 |
集成 |
|
无符号整型 |
集成 |
|
单精度浮点 |
浮动 |
|
双精度浮点 |
浮动 |
例如,如果要将 position 数据作为带符号的整型,则可以将“i”格式的字符串作为Python关键字参数传递::
vlist = program.vertex_list(3, pyglet.gl.GL_TRIANGLES, position='i')
通过追加 "n"
对于格式字符串,您还可以指定应将传递的数据“规范化”到该范围 [0, 1]
。如果数据类型为浮点型,则按原样使用该值。如果数据类型为BYTE、SHORT或INT,则该值除以该类型可表示的最大值。例如,无符号字节除以255即可得到规格化值。
一种常见的情况是对颜色数据使用标准化的无符号字节。只需将“Bn”作为格式传递::
vlist = program.vertex_list(3, pyglet.gl.GL_TRIANGLES, colors='Bn')
正在传递初始数据
而不是设置数据 after 创建Vertex List时,您还可以在创建时轻松传递初始数据数组。这是通过将格式和数据作为元组传递来实现的,使用如上所述的关键字参数。要设置创建时的位置和颜色数据,请执行以下操作:
vlist = program.vertex_list(3, pyglet.gl.GL_TRIANGLES,
position=('f', (200, 400, 300, 350, 300, 450)),
colors=('Bn', (255, 0, 0, 255, 0, 255, 0, 255, 75, 75, 255, 255),)
索引渲染
通过使用索引渲染,还可以无序绘制顶点并多次绘制。这需要给出顶点数据索引的整数列表。您还可以使用 vertex_list_indexed()
方法而不是 vertex_list()
。除了所需的索引列表外,API几乎相同。
下面的示例创建四个顶点,并提供它们的位置数据。通过传递一个索引列表 [0, 1, 2, 0, 2, 3] ,我们创建两个相邻的三角形,共享的顶点将被重复使用:
vlist = program.vertex_list_indexed(4, pyglet.gl.GL_TRIANGLES,
[0, 1, 2, 0, 2, 3],
position=('i', (100, 100, 150, 100, 150, 150, 100, 150)),
)
请注意,第一个参数提供数据中的顶点数,而不是索引数(这隐含在第三个参数中给出的索引列表的长度上)。
资源管理
顶点列表引用存储在GPU上的数据,但它们本身并不拥有任何数据。因此,在创建Vertex List之后,并不一定要保留对它的引用。但是,如果您希望从GPU中删除数据,则只能使用 VertexList.delete() 方法。同样,如果保留了对顶点列表的引用,则只能更新该顶点的顶点数据。因此,应保留对创建后可能要修改或从场景中删除的任何对象的引用。
批处理渲染
为了获得最佳的OpenGL性能,您应该在一个 draw
打电话。在内部,pyglet使用 VertexDomain
和 IndexedVertexDomain
将共享相同属性格式的顶点列表保留在相邻的内存区域中。然后,可以一次绘制顶点列表的整个域,而无需调用 draw()
在每个单独的名单上。
然而,编写一个管理顶点域本身的应用程序是相当困难和乏味的。除了为每个ShaderProgram和一组属性格式维护顶点域之外,域还必须由基本体模式和所需的OpenGL状态分隔。
这个 Batch
类实现此功能,将相关顶点列表分组在一起,并自动按OpenGL状态排序。创建的批处理不带参数::
batch = pyglet.graphics.Batch()
要使用批处理,只需在创建任何pyglet的高级对象时将其作为(关键字)参数传递即可。例如::
vlist = program.vertex_list(3, pyglet.gl.GL_TRIANGLES, batch=batch)
sprite = pyglet.sprite.Sprite(img, x, y, batch=batch)
一次绘制批次中包含的所有对象的步骤:
batch.draw()
对于包含多个对象的批处理,这比单独绘制提供了显著的性能改进。通常建议始终使用批处理。
设置OpenGL状态
在OpenGL中绘制之前,需要设置一定的状态。您可能需要激活一个 ShaderProgram
,或绑定纹理。例如,要启用和绑定纹理,需要在绘制之前执行以下操作:
from pyglet.gl import *
glActiveTexture(GL_TEXTURE0)
glBindTexture(texture.target, texture.id)
使用一个 Group
可以封装这些状态更改并将其与它们影响的顶点列表相关联。子类 Group
并重写 Group.set_state 和 Group.unset_state 执行所需状态更改的方法:
class CustomGroup(pyglet.graphics.Group):
def __init__(self, texture, shaderprogram):
super().__init__()
self.texture = texture
self.program = shaderprogram
def set_state(self):
self.program.use()
glActiveTexture(GL_TEXTURE0)
glBindTexture(self.texture.target, self.texture.id)
def unset_state(self):
self.program.stop()
现在可以将此组的实例附加到顶点列表:
custom_group = CustomGroup()
vertex_list = program.vertex_list(2, pyglet.gl.GL_POINTS, batch, custom_group,
position=('i', (10, 15, 30, 35)),
colors=('Bn', (0, 0, 255, 0, 255, 0))
)
这个 Batch
确保适当的 set_state
和 unset_state
方法在使用它们的顶点列表之前和之后调用。
着色器状态
ShaderProgram
可以被绑定 (use()
)和未绑定 (stop()
)手动。作为一种方便的方法,它还可以充当上下文管理器,自动处理绑定和解除绑定过程。如果您想要确保 ShaderProgram
在一些边缘情况下是活跃的,同时也是更具毕脱式的。
例如::
with shaderprogram as my_shader:
my_shader.my_uniform = 1.0
等级状态
小组有一个 parent 属性,该属性允许将它们隐式组织在树结构中。If组 B 和 C 拥有父级 A ,然后按顺序 set_state
和 unset_state
对批量顶点列表的调用将是::
A.set_state()
B.set_state()
# Draw B vertices
B.unset_state()
C.set_state()
# Draw C vertices
C.unset_state()
A.unset_state()
这对于将状态更改分组到尽可能少的调用中很有用。例如,如果有许多顶点列表都需要启用纹理,但具有不同的绑定纹理,则可以在父组中启用和禁用纹理,并绑定子组中的每个纹理。下面的示例演示了这一点:
class TextureEnableGroup(pyglet.graphics.Group):
def set_state(self):
glActiveTexture(GL_TEXTURE0)
def unset_state(self):
# not necessary
texture_enable_group = TextureEnableGroup()
class TextureBindGroup(pyglet.graphics.Group):
def __init__(self, texture):
super().__init__(parent=texture_enable_group)
assert texture.target = GL_TEXTURE_2D
self.texture = texture
def set_state(self):
glBindTexture(GL_TEXTURE_2D, self.texture.id)
def unset_state(self):
# not required
def __eq__(self, other):
return (self.__class__ is other.__class__ and
self.texture.id == other.texture.id and
self.texture.target == other.texture.target and
self.parent == other.parent)
def __hash__(self):
return hash((self.texture.id, self.texture.target))
program.vertex_list_indexed(4, GL_TRIANGLES, indices, batch, TextureBindGroup(texture1))
program.vertex_list_indexed(4, GL_TRIANGLES, indices, batch, TextureBindGroup(texture2))
program.vertex_list_indexed(4, GL_TRIANGLES, indices, batch, TextureBindGroup(texture1))
备注
这个 __eq__
方法允许组中的 Batch
要自动合并两个完全相同的 TextureBindGroup
实例。为获得最佳性能,请始终注意确保您的自定义组具有正确的 __eq__
和 __hash__
定义的方法。
绘图顺序
VertexDomain
不尝试以任何特定顺序保留顶点列表。因此,任何共享相同基元模式、属性格式和组的顶点列表都将以任意顺序绘制。然而, Group
对象确实有一个 order
参数,它允许 Batch
若要对共享同一父对象的对象进行排序,请执行以下操作。总而言之,在批处理中:
组按其父级(如果有)排序。(也可以订购父组)。
组按其类别进行排序 order 属性。每个订单级别有一个抽签调用。
一种常用模式是为场景中的每个级别创建多个组。例如,在“前景”组之前绘制的“背景”组:
background = pyglet.graphics.Group(0)
foreground = pyglet.graphics.Group(1)
pyglet.sprite.Sprite(image, batch=batch, group=background)
pyglet.sprite.Sprite(image, batch=batch, group=foreground)
通过将分级组与有序组相结合,可以在单个 Batch
,这将使它尽可能地高效。
可见性
组有一个布尔值 visible
财产。通过将其设置为 False
,里面的任何物体 Group
将不再呈现。一个常见的用例是专门为此目的创建一个父组,通常与自定义订购结合(如上所述)。例如,您可能会创建一个“HUD”组,命令该组在其他所有内容之前绘制。然后可以轻松切换“HUD”集团的可见性。
其他模块中的批次和组
这个 Sprite
, Label
, TextLayout
和其他默认类都接受 batch
和 group
其构造函数中的参数。这允许您在呈现代码中的任意位置添加任何这些更高级别的pyglet可绘制对象。
例如,可以将多个精灵分成一个批次,然后一次绘制,而不是调用 Sprite.draw()
在每一个单独上:
batch = pyglet.graphics.Batch()
sprites = [pyglet.sprite.Sprite(image, batch=batch) for i in range(100)]
batch.draw()
这个 group
参数可用于设置单个批次内的绘制顺序(以及哪些对象与其他对象重叠),如上一页所述。
通常,您应该将所有绘图对象批处理为尽可能少的批次,并使用组来管理绘图顺序和其他OpenGL状态更改,以获得最佳性能。
如果您正在创建自己的可绘制类,请考虑添加 batch
和 group
参数以类似的方式。