heroes-renaissance/pyglet/image/atlas.py

260 lines
8.9 KiB
Python
Raw Permalink Normal View History

2008-11-23 20:07:47 +00:00
# ----------------------------------------------------------------------------
# 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.
# ----------------------------------------------------------------------------
'''Group multiple small images into larger textures.
This module is used by `pyglet.resource` to efficiently pack small images into
larger textures. `TextureAtlas` maintains one texture; `TextureBin` manages a
collection of atlases of a given size.
Example usage::
# Load images from disk
car_image = pyglet.image.load('car.png')
boat_image = pyglet.image.load('boat.png')
# Pack these images into one or more textures
bin = TextureBin()
car_texture = bin.add(car_image)
boat_texture = bin.add(boat_image)
The result of `TextureBin.add` is a `TextureRegion` containing the image.
Once added, an image cannot be removed from a bin (or an atlas); nor can a
list of images be obtained from a given bin or atlas -- it is the
application's responsibility to keep track of the regions returned by the
``add`` methods.
:since: pyglet 1.1
'''
__docformat__ = 'restructuredtext'
__version__ = '$Id: $'
import pyglet
class AllocatorException(Exception):
'''The allocator does not have sufficient free space for the requested
image size.'''
pass
class _Strip(object):
def __init__(self, y, max_height):
self.x = 0
self.y = y
self.max_height = max_height
self.y2 = y
def add(self, width, height):
assert width > 0 and height > 0
assert height <= self.max_height
x, y = self.x, self.y
self.x += width
self.y2 = max(self.y + height, self.y2)
return x, y
def compact(self):
self.max_height = self.y2 - self.y
class Allocator(object):
'''Rectangular area allocation algorithm.
Initialise with a given ``width`` and ``height``, then repeatedly
call `alloc` to retrieve free regions of the area and protect that
area from future allocations.
`Allocator` uses a fairly simple strips-based algorithm. It performs best
when rectangles are allocated in decreasing height order.
'''
def __init__(self, width, height):
'''Create an `Allocator` of the given size.
:Parameters:
`width` : int
Width of the allocation region.
`height` : int
Height of the allocation region.
'''
assert width > 0 and height > 0
self.width = width
self.height = height
self.strips = [_Strip(0, height)]
self.used_area = 0
def alloc(self, width, height):
'''Get a free area in the allocator of the given size.
After calling `alloc`, the requested area will no longer be used.
If there is not enough room to fit the given area `AllocatorException`
is raised.
:Parameters:
`width` : int
Width of the area to allocate.
`height` : int
Height of the area to allocate.
:rtype: int, int
:return: The X and Y coordinates of the bottom-left corner of the
allocated region.
'''
for strip in self.strips:
if self.width - strip.x >= width and strip.max_height >= height:
self.used_area += width * height
return strip.add(width, height)
if self.width >= width and self.height - strip.y2 >= height:
self.used_area += width * height
strip.compact()
newstrip = _Strip(strip.y2, self.height - strip.y2)
self.strips.append(newstrip)
return newstrip.add(width, height)
raise AllocatorException('No more space in %r for box %dx%d' % (
self, width, height))
def get_usage(self):
'''Get the fraction of area already allocated.
This method is useful for debugging and profiling only.
:rtype: float
'''
return self.used_area / float(self.width * self.height)
def get_fragmentation(self):
'''Get the fraction of area that's unlikely to ever be used, based on
current allocation behaviour.
This method is useful for debugging and profiling only.
:rtype: float
'''
# The total unused area in each compacted strip is summed.
if not self.strips:
return 0.
possible_area = self.strips[-1].y2 * width
return 1.0 - self.used_area / float(possible_area)
class TextureAtlas(object):
'''Collection of images within a texture.
'''
def __init__(self, width=256, height=256):
'''Create a texture atlas of the given size.
:Parameters:
`width` : int
Width of the underlying texture.
`height` : int
Height of the underlying texture.
'''
self.texture = pyglet.image.Texture.create(
width, height, pyglet.gl.GL_RGBA, rectangle=True)
self.allocator = Allocator(width, height)
def add(self, img):
'''Add an image to the atlas.
This method will fail if the given image cannot be transferred
directly to a texture (for example, if it is another texture).
`ImageData` is the usual image type for this method.
`AllocatorException` will be raised if there is no room in the atlas
for the image.
:Parameters:
`img` : `AbstractImage`
The image to add.
:rtype: `TextureRegion`
:return: The region of the atlas containing the newly added image.
'''
x, y = self.allocator.alloc(img.width, img.height)
self.texture.blit_into(img, x, y, 0)
region = self.texture.get_region(x, y, img.width, img.height)
return region
class TextureBin(object):
'''Collection of texture atlases.
`TextureBin` maintains a collection of texture atlases, and creates new
ones as necessary to accomodate images added to the bin.
'''
def __init__(self, texture_width=256, texture_height=256):
'''Create a texture bin for holding atlases of the given size.
:Parameters:
`texture_width` : int
Width of texture atlases to create.
`texture_height` : int
Height of texture atlases to create.
'''
self.atlases = []
self.texture_width = texture_width
self.texture_height = texture_height
def add(self, img):
'''Add an image into this texture bin.
This method calls `TextureAtlas.add` for the first atlas that has room
for the image.
`AllocatorException` is raised if the image exceeds the dimensions of
``texture_width`` and ``texture_height``.
:Parameters:
`img` : `AbstractImage`
The image to add.
:rtype: `TextureRegion`
:return: The region of an atlas containing the newly added image.
'''
for atlas in list(self.atlases):
try:
return atlas.add(img)
except AllocatorException:
# Remove atlases that are no longer useful (this is so their
# textures can later be freed if the images inside them get
# collected).
if img.width < 64 and img.height < 64:
self.atlases.remove(atlas)
atlas = TextureAtlas(self.texture_width, self.texture_height)
self.atlases.append(atlas)
return atlas.add(img)