着色器和渲染

在最底层,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 能够直接生成顶点列表。

总而言之,这一过程如下:

  1. 用户创建一个 ShaderProgram 。自动自省顶点属性元数据。

  2. 用户创建一个新的带有 vertex_list() 方法。用户不需要担心自己创建内部缓冲区。

  3. 在步骤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 mat4 projection;

    void main()
    {
        gl_Position = projection * 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;
    }
"""

然后使用源字符串创建 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 对创作的内在反思。可以查询几个属性来检查各种可用的顶点属性、制服和统一块。例如, uniformsattributes 属性将返回显示以下对象的元数据的字典:

>>> 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的 projectionview 矩阵都包含在 WindowBlock 统一挡块。在顶点着色器中如下所示::

uniform WindowBlock
{
    mat4 projection;
    mat4 view;
} window;

您可以查看存在于 ShaderProgram 使用 uniform_blocks 财产。这是一个包含统一块名键的字典 UniformBlock 对象值。在上面的示例中,名称将为 WindowBlock 当. window 标识符在GLSL着色器本身中使用。

要修改制服,请在 UniformBlock ,您必须首先创建一个 UniformBufferObject 使用 create_ubo() 方法。::

ubo = program.uniform_blocks['WindowBlock'].create_ubo()

这个 UniformBufferObject 然后可以使用,并充当上下文管理器,以便轻松访问其制服:

with ubo as window_block:
    window_block.projection[:] = new_matrix

创建顶点列表

一旦有了ShaderProgram,就需要顶点数据来渲染。作为手动创建和管理折点缓冲区的一种更容易的替代方案,pyglet提供了一个高级别 VertexList 对象。顶点列表是OpenGL缓冲区上的抽象,具有用于轻松访问属性数据数组的属性。

ShaderProgram提供以下两种方法: vertex_list()vertex_list_indexed()

至少,您必须提供 countmode 创建顶点列表时。这个 count 只是要创建的顶点数。这个 mode 是OpenGL基元类型。一个 groupbatch 参数也被接受(如下所述)。

模式应使用以下常量之一进行传递:

  • pyglet.gl.GL_POINTS

  • pyglet.gl.GL_LINES

  • pyglet.gl.GL_LINE_STRIP

  • pyglet.gl.GL_TRIANGLES

  • pyglet.gl.GL_TRIANGLE_STRIP

使用时 GL_LINE_STRIPGL_TRIANGLE_STRIP ,必须注意在每个顶点列表的开始和结束处插入退化顶点。例如,给定顶点列表::

A, B, C, D

提供顶点列表的正确顶点列表为::

A, A, B, C, D, D

备注

由于高级API呈现具有共享状态的多个基元的方式, GL_POLYGONGL_LINE_LOOPGL_TRIANGLE_FAN 无法使用-结果未定义。

创建具有三个顶点的顶点列表,不包含初始数据::

vlist = program.vertex_list(3, pyglet.gl.GL_TRIANGLES)

通过检查上面的ShaderProgram.Attributes,我们知道 positioncolors 属性可用。可以直接访问底层数组::

>>> 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 类型

"b"

有符号字节

集成

"B"

无符号字节

集成

"s"

签名短

集成

"S"

无符号短码

集成

"i"

带符号整型

集成

"I"

无符号整型

集成

"f"

单精度浮点

浮动

"d"

双精度浮点

浮动

例如,如果要将 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使用 VertexDomainIndexedVertexDomain 将共享相同属性格式的顶点列表保留在相邻的内存区域中。然后,可以一次绘制顶点列表的整个域,而无需调用 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_stateGroup.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_stateunset_state 方法在使用它们的顶点列表之前和之后调用。

着色器状态

ShaderProgram 可以被绑定 (use() )和未绑定 (stop() )手动。作为一种方便的方法,它还可以充当上下文管理器,自动处理绑定和解除绑定过程。如果您想要确保 ShaderProgram 在一些边缘情况下是活跃的,同时也是更具毕脱式的。

例如::

with shaderprogram as my_shader:
    my_shader.my_uniform = 1.0

等级状态

小组有一个 parent 属性,该属性允许将它们隐式组织在树结构中。If组 BC 拥有父级 A ,然后按顺序 set_stateunset_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 若要对共享同一父对象的对象进行排序,请执行以下操作。总而言之,在批处理中:

  1. 组按其父级(如果有)排序。(也可以订购父组)。

  2. 组按其类别进行排序 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 ,则不再渲染该组中的任何对象。一个常见的用例是创建专门用于此目的的父组,通常在与自定义排序结合使用时(如上所述)。例如,您可以创建一个“HUD”组,该组被命令绘制在所有其他对象的前面。然后,可以轻松切换“HUD”组的可见性。

其他模块中的批次和组

这个 SpriteLabelTextLayout 和其他默认类都接受 batchgroup 其构造函数中的参数。这允许您在呈现代码中的任意位置添加任何这些更高级别的pyglet可绘制对象。

例如,可以将多个精灵分成一个批次,然后一次绘制,而不是调用 Sprite.draw() 在每一个单独上:

batch = pyglet.graphics.Batch()
sprites = [pyglet.sprite.Sprite(image, batch=batch) for i in range(100)]

batch.draw()

这个 group 参数可用于设置单个批次内的绘制顺序(以及哪些对象与其他对象重叠),如上一页所述。

通常,您应该将所有绘图对象批处理为尽可能少的批次,并使用组来管理绘图顺序和其他OpenGL状态更改,以获得最佳性能。

如果您正在创建自己的可绘制类,请考虑添加 batchgroup 参数以类似的方式。