2979 lines
102 KiB
Text
2979 lines
102 KiB
Text
# ----------------------------------------------------------------------------
|
|
# pyglet
|
|
# Copyright (c) 2006-2008 Alex Holkner
|
|
# All rights reserved.
|
|
#
|
|
# Redistribution and use in source and binary forms, with or without
|
|
# modification, are permitted provided that the following conditions
|
|
# are met:
|
|
#
|
|
# * Redistributions of source code must retain the above copyright
|
|
# notice, this list of conditions and the following disclaimer.
|
|
# * Redistributions in binary form must reproduce the above copyright
|
|
# notice, this list of conditions and the following disclaimer in
|
|
# the documentation and/or other materials provided with the
|
|
# distribution.
|
|
# * Neither the name of pyglet nor the names of its
|
|
# contributors may be used to endorse or promote products
|
|
# derived from this software without specific prior written
|
|
# permission.
|
|
#
|
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
|
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
|
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
|
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
|
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
|
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
|
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
# POSSIBILITY OF SUCH DAMAGE.
|
|
# ----------------------------------------------------------------------------
|
|
# $Id:$
|
|
|
|
'''Low-level graphics rendering.
|
|
|
|
This module provides an efficient low-level abstraction over OpenGL. It gives
|
|
very good performance for rendering OpenGL primitives; far better than the
|
|
typical immediate-mode usage and, on modern graphics cards, better than using
|
|
display lists in many cases. The module is used internally by other areas of
|
|
pyglet.
|
|
|
|
See the Programming Guide for details on how to use this graphics API.
|
|
|
|
Batches and groups
|
|
==================
|
|
|
|
Without even needing to understand the details on how to draw primitives with
|
|
the graphics API, developers can make use of `Batch` and `Group`
|
|
objects to improve performance of sprite and text rendering.
|
|
|
|
The `Sprite`, `Label` and `TextLayout` classes all accept a ``batch`` and
|
|
``group`` parameter in their constructors. A batch manages a set of objects
|
|
that will be drawn all at once, and a group describes the manner in which an
|
|
object is drawn.
|
|
|
|
The following example creates a batch, adds two sprites to the batch, and then
|
|
draws the entire batch::
|
|
|
|
batch = pyglet.graphics.Batch()
|
|
car = pyglet.sprite.Sprite(car_image, batch=batch)
|
|
boat = pyglet.sprite.Sprite(boat_image, batch=batch)
|
|
|
|
def on_draw()
|
|
batch.draw()
|
|
|
|
Drawing a complete batch is much faster than drawing the items in the batch
|
|
individually, especially when those items belong to a common group.
|
|
|
|
Groups describe the OpenGL state required for an item. This is for the most
|
|
part managed by the sprite and text classes, however you can also use groups
|
|
to ensure items are drawn in a particular order. For example, the following
|
|
example adds a background sprite which is guaranteed to be drawn before the
|
|
car and the boat::
|
|
|
|
batch = pyglet.graphics.Batch()
|
|
background = pyglet.graphics.OrderedGroup(0)
|
|
foreground = pyglet.graphics.OrderedGroup(1)
|
|
|
|
background = pyglet.sprite.Sprite(background_image,
|
|
batch=batch, group=background)
|
|
car = pyglet.sprite.Sprite(car_image, batch=batch, group=foreground)
|
|
boat = pyglet.sprite.Sprite(boat_image, batch=batch, group=foreground)
|
|
|
|
def on_draw()
|
|
batch.draw()
|
|
|
|
It's preferable to manage sprites and text objects within as few batches as
|
|
possible. If the drawing of sprites or text objects need to be interleaved
|
|
with other drawing that does not use the graphics API, multiple batches will
|
|
be required.
|
|
|
|
Data item parameters
|
|
====================
|
|
|
|
Many of the functions and methods in this module accept any number of ``data``
|
|
parameters as their final parameters. In the documentation these are notated
|
|
as ``*data`` in the formal parameter list.
|
|
|
|
A data parameter describes a vertex attribute format and an optional sequence
|
|
to initialise that attribute. Examples of common attribute formats are:
|
|
|
|
``"v3f"``
|
|
Vertex position, specified as three floats.
|
|
``"c4B"``
|
|
Vertex color, specifed as four unsigned bytes.
|
|
``"t2f"``
|
|
Texture coordinate, specified as two floats.
|
|
|
|
See `pyglet.graphics.vertexattribute` for the complete syntax of the vertex
|
|
format string.
|
|
|
|
When no initial data is to be given, the data item is just the format string.
|
|
For example, the following creates a 2 element vertex list with position and
|
|
color attributes::
|
|
|
|
vertex_list = pyglet.graphics.vertex_list(2, 'v2f', 'c4B')
|
|
|
|
When initial data is required, wrap the format string and the initial data in
|
|
a tuple, for example::
|
|
|
|
vertex_list = pyglet.graphics.vertex_list(2,
|
|
('v2f', (0.0, 1.0, 1.0, 0.0)),
|
|
('c4B', (255, 255, 255, 255) * 2))
|
|
|
|
Drawing modes
|
|
=============
|
|
|
|
Methods in this module that accept a ``mode`` parameter will accept any value
|
|
in the OpenGL drawing mode enumeration; for example, ``GL_POINTS``,
|
|
``GL_LINES``, ``GL_TRIANGLES``, etc.
|
|
|
|
Because of the way the graphics API renders multiple primitives with shared
|
|
state, ``GL_POLYGON``, ``GL_LINE_LOOP`` and ``GL_TRIANGLE_FAN`` cannot be used
|
|
--- the results are undefined.
|
|
|
|
When using ``GL_LINE_STRIP``, ``GL_TRIANGLE_STRIP`` or ``GL_QUAD_STRIP`` care
|
|
must be taken to insert degenrate vertices at the beginning and end of each
|
|
vertex list. For example, given the vertex list::
|
|
|
|
A, B, C, D
|
|
|
|
the correct vertex list to provide the vertex list is::
|
|
|
|
A, A, B, C, D, D
|
|
|
|
Alternatively, the ``NV_primitive_restart`` extension can be used if it is
|
|
present. This also permits use of ``GL_POLYGON``, ``GL_LINE_LOOP`` and
|
|
``GL_TRIANGLE_FAN``. Unfortunatley the extension is not provided by older
|
|
video drivers, and requires indexed vertex lists.
|
|
|
|
:since: pyglet 1.1
|
|
'''
|
|
|
|
__docformat__ = 'restructuredtext'
|
|
__version__ = '$Id: $'
|
|
|
|
import ctypes
|
|
|
|
import pyglet
|
|
from pyglet.gl import *
|
|
import ctypes
|
|
import re
|
|
|
|
_debug_graphics_batch = pyglet.options['debug_graphics_batch']
|
|
|
|
def draw(size, mode, *data):
|
|
'''Draw a primitive immediately.
|
|
|
|
:Parameters:
|
|
`size` : int
|
|
Number of vertices given
|
|
`mode` : int
|
|
OpenGL drawing mode, e.g. ``GL_TRIANGLES``
|
|
`data` : data items
|
|
Attribute formats and data. See the module summary for
|
|
details.
|
|
|
|
'''
|
|
glPushClientAttrib(GL_CLIENT_VERTEX_ARRAY_BIT)
|
|
|
|
buffers = []
|
|
for format, array in data:
|
|
attribute = vertexattribute.create_attribute(format)
|
|
assert size == len(array) // attribute.count, \
|
|
'Data for %s is incorrect length' % format
|
|
buffer = vertexbuffer.create_mappable_buffer(
|
|
size * attribute.stride, vbo=False)
|
|
|
|
attribute.set_region(buffer, 0, size, array)
|
|
attribute.enable()
|
|
attribute.set_pointer(buffer.ptr)
|
|
buffers.append(buffer)
|
|
|
|
glDrawArrays(mode, 0, size)
|
|
glFlush()
|
|
|
|
glPopClientAttrib()
|
|
|
|
def draw_indexed(size, mode, indices, *data):
|
|
'''Draw a primitive with indexed vertices immediately.
|
|
|
|
:Parameters:
|
|
`size` : int
|
|
Number of vertices given
|
|
`mode` : int
|
|
OpenGL drawing mode, e.g. ``GL_TRIANGLES``
|
|
`indices` : sequence of int
|
|
Sequence of integers giving indices into the vertex list.
|
|
`data` : data items
|
|
Attribute formats and data. See the module summary for details.
|
|
|
|
'''
|
|
glPushClientAttrib(GL_CLIENT_VERTEX_ARRAY_BIT)
|
|
|
|
buffers = []
|
|
for format, array in data:
|
|
attribute = vertexattribute.create_attribute(format)
|
|
assert size == len(array) // attribute.count, \
|
|
'Data for %s is incorrect length' % format
|
|
buffer = vertexbuffer.create_mappable_buffer(
|
|
size * attribute.stride, vbo=False)
|
|
|
|
attribute.set_region(buffer, 0, size, array)
|
|
attribute.enable()
|
|
attribute.set_pointer(buffer.ptr)
|
|
buffers.append(buffer)
|
|
|
|
if size <= 0xff:
|
|
index_type = GL_UNSIGNED_BYTE
|
|
index_c_type = ctypes.c_ubyte
|
|
elif size <= 0xffff:
|
|
index_type = GL_UNSIGNED_SHORT
|
|
index_c_type = ctypes.c_ushort
|
|
else:
|
|
index_type = GL_UNSIGNED_INT
|
|
index_c_type = ctypes.c_uint
|
|
|
|
index_array = (index_c_type * len(indices))(*indices)
|
|
glDrawElements(mode, len(indices), index_type, index_array)
|
|
glFlush()
|
|
|
|
glPopClientAttrib()
|
|
|
|
def _parse_data(data):
|
|
'''Given a list of data items, returns (formats, initial_arrays).'''
|
|
assert data, 'No attribute formats given'
|
|
|
|
# Return tuple (formats, initial_arrays).
|
|
formats = []
|
|
initial_arrays = []
|
|
for i, format in enumerate(data):
|
|
if isinstance(format, tuple):
|
|
format, array = format
|
|
initial_arrays.append((i, array))
|
|
formats.append(format)
|
|
formats = tuple(formats)
|
|
return formats, initial_arrays
|
|
|
|
def _get_default_batch():
|
|
shared_object_space = gl.current_context.object_space
|
|
try:
|
|
return shared_object_space.pyglet_graphics_default_batch
|
|
except AttributeError:
|
|
shared_object_space.pyglet_graphics_default_batch = Batch()
|
|
return shared_object_space.pyglet_graphics_default_batch
|
|
|
|
def vertex_list(count, *data):
|
|
'''Create a `VertexList` not associated with a batch, group or mode.
|
|
|
|
:Parameters:
|
|
`count` : int
|
|
The number of vertices in the list.
|
|
`data` : data items
|
|
Attribute formats and initial data for the vertex list. See the
|
|
module summary for details.
|
|
|
|
:rtype: `VertexList`
|
|
'''
|
|
# Note that mode=0 because the default batch is never drawn: vertex lists
|
|
# returned from this function are drawn directly by the app.
|
|
return _get_default_batch().add(count, 0, None, *data)
|
|
|
|
def vertex_list_indexed(count, indices, *data):
|
|
'''Create an `IndexedVertexList` not associated with a batch, group or mode.
|
|
|
|
:Parameters:
|
|
`count` : int
|
|
The number of vertices in the list.
|
|
`indices` : sequence
|
|
Sequence of integers giving indices into the vertex list.
|
|
`data` : data items
|
|
Attribute formats and initial data for the vertex list. See the
|
|
module summary for details.
|
|
|
|
:rtype: `IndexedVertexList`
|
|
'''
|
|
# Note that mode=0 because the default batch is never drawn: vertex lists
|
|
# returned from this function are drawn directly by the app.
|
|
return _get_default_batch().add_indexed(count, 0, None, indices, *data)
|
|
|
|
class Batch(object):
|
|
'''Manage a collection of vertex lists for batched rendering.
|
|
|
|
Vertex lists are added to a `Batch` using the `add` and `add_indexed`
|
|
methods. An optional group can be specified along with the vertex list,
|
|
which gives the OpenGL state required for its rendering. Vertex lists
|
|
with shared mode and group are allocated into adjacent areas of memory and
|
|
sent to the graphics card in a single operation.
|
|
|
|
Call `VertexList.delete` to remove a vertex list from the batch.
|
|
'''
|
|
def __init__(self):
|
|
'''Create a graphics batch.'''
|
|
# Mapping to find domain.
|
|
# group -> (attributes, mode, indexed) -> domain
|
|
self.group_map = {}
|
|
|
|
# Mapping of group to list of children.
|
|
self.group_children = {}
|
|
|
|
# List of top-level groups
|
|
self.top_groups = []
|
|
|
|
self._draw_list = []
|
|
self._draw_list_dirty = False
|
|
|
|
def add(self, count, mode, group, *data):
|
|
'''Add a vertex list to the batch.
|
|
|
|
:Parameters:
|
|
`count` : int
|
|
The number of vertices in the list.
|
|
`mode` : int
|
|
OpenGL drawing mode enumeration; for example, one of
|
|
``GL_POINTS``, ``GL_LINES``, ``GL_TRIANGLES``, etc.
|
|
See the module summary for additional information.
|
|
`group` : `Group`
|
|
Group of the vertex list, or ``None`` if no group is required.
|
|
`data` : data items
|
|
Attribute formats and initial data for the vertex list. See
|
|
the module summary for details.
|
|
|
|
:rtype: `VertexList`
|
|
'''
|
|
formats, initial_arrays = _parse_data(data)
|
|
domain = self._get_domain(False, mode, group, formats)
|
|
domain.__formats = formats
|
|
|
|
# Create vertex list and initialize
|
|
vlist = domain.create(count)
|
|
for i, array in initial_arrays:
|
|
vlist._set_attribute_data(i, array)
|
|
|
|
return vlist
|
|
|
|
def add_indexed(self, count, mode, group, indices, *data):
|
|
'''Add an indexed vertex list to the batch.
|
|
|
|
:Parameters:
|
|
`count` : int
|
|
The number of vertices in the list.
|
|
`mode` : int
|
|
OpenGL drawing mode enumeration; for example, one of
|
|
``GL_POINTS``, ``GL_LINES``, ``GL_TRIANGLES``, etc.
|
|
See the module summary for additional information.
|
|
`group` : `Group`
|
|
Group of the vertex list, or ``None`` if no group is required.
|
|
`indices` : sequence
|
|
Sequence of integers giving indices into the vertex list.
|
|
`data` : data items
|
|
Attribute formats and initial data for the vertex list. See
|
|
the module summary for details.
|
|
|
|
:rtype: `IndexedVertexList`
|
|
'''
|
|
formats, initial_arrays = _parse_data(data)
|
|
domain = self._get_domain(True, mode, group, formats)
|
|
|
|
# Create vertex list and initialize
|
|
vlist = domain.create(count, len(indices))
|
|
start = vlist.start
|
|
vlist._set_index_data(map(lambda i: i + start, indices))
|
|
for i, array in initial_arrays:
|
|
vlist._set_attribute_data(i, array)
|
|
|
|
return vlist
|
|
|
|
def migrate(self, vertex_list, mode, group, batch):
|
|
'''Migrate a vertex list to another batch and/or group.
|
|
|
|
`vertex_list` and `mode` together identify the vertex list to migrate.
|
|
`group` and `batch` are new owners of the vertex list after migration.
|
|
|
|
The results are undefined if `mode` is not correct or if `vertex_list`
|
|
does not belong to this batch (they are not checked and will not
|
|
necessarily throw an exception immediately).
|
|
|
|
`batch` can remain unchanged if only a group change is desired.
|
|
|
|
:Parameters:
|
|
`vertex_list` : `VertexList`
|
|
A vertex list currently belonging to this batch.
|
|
`mode` : int
|
|
The current GL drawing mode of the vertex list.
|
|
`group` : `Group`
|
|
The new group to migrate to.
|
|
`batch` : `Batch`
|
|
The batch to migrate to (or the current batch).
|
|
|
|
'''
|
|
formats = vertex_list.domain.__formats
|
|
domain = batch._get_domain(False, mode, group, formats)
|
|
vertex_list.migrate(domain)
|
|
|
|
def _get_domain(self, indexed, mode, group, formats):
|
|
if group is None:
|
|
group = null_group
|
|
|
|
# Batch group
|
|
if group not in self.group_map:
|
|
self._add_group(group)
|
|
|
|
domain_map = self.group_map[group]
|
|
|
|
# Find domain given formats, indices and mode
|
|
key = (formats, mode, indexed)
|
|
try:
|
|
domain = domain_map[key]
|
|
except KeyError:
|
|
# Create domain
|
|
if indexed:
|
|
domain = vertexdomain.create_indexed_domain(*formats)
|
|
else:
|
|
domain = create_domain(*formats)
|
|
domain_map[key] = domain
|
|
self._draw_list_dirty = True
|
|
|
|
return domain
|
|
|
|
def _add_group(self, group):
|
|
self.group_map[group] = {}
|
|
if group.parent is None:
|
|
self.top_groups.append(group)
|
|
else:
|
|
if group.parent not in self.group_map:
|
|
self._add_group(group.parent)
|
|
if group.parent not in self.group_children:
|
|
self.group_children[group.parent] = []
|
|
self.group_children[group.parent].append(group)
|
|
self._draw_list_dirty = True
|
|
|
|
def _update_draw_list(self):
|
|
'''Visit group tree in preorder and create a list of bound methods
|
|
to call.
|
|
'''
|
|
|
|
def visit(group):
|
|
draw_list = []
|
|
|
|
# Draw domains using this group
|
|
domain_map = self.group_map[group]
|
|
for (formats, mode, indexed), domain in list(domain_map.items()):
|
|
# Remove unused domains from batch
|
|
if domain._is_empty():
|
|
del domain_map[(formats, mode, indexed)]
|
|
continue
|
|
draw_list.append(
|
|
(lambda d, m: lambda: d.draw(m))(domain, mode))
|
|
|
|
# Sort and visit child groups of this group
|
|
children = self.group_children.get(group)
|
|
if children:
|
|
children.sort()
|
|
for child in list(children):
|
|
draw_list.extend(visit(child))
|
|
|
|
if children or domain_map:
|
|
return [group.set_state] + draw_list + [group.unset_state]
|
|
else:
|
|
# Remove unused group from batch
|
|
del self.group_map[group]
|
|
if group.parent:
|
|
self.group_children[group.parent].remove(group)
|
|
try:
|
|
del self.group_children[group]
|
|
except KeyError:
|
|
pass
|
|
try:
|
|
self.top_groups.remove(group)
|
|
except ValueError:
|
|
pass
|
|
return []
|
|
|
|
self._draw_list = []
|
|
|
|
self.top_groups.sort()
|
|
for group in list(self.top_groups):
|
|
self._draw_list.extend(visit(group))
|
|
|
|
self._draw_list_dirty = False
|
|
|
|
if _debug_graphics_batch:
|
|
self._dump_draw_list()
|
|
|
|
def _dump_draw_list(self):
|
|
def dump(group, indent=''):
|
|
print indent, 'Begin group', group
|
|
domain_map = self.group_map[group]
|
|
for _, domain in domain_map.items():
|
|
print indent, ' ', domain
|
|
for start, size in zip(*domain.allocator.get_allocated_regions()):
|
|
print indent, ' ', 'Region %d size %d:' % (start, size)
|
|
for key, attribute in domain.attribute_names.items():
|
|
print indent, ' ',
|
|
try:
|
|
region = attribute.get_region(attribute.buffer,
|
|
start, size)
|
|
print key, region.array[:]
|
|
except:
|
|
print key, '(unmappable)'
|
|
for child in self.group_children.get(group, ()):
|
|
dump(child, indent + ' ')
|
|
print indent, 'End group', group
|
|
|
|
print 'Draw list for %r:' % self
|
|
for group in self.top_groups:
|
|
dump(group)
|
|
|
|
def draw(self):
|
|
'''Draw the batch.
|
|
'''
|
|
if self._draw_list_dirty:
|
|
self._update_draw_list()
|
|
|
|
for func in self._draw_list:
|
|
func()
|
|
|
|
def draw_subset(self, vertex_lists):
|
|
'''Draw only some vertex lists in the batch.
|
|
|
|
The use of this method is highly discouraged, as it is quite
|
|
inefficient. Usually an application can be redesigned so that batches
|
|
can always be drawn in their entirety, using `draw`.
|
|
|
|
The given vertex lists must belong to this batch; behaviour is
|
|
undefined if this condition is not met.
|
|
|
|
:Parameters:
|
|
`vertex_lists` : sequence of `VertexList` or `IndexedVertexList`
|
|
Vertex lists to draw.
|
|
|
|
'''
|
|
# Horrendously inefficient.
|
|
def visit(group):
|
|
group.set_state()
|
|
|
|
# Draw domains using this group
|
|
domain_map = self.group_map[group]
|
|
for (_, mode, _), domain in domain_map.items():
|
|
for list in vertex_lists:
|
|
if list.domain is domain:
|
|
list.draw(mode)
|
|
|
|
# Sort and visit child groups of this group
|
|
children = self.group_children.get(group)
|
|
if children:
|
|
children.sort()
|
|
for child in children:
|
|
visit(child)
|
|
|
|
group.unset_state()
|
|
|
|
self.top_groups.sort()
|
|
for group in self.top_groups:
|
|
visit(group)
|
|
|
|
class Group(object):
|
|
'''Group of common OpenGL state.
|
|
|
|
Before a vertex list is rendered, its group's OpenGL state is set; as are
|
|
that state's ancestors' states. This can be defined arbitrarily on
|
|
subclasses; the default state change has no effect, and groups vertex
|
|
lists only in the order in which they are drawn.
|
|
'''
|
|
def __init__(self, parent=None):
|
|
'''Create a group.
|
|
|
|
:Parameters:
|
|
`parent` : `Group`
|
|
Group to contain this group; its state will be set before this
|
|
state's.
|
|
|
|
'''
|
|
self.parent = parent
|
|
|
|
def set_state(self):
|
|
'''Apply the OpenGL state change.
|
|
|
|
The default implementation does nothing.'''
|
|
pass
|
|
|
|
def unset_state(self):
|
|
'''Repeal the OpenGL state change.
|
|
|
|
The default implementation does nothing.'''
|
|
pass
|
|
|
|
def set_state_recursive(self):
|
|
'''Set this group and its ancestry.
|
|
|
|
Call this method if you are using a group in isolation: the
|
|
parent groups will be called in top-down order, with this class's
|
|
`set` being called last.
|
|
'''
|
|
if self.parent:
|
|
self.parent.set_state_recursive()
|
|
self.set_state()
|
|
|
|
def unset_state_recursive(self):
|
|
'''Unset this group and its ancestry.
|
|
|
|
The inverse of `set_state_recursive`.
|
|
'''
|
|
self.unset_state()
|
|
if self.parent:
|
|
self.parent.unset_state_recursive()
|
|
|
|
class NullGroup(Group):
|
|
'''The default group class used when ``None`` is given to a batch.
|
|
|
|
This implementation has no effect.
|
|
'''
|
|
pass
|
|
|
|
#: The default group.
|
|
#:
|
|
#: :type: `Group`
|
|
null_group = NullGroup()
|
|
|
|
class TextureGroup(Group):
|
|
'''A group that enables and binds a texture.
|
|
|
|
Texture groups are equal if their textures' targets and names are equal.
|
|
'''
|
|
# Don't use this, create your own group classes that are more specific.
|
|
# This is just an example.
|
|
def __init__(self, texture, parent=None):
|
|
'''Create a texture group.
|
|
|
|
:Parameters:
|
|
`texture` : `Texture`
|
|
Texture to bind.
|
|
`parent` : `Group`
|
|
Parent group.
|
|
|
|
'''
|
|
super(TextureGroup, self).__init__(parent)
|
|
self.texture = texture
|
|
|
|
def set_state(self):
|
|
glEnable(self.texture.target)
|
|
glBindTexture(self.texture.target, self.texture.id)
|
|
|
|
def unset_state(self):
|
|
glDisable(self.texture.target)
|
|
|
|
def __hash__(self):
|
|
return hash((self.texture.target, self.texture.id, self.parent))
|
|
|
|
def __eq__(self, other):
|
|
return (self.__class__ is other.__class__ and
|
|
self.texture.target == other.texture.target and
|
|
self.texture.id == other.texture.id and
|
|
self.parent == other.parent)
|
|
|
|
def __repr__(self):
|
|
return '%s(id=%d)' % (self.__class__.__name__, self.texture.id)
|
|
|
|
class OrderedGroup(Group):
|
|
'''A group with partial order.
|
|
|
|
Ordered groups with a common parent are rendered in ascending order of
|
|
their ``order`` field. This is a useful way to render multiple layers of
|
|
a scene within a single batch.
|
|
'''
|
|
# This can be useful as a top-level group, or as a superclass for other
|
|
# groups that need to be ordered.
|
|
#
|
|
# As a top-level group it's useful because graphics can be composited in a
|
|
# known order even if they don't know about each other or share any known
|
|
# group.
|
|
def __init__(self, order, parent=None):
|
|
'''Create an ordered group.
|
|
|
|
:Parameters:
|
|
`order` : int
|
|
Order of this group.
|
|
`parent` : `Group`
|
|
Parent of this group.
|
|
|
|
'''
|
|
super(OrderedGroup, self).__init__(parent)
|
|
self.order = order
|
|
|
|
def __cmp__(self, other):
|
|
if isinstance(other, OrderedGroup):
|
|
return cmp(self.order, other.order)
|
|
return -1
|
|
|
|
def __eq__(self, other):
|
|
return (self.__class__ is other.__class__ and
|
|
self.order == other.order and
|
|
self.parent == other.parent)
|
|
|
|
def __hash__(self):
|
|
return hash((self.order, self.parent))
|
|
|
|
def __repr__(self):
|
|
return '%s(%d)' % (self.__class__.__name__, self.order)
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# pyglet
|
|
# Copyright (c) 2006-2008 Alex Holkner
|
|
# All rights reserved.
|
|
#
|
|
# Redistribution and use in source and binary forms, with or without
|
|
# modification, are permitted provided that the following conditions
|
|
# are met:
|
|
#
|
|
# * Redistributions of source code must retain the above copyright
|
|
# notice, this list of conditions and the following disclaimer.
|
|
# * Redistributions in binary form must reproduce the above copyright
|
|
# notice, this list of conditions and the following disclaimer in
|
|
# the documentation and/or other materials provided with the
|
|
# distribution.
|
|
# * Neither the name of pyglet nor the names of its
|
|
# contributors may be used to endorse or promote products
|
|
# derived from this software without specific prior written
|
|
# permission.
|
|
#
|
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
|
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
|
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
|
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
|
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
|
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
|
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
# POSSIBILITY OF SUCH DAMAGE.
|
|
# ----------------------------------------------------------------------------
|
|
# $Id:$
|
|
|
|
'''Memory allocation algorithm for vertex arrays and buffers.
|
|
|
|
The region allocator is used to allocate vertex indices within a vertex
|
|
domain's multiple buffers. ("Buffer" refers to any abstract buffer presented
|
|
by `pyglet.graphics.vertexbuffer`.
|
|
|
|
The allocator will at times request more space from the buffers. The current
|
|
policy is to double the buffer size when there is not enough room to fulfil an
|
|
allocation. The buffer is never resized smaller.
|
|
|
|
The allocator maintains references to free space only; it is the caller's
|
|
responsibility to mantain the allocated regions.
|
|
'''
|
|
|
|
__docformat__ = 'restructuredtext'
|
|
__version__ = '$Id: $'
|
|
|
|
# Common cases:
|
|
# -regions will be the same size (instances of same object, e.g. sprites)
|
|
# -regions will not usually be resized (only exception is text)
|
|
# -alignment of 4 vertices (glyphs, sprites, images, ...)
|
|
#
|
|
# Optimise for:
|
|
# -keeping regions adjacent, reduce the number of entries in glMultiDrawArrays
|
|
# -finding large blocks of allocated regions quickly (for drawing)
|
|
# -finding block of unallocated space is the _uncommon_ case!
|
|
#
|
|
# Decisions:
|
|
# -don't over-allocate regions to any alignment -- this would require more
|
|
# work in finding the allocated spaces (for drawing) and would result in
|
|
# more entries in glMultiDrawArrays
|
|
# -don't move blocks when they truncate themselves. try not to allocate the
|
|
# space they freed too soon (they will likely need grow back into it later,
|
|
# and growing will usually require a reallocation).
|
|
# -allocator does not track individual allocated regions. Trusts caller
|
|
# to provide accurate (start, size) tuple, which completely describes
|
|
# a region from the allocator's point of view.
|
|
# -this means that compacting is probably not feasible, or would be hideously
|
|
# expensive
|
|
|
|
class AllocatorMemoryException(Exception):
|
|
'''The buffer is not large enough to fulfil an allocation.
|
|
|
|
Raised by `Allocator` methods when the operation failed due to lack of
|
|
buffer space. The buffer should be increased to at least
|
|
requested_capacity and then the operation retried (guaranteed to
|
|
pass second time).
|
|
'''
|
|
|
|
def __init__(self, requested_capacity):
|
|
self.requested_capacity = requested_capacity
|
|
|
|
class Allocator(object):
|
|
'''Buffer space allocation implementation.'''
|
|
def __init__(self, capacity):
|
|
'''Create an allocator for a buffer of the specified capacity.
|
|
|
|
:Parameters:
|
|
`capacity` : int
|
|
Maximum size of the buffer.
|
|
|
|
'''
|
|
self.capacity = capacity
|
|
|
|
# Allocated blocks. Start index and size in parallel lists.
|
|
#
|
|
# # = allocated, - = free
|
|
#
|
|
# 0 3 5 15 20 24 40
|
|
# |###--##########-----####----------------------|
|
|
#
|
|
# starts = [0, 5, 20]
|
|
# sizes = [3, 10, 4]
|
|
#
|
|
# To calculate free blocks:
|
|
# for i in range(0, len(starts)):
|
|
# free_start[i] = starts[i] + sizes[i]
|
|
# free_size[i] = starts[i+1] - free_start[i]
|
|
# free_size[i+1] = self.capacity - free_start[-1]
|
|
|
|
self.starts = []
|
|
self.sizes = []
|
|
|
|
def set_capacity(self, size):
|
|
'''Resize the maximum buffer size.
|
|
|
|
The capaity cannot be reduced.
|
|
|
|
:Parameters:
|
|
`size` : int
|
|
New maximum size of the buffer.
|
|
|
|
'''
|
|
assert size > self.capacity
|
|
self.capacity = size
|
|
|
|
def alloc(self, size):
|
|
'''Allocate memory in the buffer.
|
|
|
|
Raises `AllocatorMemoryException` if the allocation cannot be
|
|
fulfilled.
|
|
|
|
:Parameters:
|
|
`size` : int
|
|
Size of region to allocate.
|
|
|
|
:rtype: int
|
|
:return: Starting index of the allocated region.
|
|
'''
|
|
assert size > 0
|
|
|
|
# return start
|
|
# or raise AllocatorMemoryException
|
|
|
|
if not self.starts:
|
|
if size <= self.capacity:
|
|
self.starts.append(0)
|
|
self.sizes.append(size)
|
|
return 0
|
|
else:
|
|
raise AllocatorMemoryException(size)
|
|
|
|
# Allocate in a free space
|
|
free_start = self.starts[0] + self.sizes[0]
|
|
for i, (alloc_start, alloc_size) in \
|
|
enumerate(zip(self.starts[1:], self.sizes[1:])):
|
|
# Danger!
|
|
# i is actually index - 1 because of slicing above...
|
|
# starts[i] points to the block before this free space
|
|
# starts[i+1] points to the block after this free space, and is
|
|
# always valid.
|
|
free_size = alloc_start - free_start
|
|
if free_size == size:
|
|
# Merge previous block with this one (removing this free space)
|
|
self.sizes[i] += free_size + alloc_size
|
|
del self.starts[i+1]
|
|
del self.sizes[i+1]
|
|
return free_start
|
|
elif free_size > size:
|
|
# Increase size of previous block to intrude into this free
|
|
# space.
|
|
self.sizes[i] += size
|
|
return free_start
|
|
free_start = alloc_start + alloc_size
|
|
|
|
# Allocate at end of capacity
|
|
free_size = self.capacity - free_start
|
|
if free_size >= size:
|
|
self.sizes[-1] += size
|
|
return free_start
|
|
|
|
raise AllocatorMemoryException(self.capacity + size - free_size)
|
|
|
|
def realloc(self, start, size, new_size):
|
|
'''Reallocate a region of the buffer.
|
|
|
|
This is more efficient than separate `dealloc` and `alloc` calls, as
|
|
the region can often be resized in-place.
|
|
|
|
Raises `AllocatorMemoryException` if the allocation cannot be
|
|
fulfilled.
|
|
|
|
:Parameters:
|
|
`start` : int
|
|
Current starting index of the region.
|
|
`size` : int
|
|
Current size of the region.
|
|
`new_size` : int
|
|
New size of the region.
|
|
|
|
'''
|
|
assert size > 0 and new_size > 0
|
|
|
|
# return start
|
|
# or raise AllocatorMemoryException
|
|
|
|
# Truncation is the same as deallocating the tail cruft
|
|
if new_size < size:
|
|
self.dealloc(start + new_size, size - new_size)
|
|
return start
|
|
|
|
# Find which block it lives in
|
|
for i, (alloc_start, alloc_size) in \
|
|
enumerate(zip(*(self.starts, self.sizes))):
|
|
p = start - alloc_start
|
|
if p >= 0 and size <= alloc_size - p:
|
|
break
|
|
if not (p >= 0 and size <= alloc_size - p):
|
|
print zip(self.starts, self.sizes)
|
|
print start, size, new_size
|
|
print p, alloc_start, alloc_size
|
|
assert p >= 0 and size <= alloc_size - p, 'Region not allocated'
|
|
|
|
if size == alloc_size - p:
|
|
# Region is at end of block. Find how much free space is after
|
|
# it.
|
|
is_final_block = i == len(self.starts) - 1
|
|
if not is_final_block:
|
|
free_size = self.starts[i + 1] - (start + size)
|
|
else:
|
|
free_size = self.capacity - (start + size)
|
|
|
|
# TODO If region is an entire block being an island in free space,
|
|
# can possibly extend in both directions.
|
|
|
|
if free_size == new_size - size and not is_final_block:
|
|
# Merge block with next (region is expanded in place to
|
|
# exactly fill the free space)
|
|
self.sizes[i] += free_size + self.sizes[i + 1]
|
|
del self.starts[i + 1]
|
|
del self.sizes[i + 1]
|
|
return start
|
|
elif free_size > new_size - size:
|
|
# Expand region in place
|
|
self.sizes[i] += new_size - size
|
|
return start
|
|
|
|
# The block must be repositioned. Dealloc then alloc.
|
|
|
|
# But don't do this! If alloc fails, we've already silently dealloc'd
|
|
# the original block.
|
|
# self.dealloc(start, size)
|
|
# return self.alloc(new_size)
|
|
|
|
# It must be alloc'd first. We're not missing an optimisation
|
|
# here, because if freeing the block would've allowed for the block to
|
|
# be placed in the resulting free space, one of the above in-place
|
|
# checks would've found it.
|
|
result = self.alloc(new_size)
|
|
self.dealloc(start, size)
|
|
return result
|
|
|
|
def dealloc(self, start, size):
|
|
'''Free a region of the buffer.
|
|
|
|
:Parameters:
|
|
`start` : int
|
|
Starting index of the region.
|
|
`size` : int
|
|
Size of the region.
|
|
|
|
'''
|
|
assert size > 0
|
|
assert self.starts
|
|
|
|
# Find which block needs to be split
|
|
for i, (alloc_start, alloc_size) in \
|
|
enumerate(zip(*(self.starts, self.sizes))):
|
|
p = start - alloc_start
|
|
if p >= 0 and size <= alloc_size - p:
|
|
break
|
|
|
|
# Assert we left via the break
|
|
assert p >= 0 and size <= alloc_size - p, 'Region not allocated'
|
|
|
|
if p == 0 and size == alloc_size:
|
|
# Remove entire block
|
|
del self.starts[i]
|
|
del self.sizes[i]
|
|
elif p == 0:
|
|
# Truncate beginning of block
|
|
self.starts[i] += size
|
|
self.sizes[i] -= size
|
|
elif size == alloc_size - p:
|
|
# Truncate end of block
|
|
self.sizes[i] -= size
|
|
else:
|
|
# Reduce size of left side, insert block at right side
|
|
# $ = dealloc'd block, # = alloc'd region from same block
|
|
#
|
|
# <------8------>
|
|
# <-5-><-6-><-7->
|
|
# 1 2 3 4
|
|
# #####$$$$$#####
|
|
#
|
|
# 1 = alloc_start
|
|
# 2 = start
|
|
# 3 = start + size
|
|
# 4 = alloc_start + alloc_size
|
|
# 5 = start - alloc_start = p
|
|
# 6 = size
|
|
# 7 = {8} - ({5} + {6}) = alloc_size - (p + size)
|
|
# 8 = alloc_size
|
|
#
|
|
self.sizes[i] = p
|
|
self.starts.insert(i + 1, start + size)
|
|
self.sizes.insert(i + 1, alloc_size - (p + size))
|
|
|
|
def get_allocated_regions(self):
|
|
'''Get a list of (aggregate) allocated regions.
|
|
|
|
The result of this method is ``(starts, sizes)``, where ``starts`` is
|
|
a list of starting indices of the regions and ``sizes`` their
|
|
corresponding lengths.
|
|
|
|
:rtype: (list, list)
|
|
'''
|
|
# return (starts, sizes); len(starts) == len(sizes)
|
|
return (self.starts, self.sizes)
|
|
|
|
def get_fragmented_free_size(self):
|
|
'''Returns the amount of space unused, not including the final
|
|
free block.
|
|
|
|
:rtype: int
|
|
'''
|
|
if not self.starts:
|
|
return 0
|
|
|
|
# Variation of search for free block.
|
|
total_free = 0
|
|
free_start = self.starts[0] + self.sizes[0]
|
|
for i, (alloc_start, alloc_size) in \
|
|
enumerate(zip(self.starts[1:], self.sizes[1:])):
|
|
total_free += alloc_start - free_start
|
|
free_start = alloc_start + alloc_size
|
|
|
|
return total_free
|
|
|
|
def get_free_size(self):
|
|
'''Return the amount of space unused.
|
|
|
|
:rtype: int
|
|
'''
|
|
if not self.starts:
|
|
return self.capacity
|
|
|
|
free_end = self.capacity - (self.starts[-1] + self.sizes[-1])
|
|
return self.get_fragmented_free_size() + free_end
|
|
|
|
def get_usage(self):
|
|
'''Return fraction of capacity currently allocated.
|
|
|
|
:rtype: float
|
|
'''
|
|
return 1. - self.get_free_size() / float(self.capacity)
|
|
|
|
def get_fragmentation(self):
|
|
'''Return fraction of free space that is not expandable.
|
|
|
|
:rtype: float
|
|
'''
|
|
free_size = self.get_free_size()
|
|
if free_size == 0:
|
|
return 0.
|
|
return self.get_fragmented_free_size() / float(self.get_free_size())
|
|
|
|
def _is_empty(self):
|
|
return not self.starts
|
|
|
|
def __str__(self):
|
|
return 'allocs=' + repr(zip(self.starts, self.sizes))
|
|
|
|
def __repr__(self):
|
|
return '<%s %s>' % (self.__class__.__name__, str(self))
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# pyglet
|
|
# Copyright (c) 2006-2008 Alex Holkner
|
|
# All rights reserved.
|
|
#
|
|
# Redistribution and use in source and binary forms, with or without
|
|
# modification, are permitted provided that the following conditions
|
|
# are met:
|
|
#
|
|
# * Redistributions of source code must retain the above copyright
|
|
# notice, this list of conditions and the following disclaimer.
|
|
# * Redistributions in binary form must reproduce the above copyright
|
|
# notice, this list of conditions and the following disclaimer in
|
|
# the documentation and/or other materials provided with the
|
|
# distribution.
|
|
# * Neither the name of pyglet nor the names of its
|
|
# contributors may be used to endorse or promote products
|
|
# derived from this software without specific prior written
|
|
# permission.
|
|
#
|
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
|
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
|
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
|
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
|
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
|
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
|
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
# POSSIBILITY OF SUCH DAMAGE.
|
|
# ----------------------------------------------------------------------------
|
|
# $Id:$
|
|
|
|
'''Access byte arrays as arrays of vertex attributes.
|
|
|
|
Use `create_attribute` to create an attribute accessor given a simple format
|
|
string. Alternatively, the classes may be constructed directly.
|
|
|
|
Attribute format strings
|
|
========================
|
|
|
|
An attribute format string specifies the format of a vertex attribute. Format
|
|
strings are accepted by the `create_attribute` function as well as most
|
|
methods in the `pyglet.graphics` module.
|
|
|
|
Format strings have the following (BNF) syntax::
|
|
|
|
attribute ::= ( name | index 'g' 'n'? ) count type
|
|
|
|
``name`` describes the vertex attribute, and is one of the following
|
|
constants for the predefined attributes:
|
|
|
|
``c``
|
|
Vertex color
|
|
``e``
|
|
Edge flag
|
|
``f``
|
|
Fog coordinate
|
|
``n``
|
|
Normal vector
|
|
``s``
|
|
Secondary color
|
|
``t``
|
|
Texture coordinate
|
|
``v``
|
|
Vertex coordinate
|
|
|
|
You can alternatively create a generic indexed vertex attribute by
|
|
specifying its index in decimal followed by the constant ``g``. For
|
|
example, ``0g`` specifies the generic vertex attribute with index 0.
|
|
If the optional constant ``n`` is present after the ``g``, the
|
|
attribute is normalised to the range ``[0, 1]`` or ``[-1, 1]`` within
|
|
the range of the data type.
|
|
|
|
``count`` gives the number of data components in the attribute. For
|
|
example, a 3D vertex position has a count of 3. Some attributes
|
|
constrain the possible counts that can be used; for example, a normal
|
|
vector must have a count of 3.
|
|
|
|
``type`` gives the data type of each component of the attribute. The
|
|
following types can be used:
|
|
|
|
``b``
|
|
``GLbyte``
|
|
``B``
|
|
``GLubyte``
|
|
``s``
|
|
``GLshort``
|
|
``S``
|
|
``GLushort``
|
|
``i``
|
|
``GLint``
|
|
``I``
|
|
``GLuint``
|
|
``f``
|
|
``GLfloat``
|
|
``d``
|
|
``GLdouble``
|
|
|
|
Some attributes constrain the possible data types; for example,
|
|
normal vectors must use one of the signed data types. The use of
|
|
some data types, while not illegal, may have severe performance
|
|
concerns. For example, the use of ``GLdouble`` is discouraged,
|
|
and colours should be specified with ``GLubyte``.
|
|
|
|
Whitespace is prohibited within the format string.
|
|
|
|
Some examples follow:
|
|
|
|
``v3f``
|
|
3-float vertex position
|
|
``c4b``
|
|
4-byte colour
|
|
``1eb``
|
|
Edge flag
|
|
``0g3f``
|
|
3-float generic vertex attribute 0
|
|
``1gn1i``
|
|
Integer generic vertex attribute 1, normalized to [-1, 1]
|
|
``2gn4B``
|
|
4-byte generic vertex attribute 2, normalized to [0, 1] (because
|
|
the type is unsigned)
|
|
|
|
'''
|
|
|
|
__docformat__ = 'restructuredtext'
|
|
__version__ = '$Id: $'
|
|
|
|
_c_types = {
|
|
GL_BYTE: ctypes.c_byte,
|
|
GL_UNSIGNED_BYTE: ctypes.c_ubyte,
|
|
GL_SHORT: ctypes.c_short,
|
|
GL_UNSIGNED_SHORT: ctypes.c_ushort,
|
|
GL_INT: ctypes.c_int,
|
|
GL_UNSIGNED_INT: ctypes.c_uint,
|
|
GL_FLOAT: ctypes.c_float,
|
|
GL_DOUBLE: ctypes.c_double,
|
|
}
|
|
|
|
_gl_types = {
|
|
'b': GL_BYTE,
|
|
'B': GL_UNSIGNED_BYTE,
|
|
's': GL_SHORT,
|
|
'S': GL_UNSIGNED_SHORT,
|
|
'i': GL_INT,
|
|
'I': GL_UNSIGNED_INT,
|
|
'f': GL_FLOAT,
|
|
'd': GL_DOUBLE,
|
|
}
|
|
|
|
_attribute_format_re = re.compile(r'''
|
|
(?P<name>
|
|
[cefnstv] |
|
|
(?P<generic_index>[0-9]+) g
|
|
(?P<generic_normalized>n?))
|
|
(?P<count>[1234])
|
|
(?P<type>[bBsSiIfd])
|
|
''', re.VERBOSE)
|
|
|
|
_attribute_cache = {}
|
|
|
|
def _align(v, align):
|
|
return ((v - 1) & ~(align - 1)) + align
|
|
|
|
def interleave_attributes(attributes):
|
|
'''Interleave attribute offsets.
|
|
|
|
Adjusts the offsets and strides of the given attributes so that
|
|
they are interleaved. Alignment constraints are respected.
|
|
|
|
:Parameters:
|
|
`attributes` : sequence of `AbstractAttribute`
|
|
Attributes to interleave in-place.
|
|
|
|
'''
|
|
stride = 0
|
|
max_size = 0
|
|
for attribute in attributes:
|
|
stride = _align(stride, attribute.align)
|
|
attribute.offset = stride
|
|
stride += attribute.size
|
|
max_size = max(max_size, attribute.size)
|
|
stride = _align(stride, max_size)
|
|
for attribute in attributes:
|
|
attribute.stride = stride
|
|
|
|
def serialize_attributes(count, attributes):
|
|
'''Serialize attribute offsets.
|
|
|
|
Adjust the offsets of the given attributes so that they are
|
|
packed serially against each other for `count` vertices.
|
|
|
|
:Parameters:
|
|
`count` : int
|
|
Number of vertices.
|
|
`attributes` : sequence of `AbstractAttribute`
|
|
Attributes to serialze in-place.
|
|
|
|
'''
|
|
offset = 0
|
|
for attribute in attributes:
|
|
offset = _align(offset, attribute.align)
|
|
attribute.offset = offset
|
|
offset += count * attribute.stride
|
|
|
|
def create_attribute(format):
|
|
'''Create a vertex attribute description from a format string.
|
|
|
|
The initial stride and offset of the attribute will be 0.
|
|
|
|
:Parameters:
|
|
`format` : str
|
|
Attribute format string. See the module summary for details.
|
|
|
|
:rtype: `AbstractAttribute`
|
|
'''
|
|
try:
|
|
cls, args = _attribute_cache[format]
|
|
return cls(*args)
|
|
except KeyError:
|
|
pass
|
|
|
|
match = _attribute_format_re.match(format)
|
|
assert match, 'Invalid attribute format %r' % format
|
|
count = int(match.group('count'))
|
|
gl_type = _gl_types[match.group('type')]
|
|
generic_index = match.group('generic_index')
|
|
if generic_index:
|
|
normalized = match.group('generic_normalized')
|
|
attr_class = GenericAttribute
|
|
args = int(generic_index), normalized, count, gl_type
|
|
else:
|
|
name = match.group('name')
|
|
attr_class = _attribute_classes[name]
|
|
if attr_class._fixed_count:
|
|
assert count == attr_class._fixed_count, \
|
|
'Attributes named "%s" must have count of %d' % (
|
|
name, attr_class._fixed_count)
|
|
args = (gl_type,)
|
|
else:
|
|
args = (count, gl_type)
|
|
|
|
_attribute_cache[format] = attr_class, args
|
|
return attr_class(*args)
|
|
|
|
class AbstractAttribute(object):
|
|
'''Abstract accessor for an attribute in a mapped buffer.
|
|
'''
|
|
|
|
_fixed_count = None
|
|
|
|
def __init__(self, count, gl_type):
|
|
'''Create the attribute accessor.
|
|
|
|
:Parameters:
|
|
`count` : int
|
|
Number of components in the attribute.
|
|
`gl_type` : int
|
|
OpenGL type enumerant; for example, ``GL_FLOAT``
|
|
|
|
'''
|
|
assert count in (1, 2, 3, 4), 'Component count out of range'
|
|
self.gl_type = gl_type
|
|
self.c_type = _c_types[gl_type]
|
|
self.count = count
|
|
self.align = ctypes.sizeof(self.c_type)
|
|
self.size = count * self.align
|
|
self.stride = self.size
|
|
self.offset = 0
|
|
|
|
def enable(self):
|
|
'''Enable the attribute using ``glEnableClientState``.'''
|
|
raise NotImplementedError('abstract')
|
|
|
|
def set_pointer(self, offset):
|
|
'''Setup this attribute to point to the currently bound buffer at
|
|
the given offset.
|
|
|
|
``offset`` should be based on the currently bound buffer's ``ptr``
|
|
member.
|
|
|
|
:Parameters:
|
|
`offset` : int
|
|
Pointer offset to the currently bound buffer for this
|
|
attribute.
|
|
|
|
'''
|
|
raise NotImplementedError('abstract')
|
|
|
|
def get_region(self, buffer, start, count):
|
|
'''Map a buffer region using this attribute as an accessor.
|
|
|
|
The returned region can be modified as if the buffer was a contiguous
|
|
array of this attribute (though it may actually be interleaved or
|
|
otherwise non-contiguous).
|
|
|
|
The returned region consists of a contiguous array of component
|
|
data elements. For example, if this attribute uses 3 floats per
|
|
vertex, and the `count` parameter is 4, the number of floats mapped
|
|
will be ``3 * 4 = 12``.
|
|
|
|
:Parameters:
|
|
`buffer` : `AbstractMappable`
|
|
The buffer to map.
|
|
`start` : int
|
|
Offset of the first vertex to map.
|
|
`count` : int
|
|
Number of vertices to map
|
|
|
|
:rtype: `AbstractBufferRegion`
|
|
'''
|
|
byte_start = self.stride * start
|
|
byte_size = self.stride * count
|
|
array_count = self.count * count
|
|
if self.stride == self.size:
|
|
# non-interleaved
|
|
ptr_type = ctypes.POINTER(self.c_type * array_count)
|
|
return buffer.get_region(byte_start, byte_size, ptr_type)
|
|
else:
|
|
# interleaved
|
|
byte_start += self.offset
|
|
byte_size -= self.offset
|
|
elem_stride = self.stride // ctypes.sizeof(self.c_type)
|
|
elem_offset = self.offset // ctypes.sizeof(self.c_type)
|
|
ptr_type = ctypes.POINTER(
|
|
self.c_type * (count * elem_stride - elem_offset))
|
|
region = buffer.get_region(byte_start, byte_size, ptr_type)
|
|
return vertexbuffer.IndirectArrayRegion(
|
|
region, array_count, self.count, elem_stride)
|
|
|
|
def set_region(self, buffer, start, count, data):
|
|
'''Set the data over a region of the buffer.
|
|
|
|
:Parameters:
|
|
`buffer` : AbstractMappable`
|
|
The buffer to modify.
|
|
`start` : int
|
|
Offset of the first vertex to set.
|
|
`count` : int
|
|
Number of vertices to set.
|
|
`data` : sequence
|
|
Sequence of data components.
|
|
|
|
'''
|
|
if self.stride == self.size:
|
|
# non-interleaved
|
|
byte_start = self.stride * start
|
|
byte_size = self.stride * count
|
|
array_count = self.count * count
|
|
data = (self.c_type * array_count)(*data)
|
|
buffer.set_data_region(data, byte_start, byte_size)
|
|
else:
|
|
# interleaved
|
|
region = self.get_region(buffer, start, count)
|
|
region[:] = data
|
|
|
|
class ColorAttribute(AbstractAttribute):
|
|
'''Color vertex attribute.'''
|
|
|
|
plural = 'colors'
|
|
|
|
def __init__(self, count, gl_type):
|
|
assert count in (3, 4), 'Color attributes must have count of 3 or 4'
|
|
super(ColorAttribute, self).__init__(count, gl_type)
|
|
|
|
def enable(self):
|
|
glEnableClientState(GL_COLOR_ARRAY)
|
|
|
|
def set_pointer(self, pointer):
|
|
glColorPointer(self.count, self.gl_type, self.stride,
|
|
self.offset + pointer)
|
|
|
|
class EdgeFlagAttribute(AbstractAttribute):
|
|
'''Edge flag attribute.'''
|
|
|
|
plural = 'edge_flags'
|
|
_fixed_count = 1
|
|
|
|
def __init__(self, gl_type):
|
|
assert gl_type in (GL_BYTE, GL_UNSIGNED_BYTE, GL_BOOL), \
|
|
'Edge flag attribute must have boolean type'
|
|
super(EdgeFlagAttribute, self).__init__(1, gl_type)
|
|
|
|
def enable(self):
|
|
glEnableClientState(GL_EDGE_FLAG_ARRAY)
|
|
|
|
def set_pointer(self, pointer):
|
|
glEdgeFlagPointer(self.stride, self.offset + pointer)
|
|
|
|
class FogCoordAttribute(AbstractAttribute):
|
|
'''Fog coordinate attribute.'''
|
|
|
|
plural = 'fog_coords'
|
|
|
|
def __init__(self, count, gl_type):
|
|
super(FogCoordAttribute, self).__init__(count, gl_type)
|
|
|
|
def enable(self):
|
|
glEnableClientState(GL_FOG_COORD_ARRAY)
|
|
|
|
def set_pointer(self, pointer):
|
|
glFogCoordPointer(self.count, self.gl_type, self.stride,
|
|
self.offset + pointer)
|
|
|
|
class NormalAttribute(AbstractAttribute):
|
|
'''Normal vector attribute.'''
|
|
|
|
plural = 'normals'
|
|
_fixed_count = 3
|
|
|
|
def __init__(self, gl_type):
|
|
assert gl_type in (GL_BYTE, GL_SHORT, GL_INT, GL_FLOAT, GL_DOUBLE), \
|
|
'Normal attribute must have signed type'
|
|
super(NormalAttribute, self).__init__(3, gl_type)
|
|
|
|
def enable(self):
|
|
glEnableClientState(GL_NORMAL_ARRAY)
|
|
|
|
def set_pointer(self, pointer):
|
|
glNormalPointer(self.gl_type, self.stride, self.offset + pointer)
|
|
|
|
class SecondaryColorAttribute(AbstractAttribute):
|
|
'''Secondary color attribute.'''
|
|
|
|
plural = 'secondary_colors'
|
|
_fixed_count = 3
|
|
|
|
def __init__(self, gl_type):
|
|
super(SecondaryColorAttribute, self).__init__(3, gl_type)
|
|
|
|
def enable(self):
|
|
glEnableClientState(GL_SECONDARY_COLOR_ARRAY)
|
|
|
|
def set_pointer(self, pointer):
|
|
glSecondaryColorPointer(3, self.gl_type, self.stride,
|
|
self.offset + pointer)
|
|
|
|
class TexCoordAttribute(AbstractAttribute):
|
|
'''Texture coordinate attribute.'''
|
|
|
|
plural = 'tex_coords'
|
|
|
|
def __init__(self, count, gl_type):
|
|
assert gl_type in (GL_SHORT, GL_INT, GL_INT, GL_FLOAT, GL_DOUBLE), \
|
|
'Texture coord attribute must have non-byte signed type'
|
|
super(TexCoordAttribute, self).__init__(count, gl_type)
|
|
|
|
def enable(self):
|
|
glEnableClientState(GL_TEXTURE_COORD_ARRAY)
|
|
|
|
def set_pointer(self, pointer):
|
|
glTexCoordPointer(self.count, self.gl_type, self.stride,
|
|
self.offset + pointer)
|
|
|
|
class VertexAttribute(AbstractAttribute):
|
|
'''Vertex coordinate attribute.'''
|
|
|
|
plural = 'vertices'
|
|
|
|
def __init__(self, count, gl_type):
|
|
assert count > 1, \
|
|
'Vertex attribute must have count of 2, 3 or 4'
|
|
assert gl_type in (GL_SHORT, GL_INT, GL_INT, GL_FLOAT, GL_DOUBLE), \
|
|
'Vertex attribute must have signed type larger than byte'
|
|
super(VertexAttribute, self).__init__(count, gl_type)
|
|
|
|
def enable(self):
|
|
glEnableClientState(GL_VERTEX_ARRAY)
|
|
|
|
def set_pointer(self, pointer):
|
|
glVertexPointer(self.count, self.gl_type, self.stride,
|
|
self.offset + pointer)
|
|
|
|
class GenericAttribute(AbstractAttribute):
|
|
'''Generic vertex attribute, used by shader programs.'''
|
|
|
|
def __init__(self, index, normalized, count, gl_type):
|
|
self.normalized = bool(normalized)
|
|
self.index = index
|
|
super(GenericAttribute, self).__init__(count, gl_type)
|
|
|
|
def enable(self):
|
|
glEnableVertexAttribArray(self.index)
|
|
|
|
def set_pointer(self, pointer):
|
|
glVertexAttribPointer(self.index, self.count, self.gl_type,
|
|
self.normalized, self.stride,
|
|
self.offset + pointer)
|
|
|
|
_attribute_classes = {
|
|
'c': ColorAttribute,
|
|
'e': EdgeFlagAttribute,
|
|
'f': FogCoordAttribute,
|
|
'n': NormalAttribute,
|
|
's': SecondaryColorAttribute,
|
|
't': TexCoordAttribute,
|
|
'v': VertexAttribute,
|
|
}
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# pyglet
|
|
# Copyright (c) 2006-2008 Alex Holkner
|
|
# All rights reserved.
|
|
#
|
|
# Redistribution and use in source and binary forms, with or without
|
|
# modification, are permitted provided that the following conditions
|
|
# are met:
|
|
#
|
|
# * Redistributions of source code must retain the above copyright
|
|
# notice, this list of conditions and the following disclaimer.
|
|
# * Redistributions in binary form must reproduce the above copyright
|
|
# notice, this list of conditions and the following disclaimer in
|
|
# the documentation and/or other materials provided with the
|
|
# distribution.
|
|
# * Neither the name of pyglet nor the names of its
|
|
# contributors may be used to endorse or promote products
|
|
# derived from this software without specific prior written
|
|
# permission.
|
|
#
|
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
|
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
|
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
|
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
|
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
|
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
|
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
# POSSIBILITY OF SUCH DAMAGE.
|
|
# ----------------------------------------------------------------------------
|
|
# $Id:$
|
|
|
|
'''Byte abstractions of Vertex Buffer Objects and vertex arrays.
|
|
|
|
Use `create_buffer` or `create_mappable_buffer` to create a Vertex Buffer
|
|
Object, or a vertex array if VBOs are not supported by the current context.
|
|
|
|
Buffers can optionally be created "mappable" (incorporating the
|
|
`AbstractMappable` mix-in). In this case the buffer provides a ``get_region``
|
|
method which provides the most efficient path for updating partial data within
|
|
the buffer.
|
|
'''
|
|
|
|
__docformat__ = 'restructuredtext'
|
|
__version__ = '$Id: $'
|
|
|
|
_enable_vbo = pyglet.options['graphics_vbo']
|
|
|
|
# Enable workaround permanently if any VBO is created on a context that has
|
|
# this workaround. (On systems with multiple contexts where one is
|
|
# unaffected, the workaround will be enabled unconditionally on all of the
|
|
# contexts anyway. This is completely unlikely anyway).
|
|
_workaround_vbo_finish = False
|
|
|
|
def create_buffer(size,
|
|
target=GL_ARRAY_BUFFER,
|
|
usage=GL_DYNAMIC_DRAW,
|
|
vbo=True):
|
|
'''Create a buffer of vertex data.
|
|
|
|
:Parameters:
|
|
`size` : int
|
|
Size of the buffer, in bytes
|
|
`target` : int
|
|
OpenGL target buffer
|
|
`usage` : int
|
|
OpenGL usage constant
|
|
`vbo` : bool
|
|
True if a `VertexBufferObject` should be created if the driver
|
|
supports it; otherwise only a `VertexArray` is created.
|
|
|
|
:rtype: `AbstractBuffer`
|
|
'''
|
|
from pyglet import gl
|
|
if (vbo and
|
|
gl_info.have_version(1, 5) and
|
|
_enable_vbo and
|
|
not gl.current_context._workaround_vbo):
|
|
return VertexBufferObject(size, target, usage)
|
|
else:
|
|
return VertexArray(size)
|
|
|
|
def create_mappable_buffer(size,
|
|
target=GL_ARRAY_BUFFER,
|
|
usage=GL_DYNAMIC_DRAW,
|
|
vbo=True):
|
|
'''Create a mappable buffer of vertex data.
|
|
|
|
:Parameters:
|
|
`size` : int
|
|
Size of the buffer, in bytes
|
|
`target` : int
|
|
OpenGL target buffer
|
|
`usage` : int
|
|
OpenGL usage constant
|
|
`vbo` : bool
|
|
True if a `VertexBufferObject` should be created if the driver
|
|
supports it; otherwise only a `VertexArray` is created.
|
|
|
|
:rtype: `AbstractBuffer` with `AbstractMappable`
|
|
'''
|
|
from pyglet import gl
|
|
if (vbo and
|
|
gl_info.have_version(1, 5) and
|
|
_enable_vbo and
|
|
not gl.current_context._workaround_vbo):
|
|
return MappableVertexBufferObject(size, target, usage)
|
|
else:
|
|
return VertexArray(size)
|
|
|
|
class AbstractBuffer(object):
|
|
'''Abstract buffer of byte data.
|
|
|
|
:Ivariables:
|
|
`size` : int
|
|
Size of buffer, in bytes
|
|
`ptr` : int
|
|
Memory offset of the buffer, as used by the ``glVertexPointer``
|
|
family of functions
|
|
`target` : int
|
|
OpenGL buffer target, for example ``GL_ARRAY_BUFFER``
|
|
`usage` : int
|
|
OpenGL buffer usage, for example ``GL_DYNAMIC_DRAW``
|
|
|
|
'''
|
|
|
|
ptr = 0
|
|
size = 0
|
|
|
|
def bind(self):
|
|
'''Bind this buffer to its OpenGL target.'''
|
|
raise NotImplementedError('abstract')
|
|
|
|
def unbind(self):
|
|
'''Reset the buffer's OpenGL target.'''
|
|
raise NotImplementedError('abstract')
|
|
|
|
def set_data(self, data):
|
|
'''Set the entire contents of the buffer.
|
|
|
|
:Parameters:
|
|
`data` : sequence of int or ctypes pointer
|
|
The byte array to set.
|
|
|
|
'''
|
|
raise NotImplementedError('abstract')
|
|
|
|
def set_data_region(self, data, start, length):
|
|
'''Set part of the buffer contents.
|
|
|
|
:Parameters:
|
|
`data` : sequence of int or ctypes pointer
|
|
The byte array of data to set
|
|
`start` : int
|
|
Offset to start replacing data
|
|
`length` : int
|
|
Length of region to replace
|
|
|
|
'''
|
|
raise NotImplementedError('abstract')
|
|
|
|
def map(self, invalidate=False):
|
|
'''Map the entire buffer into system memory.
|
|
|
|
The mapped region must be subsequently unmapped with `unmap` before
|
|
performing any other operations on the buffer.
|
|
|
|
:Parameters:
|
|
`invalidate` : bool
|
|
If True, the initial contents of the mapped block need not
|
|
reflect the actual contents of the buffer.
|
|
|
|
:rtype: ``POINTER(ctypes.c_ubyte)``
|
|
:return: Pointer to the mapped block in memory
|
|
'''
|
|
raise NotImplementedError('abstract')
|
|
|
|
def unmap(self):
|
|
'''Unmap a previously mapped memory block.'''
|
|
raise NotImplementedError('abstract')
|
|
|
|
def resize(self, size):
|
|
'''Resize the buffer to a new size.
|
|
|
|
:Parameters:
|
|
`size` : int
|
|
New size of the buffer, in bytes
|
|
|
|
'''
|
|
|
|
def delete(self):
|
|
'''Delete this buffer, reducing system resource usage.'''
|
|
raise NotImplementedError('abstract')
|
|
|
|
class AbstractMappable(object):
|
|
def get_region(self, start, size, ptr_type):
|
|
'''Map a region of the buffer into a ctypes array of the desired
|
|
type. This region does not need to be unmapped, but will become
|
|
invalid if the buffer is resized.
|
|
|
|
Note that although a pointer type is required, an array is mapped.
|
|
For example::
|
|
|
|
get_region(0, ctypes.sizeof(c_int) * 20, ctypes.POINTER(c_int * 20))
|
|
|
|
will map bytes 0 to 80 of the buffer to an array of 20 ints.
|
|
|
|
Changes to the array may not be recognised until the region's
|
|
`AbstractBufferRegion.invalidate` method is called.
|
|
|
|
:Parameters:
|
|
`start` : int
|
|
Offset into the buffer to map from, in bytes
|
|
`size` : int
|
|
Size of the buffer region to map, in bytes
|
|
`ptr_type` : ctypes pointer type
|
|
Pointer type describing the array format to create
|
|
|
|
:rtype: `AbstractBufferRegion`
|
|
'''
|
|
raise NotImplementedError('abstract')
|
|
|
|
class VertexArray(AbstractBuffer, AbstractMappable):
|
|
'''A ctypes implementation of a vertex array.
|
|
|
|
Many of the methods on this class are effectively no-op's, such as `bind`,
|
|
`unbind`, `map`, `unmap` and `delete`; they exist in order to present
|
|
a consistent interface with `VertexBufferObject`.
|
|
|
|
This buffer type is also mappable, and so `get_region` can be used.
|
|
'''
|
|
|
|
def __init__(self, size):
|
|
self.size = size
|
|
|
|
self.array = (ctypes.c_byte * size)()
|
|
self.ptr = ctypes.cast(self.array, ctypes.c_void_p).value
|
|
|
|
def bind(self):
|
|
pass
|
|
|
|
def unbind(self):
|
|
pass
|
|
|
|
def set_data(self, data):
|
|
ctypes.memmove(self.ptr, data, self.size)
|
|
|
|
def set_data_region(self, data, start, length):
|
|
ctypes.memmove(self.ptr + start, data, length)
|
|
|
|
def map(self, invalidate=False):
|
|
return self.array
|
|
|
|
def unmap(self):
|
|
pass
|
|
|
|
def get_region(self, start, size, ptr_type):
|
|
array = ctypes.cast(self.ptr + start, ptr_type).contents
|
|
return VertexArrayRegion(array)
|
|
|
|
def delete(self):
|
|
pass
|
|
|
|
def resize(self, size):
|
|
array = (ctypes.c_byte * size)()
|
|
ctypes.memmove(array, self.array, min(size, self.size))
|
|
self.size = size
|
|
self.array = array
|
|
self.ptr = ctypes.cast(self.array, ctypes.c_void_p).value
|
|
|
|
|
|
class VertexBufferObject(AbstractBuffer):
|
|
'''Lightweight representation of an OpenGL VBO.
|
|
|
|
The data in the buffer is not replicated in any system memory (unless it
|
|
is done so by the video driver). While this can improve memory usage and
|
|
possibly performance, updates to the buffer are relatively slow.
|
|
|
|
This class does not implement `AbstractMappable`, and so has no
|
|
``get_region`` method. See `MappableVertexBufferObject` for a VBO class
|
|
that does implement ``get_region``.
|
|
'''
|
|
|
|
def __init__(self, size, target, usage):
|
|
self.size = size
|
|
self.target = target
|
|
self.usage = usage
|
|
self._context = pyglet.gl.current_context
|
|
|
|
id = GLuint()
|
|
glGenBuffers(1, id)
|
|
self.id = id.value
|
|
glPushClientAttrib(GL_CLIENT_VERTEX_ARRAY_BIT)
|
|
glBindBuffer(target, self.id)
|
|
glBufferData(target, self.size, None, self.usage)
|
|
glPopClientAttrib()
|
|
|
|
global _workaround_vbo_finish
|
|
if pyglet.gl.current_context._workaround_vbo_finish:
|
|
_workaround_vbo_finish = True
|
|
|
|
def bind(self):
|
|
glBindBuffer(self.target, self.id)
|
|
|
|
def unbind(self):
|
|
glBindBuffer(self.target, 0)
|
|
|
|
def set_data(self, data):
|
|
glPushClientAttrib(GL_CLIENT_VERTEX_ARRAY_BIT)
|
|
glBindBuffer(self.target, self.id)
|
|
glBufferData(self.target, self.size, data, self.usage)
|
|
glPopClientAttrib()
|
|
|
|
def set_data_region(self, data, start, length):
|
|
glPushClientAttrib(GL_CLIENT_VERTEX_ARRAY_BIT)
|
|
glBindBuffer(self.target, self.id)
|
|
glBufferSubData(self.target, start, length, data)
|
|
glPopClientAttrib()
|
|
|
|
def map(self, invalidate=False):
|
|
glPushClientAttrib(GL_CLIENT_VERTEX_ARRAY_BIT)
|
|
glBindBuffer(self.target, self.id)
|
|
if invalidate:
|
|
glBufferData(self.target, self.size, None, self.usage)
|
|
ptr = ctypes.cast(glMapBuffer(self.target, GL_WRITE_ONLY),
|
|
ctypes.POINTER(ctypes.c_byte * self.size)).contents
|
|
glPopClientAttrib()
|
|
return ptr
|
|
|
|
def unmap(self):
|
|
glPushClientAttrib(GL_CLIENT_VERTEX_ARRAY_BIT)
|
|
glUnmapBuffer(self.target)
|
|
glPopClientAttrib()
|
|
|
|
def __del__(self):
|
|
try:
|
|
if self.id is not None:
|
|
self._context.delete_buffer(self.id)
|
|
except:
|
|
pass
|
|
|
|
def delete(self):
|
|
id = GLuint(self.id)
|
|
glDeleteBuffers(1, id)
|
|
self.id = None
|
|
|
|
def resize(self, size):
|
|
# Map, create a copy, then reinitialize.
|
|
temp = (ctypes.c_byte * size)()
|
|
|
|
glPushClientAttrib(GL_CLIENT_VERTEX_ARRAY_BIT)
|
|
glBindBuffer(self.target, self.id)
|
|
data = glMapBuffer(self.target, GL_READ_ONLY)
|
|
ctypes.memmove(temp, data, min(size, self.size))
|
|
glUnmapBuffer(self.target)
|
|
|
|
self.size = size
|
|
glBufferData(self.target, self.size, temp, self.usage)
|
|
glPopClientAttrib()
|
|
|
|
class MappableVertexBufferObject(VertexBufferObject, AbstractMappable):
|
|
'''A VBO with system-memory backed store.
|
|
|
|
Updates to the data via `set_data`, `set_data_region` and `map` will be
|
|
held in local memory until `bind` is called. The advantage is that fewer
|
|
OpenGL calls are needed, increasing performance.
|
|
|
|
There may also be less performance penalty for resizing this buffer.
|
|
|
|
Updates to data via `map` are committed immediately.
|
|
'''
|
|
def __init__(self, size, target, usage):
|
|
super(MappableVertexBufferObject, self).__init__(size, target, usage)
|
|
self.data = (ctypes.c_byte * size)()
|
|
self.data_ptr = ctypes.cast(self.data, ctypes.c_void_p).value
|
|
self._dirty_min = sys.maxint
|
|
self._dirty_max = 0
|
|
|
|
def bind(self):
|
|
# Commit pending data
|
|
super(MappableVertexBufferObject, self).bind()
|
|
size = self._dirty_max - self._dirty_min
|
|
if size > 0:
|
|
if size == self.size:
|
|
glBufferData(self.target, self.size, self.data, self.usage)
|
|
else:
|
|
glBufferSubData(self.target, self._dirty_min, size,
|
|
self.data_ptr + self._dirty_min)
|
|
self._dirty_min = sys.maxint
|
|
self._dirty_max = 0
|
|
|
|
def set_data(self, data):
|
|
super(MappableVertexBufferObject, self).set_data(data)
|
|
ctypes.memmove(self.data, data, self.size)
|
|
self._dirty_min = 0
|
|
self._dirty_max = self.size
|
|
|
|
def set_data_region(self, data, start, length):
|
|
ctypes.memmove(self.data_ptr + start, data, length)
|
|
self._dirty_min = min(start, self._dirty_min)
|
|
self._dirty_max = max(start + length, self._dirty_max)
|
|
|
|
def map(self, invalidate=False):
|
|
self._dirty_min = 0
|
|
self._dirty_max = self.size
|
|
return self.data
|
|
|
|
def unmap(self):
|
|
pass
|
|
|
|
def get_region(self, start, size, ptr_type):
|
|
array = ctypes.cast(self.data_ptr + start, ptr_type).contents
|
|
return VertexBufferObjectRegion(self, start, start + size, array)
|
|
|
|
def resize(self, size):
|
|
data = (ctypes.c_byte * size)()
|
|
ctypes.memmove(data, self.data, min(size, self.size))
|
|
self.data = data
|
|
self.data_ptr = ctypes.cast(self.data, ctypes.c_void_p).value
|
|
|
|
self.size = size
|
|
glPushClientAttrib(GL_CLIENT_VERTEX_ARRAY_BIT)
|
|
glBindBuffer(self.target, self.id)
|
|
glBufferData(self.target, self.size, self.data, self.usage)
|
|
glPopClientAttrib()
|
|
|
|
self._dirty_min = sys.maxint
|
|
self._dirty_max = 0
|
|
|
|
class AbstractBufferRegion(object):
|
|
'''A mapped region of a buffer.
|
|
|
|
Buffer regions are obtained using `AbstractMappable.get_region`.
|
|
|
|
:Ivariables:
|
|
`array` : ctypes array
|
|
Array of data, of the type and count requested by ``get_region``.
|
|
|
|
'''
|
|
def invalidate(self):
|
|
'''Mark this region as changed.
|
|
|
|
The buffer may not be updated with the latest contents of the
|
|
array until this method is called. (However, it may not be updated
|
|
until the next time the buffer is used, for efficiency).
|
|
'''
|
|
pass
|
|
|
|
class VertexBufferObjectRegion(AbstractBufferRegion):
|
|
'''A mapped region of a VBO.'''
|
|
def __init__(self, buffer, start, end, array):
|
|
self.buffer = buffer
|
|
self.start = start
|
|
self.end = end
|
|
self.array = array
|
|
|
|
def invalidate(self):
|
|
buffer = self.buffer
|
|
buffer._dirty_min = min(buffer._dirty_min, self.start)
|
|
buffer._dirty_max = max(buffer._dirty_max, self.end)
|
|
|
|
class VertexArrayRegion(AbstractBufferRegion):
|
|
'''A mapped region of a vertex array.
|
|
|
|
The `invalidate` method is a no-op but is provided in order to present
|
|
a consistent interface with `VertexBufferObjectRegion`.
|
|
'''
|
|
def __init__(self, array):
|
|
self.array = array
|
|
|
|
class IndirectArrayRegion(AbstractBufferRegion):
|
|
'''A mapped region in which data elements are not necessarily contiguous.
|
|
|
|
This region class is used to wrap buffer regions in which the data
|
|
must be accessed with some stride. For example, in an interleaved buffer
|
|
this region can be used to access a single interleaved component as if the
|
|
data was contiguous.
|
|
'''
|
|
def __init__(self, region, size, component_count, component_stride):
|
|
'''Wrap a buffer region.
|
|
|
|
Use the `component_count` and `component_stride` parameters to specify
|
|
the data layout of the encapsulated region. For example, if RGBA
|
|
data is to be accessed as if it were packed RGB, ``component_count``
|
|
would be set to 3 and ``component_stride`` to 4. If the region
|
|
contains 10 RGBA tuples, the ``size`` parameter is ``3 * 10 = 30``.
|
|
|
|
:Parameters:
|
|
`region` : `AbstractBufferRegion`
|
|
The region with interleaved data
|
|
`size` : int
|
|
The number of elements that this region will provide access to.
|
|
`component_count` : int
|
|
The number of elements that are contiguous before some must
|
|
be skipped.
|
|
`component_stride` : int
|
|
The number of elements of interleaved data separating
|
|
the contiguous sections.
|
|
|
|
'''
|
|
self.region = region
|
|
self.size = size
|
|
self.count = component_count
|
|
self.stride = component_stride
|
|
self.array = self
|
|
|
|
def __repr__(self):
|
|
return 'IndirectArrayRegion(size=%d, count=%d, stride=%d)' % (
|
|
self.size, self.count, self.stride)
|
|
|
|
def __getitem__(self, index):
|
|
count = self.count
|
|
if not isinstance(index, slice):
|
|
elem = index // count
|
|
j = index % count
|
|
return self.region.array[elem * self.stride + j]
|
|
|
|
start = index.start or 0
|
|
stop = index.stop
|
|
step = index.step or 1
|
|
if start < 0:
|
|
start = self.size + start
|
|
if stop is None:
|
|
stop = self.size
|
|
elif stop < 0:
|
|
stop = self.size + stop
|
|
|
|
assert step == 1 or step % count == 0, \
|
|
'Step must be multiple of component count'
|
|
|
|
data_start = (start // count) * self.stride + start % count
|
|
data_stop = (stop // count) * self.stride + stop % count
|
|
data_step = step * self.stride
|
|
|
|
# TODO stepped getitem is probably wrong, see setitem for correct.
|
|
value_step = step * count
|
|
|
|
# ctypes does not support stepped slicing, so do the work in a list
|
|
# and copy it back.
|
|
data = self.region.array[:]
|
|
value = [0] * ((stop - start) // step)
|
|
stride = self.stride
|
|
for i in range(count):
|
|
value[i::value_step] = \
|
|
data[data_start + i:data_stop + i:data_step]
|
|
return value
|
|
|
|
def __setitem__(self, index, value):
|
|
count = self.count
|
|
if not isinstance(index, slice):
|
|
elem = index // count
|
|
j = index % count
|
|
self.region.array[elem * self.stride + j] = value
|
|
return
|
|
|
|
start = index.start or 0
|
|
stop = index.stop
|
|
step = index.step or 1
|
|
if start < 0:
|
|
start = self.size + start
|
|
if stop is None:
|
|
stop = self.size
|
|
elif stop < 0:
|
|
stop = self.size + stop
|
|
|
|
assert step == 1 or step % count == 0, \
|
|
'Step must be multiple of component count'
|
|
|
|
data_start = (start // count) * self.stride + start % count
|
|
data_stop = (stop // count) * self.stride + stop % count
|
|
|
|
# ctypes does not support stepped slicing, so do the work in a list
|
|
# and copy it back.
|
|
data = self.region.array[:]
|
|
if step == 1:
|
|
data_step = self.stride
|
|
value_step = count
|
|
for i in range(count):
|
|
data[data_start + i:data_stop + i:data_step] = \
|
|
value[i::value_step]
|
|
else:
|
|
data_step = (step // count) * self.stride
|
|
data[data_start:data_stop:data_step] = value
|
|
self.region.array[:] = data
|
|
|
|
def invalidate(self):
|
|
self.region.invalidate()
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# pyglet
|
|
# Copyright (c) 2006-2008 Alex Holkner
|
|
# All rights reserved.
|
|
#
|
|
# Redistribution and use in source and binary forms, with or without
|
|
# modification, are permitted provided that the following conditions
|
|
# are met:
|
|
#
|
|
# * Redistributions of source code must retain the above copyright
|
|
# notice, this list of conditions and the following disclaimer.
|
|
# * Redistributions in binary form must reproduce the above copyright
|
|
# notice, this list of conditions and the following disclaimer in
|
|
# the documentation and/or other materials provided with the
|
|
# distribution.
|
|
# * Neither the name of pyglet nor the names of its
|
|
# contributors may be used to endorse or promote products
|
|
# derived from this software without specific prior written
|
|
# permission.
|
|
#
|
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
|
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
|
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
|
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
|
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
|
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
|
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
# POSSIBILITY OF SUCH DAMAGE.
|
|
# ----------------------------------------------------------------------------
|
|
# $Id:$
|
|
|
|
'''Manage related vertex attributes within a single vertex domain.
|
|
|
|
A vertex "domain" consists of a set of attribute descriptions that together
|
|
describe the layout of one or more vertex buffers which are used together to
|
|
specify the vertices in a primitive. Additionally, the domain manages the
|
|
buffers used to store the data and will resize them as necessary to accomodate
|
|
new vertices.
|
|
|
|
Domains can optionally be indexed, in which case they also manage a buffer
|
|
containing vertex indices. This buffer is grown separately and has no size
|
|
relation to the attribute buffers.
|
|
|
|
Applications can create vertices (and optionally, indices) within a domain
|
|
with the `VertexDomain.create` method. This returns a `VertexList`
|
|
representing the list of vertices created. The vertex attribute data within
|
|
the group can be modified, and the changes will be made to the underlying
|
|
buffers automatically.
|
|
|
|
The entire domain can be efficiently drawn in one step with the
|
|
`VertexDomain.draw` method, assuming all the vertices comprise primitives of
|
|
the same OpenGL primitive mode.
|
|
'''
|
|
|
|
__docformat__ = 'restructuredtext'
|
|
__version__ = '$Id: $'
|
|
|
|
|
|
_usage_format_re = re.compile(r'''
|
|
(?P<attribute>[^/]*)
|
|
(/ (?P<usage> static|dynamic|stream|none))?
|
|
''', re.VERBOSE)
|
|
|
|
_gl_usages = {
|
|
'static': GL_STATIC_DRAW,
|
|
'dynamic': GL_DYNAMIC_DRAW,
|
|
'stream': GL_STREAM_DRAW,
|
|
'none': GL_STREAM_DRAW_ARB, # Force no VBO
|
|
}
|
|
|
|
def _nearest_pow2(v):
|
|
# From http://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2
|
|
# Credit: Sean Anderson
|
|
v -= 1
|
|
v |= v >> 1
|
|
v |= v >> 2
|
|
v |= v >> 4
|
|
v |= v >> 8
|
|
v |= v >> 16
|
|
return v + 1
|
|
|
|
def create_attribute_usage(format):
|
|
'''Create an attribute and usage pair from a format string. The
|
|
format string is as documented in `pyglet.graphics.vertexattribute`, with
|
|
the addition of an optional usage component::
|
|
|
|
usage ::= attribute ( '/' ('static' | 'dynamic' | 'stream' | 'none') )?
|
|
|
|
If the usage is not given it defaults to 'dynamic'. The usage corresponds
|
|
to the OpenGL VBO usage hint, and for ``static`` also indicates a
|
|
preference for interleaved arrays. If ``none`` is specified a buffer
|
|
object is not created, and vertex data is stored in system memory.
|
|
|
|
Some examples:
|
|
|
|
``v3f/stream``
|
|
3D vertex position using floats, for stream usage
|
|
``c4b/static``
|
|
4-byte color attribute, for static usage
|
|
|
|
:return: attribute, usage
|
|
'''
|
|
match = _usage_format_re.match(format)
|
|
attribute_format = match.group('attribute')
|
|
attribute = create_attribute(attribute_format)
|
|
usage = match.group('usage')
|
|
if usage:
|
|
vbo = not usage == 'none'
|
|
usage = _gl_usages[usage]
|
|
else:
|
|
usage = GL_DYNAMIC_DRAW
|
|
vbo = True
|
|
|
|
return (attribute, usage, vbo)
|
|
|
|
def create_domain(*attribute_usage_formats):
|
|
'''Create a vertex domain covering the given attribute usage formats.
|
|
See documentation for `create_attribute_usage` and
|
|
`pyglet.graphics.vertexattribute.create_attribute` for the grammar of
|
|
these format strings.
|
|
|
|
:rtype: `VertexDomain`
|
|
'''
|
|
attribute_usages = [create_attribute_usage(f) \
|
|
for f in attribute_usage_formats]
|
|
return VertexDomain(attribute_usages)
|
|
|
|
def create_indexed_domain(*attribute_usage_formats):
|
|
'''Create an indexed vertex domain covering the given attribute usage
|
|
formats. See documentation for `create_attribute_usage` and
|
|
`pyglet.graphics.vertexattribute.create_attribute` for the grammar of
|
|
these format strings.
|
|
|
|
:rtype: `VertexDomain`
|
|
'''
|
|
attribute_usages = [create_attribute_usage(f) \
|
|
for f in attribute_usage_formats]
|
|
return IndexedVertexDomain(attribute_usages)
|
|
|
|
class VertexDomain(object):
|
|
'''Management of a set of vertex lists.
|
|
|
|
Construction of a vertex domain is usually done with the `create_domain`
|
|
function.
|
|
'''
|
|
_version = 0
|
|
_initial_count = 16
|
|
|
|
def __init__(self, attribute_usages):
|
|
self.allocator = Allocator(self._initial_count)
|
|
|
|
static_attributes = []
|
|
attributes = []
|
|
self.buffer_attributes = [] # list of (buffer, attributes)
|
|
for attribute, usage, vbo in attribute_usages:
|
|
if usage == GL_STATIC_DRAW:
|
|
# Group attributes for interleaved buffer
|
|
static_attributes.append(attribute)
|
|
attributes.append(attribute)
|
|
else:
|
|
# Create non-interleaved buffer
|
|
attributes.append(attribute)
|
|
attribute.buffer = create_mappable_buffer(
|
|
attribute.stride * self.allocator.capacity,
|
|
usage=usage, vbo=vbo)
|
|
attribute.buffer.element_size = attribute.stride
|
|
attribute.buffer.attributes = (attribute,)
|
|
self.buffer_attributes.append(
|
|
(attribute.buffer, (attribute,)))
|
|
|
|
# Create buffer for interleaved data
|
|
if static_attributes:
|
|
vertexattribute.interleave_attributes(static_attributes)
|
|
stride = static_attributes[0].stride
|
|
buffer = vertexbuffer.create_mappable_buffer(
|
|
stride * self.allocator.capacity, usage=GL_STATIC_DRAW)
|
|
buffer.element_size = stride
|
|
self.buffer_attributes.append(
|
|
(buffer, static_attributes))
|
|
|
|
for attribute in static_attributes:
|
|
attribute.buffer = buffer
|
|
|
|
# Create named attributes for each attribute
|
|
self.attributes = attributes
|
|
self.attribute_names = {}
|
|
for attribute in attributes:
|
|
if isinstance(attribute, GenericAttribute):
|
|
index = attribute.index
|
|
if 'generic' not in self.attributes:
|
|
self.attribute_names['generic'] = {}
|
|
assert index not in self.attribute_names['generic'], \
|
|
'More than one generic attribute with index %d' % index
|
|
self.attribute_names['generic'][index] = attribute
|
|
else:
|
|
name = attribute.plural
|
|
assert name not in self.attributes, \
|
|
'More than one "%s" attribute given' % name
|
|
self.attribute_names[name] = attribute
|
|
|
|
def __del__(self):
|
|
# Break circular refs that Python GC seems to miss even when forced
|
|
# collection.
|
|
for attribute in self.attributes:
|
|
del attribute.buffer
|
|
|
|
def _safe_alloc(self, count):
|
|
'''Allocate vertices, resizing the buffers if necessary.'''
|
|
try:
|
|
return self.allocator.alloc(count)
|
|
except AllocatorMemoryException, e:
|
|
capacity = _nearest_pow2(e.requested_capacity)
|
|
self._version += 1
|
|
for buffer, _ in self.buffer_attributes:
|
|
buffer.resize(capacity * buffer.element_size)
|
|
self.allocator.set_capacity(capacity)
|
|
return self.allocator.alloc(count)
|
|
|
|
def _safe_realloc(self, start, count, new_count):
|
|
'''Reallocate vertices, resizing the buffers if necessary.'''
|
|
try:
|
|
return self.allocator.realloc(start, count, new_count)
|
|
except AllocatorMemoryException, e:
|
|
capacity = _nearest_pow2(e.requested_capacity)
|
|
self._version += 1
|
|
for buffer, _ in self.buffer_attributes:
|
|
buffer.resize(capacity * buffer.element_size)
|
|
self.allocator.set_capacity(capacity)
|
|
return self.allocator.realloc(start, count, new_count)
|
|
|
|
def create(self, count):
|
|
'''Create a `VertexList` in this domain.
|
|
|
|
:Parameters:
|
|
`count` : int
|
|
Number of vertices to create.
|
|
|
|
:rtype: `VertexList`
|
|
'''
|
|
start = self._safe_alloc(count)
|
|
return VertexList(self, start, count)
|
|
|
|
def draw(self, mode, vertex_list=None):
|
|
'''Draw vertices in the domain.
|
|
|
|
If `vertex_list` is not specified, all vertices in the domain are
|
|
drawn. This is the most efficient way to render primitives.
|
|
|
|
If `vertex_list` specifies a `VertexList`, only primitives in that
|
|
list will be drawn.
|
|
|
|
:Parameters:
|
|
`mode` : int
|
|
OpenGL drawing mode, e.g. ``GL_POINTS``, ``GL_LINES``, etc.
|
|
`vertex_list` : `VertexList`
|
|
Vertex list to draw, or ``None`` for all lists in this domain.
|
|
|
|
'''
|
|
glPushClientAttrib(GL_CLIENT_VERTEX_ARRAY_BIT)
|
|
for buffer, attributes in self.buffer_attributes:
|
|
buffer.bind()
|
|
for attribute in attributes:
|
|
attribute.enable()
|
|
attribute.set_pointer(attribute.buffer.ptr)
|
|
if _workaround_vbo_finish:
|
|
glFinish()
|
|
|
|
if vertex_list is not None:
|
|
glDrawArrays(mode, vertex_list.start, vertex_list.count)
|
|
else:
|
|
starts, sizes = self.allocator.get_allocated_regions()
|
|
primcount = len(starts)
|
|
if primcount == 0:
|
|
pass
|
|
elif primcount == 1:
|
|
# Common case
|
|
glDrawArrays(mode, starts[0], sizes[0])
|
|
elif gl_info.have_version(1, 4):
|
|
starts = (GLint * primcount)(*starts)
|
|
sizes = (GLsizei * primcount)(*sizes)
|
|
glMultiDrawArrays(mode, starts, sizes, primcount)
|
|
else:
|
|
for start, size in zip(starts, sizes):
|
|
glDrawArrays(mode, start, size)
|
|
|
|
for buffer, _ in self.buffer_attributes:
|
|
buffer.unbind()
|
|
glPopClientAttrib()
|
|
|
|
def _is_empty(self):
|
|
return not self.allocator.starts
|
|
|
|
def __repr__(self):
|
|
return '<%s@%x %s>' % (self.__class__.__name__, id(self),
|
|
self.allocator)
|
|
|
|
class VertexList(object):
|
|
'''A list of vertices within a `VertexDomain`. Use
|
|
`VertexDomain.create` to construct this list.
|
|
'''
|
|
|
|
def __init__(self, domain, start, count):
|
|
# TODO make private
|
|
self.domain = domain
|
|
self.start = start
|
|
self.count = count
|
|
|
|
def get_size(self):
|
|
'''Get the number of vertices in the list.
|
|
|
|
:rtype: int
|
|
'''
|
|
return self.count
|
|
|
|
def get_domain(self):
|
|
'''Get the domain this vertex list belongs to.
|
|
|
|
:rtype: `VertexDomain`
|
|
'''
|
|
return self.domain
|
|
|
|
def draw(self, mode):
|
|
'''Draw this vertex list in the given OpenGL mode.
|
|
|
|
:Parameters:
|
|
`mode` : int
|
|
OpenGL drawing mode, e.g. ``GL_POINTS``, ``GL_LINES``, etc.
|
|
|
|
'''
|
|
self.domain.draw(mode, self)
|
|
|
|
def resize(self, count):
|
|
'''Resize this group.
|
|
|
|
:Parameters:
|
|
`count` : int
|
|
New number of vertices in the list.
|
|
|
|
'''
|
|
new_start = self.domain._safe_realloc(self.start, self.count, count)
|
|
if new_start != self.start:
|
|
# Copy contents to new location
|
|
for attribute in self.domain.attributes:
|
|
old = attribute.get_region(attribute.buffer,
|
|
self.start, self.count)
|
|
new = attribute.get_region(attribute.buffer,
|
|
new_start, self.count)
|
|
new.array[:] = old.array[:]
|
|
new.invalidate()
|
|
self.start = new_start
|
|
self.count = count
|
|
|
|
self._colors_cache_version = None
|
|
self._fog_coords_cache_version = None
|
|
self._edge_flags_cache_version = None
|
|
self._normals_cache_version = None
|
|
self._secondary_colors_cache_version = None
|
|
self._tex_coords_cache_version = None
|
|
self._vertices_cache_version = None
|
|
|
|
def delete(self):
|
|
'''Delete this group.'''
|
|
self.domain.allocator.dealloc(self.start, self.count)
|
|
|
|
def migrate(self, domain):
|
|
'''Move this group from its current domain and add to the specified
|
|
one. Attributes on domains must match. (In practice, used to change
|
|
parent state of some vertices).
|
|
|
|
:Parameters:
|
|
`domain` : `VertexDomain`
|
|
Domain to migrate this vertex list to.
|
|
|
|
'''
|
|
assert domain.attribute_names.keys() == \
|
|
self.domain.attribute_names.keys(), 'Domain attributes must match.'
|
|
|
|
new_start = domain._safe_alloc(self.count)
|
|
for key, old_attribute in self.domain.attribute_names.items():
|
|
old = old_attribute.get_region(old_attribute.buffer,
|
|
self.start, self.count)
|
|
new_attribute = domain.attribute_names[key]
|
|
new = new_attribute.get_region(new_attribute.buffer,
|
|
new_start, self.count)
|
|
new.array[:] = old.array[:]
|
|
new.invalidate()
|
|
|
|
self.domain.allocator.dealloc(self.start, self.count)
|
|
self.domain = domain
|
|
self.start = new_start
|
|
|
|
self._colors_cache_version = None
|
|
self._fog_coords_cache_version = None
|
|
self._edge_flags_cache_version = None
|
|
self._normals_cache_version = None
|
|
self._secondary_colors_cache_version = None
|
|
self._tex_coords_cache_version = None
|
|
self._vertices_cache_version = None
|
|
|
|
def _set_attribute_data(self, i, data):
|
|
attribute = self.domain.attributes[i]
|
|
# TODO without region
|
|
region = attribute.get_region(attribute.buffer, self.start, self.count)
|
|
region.array[:] = data
|
|
region.invalidate()
|
|
|
|
# ---
|
|
|
|
def _get_colors(self):
|
|
if (self._colors_cache_version != self.domain._version):
|
|
domain = self.domain
|
|
attribute = domain.attribute_names['colors']
|
|
self._colors_cache = attribute.get_region(
|
|
attribute.buffer, self.start, self.count)
|
|
self._colors_cache_version = domain._version
|
|
|
|
region = self._colors_cache
|
|
region.invalidate()
|
|
return region.array
|
|
|
|
def _set_colors(self, data):
|
|
self._get_colors()[:] = data
|
|
|
|
_colors_cache = None
|
|
_colors_cache_version = None
|
|
colors = property(_get_colors, _set_colors,
|
|
doc='''Array of color data.''')
|
|
|
|
# ---
|
|
|
|
def _get_fog_coords(self):
|
|
if (self._fog_coords_cache_version != self.domain._version):
|
|
domain = self.domain
|
|
attribute = domain.attribute_names['fog_coords']
|
|
self._fog_coords_cache = attribute.get_region(
|
|
attribute.buffer, self.start, self.count)
|
|
self._fog_coords_cache_version = domain._version
|
|
|
|
region = self._fog_coords_cache
|
|
region.invalidate()
|
|
return region.array
|
|
|
|
def _set_fog_coords(self, data):
|
|
self._get_fog_coords()[:] = data
|
|
|
|
_fog_coords_cache = None
|
|
_fog_coords_cache_version = None
|
|
fog_coords = property(_get_fog_coords, _set_fog_coords,
|
|
doc='''Array of fog coordinate data.''')
|
|
|
|
# ---
|
|
|
|
def _get_edge_flags(self):
|
|
if (self._edge_flags_cache_version != self.domain._version):
|
|
domain = self.domain
|
|
attribute = domain.attribute_names['edge_flags']
|
|
self._edge_flags_cache = attribute.get_region(
|
|
attribute.buffer, self.start, self.count)
|
|
self._edge_flags_cache_version = domain._version
|
|
|
|
region = self._edge_flags_cache
|
|
region.invalidate()
|
|
return region.array
|
|
|
|
def _set_edge_flags(self, data):
|
|
self._get_edge_flags()[:] = data
|
|
|
|
_edge_flags_cache = None
|
|
_edge_flags_cache_version = None
|
|
edge_flags = property(_get_edge_flags, _set_edge_flags,
|
|
doc='''Array of edge flag data.''')
|
|
|
|
# ---
|
|
|
|
def _get_normals(self):
|
|
if (self._normals_cache_version != self.domain._version):
|
|
domain = self.domain
|
|
attribute = domain.attribute_names['normals']
|
|
self._normals_cache = attribute.get_region(
|
|
attribute.buffer, self.start, self.count)
|
|
self._normals_cache_version = domain._version
|
|
|
|
region = self._normals_cache
|
|
region.invalidate()
|
|
return region.array
|
|
|
|
def _set_normals(self, data):
|
|
self._get_normals()[:] = data
|
|
|
|
_normals_cache = None
|
|
_normals_cache_version = None
|
|
normals = property(_get_normals, _set_normals,
|
|
doc='''Array of normal vector data.''')
|
|
|
|
# ---
|
|
|
|
def _get_secondary_colors(self):
|
|
if (self._secondary_colors_cache_version != self.domain._version):
|
|
domain = self.domain
|
|
attribute = domain.attribute_names['secondary_colors']
|
|
self._secondary_colors_cache = attribute.get_region(
|
|
attribute.buffer, self.start, self.count)
|
|
self._secondary_colors_cache_version = domain._version
|
|
|
|
region = self._secondary_colors_cache
|
|
region.invalidate()
|
|
return region.array
|
|
|
|
def _set_secondary_colors(self, data):
|
|
self._get_secondary_colors()[:] = data
|
|
|
|
_secondary_colors_cache = None
|
|
_secondary_colors_cache_version = None
|
|
secondary_colors = property(_get_secondary_colors, _set_secondary_colors,
|
|
doc='''Array of secondary color data.''')
|
|
|
|
# ---
|
|
|
|
_tex_coords_cache = None
|
|
_tex_coords_cache_version = None
|
|
|
|
def _get_tex_coords(self):
|
|
if (self._tex_coords_cache_version != self.domain._version):
|
|
domain = self.domain
|
|
attribute = domain.attribute_names['tex_coords']
|
|
self._tex_coords_cache = attribute.get_region(
|
|
attribute.buffer, self.start, self.count)
|
|
self._tex_coords_cache_version = domain._version
|
|
|
|
region = self._tex_coords_cache
|
|
region.invalidate()
|
|
return region.array
|
|
|
|
def _set_tex_coords(self, data):
|
|
self._get_tex_coords()[:] = data
|
|
|
|
tex_coords = property(_get_tex_coords, _set_tex_coords,
|
|
doc='''Array of texture coordinate data.''')
|
|
|
|
# ---
|
|
|
|
_vertices_cache = None
|
|
_vertices_cache_version = None
|
|
|
|
def _get_vertices(self):
|
|
if (self._vertices_cache_version != self.domain._version):
|
|
domain = self.domain
|
|
attribute = domain.attribute_names['vertices']
|
|
self._vertices_cache = attribute.get_region(
|
|
attribute.buffer, self.start, self.count)
|
|
self._vertices_cache_version = domain._version
|
|
|
|
region = self._vertices_cache
|
|
region.invalidate()
|
|
return region.array
|
|
|
|
def _set_vertices(self, data):
|
|
self._get_vertices()[:] = data
|
|
|
|
vertices = property(_get_vertices, _set_vertices,
|
|
doc='''Array of vertex coordinate data.''')
|
|
|
|
class IndexedVertexDomain(VertexDomain):
|
|
'''Management of a set of indexed vertex lists.
|
|
|
|
Construction of an indexed vertex domain is usually done with the
|
|
`create_indexed_domain` function.
|
|
'''
|
|
_initial_index_count = 16
|
|
|
|
def __init__(self, attribute_usages, index_gl_type=GL_UNSIGNED_INT):
|
|
super(IndexedVertexDomain, self).__init__(attribute_usages)
|
|
|
|
self.index_allocator = allocation.Allocator(self._initial_index_count)
|
|
|
|
self.index_gl_type = index_gl_type
|
|
self.index_c_type = vertexattribute._c_types[index_gl_type]
|
|
self.index_element_size = ctypes.sizeof(self.index_c_type)
|
|
self.index_buffer = vertexbuffer.create_mappable_buffer(
|
|
self.index_allocator.capacity * self.index_element_size,
|
|
target=GL_ELEMENT_ARRAY_BUFFER)
|
|
|
|
def _safe_index_alloc(self, count):
|
|
'''Allocate indices, resizing the buffers if necessary.'''
|
|
try:
|
|
return self.index_allocator.alloc(count)
|
|
except allocation.AllocatorMemoryException, e:
|
|
capacity = _nearest_pow2(e.requested_capacity)
|
|
self._version += 1
|
|
self.index_buffer.resize(capacity * self.index_element_size)
|
|
self.index_allocator.set_capacity(capacity)
|
|
return self.index_allocator.alloc(count)
|
|
|
|
def _safe_index_realloc(self, start, count, new_count):
|
|
'''Reallocate indices, resizing the buffers if necessary.'''
|
|
try:
|
|
return self.index_allocator.realloc(start, count, new_count)
|
|
except allocation.AllocatorMemoryException, e:
|
|
capacity = _nearest_pow2(e.requested_capacity)
|
|
self._version += 1
|
|
self.index_buffer.resize(capacity * self.index_element_size)
|
|
self.index_allocator.set_capacity(capacity)
|
|
return self.index_allocator.realloc(start, count, new_count)
|
|
|
|
def create(self, count, index_count):
|
|
'''Create an `IndexedVertexList` in this domain.
|
|
|
|
:Parameters:
|
|
`count` : int
|
|
Number of vertices to create
|
|
`index_count`
|
|
Number of indices to create
|
|
|
|
'''
|
|
start = self._safe_alloc(count)
|
|
index_start = self._safe_index_alloc(index_count)
|
|
return IndexedVertexList(self, start, count, index_start, index_count)
|
|
|
|
def get_index_region(self, start, count):
|
|
'''Get a region of the index buffer.
|
|
|
|
:Parameters:
|
|
`start` : int
|
|
Start of the region to map.
|
|
`count` : int
|
|
Number of indices to map.
|
|
|
|
:rtype: Array of int
|
|
'''
|
|
byte_start = self.index_element_size * start
|
|
byte_count = self.index_element_size * count
|
|
ptr_type = ctypes.POINTER(self.index_c_type * count)
|
|
return self.index_buffer.get_region(byte_start, byte_count, ptr_type)
|
|
|
|
def draw(self, mode, vertex_list=None):
|
|
'''Draw vertices in the domain.
|
|
|
|
If `vertex_list` is not specified, all vertices in the domain are
|
|
drawn. This is the most efficient way to render primitives.
|
|
|
|
If `vertex_list` specifies a `VertexList`, only primitives in that
|
|
list will be drawn.
|
|
|
|
:Parameters:
|
|
`mode` : int
|
|
OpenGL drawing mode, e.g. ``GL_POINTS``, ``GL_LINES``, etc.
|
|
`vertex_list` : `IndexedVertexList`
|
|
Vertex list to draw, or ``None`` for all lists in this domain.
|
|
|
|
'''
|
|
glPushClientAttrib(GL_CLIENT_VERTEX_ARRAY_BIT)
|
|
for buffer, attributes in self.buffer_attributes:
|
|
buffer.bind()
|
|
for attribute in attributes:
|
|
attribute.enable()
|
|
attribute.set_pointer(attribute.buffer.ptr)
|
|
self.index_buffer.bind()
|
|
if vertexbuffer._workaround_vbo_finish:
|
|
glFinish()
|
|
|
|
if vertex_list is not None:
|
|
glDrawElements(mode, vertex_list.index_count, self.index_gl_type,
|
|
self.index_buffer.ptr +
|
|
vertex_list.index_start * self.index_element_size)
|
|
else:
|
|
starts, sizes = self.index_allocator.get_allocated_regions()
|
|
primcount = len(starts)
|
|
if primcount == 0:
|
|
pass
|
|
elif primcount == 1:
|
|
# Common case
|
|
glDrawElements(mode, sizes[0], self.index_gl_type,
|
|
self.index_buffer.ptr + starts[0])
|
|
elif gl_info.have_version(1, 4):
|
|
if not isinstance(self.index_buffer,
|
|
vertexbuffer.VertexBufferObject):
|
|
starts = [s + self.index_buffer.ptr for s in starts]
|
|
starts = (GLuint * primcount)(*starts)
|
|
sizes = (GLsizei * primcount)(*sizes)
|
|
glMultiDrawElements(mode, sizes, self.index_gl_type, starts,
|
|
primcount)
|
|
else:
|
|
for start, size in zip(starts, sizes):
|
|
glDrawElements(mode, size, self.index_gl_type,
|
|
self.index_buffer.ptr +
|
|
start * self.index_element_size)
|
|
|
|
self.index_buffer.unbind()
|
|
for buffer, _ in self.buffer_attributes:
|
|
buffer.unbind()
|
|
glPopClientAttrib()
|
|
|
|
class IndexedVertexList(VertexList):
|
|
'''A list of vertices within an `IndexedVertexDomain` that are indexed.
|
|
Use `IndexedVertexDomain.create` to construct this list.
|
|
'''
|
|
def __init__(self, domain, start, count, index_start, index_count):
|
|
super(IndexedVertexList, self).__init__(domain, start, count)
|
|
|
|
self.index_start = index_start
|
|
self.index_count = index_count
|
|
|
|
def draw(self, mode):
|
|
self.domain.draw(mode, self)
|
|
|
|
def resize(self, count, index_count):
|
|
'''Resize this group.
|
|
|
|
:Parameters:
|
|
`count` : int
|
|
New number of vertices in the list.
|
|
`index_count` : int
|
|
New number of indices in the list.
|
|
|
|
'''
|
|
old_start = self.start
|
|
super(IndexedVertexList, self).resize(count)
|
|
|
|
# Change indices (because vertices moved)
|
|
if old_start != self.start:
|
|
diff = self.start - old_start
|
|
self.indices[:] = map(lambda i: i + diff, self.indices)
|
|
|
|
# Resize indices
|
|
new_start = self.domain._safe_index_realloc(
|
|
self.index_start, self.index_count, index_count)
|
|
if new_start != self.index_start:
|
|
old = self.domain.get_index_region(
|
|
self.index_start, self.index_count)
|
|
new = self.domain.get_index_region(
|
|
self.index_start, self.index_count)
|
|
new.array[:] = old.array[:]
|
|
new.invalidate()
|
|
self.index_start = new_start
|
|
self.index_count = index_count
|
|
self._indices_cache_version = None
|
|
|
|
def delete(self):
|
|
'''Delete this group.'''
|
|
super(IndexedVertexList, self).delete()
|
|
self.domain.index_allocator.dealloc(self.index_start, self.index_count)
|
|
|
|
def _set_index_data(self, data):
|
|
# TODO without region
|
|
region = self.domain.get_index_region(
|
|
self.index_start, self.index_count)
|
|
region.array[:] = data
|
|
region.invalidate()
|
|
|
|
# ---
|
|
|
|
def _get_indices(self):
|
|
if self._indices_cache_version != self.domain._version:
|
|
domain = self.domain
|
|
self._indices_cache = domain.get_index_region(
|
|
self.index_start, self.index_count)
|
|
self._indices_cache_version = domain._version
|
|
|
|
region = self._indices_cache
|
|
region.invalidate()
|
|
return region.array
|
|
|
|
def _set_indices(self, data):
|
|
self._get_indices()[:] = data
|
|
|
|
_indices_cache = None
|
|
_indices_cache_version = None
|
|
indices = property(_get_indices, _set_indices,
|
|
doc='''Array of index data.''')
|
|
|