Batched rendering

For optimal OpenGL performance, you should render as many vertex lists as possible in a single draw call. Internally, pyglet uses VertexDomain and IndexedVertexDomain to keep vertex lists that share the same attribute formats in adjacent areas of memory. The entire domain of vertex lists can then be drawn at once, without calling VertexList.draw on each individual list.

It is quite difficult and tedious to write an application that manages vertex domains itself, though. In addition to maintaining a vertex domain for each set of attribute formats, domains must also be separated by primitive mode and required OpenGL state.

The Batch class implements this functionality, grouping related vertex lists together and sorting by OpenGL state automatically. A batch is created with no arguments:

batch = pyglet.graphics.Batch()

Vertex lists can now be created with the Batch.add and Batch.add_indexed methods instead of pyglet.graphics.vertex_list and pyglet.graphics.vertex_list_indexed functions. Unlike the module functions, these methods accept a mode parameter (the primitive mode) and a group parameter (described below).

The two coloured points from previous pages can be added to a batch as a single vertex list with:

vertex_list = batch.add(2, pyglet.gl.GL_POINTS, None,
    ('v2i', (10, 15, 30, 35)),
    ('c3B', (0, 0, 255, 0, 255, 0))
)

The resulting vertex_list can be modified as described in the previous section. However, instead of calling VertexList.draw to draw it, call Batch.draw to draw all vertex lists contained in the batch at once:

batch.draw()

For batches containing many vertex lists this gives a significant performance improvement over drawing individual vertex lists.

To remove a vertex list from a batch, call VertexList.delete.

Setting the OpenGL state

In order to achieve many effects in OpenGL one or more global state parameters must be set. For example, to enable and bind a texture requires:

from pyglet.gl import *
glEnable(texture.target)
glBindTexture(texture.target, texture.id)

before drawing vertex lists, and then:

glDisable(texture.target)

afterwards to avoid interfering with later drawing commands.

With a Group these state changes can be encapsulated and associated with the vertex lists they affect. Subclass Group and override the Group.set_state and Group.unset_state methods to perform the required state changes:

class CustomGroup(pyglet.graphics.Group):
    def set_state(self):
        glEnable(texture.target)
        glBindTexture(texture.target, texture.id)

    def unset_state(self):
        glDisable(texture.target)

An instance of this group can now be attached to vertex lists in the batch:

custom_group = CustomGroup()
vertex_list = batch.add(2, pyglet.gl.GL_POINTS, custom_group,
    ('v2i', (10, 15, 30, 35)),
    ('c3B', (0, 0, 255, 0, 255, 0))
)

The Batch ensures that the appropriate set_state and unset_state methods are called before and after the vertex lists that use them.

Hierarchical state

Groups have a parent attribute that allows them to be implicitly organised in a tree structure. If groups B and C have parent A, then the order of set_state and unset_state calls for vertex lists in a batch will be:

A.set_state()
# Draw A vertices
B.set_state()
# Draw B vertices
B.unset_state()
C.set_state()
# Draw C vertices
C.unset_state()
A.unset_state()

This is useful to group state changes into as few calls as possible. For example, if you have a number of vertex lists that all need texturing enabled, but have different bound textures, you could enable and disable texturing in the parent group and bind each texture in the child groups. The following example demonstrates this:

class TextureEnableGroup(pyglet.graphics.Group):
    def set_state(self):
        glEnable(GL_TEXTURE_2D)

    def unset_state(self):
        glDisable(GL_TEXTURE_2D)

texture_enable_group = TextureEnableGroup()

class TextureBindGroup(pyglet.graphics.Group):
    def __init__(self, texture):
        super(TextureBindGroup, self).__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)

    # No unset_state method required.

    def __eq__(self, other):
        return (self.__class__ is other.__class__ and
                self.texture == other.__class__)

batch.add(4, GL_QUADS, TextureBindGroup(texture1), 'v2f', 't2f')
batch.add(4, GL_QUADS, TextureBindGroup(texture2), 'v2f', 't2f')
batch.add(4, GL_QUADS, TextureBindGroup(texture1), 'v2f', 't2f')

Note the use of an __eq__ method on the group to allow Batch to merge the two TextureBindGroup identical instances.

Sorting vertex lists

VertexDomain does not attempt to keep vertex lists in any particular order. So, any vertex lists sharing the same primitive mode, attribute formats and group will be drawn in an arbitrary order. However, Batch will sort Group objects sharing the same parent by their __cmp__ method. This allows groups to be ordered.

The OrderedGroup class is a convenience group that does not set any OpenGL state, but is parameterised by an integer giving its draw order. In the following example a number of vertex lists are grouped into a "background" group that is drawn before the vertex lists in the "foreground" group:

background = pyglet.graphics.OrderedGroup(0)
foreground = pyglet.graphics.OrderedGroup(1)

batch.add(4, GL_QUADS, foreground, 'v2f')
batch.add(4, GL_QUADS, background, 'v2f')
batch.add(4, GL_QUADS, foreground, 'v2f')
batch.add(4, GL_QUADS, background, 'v2f', 'c4B')

By combining hierarchical groups with ordered groups it is possible to describe an entire scene within a single Batch, which then renders it as efficiently as possible.